Files
HealthLog/src/lib/insights/__tests__/pulse-status-timeout.test.ts
T
Marc-André Bombeck 2d111fcb24 chore(release): v1.4.28
Bug-fix and consistency follow-through after the v1.4.27 mobile sweep.
The headline is a tighter dashboard and insights surface: six widgets
retire from code entirely (GLP-1 tile, dashboard `<DrugLevelChart>`
mount, GLP-1 detail-page intake history + inventory, the
`<InsightAdvisorCard>` block and its regeneration affordance, the
weekly-report route), four broken edges close (workout-edit duplicate-
timestamp 409, BD-Zielbereich tile rebuilt on the shared
`<TrendCard>` primitive, `/insights/puls` chart timeout fallback,
sticky tab-strip scroll lock), and one consistency contract lands per
maintainer directive: every medication-list row, every medication-
detail section header, every trend tile, every Coach launch glyph,
every chart-height contract reads on one shape. The HealthScore card
grows to fill its column, gains an accessible `?` tooltip explaining
the delta, and drops the placeholder "Wochenbericht erstellen"
button. iOS contracts are intact; the only `/api/*` evolution is
additive (the `aggregate=monthly` grain on `/api/measurements` and an
internal-only `/api/internal/web-vitals` beacon route).
2026-05-16 03:17:00 +02:00

78 lines
2.5 KiB
TypeScript

import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
/**
* v1.4.28 R3a FB-D2 — provider timeout fallback for the pulse-status
* route. When the upstream `generateCompletion` call does not resolve
* inside the 20-second budget the route returns a deterministic
* cached-style envelope so the InsightStatusCard renders the
* fallback text instead of spinning behind React-Query's default
* retries.
*/
vi.mock("@/lib/db", () => ({
prisma: {
user: { findUnique: vi.fn() },
auditLog: { findFirst: vi.fn(), create: vi.fn() },
measurement: { findMany: vi.fn() },
moodEntry: { findMany: vi.fn() },
},
}));
vi.mock("@/lib/ai/provider", () => ({
resolveProvider: vi.fn(),
}));
vi.mock("@/lib/insights/memory", () => ({
getPreviousInsightContext: vi.fn().mockResolvedValue(null),
formatPreviousContextForPrompt: vi.fn().mockReturnValue(""),
}));
import { prisma } from "@/lib/db";
import { resolveProvider } from "@/lib/ai/provider";
import { generatePulseStatusForUser } from "../pulse-status";
beforeEach(() => {
vi.resetAllMocks();
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
describe("generatePulseStatusForUser — provider timeout fallback", () => {
it("returns a cached-style envelope when the provider exceeds 20 s", async () => {
vi.mocked(prisma.user.findUnique).mockResolvedValue({
dateOfBirth: null,
gender: null,
} as never);
vi.mocked(prisma.auditLog.findFirst).mockResolvedValue(null);
vi.mocked(prisma.measurement.findMany).mockResolvedValue([
{ value: 72, measuredAt: new Date() },
] as never);
vi.mocked(prisma.moodEntry.findMany).mockResolvedValue([] as never);
// Provider stalls indefinitely.
vi.mocked(resolveProvider).mockResolvedValue({
type: "anthropic",
generateCompletion: vi.fn(
() => new Promise<{ content: string }>(() => {}),
),
} as never);
const promise = generatePulseStatusForUser("user-1", { locale: "en" });
// Advance past the 20-second budget so the timeout race wins.
await vi.advanceTimersByTimeAsync(21_000);
const result = await promise;
expect(result.hasProvider).toBe(true);
expect(result.cached).toBe(true);
expect(typeof result.text).toBe("string");
expect(result.text?.length ?? 0).toBeGreaterThan(0);
// The route must NOT persist a fallback into the audit cache —
// a transient provider stall should not poison tomorrow's cache
// hit.
expect(prisma.auditLog.create).not.toHaveBeenCalled();
});
});