From 26d6ccece528956597bb3a49c0b0a8711f47fe37 Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Tue, 29 Sep 2026 18:52:32 +0000 Subject: [PATCH] test(mcp): hold the watcher's sync so staleness-banner edits stay pending (#2105) `MCP staleness banner > lists pending files under "Pending sync"` flaked in the full suite (2 of 4 runs): status showed "0 modified" and no Pending sync section. The tests used `debounceMs: 4000` to keep an edit pending while they queried, but since #1397 a lone edit syncs after a 300ms quiet window whatever the configured debounce. codegraph_status awaits a freshly spawned worker to count changes before reading the pending set, and under load that outlasted the window, so the sync had already cleared the entry. A 500ms pause before the query fails the status, banner and #1968 path tests deterministically. Hold the watcher's sync open with a never-settling spy in those four setups instead; no assertion changes, and the test that checks the banner drops after a sync still runs a real one. Co-authored-by: Claude Opus 5.5 --- __tests__/mcp-staleness-banner.test.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/__tests__/mcp-staleness-banner.test.ts b/__tests__/mcp-staleness-banner.test.ts index fb72cdf5e..fdf57e7a9 100644 --- a/__tests__/mcp-staleness-banner.test.ts +++ b/__tests__/mcp-staleness-banner.test.ts @@ -40,6 +40,18 @@ function waitFor(condition: () => boolean, timeoutMs = 2000, intervalMs = 25): P }); } +/** + * Keep the watcher's edits pending for the rest of the test. A long + * `debounceMs` alone does not: a lone edit syncs after a 300ms quiet window + * whatever the configured debounce (#1397), and under full-suite load a tool + * call can outlast that (status spawns a worker to count changes), so the sync + * cleared the entry before the response was built. A sync that never settles + * leaves each entry pending (marked as indexing once it starts). + */ +function holdWatcherSync(cg: CodeGraph): void { + vi.spyOn(cg, 'sync').mockReturnValue(new Promise(() => {})); +} + describe('MCP staleness banner', () => { let testDir: string; let cg: CodeGraph; @@ -92,7 +104,7 @@ describe('MCP staleness banner', () => { }; it('prepends a stale banner when the response references a pending file', async () => { - // Long debounce so the edit lingers in pendingFiles while we query. + holdWatcherSync(cg); cg.watch({ debounceMs: 4000, inertForTests: true }); await cg.waitUntilWatcherReady(); @@ -124,6 +136,7 @@ describe('MCP staleness banner', () => { }); it('uses the footer (not the banner) when pending files are not referenced', async () => { + holdWatcherSync(cg); cg.watch({ debounceMs: 4000, inertForTests: true }); await cg.waitUntilWatcherReady(); @@ -164,6 +177,7 @@ describe('MCP staleness banner', () => { }); it('lists pending files under "Pending sync" in codegraph_status', async () => { + holdWatcherSync(cg); cg.watch({ debounceMs: 4000, inertForTests: true }); await cg.waitUntilWatcherReady(); @@ -244,6 +258,7 @@ describe('MCP staleness banner — matching whole paths (#1968)', () => { cg = CodeGraph.initSync(testDir, { config: { include: ['**/*.ts', '**/*.tsx'], exclude: [] } }); await cg.indexAll(); handler = new ToolHandler(cg); + holdWatcherSync(cg); cg.watch({ debounceMs: 4000, inertForTests: true }); await cg.waitUntilWatcherReady(); });