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,34 +1,53 @@
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.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() 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>
resolve: () => void resolve: () => void
+15 -7
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,6 +7,11 @@ 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 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>)
async function importFreshManagerModule(): Promise<typeof import("./manager")> {
mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
StreamableHTTPClientTransport: class MockStreamableHTTPClientTransport { StreamableHTTPClientTransport: class MockStreamableHTTPClientTransport {
constructor(public url: URL, public options?: { requestInit?: RequestInit }) { constructor(public url: URL, public options?: { requestInit?: RequestInit }) {
@@ -22,10 +26,6 @@ mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
}, },
})) }))
// 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", () => ({ mock.module("../mcp-oauth/provider", () => ({
McpOAuthProvider: class MockMcpOAuthProvider { McpOAuthProvider: class MockMcpOAuthProvider {
tokens = mockTokens tokens = mockTokens
@@ -34,15 +34,23 @@ mock.module("../mcp-oauth/provider", () => ({
}, },
})) }))
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
+9 -4
View File
@@ -40,6 +40,11 @@ const transformModelForProviderMock = mock((provider: string, model: string) =>
return model return model
}) })
afterAll(() => {
mock.restore()
})
async function importFreshModelFallbackHookModule() {
mock.module("../../shared/connected-providers-cache", () => ({ mock.module("../../shared/connected-providers-cache", () => ({
readConnectedProvidersCache: readConnectedProvidersCacheMock, readConnectedProvidersCache: readConnectedProvidersCacheMock,
readProviderModelsCache: readProviderModelsCacheMock, readProviderModelsCache: readProviderModelsCacheMock,
@@ -53,17 +58,17 @@ mock.module("../../shared/model-error-classifier", () => ({
selectFallbackProvider: selectFallbackProviderMock, selectFallbackProvider: selectFallbackProviderMock,
})) }))
afterAll(() => { const module = await import(`./hook?test=${Date.now()}-${Math.random()}`)
mock.restore() 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(() => {
+14 -17
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,6 +17,17 @@ 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);
function createOutput(): { title: string; output: string; metadata: unknown } {
return { title: "tool", output: "", metadata: {} };
}
async function createProcessor(projectRoot: string): Promise<{
processFilePathForInjection: (
filePath: string,
sessionID: string,
output: { title: string; output: string; metadata: unknown }
) => Promise<void>;
}> {
mock.module("node:fs", () => ({ mock.module("node:fs", () => ({
...fs, ...fs,
readFileSync: (filePath: string, encoding?: string) => { readFileSync: (filePath: string, encoding?: string) => {
@@ -56,18 +67,8 @@ mock.module("./matcher", () => ({
isDuplicateByContentHash: (hash: string, cache: Set<string>) => cache.has(hash), isDuplicateByContentHash: (hash: string, cache: Set<string>) => cache.has(hash),
})); }));
function createOutput(): { title: string; output: string; metadata: unknown } { const { createRuleInjectionProcessor } = await import(`./injector?test=${Date.now()}-${Math.random()}`);
return { title: "tool", output: "", metadata: {} }; mock.restore();
}
async function createProcessor(projectRoot: string): Promise<{
processFilePathForInjection: (
filePath: string,
sessionID: string,
output: { title: string; output: string; metadata: unknown }
) => Promise<void>;
}> {
const { createRuleInjectionProcessor } = await import("./injector");
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,6 +20,11 @@ const mockLog = mock(() => {})
const mockMigrateLegacyPluginEntry = mock(() => false) const mockMigrateLegacyPluginEntry = mock(() => false)
let consoleWarnSpy: ReturnType<typeof spyOn> let consoleWarnSpy: ReturnType<typeof spyOn>
afterAll(() => {
mock.restore()
})
async function importFreshStartupWarningModule(): Promise<typeof import("./log-legacy-plugin-startup-warning")> {
mock.module("./legacy-plugin-warning", () => ({ mock.module("./legacy-plugin-warning", () => ({
checkForLegacyPluginEntry: mockCheckForLegacyPluginEntry, checkForLegacyPluginEntry: mockCheckForLegacyPluginEntry,
})) }))
@@ -32,11 +37,6 @@ mock.module("./migrate-legacy-plugin-entry", () => ({
migrateLegacyPluginEntry: mockMigrateLegacyPluginEntry, migrateLegacyPluginEntry: mockMigrateLegacyPluginEntry,
})) }))
afterAll(() => {
mock.restore()
})
async function importFreshStartupWarningModule(): Promise<typeof import("./log-legacy-plugin-startup-warning")> {
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(() => {})
+9 -4
View File
@@ -1,6 +1,11 @@
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"
afterAll(() => {
mock.restore()
})
async function importFreshModelCapabilitiesModule() {
// Mock connected-providers-cache to prevent local disk cache from polluting test results. // 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) // 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. // which causes the "prefers runtime models.dev cache" test to get different values than expected.
@@ -11,12 +16,12 @@ mock.module("./connected-providers-cache", () => ({
hasProviderModelsCache: () => false, hasProviderModelsCache: () => false,
})) }))
afterAll(() => { const module = await import(`./model-capabilities?test=${Date.now()}-${Math.random()}`)
mock.restore() mock.restore()
}) return module
}
const { getModelCapabilities, getBundledModelCapabilitiesSnapshot } = await import("./model-capabilities") const { getModelCapabilities, getBundledModelCapabilitiesSnapshot } = await importFreshModelCapabilitiesModule()
mock.restore()
import { AGENT_MODEL_REQUIREMENTS, CATEGORY_MODEL_REQUIREMENTS } from "./model-requirements" import { AGENT_MODEL_REQUIREMENTS, CATEGORY_MODEL_REQUIREMENTS } from "./model-requirements"
describe("getModelCapabilities", () => { describe("getModelCapabilities", () => {
+7 -2
View File
@@ -3,14 +3,19 @@ const { describe, expect, test, beforeEach, mock, afterAll } = require("bun:test
const readConnectedProvidersCacheMock = mock(() => null) const readConnectedProvidersCacheMock = mock(() => null)
async function importFreshModelErrorClassifierModule() {
mock.module("./connected-providers-cache", () => ({ mock.module("./connected-providers-cache", () => ({
readConnectedProvidersCache: readConnectedProvidersCacheMock, 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(() => {
+8 -1
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,6 +8,7 @@ import type { Tool as McpTool } from "@modelcontextprotocol/sdk/types.js"
const originalReadFileSync = fs.readFileSync.bind(fs) const originalReadFileSync = fs.readFileSync.bind(fs)
async function importFreshSkillToolModule(): Promise<typeof import("./tools")> {
mock.module("node:fs", () => ({ mock.module("node:fs", () => ({
...fs, ...fs,
readFileSync: (path: string, encoding?: string) => { readFileSync: (path: string, encoding?: string) => {
@@ -22,6 +22,13 @@ Test skill body content`
}, },
})) }))
const module = await import(`./tools?test=${Date.now()}-${Math.random()}`)
mock.restore()
return module
}
const { createSkillTool } = await importFreshSkillToolModule()
afterAll(() => { afterAll(() => {
mock.restore() mock.restore()
}) })