GitNexus/gitnexus/test/unit/embedding-recovery.test.ts

175 lines
6.4 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { mkdtempSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
readEmbeddingRecovery,
resolveEmbeddingRecovery,
} from '../../src/storage/embedding-recovery.js';
const stagingFile = 'lbug.staging.6e34c761-bf58-46cc-8b54-78d11607bc46';
const familySuffixes = [
'',
'.wal',
'.shadow',
'.wal.checkpoint',
'.lock',
'.checkpoint.intent.lock',
'.checkpoint.apply.lock',
];
const checkpoint = () => ({
kind: 'interrupted',
at: '2026-10-03T12:00:00.000Z',
nodesProcessed: 1,
totalNodes: 3,
chunksProcessed: 2,
model: 'test-model',
dimensions: 2,
provider: 'local',
pendingNodeIds: ['active'],
recovery: {
stagingFile,
schemaFingerprint: 'test-schema',
unsafeNodeIds: ['active', 'incomplete-restore'],
},
});
let dir: string;
beforeEach(() => {
dir = mkdtempSync(path.join(os.tmpdir(), 'gnx-embedding-recovery-'));
writeFileSync(path.join(dir, stagingFile), 'stage');
});
afterEach(() => rmSync(dir, { recursive: true, force: true }));
describe('resolveEmbeddingRecovery', () => {
it('resolves the exact staged generation and carries all unsafe node IDs', () => {
expect(resolveEmbeddingRecovery(dir, checkpoint())).toEqual({
...checkpoint().recovery,
dbPath: path.join(dir, stagingFile),
familyFiles: familySuffixes.map((suffix) => stagingFile + suffix),
});
});
it.each([
'../lbug.staging.6e34c761-bf58-46cc-8b54-78d11607bc46',
'/tmp/lbug.staging.6e34c761-bf58-46cc-8b54-78d11607bc46',
'branches/foreign/lbug.staging.6e34c761-bf58-46cc-8b54-78d11607bc46',
'..\\lbug.staging.6e34c761-bf58-46cc-8b54-78d11607bc46',
'lbug',
'lbug.new',
'lbug.staging.orphan',
`${stagingFile}.wal`,
'lbug.staging.00000000-0000-4000-8000-000000000000',
])('rejects a foreign, unrecognized or missing generation: %s', (filename) => {
const marker = checkpoint();
marker.recovery.stagingFile = filename;
expect(resolveEmbeddingRecovery(dir, marker)).toBeUndefined();
});
it.each([
null,
[],
{ recovery: checkpoint().recovery },
{ ...checkpoint(), kind: 'partial' },
{ ...checkpoint(), kind: 'unverified-count' },
{ ...checkpoint(), kind: 'unknown' },
{ ...checkpoint(), at: 'invalid-time' },
{ ...checkpoint(), nodesProcessed: -1 },
{ ...checkpoint(), nodesProcessed: 4 },
{ ...checkpoint(), totalNodes: 1.5 },
{ ...checkpoint(), chunksProcessed: Number.NaN },
{ ...checkpoint(), model: '' },
{ ...checkpoint(), provider: null },
{ ...checkpoint(), dimensions: 0 },
{ ...checkpoint(), pendingNodeIds: [42] },
{ ...checkpoint(), pendingNodeIds: ['unexcluded'] },
{ ...checkpoint(), recovery: { ...checkpoint().recovery, schemaFingerprint: '' } },
{ ...checkpoint(), recovery: { ...checkpoint().recovery, unsafeNodeIds: undefined } },
{ ...checkpoint(), recovery: { ...checkpoint().recovery, unsafeNodeIds: [''] } },
])('rejects malformed or incompatible checkpoint shape %#', (marker) => {
expect(resolveEmbeddingRecovery(dir, marker)).toBeUndefined();
});
it('accepts a durable restored stage even before a new embedding window completes', () => {
expect(
resolveEmbeddingRecovery(dir, {
...checkpoint(),
nodesProcessed: 0,
chunksProcessed: 0,
pendingNodeIds: [],
}),
).toBeDefined();
});
it('rejects a directory in place of the staged database', () => {
rmSync(path.join(dir, stagingFile));
mkdirSync(path.join(dir, stagingFile));
expect(resolveEmbeddingRecovery(dir, checkpoint())).toBeUndefined();
});
it.each(familySuffixes)('rejects a symlink in the staged family: %s', (suffix) => {
const target = path.join(dir, 'external-file');
writeFileSync(target, 'external');
const candidate = path.join(dir, stagingFile + suffix);
rmSync(candidate, { force: true });
symlinkSync(target, candidate);
expect(resolveEmbeddingRecovery(dir, checkpoint())).toBeUndefined();
});
it.each(familySuffixes.slice(1))('rejects a dangling staged sidecar symlink: %s', (suffix) => {
symlinkSync(path.join(dir, 'missing'), path.join(dir, stagingFile + suffix));
expect(resolveEmbeddingRecovery(dir, checkpoint())).toBeUndefined();
});
it.each(familySuffixes.slice(1))('rejects a non-file staged sidecar: %s', (suffix) => {
mkdirSync(path.join(dir, stagingFile + suffix));
expect(resolveEmbeddingRecovery(dir, checkpoint())).toBeUndefined();
});
});
describe('readEmbeddingRecovery', () => {
const writeMeta = (filename: string, marker: unknown = checkpoint()): void => {
writeFileSync(path.join(dir, filename), JSON.stringify({ embeddingCheckpoint: marker }));
};
it('prefers the primary metadata file over a stale legacy reference', () => {
writeMeta('gitnexus.json');
writeMeta('meta.json', null);
expect(readEmbeddingRecovery(dir)?.stagingFile).toBe(stagingFile);
});
it('loads the legacy mirror only when the primary metadata file is absent', () => {
writeMeta('meta.json');
expect(readEmbeddingRecovery(dir)?.stagingFile).toBe(stagingFile);
});
it('does not resurrect a legacy reference when the primary file is malformed', () => {
writeFileSync(path.join(dir, 'gitnexus.json'), '{');
writeMeta('meta.json');
expect(readEmbeddingRecovery(dir)).toBeUndefined();
});
it('does not fall back from valid primary metadata with no recovery reference', () => {
writeMeta('gitnexus.json', null);
writeMeta('meta.json');
expect(readEmbeddingRecovery(dir)).toBeUndefined();
});
it('rejects symlinked primary metadata rather than reading a foreign receipt', () => {
writeMeta('meta.json');
symlinkSync(path.join(dir, 'meta.json'), path.join(dir, 'gitnexus.json'));
expect(readEmbeddingRecovery(dir)).toBeUndefined();
});
it('resolves branch-slot provenance within that slot despite a flat storagePath', () => {
const branchSlot = path.join(dir, 'branches', 'feature');
mkdirSync(branchSlot, { recursive: true });
writeFileSync(path.join(branchSlot, stagingFile), 'branch-stage');
writeFileSync(
path.join(branchSlot, 'gitnexus.json'),
JSON.stringify({ storagePath: dir, embeddingCheckpoint: checkpoint() }),
);
expect(readEmbeddingRecovery(branchSlot)?.dbPath).toBe(path.join(branchSlot, stagingFile));
expect(readEmbeddingRecovery(dir)).toBeUndefined();
});
});