From 69e4de3eec6643c2077a58395234437474279e2c Mon Sep 17 00:00:00 2001 From: Janni Turunen Date: Fri, 27 Feb 2026 20:46:12 +0200 Subject: [PATCH 1/4] fix(pulse): return input.model directly instead of re-parsing through parseModel (#356) --- packages/opencode/src/session/prompt.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index d6cf1f67a0b..8f16b42d4a6 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1805,7 +1805,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the return cmdAgent.model } } - if (input.model) return Provider.parseModel(input.model) + if (input.model) return input.model return await lastModel(input.sessionID) })() @@ -1859,7 +1859,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the const userAgent = isSubtask ? (input.agent ?? (await Agent.defaultAgent())) : agentName const userModel = isSubtask ? input.model - ? Provider.parseModel(input.model) + ? input.model : await lastModel(input.sessionID) : taskModel From 5a89adc5be4b983a7e9cb9e8c3f5ce77a57de16a Mon Sep 17 00:00:00 2001 From: Janni Turunen Date: Fri, 27 Feb 2026 21:49:41 +0200 Subject: [PATCH 2/4] fix(taskctl): validate model registry before using pm session model in resolveModel (#356) --- packages/opencode/src/session/prompt.ts | 4 +- packages/opencode/src/tasks/pulse.ts | 9 +- packages/opencode/test/tasks/pulse.test.ts | 280 ++++++++++++++------- 3 files changed, 195 insertions(+), 98 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 8f16b42d4a6..d6cf1f67a0b 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1805,7 +1805,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the return cmdAgent.model } } - if (input.model) return input.model + if (input.model) return Provider.parseModel(input.model) return await lastModel(input.sessionID) })() @@ -1859,7 +1859,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the const userAgent = isSubtask ? (input.agent ?? (await Agent.defaultAgent())) : agentName const userModel = isSubtask ? input.model - ? input.model + ? Provider.parseModel(input.model) : await lastModel(input.sessionID) : taskModel diff --git a/packages/opencode/src/tasks/pulse.ts b/packages/opencode/src/tasks/pulse.ts index 421512d4f68..963eaa53c92 100644 --- a/packages/opencode/src/tasks/pulse.ts +++ b/packages/opencode/src/tasks/pulse.ts @@ -44,7 +44,14 @@ const activeTicks = new Map>() export async function resolveModel(pmSessionId: string): Promise<{ modelID: string; providerID: string }> { for await (const msg of MessageV2.stream(pmSessionId)) { if (msg.info.role === "assistant") { - return { modelID: msg.info.modelID, providerID: msg.info.providerID } + const candidate = { modelID: msg.info.modelID, providerID: msg.info.providerID } + try { + await Provider.getModel(candidate.providerID, candidate.modelID) + return candidate + } catch (e) { + if (!Provider.ModelNotFoundError.isInstance(e)) throw e + log.warn("skipping unregistered model from pm session", { modelID: candidate.modelID, providerID: candidate.providerID }) + } } } return Provider.defaultModel() diff --git a/packages/opencode/test/tasks/pulse.test.ts b/packages/opencode/test/tasks/pulse.test.ts index af3efe891e9..ea78ba4a587 100644 --- a/packages/opencode/test/tasks/pulse.test.ts +++ b/packages/opencode/test/tasks/pulse.test.ts @@ -492,122 +492,212 @@ describe("pulse.ts", () => { }) - describe("resolveModel", () => { - test("returns modelID and providerID from last assistant message", async () => { - const { resolveModel } = await import("../../src/tasks/pulse") - - // Mock MessageV2.stream to yield one assistant message - const mockMsg = { - info: { - role: "assistant", - modelID: "claude-sonnet-4-5", - providerID: "anthropic", - }, - parts: [], - } + describe("resolveModel", () => { + test("returns modelID and providerID from last assistant message", async () => { + const { resolveModel } = await import("../../src/tasks/pulse") + + // Mock MessageV2.stream to yield one assistant message + const mockMsg = { + info: { + role: "assistant", + modelID: "claude-sonnet-4-5", + providerID: "anthropic", + }, + parts: [], + } + + const MessageV2 = await import("../../src/session/message-v2") + const origStream = MessageV2.MessageV2.stream + + // Replace stream with a mock that yields our message + ;(MessageV2.MessageV2 as any).stream = async function* (_sessionId: string) { + yield mockMsg + } + + try { + const result = await resolveModel("pm-session-test-123") + expect(result.modelID).toBe("claude-sonnet-4-5") + expect(result.providerID).toBe("anthropic") + } finally { + ;(MessageV2.MessageV2 as any).stream = origStream + } + }) - const MessageV2 = await import("../../src/session/message-v2") - const origStream = MessageV2.MessageV2.stream + test("skips non-assistant messages and returns first assistant message", async () => { + const { resolveModel } = await import("../../src/tasks/pulse") + + const userMsg = { + info: { role: "user", modelID: "ignored", providerID: "ignored" }, + parts: [], + } + const assistantMsg = { + info: { role: "assistant", modelID: "gpt-4o", providerID: "openai" }, + parts: [], + } + + const MessageV2 = await import("../../src/session/message-v2") + const origStream = MessageV2.MessageV2.stream + + ;(MessageV2.MessageV2 as any).stream = async function* (_sessionId: string) { + yield userMsg + yield assistantMsg + } + + try { + const result = await resolveModel("pm-session-test-456") + expect(result.modelID).toBe("gpt-4o") + expect(result.providerID).toBe("openai") + } finally { + ;(MessageV2.MessageV2 as any).stream = origStream + } + }) - // Replace stream with a mock that yields our message - ;(MessageV2.MessageV2 as any).stream = async function* (_sessionId: string) { - yield mockMsg - } + test("falls back to Provider.defaultModel when no assistant message found", async () => { + const { resolveModel } = await import("../../src/tasks/pulse") - try { - const result = await resolveModel("pm-session-test-123") - expect(result.modelID).toBe("claude-sonnet-4-5") - expect(result.providerID).toBe("anthropic") - } finally { - ;(MessageV2.MessageV2 as any).stream = origStream - } - }) + const MessageV2 = await import("../../src/session/message-v2") + const origStream = MessageV2.MessageV2.stream - test("skips non-assistant messages and returns first assistant message", async () => { - const { resolveModel } = await import("../../src/tasks/pulse") + // Stream with only user messages — no assistant + ;(MessageV2.MessageV2 as any).stream = async function* (_sessionId: string) { + yield { info: { role: "user", modelID: "x", providerID: "y" }, parts: [] } + } - const userMsg = { - info: { role: "user", modelID: "ignored", providerID: "ignored" }, - parts: [], - } - const assistantMsg = { - info: { role: "assistant", modelID: "gpt-4o", providerID: "openai" }, - parts: [], - } + const Provider = await import("../../src/provider/provider") + const origDefaultModel = Provider.Provider.defaultModel - const MessageV2 = await import("../../src/session/message-v2") - const origStream = MessageV2.MessageV2.stream + ;(Provider.Provider as any).defaultModel = async () => ({ + modelID: "default-model", + providerID: "default-provider", + }) - ;(MessageV2.MessageV2 as any).stream = async function* (_sessionId: string) { - yield userMsg - yield assistantMsg - } + try { + const result = await resolveModel("pm-session-no-assistant") + expect(result.modelID).toBe("default-model") + expect(result.providerID).toBe("default-provider") + } finally { + ;(MessageV2.MessageV2 as any).stream = origStream + ;(Provider.Provider as any).defaultModel = origDefaultModel + } + }) - try { - const result = await resolveModel("pm-session-test-456") - expect(result.modelID).toBe("gpt-4o") - expect(result.providerID).toBe("openai") - } finally { - ;(MessageV2.MessageV2 as any).stream = origStream - } - }) + test("falls back to Provider.defaultModel when session has no messages", async () => { + const { resolveModel } = await import("../../src/tasks/pulse") - test("falls back to Provider.defaultModel when no assistant message found", async () => { - const { resolveModel } = await import("../../src/tasks/pulse") + const MessageV2 = await import("../../src/session/message-v2") + const origStream = MessageV2.MessageV2.stream - const MessageV2 = await import("../../src/session/message-v2") - const origStream = MessageV2.MessageV2.stream + // Empty stream + ;(MessageV2.MessageV2 as any).stream = async function* (_sessionId: string) { + // yields nothing + } - // Stream with only user messages — no assistant - ;(MessageV2.MessageV2 as any).stream = async function* (_sessionId: string) { - yield { info: { role: "user", modelID: "x", providerID: "y" }, parts: [] } - } + const Provider = await import("../../src/provider/provider") + const origDefaultModel = Provider.Provider.defaultModel - const Provider = await import("../../src/provider/provider") - const origDefaultModel = Provider.Provider.defaultModel + ;(Provider.Provider as any).defaultModel = async () => ({ + modelID: "fallback-model", + providerID: "fallback-provider", + }) - ;(Provider.Provider as any).defaultModel = async () => ({ - modelID: "default-model", - providerID: "default-provider", + try { + const result = await resolveModel("pm-session-empty") + expect(result.modelID).toBe("fallback-model") + expect(result.providerID).toBe("fallback-provider") + } finally { + ;(MessageV2.MessageV2 as any).stream = origStream + ;(Provider.Provider as any).defaultModel = origDefaultModel + } }) - try { - const result = await resolveModel("pm-session-no-assistant") - expect(result.modelID).toBe("default-model") - expect(result.providerID).toBe("default-provider") - } finally { - ;(MessageV2.MessageV2 as any).stream = origStream - ;(Provider.Provider as any).defaultModel = origDefaultModel - } - }) + test("resolveModel skips assistant messages with unregistered models and falls back to defaultModel", async () => { + const { resolveModel } = await import("../../src/tasks/pulse") - test("falls back to Provider.defaultModel when session has no messages", async () => { - const { resolveModel } = await import("../../src/tasks/pulse") + const invalidMsg = { + info: { role: "assistant", modelID: "invalid-model-123", providerID: "unknown-provider" }, + parts: [], + } - const MessageV2 = await import("../../src/session/message-v2") - const origStream = MessageV2.MessageV2.stream + const MessageV2 = await import("../../src/session/message-v2") + const origStream = MessageV2.MessageV2.stream - // Empty stream - ;(MessageV2.MessageV2 as any).stream = async function* (_sessionId: string) { - // yields nothing - } + ;(MessageV2.MessageV2 as any).stream = async function* (_sessionId: string) { + yield invalidMsg + } - const Provider = await import("../../src/provider/provider") - const origDefaultModel = Provider.Provider.defaultModel + const Provider = await import("../../src/provider/provider") + const origGetModel = Provider.Provider.getModel + const origDefaultModel = Provider.Provider.defaultModel - ;(Provider.Provider as any).defaultModel = async () => ({ - modelID: "fallback-model", - providerID: "fallback-provider", + ;(Provider.Provider as any).getModel = async () => { + const error = new Provider.Provider.ModelNotFoundError({ + providerID: "unknown-provider", + modelID: "invalid-model-123", + suggestions: [], + }) + throw error + } + + ;(Provider.Provider as any).defaultModel = async () => ({ + modelID: "fallback-model", + providerID: "fallback-provider", + }) + + try { + const result = await resolveModel("pm-session-invalid-model") + expect(result.modelID).toBe("fallback-model") + expect(result.providerID).toBe("fallback-provider") + } finally { + ;(MessageV2.MessageV2 as any).stream = origStream + ;(Provider.Provider as any).getModel = origGetModel + ;(Provider.Provider as any).defaultModel = origDefaultModel + } }) - try { - const result = await resolveModel("pm-session-empty") - expect(result.modelID).toBe("fallback-model") - expect(result.providerID).toBe("fallback-provider") - } finally { - ;(MessageV2.MessageV2 as any).stream = origStream - ;(Provider.Provider as any).defaultModel = origDefaultModel - } + test("resolveModel returns valid model when first message is invalid and second is valid", async () => { + const { resolveModel } = await import("../../src/tasks/pulse") + + const invalidMsg = { + info: { role: "assistant", modelID: "invalid-model", providerID: "unknown" }, + parts: [], + } + const validMsg = { + info: { role: "assistant", modelID: "claude-opus", providerID: "anthropic" }, + parts: [], + } + + const MessageV2 = await import("../../src/session/message-v2") + const origStream = MessageV2.MessageV2.stream + + ;(MessageV2.MessageV2 as any).stream = async function* (_sessionId: string) { + yield invalidMsg + yield validMsg + } + + const Provider = await import("../../src/provider/provider") + const origGetModel = Provider.Provider.getModel + + ;(Provider.Provider as any).getModel = async (providerID: string, modelID: string) => { + if (modelID === "invalid-model") { + const error = new Provider.Provider.ModelNotFoundError({ + providerID: "unknown", + modelID: "invalid-model", + suggestions: [], + }) + throw error + } + return { providerID, modelID } + } + + try { + const result = await resolveModel("pm-session-mixed-models") + expect(result.modelID).toBe("claude-opus") + expect(result.providerID).toBe("anthropic") + } finally { + ;(MessageV2.MessageV2 as any).stream = origStream + ;(Provider.Provider as any).getModel = origGetModel + } + }) }) }) - }) \ No newline at end of file +}) From 908258d99163cfc9a5b538115d45bad15854d907 Mon Sep 17 00:00:00 2001 From: Janni Turunen Date: Fri, 27 Feb 2026 23:01:22 +0200 Subject: [PATCH 3/4] test(taskctl): fix resolveModel test mocks to account for model validation (#356) --- packages/opencode/test/tasks/pulse.test.ts | 378 ++++++++++++--------- 1 file changed, 208 insertions(+), 170 deletions(-) diff --git a/packages/opencode/test/tasks/pulse.test.ts b/packages/opencode/test/tasks/pulse.test.ts index ea78ba4a587..c44da671e0b 100644 --- a/packages/opencode/test/tasks/pulse.test.ts +++ b/packages/opencode/test/tasks/pulse.test.ts @@ -488,216 +488,254 @@ describe("pulse.ts", () => { }) }) - describe("PM session notifications", () => { - - }) - describe("resolveModel", () => { test("returns modelID and providerID from last assistant message", async () => { const { resolveModel } = await import("../../src/tasks/pulse") + const { Instance } = await import("../../src/project/instance") - // Mock MessageV2.stream to yield one assistant message - const mockMsg = { - info: { - role: "assistant", - modelID: "claude-sonnet-4-5", - providerID: "anthropic", + await Instance.provide({ + directory: testDataDir, + fn: async () => { + // Mock MessageV2.stream to yield one assistant message + const mockMsg = { + info: { + role: "assistant", + modelID: "claude-sonnet-4-5", + providerID: "anthropic", + }, + parts: [], + } + + const MessageV2 = await import("../../src/session/message-v2") + const origStream = MessageV2.MessageV2.stream + + // Replace stream with a mock that yields our message + ;(MessageV2.MessageV2 as any).stream = async function* (_sessionId: string) { + yield mockMsg + } + + try { + const result = await resolveModel("pm-session-test-123") + expect(result.modelID).toBe("claude-sonnet-4-5") + expect(result.providerID).toBe("anthropic") + } finally { + ;(MessageV2.MessageV2 as any).stream = origStream + } }, - parts: [], - } - - const MessageV2 = await import("../../src/session/message-v2") - const origStream = MessageV2.MessageV2.stream - - // Replace stream with a mock that yields our message - ;(MessageV2.MessageV2 as any).stream = async function* (_sessionId: string) { - yield mockMsg - } - - try { - const result = await resolveModel("pm-session-test-123") - expect(result.modelID).toBe("claude-sonnet-4-5") - expect(result.providerID).toBe("anthropic") - } finally { - ;(MessageV2.MessageV2 as any).stream = origStream - } + }) }) test("skips non-assistant messages and returns first assistant message", async () => { const { resolveModel } = await import("../../src/tasks/pulse") + const { Instance } = await import("../../src/project/instance") - const userMsg = { - info: { role: "user", modelID: "ignored", providerID: "ignored" }, - parts: [], - } - const assistantMsg = { - info: { role: "assistant", modelID: "gpt-4o", providerID: "openai" }, - parts: [], - } - - const MessageV2 = await import("../../src/session/message-v2") - const origStream = MessageV2.MessageV2.stream - - ;(MessageV2.MessageV2 as any).stream = async function* (_sessionId: string) { - yield userMsg - yield assistantMsg - } - - try { - const result = await resolveModel("pm-session-test-456") - expect(result.modelID).toBe("gpt-4o") - expect(result.providerID).toBe("openai") - } finally { - ;(MessageV2.MessageV2 as any).stream = origStream - } + await Instance.provide({ + directory: testDataDir, + fn: async () => { + const userMsg = { + info: { role: "user", modelID: "ignored", providerID: "ignored" }, + parts: [], + } + const assistantMsg = { + info: { role: "assistant", modelID: "gpt-4o", providerID: "openai" }, + parts: [], + } + + const MessageV2 = await import("../../src/session/message-v2") + const origStream = MessageV2.MessageV2.stream + + ;(MessageV2.MessageV2 as any).stream = async function* (_sessionId: string) { + yield userMsg + yield assistantMsg + } + + const Provider = await import("../../src/provider/provider") + const origGetModel = Provider.Provider.getModel + + ;(Provider.Provider as any).getModel = async (providerID: string, modelID: string) => { + return { providerID, modelID } + } + try { + const result = await resolveModel("pm-session-test-456") + expect(result.modelID).toBe("gpt-4o") + expect(result.providerID).toBe("openai") + } finally { + ;(MessageV2.MessageV2 as any).stream = origStream + ;(Provider.Provider as any).getModel = origGetModel + } + }, + }) }) test("falls back to Provider.defaultModel when no assistant message found", async () => { const { resolveModel } = await import("../../src/tasks/pulse") + const { Instance } = await import("../../src/project/instance") - const MessageV2 = await import("../../src/session/message-v2") - const origStream = MessageV2.MessageV2.stream + await Instance.provide({ + directory: testDataDir, + fn: async () => { + const MessageV2 = await import("../../src/session/message-v2") + const origStream = MessageV2.MessageV2.stream - // Stream with only user messages — no assistant - ;(MessageV2.MessageV2 as any).stream = async function* (_sessionId: string) { - yield { info: { role: "user", modelID: "x", providerID: "y" }, parts: [] } - } + // Stream with only user messages — no assistant + ;(MessageV2.MessageV2 as any).stream = async function* (_sessionId: string) { + yield { info: { role: "user", modelID: "x", providerID: "y" }, parts: [] } + } - const Provider = await import("../../src/provider/provider") - const origDefaultModel = Provider.Provider.defaultModel + const Provider = await import("../../src/provider/provider") + const origDefaultModel = Provider.Provider.defaultModel - ;(Provider.Provider as any).defaultModel = async () => ({ - modelID: "default-model", - providerID: "default-provider", - }) + ;(Provider.Provider as any).defaultModel = async () => ({ + modelID: "default-model", + providerID: "default-provider", + }) - try { - const result = await resolveModel("pm-session-no-assistant") - expect(result.modelID).toBe("default-model") - expect(result.providerID).toBe("default-provider") - } finally { - ;(MessageV2.MessageV2 as any).stream = origStream - ;(Provider.Provider as any).defaultModel = origDefaultModel - } + try { + const result = await resolveModel("pm-session-no-assistant") + expect(result.modelID).toBe("default-model") + expect(result.providerID).toBe("default-provider") + } finally { + ;(MessageV2.MessageV2 as any).stream = origStream + ;(Provider.Provider as any).defaultModel = origDefaultModel + } + }, + }) }) test("falls back to Provider.defaultModel when session has no messages", async () => { const { resolveModel } = await import("../../src/tasks/pulse") + const { Instance } = await import("../../src/project/instance") - const MessageV2 = await import("../../src/session/message-v2") - const origStream = MessageV2.MessageV2.stream + await Instance.provide({ + directory: testDataDir, + fn: async () => { + const MessageV2 = await import("../../src/session/message-v2") + const origStream = MessageV2.MessageV2.stream - // Empty stream - ;(MessageV2.MessageV2 as any).stream = async function* (_sessionId: string) { - // yields nothing - } + // Empty stream + ;(MessageV2.MessageV2 as any).stream = async function* (_sessionId: string) { + // yields nothing + } - const Provider = await import("../../src/provider/provider") - const origDefaultModel = Provider.Provider.defaultModel + const Provider = await import("../../src/provider/provider") + const origDefaultModel = Provider.Provider.defaultModel - ;(Provider.Provider as any).defaultModel = async () => ({ - modelID: "fallback-model", - providerID: "fallback-provider", - }) + ;(Provider.Provider as any).defaultModel = async () => ({ + modelID: "fallback-model", + providerID: "fallback-provider", + }) - try { - const result = await resolveModel("pm-session-empty") - expect(result.modelID).toBe("fallback-model") - expect(result.providerID).toBe("fallback-provider") - } finally { - ;(MessageV2.MessageV2 as any).stream = origStream - ;(Provider.Provider as any).defaultModel = origDefaultModel - } + try { + const result = await resolveModel("pm-session-empty") + expect(result.modelID).toBe("fallback-model") + expect(result.providerID).toBe("fallback-provider") + } finally { + ;(MessageV2.MessageV2 as any).stream = origStream + ;(Provider.Provider as any).defaultModel = origDefaultModel + } + }, + }) }) test("resolveModel skips assistant messages with unregistered models and falls back to defaultModel", async () => { const { resolveModel } = await import("../../src/tasks/pulse") + const { Instance } = await import("../../src/project/instance") - const invalidMsg = { - info: { role: "assistant", modelID: "invalid-model-123", providerID: "unknown-provider" }, - parts: [], - } - - const MessageV2 = await import("../../src/session/message-v2") - const origStream = MessageV2.MessageV2.stream - - ;(MessageV2.MessageV2 as any).stream = async function* (_sessionId: string) { - yield invalidMsg - } - - const Provider = await import("../../src/provider/provider") - const origGetModel = Provider.Provider.getModel - const origDefaultModel = Provider.Provider.defaultModel - - ;(Provider.Provider as any).getModel = async () => { - const error = new Provider.Provider.ModelNotFoundError({ - providerID: "unknown-provider", - modelID: "invalid-model-123", - suggestions: [], - }) - throw error - } + await Instance.provide({ + directory: testDataDir, + fn: async () => { + const invalidMsg = { + info: { role: "assistant", modelID: "invalid-model-123", providerID: "unknown-provider" }, + parts: [], + } + + const MessageV2 = await import("../../src/session/message-v2") + const origStream = MessageV2.MessageV2.stream + + ;(MessageV2.MessageV2 as any).stream = async function* (_sessionId: string) { + yield invalidMsg + } + + const Provider = await import("../../src/provider/provider") + const origGetModel = Provider.Provider.getModel + const origDefaultModel = Provider.Provider.defaultModel + + ;(Provider.Provider as any).getModel = async () => { + const error = new Provider.Provider.ModelNotFoundError({ + providerID: "unknown-provider", + modelID: "invalid-model-123", + suggestions: [], + }) + throw error + } + + ;(Provider.Provider as any).defaultModel = async () => ({ + modelID: "fallback-model", + providerID: "fallback-provider", + }) - ;(Provider.Provider as any).defaultModel = async () => ({ - modelID: "fallback-model", - providerID: "fallback-provider", + try { + const result = await resolveModel("pm-session-invalid-model") + expect(result.modelID).toBe("fallback-model") + expect(result.providerID).toBe("fallback-provider") + } finally { + ;(MessageV2.MessageV2 as any).stream = origStream + ;(Provider.Provider as any).getModel = origGetModel + ;(Provider.Provider as any).defaultModel = origDefaultModel + } + }, }) - - try { - const result = await resolveModel("pm-session-invalid-model") - expect(result.modelID).toBe("fallback-model") - expect(result.providerID).toBe("fallback-provider") - } finally { - ;(MessageV2.MessageV2 as any).stream = origStream - ;(Provider.Provider as any).getModel = origGetModel - ;(Provider.Provider as any).defaultModel = origDefaultModel - } }) test("resolveModel returns valid model when first message is invalid and second is valid", async () => { const { resolveModel } = await import("../../src/tasks/pulse") + const { Instance } = await import("../../src/project/instance") - const invalidMsg = { - info: { role: "assistant", modelID: "invalid-model", providerID: "unknown" }, - parts: [], - } - const validMsg = { - info: { role: "assistant", modelID: "claude-opus", providerID: "anthropic" }, - parts: [], - } - - const MessageV2 = await import("../../src/session/message-v2") - const origStream = MessageV2.MessageV2.stream - - ;(MessageV2.MessageV2 as any).stream = async function* (_sessionId: string) { - yield invalidMsg - yield validMsg - } - - const Provider = await import("../../src/provider/provider") - const origGetModel = Provider.Provider.getModel - - ;(Provider.Provider as any).getModel = async (providerID: string, modelID: string) => { - if (modelID === "invalid-model") { - const error = new Provider.Provider.ModelNotFoundError({ - providerID: "unknown", - modelID: "invalid-model", - suggestions: [], - }) - throw error - } - return { providerID, modelID } - } - - try { - const result = await resolveModel("pm-session-mixed-models") - expect(result.modelID).toBe("claude-opus") - expect(result.providerID).toBe("anthropic") - } finally { - ;(MessageV2.MessageV2 as any).stream = origStream - ;(Provider.Provider as any).getModel = origGetModel - } + await Instance.provide({ + directory: testDataDir, + fn: async () => { + const invalidMsg = { + info: { role: "assistant", modelID: "invalid-model", providerID: "unknown" }, + parts: [], + } + const validMsg = { + info: { role: "assistant", modelID: "claude-opus", providerID: "anthropic" }, + parts: [], + } + + const MessageV2 = await import("../../src/session/message-v2") + const origStream = MessageV2.MessageV2.stream + + ;(MessageV2.MessageV2 as any).stream = async function* (_sessionId: string) { + yield invalidMsg + yield validMsg + } + + const Provider = await import("../../src/provider/provider") + const origGetModel = Provider.Provider.getModel + + ;(Provider.Provider as any).getModel = async (providerID: string, modelID: string) => { + if (modelID === "invalid-model") { + const error = new Provider.Provider.ModelNotFoundError({ + providerID: "unknown", + modelID: "invalid-model", + suggestions: [], + }) + throw error + } + return { providerID, modelID } + } + + try { + const result = await resolveModel("pm-session-mixed-models") + expect(result.modelID).toBe("claude-opus") + expect(result.providerID).toBe("anthropic") + } finally { + ;(MessageV2.MessageV2 as any).stream = origStream + ;(Provider.Provider as any).getModel = origGetModel + } + }, + }) }) }) - }) }) From 340124cdbf91cb407e22a1be643c1348ebfc6d4a Mon Sep 17 00:00:00 2001 From: Janni Turunen Date: Sun, 1 Mar 2026 13:41:12 +0200 Subject: [PATCH 4/4] test(taskctl): mock Provider.getModel in resolveModel tests for CI portability (#356) --- packages/opencode/test/tasks/pulse.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/opencode/test/tasks/pulse.test.ts b/packages/opencode/test/tasks/pulse.test.ts index c44da671e0b..2c4c72bf586 100644 --- a/packages/opencode/test/tasks/pulse.test.ts +++ b/packages/opencode/test/tasks/pulse.test.ts @@ -514,12 +514,21 @@ describe("pulse.ts", () => { yield mockMsg } + const Provider = await import("../../src/provider/provider") + const origGetModel = Provider.Provider.getModel + + ;(Provider.Provider as any).getModel = async (providerID: string, modelID: string) => { + return { providerID, modelID } + } + + try { const result = await resolveModel("pm-session-test-123") expect(result.modelID).toBe("claude-sonnet-4-5") expect(result.providerID).toBe("anthropic") } finally { ;(MessageV2.MessageV2 as any).stream = origStream + ;(Provider.Provider as any).getModel = origGetModel } }, })