Files
HealthLog/src/app/api/feedback/__tests__/route.test.ts
T
Marc-André BombeckandClaude Opus 4.7 1adda80944 feat(insights): widen per-card window from 30 days to 360+24 buckets
Every per-card KI insight generator was clipping each metric to the
last 30 daily means before sending the prompt. The model therefore
saw a one-month window per metric — far too narrow to detect drift
that plays out over a year, and the cause of repeated complaints that
a "Stable" trend assessment ignored an obvious 9-month decline.

New shared helper `applyPayloadBudget()` builds a `{ daily, monthly }`
view: the most recent 360 days as daily means (`dayOffset 0..359`)
plus 24 thirty-day windows for the older history (`monthOffset
12..35`). Empty buckets are dropped. If the canonical payload would
serialise above 50 KB the helper transparently re-buckets at 180
daily days so the prompt token budget stays bounded; a per-generator
`annotate({ payload_size_bytes })` makes the chosen window
observable in Wide Events.

All seven generators now consume the bucketed view as the metric
`series` field (general-status, blood-pressure-status, pulse-status,
weight-status, bmi-status, mood-status, medication-compliance-
status) — including their cross-metric joins (BP-pair, weight ↔ mean
BP correlation, medication-compliance per day) which are rebuilt on
top of the shared `dayOffset` instead of date strings. Mood and
medication queries widened to ~3 years so the older monthly window
can actually be filled.

One focused vitest per generator captures the prompt that goes to the
provider, JSON-parses the embedded snapshot, and asserts every
metric series carries both `daily` and `monthly` arrays with the
expected shape. The bucketing helper also has its own unit tests
covering offsets, leap-year-safe Berlin-day boundaries, and the
50 KB → 180-day fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 01:50:10 +02:00

106 lines
2.9 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
vi.mock("@/lib/api-handler", () => ({
apiHandler: <T extends (...args: unknown[]) => unknown>(fn: T) => fn,
requireAuth: vi.fn(async () => ({
user: { id: "u-1", email: "test@example.com", role: "USER" },
session: { id: "s-1" },
})),
}));
vi.mock("@/lib/db", () => ({
prisma: {
appSettings: { findUnique: vi.fn() },
feedback: { create: vi.fn() },
},
}));
vi.mock("@/lib/rate-limit", () => ({
checkRateLimit: vi.fn(async () => ({ allowed: true })),
}));
vi.mock("@/lib/logging/context", () => ({
annotate: vi.fn(),
}));
vi.mock("@/lib/auth/audit", () => ({
auditLog: vi.fn(async () => undefined),
}));
import { POST } from "../route";
import { prisma } from "@/lib/db";
beforeEach(() => {
vi.resetAllMocks();
});
function jsonRequest(body: unknown): Request {
return new Request("http://localhost/api/feedback", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
}
describe("POST /api/feedback bug-report toggle gate", () => {
it("returns 503 when bugReportEnabled is false", async () => {
vi.mocked(prisma.appSettings.findUnique).mockResolvedValue({
bugReportEnabled: false,
} as never);
const res = await POST(
jsonRequest({
category: "BUG",
subject: "test",
description: "Anything at least ten characters long.",
}) as never,
);
expect(res.status).toBe(503);
const body = (await res.json()) as { error: string };
expect(body.error).toMatch(/disabled/i);
expect(prisma.feedback.create).not.toHaveBeenCalled();
});
it("creates feedback when bugReportEnabled is true", async () => {
vi.mocked(prisma.appSettings.findUnique).mockResolvedValue({
bugReportEnabled: true,
} as never);
vi.mocked(prisma.feedback.create).mockResolvedValue({
id: "fb-1",
createdAt: new Date(),
category: "BUG",
status: "NEW",
} as never);
const res = await POST(
jsonRequest({
category: "BUG",
subject: "test",
description: "Anything at least ten characters long.",
}) as never,
);
expect(res.status).toBe(201);
expect(prisma.feedback.create).toHaveBeenCalledTimes(1);
});
it("creates feedback when bugReportEnabled has never been written (default ON)", async () => {
vi.mocked(prisma.appSettings.findUnique).mockResolvedValue(null);
vi.mocked(prisma.feedback.create).mockResolvedValue({
id: "fb-2",
createdAt: new Date(),
category: "BUG",
status: "NEW",
} as never);
const res = await POST(
jsonRequest({
category: "BUG",
subject: "test",
description: "Anything at least ten characters long.",
}) as never,
);
expect(res.status).toBe(201);
expect(prisma.feedback.create).toHaveBeenCalledTimes(1);
});
});