test: localize mock.module setup to fresh imports

This commit is contained in:
YeonGyu-Kim
2026-04-04 19:49:25 +09:00
parent 8b8559f39d
commit a4db240d47
9 changed files with 204 additions and 158 deletions
@@ -1,33 +1,52 @@
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test"
mock.module("../../shared/logger", () => ({
log: mock(() => {}),
}))
mock.module("../../shared/connected-providers-cache", () => ({
readConnectedProvidersCache: mock(() => null),
readProviderModelsCache: mock(() => null),
}))
mock.module("../../shared/model-error-classifier", () => ({
shouldRetryError: mock(() => true),
getNextFallback: mock((chain: Array<{ model: string }>, attempt: number) => chain[attempt]),
hasMoreFallbacks: mock((chain: Array<{ model: string }>, attempt: number) => attempt < chain.length),
selectFallbackProvider: mock((providers: string[]) => providers[0]),
}))
mock.module("../../shared/provider-model-id-transform", () => ({
transformModelForProvider: mock((_provider: string, model: string) => model),
}))
const sharedLogMock = mock(() => {})
const readConnectedProvidersCacheMock = mock(() => null)
const readProviderModelsCacheMock = mock(() => null)
const shouldRetryErrorMock = mock(() => true)
const getNextFallbackMock = mock((chain: Array<{ model: string }>, attempt: number) => chain[attempt])
const hasMoreFallbacksMock = mock((chain: Array<{ model: string }>, attempt: number) => attempt < chain.length)
const selectFallbackProviderMock = mock((providers: string[]) => providers[0])
const transformModelForProviderMock = mock((_provider: string, model: string) => model)
import type { BackgroundTask } from "./types"
import type { ConcurrencyManager } from "./concurrency"
import type { OpencodeClient, QueueItem } from "./constants"
const { tryFallbackRetry } = await import("./fallback-retry-handler")
const { shouldRetryError, selectFallbackProvider } = await import("../../shared/model-error-classifier")
const { readProviderModelsCache } = await import("../../shared")
mock.restore()
async function importFreshFallbackRetryHandlerModule() {
mock.module("../../shared/logger", () => ({
log: sharedLogMock,
}))
mock.module("../../shared/connected-providers-cache", () => ({
readConnectedProvidersCache: readConnectedProvidersCacheMock,
readProviderModelsCache: readProviderModelsCacheMock,
}))
mock.module("../../shared/model-error-classifier", () => ({
shouldRetryError: shouldRetryErrorMock,
getNextFallback: getNextFallbackMock,
hasMoreFallbacks: hasMoreFallbacksMock,
selectFallbackProvider: selectFallbackProviderMock,
}))
mock.module("../../shared/provider-model-id-transform", () => ({
transformModelForProvider: transformModelForProviderMock,
}))
const retryHandlerModule = await import(`./fallback-retry-handler?test=${Date.now()}-${Math.random()}`)
mock.restore()
return {
tryFallbackRetry: retryHandlerModule.tryFallbackRetry,
shouldRetryError: shouldRetryErrorMock,
selectFallbackProvider: selectFallbackProviderMock,
readProviderModelsCache: readProviderModelsCacheMock,
}
}
const { tryFallbackRetry, shouldRetryError, selectFallbackProvider, readProviderModelsCache } =
await importFreshFallbackRetryHandlerModule()
function createDeferredPromise(): {
promise: Promise<void>
+32 -24
View File
@@ -1,5 +1,4 @@
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"
@@ -8,41 +7,50 @@ const mockHttpConnect = mock(() => Promise.reject(new Error("Mocked HTTP connect
const mockHttpClose = mock(() => Promise.resolve())
let lastTransportInstance: { url?: URL; options?: { requestInit?: RequestInit } } = {}
mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
StreamableHTTPClientTransport: class MockStreamableHTTPClientTransport {
constructor(public url: URL, public options?: { requestInit?: RequestInit }) {
lastTransportInstance = { url, options }
}
async start() {
await mockHttpConnect()
}
async close() {
await mockHttpClose()
}
},
}))
// 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 {
tokens = mockTokens
login = mockLogin
constructor(_opts: unknown) {}
},
}))
async function importFreshManagerModule(): Promise<typeof import("./manager")> {
mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
StreamableHTTPClientTransport: class MockStreamableHTTPClientTransport {
constructor(public url: URL, public options?: { requestInit?: RequestInit }) {
lastTransportInstance = { url, options }
}
async start() {
await mockHttpConnect()
}
async close() {
await mockHttpClose()
}
},
}))
mock.module("../mcp-oauth/provider", () => ({
McpOAuthProvider: class MockMcpOAuthProvider {
tokens = mockTokens
login = mockLogin
constructor(_opts: unknown) {}
},
}))
const module = await import(`./manager?test=${Date.now()}-${Math.random()}`)
mock.restore()
return module
}
afterAll(() => { mock.restore() })
describe("SkillMcpManager", () => {
let manager: SkillMcpManager
let manager: any
beforeEach(() => {
beforeEach(async () => {
const { SkillMcpManager } = await importFreshManagerModule()
manager = new SkillMcpManager()
mockHttpConnect.mockClear()
mockHttpClose.mockClear()
mockTokens.mockClear()
mockLogin.mockClear()
})
afterEach(async () => {
+12 -12
View File
@@ -18,18 +18,6 @@ const mockAutoMigrate = mock((): MigrationResult => ({
const mockShowToast = mock((_arg: any) => Promise.resolve())
const mockLog = mock(() => {})
mock.module("../../shared/legacy-plugin-warning", () => ({
checkForLegacyPluginEntry: mockCheckForLegacyPluginEntry,
}))
mock.module("../../shared/logger", () => ({
log: mockLog,
}))
mock.module("./auto-migrate-runner", () => ({
autoMigrateLegacyPluginEntry: mockAutoMigrate,
}))
afterAll(() => {
mock.restore()
})
@@ -53,6 +41,18 @@ function createEvent(type: string, parentID?: string) {
}
async function importFreshModule() {
mock.module("../../shared/legacy-plugin-warning", () => ({
checkForLegacyPluginEntry: mockCheckForLegacyPluginEntry,
}))
mock.module("../../shared/logger", () => ({
log: mockLog,
}))
mock.module("./auto-migrate-runner", () => ({
autoMigrateLegacyPluginEntry: mockAutoMigrate,
}))
const module = await import(`./hook?t=${Date.now()}-${Math.random()}`)
mock.restore()
return module
+20 -15
View File
@@ -40,30 +40,35 @@ const transformModelForProviderMock = mock((provider: string, model: string) =>
return model
})
mock.module("../../shared/connected-providers-cache", () => ({
readConnectedProvidersCache: readConnectedProvidersCacheMock,
readProviderModelsCache: readProviderModelsCacheMock,
}))
mock.module("../../shared/provider-model-id-transform", () => ({
transformModelForProvider: transformModelForProviderMock,
}))
mock.module("../../shared/model-error-classifier", () => ({
selectFallbackProvider: selectFallbackProviderMock,
}))
afterAll(() => {
mock.restore()
})
async function importFreshModelFallbackHookModule() {
mock.module("../../shared/connected-providers-cache", () => ({
readConnectedProvidersCache: readConnectedProvidersCacheMock,
readProviderModelsCache: readProviderModelsCacheMock,
}))
mock.module("../../shared/provider-model-id-transform", () => ({
transformModelForProvider: transformModelForProviderMock,
}))
mock.module("../../shared/model-error-classifier", () => ({
selectFallbackProvider: selectFallbackProviderMock,
}))
const module = await import(`./hook?test=${Date.now()}-${Math.random()}`)
mock.restore()
return module
}
const {
clearPendingModelFallback,
createModelFallbackHook,
setSessionFallbackChain,
setPendingModelFallback,
} = await import("./hook")
mock.restore()
} = await importFreshModelFallbackHookModule()
describe("model fallback hook", () => {
beforeEach(() => {
+42 -45
View File
@@ -1,4 +1,4 @@
import { afterAll, afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
import * as fs from "node:fs";
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import * as os from "node:os";
@@ -17,45 +17,6 @@ const originalReadFileSync = fs.readFileSync.bind(fs);
const originalStatSync = fs.statSync.bind(fs);
const originalHomedir = os.homedir.bind(os);
mock.module("node:fs", () => ({
...fs,
readFileSync: (filePath: string, encoding?: string) => {
if (filePath === trackedRulePath) {
trackedReadFileCount += 1;
}
return originalReadFileSync(filePath, encoding as never);
},
statSync: (filePath: string) => {
if (filePath === trackedRulePath) {
const next = statSnapshots.shift();
if (next instanceof Error) {
throw next;
}
if (next) {
return {
mtimeMs: next.mtimeMs,
size: next.size,
isFile: () => true,
} as ReturnType<typeof originalStatSync>;
}
}
return originalStatSync(filePath);
},
}));
mock.module("node:os", () => ({
...os,
homedir: () => mockedHomeDir || originalHomedir(),
}));
mock.module("./matcher", () => ({
shouldApplyRule: () => ({ applies: true, reason: "matched" }),
isDuplicateByRealPath: (realPath: string, cache: Set<string>) =>
cache.has(realPath),
createContentHash: (content: string) => `hash:${content}`,
isDuplicateByContentHash: (hash: string, cache: Set<string>) => cache.has(hash),
}));
function createOutput(): { title: string; output: string; metadata: unknown } {
return { title: "tool", output: "", metadata: {} };
}
@@ -67,7 +28,47 @@ async function createProcessor(projectRoot: string): Promise<{
output: { title: string; output: string; metadata: unknown }
) => Promise<void>;
}> {
const { createRuleInjectionProcessor } = await import("./injector");
mock.module("node:fs", () => ({
...fs,
readFileSync: (filePath: string, encoding?: string) => {
if (filePath === trackedRulePath) {
trackedReadFileCount += 1;
}
return originalReadFileSync(filePath, encoding as never);
},
statSync: (filePath: string) => {
if (filePath === trackedRulePath) {
const next = statSnapshots.shift();
if (next instanceof Error) {
throw next;
}
if (next) {
return {
mtimeMs: next.mtimeMs,
size: next.size,
isFile: () => true,
} as ReturnType<typeof originalStatSync>;
}
}
return originalStatSync(filePath);
},
}));
mock.module("node:os", () => ({
...os,
homedir: () => mockedHomeDir || originalHomedir(),
}));
mock.module("./matcher", () => ({
shouldApplyRule: () => ({ applies: true, reason: "matched" }),
isDuplicateByRealPath: (realPath: string, cache: Set<string>) =>
cache.has(realPath),
createContentHash: (content: string) => `hash:${content}`,
isDuplicateByContentHash: (hash: string, cache: Set<string>) => cache.has(hash),
}));
const { createRuleInjectionProcessor } = await import(`./injector?test=${Date.now()}-${Math.random()}`);
mock.restore();
const sessionCaches = new Map<
string,
{ contentHashes: Set<string>; realPaths: Set<string> }
@@ -102,10 +103,6 @@ function getInjectedRulesPath(sessionID: string): string {
}
describe("createRuleInjectionProcessor", () => {
afterAll(() => {
mock.restore();
});
let testRoot: string;
let projectRoot: string;
let homeRoot: string;
@@ -20,23 +20,23 @@ const mockLog = mock(() => {})
const mockMigrateLegacyPluginEntry = mock(() => false)
let consoleWarnSpy: ReturnType<typeof spyOn>
mock.module("./legacy-plugin-warning", () => ({
checkForLegacyPluginEntry: mockCheckForLegacyPluginEntry,
}))
mock.module("./logger", () => ({
log: mockLog,
}))
mock.module("./migrate-legacy-plugin-entry", () => ({
migrateLegacyPluginEntry: mockMigrateLegacyPluginEntry,
}))
afterAll(() => {
mock.restore()
})
async function importFreshStartupWarningModule(): Promise<typeof import("./log-legacy-plugin-startup-warning")> {
mock.module("./legacy-plugin-warning", () => ({
checkForLegacyPluginEntry: mockCheckForLegacyPluginEntry,
}))
mock.module("./logger", () => ({
log: mockLog,
}))
mock.module("./migrate-legacy-plugin-entry", () => ({
migrateLegacyPluginEntry: mockMigrateLegacyPluginEntry,
}))
const module = await import(`./log-legacy-plugin-startup-warning?test=${Date.now()}-${Math.random()}`)
mock.restore()
consoleWarnSpy = spyOn(console, "warn").mockImplementation(() => {})
+17 -12
View File
@@ -1,22 +1,27 @@
import type { ModelCapabilitiesSnapshot } from "./model-capabilities"
import { afterAll, describe, expect, test, mock } from "bun:test"
// Mock connected-providers-cache to prevent local disk cache from polluting test results.
// Without this, findProviderModelMetadata reads real cached model metadata (e.g., from opencode serve)
// which causes the "prefers runtime models.dev cache" test to get different values than expected.
mock.module("./connected-providers-cache", () => ({
findProviderModelMetadata: () => undefined,
readConnectedProvidersCache: () => null,
hasConnectedProvidersCache: () => false,
hasProviderModelsCache: () => false,
}))
afterAll(() => {
mock.restore()
})
const { getModelCapabilities, getBundledModelCapabilitiesSnapshot } = await import("./model-capabilities")
mock.restore()
async function importFreshModelCapabilitiesModule() {
// Mock connected-providers-cache to prevent local disk cache from polluting test results.
// Without this, findProviderModelMetadata reads real cached model metadata (e.g., from opencode serve)
// which causes the "prefers runtime models.dev cache" test to get different values than expected.
mock.module("./connected-providers-cache", () => ({
findProviderModelMetadata: () => undefined,
readConnectedProvidersCache: () => null,
hasConnectedProvidersCache: () => false,
hasProviderModelsCache: () => false,
}))
const module = await import(`./model-capabilities?test=${Date.now()}-${Math.random()}`)
mock.restore()
return module
}
const { getModelCapabilities, getBundledModelCapabilitiesSnapshot } = await importFreshModelCapabilitiesModule()
import { AGENT_MODEL_REQUIREMENTS, CATEGORY_MODEL_REQUIREMENTS } from "./model-requirements"
describe("getModelCapabilities", () => {
+10 -5
View File
@@ -3,14 +3,19 @@ const { describe, expect, test, beforeEach, mock, afterAll } = require("bun:test
const readConnectedProvidersCacheMock = mock(() => null)
mock.module("./connected-providers-cache", () => ({
readConnectedProvidersCache: readConnectedProvidersCacheMock,
}))
async function importFreshModelErrorClassifierModule() {
mock.module("./connected-providers-cache", () => ({
readConnectedProvidersCache: readConnectedProvidersCacheMock,
}))
const module = await import(`./model-error-classifier?test=${Date.now()}-${Math.random()}`)
mock.restore()
return module
}
afterAll(() => { mock.restore() })
const { shouldRetryError, selectFallbackProvider } = await import("./model-error-classifier")
mock.restore()
const { shouldRetryError, selectFallbackProvider } = await importFreshModelErrorClassifierModule()
describe("model-error-classifier", () => {
beforeEach(() => {
+17 -10
View File
@@ -1,7 +1,6 @@
import { afterAll, beforeEach, describe, expect, it, mock, spyOn } from "bun:test"
import type { ToolContext } from "@opencode-ai/plugin/tool"
import * as fs from "node:fs"
import { createSkillTool } from "./tools"
import { SkillMcpManager } from "../../features/skill-mcp-manager"
import type { LoadedSkill } from "../../features/opencode-skill-loader/types"
import type { CommandInfo } from "../slashcommand/types"
@@ -9,18 +8,26 @@ import type { Tool as McpTool } from "@modelcontextprotocol/sdk/types.js"
const originalReadFileSync = fs.readFileSync.bind(fs)
mock.module("node:fs", () => ({
...fs,
readFileSync: (path: string, encoding?: string) => {
if (typeof path === "string" && path.includes("/skills/")) {
return `---
async function importFreshSkillToolModule(): Promise<typeof import("./tools")> {
mock.module("node:fs", () => ({
...fs,
readFileSync: (path: string, encoding?: string) => {
if (typeof path === "string" && path.includes("/skills/")) {
return `---
description: Test skill description
---
Test skill body content`
}
return originalReadFileSync(path, encoding as BufferEncoding)
},
}))
}
return originalReadFileSync(path, encoding as BufferEncoding)
},
}))
const module = await import(`./tools?test=${Date.now()}-${Math.random()}`)
mock.restore()
return module
}
const { createSkillTool } = await importFreshSkillToolModule()
afterAll(() => {
mock.restore()