Files
HealthLog/src/lib/insights/__tests__/status-cache.test.ts
T
Marc-André Bombeck 23f7c74c04 perf(insights): regenerate texts only when the underlying data changed
A sha256 fingerprint of the compacted feature snapshot is stored beside
every generated text; nightly, forced and ingest-triggered regeneration
all skip the model call when the fingerprint matches and refresh the
timestamp instead. Warms cover only the user's resolved locale and the
blanket per-status eviction after a comprehensive generation is gone —
a lazily-read second locale regenerates on its own miss path. The
ingest debounce widens from thirty minutes to six hours and no longer
deletes cache rows, the visit-triggered warm re-checks freshness at job
start, backs off for an hour after a failure and is capped at two
forced runs per user per day, and its dedup window now outlives the
client poll horizon. An active day drops from hundreds of model calls
to a handful tied to actual data change.
2026-06-11 21:17:05 +02:00

372 lines
12 KiB
TypeScript

import { describe, expect, it, vi, beforeEach } from "vitest";
vi.mock("@/lib/db", () => ({
prisma: {
auditLog: { findFirst: vi.fn(), findMany: vi.fn(), create: vi.fn() },
},
}));
const hasUsableStatusProvider = vi.fn();
vi.mock("@/lib/insights/status-provider", () => ({
hasUsableStatusProvider: (...a: unknown[]) => hasUsableStatusProvider(...a),
}));
const enqueueStatusGeneration = vi.fn();
vi.mock("@/lib/jobs/insight-status-generate-shared", () => ({
enqueueStatusGeneration: (...a: unknown[]) => enqueueStatusGeneration(...a),
}));
import { prisma } from "@/lib/db";
import {
isTimeoutStub,
readFreshStatusText,
refreshUnchangedStatusInsight,
resolveReadOnlyStatusMiss,
} from "../status-cache";
const TODAY = "2026-05-31";
function cacheRow(details: Record<string, unknown>, createdAt = new Date()) {
return { createdAt, details: JSON.stringify(details) };
}
beforeEach(() => {
vi.resetAllMocks();
});
describe("isTimeoutStub", () => {
it("flags the timeout-stub model marker", () => {
expect(isTimeoutStub({ model: "timeout-stub" })).toBe(true);
});
it("flags the timeout:true marker", () => {
expect(isTimeoutStub({ timeout: true })).toBe(true);
});
it("does not flag a real assessment", () => {
expect(isTimeoutStub({ model: "gpt-4o-mini", timeout: false })).toBe(false);
expect(isTimeoutStub({})).toBe(false);
});
});
describe("readFreshStatusText", () => {
it("returns today's real assessment text", async () => {
vi.mocked(prisma.auditLog.findFirst).mockResolvedValue(
cacheRow({
dateKey: TODAY,
text: "Your weight trend is stable.",
model: "gpt-4o-mini",
}) as never,
);
const hit = await readFreshStatusText({
userId: "u1",
cacheAction: "insights.weight-status.en",
todayKey: TODAY,
force: false,
});
expect(hit?.text).toBe("Your weight trend is stable.");
});
it("skips a timeout-stub row keyed to today", async () => {
vi.mocked(prisma.auditLog.findFirst).mockResolvedValue(
cacheRow({
dateKey: TODAY,
text: "Generic fallback advice.",
model: "timeout-stub",
timeout: true,
}) as never,
);
const hit = await readFreshStatusText({
userId: "u1",
cacheAction: "insights.weight-status.en",
todayKey: TODAY,
force: false,
});
expect(hit).toBeNull();
});
it("skips a stale-day row", async () => {
vi.mocked(prisma.auditLog.findFirst).mockResolvedValue(
cacheRow({ dateKey: "2026-05-30", text: "Yesterday." }) as never,
);
const hit = await readFreshStatusText({
userId: "u1",
cacheAction: "insights.weight-status.en",
todayKey: TODAY,
force: false,
});
expect(hit).toBeNull();
});
it("skips an empty-text row", async () => {
vi.mocked(prisma.auditLog.findFirst).mockResolvedValue(
cacheRow({ dateKey: TODAY, text: " " }) as never,
);
const hit = await readFreshStatusText({
userId: "u1",
cacheAction: "insights.weight-status.en",
todayKey: TODAY,
force: false,
});
expect(hit).toBeNull();
});
it("does not read the cache under force", async () => {
const hit = await readFreshStatusText({
userId: "u1",
cacheAction: "insights.weight-status.en",
todayKey: TODAY,
force: true,
});
expect(hit).toBeNull();
expect(prisma.auditLog.findFirst).not.toHaveBeenCalled();
});
it("treats a malformed payload as a miss", async () => {
vi.mocked(prisma.auditLog.findFirst).mockResolvedValue({
createdAt: new Date(),
details: "{not json",
} as never);
const hit = await readFreshStatusText({
userId: "u1",
cacheAction: "insights.weight-status.en",
todayKey: TODAY,
force: false,
});
expect(hit).toBeNull();
});
});
describe("resolveReadOnlyStatusMiss", () => {
beforeEach(() => {
// Default: no prior assessment to serve stale (readLastGoodStatusText).
vi.mocked(prisma.auditLog.findMany).mockResolvedValue([] as never);
});
it("returns no-provider without enqueuing when the user has no provider", async () => {
hasUsableStatusProvider.mockResolvedValue(false);
const outcome = await resolveReadOnlyStatusMiss({
userId: "u1",
metric: "weight",
locale: "en",
});
expect(outcome.kind).toBe("no-provider");
expect(enqueueStatusGeneration).not.toHaveBeenCalled();
});
it("enqueues generation and returns preparing on a clean miss", async () => {
hasUsableStatusProvider.mockResolvedValue(true);
// No negative stub present.
vi.mocked(prisma.auditLog.findFirst).mockResolvedValue(null as never);
const outcome = await resolveReadOnlyStatusMiss({
userId: "u1",
metric: "pulse",
locale: "de",
});
expect(outcome.kind).toBe("preparing");
// No last-good text to show, so nothing to revalidate against — the card
// polls on `preparing` alone.
expect(outcome).toEqual({
kind: "preparing",
lastGood: null,
revalidating: false,
});
expect(enqueueStatusGeneration).toHaveBeenCalledWith({
userId: "u1",
metric: "pulse",
locale: "de",
});
});
it("serves the last good assessment stale-while-revalidate on a clean miss", async () => {
hasUsableStatusProvider.mockResolvedValue(true);
vi.mocked(prisma.auditLog.findFirst).mockResolvedValue(null as never);
// A prior (e.g. yesterday's) real assessment is on record.
vi.mocked(prisma.auditLog.findMany).mockResolvedValue([
cacheRow(
{ dateKey: "2026-05-30", text: "Steady upward trend.", model: "gpt-4o-mini" },
new Date("2026-05-30T04:30:00.000Z"),
),
] as never);
const outcome = await resolveReadOnlyStatusMiss({
userId: "u1",
metric: "weight",
locale: "en",
});
expect(outcome.kind).toBe("preparing");
if (outcome.kind !== "preparing") throw new Error("expected preparing");
expect(outcome.lastGood?.text).toBe("Steady upward trend.");
// v1.9.0 — stale text served AND a refresh enqueued → revalidating so the
// open card keeps polling until the fresh assessment lands.
expect(outcome.revalidating).toBe(true);
// A refresh is still enqueued behind the stale serve.
expect(enqueueStatusGeneration).toHaveBeenCalledTimes(1);
});
it("suppresses re-enqueue while a fresh timeout stub exists", async () => {
hasUsableStatusProvider.mockResolvedValue(true);
vi.mocked(prisma.auditLog.findFirst).mockResolvedValue(
cacheRow({
dateKey: TODAY,
timeout: true,
model: "timeout-stub",
retryAt: new Date(Date.now() + 60_000).toISOString(),
}) as never,
);
const outcome = await resolveReadOnlyStatusMiss({
userId: "u1",
metric: "mood",
locale: "en",
});
expect(outcome.kind).toBe("preparing");
if (outcome.kind !== "preparing") throw new Error("expected preparing");
// No enqueue on the suppressed branch → nothing in flight to revalidate.
expect(outcome.revalidating).toBe(false);
expect(enqueueStatusGeneration).not.toHaveBeenCalled();
});
it("re-enqueues once the negative stub's retryAt has passed", async () => {
hasUsableStatusProvider.mockResolvedValue(true);
vi.mocked(prisma.auditLog.findFirst).mockResolvedValue(
cacheRow({
dateKey: TODAY,
timeout: true,
model: "timeout-stub",
retryAt: new Date(Date.now() - 60_000).toISOString(),
}) as never,
);
const outcome = await resolveReadOnlyStatusMiss({
userId: "u1",
metric: "bmi",
locale: "de",
});
expect(outcome.kind).toBe("preparing");
expect(enqueueStatusGeneration).toHaveBeenCalledTimes(1);
});
});
describe("refreshUnchangedStatusInsight (v1.16.8)", () => {
const HASH = "a".repeat(64);
it("re-persists the row under today's dateKey and returns the text on a hash match", async () => {
vi.mocked(prisma.auditLog.findFirst).mockResolvedValue(
cacheRow({
dateKey: "2026-05-30",
locale: "en",
text: "Stable weight, no concerns.",
providerType: "openai",
model: "gpt-4o-mini",
tokensUsed: 321,
snapshotHash: HASH,
}) as never,
);
vi.mocked(prisma.auditLog.create).mockResolvedValue({
createdAt: new Date("2026-05-31T04:30:00.000Z"),
} as never);
const hit = await refreshUnchangedStatusInsight({
userId: "u1",
cacheAction: "insights.weight-status.en",
todayKey: TODAY,
snapshotHash: HASH,
});
expect(hit?.text).toBe("Stable weight, no concerns.");
expect(hit?.updatedAt).toBe("2026-05-31T04:30:00.000Z");
// The refresh row carries the same payload re-keyed to today, so the
// read path and the ingest debounce both see a current assessment.
expect(prisma.auditLog.create).toHaveBeenCalledTimes(1);
const arg = vi.mocked(prisma.auditLog.create).mock.calls[0][0] as {
data: { userId: string; action: string; details: string };
};
expect(arg.data.userId).toBe("u1");
expect(arg.data.action).toBe("insights.weight-status.en");
const details = JSON.parse(arg.data.details) as Record<string, unknown>;
expect(details.dateKey).toBe(TODAY);
expect(details.text).toBe("Stable weight, no concerns.");
expect(details.snapshotHash).toBe(HASH);
});
it("misses (and writes nothing) when the stored hash differs", async () => {
vi.mocked(prisma.auditLog.findFirst).mockResolvedValue(
cacheRow({
dateKey: "2026-05-30",
text: "Older text.",
model: "gpt-4o-mini",
snapshotHash: "b".repeat(64),
}) as never,
);
const hit = await refreshUnchangedStatusInsight({
userId: "u1",
cacheAction: "insights.weight-status.en",
todayKey: TODAY,
snapshotHash: HASH,
});
expect(hit).toBeNull();
expect(prisma.auditLog.create).not.toHaveBeenCalled();
});
it("misses when the latest row carries no hash (pre-gate rows)", async () => {
vi.mocked(prisma.auditLog.findFirst).mockResolvedValue(
cacheRow({
dateKey: "2026-05-30",
text: "Older text.",
model: "gpt-4o-mini",
}) as never,
);
const hit = await refreshUnchangedStatusInsight({
userId: "u1",
cacheAction: "insights.weight-status.en",
todayKey: TODAY,
snapshotHash: HASH,
});
expect(hit).toBeNull();
expect(prisma.auditLog.create).not.toHaveBeenCalled();
});
it("never refreshes off a timeout stub, even with a matching hash", async () => {
vi.mocked(prisma.auditLog.findFirst).mockResolvedValue(
cacheRow({
dateKey: "2026-05-30",
text: "Generic fallback advice.",
model: "timeout-stub",
timeout: true,
snapshotHash: HASH,
}) as never,
);
const hit = await refreshUnchangedStatusInsight({
userId: "u1",
cacheAction: "insights.weight-status.en",
todayKey: TODAY,
snapshotHash: HASH,
});
expect(hit).toBeNull();
expect(prisma.auditLog.create).not.toHaveBeenCalled();
});
it("misses on no prior row and on malformed payloads", async () => {
vi.mocked(prisma.auditLog.findFirst).mockResolvedValueOnce(null as never);
expect(
await refreshUnchangedStatusInsight({
userId: "u1",
cacheAction: "insights.weight-status.en",
todayKey: TODAY,
snapshotHash: HASH,
}),
).toBeNull();
vi.mocked(prisma.auditLog.findFirst).mockResolvedValueOnce({
details: "{not json",
} as never);
expect(
await refreshUnchangedStatusInsight({
userId: "u1",
cacheAction: "insights.weight-status.en",
todayKey: TODAY,
snapshotHash: HASH,
}),
).toBeNull();
expect(prisma.auditLog.create).not.toHaveBeenCalled();
});
});