From db10ba714e540c4187bfa0d068c275235dd09791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 27 Sep 2026 12:11:55 +0200 Subject: [PATCH] test(coach): pin the answered-clarification line on the local path The line that resolves an answered clarifying question was already appended to the system prompt on both paths; nothing proved it for the no-tools (local model) path. Three route-level cases now do: the local prompt and the tool-mode prompt both carry the server-resolved line, and a turn without an answer carries neither. Removing the line from the local prompt fails the first case. The golden harness gains a coachMessage.findMany mock for this; it defaults to no rows, so no existing transcript changes and the golden snapshots are untouched. The result-table caption test used the regex dotAll flag, which the TypeScript target rejects (TS1501); it matches across lines with [\s\S] instead. --- .../insights/chat/__tests__/golden-harness.ts | 2 + .../chat/__tests__/golden-transcripts.test.ts | 105 ++++++++++++++++++ .../__tests__/result-table.test.tsx | 2 +- 3 files changed, 108 insertions(+), 1 deletion(-) diff --git a/src/app/api/insights/chat/__tests__/golden-harness.ts b/src/app/api/insights/chat/__tests__/golden-harness.ts index 6003ba8c5..24eb8df5c 100644 --- a/src/app/api/insights/chat/__tests__/golden-harness.ts +++ b/src/app/api/insights/chat/__tests__/golden-harness.ts @@ -114,6 +114,7 @@ export const m = { userFindUnique: vi.fn(), userUpdate: vi.fn(), conversationFindFirst: vi.fn(), + coachMessageFindMany: vi.fn(), checkRateLimit: vi.fn(), resolveServerLocale: vi.fn(), runStreamingRawCompletionWithFallback: vi.fn(), @@ -192,6 +193,7 @@ export function resetGolden(): void { return {}; }); m.conversationFindFirst.mockResolvedValue({ id: "c-existing" }); + m.coachMessageFindMany.mockResolvedValue([]); m.checkRateLimit.mockResolvedValue({ allowed: true, resetAt: 0 }); m.resolveServerLocale.mockResolvedValue("en"); m.resolveProviderChain.mockResolvedValue([ diff --git a/src/app/api/insights/chat/__tests__/golden-transcripts.test.ts b/src/app/api/insights/chat/__tests__/golden-transcripts.test.ts index aecc3083c..a0114a14d 100644 --- a/src/app/api/insights/chat/__tests__/golden-transcripts.test.ts +++ b/src/app/api/insights/chat/__tests__/golden-transcripts.test.ts @@ -62,6 +62,7 @@ vi.mock("@/lib/db", () => ({ prisma: { user: { findUnique: h.m.userFindUnique, update: h.m.userUpdate }, coachConversation: { findFirst: h.m.conversationFindFirst }, + coachMessage: { findMany: h.m.coachMessageFindMany }, }, })); vi.mock("@/lib/rate-limit", () => ({ checkRateLimit: h.m.checkRateLimit })); @@ -574,6 +575,110 @@ describe("coach chat golden transcripts", () => { expect(types.filter((t) => t === "step")).toHaveLength(6); expect(JSON.stringify(transcript)).not.toContain("Ferritin"); }); + // 23 ── an answered clarification ────────────────────────────────────── + describe("an answered clarification reaches the prompt on both paths", () => { + const QUESTION_ID = "msg-question"; + const CLARIFIED = + "CLARIFIED: the person answered your clarifying question by choosing metric=pulse window=last30days."; + + beforeEach(() => { + m.fetchConversationWithMessages.mockResolvedValue({ + id: "c-existing", + summary: null, + attachmentCount: 0, + messages: [ + { + role: "user", + content: "How is my pulse?", + providerType: null, + metricSource: null, + }, + { + role: "assistant", + content: "Which pulse do you mean?", + providerType: "anthropic", + metricSource: null, + }, + ], + }); + m.coachMessageFindMany.mockResolvedValue([ + { + id: QUESTION_ID, + role: "assistant", + providerType: "anthropic", + metricSourceJson: JSON.stringify({ + windows: [], + metrics: [], + clarification: { + kind: "metric", + choices: [ + { + id: "c1", + labelKey: "coach.step.domain.snapshot", + label: "Resting pulse", + value: { metric: "pulse", window: "last30days" }, + }, + { + id: "c2", + labelKey: "coach.step.domain.snapshot", + label: "Heart rate variability", + value: { metric: "hrv" }, + }, + ], + freeText: true, + }, + }), + }, + ]); + }); + + const body = { + conversationId: "c-existing", + message: "Resting pulse", + clarification: { messageId: QUESTION_ID, choiceId: "c1" }, + }; + + it("the no-tools (local) prompt carries the resolved line", async () => { + m.resolveProviderChain.mockResolvedValue(NO_TOOLS_CHAIN); + m.runStreamingRawCompletionWithFallback.mockImplementation( + streamingResult("Your resting pulse averaged 62 bpm."), + ); + const transcript = await runTurn(post, body, {}); + expect(transcript.thrown).toBeNull(); + expect(m.runStreamingRawCompletionWithFallback).toHaveBeenCalledTimes(1); + const { params } = m.runStreamingRawCompletionWithFallback.mock + .calls[0][0] as { params: { system: string } }; + expect(params.system).toContain(CLARIFIED); + // Server-authored: the request's choice id picks the stored value, + // the label the model once wrote never rides along. + expect(params.system).not.toContain("Resting pulse"); + }); + + it("the tool-mode prompt carries the same line", async () => { + reply("Your resting pulse averaged 62 bpm."); + const transcript = await runTurn(post, body, {}); + expect(transcript.thrown).toBeNull(); + const { system } = m.runCoachToolLoop.mock.calls[0][0] as { + system: string; + }; + expect(system).toContain(CLARIFIED); + }); + + it("an unanswered turn sends neither prompt the line", async () => { + m.resolveProviderChain.mockResolvedValue(NO_TOOLS_CHAIN); + m.runStreamingRawCompletionWithFallback.mockImplementation( + streamingResult("Your resting pulse averaged 62 bpm."), + ); + await runTurn( + post, + { conversationId: "c-existing", message: "Resting pulse" }, + {}, + ); + const { params } = m.runStreamingRawCompletionWithFallback.mock + .calls[0][0] as { params: { system: string } }; + expect(params.system).not.toContain("CLARIFIED:"); + }); + }); }); describe("coach chat golden transcripts — refusals before the stream", () => { diff --git a/src/components/insights/coach-panel/__tests__/result-table.test.tsx b/src/components/insights/coach-panel/__tests__/result-table.test.tsx index b2c34e0c4..63255b912 100644 --- a/src/components/insights/coach-panel/__tests__/result-table.test.tsx +++ b/src/components/insights/coach-panel/__tests__/result-table.test.tsx @@ -73,7 +73,7 @@ describe("CoachResultTable", () => { , ); expect(html).toMatch( - /]*>.*Blood pressure by day.*30 readings, daily means.*<\/caption>/s, + /]*>[\s\S]*Blood pressure by day[\s\S]*30 readings, daily means[\s\S]*<\/caption>/, ); });