mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-06 02:49:56 +00:00
* Initial plan * fix(analyze): add WAL auto-checkpoint CLI control and default-off behavior * test(analyze): share lbug auto-checkpoint parsing and align validation * fix(analyze): always enable lbug auto-checkpoint and expose threshold control * refactor(lbug): inline always-on auto-checkpoint constructor arg * fix(analyze): guide checkpoint-threshold on Ladybug WAL checkpoint IO failures * test(analyze): cover checkpoint IO guidance and add integration guard * fix(analyze): tighten checkpoint IO detection and remove test hook * fix(analyze): remove checkpoint test hook and tighten error matching * fix(analyze): rename to wal-checkpoint-threshold, raise default, add manual checkpoint driver with retry Address review feedback on PR #1772: - Rename CLI flag, env var, AnalyzeOptions field, recovery-hint tag, and parser/constants from lbug-* to engine-neutral wal-* (matches the existing WAL_RECOVERY_SUGGESTION / isWalCorruptionError convention). - Raise default threshold from -1 (Ladybug stock ~16 MiB) to 64 MiB so users on the default config no longer hit the original rename/remove race. - Align both READMEs to publish 67108864 (64 MiB) instead of 65536 (which would have made the crash more frequent). - Add wal-checkpoint-driver.ts: a periodic manual CHECKPOINT driver wrapped in a 3-attempt jittered retry (50/200/500 ms), driven from runFullAnalysis. Opt-out via GITNEXUS_WAL_MANUAL_CHECKPOINT=0. Moves the race window into a JS-controllable retry surface while keeping native auto-checkpoint on. - Move LBUG_CHECKPOINT_RENAME_RE / REMOVE_RE plus the predicate (renamed to isLbugCheckpointIoError) into lbug-config.ts alongside isWalCorruptionError. Predicate is now exported. Add a permissive fallback matcher and pin the matched Ladybug version in comments. - Warn instead of silently defaulting when GITNEXUS_WAL_CHECKPOINT_THRESHOLD is set to a non-empty unparseable value (closes the CLI-vs-env asymmetry). - Add a typed RecoveryHint string-literal union in cli-message.ts so future hint tags can't drift. - Add a real integration test under test/integration/ that triggers a Ladybug checkpoint IO failure via a pre-existing directory at the rename target (portable across platforms; no test-only injection hook). - Add small-disk / CI caveat (32 MiB secondary suggestion) to the recovery hint and README env-var rows. - Document CLI/env precedence in the analyze --help block. - Help placeholder: <value> -> <bytes>. - Rename analyze-lbug-auto-checkpoint.test.ts to use the new wal-* token. * chore(lbug): remove dead jitteredDelay helper and apply prettier - Drop unused `jitteredDelay` function flagged by CodeQL in PR #1772; the retry loop already inlines the same calculation with the injectable `randomImpl` so the helper was dead. Move the non-cryptographic-by-design comment next to the actual jitter site. - Apply `prettier --write` to wal-checkpoint-driver.ts and the new integration test to absorb the PR autofix bot's formatting findings. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: Test <test@example.com>
195 lines
6 KiB
TypeScript
195 lines
6 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
createLbugDatabase,
|
|
isLbugCheckpointIoError,
|
|
isWalCorruptionError,
|
|
} from '../../src/core/lbug/lbug-config.js';
|
|
import { _captureLogger } from '../../src/core/logger.js';
|
|
|
|
const DEFAULT_THRESHOLD = 64 * 1024 * 1024;
|
|
|
|
describe('isWalCorruptionError', () => {
|
|
it.each([
|
|
[
|
|
'Corrupted wal file',
|
|
'Runtime exception: Corrupted wal file. Read out invalid WAL record type.',
|
|
],
|
|
['invalid WAL record', 'Error: invalid WAL record type'],
|
|
['WAL checksum', 'Checksum verification failed, the WAL file is corrupted.'],
|
|
['WAL + corrupt', 'the WAL file is corrupted'],
|
|
])('matches WAL corruption: %s', (_label, msg) => {
|
|
expect(isWalCorruptionError(msg)).toBe(true);
|
|
expect(isWalCorruptionError(new Error(msg))).toBe(true);
|
|
});
|
|
|
|
it.each([
|
|
['lock error', 'Could not set lock on file : /path/to/db'],
|
|
['generic', 'Query failed'],
|
|
['not found', 'LadybugDB not found at /path'],
|
|
['checksum without WAL', 'Checksum verification failed for parquet file'],
|
|
['permission path with WAL', "EACCES: permission denied '/path/to/wal'"],
|
|
['schema mismatch WAL', 'schema version mismatch in WAL'],
|
|
])('does not match non-WAL error: %s', (_label, msg) => {
|
|
expect(isWalCorruptionError(msg)).toBe(false);
|
|
});
|
|
|
|
it('handles non-string input', () => {
|
|
expect(isWalCorruptionError(undefined)).toBe(false);
|
|
expect(isWalCorruptionError(null)).toBe(false);
|
|
expect(isWalCorruptionError(42)).toBe(false);
|
|
expect(isWalCorruptionError(new Error('ok'))).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('createLbugDatabase WAL replay option', () => {
|
|
it('enables auto-checkpoint by default and uses default threshold (64 MiB)', () => {
|
|
const Database = vi.fn(function (this: any) {});
|
|
const lbugModule = { Database } as any;
|
|
|
|
createLbugDatabase(lbugModule, '/tmp/lbug-default');
|
|
|
|
expect(Database).toHaveBeenCalledWith(
|
|
'/tmp/lbug-default',
|
|
0,
|
|
false,
|
|
false,
|
|
expect.any(Number),
|
|
true,
|
|
DEFAULT_THRESHOLD,
|
|
true,
|
|
true,
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
['0', 0],
|
|
['1024', 1024],
|
|
['-1', -1],
|
|
['invalid', DEFAULT_THRESHOLD],
|
|
['', DEFAULT_THRESHOLD],
|
|
])('respects GITNEXUS_WAL_CHECKPOINT_THRESHOLD=%s', (raw, expectedCheckpointThreshold) => {
|
|
try {
|
|
vi.stubEnv('GITNEXUS_WAL_CHECKPOINT_THRESHOLD', raw);
|
|
const Database = vi.fn(function (this: any) {});
|
|
const lbugModule = { Database } as any;
|
|
|
|
createLbugDatabase(lbugModule, '/tmp/lbug-env');
|
|
|
|
expect(Database).toHaveBeenCalledWith(
|
|
'/tmp/lbug-env',
|
|
0,
|
|
false,
|
|
false,
|
|
expect.any(Number),
|
|
true,
|
|
expectedCheckpointThreshold,
|
|
true,
|
|
true,
|
|
);
|
|
} finally {
|
|
vi.unstubAllEnvs();
|
|
}
|
|
});
|
|
|
|
it('warns and falls back to default when GITNEXUS_WAL_CHECKPOINT_THRESHOLD is invalid', () => {
|
|
const cap = _captureLogger();
|
|
try {
|
|
vi.stubEnv('GITNEXUS_WAL_CHECKPOINT_THRESHOLD', 'invalid');
|
|
const Database = vi.fn(function (this: any) {});
|
|
const lbugModule = { Database } as any;
|
|
|
|
createLbugDatabase(lbugModule, '/tmp/lbug-invalid');
|
|
|
|
const warn = cap
|
|
.records()
|
|
.find(
|
|
(r) =>
|
|
typeof r.msg === 'string' &&
|
|
r.msg.includes('Ignoring invalid GITNEXUS_WAL_CHECKPOINT_THRESHOLD'),
|
|
);
|
|
expect(warn).toBeDefined();
|
|
} finally {
|
|
vi.unstubAllEnvs();
|
|
cap.restore();
|
|
}
|
|
});
|
|
|
|
it('does NOT warn when GITNEXUS_WAL_CHECKPOINT_THRESHOLD is empty (treated as unset)', () => {
|
|
const cap = _captureLogger();
|
|
try {
|
|
vi.stubEnv('GITNEXUS_WAL_CHECKPOINT_THRESHOLD', '');
|
|
const Database = vi.fn(function (this: any) {});
|
|
const lbugModule = { Database } as any;
|
|
|
|
createLbugDatabase(lbugModule, '/tmp/lbug-empty');
|
|
|
|
const warn = cap
|
|
.records()
|
|
.find(
|
|
(r) =>
|
|
typeof r.msg === 'string' &&
|
|
r.msg.includes('Ignoring invalid GITNEXUS_WAL_CHECKPOINT_THRESHOLD'),
|
|
);
|
|
expect(warn).toBeUndefined();
|
|
} finally {
|
|
vi.unstubAllEnvs();
|
|
cap.restore();
|
|
}
|
|
});
|
|
|
|
it('passes throwOnWalReplayFailure and checksum constructor args explicitly', () => {
|
|
const Database = vi.fn(function (this: any) {});
|
|
const lbugModule = { Database } as any;
|
|
|
|
createLbugDatabase(lbugModule, '/tmp/lbug', {
|
|
readOnly: true,
|
|
throwOnWalReplayFailure: false,
|
|
});
|
|
|
|
expect(Database).toHaveBeenCalledWith(
|
|
'/tmp/lbug',
|
|
0,
|
|
false,
|
|
true,
|
|
expect.any(Number),
|
|
true,
|
|
DEFAULT_THRESHOLD,
|
|
false,
|
|
true,
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── Finding 8: strict + permissive checkpoint IO matchers ─────────────────
|
|
describe('isLbugCheckpointIoError', () => {
|
|
it.each([
|
|
[
|
|
'native rename failure (v0.16.x exact)',
|
|
'Runtime exception: IO exception: Error renaming file /repo/.gitnexus/lbug.wal to /repo/.gitnexus/lbug.wal.checkpoint. ErrorMessage: Permission denied',
|
|
],
|
|
[
|
|
'native remove failure (v0.16.x exact)',
|
|
'Runtime exception: IO exception: Error removing directory or file /repo/.gitnexus/lbug.wal.checkpoint. Error Message: Permission denied',
|
|
],
|
|
])('matches strict %s', (_label, msg) => {
|
|
expect(isLbugCheckpointIoError(msg)).toBe(true);
|
|
expect(isLbugCheckpointIoError(new Error(msg))).toBe(true);
|
|
});
|
|
|
|
it('matches permissive fallback for hypothetical message drift', () => {
|
|
// Permissive matcher accepts any IO-exception-shaped message mentioning .wal.checkpoint.
|
|
const drift =
|
|
'Some new wrapper preamble :: IO exception when finalizing /repo/.gitnexus/lbug.wal.checkpoint';
|
|
expect(isLbugCheckpointIoError(drift)).toBe(true);
|
|
});
|
|
|
|
it('does NOT match unrelated IO errors', () => {
|
|
expect(
|
|
isLbugCheckpointIoError(
|
|
'Runtime exception: IO exception: Error renaming file /repo/data.tmp to /repo/data.tmp.bak',
|
|
),
|
|
).toBe(false);
|
|
expect(isLbugCheckpointIoError('Some other error')).toBe(false);
|
|
expect(isLbugCheckpointIoError(undefined)).toBe(false);
|
|
});
|
|
});
|