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.
This commit is contained in:
YeonGyu-Kim
2026-02-14 16:19:55 +09:00
parent 945329e261
commit 271929a9e4
18 changed files with 168 additions and 17 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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", () => {

View File

@@ -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 {

View File

@@ -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", () => {

View File

@@ -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 () => {

View File

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

View File

@@ -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<string>()
function createMockContext(overrides?: {

View File

@@ -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"

View File

@@ -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", () => {

View File

@@ -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>): StopContext {

View File

@@ -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",

View File

@@ -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", () => {

View File

@@ -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"

View File

@@ -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<string>) => 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: {} };
}

View File

@@ -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"

View File

@@ -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")

View File

@@ -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,