Files
HealthLog/tests/integration/insights-status-real-text.test.ts
T
Marc-André Bombeck 1f4ba5939d style: apply prettier across the tree
One-shot prettier pass so the new format:check gate starts from a clean
baseline. Formatting only — no behaviour change. Generator-owned and
vendored artifacts stay untouched via .prettierignore.
2026-06-19 22:20:31 +02:00

141 lines
4.4 KiB
TypeScript

/**
* Wave 1D — the per-card status path delivers the REAL provider text.
*
* Pins two contracts against a real Postgres + a seeded rich account:
*
* 1. With a provider chain stubbed to return a known assessment, the
* status generator returns that text (the real path is reached, not
* the generic no-key fallback). This is the regression the
* sticky-stub bug used to break.
*
* 2. A timeout-stub cache row keyed to today is NOT served — the
* generator skips it and re-generates, proving the stub no longer
* pins the fallback for the day.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getPrismaClient, truncateAllTables } from "./setup";
vi.mock("@/lib/db-compat", () => ({
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
}));
// Stub the provider resolution so the chain runner has a deterministic
// provider to walk. `runRawCompletionWithFallback` runs for real on top
// of this — exercising the same plumbing the live status path uses.
const KNOWN_TEXT = "Your weight has trended down steadily over the past month.";
vi.mock("@/lib/ai/provider", () => ({
resolveProviderChain: vi.fn().mockResolvedValue([
{
providerType: "anthropic",
instance: {
type: "anthropic",
generateCompletion: vi.fn().mockResolvedValue({
content: JSON.stringify({ summary: KNOWN_TEXT }),
model: "test-model",
tokensUsed: 42,
}),
},
},
]),
resolveProvider: vi.fn().mockResolvedValue({ type: "none" }),
}));
import { toBerlinDayKey } from "@/lib/tz/resolver";
beforeEach(async () => {
await truncateAllTables(getPrismaClient());
});
afterEach(() => {
vi.restoreAllMocks();
});
async function seedRichWeightUser(username: string) {
const prisma = getPrismaClient();
const user = await prisma.user.create({
data: {
username,
email: `${username}@example.test`,
role: "USER",
heightCm: 180,
dateOfBirth: new Date("1985-01-01"),
},
});
const now = Date.now();
const DAY_MS = 24 * 60 * 60 * 1000;
const rows = Array.from({ length: 120 }, (_, i) => ({
userId: user.id,
type: "WEIGHT" as const,
value: 82 - i * 0.02,
unit: "kg",
source: "MANUAL" as const,
measuredAt: new Date(now - i * DAY_MS),
}));
await prisma.measurement.createMany({ data: rows });
return user;
}
describe("status path delivers real provider text", () => {
it("returns the provider's assessment, not the no-key fallback", async () => {
const user = await seedRichWeightUser("status-real-text-user");
const { generateWeightStatusForUser } =
await import("@/lib/insights/weight-status");
const result = await generateWeightStatusForUser(user.id, {
locale: "en",
});
expect(result.hasProvider).toBe(true);
expect(result.cached).toBe(false);
expect(result.text).toBe(KNOWN_TEXT);
// The real assessment was persisted (not a stub).
const prisma = getPrismaClient();
const cached = await prisma.auditLog.findFirst({
where: { userId: user.id, action: "insights.weight-status.en" },
orderBy: { createdAt: "desc" },
select: { details: true },
});
const parsed = JSON.parse(cached?.details ?? "{}") as {
text: string;
model: string;
timeout?: boolean;
};
expect(parsed.text).toBe(KNOWN_TEXT);
expect(parsed.model).toBe("test-model");
expect(parsed.timeout).toBeUndefined();
});
it("skips a timeout-stub cache row and regenerates the real text", async () => {
const user = await seedRichWeightUser("status-stub-skip-user");
const prisma = getPrismaClient();
// Pre-seed a stub row keyed to today — the bug used to serve this.
await prisma.auditLog.create({
data: {
userId: user.id,
action: "insights.weight-status.en",
details: JSON.stringify({
dateKey: toBerlinDayKey(new Date()),
locale: "en",
text: "Generic fallback advice that must not be served.",
model: "timeout-stub",
timeout: true,
}),
},
});
const { generateWeightStatusForUser } =
await import("@/lib/insights/weight-status");
const result = await generateWeightStatusForUser(user.id, {
locale: "en",
});
// The stub is skipped; the real provider text is returned + cached.
expect(result.text).toBe(KNOWN_TEXT);
expect(result.cached).toBe(false);
});
});