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" import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test"
mock.module("../../shared/logger", () => ({ const sharedLogMock = mock(() => {})
log: mock(() => {}), const readConnectedProvidersCacheMock = mock(() => null)
})) const readProviderModelsCacheMock = mock(() => null)
const shouldRetryErrorMock = mock(() => true)
mock.module("../../shared/connected-providers-cache", () => ({ const getNextFallbackMock = mock((chain: Array<{ model: string }>, attempt: number) => chain[attempt])
readConnectedProvidersCache: mock(() => null), const hasMoreFallbacksMock = mock((chain: Array<{ model: string }>, attempt: number) => attempt < chain.length)
readProviderModelsCache: mock(() => null), const selectFallbackProviderMock = mock((providers: string[]) => providers[0])
})) const transformModelForProviderMock = mock((_provider: string, model: string) => model)
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),
}))
import type { BackgroundTask } from "./types" import type { BackgroundTask } from "./types"
import type { ConcurrencyManager } from "./concurrency" import type { ConcurrencyManager } from "./concurrency"
import type { OpencodeClient, QueueItem } from "./constants" import type { OpencodeClient, QueueItem } from "./constants"
const { tryFallbackRetry } = await import("./fallback-retry-handler") async function importFreshFallbackRetryHandlerModule() {
const { shouldRetryError, selectFallbackProvider } = await import("../../shared/model-error-classifier") mock.module("../../shared/logger", () => ({
const { readProviderModelsCache } = await import("../../shared") log: sharedLogMock,
mock.restore() }))
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(): { function createDeferredPromise(): {
promise: Promise<void> promise: Promise<void>
+32 -24
View File
@@ -1,5 +1,4 @@
import { describe, it, expect, beforeEach, afterEach, afterAll, 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 { SkillMcpClientInfo, SkillMcpServerContext } from "./types"
import type { ClaudeCodeMcpServer } from "../claude-code-mcp-loader/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()) const mockHttpClose = mock(() => Promise.resolve())
let lastTransportInstance: { url?: URL; options?: { requestInit?: RequestInit } } = {} 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 // Mock OAuth provider for OAuth integration tests
const mockTokens = mock(() => null as { accessToken: string } | null) const mockTokens = mock(() => null as { accessToken: string } | null)
const mockLogin = mock(() => Promise.resolve({ accessToken: "test-token" }) as Promise<{ accessToken: string } | null>) const mockLogin = mock(() => Promise.resolve({ accessToken: "test-token" }) as Promise<{ accessToken: string } | null>)
mock.module("../mcp-oauth/provider", () => ({ async function importFreshManagerModule(): Promise<typeof import("./manager")> {
McpOAuthProvider: class MockMcpOAuthProvider { mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
tokens = mockTokens StreamableHTTPClientTransport: class MockStreamableHTTPClientTransport {
login = mockLogin constructor(public url: URL, public options?: { requestInit?: RequestInit }) {
constructor(_opts: unknown) {} 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() }) afterAll(() => { mock.restore() })
describe("SkillMcpManager", () => { describe("SkillMcpManager", () => {
let manager: SkillMcpManager let manager: any
beforeEach(() => { beforeEach(async () => {
const { SkillMcpManager } = await importFreshManagerModule()
manager = new SkillMcpManager() manager = new SkillMcpManager()
mockHttpConnect.mockClear() mockHttpConnect.mockClear()
mockHttpClose.mockClear() mockHttpClose.mockClear()
mockTokens.mockClear()
mockLogin.mockClear()
}) })
afterEach(async () => { afterEach(async () => {
+12 -12
View File
@@ -18,18 +18,6 @@ const mockAutoMigrate = mock((): MigrationResult => ({
const mockShowToast = mock((_arg: any) => Promise.resolve()) const mockShowToast = mock((_arg: any) => Promise.resolve())
const mockLog = mock(() => {}) 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(() => { afterAll(() => {
mock.restore() mock.restore()
}) })
@@ -53,6 +41,18 @@ function createEvent(type: string, parentID?: string) {
} }
async function importFreshModule() { 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()}`) const module = await import(`./hook?t=${Date.now()}-${Math.random()}`)
mock.restore() mock.restore()
return module return module
+20 -15
View File
@@ -40,30 +40,35 @@ const transformModelForProviderMock = mock((provider: string, model: string) =>
return model 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(() => { afterAll(() => {
mock.restore() 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 { const {
clearPendingModelFallback, clearPendingModelFallback,
createModelFallbackHook, createModelFallbackHook,
setSessionFallbackChain, setSessionFallbackChain,
setPendingModelFallback, setPendingModelFallback,
} = await import("./hook") } = await importFreshModelFallbackHookModule()
mock.restore()
describe("model fallback hook", () => { describe("model fallback hook", () => {
beforeEach(() => { 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 * as fs from "node:fs";
import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import * as os from "node:os"; import * as os from "node:os";
@@ -17,45 +17,6 @@ const originalReadFileSync = fs.readFileSync.bind(fs);
const originalStatSync = fs.statSync.bind(fs); const originalStatSync = fs.statSync.bind(fs);
const originalHomedir = os.homedir.bind(os); 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 } { function createOutput(): { title: string; output: string; metadata: unknown } {
return { title: "tool", output: "", metadata: {} }; return { title: "tool", output: "", metadata: {} };
} }
@@ -67,7 +28,47 @@ async function createProcessor(projectRoot: string): Promise<{
output: { title: string; output: string; metadata: unknown } output: { title: string; output: string; metadata: unknown }
) => Promise<void>; ) => 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< const sessionCaches = new Map<
string, string,
{ contentHashes: Set<string>; realPaths: Set<string> } { contentHashes: Set<string>; realPaths: Set<string> }
@@ -102,10 +103,6 @@ function getInjectedRulesPath(sessionID: string): string {
} }
describe("createRuleInjectionProcessor", () => { describe("createRuleInjectionProcessor", () => {
afterAll(() => {
mock.restore();
});
let testRoot: string; let testRoot: string;
let projectRoot: string; let projectRoot: string;
let homeRoot: string; let homeRoot: string;
@@ -20,23 +20,23 @@ const mockLog = mock(() => {})
const mockMigrateLegacyPluginEntry = mock(() => false) const mockMigrateLegacyPluginEntry = mock(() => false)
let consoleWarnSpy: ReturnType<typeof spyOn> 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(() => { afterAll(() => {
mock.restore() mock.restore()
}) })
async function importFreshStartupWarningModule(): Promise<typeof import("./log-legacy-plugin-startup-warning")> { 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()}`) const module = await import(`./log-legacy-plugin-startup-warning?test=${Date.now()}-${Math.random()}`)
mock.restore() mock.restore()
consoleWarnSpy = spyOn(console, "warn").mockImplementation(() => {}) consoleWarnSpy = spyOn(console, "warn").mockImplementation(() => {})
+17 -12
View File
@@ -1,22 +1,27 @@
import type { ModelCapabilitiesSnapshot } from "./model-capabilities" import type { ModelCapabilitiesSnapshot } from "./model-capabilities"
import { afterAll, describe, expect, test, mock } from "bun:test" 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(() => { afterAll(() => {
mock.restore() mock.restore()
}) })
const { getModelCapabilities, getBundledModelCapabilitiesSnapshot } = await import("./model-capabilities") async function importFreshModelCapabilitiesModule() {
mock.restore() // 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" import { AGENT_MODEL_REQUIREMENTS, CATEGORY_MODEL_REQUIREMENTS } from "./model-requirements"
describe("getModelCapabilities", () => { describe("getModelCapabilities", () => {
+10 -5
View File
@@ -3,14 +3,19 @@ const { describe, expect, test, beforeEach, mock, afterAll } = require("bun:test
const readConnectedProvidersCacheMock = mock(() => null) const readConnectedProvidersCacheMock = mock(() => null)
mock.module("./connected-providers-cache", () => ({ async function importFreshModelErrorClassifierModule() {
readConnectedProvidersCache: readConnectedProvidersCacheMock, 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() }) afterAll(() => { mock.restore() })
const { shouldRetryError, selectFallbackProvider } = await import("./model-error-classifier") const { shouldRetryError, selectFallbackProvider } = await importFreshModelErrorClassifierModule()
mock.restore()
describe("model-error-classifier", () => { describe("model-error-classifier", () => {
beforeEach(() => { beforeEach(() => {
+17 -10
View File
@@ -1,7 +1,6 @@
import { afterAll, beforeEach, describe, expect, it, mock, spyOn } from "bun:test" import { afterAll, beforeEach, describe, expect, it, mock, spyOn } from "bun:test"
import type { ToolContext } from "@opencode-ai/plugin/tool" import type { ToolContext } from "@opencode-ai/plugin/tool"
import * as fs from "node:fs" import * as fs from "node:fs"
import { createSkillTool } from "./tools"
import { SkillMcpManager } from "../../features/skill-mcp-manager" import { SkillMcpManager } from "../../features/skill-mcp-manager"
import type { LoadedSkill } from "../../features/opencode-skill-loader/types" import type { LoadedSkill } from "../../features/opencode-skill-loader/types"
import type { CommandInfo } from "../slashcommand/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) const originalReadFileSync = fs.readFileSync.bind(fs)
mock.module("node:fs", () => ({ async function importFreshSkillToolModule(): Promise<typeof import("./tools")> {
...fs, mock.module("node:fs", () => ({
readFileSync: (path: string, encoding?: string) => { ...fs,
if (typeof path === "string" && path.includes("/skills/")) { readFileSync: (path: string, encoding?: string) => {
return `--- if (typeof path === "string" && path.includes("/skills/")) {
return `---
description: Test skill description description: Test skill description
--- ---
Test skill body content` 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(() => { afterAll(() => {
mock.restore() mock.restore()