diff --git a/packages/kap-server/test/search/searchService.test.ts b/packages/kap-server/test/search/searchService.test.ts index 8b1f90bc9..15c7c1b52 100644 --- a/packages/kap-server/test/search/searchService.test.ts +++ b/packages/kap-server/test/search/searchService.test.ts @@ -1191,6 +1191,10 @@ describe('GlobalSearchService', () => { const service = track(makeService(home!, index)); await service.reindex(); expect((await service.search({ query: '苹果' })).items.length).toBe(1); + // The search above kicked a fire-and-forget background pass whose + // session enumeration already ran with block=false: drain it before the + // append, or a CI-slow pass reads the delta below and publishes it early. + await settleSync(service); // New bytes arrive, then the next background pass is blocked inside the // session enumeration. The search must return promptly with the OLD diff --git a/packages/kap-server/test/sessions.test.ts b/packages/kap-server/test/sessions.test.ts index 56d39545a..0df2ba711 100644 --- a/packages/kap-server/test/sessions.test.ts +++ b/packages/kap-server/test/sessions.test.ts @@ -1595,24 +1595,38 @@ describe('server-v2 /api/v1/sessions (minidb read model)', () => { expect(status.body.code).toBe(0); expect(status.body.data.state).toBe('ready'); - // A freshly created session lists, counts, and pages immediately — the - // mutation path never waited for the read model, the read path folds the - // mirror queue back in. + // A freshly created session lists, counts, and pages without waiting for + // the read model — the mutation path never awaited the read model, the + // read path folds the mirror queue back in. That fold is best-effort + // while a mirror flush is in flight: the flush's batch is only per-shard + // atomic and its pending-queue cleanup is not linearized with reads, so a + // read landing exactly inside that window can transiently miss (or + // double-count) the session — poll instead of sampling once. const created = await postJson('/api/v1/sessions', { metadata: { cwd: home as string }, }); const id = created.body.data.id; - const listed = await getJson('/api/v1/sessions'); - expect(listed.body.data.items.some((s) => s.id === id)).toBe(true); + await vi.waitFor( + async () => { + const listed = await getJson('/api/v1/sessions'); + expect(listed.body.data.items.some((s) => s.id === id)).toBe(true); - const workspaces = await getJson<{ items: { session_count: number }[] }>('/api/v1/workspaces'); - expect(workspaces.body.data.items[0]?.session_count).toBe(1); + const workspaces = await getJson<{ items: { session_count: number }[] }>( + '/api/v1/workspaces', + ); + expect(workspaces.body.data.items[0]?.session_count).toBe(1); - const paged = await getJson(`/api/v1/sessions?page_size=1&before_id=${id}`); - expect(paged.body.data.items).toEqual([]); - expect(paged.body.data.has_more).toBe(false); + const paged = await getJson(`/api/v1/sessions?page_size=1&before_id=${id}`); + expect(paged.body.data.items).toEqual([]); + expect(paged.body.data.has_more).toBe(false); + }, + { timeout: 10_000 }, + ); + // Archiving drains the mirror queue before responding, and the restart's + // boot prepare re-projects (or reuses) a fully settled generation — both + // of these reads are deterministic again. await postJson<{ archived: boolean }>(`/api/v1/sessions/${id}:archive`); const archivedOnly = await getJson('/api/v1/sessions?archived_only=true'); expect(archivedOnly.body.data.items.map((s) => s.id)).toEqual([id]); diff --git a/packages/kap-server/test/transcript.test.ts b/packages/kap-server/test/transcript.test.ts index 2cec22368..e0041f2d1 100644 --- a/packages/kap-server/test/transcript.test.ts +++ b/packages/kap-server/test/transcript.test.ts @@ -175,7 +175,11 @@ describe('server-v2 /api/v1/sessions/{sid}/transcript', () => { server = undefined; } if (home !== undefined) { - await rm(home, { recursive: true, force: true }); + // maxRetries: the engine's file log writers flush synchronously on scope + // dispose but their trailing async close can still be creating a file + // under home after server.close() resolves (ENOTEMPTY on a loaded CI + // runner) — same retry pattern as questions.test.ts / fs.test.ts. + await rm(home, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }); home = undefined; } });