mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
fix(insights): scope generation guard to prompt fields, clear cache on restore
This commit is contained in:
1 parent
666a85d311
commit
3aff2c16db
104 files changed
+8082
-651
No files matched your search
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { coachPaths } from "../routes/coach";
|
||||
|
||||
describe("anamnesis fact mutation OpenAPI contracts", () => {
|
||||
it("documents the DELETE revision token, success metadata, and public errors", () => {
|
||||
const operation = coachPaths["/api/anamnesis/facts/{id}"]?.delete;
|
||||
|
||||
expect(operation).toBeDefined();
|
||||
expect(operation).toMatchObject({
|
||||
summary: "Remove a current anamnesis fact",
|
||||
parameters: [
|
||||
{
|
||||
name: "id",
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Metadata for the revision whose interval was closed.",
|
||||
},
|
||||
"401": expect.any(Object),
|
||||
"404": expect.any(Object),
|
||||
"409": expect.any(Object),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("documents POST's existing-current conflict", () => {
|
||||
const operation = coachPaths["/api/anamnesis/facts"]?.post;
|
||||
|
||||
expect(operation?.responses).toMatchObject({
|
||||
"201": expect.any(Object),
|
||||
"409": {
|
||||
description: expect.stringContaining("anamnesis.fact.currentExists"),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("documents PATCH's missing-target and revision-race responses", () => {
|
||||
const operation = coachPaths["/api/anamnesis/facts/{id}"]?.patch;
|
||||
|
||||
expect(operation?.responses).toMatchObject({
|
||||
"200": expect.any(Object),
|
||||
"404": expect.any(Object),
|
||||
"409": {
|
||||
description: expect.stringContaining("anamnesis.fact.conflict"),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { insightsPaths } from "../routes/insights/paths";
|
||||
|
||||
describe("insight patterns module gate OpenAPI contract", () => {
|
||||
it("documents module.disabled for list and dismissal updates", () => {
|
||||
const listResponses =
|
||||
insightsPaths["/api/insights/patterns"]?.get?.responses;
|
||||
const updateResponses =
|
||||
insightsPaths["/api/insights/patterns/{id}"]?.patch?.responses;
|
||||
|
||||
for (const responses of [listResponses, updateResponses]) {
|
||||
expect(Object.keys(responses ?? {})).toContain("403");
|
||||
expect(responses?.["403"]?.description).toContain("module.disabled");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,14 @@
|
||||
import { z } from "zod/v4";
|
||||
import type { ZodOpenApiObject } from "zod-openapi";
|
||||
import { aboutMePutSchema } from "@/lib/validations/about-me";
|
||||
import {
|
||||
healthProfileAiSectionSchema,
|
||||
healthProfileFactCorrectionSchema,
|
||||
healthProfileFactDtoSchema,
|
||||
healthProfileFactsResponseSchema,
|
||||
healthProfileFactWriteSchema,
|
||||
removedHealthProfileFactSchema,
|
||||
} from "@/lib/validations/health-profile-facts";
|
||||
import {
|
||||
ACCEPTED_INSIGHTS_TILE_IDS,
|
||||
INSIGHTS_SECTION_IDS,
|
||||
@@ -675,6 +683,22 @@ coachChatRequestSchema.meta({
|
||||
"Inbound Coach turn. `message` is the user's turn (1–4 000 chars). `conversationId` is omitted to start a new conversation (the server mints a title from the first message) and supplied to continue one. `scope` narrows which metrics the snapshot ships and which window the timeline covers; omitted fields fall back to server defaults. `locale` picks the reply language. `guidedQuestion` carries the clarifying question a message answers (client-side bubble, never persisted). No `userId` field — the owner is narrowed from the session / Bearer.",
|
||||
});
|
||||
|
||||
const anamnesisFactCurrentExistsResponse = {
|
||||
"409": {
|
||||
description:
|
||||
"A current revision already exists for this fact kind. `meta.errorCode` = `anamnesis.fact.currentExists`.",
|
||||
content: { "application/json": { schema: errorEnvelope } },
|
||||
},
|
||||
};
|
||||
|
||||
const anamnesisFactNotFoundResponse = {
|
||||
"404": {
|
||||
description:
|
||||
"No current revision with this id exists for the authenticated account.",
|
||||
content: { "application/json": { schema: errorEnvelope } },
|
||||
},
|
||||
};
|
||||
|
||||
export const coachPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
"/api/insights/chat": {
|
||||
get: {
|
||||
@@ -1155,6 +1179,7 @@ export const coachPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
conditions: z.string().nullable(),
|
||||
allergies: z.string().nullable(),
|
||||
coachFocus: z.string().nullable(),
|
||||
aiIncludedSections: z.array(healthProfileAiSectionSchema),
|
||||
pendingQuestions: z.array(z.string()),
|
||||
updatedAt: z.iso.datetime({ offset: true }).nullable(),
|
||||
maxChars: z.number().int(),
|
||||
@@ -1195,6 +1220,7 @@ export const coachPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
conditions: z.string().nullable(),
|
||||
allergies: z.string().nullable(),
|
||||
coachFocus: z.string().nullable(),
|
||||
aiIncludedSections: z.array(healthProfileAiSectionSchema),
|
||||
pendingQuestions: z.array(z.string()),
|
||||
updatedAt: z.iso.datetime({ offset: true }),
|
||||
maxChars: z.number().int(),
|
||||
@@ -1210,6 +1236,123 @@ export const coachPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/anamnesis/facts": {
|
||||
get: {
|
||||
tags: ["Insights"],
|
||||
summary: "Read effective-dated anamnesis facts",
|
||||
description:
|
||||
"Returns the current value and immutable revision history for the caller's smoking status, alcohol pattern and shift schedule. Values are decrypted server-side; unreadable ciphertext returns `value: null` with `unreadable: true`.",
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Current facts and their revision history.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
healthProfileFactsResponseSchema,
|
||||
"GetAnamnesisFactsResponse",
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
...stdResponses,
|
||||
},
|
||||
},
|
||||
post: {
|
||||
tags: ["Insights"],
|
||||
summary: "Create an anamnesis fact",
|
||||
description:
|
||||
"Creates the first current revision for one closed fact kind. The answer is encrypted before persistence. A second current value for the same kind returns 409; corrections use the revision endpoint.",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": { schema: healthProfileFactWriteSchema },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
"201": {
|
||||
description: "The newly created current revision.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
healthProfileFactDtoSchema,
|
||||
"CreateAnamnesisFactResponse",
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
...anamnesisFactCurrentExistsResponse,
|
||||
...stdResponses,
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/anamnesis/facts/{id}": {
|
||||
patch: {
|
||||
tags: ["Insights"],
|
||||
summary: "Correct a current anamnesis fact",
|
||||
description:
|
||||
"Closes the current revision at the correction instant and creates a successor. Prior ciphertext and its half-open validity interval remain in history.",
|
||||
parameters: [
|
||||
{
|
||||
name: "id",
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
},
|
||||
],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": { schema: healthProfileFactCorrectionSchema },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "The successor revision.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
healthProfileFactDtoSchema,
|
||||
"CorrectAnamnesisFactResponse",
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
...anamnesisFactNotFoundResponse,
|
||||
...conflictResponse409("Anamnesis fact", "anamnesis.fact.conflict"),
|
||||
...stdResponses,
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
tags: ["Insights"],
|
||||
summary: "Remove a current anamnesis fact",
|
||||
description:
|
||||
"Closes the caller-owned current revision without deleting its encrypted history. The fact kind then resolves as not recorded until a new value is created. The path revision id is the optimistic concurrency token.",
|
||||
parameters: [
|
||||
{
|
||||
name: "id",
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Metadata for the revision whose interval was closed.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
removedHealthProfileFactSchema,
|
||||
"RemoveAnamnesisFactResponse",
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
...anamnesisFactNotFoundResponse,
|
||||
...conflictResponse409("Anamnesis fact", "anamnesis.fact.conflict"),
|
||||
...stdResponses,
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/coach/about-me/questions": {
|
||||
get: {
|
||||
tags: ["Insights"],
|
||||
|
||||
@@ -532,6 +532,7 @@ export const insightsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
},
|
||||
},
|
||||
},
|
||||
...moduleDisabledResponse,
|
||||
...stdResponses,
|
||||
},
|
||||
},
|
||||
@@ -565,6 +566,7 @@ export const insightsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
description: "No current pattern with that id for this account.",
|
||||
content: { "application/json": { schema: errorEnvelope } },
|
||||
},
|
||||
...moduleDisabledResponse,
|
||||
...stdResponses,
|
||||
},
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user