From 53eeac3f31ee2218ad54c4c8b62d171a8045409a Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 3 Apr 2026 23:06:35 +0900 Subject: [PATCH] fix(ci): simplify test runner to plain `bun test` by fixing mock.module() leakage - Add afterAll(() => { mock.restore() }) to 52 test files missing cleanup - Rewrite create-tool-guard-hooks.test.ts to use spyOn instead of barrel mock - Fix skill-mcp-manager OAuth tests with missing mockTokens/mockLogin definitions - Fix start-work hook: show worktree active block on resume with existing worktree_path - Extract createWorktreeActiveBlock to worktree-block.ts to avoid circular import - Replace 80-line isolated test runner CI config with single `bun test` command --- .github/workflows/ci.yml | 82 +------------------ src/cli/doctor/checks/system.test.ts | 6 +- src/cli/mcp-oauth/login.test.ts | 6 +- src/cli/run/integration.test.ts | 1 + src/cli/run/server-connection.test.ts | 1 + src/features/background-agent/manager.test.ts | 4 +- .../session-status-classifier.test.ts | 4 +- .../skill-mcp-manager/connection-race.test.ts | 4 +- .../skill-mcp-manager/manager.test.ts | 30 ++----- src/features/tmux-subagent/manager.test.ts | 4 +- .../tmux-subagent/zombie-pane.test.ts | 4 +- .../empty-content-recovery-sdk.test.ts | 6 +- .../recovery-deduplication.test.ts | 1 + .../storage.test.ts | 1 + .../atlas/compaction-agent-filter.test.ts | 4 +- ...inal-wave-approval-gate-regression.test.ts | 4 +- .../atlas/final-wave-approval-gate.test.ts | 4 +- src/hooks/atlas/index.test.ts | 4 +- .../atlas/session-last-agent.sqlite.test.ts | 4 +- ...ol-execute-after-background-launch.test.ts | 4 +- .../auto-slash-command-leak.test.ts | 6 +- .../executor-resolution.test.ts | 6 +- src/hooks/auto-update-checker/cache.test.ts | 6 +- .../checker/sync-package-json.test.ts | 6 +- src/hooks/auto-update-checker/hook.test.ts | 6 +- .../hook/background-update-check.test.ts | 6 +- .../hook/workspace-resolution.test.ts | 6 +- .../session-event-handler-retry.test.ts | 4 +- .../tool-execute-after-handler.test.ts | 4 +- src/hooks/claude-code-hooks/stop.test.ts | 4 +- src/hooks/comment-checker/cli.test.ts | 4 +- .../comment-checker/hook.apply-patch.test.ts | 4 +- .../compaction-context-injector/index.test.ts | 6 +- .../compaction-todo-preserver/index.test.ts | 1 + .../injector.test.ts | 6 +- .../injector.test.ts | 6 +- src/hooks/model-fallback/hook.test.ts | 6 +- src/hooks/openclaw.test.ts | 6 +- ...ive-compaction.context-limit-cache.test.ts | 4 +- ...ive-compaction.degradation-monitor.test.ts | 4 +- src/hooks/preemptive-compaction.test.ts | 6 +- src/hooks/prometheus-md-only/index.test.ts | 6 +- src/hooks/runtime-fallback/dispose.test.ts | 6 +- .../recover-tool-result-missing.test.ts | 6 +- src/hooks/start-work/context-info-builder.ts | 5 +- src/hooks/start-work/start-work-hook.ts | 13 +-- src/hooks/start-work/worktree-block.ts | 11 +++ src/plugin/event.model-fallback.test.ts | 4 +- .../fallback.cliproxyapi-matrix.test.ts | 4 +- .../hooks/create-tool-guard-hooks.test.ts | 48 ++++------- .../migrate-legacy-plugin-entry.test.ts | 6 +- src/shared/model-error-classifier.test.ts | 4 +- src/shared/opencode-message-dir.test.ts | 2 + src/tools/lsp/client.test.ts | 4 +- src/tools/session-manager/storage.test.ts | 5 +- 55 files changed, 219 insertions(+), 190 deletions(-) create mode 100644 src/hooks/start-work/worktree-block.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97c6fa0b6..84b1a2c93 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,86 +44,8 @@ jobs: env: BUN_INSTALL_ALLOW_SCRIPTS: "@ast-grep/napi" - - name: Run mock-heavy tests (isolated) - run: | - # These files use mock.module() which pollutes module cache - # Run them in separate processes to prevent cross-file contamination - bun test src/plugin-handlers - bun test src/hooks/atlas - bun test src/hooks/compaction-context-injector - bun test src/features/tmux-subagent - bun test src/cli/doctor/formatter.test.ts - bun test src/cli/doctor/format-default.test.ts - bun test src/tools/call-omo-agent/sync-executor.test.ts - bun test src/tools/call-omo-agent/session-creator.test.ts - bun test src/tools/session-manager - bun test src/features/opencode-skill-loader/loader.test.ts - bun test src/hooks/anthropic-context-window-limit-recovery/recovery-hook.test.ts - bun test src/hooks/anthropic-context-window-limit-recovery/executor.test.ts - # src/shared mock-heavy files (mock.module pollutes connected-providers-cache and legacy-plugin-warning) - bun test src/shared/model-capabilities.test.ts - bun test src/shared/log-legacy-plugin-startup-warning.test.ts - bun test src/shared/model-error-classifier.test.ts - bun test src/shared/opencode-message-dir.test.ts - # session-recovery mock isolation (recover-tool-result-missing mocks ./storage) - bun test src/hooks/session-recovery/recover-tool-result-missing.test.ts - # legacy-plugin-toast mock isolation (hook.test.ts mocks ./auto-migrate) - bun test src/hooks/legacy-plugin-toast/hook.test.ts - # src/plugin - ALL isolated (mock.module pollution crosses between files) - for f in $(find src/plugin -name '*.test.ts' | sort); do bun test "$f"; done - # src/features/background-agent - ALL isolated (mock.module pollution) - for f in $(find src/features/background-agent -name '*.test.ts' | sort); do bun test "$f"; done - - - name: Run remaining tests - run: | - # Enumerate subdirectories/files explicitly to EXCLUDE mock-heavy files - # that were already run in isolation above. - # Excluded from src/shared: model-capabilities, log-legacy-plugin-startup-warning, model-error-classifier, opencode-message-dir - # Excluded from src/cli: doctor/formatter.test.ts, doctor/format-default.test.ts - # Excluded from src/tools: call-omo-agent/sync-executor.test.ts, call-omo-agent/session-creator.test.ts, session-manager (all) - # Excluded from src/hooks/anthropic-context-window-limit-recovery: recovery-hook.test.ts, executor.test.ts - # Excluded: src/plugin/* (all run isolated above) - # Excluded: src/features/background-agent/* (all run isolated above) - # Build src/shared file list excluding mock-heavy files already run in isolation - SHARED_FILES=$(find src/shared -name '*.test.ts' \ - ! -name 'model-capabilities.test.ts' \ - ! -name 'log-legacy-plugin-startup-warning.test.ts' \ - ! -name 'model-error-classifier.test.ts' \ - ! -name 'opencode-message-dir.test.ts' \ - | sort | tr '\n' ' ') - # plugin and background-agent fully isolated above — excluded from remaining - bun test bin script src/config src/mcp src/index.test.ts \ - src/agents $SHARED_FILES \ - src/cli/run src/cli/config-manager src/cli/mcp-oauth \ - src/cli/index.test.ts src/cli/install.test.ts src/cli/model-fallback.test.ts \ - src/cli/config-manager.test.ts \ - src/cli/doctor/runner.test.ts src/cli/doctor/checks \ - src/tools/ast-grep src/tools/background-task src/tools/delegate-task \ - src/tools/glob src/tools/grep src/tools/interactive-bash \ - src/tools/look-at src/tools/lsp \ - src/tools/skill src/tools/skill-mcp src/tools/slashcommand src/tools/task \ - src/tools/call-omo-agent/background-agent-executor.test.ts \ - src/tools/call-omo-agent/background-executor.test.ts \ - src/tools/call-omo-agent/subagent-session-creator.test.ts \ - src/hooks/anthropic-context-window-limit-recovery/empty-content-recovery-sdk.test.ts src/hooks/anthropic-context-window-limit-recovery/parser.test.ts src/hooks/anthropic-context-window-limit-recovery/pruning-deduplication.test.ts src/hooks/anthropic-context-window-limit-recovery/recovery-deduplication.test.ts src/hooks/anthropic-context-window-limit-recovery/storage.test.ts \ - src/hooks/session-recovery/detect-error-type.test.ts src/hooks/session-recovery/index.test.ts src/hooks/session-recovery/recover-empty-content-message-sdk.test.ts src/hooks/session-recovery/resume.test.ts src/hooks/session-recovery/storage \ - src/hooks/legacy-plugin-toast/auto-migrate.test.ts \ - src/hooks/claude-code-compatibility \ - src/hooks/context-injection \ - src/hooks/provider-toast \ - src/hooks/session-notification \ - src/hooks/sisyphus \ - src/hooks/todo-continuation-enforcer \ - src/features/builtin-commands \ - src/features/builtin-skills \ - src/features/claude-code-session-state \ - src/features/hook-message-injector \ - src/features/opencode-skill-loader/config-source-discovery.test.ts \ - src/features/opencode-skill-loader/merger.test.ts \ - src/features/opencode-skill-loader/skill-content.test.ts \ - src/features/opencode-skill-loader/blocking.test.ts \ - src/features/opencode-skill-loader/async-loader.test.ts \ - src/features/skill-mcp-manager + - name: Run tests + run: bun test typecheck: runs-on: ubuntu-latest diff --git a/src/cli/doctor/checks/system.test.ts b/src/cli/doctor/checks/system.test.ts index 163031f13..c0a5ef177 100644 --- a/src/cli/doctor/checks/system.test.ts +++ b/src/cli/doctor/checks/system.test.ts @@ -1,6 +1,6 @@ /// -import { beforeEach, describe, expect, it, mock } from "bun:test" +import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test" import { PLUGIN_NAME } from "../../../shared" import type { PluginInfo } from "./system-plugin" @@ -47,6 +47,10 @@ mock.module("./system-loaded-version", () => ({ getSuggestedInstallTag: mockGetSuggestedInstallTag, })) +afterAll(() => { + mock.restore() +}) + describe("system check", () => { beforeEach(() => { mockFindOpenCodeBinary.mockReset() diff --git a/src/cli/mcp-oauth/login.test.ts b/src/cli/mcp-oauth/login.test.ts index 917652f76..12925b7fe 100644 --- a/src/cli/mcp-oauth/login.test.ts +++ b/src/cli/mcp-oauth/login.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test" +import { afterAll, describe, it, expect, beforeEach, afterEach, mock } from "bun:test" const mockLogin = mock(() => Promise.resolve({ accessToken: "test-token", expiresAt: 1710000000 })) @@ -11,6 +11,10 @@ mock.module("../../features/mcp-oauth/provider", () => ({ }, })) +afterAll(() => { + mock.restore() +}) + const { login } = await import("./login") describe("login command", () => { diff --git a/src/cli/run/integration.test.ts b/src/cli/run/integration.test.ts index 372c9249a..c2b019e62 100644 --- a/src/cli/run/integration.test.ts +++ b/src/cli/run/integration.test.ts @@ -33,6 +33,7 @@ mock.module("../../shared/port-utils", () => ({ afterAll(() => { mock.module("@opencode-ai/sdk", () => originalSdk) mock.module("../../shared/port-utils", () => originalPortUtils) + mock.restore() }) const { createServerConnection } = await import("./server-connection") diff --git a/src/cli/run/server-connection.test.ts b/src/cli/run/server-connection.test.ts index 110f9c00d..90bad1812 100644 --- a/src/cli/run/server-connection.test.ts +++ b/src/cli/run/server-connection.test.ts @@ -38,6 +38,7 @@ afterAll(() => { mock.module("@opencode-ai/sdk", () => originalSdk) mock.module("../../shared/port-utils", () => originalPortUtils) mock.module("./opencode-binary-resolver", () => originalBinaryResolver) + mock.restore() }) const { createServerConnection } = await import("./server-connection") diff --git a/src/features/background-agent/manager.test.ts b/src/features/background-agent/manager.test.ts index 269886af2..db5635d33 100644 --- a/src/features/background-agent/manager.test.ts +++ b/src/features/background-agent/manager.test.ts @@ -1,5 +1,5 @@ declare const require: (name: string) => any -const { describe, test, expect, beforeEach, afterEach, spyOn, mock } = require("bun:test") +const { describe, test, expect, beforeEach, afterEach, afterAll, spyOn, mock } = require("bun:test") mock.module("../../shared/connected-providers-cache", () => ({ readConnectedProvidersCache: () => null, @@ -10,6 +10,8 @@ mock.module("../../shared/connected-providers-cache", () => ({ updateConnectedProvidersCache: () => {}, })) +afterAll(() => { mock.restore() }) + import { getSessionPromptParams, clearSessionPromptParams } from "../../shared/session-prompt-params-state" import { tmpdir } from "node:os" import type { PluginInput } from "@opencode-ai/plugin" diff --git a/src/features/background-agent/session-status-classifier.test.ts b/src/features/background-agent/session-status-classifier.test.ts index 5a0244748..45cc394e2 100644 --- a/src/features/background-agent/session-status-classifier.test.ts +++ b/src/features/background-agent/session-status-classifier.test.ts @@ -1,9 +1,11 @@ -import { describe, test, expect, mock } from "bun:test" +import { describe, test, expect, mock, afterAll } from "bun:test" import { isActiveSessionStatus, isTerminalSessionStatus } from "./session-status-classifier" const mockLog = mock() mock.module("../../shared", () => ({ log: mockLog })) +afterAll(() => { mock.restore() }) + describe("isActiveSessionStatus", () => { describe("#given a known active session status", () => { test('#when type is "busy" #then returns true', () => { diff --git a/src/features/skill-mcp-manager/connection-race.test.ts b/src/features/skill-mcp-manager/connection-race.test.ts index 10e3c6836..3fa00b4c3 100644 --- a/src/features/skill-mcp-manager/connection-race.test.ts +++ b/src/features/skill-mcp-manager/connection-race.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test" +import { afterEach, beforeEach, describe, expect, it, mock, afterAll } from "bun:test" import type { ClaudeCodeMcpServer } from "../claude-code-mcp-loader/types" import type { SkillMcpClientInfo, SkillMcpManagerState } from "./types" @@ -47,6 +47,8 @@ mock.module("@modelcontextprotocol/sdk/client/stdio.js", () => ({ StdioClientTransport: MockStdioClientTransport, })) +afterAll(() => { mock.restore() }) + const { disconnectAll, disconnectSession } = await import("./cleanup") const { getOrCreateClient } = await import("./connection") diff --git a/src/features/skill-mcp-manager/manager.test.ts b/src/features/skill-mcp-manager/manager.test.ts index f65aa5c55..5a3525a0b 100644 --- a/src/features/skill-mcp-manager/manager.test.ts +++ b/src/features/skill-mcp-manager/manager.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test" +import { describe, it, expect, beforeEach, afterEach, afterAll, mock, spyOn } from "bun:test" import { SkillMcpManager } from "./manager" import type { SkillMcpClientInfo, SkillMcpServerContext } from "./types" import type { ClaudeCodeMcpServer } from "../claude-code-mcp-loader/types" @@ -22,33 +22,19 @@ mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ }, })) -const mockTokens = mock(() => null as { accessToken: string; refreshToken?: string; expiresAt?: number } | null) -const mockLogin = mock(() => Promise.resolve({ accessToken: "new-token" })) +// Mock OAuth provider for OAuth integration tests +const mockTokens = mock(() => null as { accessToken: string } | null) +const mockLogin = mock(() => Promise.resolve({ accessToken: "test-token" }) as Promise<{ accessToken: string } | null>) mock.module("../mcp-oauth/provider", () => ({ McpOAuthProvider: class MockMcpOAuthProvider { - constructor(public options: { serverUrl: string; clientId?: string; scopes?: string[] }) {} - tokens() { - return mockTokens() - } - async login() { - return mockLogin() - } + tokens = mockTokens + login = mockLogin + constructor(_opts: unknown) {} }, })) - - - - - - - - - - - - +afterAll(() => { mock.restore() }) describe("SkillMcpManager", () => { let manager: SkillMcpManager diff --git a/src/features/tmux-subagent/manager.test.ts b/src/features/tmux-subagent/manager.test.ts index e2052dc28..f644033ad 100644 --- a/src/features/tmux-subagent/manager.test.ts +++ b/src/features/tmux-subagent/manager.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect, mock, beforeEach, spyOn } from 'bun:test' +import { describe, test, expect, mock, beforeEach, spyOn, afterAll } from 'bun:test' import type { TmuxConfig } from '../../config/schema' import type { WindowState, PaneAction } from './types' import type { ActionResult, ExecuteContext } from './action-executor' @@ -77,6 +77,8 @@ mock.module('./pane-state-querier', () => ({ : null, })) +afterAll(() => { mock.restore() }) + mock.module('./action-executor', () => ({ executeActions: mockExecuteActions, executeAction: mockExecuteAction, diff --git a/src/features/tmux-subagent/zombie-pane.test.ts b/src/features/tmux-subagent/zombie-pane.test.ts index 932f7e9c5..42fcfb760 100644 --- a/src/features/tmux-subagent/zombie-pane.test.ts +++ b/src/features/tmux-subagent/zombie-pane.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, mock, test } from "bun:test" +import { beforeEach, describe, expect, mock, test, afterAll } from "bun:test" import type { TmuxConfig } from "../../config/schema" import type { ActionResult, ExecuteContext, ExecuteActionsResult } from "./action-executor" import type { TmuxUtilDeps } from "./manager" @@ -46,6 +46,8 @@ mock.module("../../shared/tmux", () => ({ SESSION_MISSING_GRACE_MS: 1_000, })) +afterAll(() => { mock.restore() }) + const mockTmuxDeps: TmuxUtilDeps = { isInsideTmux: mockIsInsideTmux, getCurrentPaneId: mockGetCurrentPaneId, diff --git a/src/hooks/anthropic-context-window-limit-recovery/empty-content-recovery-sdk.test.ts b/src/hooks/anthropic-context-window-limit-recovery/empty-content-recovery-sdk.test.ts index e7d0e8ee8..430113df7 100644 --- a/src/hooks/anthropic-context-window-limit-recovery/empty-content-recovery-sdk.test.ts +++ b/src/hooks/anthropic-context-window-limit-recovery/empty-content-recovery-sdk.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, mock, beforeEach } from "bun:test" +import { afterAll, describe, it, expect, mock, beforeEach } from "bun:test" import { fixEmptyMessagesWithSDK } from "./empty-content-recovery-sdk" const mockReplaceEmptyTextParts = mock(() => Promise.resolve(false)) @@ -11,6 +11,10 @@ mock.module("../session-recovery/storage/text-part-injector", () => ({ injectTextPartAsync: mockInjectTextPart, })) +afterAll(() => { + mock.restore() +}) + function createMockClient(messages: Array<{ info?: { id?: string }; parts?: Array<{ type?: string; text?: string }> }>) { return { session: { diff --git a/src/hooks/anthropic-context-window-limit-recovery/recovery-deduplication.test.ts b/src/hooks/anthropic-context-window-limit-recovery/recovery-deduplication.test.ts index d7541139c..68f23b3b0 100644 --- a/src/hooks/anthropic-context-window-limit-recovery/recovery-deduplication.test.ts +++ b/src/hooks/anthropic-context-window-limit-recovery/recovery-deduplication.test.ts @@ -11,6 +11,7 @@ mock.module("./deduplication-recovery", () => ({ afterAll(() => { mock.module("./deduplication-recovery", () => originalDeduplicationRecovery) + mock.restore() }) function createImmediateTimeouts(): () => void { diff --git a/src/hooks/anthropic-context-window-limit-recovery/storage.test.ts b/src/hooks/anthropic-context-window-limit-recovery/storage.test.ts index 407fc64bf..d884074d9 100644 --- a/src/hooks/anthropic-context-window-limit-recovery/storage.test.ts +++ b/src/hooks/anthropic-context-window-limit-recovery/storage.test.ts @@ -13,6 +13,7 @@ mock.module("./storage", () => { afterAll(() => { mock.module("./storage", () => storage) + mock.restore() }) describe("truncateUntilTargetTokens", () => { diff --git a/src/hooks/atlas/compaction-agent-filter.test.ts b/src/hooks/atlas/compaction-agent-filter.test.ts index 7dfbe0d92..790518e6c 100644 --- a/src/hooks/atlas/compaction-agent-filter.test.ts +++ b/src/hooks/atlas/compaction-agent-filter.test.ts @@ -1,5 +1,5 @@ declare const require: (name: string) => any -const { afterEach, beforeEach, describe, expect, mock, test } = require("bun:test") +const { afterEach, beforeEach, describe, expect, mock, test, afterAll } = require("bun:test") import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs" import { tmpdir } from "node:os" import { join } from "node:path" @@ -30,6 +30,8 @@ mock.module("../../shared/opencode-storage-detection", () => ({ isSqliteBackend: () => false, })) +afterAll(() => { mock.restore() }) + const { createAtlasHook } = await import("./index") describe("atlas hook compaction agent filtering", () => { diff --git a/src/hooks/atlas/final-wave-approval-gate-regression.test.ts b/src/hooks/atlas/final-wave-approval-gate-regression.test.ts index ab509d828..180ab7cef 100644 --- a/src/hooks/atlas/final-wave-approval-gate-regression.test.ts +++ b/src/hooks/atlas/final-wave-approval-gate-regression.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test" +import { afterEach, beforeEach, describe, expect, mock, test, afterAll } from "bun:test" import { randomUUID } from "node:crypto" import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs" import { tmpdir } from "node:os" @@ -29,6 +29,8 @@ mock.module("../../shared/opencode-storage-detection", () => ({ isSqliteBackend: () => false, })) +afterAll(() => { mock.restore() }) + const { createAtlasHook } = await import("./index") const { MESSAGE_STORAGE } = await import("../../features/hook-message-injector") diff --git a/src/hooks/atlas/final-wave-approval-gate.test.ts b/src/hooks/atlas/final-wave-approval-gate.test.ts index 5c0e44492..717f66016 100644 --- a/src/hooks/atlas/final-wave-approval-gate.test.ts +++ b/src/hooks/atlas/final-wave-approval-gate.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test" +import { afterEach, beforeEach, describe, expect, mock, test, afterAll } from "bun:test" import { randomUUID } from "node:crypto" import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs" import { tmpdir } from "node:os" @@ -29,6 +29,8 @@ mock.module("../../shared/opencode-storage-detection", () => ({ isSqliteBackend: () => false, })) +afterAll(() => { mock.restore() }) + const { createAtlasHook } = await import("./index") const { MESSAGE_STORAGE } = await import("../../features/hook-message-injector") diff --git a/src/hooks/atlas/index.test.ts b/src/hooks/atlas/index.test.ts index 458853915..9f10b1011 100644 --- a/src/hooks/atlas/index.test.ts +++ b/src/hooks/atlas/index.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test" +import { describe, expect, test, beforeEach, afterEach, mock, afterAll } from "bun:test" import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs" import { join } from "node:path" import { tmpdir } from "node:os" @@ -33,6 +33,8 @@ mock.module("../../shared/opencode-storage-detection", () => ({ isSqliteBackend: () => false, })) +afterAll(() => { mock.restore() }) + const { createAtlasHook } = await import("./index") const { createToolExecuteAfterHandler } = await import("./tool-execute-after") const { createToolExecuteBeforeHandler } = await import("./tool-execute-before") diff --git a/src/hooks/atlas/session-last-agent.sqlite.test.ts b/src/hooks/atlas/session-last-agent.sqlite.test.ts index 036482db5..074d17cc7 100644 --- a/src/hooks/atlas/session-last-agent.sqlite.test.ts +++ b/src/hooks/atlas/session-last-agent.sqlite.test.ts @@ -1,4 +1,4 @@ -const { describe, expect, mock, test } = require("bun:test") +const { describe, expect, mock, test, afterAll } = require("bun:test") mock.module("../../shared/opencode-message-dir", () => ({ getMessageDir: () => null, @@ -12,6 +12,8 @@ mock.module("../../shared/normalize-sdk-response", () => ({ normalizeSDKResponse: (response: { data?: TData }, fallback: TData): TData => response.data ?? fallback, })) +afterAll(() => { mock.restore() }) + const { getLastAgentFromSession } = await import("./session-last-agent") function createMockClient(messages: Array<{ info?: { agent?: string } }>) { diff --git a/src/hooks/atlas/tool-execute-after-background-launch.test.ts b/src/hooks/atlas/tool-execute-after-background-launch.test.ts index 9ed37f73c..0a1182c62 100644 --- a/src/hooks/atlas/tool-execute-after-background-launch.test.ts +++ b/src/hooks/atlas/tool-execute-after-background-launch.test.ts @@ -1,6 +1,6 @@ /// -import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test" +import { afterEach, beforeEach, describe, expect, it, mock, afterAll } from "bun:test" import { existsSync, mkdirSync, rmSync } from "node:fs" import { tmpdir } from "node:os" import { join } from "node:path" @@ -23,6 +23,8 @@ mock.module("../../shared/git-worktree", () => ({ formatFileChanges: mock(() => "No file changes"), })) +afterAll(() => { mock.restore() }) + const { createToolExecuteAfterHandler } = await import("./tool-execute-after") describe("createToolExecuteAfterHandler background launch detection", () => { diff --git a/src/hooks/auto-slash-command/auto-slash-command-leak.test.ts b/src/hooks/auto-slash-command/auto-slash-command-leak.test.ts index d402d9466..894981afa 100644 --- a/src/hooks/auto-slash-command/auto-slash-command-leak.test.ts +++ b/src/hooks/auto-slash-command/auto-slash-command-leak.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, mock, spyOn } from "bun:test" +import { afterAll, beforeEach, describe, expect, it, mock, spyOn } from "bun:test" import { AUTO_SLASH_COMMAND_TAG_OPEN } from "./constants" import type { AutoSlashCommandHookInput, @@ -19,6 +19,10 @@ mock.module("./executor", () => ({ executeSlashCommand: executeSlashCommandMock, })) +afterAll(() => { + mock.restore() +}) + const logMock = spyOn(shared, "log").mockImplementation(() => {}) const { createAutoSlashCommandHook } = await import("./hook") diff --git a/src/hooks/auto-slash-command/executor-resolution.test.ts b/src/hooks/auto-slash-command/executor-resolution.test.ts index 70956546f..5fd8df584 100644 --- a/src/hooks/auto-slash-command/executor-resolution.test.ts +++ b/src/hooks/auto-slash-command/executor-resolution.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, mock } from "bun:test" +import { afterAll, describe, expect, it, mock } from "bun:test" import type { LoadedSkill } from "../../features/opencode-skill-loader" mock.module("../../shared", () => ({ @@ -27,6 +27,10 @@ mock.module("../../features/opencode-skill-loader", () => ({ discoverAllSkills: async (): Promise => [], })) +afterAll(() => { + mock.restore() +}) + const { executeSlashCommand } = await import("./executor") function createRestrictedSkill(): LoadedSkill { diff --git a/src/hooks/auto-update-checker/cache.test.ts b/src/hooks/auto-update-checker/cache.test.ts index 4e7e9ba49..371aef936 100644 --- a/src/hooks/auto-update-checker/cache.test.ts +++ b/src/hooks/auto-update-checker/cache.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test" +import { afterAll, afterEach, beforeEach, describe, expect, it, mock } from "bun:test" import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs" import { join } from "node:path" @@ -16,6 +16,10 @@ mock.module("../../shared/logger", () => ({ log: () => {}, })) +afterAll(() => { + mock.restore() +}) + function resetTestCache(): void { if (existsSync(TEST_CACHE_DIR)) { rmSync(TEST_CACHE_DIR, { recursive: true, force: true }) diff --git a/src/hooks/auto-update-checker/checker/sync-package-json.test.ts b/src/hooks/auto-update-checker/checker/sync-package-json.test.ts index c83774810..b808699d3 100644 --- a/src/hooks/auto-update-checker/checker/sync-package-json.test.ts +++ b/src/hooks/auto-update-checker/checker/sync-package-json.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test" +import { afterAll, afterEach, beforeEach, describe, expect, it, mock } from "bun:test" import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs" import { join } from "node:path" import type { PluginEntryInfo } from "./plugin-entry" @@ -22,6 +22,10 @@ mock.module("../../../shared/logger", () => ({ log: () => {}, })) +afterAll(() => { + mock.restore() +}) + function resetTestCache(currentVersion = "3.10.0"): void { if (existsSync(TEST_CACHE_DIR)) { rmSync(TEST_CACHE_DIR, { recursive: true, force: true }) diff --git a/src/hooks/auto-update-checker/hook.test.ts b/src/hooks/auto-update-checker/hook.test.ts index 6f2f06e2e..5f5e54218 100644 --- a/src/hooks/auto-update-checker/hook.test.ts +++ b/src/hooks/auto-update-checker/hook.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test" +import { afterAll, afterEach, beforeEach, describe, expect, it, mock } from "bun:test" const mockShowConfigErrorsIfAny = mock(async () => {}) const mockShowModelCacheWarningIfNeeded = mock(async () => {}) @@ -45,6 +45,10 @@ mock.module("../../shared/logger", () => ({ log: () => {}, })) +afterAll(() => { + mock.restore() +}) + type HookFactory = typeof import("./hook").createAutoUpdateCheckerHook async function importFreshHookFactory(): Promise { diff --git a/src/hooks/auto-update-checker/hook/background-update-check.test.ts b/src/hooks/auto-update-checker/hook/background-update-check.test.ts index 1033d7854..9ba424d0f 100644 --- a/src/hooks/auto-update-checker/hook/background-update-check.test.ts +++ b/src/hooks/auto-update-checker/hook/background-update-check.test.ts @@ -1,5 +1,5 @@ import type { PluginInput } from "@opencode-ai/plugin" -import { beforeEach, describe, expect, it, mock } from "bun:test" +import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test" type PluginEntry = { entry: string @@ -51,6 +51,10 @@ mock.module("./update-toasts", () => ({ })) mock.module("../../../shared/logger", () => ({ log: () => {} })) +afterAll(() => { + mock.restore() +}) + const modulePath = "./background-update-check?test" const { runBackgroundUpdateCheck } = await import(modulePath) diff --git a/src/hooks/auto-update-checker/hook/workspace-resolution.test.ts b/src/hooks/auto-update-checker/hook/workspace-resolution.test.ts index 79f374bd8..720832b26 100644 --- a/src/hooks/auto-update-checker/hook/workspace-resolution.test.ts +++ b/src/hooks/auto-update-checker/hook/workspace-resolution.test.ts @@ -1,5 +1,5 @@ import type { PluginInput } from "@opencode-ai/plugin" -import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test" +import { afterAll, afterEach, beforeEach, describe, expect, it, mock } from "bun:test" import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs" import { join } from "node:path" @@ -101,6 +101,10 @@ mock.module("../../../shared/opencode-config-dir", () => ({ }), })) +afterAll(() => { + mock.restore() +}) + const modulePath = "./background-update-check?test" const { runBackgroundUpdateCheck } = await import(modulePath) diff --git a/src/hooks/claude-code-hooks/handlers/session-event-handler-retry.test.ts b/src/hooks/claude-code-hooks/handlers/session-event-handler-retry.test.ts index 093de4920..c052963d9 100644 --- a/src/hooks/claude-code-hooks/handlers/session-event-handler-retry.test.ts +++ b/src/hooks/claude-code-hooks/handlers/session-event-handler-retry.test.ts @@ -1,4 +1,4 @@ -const { beforeEach, describe, expect, mock, test } = require("bun:test") +const { beforeEach, describe, expect, mock, test, afterAll } = require("bun:test") const executeStopHooks = mock(async (context: { parentSessionId?: string }) => ({ block: false, @@ -19,6 +19,8 @@ mock.module("../stop", () => ({ executeStopHooks, })) +afterAll(() => { mock.restore() }) + const { createSessionEventHandler } = await import("./session-event-handler") describe("createSessionEventHandler retry behavior", () => { diff --git a/src/hooks/claude-code-hooks/handlers/tool-execute-after-handler.test.ts b/src/hooks/claude-code-hooks/handlers/tool-execute-after-handler.test.ts index 5efd27e17..e6877cfd4 100644 --- a/src/hooks/claude-code-hooks/handlers/tool-execute-after-handler.test.ts +++ b/src/hooks/claude-code-hooks/handlers/tool-execute-after-handler.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, mock } from "bun:test" +import { beforeEach, describe, expect, it, mock, afterAll } from "bun:test" function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value) @@ -26,6 +26,8 @@ mock.module("../transcript", () => ({ getTranscriptPath: () => "/tmp/transcript.jsonl", })) +afterAll(() => { mock.restore() }) + const { createToolExecuteAfterHandler } = await import("./tool-execute-after-handler") describe("createToolExecuteAfterHandler", () => { diff --git a/src/hooks/claude-code-hooks/stop.test.ts b/src/hooks/claude-code-hooks/stop.test.ts index 431b90eb4..4a7b45941 100644 --- a/src/hooks/claude-code-hooks/stop.test.ts +++ b/src/hooks/claude-code-hooks/stop.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, mock, beforeEach } from "bun:test" +import { describe, it, expect, mock, beforeEach, afterAll } from "bun:test" import type { ClaudeHooksConfig } from "./types" import type { StopContext } from "./stop" @@ -17,6 +17,8 @@ mock.module("../../shared/logger", () => ({ getLogFilePath: () => "/tmp/test.log", })) +afterAll(() => { mock.restore() }) + const { executeStopHooks } = await import("./stop") function createStopContext(overrides?: Partial): StopContext { diff --git a/src/hooks/comment-checker/cli.test.ts b/src/hooks/comment-checker/cli.test.ts index 4c7b3bef2..376b285f3 100644 --- a/src/hooks/comment-checker/cli.test.ts +++ b/src/hooks/comment-checker/cli.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect, mock } from "bun:test" +import { describe, test, expect, mock, afterAll } from "bun:test" import { chmodSync, mkdtempSync, writeFileSync } from "node:fs" import { join } from "node:path" import { tmpdir } from "node:os" @@ -24,6 +24,8 @@ function createScriptBinary(scriptContent: string): string { return binaryPath } +afterAll(() => { mock.restore() }) + describe("comment-checker CLI", () => { describe("lazy initialization", () => { test("getCommentCheckerPathSync should be lazy and callable", async () => { diff --git a/src/hooks/comment-checker/hook.apply-patch.test.ts b/src/hooks/comment-checker/hook.apply-patch.test.ts index ec1b4cd8b..0217a62c8 100644 --- a/src/hooks/comment-checker/hook.apply-patch.test.ts +++ b/src/hooks/comment-checker/hook.apply-patch.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, mock, beforeEach } from "bun:test" +import { describe, it, expect, mock, beforeEach, afterAll } from "bun:test" const processApplyPatchEditsWithCli = mock(async () => {}) @@ -10,6 +10,8 @@ mock.module("./cli-runner", () => ({ processApplyPatchEditsWithCli, })) +afterAll(() => { mock.restore() }) + const { createCommentCheckerHooks } = await import("./hook") describe("comment-checker apply_patch integration", () => { diff --git a/src/hooks/compaction-context-injector/index.test.ts b/src/hooks/compaction-context-injector/index.test.ts index 9eacd0cdd..69cb082a9 100644 --- a/src/hooks/compaction-context-injector/index.test.ts +++ b/src/hooks/compaction-context-injector/index.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, mock } from "bun:test" +import { afterAll, describe, expect, it, mock } from "bun:test" mock.module("../../shared/system-directive", () => ({ createSystemDirective: (type: string) => `[DIRECTIVE:${type}]`, @@ -14,6 +14,10 @@ mock.module("../../shared/system-directive", () => ({ }, })) +afterAll(() => { + mock.restore() +}) + import { createCompactionContextInjector } from "./index" import { TaskHistory } from "../../features/background-agent/task-history" diff --git a/src/hooks/compaction-todo-preserver/index.test.ts b/src/hooks/compaction-todo-preserver/index.test.ts index 0bc784e2c..06bb2ab4f 100644 --- a/src/hooks/compaction-todo-preserver/index.test.ts +++ b/src/hooks/compaction-todo-preserver/index.test.ts @@ -18,6 +18,7 @@ afterAll(() => { update: async () => {}, }, })) + mock.restore() }) function createMockContext(todoResponses: Array[]): PluginInput { diff --git a/src/hooks/directory-agents-injector/injector.test.ts b/src/hooks/directory-agents-injector/injector.test.ts index ce9134203..8f5701645 100644 --- a/src/hooks/directory-agents-injector/injector.test.ts +++ b/src/hooks/directory-agents-injector/injector.test.ts @@ -3,7 +3,7 @@ import { mkdirSync, rmSync, writeFileSync } from "node:fs" import { tmpdir } from "node:os" import { join } from "node:path" import type { PluginInput } from "@opencode-ai/plugin" -import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test" +import { afterAll, afterEach, beforeEach, describe, expect, it, mock } from "bun:test" const storageMaps = new Map>() @@ -22,6 +22,10 @@ mock.module("./storage", () => ({ }, })) +afterAll(() => { + mock.restore() +}) + const truncator = { truncate: async (_sessionID: string, content: string) => ({ result: content, truncated: false }), getUsage: async (_sessionID: string) => null, diff --git a/src/hooks/directory-readme-injector/injector.test.ts b/src/hooks/directory-readme-injector/injector.test.ts index da238efba..74294fd7c 100644 --- a/src/hooks/directory-readme-injector/injector.test.ts +++ b/src/hooks/directory-readme-injector/injector.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test" +import { afterAll, afterEach, beforeEach, describe, expect, it, mock } from "bun:test" import { randomUUID } from "node:crypto" import { mkdirSync, rmSync, writeFileSync } from "node:fs" import { tmpdir } from "node:os" @@ -15,6 +15,10 @@ mock.module("./storage", () => ({ }, })) +afterAll(() => { + mock.restore() +}) + function createPluginContext(directory: string): PluginInput { return { directory } as PluginInput } diff --git a/src/hooks/model-fallback/hook.test.ts b/src/hooks/model-fallback/hook.test.ts index 09757ab3f..ced094a9e 100644 --- a/src/hooks/model-fallback/hook.test.ts +++ b/src/hooks/model-fallback/hook.test.ts @@ -1,5 +1,5 @@ declare const require: (name: string) => any -const { beforeEach, describe, expect, mock, test } = require("bun:test") +const { beforeEach, describe, expect, mock, test, afterAll } = require("bun:test") const readConnectedProvidersCacheMock = mock(() => null) const readProviderModelsCacheMock = mock(() => null) @@ -53,6 +53,10 @@ mock.module("../../shared/model-error-classifier", () => ({ selectFallbackProvider: selectFallbackProviderMock, })) +afterAll(() => { + mock.restore() +}) + import { clearPendingModelFallback, createModelFallbackHook, diff --git a/src/hooks/openclaw.test.ts b/src/hooks/openclaw.test.ts index db3b69a91..424c938d5 100644 --- a/src/hooks/openclaw.test.ts +++ b/src/hooks/openclaw.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, mock, test } from "bun:test" +import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test" const wakeOpenClawMock = mock(async () => null) @@ -6,6 +6,10 @@ mock.module("../openclaw", () => ({ wakeOpenClaw: wakeOpenClawMock, })) +afterAll(() => { + mock.restore() +}) + describe("createOpenClawHook", () => { beforeEach(() => { wakeOpenClawMock.mockClear() diff --git a/src/hooks/preemptive-compaction.context-limit-cache.test.ts b/src/hooks/preemptive-compaction.context-limit-cache.test.ts index 7b533b622..a8ec3c5fc 100644 --- a/src/hooks/preemptive-compaction.context-limit-cache.test.ts +++ b/src/hooks/preemptive-compaction.context-limit-cache.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, mock } from "bun:test" +import { describe, expect, it, mock, afterAll } from "bun:test" import { applyProviderConfig } from "../plugin-handlers/provider-config-handler" import { createModelCacheState } from "../plugin-state" @@ -9,6 +9,8 @@ mock.module("../shared/logger", () => ({ log: logMock, })) +afterAll(() => { mock.restore() }) + const { createPreemptiveCompactionHook } = await import("./preemptive-compaction") function createMockCtx() { diff --git a/src/hooks/preemptive-compaction.degradation-monitor.test.ts b/src/hooks/preemptive-compaction.degradation-monitor.test.ts index 1390a57e6..ae7f73a57 100644 --- a/src/hooks/preemptive-compaction.degradation-monitor.test.ts +++ b/src/hooks/preemptive-compaction.degradation-monitor.test.ts @@ -1,6 +1,6 @@ /// -import { beforeEach, describe, expect, it, mock } from "bun:test" +import { beforeEach, describe, expect, it, mock, afterAll } from "bun:test" const logMock = mock(() => {}) @@ -8,6 +8,8 @@ mock.module("../shared/logger", () => ({ log: logMock, })) +afterAll(() => { mock.restore() }) + const { createPreemptiveCompactionHook } = await import("./preemptive-compaction") type AssistantHistoryMessage = { diff --git a/src/hooks/preemptive-compaction.test.ts b/src/hooks/preemptive-compaction.test.ts index b4b6932a0..2b6ce15a5 100644 --- a/src/hooks/preemptive-compaction.test.ts +++ b/src/hooks/preemptive-compaction.test.ts @@ -1,6 +1,6 @@ /// -import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test" +import { afterAll, describe, it, expect, mock, beforeEach, afterEach } from "bun:test" const ANTHROPIC_CONTEXT_ENV_KEY = "ANTHROPIC_1M_CONTEXT" const VERTEX_CONTEXT_ENV_KEY = "VERTEX_ANTHROPIC_1M_CONTEXT" @@ -28,6 +28,10 @@ mock.module("../shared/logger", () => ({ log: logMock, })) +afterAll(() => { + mock.restore() +}) + const { createPreemptiveCompactionHook } = await import("./preemptive-compaction") function createMockCtx() { diff --git a/src/hooks/prometheus-md-only/index.test.ts b/src/hooks/prometheus-md-only/index.test.ts index 216e9a2d9..175d5edf9 100644 --- a/src/hooks/prometheus-md-only/index.test.ts +++ b/src/hooks/prometheus-md-only/index.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test" +import { afterAll, describe, expect, test, beforeEach, afterEach, mock } from "bun:test" import { mkdirSync, rmSync, writeFileSync } from "node:fs" import { join } from "node:path" import { tmpdir } from "node:os" @@ -11,6 +11,10 @@ mock.module("../../shared/opencode-storage-detection", () => ({ resetSqliteBackendCache: () => {}, })) +afterAll(() => { + mock.restore() +}) + const { createPrometheusMdOnlyHook } = await import("./index") const { MESSAGE_STORAGE } = await import("../../features/hook-message-injector") diff --git a/src/hooks/runtime-fallback/dispose.test.ts b/src/hooks/runtime-fallback/dispose.test.ts index 4810bfb95..a5cb46ef7 100644 --- a/src/hooks/runtime-fallback/dispose.test.ts +++ b/src/hooks/runtime-fallback/dispose.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test" +import { afterAll, afterEach, beforeEach, describe, expect, mock, test } from "bun:test" import type { HookDeps, RuntimeFallbackPluginInput } from "./types" let capturedDeps: HookDeps | undefined @@ -36,6 +36,10 @@ mock.module("./chat-message-handler", () => ({ createChatMessageHandler: mockCreateChatMessageHandler, })) +afterAll(() => { + mock.restore() +}) + const { createRuntimeFallbackHook } = await import("./hook") function createMockContext(): RuntimeFallbackPluginInput { diff --git a/src/hooks/session-recovery/recover-tool-result-missing.test.ts b/src/hooks/session-recovery/recover-tool-result-missing.test.ts index eac10fdd4..d8a56f3a3 100644 --- a/src/hooks/session-recovery/recover-tool-result-missing.test.ts +++ b/src/hooks/session-recovery/recover-tool-result-missing.test.ts @@ -1,4 +1,4 @@ -const { describe, it, expect, mock, beforeEach } = require("bun:test") +const { describe, it, expect, mock, beforeEach, afterAll } = require("bun:test") import type { MessageData } from "./types" @@ -17,6 +17,10 @@ mock.module("./storage", () => ({ readParts: () => storedParts, })) +afterAll(() => { + mock.restore() +}) + const { recoverToolResultMissing } = await import("./recover-tool-result-missing") function createMockClient(messages: MessageData[] = []) { diff --git a/src/hooks/start-work/context-info-builder.ts b/src/hooks/start-work/context-info-builder.ts index e5307c8e9..17642ca73 100644 --- a/src/hooks/start-work/context-info-builder.ts +++ b/src/hooks/start-work/context-info-builder.ts @@ -13,6 +13,7 @@ import { writeBoulderState, } from "../../features/boulder-state" import { log } from "../../shared/logger" +import { createWorktreeActiveBlock } from "./worktree-block" import type { PluginInput } from "@opencode-ai/plugin" import { HOOK_NAME } from "./start-work-hook" @@ -158,7 +159,9 @@ Looking for new plans...` appendSessionId(directory, sessionId) } - const worktreeDisplay = effectiveWorktree ? worktreeBlock.replace(worktreePath ?? "", effectiveWorktree) : worktreeBlock + const worktreeDisplay = effectiveWorktree + ? (worktreeBlock || createWorktreeActiveBlock(effectiveWorktree)) + : worktreeBlock return ` ## Active Work Session Found diff --git a/src/hooks/start-work/start-work-hook.ts b/src/hooks/start-work/start-work-hook.ts index c94977c91..430e96792 100644 --- a/src/hooks/start-work/start-work-hook.ts +++ b/src/hooks/start-work/start-work-hook.ts @@ -24,6 +24,7 @@ import { import { detectWorktreePath } from "./worktree-detector" import { parseUserRequest } from "./parse-user-request" import { buildStartWorkContextInfo } from "./context-info-builder" +import { createWorktreeActiveBlock } from "./worktree-block" export const HOOK_NAME = "start-work" as const const START_WORK_TEMPLATE_MARKER = "You are starting a Sisyphus work session." @@ -44,18 +45,6 @@ interface StartWorkHookOutput { parts: Array<{ type: string; text?: string }> } -function createWorktreeActiveBlock(worktreePath: string): string { - return ` -## Worktree Active - -**Worktree**: \`${worktreePath}\` - -**CRITICAL - DO NOT FORGET**: You are working inside a git worktree. ALL operations MUST be performed exclusively within this worktree directory. -- Every file read, write, edit, and git operation MUST target paths under: \`${worktreePath}\` -- When delegating tasks to subagents, you MUST include the worktree path in your delegation prompt so they also operate exclusively within the worktree -- NEVER operate on the main repository directory - always use the worktree path above` -} - function resolveWorktreeContext( explicitWorktreePath: string | null, ): { worktreePath: string | undefined; block: string } { diff --git a/src/hooks/start-work/worktree-block.ts b/src/hooks/start-work/worktree-block.ts new file mode 100644 index 000000000..2aa865a3f --- /dev/null +++ b/src/hooks/start-work/worktree-block.ts @@ -0,0 +1,11 @@ +export function createWorktreeActiveBlock(worktreePath: string): string { + return ` +## Worktree Active + +**Worktree**: \`${worktreePath}\` + +**CRITICAL - DO NOT FORGET**: You are working inside a git worktree. ALL operations MUST be performed exclusively within this worktree directory. +- Every file read, write, edit, and git operation MUST target paths under: \`${worktreePath}\` +- When delegating tasks to subagents, you MUST include the worktree path in your delegation prompt so they also operate exclusively within the worktree +- NEVER operate on the main repository directory - always use the worktree path above` +} diff --git a/src/plugin/event.model-fallback.test.ts b/src/plugin/event.model-fallback.test.ts index b6a1f6966..a88edceaf 100644 --- a/src/plugin/event.model-fallback.test.ts +++ b/src/plugin/event.model-fallback.test.ts @@ -1,11 +1,13 @@ declare const require: (name: string) => any -const { afterEach, describe, expect, mock, test } = require("bun:test") +const { afterEach, afterAll, describe, expect, mock, test } = require("bun:test") mock.module("../shared/connected-providers-cache", () => ({ readConnectedProvidersCache: () => null, readProviderModelsCache: () => null, })) +afterAll(() => { mock.restore() }) + import { createEventHandler } from "./event" import { createChatMessageHandler } from "./chat-message" import { _resetForTesting, setMainSession } from "../features/claude-code-session-state" diff --git a/src/plugin/fallback.cliproxyapi-matrix.test.ts b/src/plugin/fallback.cliproxyapi-matrix.test.ts index 0acd7a507..b04e865c5 100644 --- a/src/plugin/fallback.cliproxyapi-matrix.test.ts +++ b/src/plugin/fallback.cliproxyapi-matrix.test.ts @@ -1,5 +1,5 @@ declare const require: (name: string) => any -const { afterEach, describe, expect, mock, test } = require("bun:test") +const { afterEach, afterAll, describe, expect, mock, test } = require("bun:test") const PROVIDER_ID = "cliproxyapi" @@ -10,6 +10,8 @@ mock.module("../shared/connected-providers-cache", () => ({ }), })) +afterAll(() => { mock.restore() }) + import { createEventHandler } from "./event" import { createChatMessageHandler } from "./chat-message" import { createModelFallbackHook } from "../hooks/model-fallback/hook" diff --git a/src/plugin/hooks/create-tool-guard-hooks.test.ts b/src/plugin/hooks/create-tool-guard-hooks.test.ts index f06e9ef6f..5eb27f5fd 100644 --- a/src/plugin/hooks/create-tool-guard-hooks.test.ts +++ b/src/plugin/hooks/create-tool-guard-hooks.test.ts @@ -1,7 +1,8 @@ -import { beforeEach, describe, expect, it, mock } from "bun:test" +import { beforeEach, describe, expect, it, spyOn } from "bun:test" import type { OhMyOpenCodeConfig } from "../../config" import type { ModelCacheState } from "../../plugin-state" import type { PluginContext } from "../types" +import * as hooks from "../../hooks" const mockContext = { directory: "/tmp", @@ -9,58 +10,41 @@ const mockContext = { const mockModelCacheState = { anthropicContext1MEnabled: false, + modelContextLimitsCache: new Map(), } satisfies ModelCacheState -let capturedRulesInjectorOptions: { skipClaudeUserRules?: boolean } | undefined - -mock.module("../../hooks", () => ({ - createCommentCheckerHooks: () => ({ name: "comment-checker" }), - createToolOutputTruncatorHook: () => ({ name: "tool-output-truncator" }), - createDirectoryAgentsInjectorHook: () => ({ name: "directory-agents-injector" }), - createDirectoryReadmeInjectorHook: () => ({ name: "directory-readme-injector" }), - createEmptyTaskResponseDetectorHook: () => ({ name: "empty-task-response-detector" }), - createRulesInjectorHook: ( - _ctx: PluginContext, - _modelCacheState: ModelCacheState, - options?: { skipClaudeUserRules?: boolean }, - ) => { - capturedRulesInjectorOptions = options - return { name: "rules-injector" } - }, - createTasksTodowriteDisablerHook: () => ({ name: "tasks-todowrite-disabler" }), - createWriteExistingFileGuardHook: () => ({ name: "write-existing-file-guard" }), - createBashFileReadGuardHook: () => ({ name: "bash-file-read-guard" }), - createHashlineReadEnhancerHook: () => ({ name: "hashline-read-enhancer" }), - createReadImageResizerHook: () => ({ name: "read-image-resizer" }), - createJsonErrorRecoveryHook: () => ({ name: "json-error-recovery" }), - createTodoDescriptionOverrideHook: () => ({ name: "todo-description-override" }), - createWebFetchRedirectGuardHook: () => ({ name: "webfetch-redirect-guard" }), -})) - describe("createToolGuardHooks", () => { + let capturedOptions: { skipClaudeUserRules?: boolean } | undefined + beforeEach(() => { - capturedRulesInjectorOptions = undefined + capturedOptions = undefined + spyOn(hooks, "createRulesInjectorHook").mockImplementation( + (_ctx: unknown, _state: unknown, options?: { skipClaudeUserRules?: boolean }) => { + capturedOptions = options + return { name: "rules-injector" } as never + }, + ) }) - it("skips Claude user rules when claude_code.hooks is false", async () => { + it("skips Claude user rules when claude_code.hooks is false", () => { // given const pluginConfig = { claude_code: { hooks: false, }, } as OhMyOpenCodeConfig - const { createToolGuardHooks } = await import("./create-tool-guard-hooks") + const { createToolGuardHooks } = require("./create-tool-guard-hooks") // when createToolGuardHooks({ ctx: mockContext, pluginConfig, modelCacheState: mockModelCacheState, - isHookEnabled: (hookName) => hookName === "rules-injector", + isHookEnabled: (hookName: string) => hookName === "rules-injector", safeHookEnabled: true, }) // then - expect(capturedRulesInjectorOptions).toEqual({ skipClaudeUserRules: true }) + expect(capturedOptions).toEqual({ skipClaudeUserRules: true }) }) }) diff --git a/src/shared/migrate-legacy-plugin-entry.test.ts b/src/shared/migrate-legacy-plugin-entry.test.ts index e43cfe809..6e382d5a0 100644 --- a/src/shared/migrate-legacy-plugin-entry.test.ts +++ b/src/shared/migrate-legacy-plugin-entry.test.ts @@ -1,6 +1,6 @@ /// -import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test" +import { afterAll, afterEach, beforeEach, describe, expect, it, mock } from "bun:test" import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs" import { tmpdir } from "node:os" import { join } from "node:path" @@ -9,6 +9,10 @@ async function importFreshMigrationModule(): Promise { + mock.restore() +}) + describe("migrateLegacyPluginEntry", () => { let testDir = "" diff --git a/src/shared/model-error-classifier.test.ts b/src/shared/model-error-classifier.test.ts index 88ba63dd5..c199d8145 100644 --- a/src/shared/model-error-classifier.test.ts +++ b/src/shared/model-error-classifier.test.ts @@ -1,5 +1,5 @@ declare const require: (name: string) => any -const { describe, expect, test, beforeEach, mock } = require("bun:test") +const { describe, expect, test, beforeEach, mock, afterAll } = require("bun:test") const readConnectedProvidersCacheMock = mock(() => null) @@ -7,6 +7,8 @@ mock.module("./connected-providers-cache", () => ({ readConnectedProvidersCache: readConnectedProvidersCacheMock, })) +afterAll(() => { mock.restore() }) + import { shouldRetryError, selectFallbackProvider } from "./model-error-classifier" describe("model-error-classifier", () => { diff --git a/src/shared/opencode-message-dir.test.ts b/src/shared/opencode-message-dir.test.ts index 521ddcdc3..97b9cac01 100644 --- a/src/shared/opencode-message-dir.test.ts +++ b/src/shared/opencode-message-dir.test.ts @@ -19,6 +19,8 @@ mock.module("./opencode-storage-detection", () => ({ resetSqliteBackendCache: () => {}, })) +afterAll(() => { mock.restore() }) + const { getMessageDir } = await import("./opencode-message-dir") describe("getMessageDir", () => { diff --git a/src/tools/lsp/client.test.ts b/src/tools/lsp/client.test.ts index 8c805d144..f89de579f 100644 --- a/src/tools/lsp/client.test.ts +++ b/src/tools/lsp/client.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs" import { join } from "node:path" import { tmpdir } from "node:os" -import { describe, it, expect, spyOn, mock, beforeEach, afterEach } from "bun:test" +import { describe, it, expect, spyOn, mock, beforeEach, afterEach, afterAll } from "bun:test" mock.module("vscode-jsonrpc/node", () => ({ createMessageConnection: () => { @@ -12,6 +12,8 @@ mock.module("vscode-jsonrpc/node", () => ({ StreamMessageWriter: function StreamMessageWriter() {}, })) +afterAll(() => { mock.restore() }) + import { LSPClient, lspManager, validateCwd } from "./client" import type { ResolvedServer } from "./types" diff --git a/src/tools/session-manager/storage.test.ts b/src/tools/session-manager/storage.test.ts index f4e3c1cb0..fc6dacce1 100644 --- a/src/tools/session-manager/storage.test.ts +++ b/src/tools/session-manager/storage.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test" +import { describe, test, expect, beforeEach, afterEach, afterAll, mock } from "bun:test" import { mkdirSync, writeFileSync, rmSync, existsSync, readdirSync } from "node:fs" import { join } from "node:path" import { tmpdir } from "node:os" @@ -59,6 +59,9 @@ mock.module("../../shared/opencode-message-dir", () => ({ return null }, })) + +afterAll(() => { mock.restore() }) + const { getAllSessions, getMessageDir, sessionExists, readSessionMessages, readSessionTodos, getSessionInfo } = await import("./storage")