Skip to content

Commit f2a4a0b

Browse files
test(opencode-go): cover OpencodeGoHandler streaming and completePrompt (#172)
Adds unit tests for the handler: client init (base URL/key), fetchModel (configured + default), createMessage streaming (text/reasoning/tool_call/ usage), and completePrompt (content + error wrapping). Raises patch coverage to green CI on #319.
1 parent 60abf37 commit f2a4a0b

1 file changed

Lines changed: 169 additions & 0 deletions

File tree

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// npx vitest run src/api/providers/__tests__/opencode-go.spec.ts
2+
3+
// Mock vscode first to avoid import errors
4+
vitest.mock("vscode", () => ({}))
5+
6+
import { Anthropic } from "@anthropic-ai/sdk"
7+
import OpenAI from "openai"
8+
9+
import { opencodeGoDefaultModelId } from "@roo-code/types"
10+
11+
import { OpencodeGoHandler } from "../opencode-go"
12+
import { ApiHandlerOptions } from "../../../shared/api"
13+
14+
vitest.mock("openai")
15+
vitest.mock("delay", () => ({ default: vitest.fn(() => Promise.resolve()) }))
16+
vitest.mock("../fetchers/modelCache", () => ({
17+
getModels: vitest.fn().mockImplementation(() =>
18+
Promise.resolve({
19+
"glm-5.1": {
20+
maxTokens: 32768,
21+
contextWindow: 200000,
22+
supportsImages: false,
23+
supportsPromptCache: false,
24+
description: "GLM 5.1",
25+
},
26+
}),
27+
),
28+
getModelsFromCache: vitest.fn().mockReturnValue(undefined),
29+
}))
30+
31+
const mockCreate = vitest.fn()
32+
33+
;(OpenAI as any).mockImplementation(() => ({
34+
chat: { completions: { create: mockCreate } },
35+
}))
36+
37+
describe("OpencodeGoHandler", () => {
38+
const mockOptions: ApiHandlerOptions = {
39+
opencodeGoApiKey: "test-key",
40+
opencodeGoModelId: "glm-5.1",
41+
}
42+
43+
beforeEach(() => {
44+
vitest.clearAllMocks()
45+
mockCreate.mockClear()
46+
})
47+
48+
it("initializes the OpenAI client with the Opencode Go base URL and key", () => {
49+
const handler = new OpencodeGoHandler(mockOptions)
50+
expect(handler).toBeInstanceOf(OpencodeGoHandler)
51+
expect(OpenAI).toHaveBeenCalledWith(
52+
expect.objectContaining({
53+
baseURL: "https://opencode.ai/zen/go/v1",
54+
apiKey: "test-key",
55+
}),
56+
)
57+
})
58+
59+
describe("fetchModel", () => {
60+
it("returns the configured model info", async () => {
61+
const handler = new OpencodeGoHandler(mockOptions)
62+
const result = await handler.fetchModel()
63+
expect(result.id).toBe("glm-5.1")
64+
expect(result.info.maxTokens).toBe(32768)
65+
expect(result.info.contextWindow).toBe(200000)
66+
expect(result.info.supportsPromptCache).toBe(false)
67+
})
68+
69+
it("falls back to the default model id when none is configured", async () => {
70+
const handler = new OpencodeGoHandler({ opencodeGoApiKey: "test-key" })
71+
const result = await handler.fetchModel()
72+
expect(result.id).toBe(opencodeGoDefaultModelId)
73+
})
74+
})
75+
76+
describe("createMessage", () => {
77+
beforeEach(() => {
78+
mockCreate.mockImplementation(async () => ({
79+
[Symbol.asyncIterator]: async function* () {
80+
yield {
81+
choices: [
82+
{
83+
delta: {
84+
content: "Hello",
85+
reasoning_content: "thinking…",
86+
tool_calls: [
87+
{
88+
index: 0,
89+
id: "call_1",
90+
function: { name: "read_file", arguments: '{"path":' },
91+
},
92+
],
93+
},
94+
index: 0,
95+
},
96+
],
97+
usage: null,
98+
}
99+
yield {
100+
choices: [{ delta: {}, index: 0 }],
101+
usage: {
102+
prompt_tokens: 12,
103+
completion_tokens: 7,
104+
total_tokens: 19,
105+
prompt_tokens_details: { cached_tokens: 4 },
106+
},
107+
}
108+
},
109+
}))
110+
})
111+
112+
it("streams text, reasoning, tool-call and usage chunks", async () => {
113+
const handler = new OpencodeGoHandler(mockOptions)
114+
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }]
115+
116+
const chunks = []
117+
for await (const chunk of handler.createMessage("You are helpful.", messages)) {
118+
chunks.push(chunk)
119+
}
120+
121+
expect(chunks).toContainEqual({ type: "text", text: "Hello" })
122+
expect(chunks).toContainEqual({ type: "reasoning", text: "thinking…" })
123+
expect(chunks).toContainEqual({
124+
type: "tool_call_partial",
125+
index: 0,
126+
id: "call_1",
127+
name: "read_file",
128+
arguments: '{"path":',
129+
})
130+
expect(chunks).toContainEqual({
131+
type: "usage",
132+
inputTokens: 12,
133+
outputTokens: 7,
134+
cacheReadTokens: 4,
135+
})
136+
})
137+
138+
it("requests a streaming completion with usage included", async () => {
139+
const handler = new OpencodeGoHandler(mockOptions)
140+
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }]
141+
for await (const _chunk of handler.createMessage("sys", messages)) {
142+
void _chunk // drain
143+
}
144+
145+
expect(mockCreate).toHaveBeenCalledWith(
146+
expect.objectContaining({
147+
model: "glm-5.1",
148+
stream: true,
149+
stream_options: { include_usage: true },
150+
}),
151+
)
152+
})
153+
})
154+
155+
describe("completePrompt", () => {
156+
it("returns the message content for a non-streaming completion", async () => {
157+
mockCreate.mockResolvedValue({ choices: [{ message: { content: "the answer" } }] })
158+
const handler = new OpencodeGoHandler(mockOptions)
159+
expect(await handler.completePrompt("ping")).toBe("the answer")
160+
expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({ model: "glm-5.1", stream: false }))
161+
})
162+
163+
it("wraps errors with an Opencode Go-specific message", async () => {
164+
mockCreate.mockRejectedValue(new Error("boom"))
165+
const handler = new OpencodeGoHandler(mockOptions)
166+
await expect(handler.completePrompt("ping")).rejects.toThrow("Opencode Go completion error: boom")
167+
})
168+
})
169+
})

0 commit comments

Comments
 (0)