test(daily): cover the due-dose rail item and the briefing grounding disposal

This commit is contained in:
Marc-André Bombeck committed 2026-07-19 09:14:09 +02:00
1 parent f8bb8f4b9b
commit cbb9528cb2
2 files changed
+313 -3

No files matched your search

+112 -3
View File
@@ -6,6 +6,7 @@ import type { MedsTodayBlock } from "@/lib/dashboard/meds-today";
import {
buildDailyDigest,
COACH_CHECKIN_RESURFACE_DAYS,
DOSE_DUE_LOOKAHEAD_MS,
MAX_WORTH_A_LOOK,
type DailyDigestCoachPlan,
type DailyDigestInput,
@@ -211,8 +212,8 @@ describe("buildDailyDigest — worth-a-look rail item builders", () => {
expect(dose).toBeDefined();
expect(dose?.status).toBe("warning");
expect(dose?.moduleKey).toBe("medications");
expect(dose?.title).toBe("Medication due");
expect(dose?.body).toBe("Ramipril is due today.");
expect(dose?.title).toBe("Medication overdue");
expect(dose?.body).toBe("Ramipril is past due.");
expect(dose?.actions).toHaveLength(1);
expect(dose?.actions[0].intent).toBe("dose.log");
// Deep-links straight to the overdue medication's card, not the bare
@@ -249,7 +250,7 @@ describe("buildDailyDigest — worth-a-look rail item builders", () => {
expect(d.worthALook.some((i) => i.kind === "dose_window")).toBe(false);
});
it("does NOT emit a dose-window item when nothing is overdue", () => {
it("does NOT emit a dose-window item when nothing is due at all", () => {
const d = buildDailyDigest(
input({ medsToday: meds({ nextDueOverdue: false }) }),
t,
@@ -257,6 +258,114 @@ describe("buildDailyDigest — worth-a-look rail item builders", () => {
expect(d.worthALook.some((i) => i.kind === "dose_window")).toBe(false);
});
it("emits a calm due item for a takeable dose that is not yet overdue", () => {
const d = buildDailyDigest(
input({
medsToday: meds({
activeCount: 1,
scheduledToday: 1,
nextDueAt: new Date(NOW.getTime() + 30 * 60_000).toISOString(),
nextDueOverdue: false,
nextDueMedicationName: "Ramipril",
nextDueMedicationId: "med-1",
}),
}),
t,
);
const dose = d.worthALook.find((i) => i.kind === "dose_window");
expect(dose).toBeDefined();
// Reads as informational, NOT as the overdue warning.
expect(dose?.status).toBe("info");
expect(dose?.title).toBe(t("daily.item.doseWindow.title"));
expect(dose?.body).toBe(
t("daily.item.doseWindow.bodyNamed", { name: "Ramipril" }),
);
expect(dose?.actions[0]?.href).toBe("/medications?highlight=med-1");
});
it("reads an overdue dose differently from a merely due one", () => {
const due = buildDailyDigest(
input({
medsToday: meds({
nextDueAt: new Date(NOW.getTime() + 30 * 60_000).toISOString(),
nextDueOverdue: false,
nextDueMedicationName: "Ramipril",
}),
}),
t,
).worthALook.find((i) => i.kind === "dose_window");
const overdue = buildDailyDigest(
input({
medsToday: meds({
nextDueAt: new Date(NOW.getTime() - 30 * 60_000).toISOString(),
nextDueOverdue: true,
nextDueMedicationName: "Ramipril",
}),
}),
t,
).worthALook.find((i) => i.kind === "dose_window");
expect(overdue?.status).toBe("warning");
expect(overdue?.title).toBe(t("daily.item.doseWindow.overdueTitle"));
expect(overdue?.body).toBe(
t("daily.item.doseWindow.overdueBodyNamed", { name: "Ramipril" }),
);
// The two faces must not collapse into the same copy.
expect(overdue?.title).not.toBe(due?.title);
expect(overdue?.body).not.toBe(due?.body);
expect(overdue?.status).not.toBe(due?.status);
});
it("does NOT manufacture a dose item on a day whose doses are all taken", () => {
// Every scheduled dose resolved; the next display-due slot is tomorrow.
const d = buildDailyDigest(
input({
medsToday: meds({
activeCount: 2,
scheduledToday: 2,
takenToday: 2,
nextDueAt: new Date(NOW.getTime() + 22 * 60 * 60_000).toISOString(),
nextDueOverdue: false,
nextDueMedicationName: "Ramipril",
}),
}),
t,
);
expect(d.worthALook.some((i) => i.kind === "dose_window")).toBe(false);
});
it("keeps a dose beyond the takeable lead off the rail", () => {
const d = buildDailyDigest(
input({
medsToday: meds({
nextDueAt: new Date(
NOW.getTime() + DOSE_DUE_LOOKAHEAD_MS + 60_000,
).toISOString(),
nextDueOverdue: false,
nextDueMedicationName: "Ramipril",
}),
}),
t,
);
expect(d.worthALook.some((i) => i.kind === "dose_window")).toBe(false);
});
it("renders neither face for a cached block whose anchor has since passed", () => {
// `MedsTodayBlock` contract: a past `nextDueAt` with `nextDueOverdue:
// false` means the anchor passed AFTER the block was built.
const d = buildDailyDigest(
input({
medsToday: meds({
nextDueAt: new Date(NOW.getTime() - 5 * 60_000).toISOString(),
nextDueOverdue: false,
nextDueMedicationName: "Ramipril",
}),
}),
t,
);
expect(d.worthALook.some((i) => i.kind === "dose_window")).toBe(false);
});
it("maps one sync-issue item per broken integration", () => {
const d = buildDailyDigest(
input({
@@ -0,0 +1,201 @@
/**
* Daily-briefing grounding gate — what stands in place of a rejected briefing.
*
* The gate itself is absolute: a briefing carrying a number the server never
* computed is discarded, always. These tests pin the DISPOSAL, which used to
* fork on why the corrective retry failed — a transport failure fell back to
* the previous cached briefing, a content failure left a hole and the reader
* lost the paragraph, signals, findings and recommendations over one figure.
* Both paths now fall back, and the hard strip survives only when there is
* genuinely no previous briefing to show.
*
* They also pin the widened allow-set: the prompt hands the model the WHOLE
* features object, so a figure from any block in it (here a medication
* compliance rate) is grounded — while an invented number still is not.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
const findUnique = vi.fn();
const userUpdate = vi.fn();
const resolveProviderChain = vi.fn();
const resolveProvider = vi.fn();
const runRawCompletionWithFallback = vi.fn();
const extractFeatures = vi.fn();
vi.mock("@/lib/db", () => ({
prisma: {
$queryRaw: vi.fn(async () => [{ total_tokens: 0 }]),
$executeRaw: vi.fn(async () => 0),
user: {
findUnique: (...a: unknown[]) => findUnique(...a),
update: (...a: unknown[]) => userUpdate(...a),
},
auditLog: { deleteMany: vi.fn() },
},
}));
vi.mock("@/lib/ai/provider", () => ({
resolveProviderChain: (...a: unknown[]) => resolveProviderChain(...a),
resolveProvider: (...a: unknown[]) => resolveProvider(...a),
}));
vi.mock("@/lib/ai/provider-runner", () => ({
AllProvidersFailedError: class extends Error {},
runRawCompletionWithFallback: (...a: unknown[]) =>
runRawCompletionWithFallback(...a),
}));
vi.mock("@/lib/insights/features", () => ({
FeaturesPayloadTooLargeError: class extends Error {
sizeBytes = 0;
},
extractFeatures: (...a: unknown[]) => extractFeatures(...a),
BRIEFING_FEATURE_WINDOW_DAYS: 400,
}));
vi.mock("@/lib/insights/illness-cycle-briefing", () => ({
buildBriefingIllnessCycleContext: vi.fn().mockResolvedValue(null),
buildBriefingIllnessCyclePrompt: vi.fn().mockReturnValue(""),
}));
vi.mock("@/lib/insights/glp1-plateau", () => ({
detectGlp1Plateau: vi.fn(async () => null),
buildGlp1PlateauPrompt: vi.fn(() => ""),
}));
vi.mock("@/lib/ai/coach/about-me", () => ({
getSelfContextTextForUser: vi.fn(async () => null),
buildAboutMeInsightBlock: vi.fn(() => ""),
}));
vi.mock("@/lib/cache/invalidate", () => ({
invalidateUserInsights: vi.fn(),
}));
import { generateComprehensiveInsight } from "../comprehensive-generate";
/**
* A signals block (so the gate engages at all) plus a medications block. The
* medication figures are server-computed and reach the prompt, but the old
* hand-maintained allow-set never admitted them.
*/
const FEATURES = {
signalsOfDay: [
{
sourceMetric: "weight",
latest: 81.4,
avg7: 82,
avg30: 82.1,
deltaVs7: -0.6,
deltaVs30: -0.7,
spread30: 1.2,
recentAnomaly: null,
},
],
medications: [
{
name: "Ramipril",
dose: "5 mg",
category: "cardio",
compliance7: 100,
compliance30: 86,
compliance90: 88,
streak: 12,
missedLast7: 0,
},
],
};
/** A number that appears nowhere in the features payload. */
const FABRICATED = JSON.stringify({
dailyBriefing: { paragraph: "Your weight is down 47.9 kg this week." },
});
/** Cites the medication compliance rate the server itself computed. */
const GROUNDED_VIA_MEDS = JSON.stringify({
dailyBriefing: { paragraph: "Adherence sits at 86 % over the last 30 days." },
});
const PREVIOUS_PAYLOAD = JSON.stringify({
dailyBriefing: { paragraph: "Yesterday's grounded briefing." },
recommendations: [],
});
function cachedUser(over: Record<string, unknown> = {}) {
return {
insightsPrivacyMode: "aggregated",
insightsCachedAt: null,
insightsCachedText: null,
insightsExcludeMetrics: [],
// Never equal to the freshly computed hash, so a full generation runs.
insightsSnapshotHash: null,
insightsBriefingRerollDate: null,
...over,
};
}
function completion(content: string) {
return {
result: { content, tokensUsed: 10, model: "m" },
workingProvider: { providerType: "openai" },
fallbackHops: [],
};
}
/** The `dailyBriefing` the generation persisted. */
function persistedBriefing(): unknown {
const call = userUpdate.mock.calls.at(-1);
const text = call?.[0]?.data?.insightsCachedText as string | undefined;
if (!text) return undefined;
return (JSON.parse(text) as Record<string, unknown>).dailyBriefing;
}
beforeEach(() => {
vi.clearAllMocks();
resolveProviderChain.mockResolvedValue([
{ providerType: "openai", instance: {} },
]);
resolveProvider.mockResolvedValue({ type: "none" });
extractFeatures.mockResolvedValue(FEATURES);
userUpdate.mockResolvedValue({});
findUnique.mockResolvedValue(cachedUser());
});
describe("briefing grounding — disposal", () => {
it("keeps the previous briefing when the corrective retry fails on CONTENT", async () => {
findUnique.mockResolvedValue(
cachedUser({
insightsCachedAt: new Date("2026-07-15T03:00:00.000Z"),
insightsCachedText: PREVIOUS_PAYLOAD,
}),
);
// Both passes carry the same fabricated figure: a content failure.
runRawCompletionWithFallback.mockResolvedValue(completion(FABRICATED));
await generateComprehensiveInsight("u1", { locale: "en" });
// The fabricated text never persists...
expect(JSON.stringify(persistedBriefing())).not.toContain("47.9");
// ...and the reader keeps yesterday's grounded briefing rather than a hole.
expect(persistedBriefing()).toEqual({
paragraph: "Yesterday's grounded briefing.",
});
});
it("still strips hard when there is no previous briefing to stand in", async () => {
runRawCompletionWithFallback.mockResolvedValue(completion(FABRICATED));
await generateComprehensiveInsight("u1", { locale: "en" });
expect(persistedBriefing()).toBeNull();
});
it("admits a figure from a features block the old allow-set never listed", async () => {
runRawCompletionWithFallback.mockResolvedValue(
completion(GROUNDED_VIA_MEDS),
);
const outcome = await generateComprehensiveInsight("u1", { locale: "en" });
expect(outcome).toEqual({ status: "generated", providerType: "openai" });
// No corrective retry: the compliance rate is grounded, so the gate is
// satisfied on the first pass and the briefing survives intact.
expect(runRawCompletionWithFallback).toHaveBeenCalledTimes(1);
expect(persistedBriefing()).toEqual({
paragraph: "Adherence sits at 86 % over the last 30 days.",
});
});
});