Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cli/knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down
19 changes: 15 additions & 4 deletions cli/src/utils/__tests__/chat-history.test.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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 })
})

Expand Down
52 changes: 19 additions & 33 deletions cli/src/utils/__tests__/logger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
// `<root>/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 {
Expand All @@ -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 })
}
Expand Down
5 changes: 4 additions & 1 deletion cli/src/utils/__tests__/turn-checkpoint.test.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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 })
}
Expand Down
Loading