mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
The Coach read lab results from the last twelve months only, even with the lookback set to its widest option, so a slow change in LDL or PSA across years stayed out of reach. Labs are sparse, so they are no longer held to the one-year window that bounds the dense readings: they reach back over the whole record, and a lookback limit the person set still applies. The snapshot block keeps the latest value per marker, now over that wider window, at its old size: it shares the prompt's size cap and is never shed. get_labs with an analyte adds each matching marker's history, summarised so it stays bounded: the newest five earlier readings in full and older ones as one mean / min / max / count per year, ten years at most, older years counted. With a heavy fixture (30 markers, quarterly for 20 years) the overview stays at 4.3k characters and a named LDL read grows from 0.2k to 1.2k characters for 80 readings. The MCP get_labs read and the lab resource follow, since they run the same tool. The widest lookback option no longer reads "All": it names what it reads, twelve months in detail and every lab result, in every locale. Refs #1263
145 lines
5.0 KiB
TypeScript
145 lines
5.0 KiB
TypeScript
/**
|
|
* The Coach reads lab results over the whole record, summarised (#1263).
|
|
*
|
|
* The labs block used to stop at twelve months whatever the lookback setting
|
|
* said, so a slow drift in LDL or PSA across years was invisible even with
|
|
* "All" chosen. Labs are sparse, so they now reach back over the whole record
|
|
* while the dense readings keep their one-year daily window; a long history
|
|
* folds into a bounded summary so the prompt stays small. A lookback limit the
|
|
* person set still applies.
|
|
*
|
|
* Real Postgres, real tool executor, a heavy fixture: thirty markers measured
|
|
* every quarter for twenty years.
|
|
*/
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { getPrismaClient, truncateAllTables } from "./setup";
|
|
import { makeUser } from "../kit/factories";
|
|
import { executeCoachTool } from "@/lib/ai/coach/tools/executor";
|
|
import { __resetCoachSnapshotCacheForTests } from "@/lib/ai/coach/snapshot";
|
|
import {
|
|
HISTORY_REACH_DAYS,
|
|
UNBOUNDED_REACH,
|
|
type CoachHistoryReach,
|
|
} from "@/lib/ai/coach/history-reach";
|
|
import type { CoachLabsBlock } from "@/lib/ai/coach/labs-snapshot";
|
|
|
|
vi.mock("@/lib/db-compat", () => ({
|
|
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
|
|
}));
|
|
vi.mock("@/lib/jobs/boss-instance", () => ({
|
|
getGlobalBoss: vi.fn(() => null),
|
|
}));
|
|
|
|
const prisma = getPrismaClient();
|
|
const DAY = 86_400_000;
|
|
const MARKERS = 30;
|
|
const YEARS = 20;
|
|
const NOW = Date.now();
|
|
|
|
async function seedHeavyHistory(): Promise<string> {
|
|
const user = await makeUser({ timezone: "UTC" });
|
|
const rows = [];
|
|
for (let m = 0; m < MARKERS; m++) {
|
|
for (let q = 0; q < YEARS * 4; q++) {
|
|
rows.push({
|
|
userId: user.id,
|
|
analyte: m === 0 ? "LDL" : `Marker ${m}`,
|
|
// LDL climbs one unit a quarter: 100 twenty years ago, 179 now.
|
|
value: m === 0 ? 179 - q : 50 + ((m * 7 + q) % 40),
|
|
unit: "mg/dL",
|
|
referenceLow: 0,
|
|
referenceHigh: 130,
|
|
takenAt: new Date(NOW - (q * 91 + 2) * DAY),
|
|
});
|
|
}
|
|
}
|
|
await prisma.labResult.createMany({ data: rows });
|
|
return user.id;
|
|
}
|
|
|
|
async function readLabs(
|
|
userId: string,
|
|
reach: CoachHistoryReach,
|
|
args: Record<string, unknown> = {},
|
|
): Promise<CoachLabsBlock> {
|
|
const result = await executeCoachTool({
|
|
userId,
|
|
name: "get_labs",
|
|
rawArguments: JSON.stringify(args),
|
|
fallbackWindow: "allTime",
|
|
reach,
|
|
});
|
|
expect(result.present).toBe(true);
|
|
return (result as { data: CoachLabsBlock }).data;
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
await truncateAllTables(prisma);
|
|
__resetCoachSnapshotCacheForTests();
|
|
});
|
|
afterEach(() => {
|
|
__resetCoachSnapshotCacheForTests();
|
|
});
|
|
|
|
describe("the Coach labs read over a twenty-year history", () => {
|
|
it("reads a marker last measured years ago, and keeps the overview small", async () => {
|
|
const userId = await seedHeavyHistory();
|
|
// A panel from six years ago and nothing since: outside the old
|
|
// twelve-month read, inside the whole record.
|
|
await prisma.labResult.create({
|
|
data: {
|
|
userId,
|
|
analyte: "PSA",
|
|
value: 1.4,
|
|
unit: "ng/mL",
|
|
takenAt: new Date(NOW - 6 * 365 * DAY),
|
|
},
|
|
});
|
|
const labs = await readLabs(userId, UNBOUNDED_REACH, { analyte: "psa" });
|
|
expect(labs.recent.map((entry) => entry.value)).toEqual([1.4]);
|
|
|
|
// The overview (and the snapshot block it is) carries the latest value
|
|
// per marker and no history: it shares the prompt's size cap.
|
|
const overview = await readLabs(userId, UNBOUNDED_REACH);
|
|
expect(overview.recent).toHaveLength(24);
|
|
expect(overview.recent.every((entry) => !entry.history)).toBe(true);
|
|
expect(JSON.stringify(overview).length).toBeLessThan(8_000);
|
|
});
|
|
|
|
it("summarises a named marker's whole history in a bounded entry", async () => {
|
|
const userId = await seedHeavyHistory();
|
|
const labs = await readLabs(userId, UNBOUNDED_REACH, { analyte: "LDL" });
|
|
|
|
expect(labs.recent).toHaveLength(1);
|
|
const ldl = labs.recent[0];
|
|
expect(ldl.value).toBe(179);
|
|
expect(ldl.history?.count).toBe(YEARS * 4);
|
|
const first = Date.parse(ldl.history!.firstTakenAt);
|
|
expect(NOW - first).toBeGreaterThan(19 * 365 * DAY);
|
|
// The drift is readable from the yearly means: lower long ago.
|
|
const yearly = ldl.history!.yearly;
|
|
expect(yearly.at(-1)!.mean).toBeLessThan(yearly[0].mean);
|
|
expect(yearly.length + ldl.history!.yearsOmitted).toBeGreaterThanOrEqual(
|
|
YEARS - 2,
|
|
);
|
|
// Eighty readings, one bounded entry.
|
|
expect(JSON.stringify(labs).length).toBeLessThan(2_000);
|
|
});
|
|
|
|
it("stops at the lookback limit the person set", async () => {
|
|
const userId = await seedHeavyHistory();
|
|
const labs = await readLabs(
|
|
userId,
|
|
{ window: "lastYear", days: HISTORY_REACH_DAYS.lastYear },
|
|
{ analyte: "LDL" },
|
|
);
|
|
const ldl = labs.recent[0];
|
|
expect(ldl.history!.count).toBeLessThanOrEqual(5);
|
|
expect(NOW - Date.parse(ldl.history!.firstTakenAt)).toBeLessThanOrEqual(
|
|
365 * DAY,
|
|
);
|
|
expect(ldl.history!.yearly).toEqual([]);
|
|
});
|
|
});
|