Merge branch 'coach/wp1-steps' into release/v1.39.4

# Conflicts:
#	src/app/api/insights/chat/__tests__/route-capability-gate.test.ts
#	src/app/api/insights/chat/__tests__/route-list-query.test.ts
This commit is contained in:
Marc-André Bombeck committed 2026-09-27 11:53:39 +02:00
commit b84ea0604f
10 files changed
+1824 -20

No files matched your search

@@ -816,6 +816,17 @@ exports[`coach chat golden transcripts > 03 no-tools path with the full snapshot
"metrics": [
"bp",
],
"steps": [
{
"count": 1,
"domain": "snapshot",
"id": "s1",
"label": "Reading your record summary",
"labelKey": "coach.step.snapshot",
"status": "done",
"tool": "snapshot",
},
],
"windows": [
"last30days",
],
@@ -844,6 +855,18 @@ exports[`coach chat golden transcripts > 03 no-tools path with the full snapshot
],
"contentType": "text/event-stream; charset=utf-8",
"frames": [
{
"step": {
"count": 1,
"domain": "snapshot",
"id": "s1",
"label": "Reading your record summary",
"labelKey": "coach.step.snapshot",
"status": "done",
"tool": "snapshot",
},
"type": "step",
},
{
"token": "Your ",
"type": "token",
@@ -889,6 +912,17 @@ exports[`coach chat golden transcripts > 03 no-tools path with the full snapshot
"metrics": [
"bp",
],
"steps": [
{
"count": 1,
"domain": "snapshot",
"id": "s1",
"label": "Reading your record summary",
"labelKey": "coach.step.snapshot",
"status": "done",
"tool": "snapshot",
},
],
"windows": [
"last30days",
],
@@ -4004,6 +4038,17 @@ exports[`coach chat golden transcripts > 16b an empty chain served by the legacy
"metrics": [
"bp",
],
"steps": [
{
"count": 1,
"domain": "snapshot",
"id": "s1",
"label": "Reading your record summary",
"labelKey": "coach.step.snapshot",
"status": "done",
"tool": "snapshot",
},
],
"windows": [
"last30days",
],
@@ -4032,6 +4077,18 @@ exports[`coach chat golden transcripts > 16b an empty chain served by the legacy
],
"contentType": "text/event-stream; charset=utf-8",
"frames": [
{
"step": {
"count": 1,
"domain": "snapshot",
"id": "s1",
"label": "Reading your record summary",
"labelKey": "coach.step.snapshot",
"status": "done",
"tool": "snapshot",
},
"type": "step",
},
{
"token": "Your ",
"type": "token",
@@ -4077,6 +4134,17 @@ exports[`coach chat golden transcripts > 16b an empty chain served by the legacy
"metrics": [
"bp",
],
"steps": [
{
"count": 1,
"domain": "snapshot",
"id": "s1",
"label": "Reading your record summary",
"labelKey": "coach.step.snapshot",
"status": "done",
"tool": "snapshot",
},
],
"windows": [
"last30days",
],
@@ -5259,6 +5327,352 @@ exports[`coach chat golden transcripts > 21 the German locale with a stored self
}
`;
exports[`coach chat golden transcripts > 22 tool mode with live steps: found, empty and failed, all before the first token 1`] = `
{
"body": null,
"calls": [
{
"call": "createConversation",
},
{
"call": "appendMessage",
"content": "How is my blood pressure?",
"metricSource": null,
"model": null,
"providerType": null,
"role": "user",
"tokensUsed": null,
},
{
"call": "storeDeterministicFacts",
},
{
"call": "assertConsentForChain",
"chain": [
"anthropic",
],
},
{
"amount": 1800,
"call": "reserveBudget",
"cap": 2000000,
"dateKey": "2026-09-26",
"owner": "user",
"surface": "coach",
},
{
"call": "runCoachToolLoop",
"fallbackWindow": "allTime",
"maxTokens": 600,
"messages": [
"user:0c52b5bc28a6:239",
],
"providers": [
"anthropic",
],
"sharedScope": {
"sources": [
"bp",
],
"window": "last30days",
},
"signal": true,
"system": "c7cb63b1723d:2954",
"temperature": 0.4,
"timeoutMs": 180000,
"tools": 2,
},
{
"actual": 80,
"attribution": {
"reservedOwner": "user",
"servedBy": "anthropic",
},
"cached": 0,
"call": "reconcileSpend",
"dateKey": "2026-09-26",
"reserved": 1800,
},
{
"call": "appendMessage",
"content": "Your systolic averaged 128 mmHg.",
"metricSource": {
"groundedFigures": [
128,
82,
46,
105,
35.9375,
56.09756097560976,
42,
],
"method": {
"entries": [
{
"count": 142,
"domain": "bp",
"window": "last90days",
},
{
"absent": "no_data",
"domain": "sleep",
"window": "allTime",
},
],
"text": "Blood pressure, last 90 days: 142 readings · Sleep, all time: no readings",
},
"metrics": [
"bp",
],
"steps": [
{
"count": 142,
"domain": "bp",
"id": "s1",
"label": "Checking: Blood pressure, last 90 days",
"labelKey": "coach.step.readWindow",
"status": "done",
"tool": "get_metric_series",
"window": "last90days",
},
{
"domain": "sleep",
"id": "s2",
"label": "Checking: Sleep, all time",
"labelKey": "coach.step.readWindow",
"reason": "no_data",
"status": "empty",
"tool": "get_sleep",
"window": "allTime",
},
{
"domain": "labs",
"id": "s3",
"label": "Checking: Lab results, last 12 months",
"labelKey": "coach.step.readWindow",
"reason": "retrieval_failed",
"status": "failed",
"tool": "get_labs",
"window": "lastYear",
},
],
"toolCalls": [
{
"name": "get_metric_series",
"present": true,
},
],
"windows": [
"last30days",
],
},
"model": "m-tool",
"providerType": "anthropic",
"role": "assistant",
"tokensUsed": 80,
},
{
"call": "annotate",
"metaKeys": [
"clusterCount",
"conversationId",
"historyTurns",
"promptChars",
"promptVersion",
"provider",
"snapshotSent",
"tokens",
"toolMode",
"toolsCalled",
],
"name": "insights.coach.replied",
},
],
"contentType": "text/event-stream; charset=utf-8",
"frames": [
{
"step": {
"domain": "bp",
"id": "s1",
"label": "Checking: Blood pressure, last 90 days",
"labelKey": "coach.step.readWindow",
"status": "running",
"tool": "get_metric_series",
"window": "last90days",
},
"type": "step",
},
{
"step": {
"domain": "sleep",
"id": "s2",
"label": "Checking: Sleep, all time",
"labelKey": "coach.step.readWindow",
"status": "running",
"tool": "get_sleep",
"window": "allTime",
},
"type": "step",
},
{
"step": {
"domain": "labs",
"id": "s3",
"label": "Checking: Lab results, last 12 months",
"labelKey": "coach.step.readWindow",
"status": "running",
"tool": "get_labs",
"window": "lastYear",
},
"type": "step",
},
{
"step": {
"count": 142,
"domain": "bp",
"id": "s1",
"label": "Checking: Blood pressure, last 90 days",
"labelKey": "coach.step.readWindow",
"status": "done",
"tool": "get_metric_series",
"window": "last90days",
},
"type": "step",
},
{
"step": {
"domain": "sleep",
"id": "s2",
"label": "Checking: Sleep, all time",
"labelKey": "coach.step.readWindow",
"reason": "no_data",
"status": "empty",
"tool": "get_sleep",
"window": "allTime",
},
"type": "step",
},
{
"step": {
"domain": "labs",
"id": "s3",
"label": "Checking: Lab results, last 12 months",
"labelKey": "coach.step.readWindow",
"reason": "retrieval_failed",
"status": "failed",
"tool": "get_labs",
"window": "lastYear",
},
"type": "step",
},
{
"token": "Your ",
"type": "token",
},
{
"token": "systolic ",
"type": "token",
},
{
"token": "averaged ",
"type": "token",
},
{
"token": "128 ",
"type": "token",
},
{
"token": "mmHg.",
"type": "token",
},
{
"metricSource": {
"groundedFigures": [
128,
82,
46,
105,
35.9375,
56.09756097560976,
42,
],
"method": {
"entries": [
{
"count": 142,
"domain": "bp",
"window": "last90days",
},
{
"absent": "no_data",
"domain": "sleep",
"window": "allTime",
},
],
"text": "Blood pressure, last 90 days: 142 readings · Sleep, all time: no readings",
},
"metrics": [
"bp",
],
"steps": [
{
"count": 142,
"domain": "bp",
"id": "s1",
"label": "Checking: Blood pressure, last 90 days",
"labelKey": "coach.step.readWindow",
"status": "done",
"tool": "get_metric_series",
"window": "last90days",
},
{
"domain": "sleep",
"id": "s2",
"label": "Checking: Sleep, all time",
"labelKey": "coach.step.readWindow",
"reason": "no_data",
"status": "empty",
"tool": "get_sleep",
"window": "allTime",
},
{
"domain": "labs",
"id": "s3",
"label": "Checking: Lab results, last 12 months",
"labelKey": "coach.step.readWindow",
"reason": "retrieval_failed",
"status": "failed",
"tool": "get_labs",
"window": "lastYear",
},
],
"toolCalls": [
{
"name": "get_metric_series",
"present": true,
},
],
"windows": [
"last30days",
],
},
"type": "provenance",
},
{
"conversationId": "c-new",
"messageId": "m2",
"type": "done",
"usage": {
"model": "m-tool",
"totalTokens": 80,
},
},
],
"status": 200,
"thrown": null,
}
`;
exports[`coach chat golden transcripts — refusals before the stream > R1 a fence-drift conversation 404s with an audit row 1`] = `
{
"body": null,
@@ -120,8 +120,13 @@ vi.mock("@/lib/medications/scheduled-doses", () => ({
vi.mock("@/lib/ai/coach/workout-evidence-builder", () => ({
buildWorkoutEvidenceSection: h.m.buildWorkoutEvidenceSection,
}));
vi.mock("@/lib/ai/coach/tools", () => ({
vi.mock("@/lib/ai/coach/tools", async () => ({
COACH_TOOL_DEFS: [{ name: "get_metric_series" }, { name: "get_sleep" }],
parseCoachToolArgs: (
await vi.importActual<typeof import("@/lib/ai/coach/tools/definitions")>(
"@/lib/ai/coach/tools/definitions",
)
).parseCoachToolArgs,
MAX_ROUNDS: 3,
buildCoachDataInventory: h.m.buildCoachDataInventory,
renderDataInventory: h.m.renderDataInventory,
@@ -521,6 +526,54 @@ describe("coach chat golden transcripts", () => {
reply("Du läufst 30 km pro Woche, dein Blutdruck liegt bei 128 mmHg.");
await golden({ message: "Wie ist mein Blutdruck?", locale: "de" });
});
it("22 tool mode with live steps: found, empty and failed, all before the first token", async () => {
type Call = { id: string; name: string; arguments: string };
type Settled = { present: boolean; reason?: string; data?: unknown };
const calls: Array<[Call, Settled]> = [
[
{
id: "a",
name: "get_metric_series",
arguments: '{"metric":"bp","window":"last90days"}',
},
{
present: true,
data: {
metric: "bp",
section: { aggregate: { coverage: { count: 142 } } },
},
},
],
[
{ id: "b", name: "get_sleep", arguments: "{}" },
{ present: false, reason: "no_data" },
],
[
{ id: "c", name: "get_labs", arguments: '{"analyte":"Ferritin"}' },
{ present: false, reason: "retrieval_failed" },
],
];
m.runCoachToolLoop.mockImplementation(
async (args: {
onCallStart?: (call: Call, index: number) => void;
onCallSettled?: (call: Call, result: Settled, index: number) => void;
}) => {
calls.forEach(([call], i) => args.onCallStart?.(call, i));
calls.forEach(([call, result], i) =>
args.onCallSettled?.(call, result, i),
);
return toolLoopResult("Your systolic averaged 128 mmHg.");
},
);
const transcript = await golden({ message: "How is my blood pressure?" });
const types = transcript.frames.map((f) => (f as { type: string }).type);
const firstToken = types.indexOf("token");
expect(firstToken).toBeGreaterThan(0);
expect(types.lastIndexOf("step")).toBeLessThan(firstToken);
expect(types.filter((t) => t === "step")).toHaveLength(6);
expect(JSON.stringify(transcript)).not.toContain("Ferritin");
});
});
describe("coach chat golden transcripts — refusals before the stream", () => {
@@ -66,6 +66,7 @@ vi.mock("@/lib/ai/provider", () => ({
}));
vi.mock("@/lib/ai/consent-guard", () => ({ assertConsentForChain: vi.fn() }));
vi.mock("@/lib/ai/prompts/insight-generator", () => ({ PROMPT_VERSION: "x" }));
// The rest of the module stays real: the step labels read its enums at load.
vi.mock("@/lib/ai/coach/types", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/ai/coach/types")>()),
coachChatRequestSchema: {
@@ -53,6 +53,7 @@ vi.mock("@/lib/ai/provider", () => ({
}));
vi.mock("@/lib/ai/consent-guard", () => ({ assertConsentForChain: vi.fn() }));
vi.mock("@/lib/ai/prompts/insight-generator", () => ({ PROMPT_VERSION: "x" }));
// The rest of the module stays real: the step labels read its enums at load.
vi.mock("@/lib/ai/coach/types", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/ai/coach/types")>()),
coachChatRequestSchema: { safeParse: () => ({ success: false }) },