From a92844d75b0ab162ca5730141b329c323880e0ff Mon Sep 17 00:00:00 2001 From: AnzoBenjamin Date: Thu, 3 Sep 2026 13:37:23 +0300 Subject: [PATCH 1/2] fix(cli): isolate project-files mocks with scoped spies Replace mock.module with spyOn plus mock.restore in logger, chat-history, and turn-checkpoint tests. Module mocks leak across the shared bun process and misdirected checkpoint paths in CI; scoped spies restore cleanly and keep each test isolated. --- cli/src/utils/__tests__/chat-history.test.ts | 19 +++++-- cli/src/utils/__tests__/logger.test.ts | 52 +++++++------------ .../utils/__tests__/turn-checkpoint.test.ts | 5 +- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/cli/src/utils/__tests__/chat-history.test.ts b/cli/src/utils/__tests__/chat-history.test.ts index f4acbf894b..4f4d5f3fc8 100644 --- a/cli/src/utils/__tests__/chat-history.test.ts +++ b/cli/src/utils/__tests__/chat-history.test.ts @@ -1,13 +1,22 @@ -import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test' +import { + describe, + test, + expect, + beforeEach, + afterEach, + mock, + spyOn, +} from 'bun:test' import * as fs from 'fs' import * as os from 'os' import * as path from 'path' +import * as projectFiles from '../../project-files' + let tempDataDir = '' -mock.module('../../project-files', () => ({ - getProjectDataDir: () => tempDataDir, -})) +// spyOn (restored after each test) instead of mock.module: a module mock +// leaks into every other test file in the same bun process. import { deleteChatSession, getAllChats } from '../chat-history' @@ -31,9 +40,11 @@ function writeChat(chatId: string, prompt: string) { describe('chat-history', () => { beforeEach(() => { tempDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codebuff-history-')) + spyOn(projectFiles, 'getProjectDataDir').mockReturnValue(tempDataDir) }) afterEach(() => { + mock.restore() fs.rmSync(tempDataDir, { recursive: true, force: true }) }) diff --git a/cli/src/utils/__tests__/logger.test.ts b/cli/src/utils/__tests__/logger.test.ts index c9f30eff7b..ccadee52b2 100644 --- a/cli/src/utils/__tests__/logger.test.ts +++ b/cli/src/utils/__tests__/logger.test.ts @@ -2,42 +2,26 @@ import fs from 'node:fs' import os from 'node:os' import path from 'node:path' -import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' -import { setProjectRootResolver } from '@codebuff/common/util/plan-artifacts' +import { + afterEach, + beforeEach, + describe, + expect, + mock, + spyOn, + test, +} from 'bun:test' + +import * as projectFiles from '../../project-files' -let mockProjectRoot = '' let mockChatDir = '' -// Full stub: do not import.meta.require('../../project-files') inside this -// mock factory (bun deadlocks on a self-require). setProjectRoot still wires -// the plan-artifact resolver so command-args / plan-timeline in the same -// process keep working. -mock.module('../../project-files', () => ({ - setProjectRoot: (dir: string) => { - mockProjectRoot = dir - setProjectRootResolver(() => mockProjectRoot) - return dir - }, - getProjectRoot: () => { - if (!mockProjectRoot) { - throw new Error('Project root not set') - } - return mockProjectRoot - }, - getCurrentChatDir: () => { - if (mockChatDir) return mockChatDir - if (!mockProjectRoot) { - throw new Error('Project root not set') - } - return path.join(mockProjectRoot, 'chat') - }, - getProjectDataDir: () => mockProjectRoot, - getProjectStorageKey: (root: string) => path.basename(root) || 'project', - getCurrentChatId: () => 'logger-test-chat', - setCurrentChatId: (chatId: string) => chatId, - startNewChat: () => 'logger-test-chat', - getMostRecentChatDir: () => null, -})) +// Spy on the real project-files module instead of mock.module: a module mock +// replaces the module for every other test file in the same bun process, +// which sent turn-checkpoint.test.ts's checkpoint path into this file's +// `/chat` stub in CI. Spies restore cleanly via mock.restore() in +// teardown. setProjectRoot still wires the plan-artifact resolver so +// command-args / plan-timeline in the same process keep working. import { setProjectRoot } from '../../project-files' import { @@ -61,10 +45,12 @@ function setupLoggerTempDir() { mockChatDir = path.join(tempDir, 'chat') fs.mkdirSync(mockChatDir, { recursive: true }) setProjectRoot(tempDir) + spyOn(projectFiles, 'getCurrentChatDir').mockReturnValue(mockChatDir) } function teardownLoggerTempDir() { clearLogFile() + mock.restore() mockChatDir = '' fs.rmSync(tempDir, { recursive: true, force: true }) } diff --git a/cli/src/utils/__tests__/turn-checkpoint.test.ts b/cli/src/utils/__tests__/turn-checkpoint.test.ts index 49987c426d..f209ee6205 100644 --- a/cli/src/utils/__tests__/turn-checkpoint.test.ts +++ b/cli/src/utils/__tests__/turn-checkpoint.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect, beforeEach, afterEach } from 'bun:test' +import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test' import * as fs from 'fs' import * as path from 'path' import * as os from 'os' @@ -47,6 +47,9 @@ function makeAgentState(agentId: string): AgentState { describe('turn checkpoint (P2-3)', () => { beforeEach(() => { + // Defensive: drop any mock/spy leaked by another test file in the same + // bun process so the real project-files resolution below is authoritative. + mock.restore() if (fs.existsSync(tmpProjectRoot)) { fs.rmSync(tmpProjectRoot, { recursive: true, force: true }) } From d5b715e51f56934bec87f665fcfe203d89b06906 Mon Sep 17 00:00:00 2001 From: AnzoBenjamin Date: Thu, 3 Sep 2026 13:43:00 +0300 Subject: [PATCH 2/2] docs(knowledge): document test-isolation mock pattern Record scoped spyOn plus mock.restore usage for project-files stubs so future test files avoid cross-file mock leakage in the shared bun process. --- cli/knowledge.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/knowledge.md b/cli/knowledge.md index b12174ab66..bbc3a44568 100644 --- a/cli/knowledge.md +++ b/cli/knowledge.md @@ -904,6 +904,8 @@ Streaming markdown renders as plain text until the message or agent finishes. Th - _Knowledge refresh 2026-09-03: agent-branch status display (`cli/src/components/blocks/agent-branch-wrapper.tsx`), status-label/chip utilities (`cli/src/utils/status-label.ts`, `cli/src/utils/status-bar-chips.ts`), and code-search summary rendering (`cli/src/utils/code-search-summary.ts`) changed alongside the code-searcher removal; regenerated agent type sources kept in sync._ +- _Knowledge refresh 2026-09-03 (test isolation): `cli/src/utils/__tests__/logger.test.ts` and `chat-history.test.ts` stub `project-files` with `spyOn` plus `mock.restore()` instead of `mock.module`, which leaks across every test file in the same bun process and misdirected `turn-checkpoint.test.ts` checkpoint paths in CI; `turn-checkpoint.test.ts` also restores defensively in `beforeEach`._ + - `cli/src/components/renderers/compaction-box.tsx` derives the pending/interrupted/unsettled triple in one `derivePresentation` helper that both `deriveTone` and the render path consume, so the chosen tone and the rendered lines cannot drift. A `status: 'pending'` block is only presented as live when `isLiveCompaction` confirms it belongs to THIS process (matching `liveSessionId`); a replayed pending block from a persisted transcript renders as "Interrupted before this pass reported a result." rather than a permanently spinning "Compacting context…" card. `cli/src/utils/sdk-event-handlers.ts` consumes the additive `context_compaction_status` event and pairs `started`/`settled` strictly by the event's required `runId` — never by `agentId`, which subagent forwarding rewrites — so a nested agent loop's settle cannot clear the root turn's live card; `handleFinish` rewrites any stray pending block as interrupted so an aborted turn leaves an honest terminal record. - _Knowledge refresh 2026-08-23: add `/memory` (alias `/mem`) slash command; staleness guard touch._