Skip to content

Commit b134dfa

Browse files
HXYerrorclaude
andcommitted
fix(responses): dead export, explicit types, X-Initiator and error tests (#4)
- Delete unused `ResponseStreamEvent` interface from create-responses.ts - Add explicit `Promise<ResponsesResponse | AsyncGenerator<ServerSentEventMessage, void, unknown>>` return type to `createResponses` - Wire `forwardError` into responses route so upstream 4xx/5xx propagates correctly - Expand tests/responses-route.test.ts: new "createResponses behavior" describe block with X-Initiator=agent (assistant message), X-Initiator=user (pure user), X-Initiator=agent (function_call_output), and upstream 429 error path - Remove spurious `// eslint-disable-next-line require-atomic-updates` comment; fix underlying lint issue Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 65a4522 commit b134dfa

3 files changed

Lines changed: 159 additions & 18 deletions

File tree

src/routes/responses/route.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
import { Hono } from "hono"
22

3+
import { forwardError } from "~/lib/error"
4+
35
import { handleResponses } from "./handler"
46

57
const responses = new Hono()
68

7-
responses.post("/", handleResponses)
9+
responses.post("/", async (c) => {
10+
try {
11+
return await handleResponses(c)
12+
} catch (error) {
13+
return await forwardError(c, error)
14+
}
15+
})
816

917
export default responses

src/services/copilot/create-responses.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { ServerSentEventMessage } from "fetch-event-stream"
2+
13
import consola from "consola"
24
import { events } from "fetch-event-stream"
35

@@ -52,7 +54,11 @@ export function isAgentCall(payload: ResponsesPayload): boolean {
5254
// Service client
5355
// ---------------------------------------------------------------------------
5456

55-
export const createResponses = async (payload: ResponsesPayload) => {
57+
export const createResponses = async (
58+
payload: ResponsesPayload,
59+
): Promise<
60+
ResponsesResponse | AsyncGenerator<ServerSentEventMessage, void, unknown>
61+
> => {
5662
if (!state.copilotToken) throw new Error("Copilot token not found")
5763

5864
const enableVision = inputHasImages(payload)
@@ -82,12 +88,3 @@ export const createResponses = async (payload: ResponsesPayload) => {
8288

8389
return (await response.json()) as ResponsesResponse
8490
}
85-
86-
// ---------------------------------------------------------------------------
87-
// Streaming event types for Responses API SSE
88-
// ---------------------------------------------------------------------------
89-
90-
export interface ResponseStreamEvent {
91-
type: string // "response.created" | "response.output_text.delta" | etc.
92-
[key: string]: unknown
93-
}

tests/responses-route.test.ts

Lines changed: 143 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, test, expect, mock, beforeAll } from "bun:test"
1+
import { describe, test, expect, mock, beforeAll, beforeEach } from "bun:test"
22

33
import { state } from "../src/lib/state"
44
import { server } from "../src/server"
@@ -75,10 +75,8 @@ describe("POST /v1/responses — wired handler", () => {
7575
})
7676

7777
test("missing copilot token returns 500", async () => {
78-
// Temporarily clear the token via a describe-level wrapper so the
79-
// assignment happens synchronously (no await between read and write).
80-
const tokenBackup = state.copilotToken
81-
state.copilotToken = undefined // synchronous — no race condition
78+
// Temporarily clear the token — write is synchronous, no await in between.
79+
state.copilotToken = undefined
8280

8381
const res = await server.request("/v1/responses", {
8482
method: "POST",
@@ -87,7 +85,145 @@ describe("POST /v1/responses — wired handler", () => {
8785
})
8886
expect(res.status).toBe(500)
8987

90-
// eslint-disable-next-line require-atomic-updates
91-
state.copilotToken = tokenBackup
88+
state.copilotToken = "test-token"
89+
})
90+
})
91+
92+
// ---------------------------------------------------------------------------
93+
// createResponses behavior: X-Initiator header and error propagation
94+
// ---------------------------------------------------------------------------
95+
96+
describe("createResponses behavior", () => {
97+
// Restore state before each test in this block
98+
beforeEach(() => {
99+
state.copilotToken = "test-token"
100+
state.vsCodeVersion = "1.99.0"
101+
state.accountType = "individual"
102+
state.manualApprove = false
103+
})
104+
105+
test("X-Initiator = agent when assistant message present", async () => {
106+
const captureMock = mock(
107+
(_url: string, opts: { headers: Record<string, string> }) =>
108+
Promise.resolve({
109+
ok: true,
110+
json: () => Promise.resolve(mockResponseBody),
111+
headers: opts.headers,
112+
}),
113+
)
114+
// @ts-expect-error – mock doesn't implement full fetch signature
115+
globalThis.fetch = captureMock
116+
117+
await server.request("/v1/responses", {
118+
method: "POST",
119+
headers: { "Content-Type": "application/json" },
120+
body: JSON.stringify({
121+
model: "gpt-4o",
122+
stream: false,
123+
input: [
124+
{ type: "message", role: "user", content: "hello" },
125+
{ type: "message", role: "assistant", content: "hi there" },
126+
],
127+
}),
128+
})
129+
130+
expect(captureMock).toHaveBeenCalled()
131+
const sentHeaders = (
132+
captureMock.mock.calls[0][1] as { headers: Record<string, string> }
133+
).headers
134+
expect(sentHeaders["X-Initiator"]).toBe("agent")
135+
136+
// Restore default mock
137+
// @ts-expect-error – mock doesn't implement full fetch signature
138+
globalThis.fetch = fetchMock
139+
})
140+
141+
test("X-Initiator = user for pure user messages", async () => {
142+
const captureMock = mock(
143+
(_url: string, opts: { headers: Record<string, string> }) =>
144+
Promise.resolve({
145+
ok: true,
146+
json: () => Promise.resolve(mockResponseBody),
147+
headers: opts.headers,
148+
}),
149+
)
150+
// @ts-expect-error – mock doesn't implement full fetch signature
151+
globalThis.fetch = captureMock
152+
153+
await server.request("/v1/responses", {
154+
method: "POST",
155+
headers: { "Content-Type": "application/json" },
156+
body: JSON.stringify({
157+
model: "gpt-4o",
158+
stream: false,
159+
input: [{ type: "message", role: "user", content: "just a user" }],
160+
}),
161+
})
162+
163+
expect(captureMock).toHaveBeenCalled()
164+
const sentHeaders = (
165+
captureMock.mock.calls[0][1] as { headers: Record<string, string> }
166+
).headers
167+
expect(sentHeaders["X-Initiator"]).toBe("user")
168+
169+
// @ts-expect-error – mock doesn't implement full fetch signature
170+
globalThis.fetch = fetchMock
171+
})
172+
173+
test("X-Initiator = agent for function_call_output item", async () => {
174+
const captureMock = mock(
175+
(_url: string, opts: { headers: Record<string, string> }) =>
176+
Promise.resolve({
177+
ok: true,
178+
json: () => Promise.resolve(mockResponseBody),
179+
headers: opts.headers,
180+
}),
181+
)
182+
// @ts-expect-error – mock doesn't implement full fetch signature
183+
globalThis.fetch = captureMock
184+
185+
await server.request("/v1/responses", {
186+
method: "POST",
187+
headers: { "Content-Type": "application/json" },
188+
body: JSON.stringify({
189+
model: "gpt-4o",
190+
stream: false,
191+
input: [
192+
{ type: "function_call_output", call_id: "call_1", output: "{}" },
193+
],
194+
}),
195+
})
196+
197+
expect(captureMock).toHaveBeenCalled()
198+
const sentHeaders = (
199+
captureMock.mock.calls[0][1] as { headers: Record<string, string> }
200+
).headers
201+
expect(sentHeaders["X-Initiator"]).toBe("agent")
202+
203+
// @ts-expect-error – mock doesn't implement full fetch signature
204+
globalThis.fetch = fetchMock
205+
})
206+
207+
test("upstream 4xx returns error response", async () => {
208+
const errorMock = mock(() =>
209+
Promise.resolve({
210+
ok: false,
211+
status: 429,
212+
text: () => Promise.resolve("rate limited"),
213+
}),
214+
)
215+
// @ts-expect-error – mock doesn't implement full fetch signature
216+
globalThis.fetch = errorMock
217+
218+
const res = await server.request("/v1/responses", {
219+
method: "POST",
220+
headers: { "Content-Type": "application/json" },
221+
body: JSON.stringify({ model: "gpt-4o", stream: false, input: [] }),
222+
})
223+
224+
expect(res.status).toBe(429)
225+
226+
// @ts-expect-error – mock doesn't implement full fetch signature
227+
globalThis.fetch = fetchMock
92228
})
93229
})

0 commit comments

Comments
 (0)