From 271929a9e42e99c32b9866ade43e65fc84429c96 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 14 Feb 2026 16:19:55 +0900 Subject: [PATCH] ci: restore mock.module() overrides in afterAll to prevent cross-file pollution Add afterAll hooks that restore original module implementations after mock.module() overrides. This prevents mock state from leaking across test files when bun runs them in the same process. Pattern: capture original module with await import() before mocking, restore in afterAll. --- src/cli/doctor/formatter.test.ts | 12 +++++++++++- src/cli/doctor/runner.test.ts | 10 +++++++++- src/cli/mcp-oauth/login.test.ts | 8 +++++++- src/cli/run/integration.test.ts | 12 +++++++++++- src/cli/run/server-connection.test.ts | 12 +++++++++++- .../claude-code-mcp-loader/loader.test.ts | 8 ++++++++ src/features/skill-mcp-manager/manager.test.ts | 14 +++++++++++++- src/features/tmux-subagent/manager.test.ts | 12 +++++++++++- .../storage.test.ts | 6 +++++- .../hook/background-update-check.test.ts | 18 +++++++++++++++++- src/hooks/claude-code-hooks/stop.test.ts | 10 +++++++++- src/hooks/comment-checker/cli.test.ts | 11 ++++++++++- .../comment-checker/hook.apply-patch.test.ts | 8 +++++++- .../compaction-context-injector/index.test.ts | 8 +++++++- src/hooks/rules-injector/injector.test.ts | 12 +++++++++++- src/tools/lsp/client.test.ts | 8 +++++++- src/tools/session-manager/storage.test.ts | 8 +++++++- src/tools/skill/tools.test.ts | 8 +++++++- 18 files changed, 168 insertions(+), 17 deletions(-) diff --git a/src/cli/doctor/formatter.test.ts b/src/cli/doctor/formatter.test.ts index 5884997af..e0c280995 100644 --- a/src/cli/doctor/formatter.test.ts +++ b/src/cli/doctor/formatter.test.ts @@ -1,6 +1,10 @@ -import { afterEach, describe, expect, it, mock } from "bun:test" +import { afterEach, afterAll, describe, expect, it, mock } from "bun:test" import type { DoctorResult } from "./types" +const realFormatDefault = await import("./format-default") +const realFormatStatus = await import("./format-status") +const realFormatVerbose = await import("./format-verbose") + function createDoctorResult(): DoctorResult { return { results: [ @@ -44,6 +48,12 @@ describe("formatter", () => { mock.restore() }) + afterAll(() => { + mock.module("./format-default", () => ({ ...realFormatDefault })) + mock.module("./format-status", () => ({ ...realFormatStatus })) + mock.module("./format-verbose", () => ({ ...realFormatVerbose })) + }) + describe("formatDoctorOutput", () => { it("dispatches to default formatter for default mode", async () => { //#given diff --git a/src/cli/doctor/runner.test.ts b/src/cli/doctor/runner.test.ts index ca96b0794..e2070d1cc 100644 --- a/src/cli/doctor/runner.test.ts +++ b/src/cli/doctor/runner.test.ts @@ -1,6 +1,9 @@ -import { afterEach, describe, expect, it, mock } from "bun:test" +import { afterEach, afterAll, describe, expect, it, mock } from "bun:test" import type { CheckDefinition, CheckResult, DoctorResult, SystemInfo, ToolsSummary } from "./types" +const realChecks = await import("./checks") +const realFormatter = await import("./formatter") + function createSystemInfo(): SystemInfo { return { opencodeVersion: "1.0.200", @@ -47,6 +50,11 @@ describe("runner", () => { mock.restore() }) + afterAll(() => { + mock.module("./checks", () => ({ ...realChecks })) + mock.module("./formatter", () => ({ ...realFormatter })) + }) + describe("runCheck", () => { it("returns fail result with issue when check throws", async () => { //#given diff --git a/src/cli/mcp-oauth/login.test.ts b/src/cli/mcp-oauth/login.test.ts index 917652f76..6d9998a1e 100644 --- a/src/cli/mcp-oauth/login.test.ts +++ b/src/cli/mcp-oauth/login.test.ts @@ -1,4 +1,6 @@ -import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test" +import { describe, it, expect, beforeEach, afterEach, afterAll, mock } from "bun:test" + +const realMcpOauthProvider = await import("../../features/mcp-oauth/provider") const mockLogin = mock(() => Promise.resolve({ accessToken: "test-token", expiresAt: 1710000000 })) @@ -11,6 +13,10 @@ mock.module("../../features/mcp-oauth/provider", () => ({ }, })) +afterAll(() => { + mock.module("../../features/mcp-oauth/provider", () => ({ ...realMcpOauthProvider })) +}) + 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 1cbfa0847..c38e72ea9 100644 --- a/src/cli/run/integration.test.ts +++ b/src/cli/run/integration.test.ts @@ -1,10 +1,13 @@ -import { describe, it, expect, mock, spyOn, beforeEach, afterEach } from "bun:test" +import { describe, it, expect, mock, spyOn, beforeEach, afterEach, afterAll } from "bun:test" import type { RunResult } from "./types" import { createJsonOutputManager } from "./json-output" import { resolveSession } from "./session-resolver" import { executeOnCompleteHook } from "./on-complete-hook" import type { OpencodeClient } from "./types" +const realSdk = await import("@opencode-ai/sdk") +const realPortUtils = await import("../../shared/port-utils") + const mockServerClose = mock(() => {}) const mockCreateOpencode = mock(() => Promise.resolve({ @@ -17,16 +20,23 @@ const mockIsPortAvailable = mock(() => Promise.resolve(true)) const mockGetAvailableServerPort = mock(() => Promise.resolve({ port: 9999, wasAutoSelected: false })) mock.module("@opencode-ai/sdk", () => ({ + ...realSdk, createOpencode: mockCreateOpencode, createOpencodeClient: mockCreateOpencodeClient, })) mock.module("../../shared/port-utils", () => ({ + ...realPortUtils, isPortAvailable: mockIsPortAvailable, getAvailableServerPort: mockGetAvailableServerPort, DEFAULT_SERVER_PORT: 4096, })) +afterAll(() => { + mock.module("@opencode-ai/sdk", () => ({ ...realSdk })) + mock.module("../../shared/port-utils", () => ({ ...realPortUtils })) +}) + const { createServerConnection } = await import("./server-connection") interface MockWriteStream { diff --git a/src/cli/run/server-connection.test.ts b/src/cli/run/server-connection.test.ts index 100154a0e..f9cabc870 100644 --- a/src/cli/run/server-connection.test.ts +++ b/src/cli/run/server-connection.test.ts @@ -1,4 +1,7 @@ -import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test" +import { describe, it, expect, mock, beforeEach, afterEach, afterAll } from "bun:test" + +const realSdk = await import("@opencode-ai/sdk") +const realPortUtils = await import("../../shared/port-utils") const originalConsole = globalThis.console @@ -15,16 +18,23 @@ const mockGetAvailableServerPort = mock(() => Promise.resolve({ port: 4096, wasA const mockConsoleLog = mock(() => {}) mock.module("@opencode-ai/sdk", () => ({ + ...realSdk, createOpencode: mockCreateOpencode, createOpencodeClient: mockCreateOpencodeClient, })) mock.module("../../shared/port-utils", () => ({ + ...realPortUtils, isPortAvailable: mockIsPortAvailable, getAvailableServerPort: mockGetAvailableServerPort, DEFAULT_SERVER_PORT: 4096, })) +afterAll(() => { + mock.module("@opencode-ai/sdk", () => ({ ...realSdk })) + mock.module("../../shared/port-utils", () => ({ ...realPortUtils })) +}) + const { createServerConnection } = await import("./server-connection") describe("createServerConnection", () => { diff --git a/src/features/claude-code-mcp-loader/loader.test.ts b/src/features/claude-code-mcp-loader/loader.test.ts index 848c7aed5..711a721c3 100644 --- a/src/features/claude-code-mcp-loader/loader.test.ts +++ b/src/features/claude-code-mcp-loader/loader.test.ts @@ -6,15 +6,20 @@ import { tmpdir } from "os" const TEST_DIR = join(tmpdir(), "mcp-loader-test-" + Date.now()) const TEST_HOME = join(TEST_DIR, "home") +const realOs = await import("os") +const realShared = await import("../../shared") + describe("getSystemMcpServerNames", () => { beforeEach(() => { mkdirSync(TEST_DIR, { recursive: true }) mkdirSync(TEST_HOME, { recursive: true }) mock.module("os", () => ({ + ...realOs, homedir: () => TEST_HOME, tmpdir, })) mock.module("../../shared", () => ({ + ...realShared, getClaudeConfigDir: () => join(TEST_HOME, ".claude"), })) }) @@ -22,6 +27,9 @@ describe("getSystemMcpServerNames", () => { afterEach(() => { mock.restore() rmSync(TEST_DIR, { recursive: true, force: true }) + + mock.module("os", () => ({ ...realOs })) + mock.module("../../shared", () => ({ ...realShared })) }) it("returns empty set when no .mcp.json files exist", async () => { diff --git a/src/features/skill-mcp-manager/manager.test.ts b/src/features/skill-mcp-manager/manager.test.ts index f65aa5c55..8e5d015a7 100644 --- a/src/features/skill-mcp-manager/manager.test.ts +++ b/src/features/skill-mcp-manager/manager.test.ts @@ -1,8 +1,13 @@ -import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test" +import { describe, it, expect, beforeEach, afterEach, mock, spyOn, afterAll } from "bun:test" import { SkillMcpManager } from "./manager" import type { SkillMcpClientInfo, SkillMcpServerContext } from "./types" import type { ClaudeCodeMcpServer } from "../claude-code-mcp-loader/types" +const realStreamableHttp = await import( + "@modelcontextprotocol/sdk/client/streamableHttp.js" +) +const realMcpOauthProvider = await import("../mcp-oauth/provider") + // Mock the MCP SDK transports to avoid network calls const mockHttpConnect = mock(() => Promise.reject(new Error("Mocked HTTP connection failure"))) const mockHttpClose = mock(() => Promise.resolve()) @@ -37,6 +42,13 @@ mock.module("../mcp-oauth/provider", () => ({ }, })) +afterAll(() => { + mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ + ...realStreamableHttp, + })) + mock.module("../mcp-oauth/provider", () => ({ ...realMcpOauthProvider })) +}) + diff --git a/src/features/tmux-subagent/manager.test.ts b/src/features/tmux-subagent/manager.test.ts index 954a9d8b2..1ab57cbcc 100644 --- a/src/features/tmux-subagent/manager.test.ts +++ b/src/features/tmux-subagent/manager.test.ts @@ -1,9 +1,13 @@ -import { describe, test, expect, mock, beforeEach } from 'bun:test' +import { describe, test, expect, mock, beforeEach, afterAll } from 'bun:test' import type { TmuxConfig } from '../../config/schema' import type { WindowState, PaneAction } from './types' import type { ActionResult, ExecuteContext } from './action-executor' import type { TmuxUtilDeps } from './manager' +const realPaneStateQuerier = await import('./pane-state-querier') +const realActionExecutor = await import('./action-executor') +const realSharedTmux = await import('../../shared/tmux') + type ExecuteActionsResult = { success: boolean spawnedPaneId?: string @@ -71,6 +75,12 @@ mock.module('../../shared/tmux', () => { } }) +afterAll(() => { + mock.module('./pane-state-querier', () => ({ ...realPaneStateQuerier })) + mock.module('./action-executor', () => ({ ...realActionExecutor })) + mock.module('../../shared/tmux', () => ({ ...realSharedTmux })) +}) + const trackedSessions = new Set() function createMockContext(overrides?: { 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 d5797590f..301af8cb4 100644 --- a/src/hooks/anthropic-context-window-limit-recovery/storage.test.ts +++ b/src/hooks/anthropic-context-window-limit-recovery/storage.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect, mock, beforeEach } from "bun:test" +import { describe, test, expect, mock, beforeEach, afterAll } from "bun:test" import { truncateUntilTargetTokens } from "./storage" import * as storage from "./storage" @@ -11,6 +11,10 @@ mock.module("./storage", () => { } }) +afterAll(() => { + mock.module("./storage", () => ({ ...storage })) +}) + describe("truncateUntilTargetTokens", () => { const sessionID = "test-session" 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 8d3009b5d..3f073b1fd 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,4 +1,11 @@ -import { describe, it, expect, mock, beforeEach } from "bun:test" +import { describe, it, expect, mock, beforeEach, afterAll } from "bun:test" + +const realChecker = await import("../checker") +const realVersionChannel = await import("../version-channel") +const realCache = await import("../cache") +const realConfigManager = await import("../../../cli/config-manager") +const realUpdateToasts = await import("./update-toasts") +const realLogger = await import("../../../shared/logger") // Mock modules before importing const mockFindPluginEntry = mock(() => null as any) @@ -39,6 +46,15 @@ mock.module("../../../shared/logger", () => ({ log: () => {}, })) +afterAll(() => { + mock.module("../checker", () => ({ ...realChecker })) + mock.module("../version-channel", () => ({ ...realVersionChannel })) + mock.module("../cache", () => ({ ...realCache })) + mock.module("../../../cli/config-manager", () => ({ ...realConfigManager })) + mock.module("./update-toasts", () => ({ ...realUpdateToasts })) + mock.module("../../../shared/logger", () => ({ ...realLogger })) +}) + const { runBackgroundUpdateCheck } = await import("./background-update-check") describe("runBackgroundUpdateCheck", () => { diff --git a/src/hooks/claude-code-hooks/stop.test.ts b/src/hooks/claude-code-hooks/stop.test.ts index 431b90eb4..9834be85e 100644 --- a/src/hooks/claude-code-hooks/stop.test.ts +++ b/src/hooks/claude-code-hooks/stop.test.ts @@ -1,7 +1,10 @@ -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" +const realCommandExecutor = await import("../../shared/command-executor") +const realLogger = await import("../../shared/logger") + const mockExecuteHookCommand = mock(() => Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }) ) @@ -17,6 +20,11 @@ mock.module("../../shared/logger", () => ({ getLogFilePath: () => "/tmp/test.log", })) +afterAll(() => { + mock.module("../../shared/command-executor", () => ({ ...realCommandExecutor })) + mock.module("../../shared/logger", () => ({ ...realLogger })) +}) + 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..1c75d672c 100644 --- a/src/hooks/comment-checker/cli.test.ts +++ b/src/hooks/comment-checker/cli.test.ts @@ -1,10 +1,19 @@ -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" import type { PendingCall } from "./types" +const realCli = await import("./cli") +const cliTsHref = new URL("./cli.ts", import.meta.url).href + +afterAll(() => { + mock.module("./cli", () => ({ ...realCli })) + mock.module("./cli.ts", () => ({ ...realCli })) + mock.module(cliTsHref, () => ({ ...realCli })) +}) + function createMockInput() { return { session_id: "test", diff --git a/src/hooks/comment-checker/hook.apply-patch.test.ts b/src/hooks/comment-checker/hook.apply-patch.test.ts index ec1b4cd8b..46f3171c9 100644 --- a/src/hooks/comment-checker/hook.apply-patch.test.ts +++ b/src/hooks/comment-checker/hook.apply-patch.test.ts @@ -1,4 +1,6 @@ -import { describe, it, expect, mock, beforeEach } from "bun:test" +import { describe, it, expect, mock, beforeEach, afterAll } from "bun:test" + +const realCliRunner = await import("./cli-runner") const processApplyPatchEditsWithCli = mock(async () => {}) @@ -10,6 +12,10 @@ mock.module("./cli-runner", () => ({ processApplyPatchEditsWithCli, })) +afterAll(() => { + mock.module("./cli-runner", () => ({ ...realCliRunner })) +}) + 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 a2813916f..737a4e5bb 100644 --- a/src/hooks/compaction-context-injector/index.test.ts +++ b/src/hooks/compaction-context-injector/index.test.ts @@ -1,4 +1,6 @@ -import { describe, expect, it, mock } from "bun:test" +import { describe, expect, it, mock, afterAll } from "bun:test" + +const realSystemDirective = await import("../../shared/system-directive") mock.module("../../shared/system-directive", () => ({ createSystemDirective: (type: string) => `[DIRECTIVE:${type}]`, @@ -14,6 +16,10 @@ mock.module("../../shared/system-directive", () => ({ }, })) +afterAll(() => { + mock.module("../../shared/system-directive", () => ({ ...realSystemDirective })) +}) + import { createCompactionContextInjector } from "./index" import { TaskHistory } from "../../features/background-agent/task-history" diff --git a/src/hooks/rules-injector/injector.test.ts b/src/hooks/rules-injector/injector.test.ts index e07b7fc42..36b815f1e 100644 --- a/src/hooks/rules-injector/injector.test.ts +++ b/src/hooks/rules-injector/injector.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 * as fs from "node:fs"; import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import * as os from "node:os"; @@ -6,6 +6,10 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { RULES_INJECTOR_STORAGE } from "./constants"; +const realNodeFs = await import("node:fs"); +const realNodeOs = await import("node:os"); +const realMatcher = await import("./matcher"); + type StatSnapshot = { mtimeMs: number; size: number }; let trackedRulePath = ""; @@ -56,6 +60,12 @@ mock.module("./matcher", () => ({ isDuplicateByContentHash: (hash: string, cache: Set) => cache.has(hash), })); +afterAll(() => { + mock.module("node:fs", () => ({ ...realNodeFs })); + mock.module("node:os", () => ({ ...realNodeOs })); + mock.module("./matcher", () => ({ ...realMatcher })); +}); + function createOutput(): { title: string; output: string; metadata: unknown } { return { title: "tool", output: "", metadata: {} }; } diff --git a/src/tools/lsp/client.test.ts b/src/tools/lsp/client.test.ts index 8c805d144..c750f3dea 100644 --- a/src/tools/lsp/client.test.ts +++ b/src/tools/lsp/client.test.ts @@ -2,7 +2,9 @@ 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" + +const realJsonRpcNode = await import("vscode-jsonrpc/node") mock.module("vscode-jsonrpc/node", () => ({ createMessageConnection: () => { @@ -12,6 +14,10 @@ mock.module("vscode-jsonrpc/node", () => ({ StreamMessageWriter: function StreamMessageWriter() {}, })) +afterAll(() => { + mock.module("vscode-jsonrpc/node", () => ({ ...realJsonRpcNode })) +}) + 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 76507867a..1f7a400c3 100644 --- a/src/tools/session-manager/storage.test.ts +++ b/src/tools/session-manager/storage.test.ts @@ -1,9 +1,11 @@ -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 } from "node:fs" import { join } from "node:path" import { tmpdir } from "node:os" import { randomUUID } from "node:crypto" +const realConstants = await import("./constants") + const TEST_DIR = join(tmpdir(), `omo-test-session-manager-${randomUUID()}`) const TEST_MESSAGE_STORAGE = join(TEST_DIR, "message") const TEST_PART_STORAGE = join(TEST_DIR, "part") @@ -26,6 +28,10 @@ mock.module("./constants", () => ({ TOOL_NAME_PREFIX: "session_", })) +afterAll(() => { + mock.module("./constants", () => ({ ...realConstants })) +}) + const { getAllSessions, getMessageDir, sessionExists, readSessionMessages, readSessionTodos, getSessionInfo } = await import("./storage") diff --git a/src/tools/skill/tools.test.ts b/src/tools/skill/tools.test.ts index e5ce213e9..a4b5bb479 100644 --- a/src/tools/skill/tools.test.ts +++ b/src/tools/skill/tools.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach, mock, spyOn } from "bun:test" +import { describe, it, expect, beforeEach, mock, spyOn, afterAll } from "bun:test" import type { ToolContext } from "@opencode-ai/plugin/tool" import * as fs from "node:fs" import { createSkillTool } from "./tools" @@ -8,6 +8,8 @@ import type { Tool as McpTool } from "@modelcontextprotocol/sdk/types.js" const originalReadFileSync = fs.readFileSync.bind(fs) +const realNodeFs = await import("node:fs") + mock.module("node:fs", () => ({ ...fs, readFileSync: (path: string, encoding?: string) => { @@ -21,6 +23,10 @@ Test skill body content` }, })) +afterAll(() => { + mock.module("node:fs", () => ({ ...realNodeFs })) +}) + function createMockSkill(name: string, options: { agent?: string } = {}): LoadedSkill { return { name,