GitNexus/gitnexus/test/integration/analyze-wal-checkpoint-failure.test.ts
gitnexus-release-bot[bot] 47d63a0601
chore(vendor): update tree-sitter-dart to 1.0.0-g0038280 (#3633)
* chore(vendor): update tree-sitter-dart to 1.0.0-g0038280

* chore(vendor): rebuild native prebuilds (tree-sitter-dart)

Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/39212887661

* fix(dart): use native extension type parsing

The upgraded grammar parses extension types directly. The compatibility
rewrite loses generic extension bodies and implements clauses, so capture
the native nodes and register their member ownership instead.

Refresh the Dart capture fingerprint after comparing the old grammar and
emitter over the same fixture corpus. Keep the scaling budget unchanged.

Refs #3633

* chore(vendor): rebuild native prebuilds (tree-sitter-dart)

Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/39239767139

* fix: repair Dart parity and checkpoint regressions

Remove Dart from the preprocessing fixture inventory after retiring its
hook. Use the parsed body boundary when reading extension-type heritage
so named-record representations retain their implements clauses.

Seed the WAL recovery fixture with the normal checkpoint threshold, then
force one-byte checkpoints only for the deliberately blocked write.
Require an actual error exit instead of accepting a subprocess timeout.

Refs #3633

* chore(vendor): rebuild native prebuilds (tree-sitter-dart)

Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/39248441688

---------

Co-authored-by: gitnexus-release-bot[bot] <gitnexus-release-bot[bot]@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
2026-10-19 15:41:34 +01:00

157 lines
7.2 KiB
TypeScript

/**
* Integration test: WAL auto-checkpoint rename failure (#1741 / #1772).
*
* Drives the real `analyzeCommand` against a real LadybugDB instance and
* provokes a genuine Ladybug-engine `IO exception: Error renaming file
* <db>.wal to <db>.wal.checkpoint` by pre-planting a *directory* at the
* `<db>.wal.checkpoint` rename target. `fs.rename` (which Ladybug's native
* `LocalFileSystem` ultimately invokes) cannot overwrite a non-empty
* directory with a file on either POSIX or Windows, and Ladybug's
* `doInitLbug` orphan-cleanup uses `fs.unlink` which fails on a directory
* — so the blocker survives initialization and the next auto-checkpoint
* fires the natural rename failure that motivated PR #1772.
*
* No test-only hooks, no env-var fault toggles in production code: we use
* the same `GITNEXUS_WAL_CHECKPOINT_THRESHOLD=1` knob that real users have
* available to force checkpointing on every write, then arrange a real
* filesystem state that makes the rename impossible.
*
* Verifies that:
* 1. The CLI exits non-zero.
* 2. stderr contains the actionable recovery hint pointing at
* `--wal-checkpoint-threshold 67108864` (the
* `RECOMMENDED_WAL_CHECKPOINT_THRESHOLD` constant in `analyze.ts`).
* 3. The recovery message references the
* `GITNEXUS_WAL_CHECKPOINT_THRESHOLD` env var as a parallel route.
*
* Empirically confirmed portable on Windows; the same mechanism is
* expected to work on POSIX (`rename(2)` fails with `EISDIR`/`ENOTEMPTY`
* when the target is a non-empty directory). If a future Ladybug release
* changes the rename ordering, the loose match on the recovery hint
* (rather than the exact engine error wording) keeps this test stable.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { CLI_SPAWN_PREFIX } from '../helpers/cli-entry.js';
import { spawnSync } from 'child_process';
import path from 'path';
import fs from 'fs';
import os from 'os';
import { fileURLToPath } from 'url';
import { cleanupTempDirSync } from '../helpers/test-db.js';
const testDir = path.dirname(fileURLToPath(import.meta.url));
const FIXTURE_SRC = path.resolve(testDir, '..', 'fixtures', 'mini-repo');
let tmpParent: string;
let suiteGitnexusHome: string;
let repoPath: string;
beforeAll(() => {
tmpParent = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-wal-checkpoint-e2e-'));
suiteGitnexusHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-wal-checkpoint-home-'));
repoPath = path.join(tmpParent, 'mini-repo');
fs.cpSync(FIXTURE_SRC, repoPath, { recursive: true });
spawnSync('git', ['init'], { cwd: repoPath, stdio: 'pipe' });
spawnSync('git', ['add', '-A'], { cwd: repoPath, stdio: 'pipe' });
spawnSync('git', ['commit', '-m', 'initial commit'], {
cwd: repoPath,
stdio: 'pipe',
env: {
...process.env,
GIT_AUTHOR_NAME: 'test',
GIT_AUTHOR_EMAIL: 'test@test',
GIT_COMMITTER_NAME: 'test',
GIT_COMMITTER_EMAIL: 'test@test',
},
});
});
afterAll(() => {
if (tmpParent) cleanupTempDirSync(tmpParent);
if (suiteGitnexusHome) cleanupTempDirSync(suiteGitnexusHome);
});
const runAnalyze = (walCheckpointThreshold: string) =>
spawnSync(process.execPath, [...CLI_SPAWN_PREFIX, 'analyze', '--skip-skills'], {
cwd: repoPath,
encoding: 'utf8',
// Generous timeout: the test does real CSV/COPY work before the
// first failing checkpoint, and CI runners are slow.
timeout: process.env.CI ? 120_000 : 60_000,
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,
GITNEXUS_HOME: suiteGitnexusHome,
// Skip ensureHeap re-exec (which drops the tsx loader).
NODE_OPTIONS: `${process.env.NODE_OPTIONS || ''} --max-old-space-size=8192`.trim(),
GITNEXUS_WAL_CHECKPOINT_THRESHOLD: walCheckpointThreshold,
CI: '1',
},
});
describe('analyze WAL auto-checkpoint rename failure (real lbug, no mocks)', () => {
it('surfaces the --wal-checkpoint-threshold recovery hint when the rename target is blocked', () => {
// The checkpoint rename target must be a PREDICTABLE path so the blocker
// can be pre-planted. A full rebuild builds into a per-run
// `lbug.staging.<uuid>` and checkpoints `lbug.staging.<uuid>.wal.checkpoint`
// (#2658) — an unknowable name. An INCREMENTAL run instead writes the live
// index in place, so its auto-checkpoint targets the fixed
// `lbug.wal.checkpoint`. So: first do a clean full analyze to create the
// index, then plant the blocker and drive an incremental analyze into it.
const storageDir = path.join(repoPath, '.gitnexus');
// 1) Clean full analyze (no blocker) — builds the index into staging and
// swaps it in. Must succeed; the staging checkpoint name is unblocked.
// Use the normal 64 MiB threshold while seeding the index. Checkpointing
// every setup write needlessly stalls schema and FTS creation on Windows.
const first = runAnalyze('67108864');
expect(first.status === null ? 'timeout' : first.status, first.stderr).toBe(0);
// 2) Change a tracked source file and commit, so the next analyze is an
// incremental writeback (in-place), not a full rebuild.
const churnFile = path.join(repoPath, 'src', 'logger.ts');
fs.appendFileSync(churnFile, `\nexport const walChurnMarker = ${Date.now()};\n`);
const gitEnv = {
...process.env,
GIT_AUTHOR_NAME: 'test',
GIT_AUTHOR_EMAIL: 'test@test',
GIT_COMMITTER_NAME: 'test',
GIT_COMMITTER_EMAIL: 'test@test',
};
spawnSync('git', ['add', '-A'], { cwd: repoPath, stdio: 'pipe' });
spawnSync('git', ['commit', '-m', 'churn for incremental'], {
cwd: repoPath,
stdio: 'pipe',
env: gitEnv,
});
// 3) Plant a non-empty directory at `lbug.wal.checkpoint`, the fixed rename
// target of the in-place checkpoint. `fs.rename` cannot overwrite a
// non-empty directory, and the adapter's orphan-sidecar cleanup uses
// `fs.unlink` (which fails on a directory) — so the blocker persists through
// `doInitLbug` and trips the auto-checkpoint the incremental writeback
// forces at `GITNEXUS_WAL_CHECKPOINT_THRESHOLD=1`.
const blockerDir = path.join(storageDir, 'lbug.wal.checkpoint');
fs.rmSync(blockerDir, { recursive: true, force: true });
fs.mkdirSync(blockerDir, { recursive: true });
fs.writeFileSync(path.join(blockerDir, 'blocker'), 'cannot-be-renamed-over');
// 4) Incremental analyze into the blocked checkpoint target.
const result = runAnalyze('1');
const combined = `${result.stderr}\n${result.stdout}`;
// The blocked checkpoint must produce an error, rather than a timeout.
expect(result.status, combined).not.toBeNull();
expect(result.status).not.toBe(0);
// Recovery hint must reference the CLI flag and the recommended
// 64 MiB threshold (67_108_864 bytes). Both come from the
// RECOMMENDED_WAL_CHECKPOINT_THRESHOLD constant in analyze.ts; keep
// those values in sync with this assertion if the constant changes.
expect(combined).toContain('gitnexus analyze --wal-checkpoint-threshold');
expect(combined).toContain('67108864');
// The env-var route should be advertised alongside the flag.
expect(combined).toContain('GITNEXUS_WAL_CHECKPOINT_THRESHOLD');
}, 180_000);
});