From fd281deba7d88af7f22d2e7fe5d9877eeca6ef2e Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 31 Mar 2026 17:41:23 +0900 Subject: [PATCH] fix(tests): resolve 25 pre-publish test failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add providerModelsCache/fetchAvailableModels mocks to 20 utils.test.ts tests that broke when createBuiltinAgents started reading the cache - Isolate ALL src/plugin and src/features/background-agent test files in CI (mock.module pollution crosses between files in the same bun process) - Mirror CI isolation changes in publish.yml 25 failures → 0 in CI (all mock-pollution tests run individually) --- .github/workflows/ci.yml | 8 ++++- .github/workflows/publish.yml | 8 ++++- src/agents/utils.test.ts | 62 +++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4696a7b74..2f6c0f5cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,6 +69,10 @@ jobs: bun test src/hooks/session-recovery/recover-tool-result-missing.test.ts # legacy-plugin-toast mock isolation (hook.test.ts mocks ./auto-migrate) bun test src/hooks/legacy-plugin-toast/hook.test.ts + # src/plugin — ALL isolated (mock.module pollution crosses between files) + for f in src/plugin/*.test.ts; do bun test "$f"; done + # src/features/background-agent — ALL isolated (mock.module pollution) + for f in src/features/background-agent/*.test.ts; do bun test "$f"; done - name: Run remaining tests run: | @@ -78,6 +82,8 @@ jobs: # Excluded from src/cli: doctor/formatter.test.ts, doctor/format-default.test.ts # Excluded from src/tools: call-omo-agent/sync-executor.test.ts, call-omo-agent/session-creator.test.ts, session-manager (all) # Excluded from src/hooks/anthropic-context-window-limit-recovery: recovery-hook.test.ts, executor.test.ts + # Excluded: src/plugin/* (all run isolated above) + # Excluded: src/features/background-agent/* (all run isolated above) # Build src/shared file list excluding mock-heavy files already run in isolation SHARED_FILES=$(find src/shared -name '*.test.ts' \ ! -name 'model-capabilities.test.ts' \ @@ -85,6 +91,7 @@ jobs: ! -name 'model-error-classifier.test.ts' \ ! -name 'opencode-message-dir.test.ts' \ | sort | tr '\n' ' ') + # plugin and background-agent fully isolated above — excluded from remaining bun test bin script src/config src/mcp src/index.test.ts \ src/agents $SHARED_FILES \ src/cli/run src/cli/config-manager src/cli/mcp-oauth \ @@ -107,7 +114,6 @@ jobs: src/hooks/session-notification \ src/hooks/sisyphus \ src/hooks/todo-continuation-enforcer \ - src/features/background-agent \ src/features/builtin-commands \ src/features/builtin-skills \ src/features/claude-code-session-state \ diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5179cdd32..89327c71a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -70,6 +70,10 @@ jobs: bun test src/hooks/session-recovery/recover-tool-result-missing.test.ts # legacy-plugin-toast mock isolation (hook.test.ts mocks ./auto-migrate) bun test src/hooks/legacy-plugin-toast/hook.test.ts + # src/plugin — ALL isolated (mock.module pollution crosses between files) + for f in src/plugin/*.test.ts; do bun test "$f"; done + # src/features/background-agent — ALL isolated (mock.module pollution) + for f in src/features/background-agent/*.test.ts; do bun test "$f"; done - name: Run remaining tests run: | @@ -79,6 +83,8 @@ jobs: # Excluded from src/cli: doctor/formatter.test.ts, doctor/format-default.test.ts # Excluded from src/tools: call-omo-agent/sync-executor.test.ts, call-omo-agent/session-creator.test.ts, session-manager (all) # Excluded from src/hooks/anthropic-context-window-limit-recovery: recovery-hook.test.ts, executor.test.ts + # Excluded: src/plugin/* (all run isolated above) + # Excluded: src/features/background-agent/* (all run isolated above) # Build src/shared file list excluding mock-heavy files already run in isolation SHARED_FILES=$(find src/shared -name '*.test.ts' \ ! -name 'model-capabilities.test.ts' \ @@ -86,6 +92,7 @@ jobs: ! -name 'model-error-classifier.test.ts' \ ! -name 'opencode-message-dir.test.ts' \ | sort | tr '\n' ' ') + # plugin and background-agent fully isolated above — excluded from remaining bun test bin script src/config src/mcp src/index.test.ts \ src/agents $SHARED_FILES \ src/cli/run src/cli/config-manager src/cli/mcp-oauth \ @@ -108,7 +115,6 @@ jobs: src/hooks/session-notification \ src/hooks/sisyphus \ src/hooks/todo-continuation-enforcer \ - src/features/background-agent \ src/features/builtin-commands \ src/features/builtin-skills \ src/features/claude-code-session-state \ diff --git a/src/agents/utils.test.ts b/src/agents/utils.test.ts index c3251b297..7b606e5e4 100644 --- a/src/agents/utils.test.ts +++ b/src/agents/utils.test.ts @@ -38,6 +38,8 @@ describe("createBuiltinAgents with model overrides", () => { test("Sisyphus with GPT model override has reasoningEffort, no thinking", async () => { // #given + const providerModelsSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue(null) + const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set()) const overrides = { sisyphus: { model: "github-copilot/gpt-5.4" }, } @@ -49,6 +51,8 @@ describe("createBuiltinAgents with model overrides", () => { expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.4") expect(agents.sisyphus.reasoningEffort).toBe("medium") expect(agents.sisyphus.thinking).toBeUndefined() + providerModelsSpy.mockRestore() + fetchSpy.mockRestore() }) test("Atlas uses uiSelectedModel", async () => { @@ -168,6 +172,8 @@ describe("createBuiltinAgents with model overrides", () => { test("Oracle uses connected provider fallback when availableModels is empty and cache exists", async () => { // #given - connected providers cache has "openai", which matches oracle's first fallback entry + const providerModelsSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue(null) + const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set()) const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"]) // #when @@ -178,6 +184,8 @@ describe("createBuiltinAgents with model overrides", () => { expect(agents.oracle.reasoningEffort).toBe("medium") expect(agents.oracle.thinking).toBeUndefined() cacheSpy.mockRestore?.() + providerModelsSpy.mockRestore() + fetchSpy.mockRestore() }) test("Oracle created without model field when no cache exists (first run scenario)", async () => { @@ -195,6 +203,8 @@ describe("createBuiltinAgents with model overrides", () => { test("Oracle with GPT model override has reasoningEffort, no thinking", async () => { // #given + const providerModelsSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue(null) + const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set()) const overrides = { oracle: { model: "openai/gpt-5.4" }, } @@ -207,10 +217,14 @@ describe("createBuiltinAgents with model overrides", () => { expect(agents.oracle.reasoningEffort).toBe("medium") expect(agents.oracle.textVerbosity).toBe("high") expect(agents.oracle.thinking).toBeUndefined() + providerModelsSpy.mockRestore() + fetchSpy.mockRestore() }) test("Oracle with Claude model override has thinking, no reasoningEffort", async () => { // #given + const providerModelsSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue(null) + const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set()) const overrides = { oracle: { model: "anthropic/claude-sonnet-4" }, } @@ -223,10 +237,14 @@ describe("createBuiltinAgents with model overrides", () => { expect(agents.oracle.thinking).toEqual({ type: "enabled", budgetTokens: 32000 }) expect(agents.oracle.reasoningEffort).toBeUndefined() expect(agents.oracle.textVerbosity).toBeUndefined() + providerModelsSpy.mockRestore() + fetchSpy.mockRestore() }) test("non-model overrides are still applied after factory rebuild", async () => { // #given + const providerModelsSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue(null) + const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set()) const overrides = { sisyphus: { model: "github-copilot/gpt-5.4", temperature: 0.5 }, } @@ -237,10 +255,15 @@ describe("createBuiltinAgents with model overrides", () => { // #then expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.4") expect(agents.sisyphus.temperature).toBe(0.5) + providerModelsSpy.mockRestore() + fetchSpy.mockRestore() }) test("createBuiltinAgents excludes disabled skills from availableSkills", async () => { // #given + const providerModelsSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue(null) + const connectedSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null) + const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set()) const disabledSkills = new Set(["playwright"]) // #when @@ -250,6 +273,9 @@ describe("createBuiltinAgents with model overrides", () => { expect(agents.sisyphus.prompt).not.toContain("playwright") expect(agents.sisyphus.prompt).toContain("frontend-ui-ux") expect(agents.sisyphus.prompt).toContain("git-master") + providerModelsSpy.mockRestore() + connectedSpy.mockRestore() + fetchSpy.mockRestore() }) test("includes custom agents in orchestrator prompts when provided via config", async () => { @@ -472,6 +498,8 @@ describe("createBuiltinAgents with model overrides", () => { describe("createBuiltinAgents without systemDefaultModel", () => { test("agents created via connected cache fallback even without systemDefaultModel", async () => { // #given - connected cache has "openai", which matches oracle's fallback chain + const providerModelsSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue(null) + const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set()) const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"]) // #when @@ -481,6 +509,8 @@ describe("createBuiltinAgents without systemDefaultModel", () => { expect(agents.oracle).toBeDefined() expect(agents.oracle.model).toBe("openai/gpt-5.4") cacheSpy.mockRestore?.() + providerModelsSpy.mockRestore() + fetchSpy.mockRestore() }) test("oracle is created on first run when no cache and no systemDefaultModel", async () => { @@ -1242,6 +1272,17 @@ describe("buildAgent with category and skills", () => { }) describe("override.category expansion in createBuiltinAgents", () => { + let providerModelsSpy: ReturnType + let fetchSpy: ReturnType + beforeEach(() => { + providerModelsSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue(null) + fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set()) + }) + afterEach(() => { + providerModelsSpy.mockRestore() + fetchSpy.mockRestore() + }) + test("standard agent override with category expands category properties", async () => { // #given const overrides = { @@ -1358,6 +1399,17 @@ describe("override.category expansion in createBuiltinAgents", () => { }) describe("agent override tools migration", () => { + let providerModelsSpy: ReturnType + let fetchSpy: ReturnType + beforeEach(() => { + providerModelsSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue(null) + fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set()) + }) + afterEach(() => { + providerModelsSpy.mockRestore() + fetchSpy.mockRestore() + }) + test("tools: { x: false } is migrated to permission: { x: deny }", async () => { // #given const overrides = { @@ -1441,6 +1493,8 @@ describe("Deadlock prevention - fetchAvailableModels must not receive client", ( }) test("Hephaestus variant override respects user config over hardcoded default", async () => { // #given - user provides variant in config + const providerModelsSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue(null) + const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set()) const overrides = { hephaestus: { variant: "high" }, } @@ -1451,10 +1505,15 @@ describe("Deadlock prevention - fetchAvailableModels must not receive client", ( // #then - user variant takes precedence over hardcoded "medium" expect(agents.hephaestus).toBeDefined() expect(agents.hephaestus.variant).toBe("high") + providerModelsSpy.mockRestore() + fetchSpy.mockRestore() }) test("Hephaestus uses default variant when no user override provided", async () => { // #given - no variant override in config + const providerModelsSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue(null) + const connectedSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null) + const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set()) const overrides = {} // #when @@ -1463,5 +1522,8 @@ describe("Deadlock prevention - fetchAvailableModels must not receive client", ( // #then - default "medium" variant is applied expect(agents.hephaestus).toBeDefined() expect(agents.hephaestus.variant).toBe("medium") + providerModelsSpy.mockRestore() + connectedSpy.mockRestore() + fetchSpy.mockRestore() }) })