GitNexus/gitnexus/test/integration/shared-store-analyze.test.ts
Parafee41 2925cfc024
fix(analyze): revisit dirty snapshots after clean revert (#3389)
* fix(analyze): revisit dirty snapshots after clean revert

* test(shared-store): cover clean revert publication

* fix(analyze): clear hidden index flags after clean runs

* fix(analyze): reconcile hidden dirty paths

* fix(analyze): detect newly hidden edits

* test(bench): record hidden-index fixture calls

* fix(analyze): clear restored mode-only receipts

* test(bench): restore receiver baseline after mode fixture

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-09-28 14:48:07 +01:00

700 lines
28 KiB
TypeScript

import { execFileSync } from 'child_process';
import { existsSync } from 'fs';
import fs from 'fs/promises';
import path from 'path';
import { pathToFileURL } from 'url';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { CLASS_FRAMEWORK_ANNOTATIONS_FEATURE } from '../../src/core/analysis-features.js';
import { resolveAnalyzerRunnerIdentity } from '../../src/core/analyzer-identity.js';
import { RebuildReasonCollector } from '../../src/core/rebuild-reasons.js';
import type { EmbeddingCheckpoint } from '../../src/core/embedding-checkpoint.js';
import { SCHEMA_FINGERPRINT } from '../../src/core/lbug/schema.js';
import {
ensurePrivateSharedGraph,
featureKeyOf,
listStoreMetaRoots,
publishSharedGraph,
seedSharedSlot,
} from '../../src/core/shared-store-analyze.js';
import {
getStoragePaths,
listRegisteredRepos,
loadMeta,
saveMeta,
} from '../../src/storage/repo-manager.js';
import type { RepoMeta } from '../../src/storage/repo-meta.js';
import {
commitGraphDir,
resolveGraphPath,
resolveSharedStore,
type SharedStoreLayout,
} from '../../src/storage/shared-store.js';
import { createTempDir } from '../helpers/test-db.js';
// These suites exercise sharing; an inherited opt-out would silently disable it.
const savedSharedStoreSwitch = process.env.GITNEXUS_SHARED_STORE;
beforeAll(() => {
delete process.env.GITNEXUS_SHARED_STORE;
});
afterAll(() => {
if (savedSharedStoreSwitch === undefined) delete process.env.GITNEXUS_SHARED_STORE;
else process.env.GITNEXUS_SHARED_STORE = savedSharedStoreSwitch;
});
/**
* #3352 — linked worktrees at one commit share one immutable commit graph in
* the store under GITNEXUS_HOME, and a second worktree's analyze reuses it
* without writing a graph.
*/
const git = (cwd: string, ...args: string[]): string =>
execFileSync('git', args, { cwd, stdio: 'pipe', encoding: 'utf-8' }).trim();
const commitAll = (cwd: string, message: string): void => {
git(cwd, 'add', '-A');
git(cwd, '-c', 'user.name=t', '-c', 'user.email=t@t', 'commit', '-q', '-m', message);
};
const layoutOf = (checkout: string): SharedStoreLayout => {
const layout = resolveSharedStore(checkout);
expect(layout).not.toBeNull();
return layout as SharedStoreLayout;
};
const listCommitDirs = async (layout: SharedStoreLayout): Promise<string[]> =>
(await fs.readdir(layout.commitsDir).catch(() => [] as string[])).filter(
(name) => !name.startsWith('.'),
);
describe('shared sibling store analyze (#3352)', () => {
let tmpHome: Awaited<ReturnType<typeof createTempDir>>;
let tmpRepo: Awaited<ReturnType<typeof createTempDir>>;
let savedHome: string | undefined;
let main: string;
let wtA: string;
let wtB: string;
beforeEach(async () => {
tmpHome = await createTempDir('gitnexus-test-shared-home-');
tmpRepo = await createTempDir('gitnexus-test-shared-repo-');
savedHome = process.env.GITNEXUS_HOME;
process.env.GITNEXUS_HOME = tmpHome.dbPath;
const root = await fs.realpath(tmpRepo.dbPath);
main = path.join(root, 'main');
wtA = path.join(root, 'wt-a');
wtB = path.join(root, 'wt-b');
await fs.mkdir(main);
git(main, 'init', '-q', '-b', 'main');
await fs.writeFile(
path.join(main, 'a.ts'),
'export function a() { return b(); }\nexport function b() { return 1; }\n',
);
commitAll(main, 'init');
git(main, 'worktree', 'add', '-q', '-b', 'wt-a', wtA);
git(main, 'worktree', 'add', '-q', '-b', 'wt-b', wtB);
});
afterEach(async () => {
if (savedHome === undefined) delete process.env.GITNEXUS_HOME;
else process.env.GITNEXUS_HOME = savedHome;
await tmpRepo.cleanup();
await tmpHome.cleanup();
});
it('Covers AE1: three clean worktrees at one commit share one commit graph', async () => {
const { runFullAnalysis } = await import('../../src/core/run-analyze.js');
const results = [];
for (const checkout of [main, wtA, wtB]) {
results.push(await runFullAnalysis(checkout, {}, { onProgress: () => {} }));
}
const layout = layoutOf(main);
const commitDirs = await listCommitDirs(layout);
expect(commitDirs).toHaveLength(1);
const graph = path.join(layout.commitsDir, commitDirs[0], 'lbug');
for (const checkout of [main, wtA, wtB]) {
const slot = layoutOf(checkout).checkoutSlot;
expect(getStoragePaths(checkout, undefined, slot).lbugPath).toBe(graph);
expect(existsSync(path.join(slot, 'lbug'))).toBe(false);
// The pre-existing repository-local index location is never written.
expect(existsSync(path.join(checkout, '.gitnexus', 'lbug'))).toBe(false);
}
// Siblings after the first reuse the published graph without a pipeline run.
expect(results.map((r) => r.alreadyUpToDate === true)).toEqual([false, true, true]);
const registered = await listRegisteredRepos();
for (const checkout of [main, wtA, wtB]) {
const entry = registered.find((e) => e.path === checkout);
expect(entry?.storagePath).toBe(layoutOf(checkout).checkoutSlot);
}
}, 180_000);
it('reports a checkout that reads a commit graph as indexed', async () => {
const { runFullAnalysis } = await import('../../src/core/run-analyze.js');
await runFullAnalysis(wtA, {}, { onProgress: () => {} });
await runFullAnalysis(wtB, {}, { onProgress: () => {} });
const slot = layoutOf(wtB).checkoutSlot;
expect(existsSync(path.join(slot, 'lbug'))).toBe(false);
const { inspectRegisteredStorage } = await import('../../src/storage/storage-resolver.js');
const inspection = await inspectRegisteredStorage({ path: wtB, storagePath: slot });
expect(inspection).toMatchObject({ state: 'owned', hasCodeIndexDB: true });
const validated = await listRegisteredRepos({ validate: true });
expect(validated.map((e) => e.path)).toEqual(expect.arrayContaining([wtA, wtB]));
}, 180_000);
it('Covers AE1: MCP opens one database for three checkouts on one commit graph', async () => {
const { runFullAnalysis } = await import('../../src/core/run-analyze.js');
for (const checkout of [main, wtA, wtB]) {
await runFullAnalysis(checkout, {}, { onProgress: () => {} });
}
const graphs = [main, wtA, wtB].map(
(c) => getStoragePaths(c, undefined, layoutOf(c).checkoutSlot).lbugPath,
);
expect(new Set(graphs).size).toBe(1);
const { initLbug, closeLbug } = await import('../../src/core/lbug/pool-adapter.js');
const savedTrace = process.env.GITNEXUS_POOL_RSS_TRACE;
process.env.GITNEXUS_POOL_RSS_TRACE = '1';
const traces: string[] = [];
const write = process.stderr.write.bind(process.stderr);
process.stderr.write = ((chunk: string | Uint8Array, ...rest: unknown[]) => {
if (String(chunk).startsWith('[pool-rss]')) traces.push(String(chunk));
return (write as (...a: unknown[]) => boolean)(chunk, ...rest);
}) as typeof process.stderr.write;
try {
for (const [i, graph] of graphs.entries()) await initLbug(`shared-${i}`, graph);
} finally {
process.stderr.write = write;
if (savedTrace === undefined) delete process.env.GITNEXUS_POOL_RSS_TRACE;
else process.env.GITNEXUS_POOL_RSS_TRACE = savedTrace;
await closeLbug();
}
const last = traces.filter((t) => t.includes(' init ')).pop();
expect(last).toMatch(/pool=3 dbCache=1 /);
}, 240_000);
it('serves a sibling the same relative file paths from the shared graph', async () => {
const { runFullAnalysis } = await import('../../src/core/run-analyze.js');
await runFullAnalysis(wtA, {}, { onProgress: () => {} });
await runFullAnalysis(wtB, {}, { onProgress: () => {} });
const lbug = (await import('@ladybugdb/core')).default;
const graph = getStoragePaths(wtB, undefined, layoutOf(wtB).checkoutSlot).lbugPath;
const db = new lbug.Database(graph, 0, true, true);
const conn = new lbug.Connection(db);
let rows: { p: string }[];
try {
rows = (await (
await conn.query('MATCH (f:File) RETURN f.filePath AS p ORDER BY p')
).getAll()) as { p: string }[];
} finally {
await conn.close();
await db.close();
}
expect(rows.map((r) => r.p)).toEqual(['a.ts']);
}, 180_000);
it('publishes a new commit graph when a clean worktree moves to a new commit', async () => {
const { runFullAnalysis } = await import('../../src/core/run-analyze.js');
await runFullAnalysis(wtA, {}, { onProgress: () => {} });
await fs.writeFile(path.join(wtA, 'c.ts'), 'export const c = 3;\n');
commitAll(wtA, 'c');
await runFullAnalysis(wtA, {}, { onProgress: () => {} });
const layout = layoutOf(wtA);
const head = git(wtA, 'rev-parse', 'HEAD');
const meta = await loadMeta(layout.checkoutSlot);
expect(meta?.lastCommit).toBe(head);
expect(meta?.graphPath).toBe(
path.join(commitGraphDir(layout, head, featureKeyOf(meta as RepoMeta)), 'lbug'),
);
// The previous commit's graph is no longer referenced and is reclaimed at once.
expect(await listCommitDirs(layout)).toEqual([
path.basename(path.dirname(meta?.graphPath as string)),
]);
}, 180_000);
it('never publishes a graph that was built from uncommitted edits', async () => {
const { runFullAnalysis } = await import('../../src/core/run-analyze.js');
await fs.writeFile(path.join(wtA, 'a.ts'), 'export function uncommitted() { return 9; }\n');
await runFullAnalysis(wtA, {}, { onProgress: () => {} });
const layout = layoutOf(wtA);
expect(await listCommitDirs(layout)).toEqual([]);
expect(existsSync(path.join(layout.checkoutSlot, 'lbug'))).toBe(true);
git(wtA, 'checkout', '--', 'a.ts');
await runFullAnalysis(wtA, {}, { onProgress: () => {} });
const meta = await loadMeta(layout.checkoutSlot);
expect(meta?.indexCoverage?.dirtyPaths).toEqual([]);
expect(await listCommitDirs(layout)).toEqual([
path.basename(path.dirname(meta?.graphPath as string)),
]);
expect(existsSync(path.join(layout.checkoutSlot, 'lbug'))).toBe(false);
}, 180_000);
});
describe('featureKeyOf', () => {
const base: RepoMeta = {
repoPath: '/a',
storagePath: '/a/.gitnexus',
lastCommit: 'abc1234',
indexedAt: '2026-01-01T00:00:00.000Z',
schemaFingerprint: 'fp1',
analysisFeatures: { x: 1 },
};
it('ignores per-checkout and per-run fields', () => {
expect(
featureKeyOf({
...base,
repoPath: '/b',
storagePath: '/b/.gitnexus',
indexedAt: '2027-01-01T00:00:00.000Z',
lastCommit: 'def5678',
branch: 'feature',
fileHashes: { 'a.ts': 'h' },
stats: { nodes: 9 },
}),
).toBe(featureKeyOf(base));
});
it('is independent of key order', () => {
const reordered = Object.fromEntries(Object.entries(base).reverse()) as RepoMeta;
expect(featureKeyOf(reordered)).toBe(featureKeyOf(base));
});
it.each([
['schema fingerprint', { schemaFingerprint: 'fp2' }],
['analysis features', { analysisFeatures: { x: 2 } }],
['PDG layer', { pdg: {} as RepoMeta['pdg'] }],
['content retention', { contentRetention: 'none' as const }],
['embeddings present', { stats: { embeddings: 3 } }],
])('changes with %s', (_label, delta) => {
expect(featureKeyOf({ ...base, ...delta })).not.toBe(featureKeyOf(base));
});
});
describe('publishSharedGraph race (#3352)', () => {
let tmpHome: Awaited<ReturnType<typeof createTempDir>>;
let tmpRepo: Awaited<ReturnType<typeof createTempDir>>;
let savedHome: string | undefined;
beforeEach(async () => {
tmpHome = await createTempDir('gitnexus-test-shared-race-home-');
tmpRepo = await createTempDir('gitnexus-test-shared-race-repo-');
savedHome = process.env.GITNEXUS_HOME;
process.env.GITNEXUS_HOME = tmpHome.dbPath;
});
afterEach(async () => {
if (savedHome === undefined) delete process.env.GITNEXUS_HOME;
else process.env.GITNEXUS_HOME = savedHome;
await tmpRepo.cleanup();
await tmpHome.cleanup();
});
const setup = async (): Promise<{ checkouts: string[]; head: string }> => {
const root = await fs.realpath(tmpRepo.dbPath);
const main = path.join(root, 'main');
await fs.mkdir(main);
git(main, 'init', '-q', '-b', 'main');
await fs.writeFile(path.join(main, 'a.ts'), 'export const a = 1;\n');
commitAll(main, 'init');
const wt = path.join(root, 'wt');
git(main, 'worktree', 'add', '-q', '-b', 'wt', wt);
const head = git(main, 'rev-parse', 'HEAD');
for (const checkout of [main, wt]) {
const slot = layoutOf(checkout).checkoutSlot;
await fs.mkdir(slot, { recursive: true });
await fs.writeFile(path.join(slot, 'lbug'), `graph from ${checkout}`);
await saveMeta(slot, {
repoPath: checkout,
storagePath: slot,
lastCommit: head,
indexedAt: new Date().toISOString(),
});
}
return { checkouts: [main, wt], head };
};
it('Covers AE6: two checkouts publishing one new commit produce exactly one graph', async () => {
const { checkouts } = await setup();
await Promise.all(
checkouts.map((c) =>
publishSharedGraph(layoutOf(c), c, git(c, 'rev-parse', 'HEAD'), () => {}),
),
);
const layout = layoutOf(checkouts[0]);
expect(await listCommitDirs(layout)).toHaveLength(1);
const pointers = await Promise.all(
checkouts.map(async (c) => (await loadMeta(layoutOf(c).checkoutSlot))?.graphPath),
);
expect(new Set(pointers).size).toBe(1);
for (const c of checkouts) {
expect(existsSync(path.join(layoutOf(c).checkoutSlot, 'lbug'))).toBe(false);
}
});
it('keeps a private graph whose sidecars are not consolidated', async () => {
const { checkouts, head } = await setup();
const [main] = checkouts;
const slot = layoutOf(main).checkoutSlot;
await fs.writeFile(path.join(slot, 'lbug.wal'), 'pending');
await publishSharedGraph(layoutOf(main), main, head, () => {});
expect(await listCommitDirs(layoutOf(main))).toEqual([]);
expect(existsSync(path.join(slot, 'lbug'))).toBe(true);
expect((await loadMeta(slot))?.graphPath).toBeUndefined();
});
it('keeps a dirty checkout private', async () => {
const { checkouts, head } = await setup();
const [main] = checkouts;
await fs.writeFile(path.join(main, 'a.ts'), 'export const a = 2;\n');
await publishSharedGraph(layoutOf(main), main, head, () => {});
expect(await listCommitDirs(layoutOf(main))).toEqual([]);
expect(existsSync(path.join(layoutOf(main).checkoutSlot, 'lbug'))).toBe(true);
});
// #3374: `git status` is clean in a sparse checkout, but the graph lacks the
// files the checkout leaves out.
it('keeps a checkout that hides committed files private', async () => {
const { checkouts, head } = await setup();
const [main] = checkouts;
git(main, 'update-index', '--skip-worktree', '--', 'a.ts');
await fs.rm(path.join(main, 'a.ts'));
await publishSharedGraph(layoutOf(main), main, head, () => {});
expect(await listCommitDirs(layoutOf(main))).toEqual([]);
expect(existsSync(path.join(layoutOf(main).checkoutSlot, 'lbug'))).toBe(true);
});
/** Fail every rename onto one of `blocked`; others run for real. */
const blockRenamesOnto = (blocked: ReadonlySet<string>) => {
const realRename = fs.rename.bind(fs);
return vi
.spyOn(fs, 'rename')
.mockImplementation((from, to) =>
blocked.has(String(to))
? Promise.reject(Object.assign(new Error('rename blocked'), { code: 'EIO' }))
: realRename(from, to),
);
};
// #3374: the staging dir holds the checkout's only graph once putting it
// back fails; deleting it would leave metadata at HEAD with no graph.
it('keeps the staged graph when putting it back fails', async () => {
const { checkouts, head } = await setup();
const [main] = checkouts;
const layout = layoutOf(main);
const slot = layout.checkoutSlot;
const meta = (await loadMeta(slot)) as RepoMeta;
const target = commitGraphDir(layout, head, featureKeyOf(meta));
const spy = blockRenamesOnto(new Set([target, path.join(slot, 'lbug')]));
try {
await publishSharedGraph(layout, main, head, () => {});
} finally {
spy.mockRestore();
}
const staging = (await fs.readdir(layout.commitsDir)).filter((n) => n.startsWith('.publish-'));
expect(staging).toHaveLength(1);
expect(await fs.readFile(path.join(layout.commitsDir, staging[0], 'lbug'), 'utf-8')).toBe(
`graph from ${main}`,
);
expect(await listCommitDirs(layout)).toEqual([]);
expect((await loadMeta(slot))?.graphPath).toBeUndefined();
});
it('drops the staging dir when the graph never left the slot', async () => {
const { checkouts, head } = await setup();
const [main] = checkouts;
const layout = layoutOf(main);
const slot = layout.checkoutSlot;
const meta = (await loadMeta(slot)) as RepoMeta;
const target = commitGraphDir(layout, head, featureKeyOf(meta));
const spy = blockRenamesOnto(new Set([target]));
try {
await publishSharedGraph(layout, main, head, () => {});
} finally {
spy.mockRestore();
}
expect(await fs.readdir(layout.commitsDir)).toEqual([]);
expect(await fs.readFile(path.join(slot, 'lbug'), 'utf-8')).toBe(`graph from ${main}`);
});
const seedFreshSlot = async (checkout: string): Promise<RepoMeta | null> => {
const layout = layoutOf(checkout);
await fs.rm(layout.checkoutSlot, { recursive: true, force: true });
await seedSharedSlot(layout, checkout, () => {});
return loadMeta(layout.checkoutSlot);
};
it('seeds a pristine checkout at the graph commit as up to date', async () => {
const { checkouts, head } = await setup();
const [main, wt] = checkouts;
await publishSharedGraph(layoutOf(main), main, head, () => {});
const seeded = await seedFreshSlot(wt);
expect(seeded?.graphPath).toBe((await loadMeta(layoutOf(main).checkoutSlot))?.graphPath);
expect(seeded?.lastCommit).toBe(head);
});
it('seeds a checkout that hides committed files without a commit, then re-points', async () => {
const { checkouts, head } = await setup();
const [main, wt] = checkouts;
await publishSharedGraph(layoutOf(main), main, head, () => {});
const shared = (await loadMeta(layoutOf(main).checkoutSlot))?.graphPath;
git(wt, 'update-index', '--skip-worktree', '--', 'a.ts');
const seeded = await seedFreshSlot(wt);
expect(seeded?.graphPath).toBe(shared);
// An empty lastCommit sends the next analyze through the file-hash diff.
expect(seeded?.lastCommit).toBe('');
// That analyze copies the graph, finds nothing to change, and stamps HEAD;
// once the checkout shows every file again, publish drops the copy.
const slot = layoutOf(wt).checkoutSlot;
expect(await ensurePrivateSharedGraph(slot, () => {})).toBe(true);
const copied = await loadMeta(slot);
expect(copied).not.toBeNull();
await saveMeta(slot, { ...(copied as RepoMeta), lastCommit: head });
git(wt, 'update-index', '--no-skip-worktree', '--', 'a.ts');
await publishSharedGraph(layoutOf(wt), wt, head, () => {});
expect((await loadMeta(slot))?.graphPath).toBe(shared);
expect(existsSync(path.join(slot, 'lbug'))).toBe(false);
});
const checkpoint: EmbeddingCheckpoint = {
at: '2026-01-01T00:00:00.000Z',
nodesProcessed: 1,
totalNodes: 2,
chunksProcessed: 1,
model: 'm',
dimensions: 4,
provider: 'local',
kind: 'partial',
pendingNodeIds: ['n2'],
};
// #3374: a graph with embeddings still owed would become every checkout's
// graph, and its checkpoint-free copy would look complete forever.
it('keeps a graph with pending embeddings private', async () => {
const { checkouts, head } = await setup();
const [main] = checkouts;
const slot = layoutOf(main).checkoutSlot;
const meta = (await loadMeta(slot)) as RepoMeta;
await saveMeta(slot, { ...meta, embeddingCheckpoint: checkpoint });
await publishSharedGraph(layoutOf(main), main, head, () => {});
expect(await listCommitDirs(layoutOf(main))).toEqual([]);
expect(existsSync(path.join(slot, 'lbug'))).toBe(true);
expect((await loadMeta(slot))?.embeddingCheckpoint).toEqual(checkpoint);
});
// A commit graph published before that rule may still be the weaker one;
// the checkout keeps its own graph rather than trading down to it.
it.each<[string, Partial<RepoMeta>]>([
['records pending embeddings', { embeddingCheckpoint: checkpoint, stats: { embeddings: 5 } }],
['has fewer embeddings', { stats: { embeddings: 2 } }],
])('keeps the private graph when the published one %s', async (_label, targetDelta) => {
const { checkouts, head } = await setup();
const [main, wt] = checkouts;
const layout = layoutOf(main);
const slot = layout.checkoutSlot;
const own: RepoMeta = { ...((await loadMeta(slot)) as RepoMeta), stats: { embeddings: 5 } };
await saveMeta(slot, own);
const target = commitGraphDir(layout, head, featureKeyOf(own));
await fs.mkdir(target, { recursive: true });
await fs.writeFile(path.join(target, 'lbug'), 'older published graph');
const targetMeta: Partial<RepoMeta> = {
lastCommit: head,
indexedAt: own.indexedAt,
...targetDelta,
};
await fs.writeFile(path.join(target, 'gitnexus.json'), JSON.stringify(targetMeta));
// Another checkout reads the published graph.
const wtSlot = layoutOf(wt).checkoutSlot;
await fs.rm(path.join(wtSlot, 'lbug'));
const wtMeta = (await loadMeta(wtSlot)) as RepoMeta;
await saveMeta(wtSlot, { ...wtMeta, graphPath: path.join(target, 'lbug') });
await publishSharedGraph(layout, main, head, () => {});
expect(await fs.readFile(path.join(slot, 'lbug'), 'utf-8')).toBe(`graph from ${main}`);
expect((await loadMeta(slot))?.graphPath).toBeUndefined();
// The published graph is immutable: other checkouts may point at it.
expect(await fs.readFile(path.join(target, 'lbug'), 'utf-8')).toBe('older published graph');
});
});
// #3374: a publish interrupted between its renames (or a reclaimed commit
// graph) leaves slot metadata at HEAD with no graph behind it.
describe('up-to-date fast path over a missing shared graph (#3374)', () => {
let tmpHome: Awaited<ReturnType<typeof createTempDir>>;
let tmpRepo: Awaited<ReturnType<typeof createTempDir>>;
let savedHome: string | undefined;
beforeEach(async () => {
tmpHome = await createTempDir('gitnexus-test-shared-missing-home-');
tmpRepo = await createTempDir('gitnexus-test-shared-missing-repo-');
savedHome = process.env.GITNEXUS_HOME;
process.env.GITNEXUS_HOME = tmpHome.dbPath;
});
afterEach(async () => {
if (savedHome === undefined) delete process.env.GITNEXUS_HOME;
else process.env.GITNEXUS_HOME = savedHome;
await tmpRepo.cleanup();
await tmpHome.cleanup();
});
/** A linked-worktree checkout slot whose metadata is current in every stamp. */
const currentCheckoutSlot = async (
overrides: Partial<RepoMeta> = {},
): Promise<{ wt: string; slot: string }> => {
const root = await fs.realpath(tmpRepo.dbPath);
const main = path.join(root, 'main');
await fs.mkdir(main);
git(main, 'init', '-q', '-b', 'main');
git(
main,
'-c',
'user.name=t',
'-c',
'user.email=t@t',
'commit',
'-q',
'--allow-empty',
'-m',
'init',
);
const wt = path.join(root, 'wt');
git(main, 'worktree', 'add', '-q', '-b', 'wt', wt);
const slot = layoutOf(wt).checkoutSlot;
await fs.mkdir(slot, { recursive: true });
await saveMeta(slot, {
repoPath: wt,
storagePath: slot,
lastCommit: git(wt, 'rev-parse', 'HEAD'),
indexedAt: new Date().toISOString(),
schemaFingerprint: SCHEMA_FINGERPRINT,
analysisFeatures: {
[CLASS_FRAMEWORK_ANNOTATIONS_FEATURE.id]: CLASS_FRAMEWORK_ANNOTATIONS_FEATURE.version,
},
runnerIdentity: resolveAnalyzerRunnerIdentity(
pathToFileURL(path.resolve(__dirname, '../../src/core/run-analyze.ts')).href,
),
// Same FTS mode as the runs below, so only the graph can decide
// against the fast path.
capabilities: {
graph: { provider: 'ladybugdb', status: 'available' },
fts: { provider: 'ladybugdb-fts', status: 'unavailable', skipReason: 'disabled-by-flag' },
vectorSearch: { provider: 'exact-scan', status: 'unavailable', exactScanLimit: 0 },
},
...overrides,
});
return { wt, slot };
};
it('rebuilds a slot whose metadata is at HEAD but whose graph is gone', async () => {
const { wt, slot } = await currentCheckoutSlot();
const { runFullAnalysis } = await import('../../src/core/run-analyze.js');
const result = await runFullAnalysis(
wt,
{ skipAgentsMd: true, skipSkills: true, skipFts: true },
{ onProgress: () => {} },
);
expect(result.alreadyUpToDate).not.toBe(true);
expect(result.rebuildReasons).toEqual(['shared-store-missing-graph']);
expect(existsSync(resolveGraphPath(slot))).toBe(true);
}, 120_000);
it('names a failed shared-graph copy in the summary, not in a follow-up (#3137)', async () => {
// The slot records an older commit, so the run reaches the copy, and
// points at a published commit graph that exists but cannot be copied (a
// directory where the graph file belongs).
const olderCommit = '0'.repeat(40);
const { wt, slot } = await currentCheckoutSlot({ lastCommit: olderCommit });
const unreadableGraph = path.join(
commitGraphDir(layoutOf(wt), olderCommit, 'deadbeefdeadbeef'),
'lbug',
);
await fs.mkdir(unreadableGraph, { recursive: true });
const meta = await loadMeta(slot);
if (meta === null) throw new Error('slot metadata missing');
await saveMeta(slot, { ...meta, graphPath: unreadableGraph });
expect(resolveGraphPath(slot)).toBe(unreadableGraph);
const { runFullAnalysis } = await import('../../src/core/run-analyze.js');
const logs: string[] = [];
const result = await runFullAnalysis(
wt,
{ skipAgentsMd: true, skipSkills: true, skipFts: true },
{ onProgress: () => {}, onLog: (m) => logs.push(m) },
);
expect(result.rebuildReasons).toEqual(['private-graph-unavailable']);
const single = new RebuildReasonCollector();
single.add({ key: 'private-graph-unavailable', text: '' });
const summaryPrefix = single.formatSummary() ?? '';
const late = new RebuildReasonCollector();
late.formatSummary();
late.add({ key: 'private-graph-unavailable', text: '' });
const followUpPrefix = late.formatFollowUp() ?? '';
expect(logs.filter((m) => m.startsWith(summaryPrefix))).toHaveLength(1);
expect(logs.filter((m) => m.startsWith(followUpPrefix))).toEqual([]);
}, 120_000);
});
describe('listStoreMetaRoots', () => {
let tmp: Awaited<ReturnType<typeof createTempDir>>;
let layout: SharedStoreLayout;
beforeEach(async () => {
tmp = await createTempDir('gitnexus-test-store-roots-');
const root = path.join(tmp.dbPath, 'store');
layout = {
key: 'repo-0000',
root,
cachesDir: path.join(root, 'caches'),
commitsDir: path.join(root, 'commits'),
checkoutsDir: path.join(root, 'checkouts'),
checkoutSlot: path.join(root, 'checkouts', 'slot-a'),
canonicalCheckout: null,
};
});
afterEach(async () => {
vi.restoreAllMocks();
await tmp.cleanup();
});
it('stays complete when the store directories do not exist yet', async () => {
expect(await listStoreMetaRoots(layout)).toEqual({ roots: [], complete: true });
});
it('lists checkout slots and commit graphs, skipping dot entries', async () => {
await fs.mkdir(path.join(layout.checkoutsDir, 'slot-a'), { recursive: true });
await fs.mkdir(path.join(layout.checkoutsDir, '.lock'), { recursive: true });
await fs.mkdir(path.join(layout.commitsDir, 'abc'), { recursive: true });
expect(await listStoreMetaRoots(layout)).toEqual({
roots: [path.join(layout.checkoutsDir, 'slot-a'), path.join(layout.commitsDir, 'abc')],
complete: true,
});
});
it('reports an incomplete listing when a store directory cannot be read', async () => {
await fs.mkdir(path.join(layout.commitsDir, 'abc'), { recursive: true });
await fs.mkdir(layout.checkoutsDir, { recursive: true });
const realReaddir = fs.readdir;
vi.spyOn(fs, 'readdir').mockImplementation((async (dir: string) => {
if (dir === layout.checkoutsDir) {
throw Object.assign(new Error('permission denied'), { code: 'EACCES' });
}
return realReaddir(dir);
}) as unknown as typeof fs.readdir);
expect(await listStoreMetaRoots(layout)).toEqual({
roots: [path.join(layout.commitsDir, 'abc')],
complete: false,
});
});
});