mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
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>
106 lines
2.9 KiB
TypeScript
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);
|
|
});
|
|
});
|