mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
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).
78 lines
2.5 KiB
TypeScript
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();
|
|
});
|
|
});
|