diff --git a/src/features/background-agent/fallback-retry-handler.test.ts b/src/features/background-agent/fallback-retry-handler.test.ts index 689785ba2..78e632bd2 100644 --- a/src/features/background-agent/fallback-retry-handler.test.ts +++ b/src/features/background-agent/fallback-retry-handler.test.ts @@ -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 diff --git a/src/features/skill-mcp-manager/manager.test.ts b/src/features/skill-mcp-manager/manager.test.ts index 5a3525a0b..541ab18f8 100644 --- a/src/features/skill-mcp-manager/manager.test.ts +++ b/src/features/skill-mcp-manager/manager.test.ts @@ -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 { + 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 () => { diff --git a/src/hooks/legacy-plugin-toast/hook.test.ts b/src/hooks/legacy-plugin-toast/hook.test.ts index e44811be5..4f889aefa 100644 --- a/src/hooks/legacy-plugin-toast/hook.test.ts +++ b/src/hooks/legacy-plugin-toast/hook.test.ts @@ -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 diff --git a/src/hooks/model-fallback/hook.test.ts b/src/hooks/model-fallback/hook.test.ts index a401314ab..8c59db5f5 100644 --- a/src/hooks/model-fallback/hook.test.ts +++ b/src/hooks/model-fallback/hook.test.ts @@ -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(() => { diff --git a/src/hooks/rules-injector/injector.test.ts b/src/hooks/rules-injector/injector.test.ts index 6df726e7c..129d6364b 100644 --- a/src/hooks/rules-injector/injector.test.ts +++ b/src/hooks/rules-injector/injector.test.ts @@ -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; - } - } - return originalStatSync(filePath); - }, -})); - -mock.module("node:os", () => ({ - ...os, - homedir: () => mockedHomeDir || originalHomedir(), -})); - -mock.module("./matcher", () => ({ - shouldApplyRule: () => ({ applies: true, reason: "matched" }), - isDuplicateByRealPath: (realPath: string, cache: Set) => - cache.has(realPath), - createContentHash: (content: string) => `hash:${content}`, - isDuplicateByContentHash: (hash: string, cache: Set) => 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; }> { - 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; + } + } + return originalStatSync(filePath); + }, + })); + + mock.module("node:os", () => ({ + ...os, + homedir: () => mockedHomeDir || originalHomedir(), + })); + + mock.module("./matcher", () => ({ + shouldApplyRule: () => ({ applies: true, reason: "matched" }), + isDuplicateByRealPath: (realPath: string, cache: Set) => + cache.has(realPath), + createContentHash: (content: string) => `hash:${content}`, + isDuplicateByContentHash: (hash: string, cache: Set) => cache.has(hash), + })); + + const { createRuleInjectionProcessor } = await import(`./injector?test=${Date.now()}-${Math.random()}`); + mock.restore(); const sessionCaches = new Map< string, { contentHashes: Set; realPaths: Set } @@ -102,10 +103,6 @@ function getInjectedRulesPath(sessionID: string): string { } describe("createRuleInjectionProcessor", () => { - afterAll(() => { - mock.restore(); - }); - let testRoot: string; let projectRoot: string; let homeRoot: string; diff --git a/src/shared/log-legacy-plugin-startup-warning.test.ts b/src/shared/log-legacy-plugin-startup-warning.test.ts index 41a034975..421d2a7f3 100644 --- a/src/shared/log-legacy-plugin-startup-warning.test.ts +++ b/src/shared/log-legacy-plugin-startup-warning.test.ts @@ -20,23 +20,23 @@ const mockLog = mock(() => {}) const mockMigrateLegacyPluginEntry = mock(() => false) let consoleWarnSpy: ReturnType -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 { + 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(() => {}) diff --git a/src/shared/model-capabilities.test.ts b/src/shared/model-capabilities.test.ts index 917b4518a..711ee7906 100644 --- a/src/shared/model-capabilities.test.ts +++ b/src/shared/model-capabilities.test.ts @@ -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", () => { diff --git a/src/shared/model-error-classifier.test.ts b/src/shared/model-error-classifier.test.ts index 4cb870803..60c2572a9 100644 --- a/src/shared/model-error-classifier.test.ts +++ b/src/shared/model-error-classifier.test.ts @@ -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(() => { diff --git a/src/tools/skill/tools.test.ts b/src/tools/skill/tools.test.ts index bf7ebf451..5a51bec7a 100644 --- a/src/tools/skill/tools.test.ts +++ b/src/tools/skill/tools.test.ts @@ -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 { + 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()