fix(ci): resolve mock.module() cross-file leakage in test suite

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-04-04 02:35:23 +09:00
parent 673b107f53
commit 6acca09bd0
15 changed files with 130 additions and 66 deletions
@@ -6,9 +6,11 @@ mock.module("../../shared", () => ({
log: logMock, log: logMock,
})) }))
import { abortWithTimeout } from "./abort-with-timeout"
import type { OpencodeClient } from "./opencode-client" import type { OpencodeClient } from "./opencode-client"
const { abortWithTimeout } = await import("./abort-with-timeout")
mock.restore()
function createClient(abort: (...args: Array<unknown>) => Promise<unknown>): OpencodeClient { function createClient(abort: (...args: Array<unknown>) => Promise<unknown>): OpencodeClient {
return { return {
session: { session: {
@@ -17,14 +17,15 @@ mock.module("../../shared/provider-model-id-transform", () => ({
transformModelForProvider: mock((_provider: string, model: string) => model), transformModelForProvider: mock((_provider: string, model: string) => model),
})) }))
import { tryFallbackRetry } from "./fallback-retry-handler"
import { shouldRetryError } from "../../shared/model-error-classifier"
import { selectFallbackProvider } from "../../shared/model-error-classifier"
import { readProviderModelsCache } from "../../shared"
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")
const { shouldRetryError, selectFallbackProvider } = await import("../../shared/model-error-classifier")
const { readProviderModelsCache } = await import("../../shared")
mock.restore()
function createDeferredPromise(): { function createDeferredPromise(): {
promise: Promise<void> promise: Promise<void>
resolve: () => void resolve: () => void
@@ -20,6 +20,7 @@ import { MIN_IDLE_TIME_MS } from "./constants"
import { BackgroundManager } from "./manager" import { BackgroundManager } from "./manager"
import { ConcurrencyManager } from "./concurrency" import { ConcurrencyManager } from "./concurrency"
import { initTaskToastManager, _resetTaskToastManagerForTesting } from "../task-toast-manager/manager" import { initTaskToastManager, _resetTaskToastManagerForTesting } from "../task-toast-manager/manager"
mock.restore()
const TASK_TTL_MS = 30 * 60 * 1000 const TASK_TTL_MS = 30 * 60 * 1000
@@ -1,11 +1,13 @@
import { describe, test, expect, mock, afterAll } from "bun:test" import { describe, test, expect, mock, afterAll } from "bun:test"
import { isActiveSessionStatus, isTerminalSessionStatus } from "./session-status-classifier"
const mockLog = mock() const mockLog = mock()
mock.module("../../shared", () => ({ log: mockLog })) mock.module("../../shared", () => ({ log: mockLog }))
afterAll(() => { mock.restore() }) afterAll(() => { mock.restore() })
const { isActiveSessionStatus, isTerminalSessionStatus } = await import("./session-status-classifier")
mock.restore()
describe("isActiveSessionStatus", () => { describe("isActiveSessionStatus", () => {
describe("#given a known active session status", () => { describe("#given a known active session status", () => {
test('#when type is "busy" #then returns true', () => { test('#when type is "busy" #then returns true', () => {
@@ -33,7 +33,9 @@ mock.module("../session-recovery/storage/text-part-injector", () => ({
})) }))
async function importFreshMessageBuilder(): Promise<typeof import("./message-builder")> { async function importFreshMessageBuilder(): Promise<typeof import("./message-builder")> {
return import(`./message-builder?test=${Date.now()}-${Math.random()}`) const module = await import(`./message-builder?test=${Date.now()}-${Math.random()}`)
mock.restore()
return module
} }
afterAll(() => { afterAll(() => {
@@ -1,13 +1,19 @@
import { afterAll, describe, expect, it, mock } from "bun:test" import { afterEach, describe, expect, it, spyOn } from "bun:test"
import type { LoadedSkill } from "../../features/opencode-skill-loader" import type { LoadedSkill } from "../../features/opencode-skill-loader"
import * as shared from "../../shared"
import * as slashcommand from "../../tools/slashcommand"
import { executeSlashCommand } from "./executor"
mock.module("../../shared", () => ({ let resolveCommandsInTextSpy: { mockRestore: () => void } | undefined
resolveCommandsInText: async (content: string) => content, let resolveFileReferencesInTextSpy: { mockRestore: () => void } | undefined
resolveFileReferencesInText: async (content: string) => content, let discoverCommandsSyncSpy: { mockRestore: () => void } | undefined
}))
mock.module("../../tools/slashcommand", () => ({ function setupExecutorSpies(): void {
discoverCommandsSync: () => [ resolveCommandsInTextSpy = spyOn(shared, "resolveCommandsInText")
.mockImplementation(async (content: string) => content)
resolveFileReferencesInTextSpy = spyOn(shared, "resolveFileReferencesInText")
.mockImplementation(async (content: string) => content)
discoverCommandsSyncSpy = spyOn(slashcommand, "discoverCommandsSync").mockReturnValue([
{ {
name: "shadowed", name: "shadowed",
metadata: { name: "shadowed", description: "builtin" }, metadata: { name: "shadowed", description: "builtin" },
@@ -20,18 +26,19 @@ mock.module("../../tools/slashcommand", () => ({
content: "project template", content: "project template",
scope: "project", scope: "project",
}, },
], ])
})) }
mock.module("../../features/opencode-skill-loader", () => ({ function restoreExecutorSpies(): void {
discoverAllSkills: async (): Promise<LoadedSkill[]> => [], resolveCommandsInTextSpy?.mockRestore()
})) resolveFileReferencesInTextSpy?.mockRestore()
discoverCommandsSyncSpy?.mockRestore()
resolveCommandsInTextSpy = undefined
resolveFileReferencesInTextSpy = undefined
discoverCommandsSyncSpy = undefined
}
afterAll(() => { afterEach(restoreExecutorSpies)
mock.restore()
})
const { executeSlashCommand } = await import("./executor")
function createRestrictedSkill(): LoadedSkill { function createRestrictedSkill(): LoadedSkill {
return { return {
@@ -49,6 +56,7 @@ function createRestrictedSkill(): LoadedSkill {
describe("executeSlashCommand resolution semantics", () => { describe("executeSlashCommand resolution semantics", () => {
it("returns project command when project and builtin names collide", async () => { it("returns project command when project and builtin names collide", async () => {
//#given //#given
setupExecutorSpies()
const parsed = { const parsed = {
command: "shadowed", command: "shadowed",
args: "", args: "",
@@ -67,6 +75,7 @@ describe("executeSlashCommand resolution semantics", () => {
it("blocks slash skill invocation when invoking agent is missing", async () => { it("blocks slash skill invocation when invoking agent is missing", async () => {
//#given //#given
setupExecutorSpies()
const parsed = { const parsed = {
command: "restricted-skill", command: "restricted-skill",
args: "", args: "",
@@ -83,6 +92,7 @@ describe("executeSlashCommand resolution semantics", () => {
it("allows slash skill invocation when invoking agent matches restriction", async () => { it("allows slash skill invocation when invoking agent matches restriction", async () => {
//#given //#given
setupExecutorSpies()
const parsed = { const parsed = {
command: "restricted-skill", command: "restricted-skill",
args: "", args: "",
+3 -1
View File
@@ -53,7 +53,9 @@ function createEvent(type: string, parentID?: string) {
} }
async function importFreshModule() { async function importFreshModule() {
return import(`./hook?t=${Date.now()}-${Math.random()}`) const module = await import(`./hook?t=${Date.now()}-${Math.random()}`)
mock.restore()
return module
} }
describe("createLegacyPluginToastHook", () => { describe("createLegacyPluginToastHook", () => {
+5 -2
View File
@@ -57,12 +57,13 @@ afterAll(() => {
mock.restore() mock.restore()
}) })
import { const {
clearPendingModelFallback, clearPendingModelFallback,
createModelFallbackHook, createModelFallbackHook,
setSessionFallbackChain, setSessionFallbackChain,
setPendingModelFallback, setPendingModelFallback,
} from "./hook" } = await import("./hook")
mock.restore()
describe("model fallback hook", () => { describe("model fallback hook", () => {
beforeEach(() => { beforeEach(() => {
@@ -452,3 +453,5 @@ describe("model fallback hook", () => {
clearPendingModelFallback(sessionID) clearPendingModelFallback(sessionID)
}) })
}) })
export {}
+27 -14
View File
@@ -1,9 +1,13 @@
/// <reference types="bun-types" /> /// <reference types="bun-types" />
import { beforeEach, describe, expect, it, mock } from "bun:test" import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test"
import type { PluginInput } from "@opencode-ai/plugin" import type { PluginInput } from "@opencode-ai/plugin"
import type { ImageDimensions, ResizeResult } from "./types" import type { ImageDimensions, ResizeResult } from "./types"
import * as imageDimensions from "./image-dimensions"
import * as imageResizer from "./image-resizer"
import * as sessionModelState from "../../shared/session-model-state"
import { createReadImageResizerHook } from "./hook"
const mockParseImageDimensions = mock((): ImageDimensions | null => null) const mockParseImageDimensions = mock((): ImageDimensions | null => null)
const mockCalculateTargetDimensions = mock((): ImageDimensions | null => null) const mockCalculateTargetDimensions = mock((): ImageDimensions | null => null)
@@ -13,20 +17,17 @@ const mockGetSessionModel = mock((_sessionID: string) => ({
modelID: "claude-sonnet-4-6", modelID: "claude-sonnet-4-6",
} as { providerID: string; modelID: string } | undefined)) } as { providerID: string; modelID: string } | undefined))
mock.module("./image-dimensions", () => ({ let parseImageDimensionsSpy: { mockRestore: () => void } | undefined
parseImageDimensions: mockParseImageDimensions, let calculateTargetDimensionsSpy: { mockRestore: () => void } | undefined
})) let resizeImageSpy: { mockRestore: () => void } | undefined
let getSessionModelSpy: { mockRestore: () => void } | undefined
mock.module("./image-resizer", () => ({ function setupHookSpies(): void {
calculateTargetDimensions: mockCalculateTargetDimensions, parseImageDimensionsSpy = spyOn(imageDimensions, "parseImageDimensions").mockImplementation(mockParseImageDimensions)
resizeImage: mockResizeImage, calculateTargetDimensionsSpy = spyOn(imageResizer, "calculateTargetDimensions").mockImplementation(mockCalculateTargetDimensions)
})) resizeImageSpy = spyOn(imageResizer, "resizeImage").mockImplementation(mockResizeImage)
getSessionModelSpy = spyOn(sessionModelState, "getSessionModel").mockImplementation(mockGetSessionModel)
mock.module("../../shared/session-model-state", () => ({ }
getSessionModel: mockGetSessionModel,
}))
import { createReadImageResizerHook } from "./hook"
type ToolOutput = { type ToolOutput = {
title: string title: string
@@ -52,6 +53,7 @@ function createInput(tool: string): { tool: string; sessionID: string; callID: s
describe("createReadImageResizerHook", () => { describe("createReadImageResizerHook", () => {
beforeEach(() => { beforeEach(() => {
setupHookSpies()
mockParseImageDimensions.mockReset() mockParseImageDimensions.mockReset()
mockCalculateTargetDimensions.mockReset() mockCalculateTargetDimensions.mockReset()
mockResizeImage.mockReset() mockResizeImage.mockReset()
@@ -59,6 +61,17 @@ describe("createReadImageResizerHook", () => {
mockGetSessionModel.mockReturnValue({ providerID: "anthropic", modelID: "claude-sonnet-4-6" }) mockGetSessionModel.mockReturnValue({ providerID: "anthropic", modelID: "claude-sonnet-4-6" })
}) })
afterEach(() => {
parseImageDimensionsSpy?.mockRestore()
calculateTargetDimensionsSpy?.mockRestore()
resizeImageSpy?.mockRestore()
getSessionModelSpy?.mockRestore()
parseImageDimensionsSpy = undefined
calculateTargetDimensionsSpy = undefined
resizeImageSpy = undefined
getSessionModelSpy = undefined
})
it("skips non-Read tools", async () => { it("skips non-Read tools", async () => {
//#given //#given
const hook = createReadImageResizerHook(createMockContext()) const hook = createReadImageResizerHook(createMockContext())
@@ -22,6 +22,7 @@ afterAll(() => {
}) })
const { recoverToolResultMissing } = await import("./recover-tool-result-missing") const { recoverToolResultMissing } = await import("./recover-tool-result-missing")
mock.restore()
function createMockClient(messages: MessageData[] = []) { function createMockClient(messages: MessageData[] = []) {
const promptAsync = mock(() => Promise.resolve({})) const promptAsync = mock(() => Promise.resolve({}))
+17 -8
View File
@@ -1,19 +1,23 @@
declare const require: (name: string) => any declare const require: (name: string) => any
const { afterEach, afterAll, describe, expect, mock, test } = require("bun:test") const { afterEach, describe, expect, spyOn, test } = require("bun:test")
mock.module("../shared/connected-providers-cache", () => ({
readConnectedProvidersCache: () => null,
readProviderModelsCache: () => null,
}))
afterAll(() => { mock.restore() })
import { createEventHandler } from "./event" import { createEventHandler } from "./event"
import { createChatMessageHandler } from "./chat-message" import { createChatMessageHandler } from "./chat-message"
import { _resetForTesting, setMainSession } from "../features/claude-code-session-state" import { _resetForTesting, setMainSession } from "../features/claude-code-session-state"
import { createModelFallbackHook, clearPendingModelFallback } from "../hooks/model-fallback/hook" import { createModelFallbackHook, clearPendingModelFallback } from "../hooks/model-fallback/hook"
import * as connectedProvidersCache from "../shared/connected-providers-cache"
let readConnectedProvidersCacheSpy: { mockRestore: () => void } | undefined
let readProviderModelsCacheSpy: { mockRestore: () => void } | undefined
function setupConnectedProviderCacheMocks(): void {
readConnectedProvidersCacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
readProviderModelsCacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue(null)
}
describe("createEventHandler - model fallback", () => { describe("createEventHandler - model fallback", () => {
const createHandler = (args?: { hooks?: any; pluginConfig?: any }) => { const createHandler = (args?: { hooks?: any; pluginConfig?: any }) => {
setupConnectedProviderCacheMocks()
const abortCalls: string[] = [] const abortCalls: string[] = []
const promptCalls: string[] = [] const promptCalls: string[] = []
@@ -54,6 +58,10 @@ describe("createEventHandler - model fallback", () => {
} }
afterEach(() => { afterEach(() => {
readConnectedProvidersCacheSpy?.mockRestore()
readProviderModelsCacheSpy?.mockRestore()
readConnectedProvidersCacheSpy = undefined
readProviderModelsCacheSpy = undefined
_resetForTesting() _resetForTesting()
}) })
@@ -442,6 +450,7 @@ describe("createEventHandler - model fallback", () => {
const modelFallback = createModelFallbackHook() const modelFallback = createModelFallbackHook()
setupConnectedProviderCacheMocks()
const eventHandler = createEventHandler({ const eventHandler = createEventHandler({
ctx: { ctx: {
directory: "/tmp", directory: "/tmp",
+24 -10
View File
@@ -1,17 +1,8 @@
declare const require: (name: string) => any declare const require: (name: string) => any
const { afterEach, afterAll, describe, expect, mock, test } = require("bun:test") const { afterEach, describe, expect, spyOn, test } = require("bun:test")
const PROVIDER_ID = "cliproxyapi" const PROVIDER_ID = "cliproxyapi"
mock.module("../shared/connected-providers-cache", () => ({
readConnectedProvidersCache: () => [PROVIDER_ID],
readProviderModelsCache: () => ({
connected: [PROVIDER_ID],
}),
}))
afterAll(() => { mock.restore() })
import { createEventHandler } from "./event" import { createEventHandler } from "./event"
import { createChatMessageHandler } from "./chat-message" import { createChatMessageHandler } from "./chat-message"
import { createModelFallbackHook } from "../hooks/model-fallback/hook" import { createModelFallbackHook } from "../hooks/model-fallback/hook"
@@ -20,6 +11,7 @@ import type { RuntimeFallbackPluginInput } from "../hooks/runtime-fallback/types
import { _resetForTesting } from "../features/claude-code-session-state" import { _resetForTesting } from "../features/claude-code-session-state"
import { _resetForTesting as _resetModelFallbackForTesting } from "../hooks/model-fallback/hook" import { _resetForTesting as _resetModelFallbackForTesting } from "../hooks/model-fallback/hook"
import { SessionCategoryRegistry } from "../shared/session-category-registry" import { SessionCategoryRegistry } from "../shared/session-category-registry"
import * as connectedProvidersCache from "../shared/connected-providers-cache"
type EventHandlerArgs = Parameters<typeof createEventHandler>[0] type EventHandlerArgs = Parameters<typeof createEventHandler>[0]
type ChatMessageHandlerArgs = Parameters<typeof createChatMessageHandler>[0] type ChatMessageHandlerArgs = Parameters<typeof createChatMessageHandler>[0]
@@ -92,6 +84,9 @@ type PromptAsyncCall = {
parts?: Array<{ type?: string; text?: string }> parts?: Array<{ type?: string; text?: string }>
} }
let readConnectedProvidersCacheSpy: { mockRestore: () => void } | undefined
let readProviderModelsCacheSpy: { mockRestore: () => void } | undefined
function createPluginConfig(mode: HarnessMode) { function createPluginConfig(mode: HarnessMode) {
return { return {
agents: { agents: {
@@ -114,6 +109,7 @@ function createHarness(args: {
promptAsyncImpl?: (call: PromptAsyncCall) => Promise<unknown> promptAsyncImpl?: (call: PromptAsyncCall) => Promise<unknown>
sessionTimeoutMs?: number sessionTimeoutMs?: number
}) { }) {
setupConnectedProviderCacheMocks()
const abortCalls: string[] = [] const abortCalls: string[] = []
const promptCalls: string[] = [] const promptCalls: string[] = []
const promptAsyncCalls: PromptAsyncCall[] = [] const promptAsyncCalls: PromptAsyncCall[] = []
@@ -353,6 +349,24 @@ async function triggerAssistantMessageError(
})) }))
} }
afterEach(() => {
readConnectedProvidersCacheSpy?.mockRestore()
readProviderModelsCacheSpy?.mockRestore()
readConnectedProvidersCacheSpy = undefined
readProviderModelsCacheSpy = undefined
})
function setupConnectedProviderCacheMocks(): void {
readConnectedProvidersCacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue([
PROVIDER_ID,
])
readProviderModelsCacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({
connected: [PROVIDER_ID],
models: {},
updatedAt: new Date(0).toISOString(),
})
}
afterEach(() => { afterEach(() => {
_resetForTesting() _resetForTesting()
_resetModelFallbackForTesting() _resetModelFallbackForTesting()
@@ -37,7 +37,10 @@ afterAll(() => {
}) })
async function importFreshStartupWarningModule(): Promise<typeof import("./log-legacy-plugin-startup-warning")> { async function importFreshStartupWarningModule(): Promise<typeof import("./log-legacy-plugin-startup-warning")> {
return 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()
consoleWarnSpy = spyOn(console, "warn").mockImplementation(() => {})
return module
} }
describe("logLegacyPluginStartupWarning", () => { describe("logLegacyPluginStartupWarning", () => {
+3 -5
View File
@@ -1,3 +1,4 @@
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. // Mock connected-providers-cache to prevent local disk cache from polluting test results.
@@ -14,11 +15,8 @@ afterAll(() => {
mock.restore() mock.restore()
}) })
import { const { getModelCapabilities, getBundledModelCapabilitiesSnapshot } = await import("./model-capabilities")
getModelCapabilities, mock.restore()
getBundledModelCapabilitiesSnapshot,
type ModelCapabilitiesSnapshot,
} from "./model-capabilities"
import { AGENT_MODEL_REQUIREMENTS, CATEGORY_MODEL_REQUIREMENTS } from "./model-requirements" import { AGENT_MODEL_REQUIREMENTS, CATEGORY_MODEL_REQUIREMENTS } from "./model-requirements"
describe("getModelCapabilities", () => { describe("getModelCapabilities", () => {
+4 -1
View File
@@ -9,7 +9,8 @@ mock.module("./connected-providers-cache", () => ({
afterAll(() => { mock.restore() }) afterAll(() => { mock.restore() })
import { shouldRetryError, selectFallbackProvider } from "./model-error-classifier" const { shouldRetryError, selectFallbackProvider } = await import("./model-error-classifier")
mock.restore()
describe("model-error-classifier", () => { describe("model-error-classifier", () => {
beforeEach(() => { beforeEach(() => {
@@ -107,3 +108,5 @@ describe("model-error-classifier", () => {
expect(result).toBe(true) expect(result).toBe(true)
}) })
}) })
export {}