From 8dc34c769128d6e66ed5bb1bcb631d0bc1399bac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 5 Oct 2026 07:52:15 +0200 Subject: [PATCH] fix(coach): keep GLP-1 dose changes before the lookback limit out The GLP-1 block sent every dose change with its decrypted note whatever the lookback limit said. Dose changes are entries, so only those inside the limit reach the prompt; the dose in force stays as the current dose. --- .../ai/coach/__tests__/glp1-snapshot.test.ts | 63 +++++++++++++++++++ src/lib/ai/coach/glp1-snapshot.ts | 14 +++-- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/src/lib/ai/coach/__tests__/glp1-snapshot.test.ts b/src/lib/ai/coach/__tests__/glp1-snapshot.test.ts index 4836ac307..ab8fcbe7d 100644 --- a/src/lib/ai/coach/__tests__/glp1-snapshot.test.ts +++ b/src/lib/ai/coach/__tests__/glp1-snapshot.test.ts @@ -332,3 +332,66 @@ describe("buildGlp1SnapshotBlock injection days in the user's zone", () => { }); }); }); + +describe("buildGlp1SnapshotBlock under the lookback limit", () => { + it("leaves dose changes before the limit out and keeps the current dose", async () => { + const now = new Date("2026-10-04T12:00:00.000Z"); + prismaMock.medication.findMany.mockResolvedValue([ + fakeMedication({ + doseChanges: [ + { + doseValue: 2.5, + doseUnit: "mg", + effectiveFrom: new Date("2026-06-01T00:00:00.000Z"), + note: "Nausea in the first week", + }, + { + doseValue: 5, + doseUnit: "mg", + effectiveFrom: new Date("2026-08-01T00:00:00.000Z"), + note: "Stepped up after review", + }, + ], + }), + ]); + const out = await buildGlp1SnapshotBlock("user-1", now, "UTC", { + window: "last30days", + days: 30, + }); + const med = out?.medications[0]; + // The current dose is therapy state, not history. + expect(med?.currentDose).toMatchObject({ value: 5, since: "2026-08-01" }); + // Both changes lie before the 30-day floor: neither they nor their + // notes reach the prompt. + expect(med?.doseHistory).toEqual([]); + expect(JSON.stringify(out)).not.toContain("Nausea"); + expect(JSON.stringify(out)).not.toContain("Stepped up"); + }); + + it("keeps the dose changes inside the limit", async () => { + const now = new Date("2026-10-04T12:00:00.000Z"); + prismaMock.medication.findMany.mockResolvedValue([ + fakeMedication({ + doseChanges: [ + { + doseValue: 5, + doseUnit: "mg", + effectiveFrom: new Date("2026-08-01T00:00:00.000Z"), + note: null, + }, + { + doseValue: 7.5, + doseUnit: "mg", + effectiveFrom: new Date("2026-09-20T00:00:00.000Z"), + note: null, + }, + ], + }), + ]); + const out = await buildGlp1SnapshotBlock("user-1", now, "UTC", { + window: "last30days", + days: 30, + }); + expect(out?.medications[0].doseHistory.map((d) => d.value)).toEqual([7.5]); + }); +}); diff --git a/src/lib/ai/coach/glp1-snapshot.ts b/src/lib/ai/coach/glp1-snapshot.ts index 8ea49b05b..2edec8159 100644 --- a/src/lib/ai/coach/glp1-snapshot.ts +++ b/src/lib/ai/coach/glp1-snapshot.ts @@ -225,9 +225,10 @@ export async function buildGlp1SnapshotBlock( now: Date = new Date(), tz: string = DEFAULT_TIMEZONE, /** - * The Coach's lookback limit. The therapy plan (dose, titration, pens) is - * current state and stays; the last injection and the side-effect tags are - * entries and are read from inside the limit only. + * The Coach's lookback limit. The therapy plan (current dose, schedule, + * pens) is current state and stays; the last injection, the side-effect + * tags and the dose history (each change with its note) are entries and + * are read from inside the limit only. */ reach: CoachHistoryReach = UNBOUNDED_REACH, ): Promise { @@ -309,7 +310,12 @@ export async function buildGlp1SnapshotBlock( const genericSafe = sanitizeForPrompt(generic, 80); const latestChange = med.doseChanges[med.doseChanges.length - 1] ?? null; - const doseHistory: DoseHistoryEntry[] = med.doseChanges.map((dc) => { + // A dose change is an entry like any other: one made before the limit + // is left out with its note. The dose in force stays as `currentDose`. + const changesInReach = med.doseChanges.filter( + (dc) => floor === null || dc.effectiveFrom.getTime() >= floor.getTime(), + ); + const doseHistory: DoseHistoryEntry[] = changesInReach.map((dc) => { const decryptedNote = readNote(dc.noteEncrypted, dc.note); return { value: dc.doseValue,