test(anamnesis): drive the life circumstances, AUDIT-C and its backup through real Postgres and the manage matrix

This commit is contained in:
Marc-André Bombeck committed 2026-10-16 01:34:17 +02:00
1 parent 88aa5ccacd
commit 4d2c8c2bbb
8 files changed
+718 -16

No files matched your search

+2 -2
View File
@@ -16,7 +16,7 @@ import {
correctLifeFact,
removeHealthProfileFact,
} from "@/lib/profile/health-facts";
import { dayStartUtc } from "@/lib/profile/life-derived";
import { dateOnlyAtNoonUtc } from "@/lib/tz/date-only";
import {
validateCorrectedFactValue,
type HealthProfileFactKind,
@@ -63,7 +63,7 @@ export const PATCH = apiHandler(
// circumstances (v1.43, #614).
const now = new Date();
const validFrom = parsed.data.validFrom
? dayStartUtc(parsed.data.validFrom)
? dateOnlyAtNoonUtc(parsed.data.validFrom)
: undefined;
let corrected: { id: string; kind: string } | null;
if (isLifeFactKind(current.kind)) {
+2 -2
View File
@@ -17,7 +17,7 @@ import {
createLifeFact,
readHealthProfileFacts,
} from "@/lib/profile/health-facts";
import { dayStartUtc } from "@/lib/profile/life-derived";
import { dateOnlyAtNoonUtc } from "@/lib/tz/date-only";
import { factWriteSchema } from "@/lib/validations/life-circumstances";
/**
@@ -51,7 +51,7 @@ async function postFact(request: NextRequest): Promise<Response> {
const now = new Date();
const validFrom = parsed.data.validFrom
? dayStartUtc(parsed.data.validFrom)
? dateOnlyAtNoonUtc(parsed.data.validFrom)
: undefined;
try {
const input = parsed.data;
+7 -3
View File
@@ -27,6 +27,7 @@
* Server-only — reads `@/lib/db`.
*/
import { prisma } from "@/lib/db";
import { userDayKey } from "@/lib/tz/format";
import { getEvent } from "@/lib/logging/context";
import { decryptFromBytes, encryptToBytes } from "@/lib/ai/coach/bytes-codec";
import {
@@ -508,6 +509,8 @@ export interface LifeFactForPrompt {
export interface AuditCForPrompt {
totalScore: number;
takenAt: Date;
/** The zone the entry was taken in, for its calendar day. */
tz: string;
cutoff: number | null;
aboveCutoff: boolean | null;
}
@@ -624,7 +627,7 @@ export function composeLifeCircumstancesText(
lines.push(
`${L.auditC}: ${fill(L.auditCValue, {
score: auditC.totalScore,
date: auditC.takenAt.toISOString().slice(0, 10),
date: userDayKey(auditC.takenAt, auditC.tz),
})}${verdict}`,
);
}
@@ -645,12 +648,12 @@ async function loadAuditCForPrompt(
const audit = await prisma.mentalHealthAssessment.findFirst({
where: { userId, instrument: "AUDIT_C", deletedAt: null },
orderBy: [{ takenAt: "desc" }, { id: "desc" }],
select: { totalScore: true, takenAt: true, locale: true },
select: { totalScore: true, takenAt: true, locale: true, tz: true },
});
if (!audit) return null;
const user = await prisma.user.findUnique({
where: { id: userId },
select: { gender: true, locale: true },
select: { gender: true, locale: true, timezone: true },
});
const cutoff = auditCCutoff(
audit.locale,
@@ -660,6 +663,7 @@ async function loadAuditCForPrompt(
return {
totalScore: audit.totalScore,
takenAt: audit.takenAt,
tz: audit.tz ?? user?.timezone ?? "UTC",
cutoff: cutoff?.value ?? null,
aboveCutoff: cutoff ? audit.totalScore >= cutoff.value : null,
};
-5
View File
@@ -62,11 +62,6 @@ export const LIFE_KIND_AI_SECTION: Readonly<
export const REVIEW_AFTER_DAYS = 365;
const MS_PER_DAY = 86_400_000;
/** UTC midnight of a `YYYY-MM-DD` calendar day. */
export function dayStartUtc(day: string): Date {
return new Date(`${day}T00:00:00.000Z`);
}
/** Whether an instant is at least `REVIEW_AFTER_DAYS` before `now`. */
export function isReviewDue(at: Date | null, now: Date): boolean {
return (
+1 -1
View File
@@ -249,7 +249,7 @@ const validFromDay = z.iso
.date()
.optional()
.describe(
"The calendar day the answer holds from (UTC midnight). Omitted, it holds from now. Never later than today; an earlier day than the end of the previous answer of the same kind starts at that end instead.",
"The calendar day the answer holds from, stored at noon UTC like every date-only value. Omitted, it holds from now. Never later than today; an earlier day than the end of the previous answer of the same kind starts at that end instead.",
);
/**
@@ -8,6 +8,7 @@ process.env.ENCRYPTION_KEY =
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
import { buildFullBackupPayload } from "@/lib/export/full-backup-payload";
import { restoreProfileData } from "@/lib/export/profile-backup";
import { restoreSensitiveData } from "@/lib/export/sensitive-backup";
import { parseBackupPayload } from "@/lib/validations/backup";
import { getPrismaClient, truncateAllTables } from "./setup";
@@ -172,4 +173,130 @@ describe("health profile disaster-recovery round trip", () => {
await prisma.userHealthProfile.count({ where: { userId: user.id } }),
).toBe(0);
});
it("carries the life circumstances both ways: new kinds, new AI sections, AUDIT-C (v1.43)", async () => {
const prisma = getPrismaClient();
const user = await prisma.user.create({
data: {
username: "life-roundtrip",
email: "life-roundtrip@example.test",
role: "USER",
},
});
await prisma.userHealthProfile.create({
data: {
userId: user.id,
aiIncludedSections: ["ABOUT_ME", "HABITS", "GOALS"],
lifeConfirmedJson: { work: "2026-09-01T10:00:00.000Z" },
},
});
const goals = { schema: 1, goals: ["sleep", "fitness"] };
const tobacco = {
schema: 1,
products: ["cigarettes"],
cigarettesPerDay: 10,
startYear: 2001,
quitMonth: null,
secondhandSmoke: null,
};
await prisma.healthProfileFactRevision.createMany({
data: [
{
id: "goals-current",
userId: user.id,
kind: "HEALTH_GOALS",
valueEncrypted: encryptToBytes(JSON.stringify(goals)),
validFrom: new Date("2026-08-01T00:00:00.000Z"),
},
{
id: "tobacco-current",
userId: user.id,
kind: "TOBACCO_DETAIL",
valueEncrypted: encryptToBytes(JSON.stringify(tobacco)),
validFrom: new Date("2026-08-01T00:00:00.000Z"),
},
],
});
await prisma.mentalHealthAssessment.create({
data: {
id: "audit-c-entry",
userId: user.id,
instrument: "AUDIT_C",
locale: "de",
responsesEncrypted: encryptToBytes(
JSON.stringify({ schema: 1, items: [2, 1, 1] }),
),
totalScore: 4,
severityBand: "unbanded",
takenAt: new Date("2026-09-15T08:00:00.000Z"),
},
});
// Disaster recovery: everything, the AUDIT-C entry included.
const dr = parseBackupPayload(
JSON.parse(
JSON.stringify(
(
await buildFullBackupPayload(prisma, user.id, {
purpose: "disaster-recovery",
})
).payload,
),
),
);
expect(dr.healthProfile?.aiIncludedSections).toEqual([
"ABOUT_ME",
"HABITS",
"GOALS",
]);
expect(dr.healthProfileFacts.map((f) => f.kind).sort()).toEqual([
"HEALTH_GOALS",
"TOBACCO_DETAIL",
]);
expect(dr.mentalHealthAssessments.map((a) => a.instrument)).toEqual([
"AUDIT_C",
]);
await prisma.$transaction(async (tx) => {
await restoreProfileData(tx, user.id, dr);
await restoreSensitiveData(tx, user.id, dr);
});
const restored = await prisma.userHealthProfile.findUniqueOrThrow({
where: { userId: user.id },
});
expect(restored.aiIncludedSections).toEqual([
"ABOUT_ME",
"HABITS",
"GOALS",
]);
expect(restored.lifeConfirmedJson).toEqual({
work: "2026-09-01T10:00:00.000Z",
});
const audit = await prisma.mentalHealthAssessment.findUniqueOrThrow({
where: { id: "audit-c-entry" },
});
expect(audit.instrument).toBe("AUDIT_C");
expect(audit.totalScore).toBe(4);
// Portable: the values travel readable and come back sealed and valid.
const portable = parseBackupPayload(
JSON.parse(
JSON.stringify((await buildFullBackupPayload(prisma, user.id)).payload),
),
);
const goalsEntry = portable.healthProfileFacts.find(
(f) => f.kind === "HEALTH_GOALS",
);
expect(JSON.parse(goalsEntry!.value!)).toEqual(goals);
await prisma.$transaction((tx) =>
restoreProfileData(tx, user.id, portable),
);
const { decryptLifeFactValue } = await import("@/lib/profile/health-facts");
const row = await prisma.healthProfileFactRevision.findFirstOrThrow({
where: { userId: user.id, kind: "TOBACCO_DETAIL" },
});
expect(
decryptLifeFactValue("TOBACCO_DETAIL", row.valueEncrypted).value,
).toEqual(tobacco);
});
});
@@ -0,0 +1,523 @@
/**
* v1.43 (#614) — the life circumstances through the real routes against real
* Postgres: every new fact kind written, corrected and removed and read back
* through `GET /api/anamnesis/life-circumstances`; the closed legacy lists
* (`GET /api/anamnesis/facts`, `GET /api/mental-health/assessments`) never
* showing a new kind or AUDIT-C; the AUDIT-C cut-off from the entry's
* language and the profile's sex; the AI switches leaving the about-me wire
* at its eight values; and the sealed values never sitting in the clear.
*/
import { Buffer } from "node:buffer";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { cookieJar, headerJar } from "./mock-next-headers";
import { getPrismaClient, truncateAllTables } from "./setup";
vi.mock("next/headers", async () => {
const { cookieJar, headerJar } = await import("./mock-next-headers");
return {
headers: vi.fn(async () => ({
get: (name: string) => headerJar.get(name.toLowerCase()) ?? null,
})),
cookies: vi.fn(async () => ({
get: (name: string) => {
const value = cookieJar.get(name);
return value ? { name, value } : undefined;
},
set: (name: string, value: string) => {
cookieJar.set(name, value);
},
delete: (name: string) => {
cookieJar.delete(name);
},
})),
};
});
vi.mock("@/lib/db-compat", () => ({
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
}));
type Handler = (request: Request, ctx?: unknown) => Promise<Response>;
let counter = 0;
async function makeUser(overrides: Record<string, unknown> = {}) {
const suffix = `${counter++}`;
return getPrismaClient().user.create({
data: {
username: `life-${suffix}`,
email: `life-${suffix}@example.test`,
role: "USER",
timezone: "UTC",
locale: "en",
onboardingCompletedAt: new Date(),
modulePreferencesJson: { mentalHealth: true },
...overrides,
},
});
}
async function signIn(userId: string) {
const session = await getPrismaClient().session.create({
data: { userId, expiresAt: new Date(Date.now() + 60_000) },
});
cookieJar.set("healthlog_session", session.id);
}
function request(method: string, path: string, body?: unknown): Request {
return new Request(`http://localhost${path}`, {
method,
headers: { "content-type": "application/json" },
...(body === undefined ? {} : { body: JSON.stringify(body) }),
});
}
async function data<T>(response: Response, status = 200): Promise<T> {
const text = await response.text();
expect(response.status, text).toBe(status);
return (JSON.parse(text) as { data: T }).data;
}
async function postFact(body: unknown): Promise<Response> {
const { POST } = await import("@/app/api/anamnesis/facts/route");
return (POST as unknown as Handler)(
request("POST", "/api/anamnesis/facts", body),
);
}
async function patchFact(id: string, body: unknown): Promise<Response> {
const { PATCH } = await import("@/app/api/anamnesis/facts/[id]/route");
return (PATCH as unknown as Handler)(
request("PATCH", `/api/anamnesis/facts/${id}`, body),
{ params: Promise.resolve({ id }) },
);
}
async function deleteFact(id: string): Promise<Response> {
const { DELETE } = await import("@/app/api/anamnesis/facts/[id]/route");
return (DELETE as unknown as Handler)(
request("DELETE", `/api/anamnesis/facts/${id}`),
{ params: Promise.resolve({ id }) },
);
}
interface Life {
sections: Array<{
key: string;
aiIncluded: boolean;
lastConfirmedAt: string | null;
facts: Array<{ id: string; kind: string; value: unknown }>;
}>;
derived: {
packYears: { value: number | null; status: string };
activityMinutesPerWeek: {
value: number | null;
meetsWho150: boolean | null;
};
freeWeekdays: { days: number[]; source: string };
};
auditC: {
latest: {
id: string;
totalScore: number;
cutoff: { value: number; source: string } | null;
aboveCutoff: boolean | null;
} | null;
history: Array<{ id: string }>;
offerDue: boolean;
instrument: { locale: string; validatedLocale: boolean };
};
}
async function readLife(): Promise<Life> {
const { GET } = await import("@/app/api/anamnesis/life-circumstances/route");
return data<Life>(await (GET as unknown as () => Promise<Response>)());
}
function factOf(life: Life, kind: string) {
return life.sections.flatMap((s) => s.facts).find((f) => f.kind === kind);
}
const VALUES = {
TOBACCO_DETAIL: {
schema: 1,
products: ["cigarettes"],
cigarettesPerDay: 20,
startYear: 1990,
quitMonth: "2020-06",
secondhandSmoke: false,
},
ACTIVITY_HABIT: {
schema: 1,
moderateDaysPerWeek: 3,
moderateMinutesPerDay: 40,
sittingHoursWorkday: 7.5,
},
WORK_CONTEXT: {
schema: 1,
situation: "outdoor",
physicalNature: "heavy_physical",
nightWork: null,
exposures: ["heat", "dust"],
},
FREE_WEEKDAYS: { schema: 1, days: [2, 3] },
HABITS: {
schema: 1,
napRegularly: true,
snoringReported: null,
dietPattern: "vegetarian",
dietSpecifics: ["low_carb"],
lateMealsPerWeek: 2,
fruitVegPortionsPerDay: 4,
caffeineDrinksPerDay: 3,
latestCaffeine: "15_18",
},
HEALTH_GOALS: { schema: 1, goals: ["sleep", "fitness"] },
} as const;
beforeEach(async () => {
await truncateAllTables(getPrismaClient());
cookieJar.clear();
headerJar.clear();
counter = 0;
});
describe("life-circumstance facts", () => {
it("writes, corrects and removes every new kind, and the closed list never shows one", async () => {
const user = await makeUser();
await signIn(user.id);
await data(
await postFact({ kind: "SMOKING_STATUS", value: "FORMER" }),
201,
);
for (const [kind, value] of Object.entries(VALUES)) {
await data(await postFact({ kind, value }), 201);
}
let life = await readLife();
for (const [kind, value] of Object.entries(VALUES)) {
expect(factOf(life, kind)?.value, kind).toEqual(value);
}
expect(life.sections.map((s) => s.key)).toEqual([
"tobacco",
"alcohol",
"activity",
"work",
"habits",
"goals",
]);
expect(life.derived.packYears).toEqual({ value: 30, status: "computed" });
expect(life.derived.activityMinutesPerWeek).toMatchObject({
value: 120,
meetsWho150: false,
});
expect(life.derived.freeWeekdays).toEqual({ days: [2, 3], source: "fact" });
// A second current answer of the same kind is the existing 409.
expect(
(await postFact({ kind: "HABITS", value: VALUES.HABITS })).status,
).toBe(409);
// Correct: a dated successor with the new value.
const goals = factOf(life, "HEALTH_GOALS")!;
const corrected = await data<{ id: string; value: unknown }>(
await patchFact(goals.id, {
value: { schema: 1, goals: ["quit_smoking"] },
validFrom: "2026-01-01",
}),
);
expect(corrected.value).toEqual({ schema: 1, goals: ["quit_smoking"] });
// A string for an object kind is the existing 422.
expect((await patchFact(corrected.id, { value: "SLEEP" })).status).toBe(
422,
);
// Remove: the interval closes; the kind reads as not recorded.
const habits = factOf(life, "HABITS")!;
await data(await deleteFact(habits.id));
life = await readLife();
expect(factOf(life, "HABITS")).toBeUndefined();
expect(factOf(life, "HEALTH_GOALS")?.value).toEqual({
schema: 1,
goals: ["quit_smoking"],
});
// The closed list: three kinds, nothing else, in `current` and `history`.
const { GET } = await import("@/app/api/anamnesis/facts/route");
const legacy = await data<{
current: Record<string, unknown>;
history: Array<{ kind: string }>;
}>(await (GET as unknown as () => Promise<Response>)());
expect(Object.keys(legacy.current).sort()).toEqual([
"ALCOHOL_PATTERN",
"SHIFT_SCHEDULE",
"SMOKING_STATUS",
]);
expect(
legacy.history.every((row) =>
["SMOKING_STATUS", "ALCOHOL_PATTERN", "SHIFT_SCHEDULE"].includes(
row.kind,
),
),
).toBe(true);
expect(legacy.history).toHaveLength(1);
});
it("seals the values: no answer sits in the clear", async () => {
const user = await makeUser();
await signIn(user.id);
await data(
await postFact({ kind: "WORK_CONTEXT", value: VALUES.WORK_CONTEXT }),
201,
);
const row =
await getPrismaClient().healthProfileFactRevision.findFirstOrThrow({
where: { userId: user.id, kind: "WORK_CONTEXT" },
});
const raw = Buffer.from(row.valueEncrypted).toString("latin1");
expect(raw).not.toContain("outdoor");
expect(raw).not.toContain("heavy_physical");
});
it("dates an answer back to the month given", async () => {
const user = await makeUser();
await signIn(user.id);
const created = await data<{ validFrom: string }>(
await postFact({
kind: "FREE_WEEKDAYS",
value: VALUES.FREE_WEEKDAYS,
validFrom: "2025-04-01",
}),
201,
);
expect(created.validFrom).toBe("2025-04-01T12:00:00.000Z");
});
});
describe("AUDIT-C", () => {
async function postAuditC(body: unknown): Promise<Response> {
const { POST } = await import("@/app/api/anamnesis/audit-c/route");
return (POST as unknown as Handler)(
request("POST", "/api/anamnesis/audit-c", body),
);
}
it("scores a German entry of a woman with sum 4 at the S3 cut-off, and none without a sex", async () => {
const woman = await makeUser({ locale: "de", gender: "FEMALE" });
await signIn(woman.id);
const { result } = await data<{
result: {
totalScore: number;
aboveCutoff: boolean | null;
cutoff: { value: number; source: string; sourceLabel: string } | null;
validatedLocale: boolean;
};
}>(
await postAuditC({ items: [2, 1, 1], locale: "de", source: "WEB" }),
201,
);
expect(result.totalScore).toBe(4);
expect(result.aboveCutoff).toBe(true);
expect(result.cutoff).toMatchObject({ value: 4, source: "S3_ALCOHOL_DE" });
expect(result.validatedLocale).toBe(true);
const nobody = await makeUser({ locale: "de", gender: null });
await signIn(nobody.id);
const unsexed = await data<{
result: { cutoff: unknown; aboveCutoff: unknown };
}>(
await postAuditC({ items: [2, 1, 1], locale: "de", source: "WEB" }),
201,
);
expect(unsexed.result.cutoff).toBeNull();
expect(unsexed.result.aboveCutoff).toBeNull();
});
it("replays one externalId to the same entry and never lists it among the screeners", async () => {
const user = await makeUser({ locale: "fr", gender: "MALE" });
await signIn(user.id);
const body = {
items: [1, 0, 0],
source: "IOS",
externalId: "outbox-audit-1",
};
const first = await data<{ result: { id: string } }>(
await postAuditC(body),
201,
);
const again = await data<{ result: { id: string } }>(
await postAuditC(body),
200,
);
expect(again.result.id).toBe(first.result.id);
// A screener with the same client id is its own record, not this one.
const { POST: postScreener, GET: listScreeners } =
await import("@/app/api/mental-health/assessments/route");
const screener = await (postScreener as unknown as Handler)(
request("POST", "/api/mental-health/assessments", {
instrument: "WHO5",
items: [3, 3, 3, 3, 3],
source: "IOS",
externalId: "outbox-audit-1",
}),
);
expect(screener.status).toBe(201);
const { NextRequest } = await import("next/server");
const list = await data<{ assessments: Array<{ instrument: string }> }>(
await (listScreeners as unknown as (r: Request) => Promise<Response>)(
new NextRequest("http://localhost/api/mental-health/assessments"),
),
);
expect(list.assessments.map((a) => a.instrument)).toEqual(["WHO5"]);
// The detail route answers 404 for an AUDIT-C id.
const { GET: detail } =
await import("@/app/api/mental-health/assessments/[id]/route");
const byId = await (detail as unknown as Handler)(
new NextRequest(
`http://localhost/api/mental-health/assessments/${first.result.id}`,
),
{ params: Promise.resolve({ id: first.result.id }) },
);
expect(byId.status).toBe(404);
// The read route carries it, with the English items for French.
const life = await readLife();
expect(life.auditC.latest?.id).toBe(first.result.id);
expect(life.auditC.offerDue).toBe(false);
expect(life.auditC.instrument).toMatchObject({
locale: "en",
validatedLocale: false,
});
});
it("soft-deletes an entry once, and only an AUDIT-C entry of the record", async () => {
const user = await makeUser();
await signIn(user.id);
const { result } = await data<{ result: { id: string } }>(
await postAuditC({ items: [0, 0, 0], source: "WEB" }),
201,
);
const { DELETE } = await import("@/app/api/anamnesis/audit-c/[id]/route");
const remove = (id: string) =>
(DELETE as unknown as Handler)(
request("DELETE", `/api/anamnesis/audit-c/${id}`),
{ params: Promise.resolve({ id }) },
);
const removed = await data<{ id: string; removedAt: string }>(
await remove(result.id),
);
expect(removed.id).toBe(result.id);
const again = await remove(result.id);
expect(again.status).toBe(404);
expect(
((await again.json()) as { error: { code?: string }; meta?: unknown })
.error,
).toBeTruthy();
const row =
await getPrismaClient().mentalHealthAssessment.findUniqueOrThrow({
where: { id: result.id },
});
expect(row.deletedAt).not.toBeNull();
expect((await readLife()).auditC.latest).toBeNull();
});
});
describe("AI switches and confirmations", () => {
it("switches a life section without showing it on the about-me wire, and a PUT there keeps it", async () => {
const user = await makeUser({
modulePreferencesJson: { mentalHealth: true, coach: true },
});
await signIn(user.id);
const { PATCH } =
await import("@/app/api/anamnesis/life-circumstances/ai-sections/route");
const switched = await data<{ aiSections: Record<string, boolean> }>(
await (PATCH as unknown as Handler)(
request("PATCH", "/api/anamnesis/life-circumstances/ai-sections", {
sections: { HABITS: true, SMOKING_STATUS: false },
}),
),
);
expect(switched.aiSections).toMatchObject({
HABITS: true,
SMOKING_STATUS: false,
ACTIVITY: false,
});
const life = await readLife();
expect(life.sections.find((s) => s.key === "habits")?.aiIncluded).toBe(
true,
);
expect(life.sections.find((s) => s.key === "tobacco")?.aiIncluded).toBe(
false,
);
const aboutMe = await import("@/app/api/coach/about-me/route");
const read = await data<{ aiIncludedSections: string[] }>(
await (aboutMe.GET as unknown as () => Promise<Response>)(),
);
expect(read.aiIncludedSections).not.toContain("HABITS");
await data(
await (aboutMe.PUT as unknown as Handler)(
request("PUT", "/api/coach/about-me", {
aiIncludedSections: ["ABOUT_ME"],
}),
),
);
const stored = await getPrismaClient().userHealthProfile.findUniqueOrThrow({
where: { userId: user.id },
});
expect([...stored.aiIncludedSections].sort()).toEqual([
"ABOUT_ME",
"HABITS",
]);
});
it("answers a stale token with about_me_conflict", async () => {
const user = await makeUser();
await signIn(user.id);
await getPrismaClient().userHealthProfile.create({
data: { userId: user.id },
});
const { PATCH } =
await import("@/app/api/anamnesis/life-circumstances/ai-sections/route");
const response = await (PATCH as unknown as Handler)(
request("PATCH", "/api/anamnesis/life-circumstances/ai-sections", {
sections: { WORK: true },
baseUpdatedAt: "2020-01-01T00:00:00.000Z",
}),
);
expect(response.status).toBe(409);
expect(
((await response.json()) as { error: { errorCode?: string } }).error,
).toBeTruthy();
});
it("stores a confirmation as a stamp, never as a revision", async () => {
const user = await makeUser();
await signIn(user.id);
const { POST } =
await import("@/app/api/anamnesis/life-circumstances/confirm/route");
const confirmed = await data<{ section: string; lastConfirmedAt: string }>(
await (POST as unknown as Handler)(
request("POST", "/api/anamnesis/life-circumstances/confirm", {
section: "work",
}),
),
);
expect(confirmed.section).toBe("work");
const life = await readLife();
expect(life.sections.find((s) => s.key === "work")?.lastConfirmedAt).toBe(
confirmed.lastConfirmedAt,
);
expect(
await getPrismaClient().healthProfileFactRevision.count({
where: { userId: user.id },
}),
).toBe(0);
});
});
@@ -282,7 +282,7 @@ const ROUTE_IMPORTS = import.meta.glob<Record<string, unknown>>(
describe("the complete MANAGE handler matrix", () => {
it("starts with a non-empty, exact handler inventory", () => {
expect(ADMITTED_MUTATING_HANDLERS.length).toBe(89);
expect(ADMITTED_MUTATING_HANDLERS.length).toBe(92);
});
});
@@ -1169,6 +1169,59 @@ manageContract("POST /api/biomarkers/[id]/merge", {
})) === 1,
});
manageContract("POST /api/anamnesis/audit-c", {
contract: admittedContract("/api/anamnesis/audit-c", "POST"),
prepare: async (ownerId) => ({ ownerId }),
act: async () => {
const { POST } = await import("@/app/api/anamnesis/audit-c/route");
return call(POST as Handler, "POST", "/api/anamnesis/audit-c", {
items: [1, 1, 0],
source: "WEB",
});
},
ok: 201,
auditAction: "anamnesis.audit_c.create",
applied: async ({ ownerId }) =>
(await getPrismaClient().mentalHealthAssessment.count({
where: { userId: ownerId, instrument: "AUDIT_C", deletedAt: null },
})) === 1,
});
manageContract("DELETE /api/anamnesis/audit-c/[id]", {
contract: admittedContract("/api/anamnesis/audit-c/[id]", "DELETE"),
prepare: async (ownerId) =>
getPrismaClient().mentalHealthAssessment.create({
data: {
userId: ownerId,
instrument: "AUDIT_C",
locale: "en",
responsesEncrypted: Buffer.from("sealed"),
totalScore: 2,
severityBand: "unbanded",
takenAt: new Date("2026-09-01T08:00:00.000Z"),
},
select: { id: true },
}),
act: async (row) => {
const { DELETE } = await import("@/app/api/anamnesis/audit-c/[id]/route");
return call(
DELETE as Handler,
"DELETE",
`/api/anamnesis/audit-c/${row.id}`,
undefined,
{ id: row.id },
);
},
ok: 200,
auditAction: "anamnesis.audit_c.delete",
applied: async (row) =>
(
await getPrismaClient().mentalHealthAssessment.findUnique({
where: { id: row.id },
})
)?.deletedAt != null,
});
manageContract("POST /api/allergies", {
contract: ADMITTED_MUTATING_HANDLERS.find(
(entry) => entry.route === "/api/allergies" && entry.action === "POST",
@@ -2953,13 +3006,13 @@ describe("the conditions the admissions were granted on", () => {
describe("the complete MANAGE handler matrix", () => {
it("registers one strict actor-and-effect driver for every admission", () => {
const expected = ADMITTED_MUTATING_HANDLERS.map(matrixKey).sort();
expect(STRICT_DRIVER_KEYS.size).toBe(89);
expect(STRICT_DRIVER_KEYS.size).toBe(92);
expect([...STRICT_DRIVER_KEYS].sort()).toEqual(expected);
});
it("executes every registered driver through its owned effect and actor audit", () => {
const expected = ADMITTED_MUTATING_HANDLERS.map(matrixKey).sort();
expect(REAL_EFFECT_KEYS.size).toBe(89);
expect(REAL_EFFECT_KEYS.size).toBe(92);
expect([...REAL_EFFECT_KEYS].sort()).toEqual(expected);
});
});