A long-lived MCP server (the shared daemon) cached both the project handle (projectCache) and the worktree-mismatch verdict (worktreeMismatchCache) for its whole lifetime — cleared only on shutdown — keyed off the input path and never re-checked. So a worktree path first resolved BEFORE it had its own .codegraph/ — when the walk-up reached the main checkout — stayed pinned to the main checkout: every query kept hitting the wrong index and every result carried a false "this index belongs to a different git working tree" warning, until the server restarted. The CLI was correct (fresh process per run); re-indexing didn't help. - getCodeGraph: re-resolve findNearestCodeGraphRoot on every call (cheap stat walk, no git, no reopen) and cache the open DB by RESOLVED ROOT only. Drops the input-path short-circuit that pinned the first resolution, and the double-keying (which also double-closed each instance in closeAll()). - worktreeMismatchFor: key the verdict on (startPath, indexRoot) so a changed index root recomputes instead of serving the stale "borrowed the parent's index" verdict. Adds a regression test that fails pre-fix (the stale warning survives the index-root flip); the existing "no further git spawn" caching test still passes. The query-staleness half (a second project opened through the handler) isn't unit-testable under vitest, so it was validated with a dist A/B. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b218f625f7
commit
62d5cdde2c
@@ -187,3 +187,78 @@ describe('worktree mismatch surfaces on hot read tools (issue #155)', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* A long-lived MCP server (the shared daemon) cached its worktree-mismatch
|
||||
* verdict keyed only by the start path, and that cache was cleared only on
|
||||
* shutdown. So once the server decided "this worktree borrows the main
|
||||
* checkout's index" — true while the worktree had no `.codegraph/` of its own —
|
||||
* the verdict was pinned for the daemon's whole life. After the worktree got
|
||||
* its own index (the resolved index root flipped from the main checkout to the
|
||||
* worktree itself), the CLI saw the worktree's index but the MCP server kept
|
||||
* emitting the stale false warning until a restart (issue #926).
|
||||
*
|
||||
* The verdict depends on BOTH the start path and the resolved index root, so it
|
||||
* must be cached under both — a changed index root has to invalidate it. This
|
||||
* drives the real `ToolHandler` worktree-notice path across exactly that change
|
||||
* (the resolved index root flips when the server's default project is re-opened
|
||||
* onto the worktree's own index), with no mocking.
|
||||
*/
|
||||
describe('worktree mismatch verdict re-resolves when the index root changes (issue #926)', () => {
|
||||
let mainRepo: string;
|
||||
let worktree: string;
|
||||
let mainCg: CodeGraph;
|
||||
let worktreeCg: CodeGraph;
|
||||
let handler: ToolHandler;
|
||||
|
||||
beforeEach(async () => {
|
||||
mainRepo = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-wt-926-'));
|
||||
git(mainRepo, 'init', '-q');
|
||||
git(mainRepo, 'config', 'user.email', 'test@example.com');
|
||||
git(mainRepo, 'config', 'user.name', 'Test');
|
||||
git(mainRepo, 'config', 'commit.gpgsign', 'false');
|
||||
fs.mkdirSync(path.join(mainRepo, 'src'));
|
||||
fs.writeFileSync(path.join(mainRepo, 'src', 'a.ts'), 'export function mainOnly() { return 1; }\n');
|
||||
git(mainRepo, 'add', '.');
|
||||
git(mainRepo, 'commit', '-q', '-m', 'init');
|
||||
|
||||
// The long-lived server's default project starts as the MAIN checkout.
|
||||
mainCg = CodeGraph.initSync(mainRepo);
|
||||
await mainCg.indexAll();
|
||||
|
||||
// Nested worktree that later gains its own index.
|
||||
worktree = path.join(mainRepo, 'wt');
|
||||
git(mainRepo, 'worktree', 'add', '-q', '-b', 'feature', worktree);
|
||||
worktreeCg = CodeGraph.initSync(worktree);
|
||||
await worktreeCg.indexAll();
|
||||
|
||||
handler = new ToolHandler(mainCg);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try { mainCg.destroy(); } catch { /* best effort */ }
|
||||
try { worktreeCg.destroy(); } catch { /* best effort */ }
|
||||
try { git(mainRepo, 'worktree', 'remove', '--force', worktree); } catch { /* best effort */ }
|
||||
fs.rmSync(mainRepo, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('drops the stale "borrowed the main index" warning once the index root flips to the worktree', async () => {
|
||||
// The server runs from inside the worktree, default project = main checkout.
|
||||
handler.setDefaultProjectHint(worktree);
|
||||
|
||||
// Phase 1: the index genuinely belongs to a different working tree (the main
|
||||
// checkout) → warn, and cache that verdict.
|
||||
const before = await handler.execute('codegraph_status', {});
|
||||
expect(before.content[0].text).toContain('different git working tree');
|
||||
expect(before.content[0].text).toContain(real(mainRepo));
|
||||
|
||||
// Phase 2: the worktree's own index is now the server's default project
|
||||
// (engine re-open → setDefaultCodeGraph). The resolved index root for the
|
||||
// SAME start path flipped to the worktree itself, so the verdict must be
|
||||
// recomputed to "no mismatch" — not served stale from before.
|
||||
handler.setDefaultCodeGraph(worktreeCg);
|
||||
|
||||
const after = await handler.execute('codegraph_status', {});
|
||||
expect(after.content[0].text).not.toContain('different git working tree');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user