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 5884997a..e0c28099 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 ca96b079..e2070d1c 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 917652f7..6d9998a1 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 1cbfa084..c38e72ea 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 100154a0..f9cabc87 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 848c7aed..711a721c 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 f65aa5c5..8e5d015a 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 954a9d8b..1ab57cbc 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 d5797590..301af8cb 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 8d3009b5..3f073b1f 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 431b90eb..9834be85 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 4c7b3bef..1c75d672 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 ec1b4cd8..46f3171c 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 a2813916..737a4e5b 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 e07b7fc4..36b815f1 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 8c805d14..c750f3de 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 76507867..1f7a400c 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 e5ce213e..a4b5bb47 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,