mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
Merge feat/w-d-mental-wellbeing into release/v1.27.6
The Vorsorge card resolves both rules at once: the action button keeps its constant look in every state (v1.27.5), and a screening reminder's action reads 'start the check-in' instead of the numeric measure wording (this wave).
This commit is contained in:
commit
f1e5df52ab
28 files changed
+657
-886
No files matched your search
@@ -37,6 +37,12 @@ vi.mock("@/lib/ai/coach/bytes-codec", () => ({
|
||||
encryptToBytes: vi.fn(() => Buffer.from("ciphertext")),
|
||||
}));
|
||||
|
||||
// v1.27.6 — a completed check-in kicks the eventful Vorsorge satisfy worker
|
||||
// so a plannable screening reminder resolves immediately.
|
||||
vi.mock("@/lib/jobs/reminder-satisfy", () => ({
|
||||
enqueueReminderSatisfy: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth/session", () => ({ getSession: vi.fn() }));
|
||||
vi.mock("@/lib/auth/audit", () => ({
|
||||
auditLog: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -55,6 +61,7 @@ vi.mock("next/headers", () => ({
|
||||
}));
|
||||
|
||||
import { POST } from "../route";
|
||||
import { enqueueReminderSatisfy } from "@/lib/jobs/reminder-satisfy";
|
||||
import { getSession } from "@/lib/auth/session";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
@@ -183,13 +190,27 @@ describe("POST /api/mental-health/assessments", () => {
|
||||
expect(createArg.data.item9Flagged).toBe(false);
|
||||
|
||||
// Derived projection: one PHQ9_SCORE/GAD7_SCORE Measurement per assessment.
|
||||
// Server-owned: the row carries the COMPUTED source (the RECOVERY_SCORE
|
||||
// precedent) — a client can never attribute COMPUTED on a write surface,
|
||||
// so the trend cannot be forged through the measurement POST.
|
||||
const measArg = vi.mocked(prisma.measurement.create).mock.calls[0][0] as {
|
||||
data: { type: string; value: number; unit: string; externalId: string };
|
||||
data: {
|
||||
type: string;
|
||||
value: number;
|
||||
unit: string;
|
||||
source: string;
|
||||
externalId: string;
|
||||
};
|
||||
};
|
||||
expect(measArg.data.type).toBe("GAD7_SCORE");
|
||||
expect(measArg.data.value).toBe(6);
|
||||
expect(measArg.data.unit).toBe("score");
|
||||
expect(measArg.data.source).toBe("COMPUTED");
|
||||
expect(measArg.data.externalId).toBe("assessment:mha_2");
|
||||
|
||||
// The eventful Vorsorge satisfy worker is kicked so a plannable
|
||||
// screening reminder (PHQ9_SCORE / GAD7_SCORE) auto-resolves at once.
|
||||
expect(enqueueReminderSatisfy).toHaveBeenCalledWith("user-1");
|
||||
});
|
||||
|
||||
it("dedups a repeat externalId: returns the existing administration, writes nothing new", async () => {
|
||||
@@ -229,6 +250,8 @@ describe("POST /api/mental-health/assessments", () => {
|
||||
);
|
||||
expect(prisma.mentalHealthAssessment.create).not.toHaveBeenCalled();
|
||||
expect(prisma.measurement.create).not.toHaveBeenCalled();
|
||||
// No new trend point → nothing to satisfy either.
|
||||
expect(enqueueReminderSatisfy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("persists client provenance + externalId on a first write", async () => {
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { withIdempotency } from "@/lib/idempotency";
|
||||
import { enqueueReminderSatisfy } from "@/lib/jobs/reminder-satisfy";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
|
||||
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
|
||||
@@ -308,9 +309,11 @@ async function postAssessment(request: NextRequest): Promise<Response> {
|
||||
// chart/rollup infra reads the trend without ever touching item content. The
|
||||
// externalId anchors the trend point to this administration; duplicate trend
|
||||
// points are structurally impossible because a replayed externalId returns
|
||||
// the existing assessment above before reaching this create. `MeasurementSource`
|
||||
// carries no WEB/IOS member (a screener is questionnaire input, not a device
|
||||
// sample), so the row stays MANUAL — the WEB/IOS provenance + client
|
||||
// the existing assessment above before reaching this create. The row is
|
||||
// `COMPUTED` — a server-derived projection of the encrypted answers, exactly
|
||||
// like RECOVERY_SCORE: clients cannot attribute the COMPUTED source on any
|
||||
// write surface, so a forged PHQ9_SCORE / GAD7_SCORE trend point can never
|
||||
// enter through the measurement POST. The WEB/IOS provenance + client
|
||||
// externalId live on the assessment this row links to.
|
||||
await prisma.measurement.create({
|
||||
data: {
|
||||
@@ -318,13 +321,19 @@ async function postAssessment(request: NextRequest): Promise<Response> {
|
||||
type: INSTRUMENT_MEASUREMENT_TYPE[id] as MeasurementType,
|
||||
value: total,
|
||||
unit: "score",
|
||||
source: "MANUAL" as MeasurementSource,
|
||||
source: "COMPUTED" as MeasurementSource,
|
||||
measuredAt: when,
|
||||
notes: null,
|
||||
externalId: `assessment:${assessment.id}`,
|
||||
},
|
||||
});
|
||||
|
||||
// v1.27.6 — a screening can be planned as a Vorsorge reminder keyed on
|
||||
// PHQ9_SCORE / GAD7_SCORE. Kick the eventful satisfy worker so completing
|
||||
// a check-in resolves the reminder immediately (the ingest-route
|
||||
// precedent); the 15-min cron stays the idempotent safety-net.
|
||||
await enqueueReminderSatisfy(user.id);
|
||||
|
||||
await auditLog("mental-health.create", {
|
||||
userId: user.id,
|
||||
// The score band is non-sensitive; item answers are NEVER logged.
|
||||
|
||||
Reference in new issue
Block a user