Skip to content

Commit 71b9f3e

Browse files
test(ai): step 2 — cover Luna streaming hotfix
Checkpoint: verifies the production streaming request and isolates environment overrides. Reviewed against testing/hotfix-luna-chat-no-explicit-reasoning.md.
1 parent 47665af commit 71b9f3e

3 files changed

Lines changed: 55 additions & 5 deletions

File tree

web/src/lib/ai-coach.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
normalizeReasoningEffort,
1010
reasoningEffortForModel,
1111
runCoach,
12+
streamChatCoach,
1213
type CoachRequest,
1314
} from "./ai-coach";
1415

@@ -21,6 +22,17 @@ function mockOpenAi(content: string, ok = true, status = 200) {
2122
} as unknown as Response);
2223
}
2324

25+
function mockOpenAiStream(content: string) {
26+
const body = new ReadableStream({
27+
start(controller) {
28+
const payload = JSON.stringify({ choices: [{ delta: { content } }] });
29+
controller.enqueue(new TextEncoder().encode(`data: ${payload}\n\ndata: [DONE]\n\n`));
30+
controller.close();
31+
},
32+
});
33+
return vi.fn().mockResolvedValue({ ok: true, status: 200, body } as unknown as Response);
34+
}
35+
2436
const baseReq: CoachRequest = {
2537
mode: "explain",
2638
word: "見る",
@@ -111,6 +123,44 @@ describe("runCoach", () => {
111123
});
112124
});
113125

126+
describe("streamChatCoach", () => {
127+
const chatReq: CoachRequest = {
128+
...baseReq,
129+
mode: "chat",
130+
message: "What does this mean?",
131+
};
132+
133+
it("omits reasoning_effort from the default Luna streaming request", async () => {
134+
const fetchMock = mockOpenAiStream("It means to look.");
135+
vi.stubGlobal("fetch", fetchMock);
136+
137+
const chunks: string[] = [];
138+
for await (const chunk of streamChatCoach("sk-test", DEFAULT_COACH_MODEL, chatReq)) {
139+
chunks.push(chunk);
140+
}
141+
142+
expect(chunks).toEqual(["It means to look."]);
143+
const body = JSON.parse((fetchMock.mock.calls[0][1] as RequestInit).body as string);
144+
expect(body).toMatchObject({ model: "gpt-5.6-luna", stream: true });
145+
expect(body.max_completion_tokens).toBeGreaterThan(400);
146+
expect(body).not.toHaveProperty("reasoning_effort");
147+
expect(body).not.toHaveProperty("temperature");
148+
expect(body).not.toHaveProperty("max_tokens");
149+
});
150+
151+
it("preserves an explicit supported streaming effort", async () => {
152+
const fetchMock = mockOpenAiStream("It means to look.");
153+
vi.stubGlobal("fetch", fetchMock);
154+
155+
for await (const chunk of streamChatCoach("sk-test", DEFAULT_COACH_MODEL, chatReq, "low")) {
156+
expect(chunk).toBe("It means to look.");
157+
}
158+
159+
const body = JSON.parse((fetchMock.mock.calls[0][1] as RequestInit).body as string);
160+
expect(body.reasoning_effort).toBe("low");
161+
});
162+
});
163+
114164
describe("reasoning model tuning", () => {
115165
it("defaults the coach to gpt-5.6-luna and OpenAI's medium effort", () => {
116166
expect(DEFAULT_COACH_MODEL).toBe("gpt-5.6-luna");

web/src/lib/ai-coach.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export interface ChatResult {
7272
export type CoachResult = ExplainResult | HooksResult | ChatResult;
7373

7474
// Verified August 2026: gpt-5.6-luna (released 2026-07-09) is $0.20 / 1M input,
75-
// $1.20 / 1M output, $0.02 / 1M cached input. It is a reasoning model; at the
75+
// $1.20 / 1M output, $0.02 / 1M cached input. It is a reasoning model, so
7676
// reasoning tokens are billed as output. The actual cost is workload-dependent
7777
// and can exceed the backend's $0.002/call assumption, so usage must be
7878
// monitored. The model is overridable via AI_COACH_MODEL; reasoning effort is

web/src/lib/ai-store.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ const { currentMonth, getCoachConfig, getUsage, incrementUsage, quotaFor, refund
1414

1515
const MONTH = "2026-08";
1616

17-
describe("usage buckets", () => {
18-
beforeEach(() => {
19-
vi.unstubAllEnvs();
20-
});
17+
beforeEach(() => {
18+
vi.unstubAllEnvs();
19+
});
2120

21+
describe("usage buckets", () => {
2222
// The bug this pins: pronunciation audio and smart word picking were metered
2323
// against the same counter as the coach, so a learner could exhaust the
2424
// advertised "AI messages" allowance without ever opening the coach.

0 commit comments

Comments
 (0)