diff --git a/docs/contributing/testing.md b/docs/contributing/testing.md index 18e2a30b2..c7694cad1 100644 --- a/docs/contributing/testing.md +++ b/docs/contributing/testing.md @@ -284,3 +284,14 @@ earns its place; do not extend the date. Order dependence is looked for on purpose: the nightly workflow runs both suites with `--sequence.shuffle` and prints the seed. Replay a failure with `--sequence.shuffle --sequence.seed=`. + +A case resets or states what it reads: `vi.resetAllMocks()` (or +`mockReset()`) where a case swaps in its own implementation, since +`clearAllMocks` keeps it and a `mockResolvedValueOnce` the code never consumed +answers the next case; a module cache dropped through its own invalidator; a +row the case needs written by the case or its own `beforeAll`. A file that is +one walk by design (a parity sequence, a migration's before and after, a +dry run followed by the import and the same file again) says so in a comment +and passes `{ shuffle: false }` to its `describe`, which keeps that block in +order while the rest of the suite still shuffles. Wrap the whole file in one +such block when its top-level blocks build on each other. diff --git a/tests/integration/routes/parity/analytics.parity.test.ts b/tests/integration/routes/parity/analytics.parity.test.ts index d68a489d9..c251e592b 100644 --- a/tests/integration/routes/parity/analytics.parity.test.ts +++ b/tests/integration/routes/parity/analytics.parity.test.ts @@ -210,7 +210,7 @@ afterAll(() => { vi.useRealTimers(); }); -describe("GET /api/analytics parity", () => { +describe("GET /api/analytics parity", { shuffle: false }, () => { it("01 a slice outside the closed set", async () => { await step("01-invalid-slice", { query: { slice: "summary" } }); }); diff --git a/tests/integration/routes/parity/dashboard-summary.parity.test.ts b/tests/integration/routes/parity/dashboard-summary.parity.test.ts index 7467d68fe..4840f50a8 100644 --- a/tests/integration/routes/parity/dashboard-summary.parity.test.ts +++ b/tests/integration/routes/parity/dashboard-summary.parity.test.ts @@ -159,7 +159,7 @@ afterAll(() => { vi.useRealTimers(); }); -describe("GET /api/dashboard/summary parity", () => { +describe("GET /api/dashboard/summary parity", { shuffle: false }, () => { it("01 cold, then warm", async () => { await step("01-cold"); await step("02-warm"); diff --git a/tests/integration/routes/parity/dashboard-widgets.parity.test.ts b/tests/integration/routes/parity/dashboard-widgets.parity.test.ts index 0faca0745..fa2bdfa03 100644 --- a/tests/integration/routes/parity/dashboard-widgets.parity.test.ts +++ b/tests/integration/routes/parity/dashboard-widgets.parity.test.ts @@ -169,7 +169,7 @@ afterAll(() => { vi.useRealTimers(); }); -describe("/api/dashboard/widgets parity", () => { +describe("/api/dashboard/widgets parity", { shuffle: false }, () => { let lastToken = ""; it("01 the first read, filled into the cache", async () => { diff --git a/tests/integration/routes/parity/documents-inbound.parity.test.ts b/tests/integration/routes/parity/documents-inbound.parity.test.ts index 81ef63c24..173012620 100644 --- a/tests/integration/routes/parity/documents-inbound.parity.test.ts +++ b/tests/integration/routes/parity/documents-inbound.parity.test.ts @@ -215,129 +215,133 @@ afterAll(() => { vi.useRealTimers(); }); -describe("POST /api/documents/inbound parity", () => { - it("a stored upload and the same bytes again", async () => { - const body = () => - form(PDF("discharge"), "discharge.pdf", { - title: "Discharge letter", - kind: "DISCHARGE_LETTER", - documentDate: "2026-10-02", - episodeIds: [episodeId], +// One walk: each request sees what the earlier ones stored, so the cases +// keep their order even when a run shuffles (`shuffle: false`). +describe("documents-inbound parity walk", { shuffle: false }, () => { + describe("POST /api/documents/inbound parity", () => { + it("a stored upload and the same bytes again", async () => { + const body = () => + form(PDF("discharge"), "discharge.pdf", { + title: "Discharge letter", + kind: "DISCHARGE_LETTER", + documentDate: "2026-10-02", + episodeIds: [episodeId], + }); + await upload("post-01-stored", body()); + await upload("post-02-duplicate", body()); + await upload( + "post-03-second", + form(PDF("labs"), "labs.pdf", { + title: "Blood test", + kind: "LAB_RESULT", + documentDate: "2025-03-04", + }), + ); + }); + + it("the refusals", async () => { + await upload("post-04-no-file", form(null, "x", { title: "Nothing" })); + await upload( + "post-05-invalid-metadata", + form(PDF("bad-kind"), "bad.pdf", { kind: "SHOPPING_LIST" }), + ); + await upload("post-06-empty", form(Buffer.alloc(0), "empty.pdf")); + await upload( + "post-07-unsupported", + form( + Buffer.from("hi\n"), + "page.html", + ), + ); + await upload( + "post-08-unknown-episode", + form(PDF("orphan"), "orphan.pdf", { + episodeIds: ["cmissingepisode000000000"], + }), + ); + await upload("post-09-half-key", form(PDF("half"), "half.pdf"), { + query: { sourceSystem: "PAPERLESS" }, }); - await upload("post-01-stored", body()); - await upload("post-02-duplicate", body()); - await upload( - "post-03-second", - form(PDF("labs"), "labs.pdf", { - title: "Blood test", - kind: "LAB_RESULT", - documentDate: "2025-03-04", - }), - ); + await upload( + "post-10-keys-differ", + form(PDF("differ"), "differ.pdf", { + sourceSystem: "PAPERLESS", + sourceId: "77", + }), + { query: { sourceSystem: "PAPERLESS", sourceId: "78" } }, + ); + }); + + it("a narrow token storing under a source key", async () => { + const stored = await upload( + "post-11-token-stored", + form(PDF("imported"), "imported.pdf", { + sourceSystem: "PAPERLESS", + sourceId: "41", + aiRead: "defer", + }), + { auth: writer }, + ); + await upload("post-12-token-query-key", form(PDF("imported"), "x.pdf"), { + auth: writer, + query: { sourceSystem: "PAPERLESS", sourceId: "41" }, + }); + await prisma.inboundDocument.update({ + where: { id: stored.id! }, + data: { deletedAt: new Date("2026-10-14T09:00:00.000Z") }, + }); + await upload( + "post-13-token-deleted", + form(PDF("imported"), "imported.pdf", { + sourceSystem: "PAPERLESS", + sourceId: "41", + }), + { auth: writer }, + ); + }); + + it("the module switched off", async () => { + await prisma.user.update({ + where: { id: userId }, + data: { modulePreferencesJson: { inboundDocuments: false } }, + }); + await upload("post-14-module-off", form(PDF("dark"), "dark.pdf")); + await prisma.user.update({ + where: { id: userId }, + data: { modulePreferencesJson: { inboundDocuments: true } }, + }); + }); }); - it("the refusals", async () => { - await upload("post-04-no-file", form(null, "x", { title: "Nothing" })); - await upload( - "post-05-invalid-metadata", - form(PDF("bad-kind"), "bad.pdf", { kind: "SHOPPING_LIST" }), - ); - await upload("post-06-empty", form(Buffer.alloc(0), "empty.pdf")); - await upload( - "post-07-unsupported", - form( - Buffer.from("hi\n"), - "page.html", - ), - ); - await upload( - "post-08-unknown-episode", - form(PDF("orphan"), "orphan.pdf", { - episodeIds: ["cmissingepisode000000000"], - }), - ); - await upload("post-09-half-key", form(PDF("half"), "half.pdf"), { - query: { sourceSystem: "PAPERLESS" }, + describe("GET /api/documents/inbound parity", () => { + it("the default page, a search and the filters", async () => { + await list("get-01-default", {}); + await list("get-02-search", { q: "blood" }); + await list("get-03-kind", { kind: "DISCHARGE_LETTER,LAB_RESULT" }); + await list("get-04-episode", { episodeId }); + await list("get-05-year", { year: 2025 }); + await list("get-06-range", { from: "2026-10-01", to: "2026-10-02" }); }); - await upload( - "post-10-keys-differ", - form(PDF("differ"), "differ.pdf", { - sourceSystem: "PAPERLESS", - sourceId: "77", - }), - { query: { sourceSystem: "PAPERLESS", sourceId: "78" } }, - ); - }); - it("a narrow token storing under a source key", async () => { - const stored = await upload( - "post-11-token-stored", - form(PDF("imported"), "imported.pdf", { - sourceSystem: "PAPERLESS", - sourceId: "41", - aiRead: "defer", - }), - { auth: writer }, - ); - await upload("post-12-token-query-key", form(PDF("imported"), "x.pdf"), { - auth: writer, - query: { sourceSystem: "PAPERLESS", sourceId: "41" }, + it("a sorted page and the page after it", async () => { + const first = await list("get-07-sorted", { + sort: "title", + order: "asc", + limit: 1, + }); + await list("get-08-next-page", { + sort: "title", + order: "asc", + limit: 1, + cursor: first.nextCursor!, + }); }); - await prisma.inboundDocument.update({ - where: { id: stored.id! }, - data: { deletedAt: new Date("2026-10-14T09:00:00.000Z") }, - }); - await upload( - "post-13-token-deleted", - form(PDF("imported"), "imported.pdf", { - sourceSystem: "PAPERLESS", - sourceId: "41", - }), - { auth: writer }, - ); - }); - it("the module switched off", async () => { - await prisma.user.update({ - where: { id: userId }, - data: { modulePreferencesJson: { inboundDocuments: false } }, - }); - await upload("post-14-module-off", form(PDF("dark"), "dark.pdf")); - await prisma.user.update({ - where: { id: userId }, - data: { modulePreferencesJson: { inboundDocuments: true } }, + it("a refused query, and a delegate reading the record", async () => { + await list("get-09-invalid", { sort: "size" }); + await switchSessionTo(delegate.sessionId, userId); + await list("get-10-delegated", {}, delegate); + await switchSessionTo(delegate.sessionId, null); }); }); }); - -describe("GET /api/documents/inbound parity", () => { - it("the default page, a search and the filters", async () => { - await list("get-01-default", {}); - await list("get-02-search", { q: "blood" }); - await list("get-03-kind", { kind: "DISCHARGE_LETTER,LAB_RESULT" }); - await list("get-04-episode", { episodeId }); - await list("get-05-year", { year: 2025 }); - await list("get-06-range", { from: "2026-10-01", to: "2026-10-02" }); - }); - - it("a sorted page and the page after it", async () => { - const first = await list("get-07-sorted", { - sort: "title", - order: "asc", - limit: 1, - }); - await list("get-08-next-page", { - sort: "title", - order: "asc", - limit: 1, - cursor: first.nextCursor!, - }); - }); - - it("a refused query, and a delegate reading the record", async () => { - await list("get-09-invalid", { sort: "size" }); - await switchSessionTo(delegate.sessionId, userId); - await list("get-10-delegated", {}, delegate); - await switchSessionTo(delegate.sessionId, null); - }); -}); diff --git a/tests/integration/routes/parity/import-csv.parity.test.ts b/tests/integration/routes/parity/import-csv.parity.test.ts index 60ee5ba50..2a84aac5b 100644 --- a/tests/integration/routes/parity/import-csv.parity.test.ts +++ b/tests/integration/routes/parity/import-csv.parity.test.ts @@ -199,7 +199,7 @@ afterAll(() => { vi.useRealTimers(); }); -describe("POST /api/import/csv parity", () => { +describe("POST /api/import/csv parity", { shuffle: false }, () => { it("01 a dry run", async () => { await step("01-dry-run", FILE, { dryRun: "1" }); }); diff --git a/tests/integration/routes/parity/import-json.parity.test.ts b/tests/integration/routes/parity/import-json.parity.test.ts index 265de2378..a63e05b6f 100644 --- a/tests/integration/routes/parity/import-json.parity.test.ts +++ b/tests/integration/routes/parity/import-json.parity.test.ts @@ -263,7 +263,7 @@ afterAll(() => { vi.useRealTimers(); }); -describe("POST /api/import parity", () => { +describe("POST /api/import parity", { shuffle: false }, () => { it("01 a first file", async () => { await step("01-first-file", FILE); }); diff --git a/tests/integration/routes/parity/insights-comprehensive.parity.test.ts b/tests/integration/routes/parity/insights-comprehensive.parity.test.ts index 72c1ae5b9..78adab744 100644 --- a/tests/integration/routes/parity/insights-comprehensive.parity.test.ts +++ b/tests/integration/routes/parity/insights-comprehensive.parity.test.ts @@ -408,7 +408,7 @@ afterAll(() => { vi.useRealTimers(); }); -describe("GET /api/insights/comprehensive parity", () => { +describe("GET /api/insights/comprehensive parity", { shuffle: false }, () => { it("01 an anonymous call", async () => { await step("01-anonymous", { auth: anonymous }); }); diff --git a/tests/integration/routes/parity/insights-generate.parity.test.ts b/tests/integration/routes/parity/insights-generate.parity.test.ts index 0cdb9d831..b451b8b07 100644 --- a/tests/integration/routes/parity/insights-generate.parity.test.ts +++ b/tests/integration/routes/parity/insights-generate.parity.test.ts @@ -635,380 +635,394 @@ afterAll(() => { vi.useRealTimers(); }); -describe("GET /api/insights/generate parity", () => { - it("G01 an anonymous call", async () => { - await get("G01-anonymous", { auth: anonymous }); - }); - - it("G02 the AI analysis opt-out", async () => { - await get("G02-opted-out", { auth: optedOut, userId: OPTED_OUT_ID }); - }); - - it("G03 no cached briefing", async () => { - await resetBriefing(OWNER_ID); - await get("G03-empty"); - }); - - it("G04 a briefing written today", async () => { - await resetBriefing(OWNER_ID, { - text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - HOUR)), - cachedAt: new Date(NOW.getTime() - HOUR), - cachedLocale: "en", +// One walk: each request sees what the earlier ones stored, so the cases +// keep their order even when a run shuffles (`shuffle: false`). +describe("insights-generate parity walk", { shuffle: false }, () => { + describe("GET /api/insights/generate parity", () => { + it("G01 an anonymous call", async () => { + await get("G01-anonymous", { auth: anonymous }); }); - await get("G04-fresh"); - }); - it("G05 a briefing written yesterday", async () => { - await resetBriefing(OWNER_ID, { - text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - 20 * HOUR)), - cachedAt: new Date(NOW.getTime() - 2 * HOUR), - cachedLocale: "en", + it("G02 the AI analysis opt-out", async () => { + await get("G02-opted-out", { auth: optedOut, userId: OPTED_OUT_ID }); }); - await get("G05-stale"); - }); - it("G06 a briefing in another language", async () => { - await resetBriefing(OWNER_ID, { - text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - HOUR)), - cachedAt: new Date(NOW.getTime() - HOUR), - cachedLocale: "de", - }); - await get("G06-other-language"); - }); - - it("G07 an unparseable cache row", async () => { - await resetBriefing(OWNER_ID, { - text: "{not json", - cachedAt: new Date(NOW.getTime() - HOUR), - }); - await get("G07-unparseable"); - }); - - it("G08 a legacy payload, untagged", async () => { - await resetBriefing(OWNER_ID, { - text: { summary: "Legacy", recommendations: ["Drink water."] }, - cachedAt: new Date(NOW.getTime() - HOUR), - }); - await get("G08-legacy"); - }); - - it("G09 a failure newer than the last good briefing", async () => { - await resetBriefing(OWNER_ID, { - text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - 3 * HOUR)), - cachedAt: new Date(NOW.getTime() - 3 * HOUR), - cachedLocale: "en", - markers: [ - { - reason: "all-providers-failed", - at: new Date(NOW.getTime() - HOUR), - httpStatus: 429, - }, - ], - }); - await get("G09-held-after-failure"); - }); - - it("G10 a failure with no good briefing, and an older superseded one", async () => { - await resetBriefing(OWNER_ID, { - markers: [ - { reason: "provider-error", at: new Date(NOW.getTime() - 5 * HOUR) }, - { reason: "invalid-json", at: new Date(NOW.getTime() - HOUR) }, - ], - }); - await get("G10-failed-empty"); - }); - - it("G11 a briefing the grounding gate withheld", async () => { - await resetBriefing(OWNER_ID, { - text: cached(null, new Date(NOW.getTime() - 2 * HOUR)), - cachedAt: new Date(NOW.getTime() - 2 * HOUR), - cachedLocale: "en", - markers: [ - { reason: "briefing-ungrounded", at: new Date(NOW.getTime() - HOUR) }, - ], - }); - await get("G11-withheld"); - }); -}); - -describe("POST /api/insights/generate parity", () => { - it("P01 an anonymous call", async () => { - await post("P01-anonymous", { force: true }, { auth: anonymous }); - }); - - it("P02 the AI analysis opt-out", async () => { - await post( - "P02-opted-out", - { force: true }, - { auth: optedOut, userId: OPTED_OUT_ID }, - ); - }); - - it("P03 a body over the cap", async () => { - await resetBriefing(OWNER_ID); - await post("P03-body-too-large", { pad: "x".repeat(17 * 1024) }); - }); - - it("P04 a cached briefing written today", async () => { - await resetBriefing(OWNER_ID, { - text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - HOUR)), - cachedAt: new Date(NOW.getTime() - HOUR), - cachedLocale: "en", - }); - await post("P04-cache-hit", {}); - }); - - it("P05 a cached briefing from yesterday, regenerated", async () => { - await resetBriefing(OWNER_ID, { - text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - 20 * HOUR)), - cachedAt: new Date(NOW.getTime() - 2 * HOUR), - cachedLocale: "en", - }); - seams.script.push({ content: reply(GROUNDED_BRIEFING) }); - await post("P05-yesterday-regenerated", {}); - }); - - it("P06 a cache in another language, regenerated", async () => { - await resetBriefing(OWNER_ID, { - text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - HOUR)), - cachedAt: new Date(NOW.getTime() - HOUR), - cachedLocale: "de", - }); - seams.script.push({ content: reply(GROUNDED_BRIEFING) }); - await post("P06-other-language-regenerated", {}); - }); - - it("P07 a briefingless cache when the hour's quota is spent", async () => { - await resetBriefing(OWNER_ID, { - text: cached(null, new Date(NOW.getTime() - HOUR)), - cachedAt: new Date(NOW.getTime() - HOUR), - cachedLocale: "en", - quotaUsed: 10, - }); - await post("P07-quota-cached-fallback", {}); - }); - - it("P08 a forced generation when the hour's quota is spent", async () => { - await resetBriefing(OWNER_ID, { quotaUsed: 10 }); - await post("P08-quota-refused"); - }); - - it("P09 a key no resolver can open", async () => { - await resetBriefing(UNREADABLE_KEY_ID); - await post( - "P09-unreadable-key", - { force: true }, - { auth: unreadableKey, userId: UNREADABLE_KEY_ID }, - ); - }); - - it("P10 a fresh generation", async () => { - await resetBriefing(OWNER_ID); - seams.script.push({ content: reply(GROUNDED_BRIEFING), tokensUsed: 2100 }); - await post("P10-generated"); - }); - - it("P11 a reply the strict schema accepts", async () => { - await resetBriefing(OWNER_ID); - seams.script.push({ - content: reply(GROUNDED_BRIEFING, { disclaimer: "Consult your doctor." }), - }); - await post("P11-strict-reply"); - }); - - it("P12 a generation in German with the comparison, about-me and short form", async () => { - await prisma.user.update({ - where: { id: OWNER_ID }, - data: { - locale: "de", - aiUsageLevel: "FRUGAL", - dashboardWidgetsJson: { comparisonBaseline: "lastMonth" }, - }, - }); - await prisma.allergy.create({ - data: { userId: OWNER_ID, substance: "Penicillin" }, - }); - await resetBriefing(OWNER_ID); - seams.script.push({ content: reply(GROUNDED_BRIEFING) }); - await post("P12-generated-de-short"); - await prisma.allergy.deleteMany({ where: { userId: OWNER_ID } }); - await prisma.user.update({ - where: { id: OWNER_ID }, - data: { locale: "en", aiUsageLevel: null, dashboardWidgetsJson: {} }, - }); - }); - - it("P13 an oversized raw payload, downgraded once", async () => { - await prisma.user.update({ - where: { id: OWNER_ID }, - data: { insightsPrivacyMode: "raw" }, - }); - await resetBriefing(OWNER_ID); - seams.oversize.push(6_100_000); - seams.script.push({ content: reply(GROUNDED_BRIEFING) }); - await post("P13-downgraded"); - }); - - it("P14 an oversized aggregate too, downgraded twice", async () => { - await resetBriefing(OWNER_ID); - seams.oversize.push(6_100_000, 5_400_000); - seams.script.push({ content: reply(GROUNDED_BRIEFING) }); - await post("P14-hard-downgraded"); - }); - - it("P15 oversized on every rung", async () => { - await resetBriefing(OWNER_ID); - seams.oversize.push(6_100_000, 5_400_000, 5_200_000); - await post("P15-too-large"); - await prisma.user.update({ - where: { id: OWNER_ID }, - data: { insightsPrivacyMode: "aggregated" }, - }); - }); - - it("P16 the day's token ceiling", async () => { - await resetBriefing(OWNER_ID, { spentToday: 5_000_000 }); - await post("P16-budget-exceeded"); - }); - - it("P17 every provider refused the credential", async () => { - await resetBriefing(OWNER_ID); - seams.script.push({ - error: () => new AllProvidersFailedError([hop(401), hop(403, 2)]), - }); - await post("P17-chain-auth"); - }); - - it("P18 every provider rate-limited", async () => { - await resetBriefing(OWNER_ID); - seams.script.push({ error: () => new AllProvidersFailedError([hop(429)]) }); - await post("P18-chain-rate-limited"); - }); - - it("P19 a provider in a 5xx", async () => { - await resetBriefing(OWNER_ID); - seams.script.push({ - error: () => new AllProvidersFailedError([hop(401), hop(502, 2)]), - }); - await post("P19-chain-5xx"); - }); - - it("P20 every provider unreachable", async () => { - await resetBriefing(OWNER_ID); - seams.script.push({ - error: () => new AllProvidersFailedError([hop(null), hop(null, 2)]), - }); - await post("P20-chain-transport"); - }); - - it("P21 a mixed chain failure", async () => { - await resetBriefing(OWNER_ID); - seams.script.push({ - error: () => new AllProvidersFailedError([hop(404), hop(429, 2)]), - }); - await post("P21-chain-mixed"); - }); - - it("P22 one provider error per status class", async () => { - for (const status of [401, 429, 500, 400]) { + it("G03 no cached briefing", async () => { await resetBriefing(OWNER_ID); - seams.script.push({ error: providerError(status) }); - await post(`P22-provider-${status}`); - } - }); - - it("P23 a reply that is not JSON", async () => { - await resetBriefing(OWNER_ID); - seams.script.push({ content: "Sure! Here is your briefing." }); - await post("P23-invalid-json"); - }); - - it("P24 a reply cut off at the token ceiling", async () => { - await resetBriefing(OWNER_ID); - seams.script.push({ content: '{"summary": "Your', finishReason: "length" }); - await post("P24-truncated"); - }); - - it("P25 an ungrounded briefing the retry fixes", async () => { - await resetBriefing(OWNER_ID); - seams.script.push( - { content: reply(UNGROUNDED_BRIEFING) }, - { content: reply(GROUNDED_BRIEFING), tokensUsed: 900 }, - ); - await post("P25-grounding-retry-kept"); - }); - - it("P26 still ungrounded, the previous briefing stands in", async () => { - await resetBriefing(OWNER_ID, { - text: cached(PREVIOUS_BRIEFING, new Date(NOW.getTime() - 26 * HOUR)), - cachedAt: new Date(NOW.getTime() - 26 * HOUR), - cachedLocale: "en", + await get("G03-empty"); }); - seams.script.push( - { content: reply(UNGROUNDED_BRIEFING) }, - { content: reply(UNGROUNDED_BRIEFING) }, - ); - await post("P26-grounding-previous"); - }); - it("P27 still ungrounded, nothing to stand in", async () => { - await resetBriefing(OWNER_ID); - seams.script.push( - { content: reply(UNGROUNDED_BRIEFING) }, - { content: reply(UNGROUNDED_BRIEFING) }, - ); - await post("P27-grounding-stripped"); - }); + it("G04 a briefing written today", async () => { + await resetBriefing(OWNER_ID, { + text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - HOUR)), + cachedAt: new Date(NOW.getTime() - HOUR), + cachedLocale: "en", + }); + await get("G04-fresh"); + }); - it("P28 an ungrounded briefing whose retry is not JSON", async () => { - await resetBriefing(OWNER_ID); - seams.script.push( - { content: reply(UNGROUNDED_BRIEFING) }, - { content: "not json" }, - ); - await post("P28-grounding-retry-unparseable"); - }); + it("G05 a briefing written yesterday", async () => { + await resetBriefing(OWNER_ID, { + text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - 20 * HOUR)), + cachedAt: new Date(NOW.getTime() - 2 * HOUR), + cachedLocale: "en", + }); + await get("G05-stale"); + }); - it("P29 a reply the outbound screen blocks", async () => { - await resetBriefing(OWNER_ID); - seams.script.push({ - content: reply(GROUNDED_BRIEFING, { - summary: - "Your 10-year cardiovascular risk is elevated — consider stepping your dose up.", - }), + it("G06 a briefing in another language", async () => { + await resetBriefing(OWNER_ID, { + text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - HOUR)), + cachedAt: new Date(NOW.getTime() - HOUR), + cachedLocale: "de", + }); + await get("G06-other-language"); + }); + + it("G07 an unparseable cache row", async () => { + await resetBriefing(OWNER_ID, { + text: "{not json", + cachedAt: new Date(NOW.getTime() - HOUR), + }); + await get("G07-unparseable"); + }); + + it("G08 a legacy payload, untagged", async () => { + await resetBriefing(OWNER_ID, { + text: { summary: "Legacy", recommendations: ["Drink water."] }, + cachedAt: new Date(NOW.getTime() - HOUR), + }); + await get("G08-legacy"); + }); + + it("G09 a failure newer than the last good briefing", async () => { + await resetBriefing(OWNER_ID, { + text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - 3 * HOUR)), + cachedAt: new Date(NOW.getTime() - 3 * HOUR), + cachedLocale: "en", + markers: [ + { + reason: "all-providers-failed", + at: new Date(NOW.getTime() - HOUR), + httpStatus: 429, + }, + ], + }); + await get("G09-held-after-failure"); + }); + + it("G10 a failure with no good briefing, and an older superseded one", async () => { + await resetBriefing(OWNER_ID, { + markers: [ + { reason: "provider-error", at: new Date(NOW.getTime() - 5 * HOUR) }, + { reason: "invalid-json", at: new Date(NOW.getTime() - HOUR) }, + ], + }); + await get("G10-failed-empty"); + }); + + it("G11 a briefing the grounding gate withheld", async () => { + await resetBriefing(OWNER_ID, { + text: cached(null, new Date(NOW.getTime() - 2 * HOUR)), + cachedAt: new Date(NOW.getTime() - 2 * HOUR), + cachedLocale: "en", + markers: [ + { reason: "briefing-ungrounded", at: new Date(NOW.getTime() - HOUR) }, + ], + }); + await get("G11-withheld"); }); - await post("P29-outbound-screened", { force: true }); }); - it("P30 the about-me text changing during the generation", async () => { - await resetBriefing(OWNER_ID); - seams.script.push({ - content: reply(GROUNDED_BRIEFING), - during: async () => { - await prisma.allergy.create({ - data: { userId: OWNER_ID, substance: "Latex" }, - }); - }, + describe("POST /api/insights/generate parity", () => { + it("P01 an anonymous call", async () => { + await post("P01-anonymous", { force: true }, { auth: anonymous }); }); - await post("P30-profile-scope-changed"); - await prisma.allergy.deleteMany({ where: { userId: OWNER_ID } }); - }); - it("P31 the profile changing during the generation", async () => { - await resetBriefing(OWNER_ID); - seams.script.push({ - content: reply(GROUNDED_BRIEFING), - during: async () => { - await prisma.user.update({ - where: { id: OWNER_ID }, - data: { displayName: "Renamed Person" }, - }); - }, + it("P02 the AI analysis opt-out", async () => { + await post( + "P02-opted-out", + { force: true }, + { auth: optedOut, userId: OPTED_OUT_ID }, + ); }); - await post("P31-scope-changed"); - await prisma.user.update({ - where: { id: OWNER_ID }, - data: { displayName: "Parity Person" }, + + it("P03 a body over the cap", async () => { + await resetBriefing(OWNER_ID); + await post("P03-body-too-large", { pad: "x".repeat(17 * 1024) }); + }); + + it("P04 a cached briefing written today", async () => { + await resetBriefing(OWNER_ID, { + text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - HOUR)), + cachedAt: new Date(NOW.getTime() - HOUR), + cachedLocale: "en", + }); + await post("P04-cache-hit", {}); + }); + + it("P05 a cached briefing from yesterday, regenerated", async () => { + await resetBriefing(OWNER_ID, { + text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - 20 * HOUR)), + cachedAt: new Date(NOW.getTime() - 2 * HOUR), + cachedLocale: "en", + }); + seams.script.push({ content: reply(GROUNDED_BRIEFING) }); + await post("P05-yesterday-regenerated", {}); + }); + + it("P06 a cache in another language, regenerated", async () => { + await resetBriefing(OWNER_ID, { + text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - HOUR)), + cachedAt: new Date(NOW.getTime() - HOUR), + cachedLocale: "de", + }); + seams.script.push({ content: reply(GROUNDED_BRIEFING) }); + await post("P06-other-language-regenerated", {}); + }); + + it("P07 a briefingless cache when the hour's quota is spent", async () => { + await resetBriefing(OWNER_ID, { + text: cached(null, new Date(NOW.getTime() - HOUR)), + cachedAt: new Date(NOW.getTime() - HOUR), + cachedLocale: "en", + quotaUsed: 10, + }); + await post("P07-quota-cached-fallback", {}); + }); + + it("P08 a forced generation when the hour's quota is spent", async () => { + await resetBriefing(OWNER_ID, { quotaUsed: 10 }); + await post("P08-quota-refused"); + }); + + it("P09 a key no resolver can open", async () => { + await resetBriefing(UNREADABLE_KEY_ID); + await post( + "P09-unreadable-key", + { force: true }, + { auth: unreadableKey, userId: UNREADABLE_KEY_ID }, + ); + }); + + it("P10 a fresh generation", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + content: reply(GROUNDED_BRIEFING), + tokensUsed: 2100, + }); + await post("P10-generated"); + }); + + it("P11 a reply the strict schema accepts", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + content: reply(GROUNDED_BRIEFING, { + disclaimer: "Consult your doctor.", + }), + }); + await post("P11-strict-reply"); + }); + + it("P12 a generation in German with the comparison, about-me and short form", async () => { + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { + locale: "de", + aiUsageLevel: "FRUGAL", + dashboardWidgetsJson: { comparisonBaseline: "lastMonth" }, + }, + }); + await prisma.allergy.create({ + data: { userId: OWNER_ID, substance: "Penicillin" }, + }); + await resetBriefing(OWNER_ID); + seams.script.push({ content: reply(GROUNDED_BRIEFING) }); + await post("P12-generated-de-short"); + await prisma.allergy.deleteMany({ where: { userId: OWNER_ID } }); + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { locale: "en", aiUsageLevel: null, dashboardWidgetsJson: {} }, + }); + }); + + it("P13 an oversized raw payload, downgraded once", async () => { + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { insightsPrivacyMode: "raw" }, + }); + await resetBriefing(OWNER_ID); + seams.oversize.push(6_100_000); + seams.script.push({ content: reply(GROUNDED_BRIEFING) }); + await post("P13-downgraded"); + }); + + it("P14 an oversized aggregate too, downgraded twice", async () => { + await resetBriefing(OWNER_ID); + seams.oversize.push(6_100_000, 5_400_000); + seams.script.push({ content: reply(GROUNDED_BRIEFING) }); + await post("P14-hard-downgraded"); + }); + + it("P15 oversized on every rung", async () => { + await resetBriefing(OWNER_ID); + seams.oversize.push(6_100_000, 5_400_000, 5_200_000); + await post("P15-too-large"); + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { insightsPrivacyMode: "aggregated" }, + }); + }); + + it("P16 the day's token ceiling", async () => { + await resetBriefing(OWNER_ID, { spentToday: 5_000_000 }); + await post("P16-budget-exceeded"); + }); + + it("P17 every provider refused the credential", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + error: () => new AllProvidersFailedError([hop(401), hop(403, 2)]), + }); + await post("P17-chain-auth"); + }); + + it("P18 every provider rate-limited", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + error: () => new AllProvidersFailedError([hop(429)]), + }); + await post("P18-chain-rate-limited"); + }); + + it("P19 a provider in a 5xx", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + error: () => new AllProvidersFailedError([hop(401), hop(502, 2)]), + }); + await post("P19-chain-5xx"); + }); + + it("P20 every provider unreachable", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + error: () => new AllProvidersFailedError([hop(null), hop(null, 2)]), + }); + await post("P20-chain-transport"); + }); + + it("P21 a mixed chain failure", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + error: () => new AllProvidersFailedError([hop(404), hop(429, 2)]), + }); + await post("P21-chain-mixed"); + }); + + it("P22 one provider error per status class", async () => { + for (const status of [401, 429, 500, 400]) { + await resetBriefing(OWNER_ID); + seams.script.push({ error: providerError(status) }); + await post(`P22-provider-${status}`); + } + }); + + it("P23 a reply that is not JSON", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ content: "Sure! Here is your briefing." }); + await post("P23-invalid-json"); + }); + + it("P24 a reply cut off at the token ceiling", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + content: '{"summary": "Your', + finishReason: "length", + }); + await post("P24-truncated"); + }); + + it("P25 an ungrounded briefing the retry fixes", async () => { + await resetBriefing(OWNER_ID); + seams.script.push( + { content: reply(UNGROUNDED_BRIEFING) }, + { content: reply(GROUNDED_BRIEFING), tokensUsed: 900 }, + ); + await post("P25-grounding-retry-kept"); + }); + + it("P26 still ungrounded, the previous briefing stands in", async () => { + await resetBriefing(OWNER_ID, { + text: cached(PREVIOUS_BRIEFING, new Date(NOW.getTime() - 26 * HOUR)), + cachedAt: new Date(NOW.getTime() - 26 * HOUR), + cachedLocale: "en", + }); + seams.script.push( + { content: reply(UNGROUNDED_BRIEFING) }, + { content: reply(UNGROUNDED_BRIEFING) }, + ); + await post("P26-grounding-previous"); + }); + + it("P27 still ungrounded, nothing to stand in", async () => { + await resetBriefing(OWNER_ID); + seams.script.push( + { content: reply(UNGROUNDED_BRIEFING) }, + { content: reply(UNGROUNDED_BRIEFING) }, + ); + await post("P27-grounding-stripped"); + }); + + it("P28 an ungrounded briefing whose retry is not JSON", async () => { + await resetBriefing(OWNER_ID); + seams.script.push( + { content: reply(UNGROUNDED_BRIEFING) }, + { content: "not json" }, + ); + await post("P28-grounding-retry-unparseable"); + }); + + it("P29 a reply the outbound screen blocks", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + content: reply(GROUNDED_BRIEFING, { + summary: + "Your 10-year cardiovascular risk is elevated — consider stepping your dose up.", + }), + }); + await post("P29-outbound-screened", { force: true }); + }); + + it("P30 the about-me text changing during the generation", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + content: reply(GROUNDED_BRIEFING), + during: async () => { + await prisma.allergy.create({ + data: { userId: OWNER_ID, substance: "Latex" }, + }); + }, + }); + await post("P30-profile-scope-changed"); + await prisma.allergy.deleteMany({ where: { userId: OWNER_ID } }); + }); + + it("P31 the profile changing during the generation", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + content: reply(GROUNDED_BRIEFING), + during: async () => { + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { displayName: "Renamed Person" }, + }); + }, + }); + await post("P31-scope-changed"); + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { displayName: "Parity Person" }, + }); }); }); }); diff --git a/tests/integration/routes/parity/measurement-series.parity.test.ts b/tests/integration/routes/parity/measurement-series.parity.test.ts index 3c2e82bc6..9b9f9f2c5 100644 --- a/tests/integration/routes/parity/measurement-series.parity.test.ts +++ b/tests/integration/routes/parity/measurement-series.parity.test.ts @@ -239,7 +239,7 @@ beforeAll(async () => { delegate = { auth: session }; }); -describe("GET /api/measurements/series parity", () => { +describe("GET /api/measurements/series parity", { shuffle: false }, () => { it("the plain readers", async () => { await step("01-weight", { kind: "weight" }); await step("02-steps", { kind: "steps", days: 14 }); diff --git a/tests/integration/routes/parity/measurements-batch.parity.test.ts b/tests/integration/routes/parity/measurements-batch.parity.test.ts index 2cb309931..9315aa8ab 100644 --- a/tests/integration/routes/parity/measurements-batch.parity.test.ts +++ b/tests/integration/routes/parity/measurements-batch.parity.test.ts @@ -327,7 +327,7 @@ afterAll(() => { vi.useRealTimers(); }); -describe("POST /api/measurements/batch parity", () => { +describe("POST /api/measurements/batch parity", { shuffle: false }, () => { it("01 a first sync of mixed entries", async () => { await step("01-first-sync", { entries: FIRST_SYNC, diff --git a/tests/integration/routes/parity/measurements.parity.test.ts b/tests/integration/routes/parity/measurements.parity.test.ts index 5055c39e0..8401da77f 100644 --- a/tests/integration/routes/parity/measurements.parity.test.ts +++ b/tests/integration/routes/parity/measurements.parity.test.ts @@ -220,185 +220,192 @@ afterAll(() => { vi.useRealTimers(); }); -describe("POST /api/measurements parity", () => { - it("a single reading, its duplicate, a refused one", async () => { - const weight = { - type: "WEIGHT", - value: 80.4, - measuredAt: "2026-10-14T06:30:00.000Z", - notes: "after the run", - deviceType: "scale", - }; - await post("post-01-single", weight); - await post("post-02-single-duplicate", weight); - await post("post-03-single-invalid", { - type: "WEIGHT", - value: "heavy", - measuredAt: "2026-10-14T06:30:00.000Z", - }); - await post("post-04-single-glucose", { - type: "BLOOD_GLUCOSE", - value: 104, - measuredAt: "2026-10-14T07:00:00.000Z", - glucoseContext: "FASTING", - symptomsPresent: false, - }); - }); - - it("a combined two-value reading, its duplicate, a refused one", async () => { - const reading = [ - { - type: "BLOOD_PRESSURE_SYS", - value: 128, - measuredAt: "2026-10-14T08:00:00.000Z", - }, - { - type: "BLOOD_PRESSURE_DIA", - value: 82, - measuredAt: "2026-10-14T08:00:00.000Z", - }, - { - type: "PULSE", - value: 66, - measuredAt: "2026-10-14T08:00:00.000Z", - notes: "seated", - symptomsPresent: true, - }, - ]; - await post("post-05-batch", reading); - await post("post-06-batch-duplicate", reading); - await post("post-07-batch-invalid", [{ type: "PULSE", value: -1 }]); - }); - - it("a named sleep-stage segment, its re-post and its mirror", async () => { - const segment = { - type: "SLEEP_DURATION", - sleepStage: "DEEP", - startDate: "2026-10-14T01:00:00.000Z", - endDate: "2026-10-14T02:30:00.000Z", - externalId: "bridge-sleep-1", - }; - await post("post-08-sleep-stage", segment); - await post("post-09-sleep-stage-again", segment); - await post("post-10-sleep-stage-mirror", { - ...segment, - externalId: "hk-sleep-1", - source: "APPLE_HEALTH", - }); - await post("post-11-sleep-stage-invalid", { - type: "SLEEP_DURATION", - sleepStage: "DEEP", - }); - }); - - it("a narrow bridge token with and without a source", async () => { - const bridge = await makeToken( - { id: ownerId }, - { permissions: [MEASUREMENTS_WRITE_SCOPE] }, - ); - await post( - "post-12-bridge-with-source", - { +// One walk: each request sees what the earlier ones stored, so the cases +// keep their order even when a run shuffles (`shuffle: false`). +describe("measurements parity walk", { shuffle: false }, () => { + describe("POST /api/measurements parity", () => { + it("a single reading, its duplicate, a refused one", async () => { + const weight = { type: "WEIGHT", - value: 80.1, - measuredAt: "2026-10-15T06:30:00.000Z", - source: "MANUAL", - }, - bridge, - ); - await post( - "post-13-bridge", - { + value: 80.4, + measuredAt: "2026-10-14T06:30:00.000Z", + notes: "after the run", + deviceType: "scale", + }; + await post("post-01-single", weight); + await post("post-02-single-duplicate", weight); + await post("post-03-single-invalid", { type: "WEIGHT", - value: 80.1, - measuredAt: "2026-10-15T06:30:00.000Z", - }, - bridge, - ); - }); - - it("a delegate writing into a shared record", async () => { - const delegateUser = await makeUser({ timezone: "UTC" }); - await makeGrant({ id: ownerId }, delegateUser, { access: "WRITE" }); - const delegate = await makeSession(delegateUser); - await switchSessionTo(delegate.sessionId, ownerId); - await post( - "post-14-delegated", - { - type: "PULSE", - value: 71, - measuredAt: "2026-10-15T07:00:00.000Z", - }, - delegate, - ); - await switchSessionTo(delegate.sessionId, null); - }); -}); - -describe("GET /api/measurements parity", () => { - beforeAll(async () => { - // Day totals for the cumulative list modes, from the phone. - for (const [day, value] of [ - ["2026-10-12", 7_100], - ["2026-10-13", 8_400], - ["2026-10-14", 5_100], - ] as const) { - await prisma.measurement.create({ - data: { - userId: ownerId, - type: "ACTIVITY_STEPS", - value, - unit: "steps", - source: "APPLE_HEALTH", - measuredAt: new Date(`${day}T12:00:00.000Z`), - externalId: `stats:${STEPS}:${day}`, - aggregationProvenance: "HEALTHKIT_STATISTICS", - }, + value: "heavy", + measuredAt: "2026-10-14T06:30:00.000Z", + }); + await post("post-04-single-glucose", { + type: "BLOOD_GLUCOSE", + value: 104, + measuredAt: "2026-10-14T07:00:00.000Z", + glucoseContext: "FASTING", + symptomsPresent: false, }); - } - }); - - it("the plain page and its filters", async () => { - await get("get-01-page", { limit: 50 }); - await get("get-02-type-sorted", { - type: "PULSE", - sortBy: "value", - sortDir: "asc", }); - await get("get-03-filtered", { - sourceEq: "MANUAL", - valueMin: 70, - valueMax: 130, - from: "2026-10-14T00:00:00.000Z", - to: "2026-10-15T23:59:59.000Z", - limit: 2, - offset: 1, + + it("a combined two-value reading, its duplicate, a refused one", async () => { + const reading = [ + { + type: "BLOOD_PRESSURE_SYS", + value: 128, + measuredAt: "2026-10-14T08:00:00.000Z", + }, + { + type: "BLOOD_PRESSURE_DIA", + value: 82, + measuredAt: "2026-10-14T08:00:00.000Z", + }, + { + type: "PULSE", + value: 66, + measuredAt: "2026-10-14T08:00:00.000Z", + notes: "seated", + symptomsPresent: true, + }, + ]; + await post("post-05-batch", reading); + await post("post-06-batch-duplicate", reading); + await post("post-07-batch-invalid", [{ type: "PULSE", value: -1 }]); + }); + + it("a named sleep-stage segment, its re-post and its mirror", async () => { + const segment = { + type: "SLEEP_DURATION", + sleepStage: "DEEP", + startDate: "2026-10-14T01:00:00.000Z", + endDate: "2026-10-14T02:30:00.000Z", + externalId: "bridge-sleep-1", + }; + await post("post-08-sleep-stage", segment); + await post("post-09-sleep-stage-again", segment); + await post("post-10-sleep-stage-mirror", { + ...segment, + externalId: "hk-sleep-1", + source: "APPLE_HEALTH", + }); + await post("post-11-sleep-stage-invalid", { + type: "SLEEP_DURATION", + sleepStage: "DEEP", + }); + }); + + it("a narrow bridge token with and without a source", async () => { + const bridge = await makeToken( + { id: ownerId }, + { permissions: [MEASUREMENTS_WRITE_SCOPE] }, + ); + await post( + "post-12-bridge-with-source", + { + type: "WEIGHT", + value: 80.1, + measuredAt: "2026-10-15T06:30:00.000Z", + source: "MANUAL", + }, + bridge, + ); + await post( + "post-13-bridge", + { + type: "WEIGHT", + value: 80.1, + measuredAt: "2026-10-15T06:30:00.000Z", + }, + bridge, + ); + }); + + it("a delegate writing into a shared record", async () => { + const delegateUser = await makeUser({ timezone: "UTC" }); + await makeGrant({ id: ownerId }, delegateUser, { access: "WRITE" }); + const delegate = await makeSession(delegateUser); + await switchSessionTo(delegate.sessionId, ownerId); + await post( + "post-14-delegated", + { + type: "PULSE", + value: 71, + measuredAt: "2026-10-15T07:00:00.000Z", + }, + delegate, + ); + await switchSessionTo(delegate.sessionId, null); }); }); - it("the list modes", async () => { - await get("get-04-sleep", { type: "SLEEP_DURATION" }); - await get("get-05-drill-down", { - type: "ACTIVITY_STEPS", - dayKey: "2026-10-13", + describe("GET /api/measurements parity", () => { + beforeAll(async () => { + // Day totals for the cumulative list modes, from the phone. + for (const [day, value] of [ + ["2026-10-12", 7_100], + ["2026-10-13", 8_400], + ["2026-10-14", 5_100], + ] as const) { + await prisma.measurement.create({ + data: { + userId: ownerId, + type: "ACTIVITY_STEPS", + value, + unit: "steps", + source: "APPLE_HEALTH", + measuredAt: new Date(`${day}T12:00:00.000Z`), + externalId: `stats:${STEPS}:${day}`, + aggregationProvenance: "HEALTHKIT_STATISTICS", + }, + }); + } }); - await get("get-06-day-grouped", { type: "ACTIVITY_STEPS", groupBy: "day" }); - await get("get-07-rollup-series", { - type: "ACTIVITY_STEPS", - source: "rollup", - aggregate: "daily", - from: "2026-10-10T00:00:00.000Z", - to: "2026-10-15T23:59:59.000Z", - }); - await get("get-08-live-aggregate", { - type: "PULSE", - aggregate: "weekly", - from: "2026-09-01T00:00:00.000Z", - to: "2026-10-15T23:59:59.000Z", - }); - }); - it("a refused query", async () => { - await get("get-09-invalid", { limit: 0, sortDir: "sideways" }); + it("the plain page and its filters", async () => { + await get("get-01-page", { limit: 50 }); + await get("get-02-type-sorted", { + type: "PULSE", + sortBy: "value", + sortDir: "asc", + }); + await get("get-03-filtered", { + sourceEq: "MANUAL", + valueMin: 70, + valueMax: 130, + from: "2026-10-14T00:00:00.000Z", + to: "2026-10-15T23:59:59.000Z", + limit: 2, + offset: 1, + }); + }); + + it("the list modes", async () => { + await get("get-04-sleep", { type: "SLEEP_DURATION" }); + await get("get-05-drill-down", { + type: "ACTIVITY_STEPS", + dayKey: "2026-10-13", + }); + await get("get-06-day-grouped", { + type: "ACTIVITY_STEPS", + groupBy: "day", + }); + await get("get-07-rollup-series", { + type: "ACTIVITY_STEPS", + source: "rollup", + aggregate: "daily", + from: "2026-10-10T00:00:00.000Z", + to: "2026-10-15T23:59:59.000Z", + }); + await get("get-08-live-aggregate", { + type: "PULSE", + aggregate: "weekly", + from: "2026-09-01T00:00:00.000Z", + to: "2026-10-15T23:59:59.000Z", + }); + }); + + it("a refused query", async () => { + await get("get-09-invalid", { limit: 0, sortDir: "sideways" }); + }); }); }); diff --git a/tests/integration/routes/parity/medication-detail.parity.test.ts b/tests/integration/routes/parity/medication-detail.parity.test.ts index 8ca3932e7..d914179d9 100644 --- a/tests/integration/routes/parity/medication-detail.parity.test.ts +++ b/tests/integration/routes/parity/medication-detail.parity.test.ts @@ -358,212 +358,216 @@ afterAll(() => { vi.useRealTimers(); }); -describe("GET /api/medications/{id} parity", () => { - it("01 a rolling injection with stock, a composition and a custom category", async () => { - await get("01-rolling", med.rolling); +// One walk: each request sees what the earlier ones stored, so the cases +// keep their order even when a run shuffles (`shuffle: false`). +describe("medication-detail parity walk", { shuffle: false }, () => { + describe("GET /api/medications/{id} parity", () => { + it("01 a rolling injection with stock, a composition and a custom category", async () => { + await get("01-rolling", med.rolling); + }); + + it("02 a daily tablet without stock", async () => { + await get("02-daily", med.tablet); + }); + + it("03 a record-only medication", async () => { + await get("03-record-only", med.recordOnly); + }); + + it("04 another account's medication", async () => { + await get("04-not-found", med.foreign); + }); + + it("05 a reading delegate, and nobody", async () => { + await get("05a-reader", med.rolling, reader); + await get("05b-anonymous", med.rolling, { auth: anonymous }); + }); }); - it("02 a daily tablet without stock", async () => { - await get("02-daily", med.tablet); + describe("PUT /api/medications/{id} parity", () => { + it("11 plain fields", async () => { + await put("11-plain-fields", med.tablet, { + name: "Daily tablet renamed", + dose: "20mg", + treatmentClass: "GENERIC", + dosesPerUnit: 30, + reorderLeadDays: 5, + deliveryForm: "ORAL", + notificationsEnabled: false, + liveActivityEnabled: true, + criticalAlarmEnabled: true, + atcCode: "C09AA05", + rxNormCode: null, + }); + }); + + it("12 a schedule replace whose dose time leaves the stale window", async () => { + await put("12-schedule-replace", med.tablet, { + schedules: [ + { + windowStart: "08:00", + windowEnd: "10:00", + timesOfDay: ["11:30"], + label: "Late morning", + dose: "10mg", + unitsPerDose: 2, + reminderGraceMinutes: 30, + }, + { + windowStart: "20:00", + windowEnd: "22:00", + daysOfWeek: [1, 3, 5], + intervalWeeks: 2, + doseWindows: [{ timeOfDay: "20:00", start: "19:30", end: "21:00" }], + }, + ], + }); + }); + + it("13 the same schedule echoed back", async () => { + await put("13-schedule-echo", med.tablet, { + schedules: [ + { + windowStart: "11:30", + windowEnd: "11:30", + timesOfDay: ["11:30"], + label: "Late morning", + dose: "10mg", + unitsPerDose: 2, + reminderGraceMinutes: 30, + rrule: "FREQ=DAILY", + }, + { + windowStart: "20:00", + windowEnd: "22:00", + daysOfWeek: [1, 3, 5], + intervalWeeks: 2, + doseWindows: [{ timeOfDay: "20:00", start: "19:30", end: "21:00" }], + }, + ], + }); + }); + + it("14 the top-level reminder window", async () => { + await put("14-grace-bridge", med.tablet, { reminderGraceMinutes: 45 }); + }); + + it("15 a pause and a resume", async () => { + await put("15a-pause", med.tablet, { active: false }); + await put("15b-resume", med.tablet, { active: true }); + }); + + it("16 automatic recording switched on", async () => { + await put("16-auto-mark-taken", med.tablet, { autoMarkTaken: true }); + }); + + it("17 the category writes", async () => { + await put("17a-custom-category", med.tablet, { category: CUSTOM_KEY }); + await put( + "17b-native-other-kept", + med.tablet, + { category: "OTHER" }, + { auth: ownerBearer }, + ); + await put("17c-web-other", med.tablet, { category: "OTHER" }); + await put("17d-unknown-category", med.tablet, { + category: "custom:0a6c2d4e-1111-4222-8333-944455556666", + }); + }); + + it("18 a record-only medication and both tracking transitions", async () => { + await put("18a-record-schedule-kept", med.recordOnly, { + name: "Record only renamed", + schedules: [daily("06:00", "07:00")], + oneShot: false, + }); + await put("18b-tracking-on", med.recordOnly, { trackIntake: true }); + await put("18c-tracking-off", med.recordOnly, { trackIntake: false }); + }); + + it("19 the as-needed flips and their refusals", async () => { + await put("19a-scheduled-without-schedules", med.asNeeded, { + asNeeded: false, + }); + await put("19b-scheduled", med.asNeeded, { + asNeeded: false, + schedules: [daily("08:00", "09:00")], + }); + await put("19c-as-needed-keeping-schedules", med.asNeeded, { + asNeeded: true, + }); + await put("19d-as-needed", med.asNeeded, { + asNeeded: true, + schedules: [], + }); + }); + + it("20 the one-shot refusals and a conversion", async () => { + await put("20a-one-shot-two-schedules", med.booster, { + oneShot: true, + startsOn: "2026-10-20", + schedules: [daily("09:00", "10:00"), daily("18:00", "19:00")], + }); + await put("20b-one-shot-recurrence", med.booster, { + oneShot: true, + startsOn: "2026-10-20", + schedules: [ + { windowStart: "09:00", windowEnd: "10:00", rrule: "FREQ=DAILY" }, + ], + }); + await put("20c-one-shot", med.booster, { + oneShot: true, + startsOn: "2026-10-20", + schedules: [{ windowStart: "09:00", windowEnd: "10:00" }], + }); + }); + + it("21 the course refusals and a course edit", async () => { + await put("21a-course-overlap", med.course, { startsOn: "2026-08-20" }); + await put("21b-course-window-required", med.course, { + startsOn: null, + endsOn: null, + }); + await put("21c-course-end", med.course, { endsOn: "2026-12-31" }); + }); + + it("22 a units change on a medication with a composition", async () => { + await put("22-composition-units", med.rolling, { unitsPerDose: 2 }); + }); + + it("23 a body the schema refuses, and another account's medication", async () => { + await put("23a-invalid", med.tablet, { + name: "", + unitsPerDose: -1, + schedules: "daily", + }); + await put("23b-not-found", med.foreign, { name: "Taken over" }); + }); + + it("24 a delegate who may write, one who may manage, and nobody", async () => { + await put("24a-writer", med.tablet, { name: "By a writer" }, writer); + await put("24b-manager", med.tablet, { name: "By a manager" }, manager); + await put( + "24c-anonymous", + med.tablet, + { name: "By nobody" }, + { auth: anonymous }, + ); + }); }); - it("03 a record-only medication", async () => { - await get("03-record-only", med.recordOnly); - }); + describe("DELETE /api/medications/{id} parity", () => { + it("31 the owner deletes a medication with a composition and an ingest token", async () => { + await del("31-delete", med.doomed); + }); - it("04 another account's medication", async () => { - await get("04-not-found", med.foreign); - }); + it("32 the same medication again", async () => { + await del("32-not-found", med.doomed); + }); - it("05 a reading delegate, and nobody", async () => { - await get("05a-reader", med.rolling, reader); - await get("05b-anonymous", med.rolling, { auth: anonymous }); - }); -}); - -describe("PUT /api/medications/{id} parity", () => { - it("11 plain fields", async () => { - await put("11-plain-fields", med.tablet, { - name: "Daily tablet renamed", - dose: "20mg", - treatmentClass: "GENERIC", - dosesPerUnit: 30, - reorderLeadDays: 5, - deliveryForm: "ORAL", - notificationsEnabled: false, - liveActivityEnabled: true, - criticalAlarmEnabled: true, - atcCode: "C09AA05", - rxNormCode: null, + it("33 a managing delegate, and nobody", async () => { + await del("33a-manager", med.booster, manager); + await del("33b-anonymous", med.booster, { auth: anonymous }); }); - }); - - it("12 a schedule replace whose dose time leaves the stale window", async () => { - await put("12-schedule-replace", med.tablet, { - schedules: [ - { - windowStart: "08:00", - windowEnd: "10:00", - timesOfDay: ["11:30"], - label: "Late morning", - dose: "10mg", - unitsPerDose: 2, - reminderGraceMinutes: 30, - }, - { - windowStart: "20:00", - windowEnd: "22:00", - daysOfWeek: [1, 3, 5], - intervalWeeks: 2, - doseWindows: [{ timeOfDay: "20:00", start: "19:30", end: "21:00" }], - }, - ], - }); - }); - - it("13 the same schedule echoed back", async () => { - await put("13-schedule-echo", med.tablet, { - schedules: [ - { - windowStart: "11:30", - windowEnd: "11:30", - timesOfDay: ["11:30"], - label: "Late morning", - dose: "10mg", - unitsPerDose: 2, - reminderGraceMinutes: 30, - rrule: "FREQ=DAILY", - }, - { - windowStart: "20:00", - windowEnd: "22:00", - daysOfWeek: [1, 3, 5], - intervalWeeks: 2, - doseWindows: [{ timeOfDay: "20:00", start: "19:30", end: "21:00" }], - }, - ], - }); - }); - - it("14 the top-level reminder window", async () => { - await put("14-grace-bridge", med.tablet, { reminderGraceMinutes: 45 }); - }); - - it("15 a pause and a resume", async () => { - await put("15a-pause", med.tablet, { active: false }); - await put("15b-resume", med.tablet, { active: true }); - }); - - it("16 automatic recording switched on", async () => { - await put("16-auto-mark-taken", med.tablet, { autoMarkTaken: true }); - }); - - it("17 the category writes", async () => { - await put("17a-custom-category", med.tablet, { category: CUSTOM_KEY }); - await put( - "17b-native-other-kept", - med.tablet, - { category: "OTHER" }, - { auth: ownerBearer }, - ); - await put("17c-web-other", med.tablet, { category: "OTHER" }); - await put("17d-unknown-category", med.tablet, { - category: "custom:0a6c2d4e-1111-4222-8333-944455556666", - }); - }); - - it("18 a record-only medication and both tracking transitions", async () => { - await put("18a-record-schedule-kept", med.recordOnly, { - name: "Record only renamed", - schedules: [daily("06:00", "07:00")], - oneShot: false, - }); - await put("18b-tracking-on", med.recordOnly, { trackIntake: true }); - await put("18c-tracking-off", med.recordOnly, { trackIntake: false }); - }); - - it("19 the as-needed flips and their refusals", async () => { - await put("19a-scheduled-without-schedules", med.asNeeded, { - asNeeded: false, - }); - await put("19b-scheduled", med.asNeeded, { - asNeeded: false, - schedules: [daily("08:00", "09:00")], - }); - await put("19c-as-needed-keeping-schedules", med.asNeeded, { - asNeeded: true, - }); - await put("19d-as-needed", med.asNeeded, { - asNeeded: true, - schedules: [], - }); - }); - - it("20 the one-shot refusals and a conversion", async () => { - await put("20a-one-shot-two-schedules", med.booster, { - oneShot: true, - startsOn: "2026-10-20", - schedules: [daily("09:00", "10:00"), daily("18:00", "19:00")], - }); - await put("20b-one-shot-recurrence", med.booster, { - oneShot: true, - startsOn: "2026-10-20", - schedules: [ - { windowStart: "09:00", windowEnd: "10:00", rrule: "FREQ=DAILY" }, - ], - }); - await put("20c-one-shot", med.booster, { - oneShot: true, - startsOn: "2026-10-20", - schedules: [{ windowStart: "09:00", windowEnd: "10:00" }], - }); - }); - - it("21 the course refusals and a course edit", async () => { - await put("21a-course-overlap", med.course, { startsOn: "2026-08-20" }); - await put("21b-course-window-required", med.course, { - startsOn: null, - endsOn: null, - }); - await put("21c-course-end", med.course, { endsOn: "2026-12-31" }); - }); - - it("22 a units change on a medication with a composition", async () => { - await put("22-composition-units", med.rolling, { unitsPerDose: 2 }); - }); - - it("23 a body the schema refuses, and another account's medication", async () => { - await put("23a-invalid", med.tablet, { - name: "", - unitsPerDose: -1, - schedules: "daily", - }); - await put("23b-not-found", med.foreign, { name: "Taken over" }); - }); - - it("24 a delegate who may write, one who may manage, and nobody", async () => { - await put("24a-writer", med.tablet, { name: "By a writer" }, writer); - await put("24b-manager", med.tablet, { name: "By a manager" }, manager); - await put( - "24c-anonymous", - med.tablet, - { name: "By nobody" }, - { auth: anonymous }, - ); - }); -}); - -describe("DELETE /api/medications/{id} parity", () => { - it("31 the owner deletes a medication with a composition and an ingest token", async () => { - await del("31-delete", med.doomed); - }); - - it("32 the same medication again", async () => { - await del("32-not-found", med.doomed); - }); - - it("33 a managing delegate, and nobody", async () => { - await del("33a-manager", med.booster, manager); - await del("33b-anonymous", med.booster, { auth: anonymous }); }); }); diff --git a/tests/integration/routes/parity/medication-intake-bulk.parity.test.ts b/tests/integration/routes/parity/medication-intake-bulk.parity.test.ts index f51313028..4175929d7 100644 --- a/tests/integration/routes/parity/medication-intake-bulk.parity.test.ts +++ b/tests/integration/routes/parity/medication-intake-bulk.parity.test.ts @@ -375,7 +375,7 @@ afterAll(() => { vi.useRealTimers(); }); -describe("POST /api/medications/intake/bulk parity", () => { +describe("POST /api/medications/intake/bulk parity", { shuffle: false }, () => { it("01 a mixed first batch", async () => { await post("01-mixed", mixedBatch()); }); diff --git a/tests/integration/routes/parity/medication-intake-event.parity.test.ts b/tests/integration/routes/parity/medication-intake-event.parity.test.ts index acfbe8d4b..775a38dba 100644 --- a/tests/integration/routes/parity/medication-intake-event.parity.test.ts +++ b/tests/integration/routes/parity/medication-intake-event.parity.test.ts @@ -229,131 +229,135 @@ afterAll(() => { vi.useRealTimers(); }); -describe("PUT /api/medications/{id}/intake/{eventId} parity", () => { - it("01 a pending dose taken inside its own window", async () => { - await put("01-take-in-place", events.pending, { - takenAt: "2026-10-13T08:40:00.000Z", +// One walk: each request sees what the earlier ones stored, so the cases +// keep their order even when a run shuffles (`shuffle: false`). +describe("medication-intake-event parity walk", { shuffle: false }, () => { + describe("PUT /api/medications/{id}/intake/{eventId} parity", () => { + it("01 a pending dose taken inside its own window", async () => { + await put("01-take-in-place", events.pending, { + takenAt: "2026-10-13T08:40:00.000Z", + }); + }); + + it("02 the same dose flipped to a skip", async () => { + await put("02-skip-flip", events.pending, { skipped: true }); + }); + + it("03 a take corrected into the other window, onto a skipped slot", async () => { + const res = await put("03-move-by-band", events.morningTaken, { + takenAt: "2026-10-14T20:30:00.000Z", + }); + events.moved = (res.data as { id: string }).id; + }); + + it("04 a legacy take pinned onto a free slot", async () => { + const res = await put("04-pin-free-slot", events.legacyTaken, { + forceSlotInstant: "2026-10-12T20:00:00.000Z", + }); + events.pinned = (res.data as { id: string }).id; + }); + + it("05 a pin onto an instant that is no slot", async () => { + await put("05-pin-not-a-slot", events.moved, { + forceSlotInstant: "2026-10-12T15:00:00.000Z", + }); + }); + + it("06 a pin onto a slot another dose holds", async () => { + await put("06-pin-occupied", events.moved, { + forceSlotInstant: "2026-10-12T20:00:00.000Z", + }); + }); + + it("07 an unpin", async () => { + const res = await put("07-unpin", events.pinned, { + forceSlotInstant: null, + }); + events.pinned = (res.data as { id: string }).id; + }); + + it("08 an explicit slot in the body", async () => { + const res = await put("08-explicit-slot", events.moved, { + scheduledFor: "2026-10-14T08:00:00.000Z", + takenAt: "2026-10-14T08:45:00.000Z", + }); + events.moved = (res.data as { id: string }).id; + }); + + it("09 a take before the start day", async () => { + await put("09-before-start", events.pinned, { + takenAt: "2026-09-20T08:00:00.000Z", + }); + }); + + it("10 a body the schema refuses", async () => { + await put("10-invalid", events.pinned, { + takenAt: "yesterday", + skipped: "no", + }); + }); + + it("11 a tombstoned dose, and a dose under another medication", async () => { + await put("11a-tombstoned", events.tombstoned, { skipped: true }); + await put( + "11b-other-medication", + events.pinned, + { skipped: true }, + { medicationId: oneShotId }, + ); + }); + + it("12 a delegate who may write, and one who may manage", async () => { + await put( + "12a-writer", + events.pinned, + { takenAt: "2026-10-13T20:50:00.000Z" }, + writer, + ); + await put( + "12b-manager", + events.pinned, + { takenAt: "2026-10-13T20:50:00.000Z" }, + manager, + ); + }); + + it("13 no credentials", async () => { + await put( + "13-anonymous", + events.pinned, + { skipped: true }, + { + auth: anonymous, + }, + ); }); }); - it("02 the same dose flipped to a skip", async () => { - await put("02-skip-flip", events.pending, { skipped: true }); - }); - - it("03 a take corrected into the other window, onto a skipped slot", async () => { - const res = await put("03-move-by-band", events.morningTaken, { - takenAt: "2026-10-14T20:30:00.000Z", + describe("DELETE /api/medications/{id}/intake/{eventId} parity", () => { + it("21 the owner undoes a dose", async () => { + await del("21-delete", events.forDelete); }); - events.moved = (res.data as { id: string }).id; - }); - it("04 a legacy take pinned onto a free slot", async () => { - const res = await put("04-pin-free-slot", events.legacyTaken, { - forceSlotInstant: "2026-10-12T20:00:00.000Z", + it("22 undoing the only dose of a one-shot medication", async () => { + await del("22-delete-one-shot", events.oneShotDose, { + medicationId: oneShotId, + }); }); - events.pinned = (res.data as { id: string }).id; - }); - it("05 a pin onto an instant that is no slot", async () => { - await put("05-pin-not-a-slot", events.moved, { - forceSlotInstant: "2026-10-12T15:00:00.000Z", + it("23 a dose under another medication", async () => { + await del("23-other-medication", events.forManagerDelete, { + medicationId: oneShotId, + }); }); - }); - it("06 a pin onto a slot another dose holds", async () => { - await put("06-pin-occupied", events.moved, { - forceSlotInstant: "2026-10-12T20:00:00.000Z", + it("24 a delegate who may write, and one who may manage", async () => { + await del("24a-writer", events.forManagerDelete, writer); + await del("24b-manager", events.forManagerDelete, manager); }); - }); - it("07 an unpin", async () => { - const res = await put("07-unpin", events.pinned, { - forceSlotInstant: null, + it("25 no credentials", async () => { + await del("25-anonymous", events.forDelete, { auth: anonymous }); }); - events.pinned = (res.data as { id: string }).id; - }); - - it("08 an explicit slot in the body", async () => { - const res = await put("08-explicit-slot", events.moved, { - scheduledFor: "2026-10-14T08:00:00.000Z", - takenAt: "2026-10-14T08:45:00.000Z", - }); - events.moved = (res.data as { id: string }).id; - }); - - it("09 a take before the start day", async () => { - await put("09-before-start", events.pinned, { - takenAt: "2026-09-20T08:00:00.000Z", - }); - }); - - it("10 a body the schema refuses", async () => { - await put("10-invalid", events.pinned, { - takenAt: "yesterday", - skipped: "no", - }); - }); - - it("11 a tombstoned dose, and a dose under another medication", async () => { - await put("11a-tombstoned", events.tombstoned, { skipped: true }); - await put( - "11b-other-medication", - events.pinned, - { skipped: true }, - { medicationId: oneShotId }, - ); - }); - - it("12 a delegate who may write, and one who may manage", async () => { - await put( - "12a-writer", - events.pinned, - { takenAt: "2026-10-13T20:50:00.000Z" }, - writer, - ); - await put( - "12b-manager", - events.pinned, - { takenAt: "2026-10-13T20:50:00.000Z" }, - manager, - ); - }); - - it("13 no credentials", async () => { - await put( - "13-anonymous", - events.pinned, - { skipped: true }, - { - auth: anonymous, - }, - ); - }); -}); - -describe("DELETE /api/medications/{id}/intake/{eventId} parity", () => { - it("21 the owner undoes a dose", async () => { - await del("21-delete", events.forDelete); - }); - - it("22 undoing the only dose of a one-shot medication", async () => { - await del("22-delete-one-shot", events.oneShotDose, { - medicationId: oneShotId, - }); - }); - - it("23 a dose under another medication", async () => { - await del("23-other-medication", events.forManagerDelete, { - medicationId: oneShotId, - }); - }); - - it("24 a delegate who may write, and one who may manage", async () => { - await del("24a-writer", events.forManagerDelete, writer); - await del("24b-manager", events.forManagerDelete, manager); - }); - - it("25 no credentials", async () => { - await del("25-anonymous", events.forDelete, { auth: anonymous }); }); }); diff --git a/tests/integration/routes/parity/medication-intake-status.parity.test.ts b/tests/integration/routes/parity/medication-intake-status.parity.test.ts index 92f6c25bb..1c4859108 100644 --- a/tests/integration/routes/parity/medication-intake-status.parity.test.ts +++ b/tests/integration/routes/parity/medication-intake-status.parity.test.ts @@ -317,138 +317,145 @@ afterAll(() => { vi.useRealTimers(); }); -describe("GET /api/medications/intake parity", () => { - it("01 today's list", async () => { - await get("01-today", { scope: "today" }); +// One walk: each request sees what the earlier ones stored, so the cases +// keep their order even when a run shuffles (`shuffle: false`). +describe("medication-intake-status parity walk", { shuffle: false }, () => { + describe("GET /api/medications/intake parity", () => { + it("01 today's list", async () => { + await get("01-today", { scope: "today" }); + }); + + it("02 the compliance buckets, cold and from the cache", async () => { + await get("02a-compliance-cold", { scope: "compliance", days: "7" }); + await get("02b-compliance-warm", { scope: "compliance", days: "7" }); + }); + + it("03 a query the schema refuses", async () => { + await get("03-invalid", { scope: "yesterday", days: "0" }); + }); + + it("04 a reading delegate, and nobody", async () => { + await get("04a-reader", { scope: "today" }, reader); + await get("04b-anonymous", { scope: "today" }, { auth: anonymous }); + }); }); - it("02 the compliance buckets, cold and from the cache", async () => { - await get("02a-compliance-cold", { scope: "compliance", days: "7" }); - await get("02b-compliance-warm", { scope: "compliance", days: "7" }); - }); + describe("POST /api/medications/intake parity", () => { + it("11 a take in place", async () => { + await post("11-take-in-place", { + intakeId: ev.todayMorning, + status: "taken", + takenAt: "2026-10-15T08:30:00.000Z", + }); + }); - it("03 a query the schema refuses", async () => { - await get("03-invalid", { scope: "yesterday", days: "0" }); - }); + it("12 a take that re-binds to another slot", async () => { + await post("12-take-slot-move", { + intakeId: ev.moveMe, + status: "taken", + takenAt: "2026-10-14T20:30:00.000Z", + }); + }); - it("04 a reading delegate, and nobody", async () => { - await get("04a-reader", { scope: "today" }, reader); - await get("04b-anonymous", { scope: "today" }, { auth: anonymous }); - }); -}); - -describe("POST /api/medications/intake parity", () => { - it("11 a take in place", async () => { - await post("11-take-in-place", { - intakeId: ev.todayMorning, - status: "taken", - takenAt: "2026-10-15T08:30:00.000Z", + it("13 a pinned take, and a pin that is no slot", async () => { + await post("13a-pin", { + intakeId: ev.pinMe, + status: "taken", + takenAt: "2026-10-13T11:00:00.000Z", + forceSlotInstant: "2026-10-13T08:00:00.000Z", + }); + await post("13b-pin-not-a-slot", { + intakeId: ev.snoozeMe, + status: "taken", + forceSlotInstant: "2026-10-11T15:00:00.000Z", + }); }); - }); - it("12 a take that re-binds to another slot", async () => { - await post("12-take-slot-move", { - intakeId: ev.moveMe, - status: "taken", - takenAt: "2026-10-14T20:30:00.000Z", + it("14 a skip over a recorded dose", async () => { + await post("14-skip", { intakeId: ev.skipMe, status: "skipped" }); }); - }); - it("13 a pinned take, and a pin that is no slot", async () => { - await post("13a-pin", { - intakeId: ev.pinMe, - status: "taken", - takenAt: "2026-10-13T11:00:00.000Z", - forceSlotInstant: "2026-10-13T08:00:00.000Z", - }); - await post("13b-pin-not-a-slot", { - intakeId: ev.snoozeMe, - status: "taken", - forceSlotInstant: "2026-10-11T15:00:00.000Z", + it("15 a snooze with its end, and one without", async () => { + await post("15a-snooze-until", { + intakeId: ev.snoozeMe, + status: "snoozed", + snoozedUntil: "2026-10-15T13:00:00.000Z", + }); + await post("15b-snooze-default", { + intakeId: ev.snoozeMe, + status: "snoozed", + }); }); - }); - it("14 a skip over a recorded dose", async () => { - await post("14-skip", { intakeId: ev.skipMe, status: "skipped" }); - }); - - it("15 a snooze with its end, and one without", async () => { - await post("15a-snooze-until", { - intakeId: ev.snoozeMe, - status: "snoozed", - snoozedUntil: "2026-10-15T13:00:00.000Z", - }); - await post("15b-snooze-default", { - intakeId: ev.snoozeMe, - status: "snoozed", + it("16 the not-found answers", async () => { + await post("16a-unknown", { + intakeId: "cmv0000000000000000000000", + status: "taken", + }); + await post("16b-foreign", { intakeId: ev.foreign, status: "taken" }); + await post("16c-tombstoned", { + intakeId: ev.tombstoned, + status: "taken", + }); }); - }); - it("16 the not-found answers", async () => { - await post("16a-unknown", { - intakeId: "cmv0000000000000000000000", - status: "taken", + it("17 the record-only rule both ways", async () => { + await post("17a-record-only-open", { + intakeId: ev.recordOpen, + status: "taken", + }); + await post("17b-record-only-correction", { + intakeId: ev.recordTaken, + status: "taken", + takenAt: "2026-10-13T07:20:00.000Z", + }); }); - await post("16b-foreign", { intakeId: ev.foreign, status: "taken" }); - await post("16c-tombstoned", { intakeId: ev.tombstoned, status: "taken" }); - }); - it("17 the record-only rule both ways", async () => { - await post("17a-record-only-open", { - intakeId: ev.recordOpen, - status: "taken", + it("18 the injection-site refusal, and an allowed site", async () => { + await post("18a-site-not-allowed", { + intakeId: ev.penToday, + status: "taken", + takenAt: "2026-10-15T09:15:00.000Z", + injectionSite: "ABDOMEN_RIGHT", + }); + await post("18b-site", { + intakeId: ev.penToday, + status: "taken", + takenAt: "2026-10-15T09:15:00.000Z", + injectionSite: "ABDOMEN_LEFT", + }); }); - await post("17b-record-only-correction", { - intakeId: ev.recordTaken, - status: "taken", - takenAt: "2026-10-13T07:20:00.000Z", - }); - }); - it("18 the injection-site refusal, and an allowed site", async () => { - await post("18a-site-not-allowed", { - intakeId: ev.penToday, - status: "taken", - takenAt: "2026-10-15T09:15:00.000Z", - injectionSite: "ABDOMEN_RIGHT", - }); - await post("18b-site", { - intakeId: ev.penToday, - status: "taken", - takenAt: "2026-10-15T09:15:00.000Z", - injectionSite: "ABDOMEN_LEFT", + it("19 a body the schema refuses", async () => { + await post("19-invalid", { intakeId: "", status: "forgotten" }); }); - }); - it("19 a body the schema refuses", async () => { - await post("19-invalid", { intakeId: "", status: "forgotten" }); - }); - - it("20 a delegate who marks an open dose, a snooze, a correction, a reader and nobody", async () => { - await post( - "20a-writer-take", - { intakeId: ev.todayEvening, status: "taken" }, - { ...writer }, - ); - await post( - "20b-writer-snooze-refused", - { intakeId: ev.snoozeMe, status: "snoozed" }, - writer, - ); - await post( - "20c-writer-correction-refused", - { intakeId: ev.resolved, status: "skipped" }, - writer, - ); - await post( - "20d-reader", - { intakeId: ev.snoozeMe, status: "taken" }, - reader, - ); - await post( - "20e-anonymous", - { intakeId: ev.snoozeMe, status: "taken" }, - { auth: anonymous }, - ); + it("20 a delegate who marks an open dose, a snooze, a correction, a reader and nobody", async () => { + await post( + "20a-writer-take", + { intakeId: ev.todayEvening, status: "taken" }, + { ...writer }, + ); + await post( + "20b-writer-snooze-refused", + { intakeId: ev.snoozeMe, status: "snoozed" }, + writer, + ); + await post( + "20c-writer-correction-refused", + { intakeId: ev.resolved, status: "skipped" }, + writer, + ); + await post( + "20d-reader", + { intakeId: ev.snoozeMe, status: "taken" }, + reader, + ); + await post( + "20e-anonymous", + { intakeId: ev.snoozeMe, status: "taken" }, + { auth: anonymous }, + ); + }); }); }); diff --git a/tests/integration/routes/parity/medication-intake.parity.test.ts b/tests/integration/routes/parity/medication-intake.parity.test.ts index e87cf639b..e5ac70486 100644 --- a/tests/integration/routes/parity/medication-intake.parity.test.ts +++ b/tests/integration/routes/parity/medication-intake.parity.test.ts @@ -343,189 +343,193 @@ afterAll(() => { vi.useRealTimers(); }); -describe("POST /api/medications/{id}/intake parity", () => { - it("01 a take inside its band, onto the pending reminder row", async () => { - await post("01-take-in-band", { takenAt: "2026-10-15T08:30:00.000Z" }); - }); - - it("02 an as-needed take recorded standalone, and its repeat inside the dedup window", async () => { - await post( - "02a-take-standalone", - { takenAt: "2026-10-15T11:00:00.000Z" }, - { medicationId: med.asNeeded }, - ); - await post( - "02b-take-standalone-repeat", - { takenAt: "2026-10-15T11:00:00.000Z" }, - { medicationId: med.asNeeded }, - ); - }); - - it("03 a skip on a named slot, a slot-less skip, and a skip onto a standalone dose", async () => { - await post("03a-skip-named-slot", { - skipped: true, - scheduledFor: "2026-10-15T20:00:00.000Z", +// One walk: each request sees what the earlier ones stored, so the cases +// keep their order even when a run shuffles (`shuffle: false`). +describe("medication-intake parity walk", { shuffle: false }, () => { + describe("POST /api/medications/{id}/intake parity", () => { + it("01 a take inside its band, onto the pending reminder row", async () => { + await post("01-take-in-band", { takenAt: "2026-10-15T08:30:00.000Z" }); }); - await post("03b-skip-slotless", { skipped: true }); - await post( - "03c-delegate-skip-standalone-refused", - { skipped: true, scheduledFor: "2026-10-15T11:00:00.000Z" }, - { ...writer, medicationId: med.asNeeded, settle: () => refusalRows(1) }, - ); - await post( - "03d-owner-skip-standalone", - { skipped: true, scheduledFor: "2026-10-15T11:00:00.000Z" }, - { medicationId: med.asNeeded }, - ); - }); - it("04 a pinned take and the two pin refusals", async () => { - await post("04a-pin", { - takenAt: "2026-10-14T11:00:00.000Z", - forceSlotInstant: "2026-10-14T08:00:00.000Z", + it("02 an as-needed take recorded standalone, and its repeat inside the dedup window", async () => { + await post( + "02a-take-standalone", + { takenAt: "2026-10-15T11:00:00.000Z" }, + { medicationId: med.asNeeded }, + ); + await post( + "02b-take-standalone-repeat", + { takenAt: "2026-10-15T11:00:00.000Z" }, + { medicationId: med.asNeeded }, + ); }); - await post("04b-pin-not-a-slot", { - takenAt: "2026-10-14T16:00:00.000Z", - forceSlotInstant: "2026-10-14T15:00:00.000Z", + + it("03 a skip on a named slot, a slot-less skip, and a skip onto a standalone dose", async () => { + await post("03a-skip-named-slot", { + skipped: true, + scheduledFor: "2026-10-15T20:00:00.000Z", + }); + await post("03b-skip-slotless", { skipped: true }); + await post( + "03c-delegate-skip-standalone-refused", + { skipped: true, scheduledFor: "2026-10-15T11:00:00.000Z" }, + { ...writer, medicationId: med.asNeeded, settle: () => refusalRows(1) }, + ); + await post( + "03d-owner-skip-standalone", + { skipped: true, scheduledFor: "2026-10-15T11:00:00.000Z" }, + { medicationId: med.asNeeded }, + ); }); - await post("04c-pin-occupied", { - takenAt: "2026-10-14T23:00:00.000Z", - forceSlotInstant: "2026-10-14T20:00:00.000Z", + + it("04 a pinned take and the two pin refusals", async () => { + await post("04a-pin", { + takenAt: "2026-10-14T11:00:00.000Z", + forceSlotInstant: "2026-10-14T08:00:00.000Z", + }); + await post("04b-pin-not-a-slot", { + takenAt: "2026-10-14T16:00:00.000Z", + forceSlotInstant: "2026-10-14T15:00:00.000Z", + }); + await post("04c-pin-occupied", { + takenAt: "2026-10-14T23:00:00.000Z", + forceSlotInstant: "2026-10-14T20:00:00.000Z", + }); + }); + + it("05 a backfill onto a named earlier slot", async () => { + await post("05-named-backfill", { + takenAt: "2026-10-15T11:30:00.000Z", + scheduledFor: "2026-10-13T08:00:00.000Z", + }); + }); + + it("06 a body idempotency key and its replay", async () => { + const body = { + takenAt: "2026-10-13T20:30:00.000Z", + idempotencyKey: "dose-key-0001", + }; + await post("06a-body-key", body); + await post("06b-body-key-replay", body); + }); + + it("07 an Idempotency-Key header and its replay", async () => { + const headers = { + "idempotency-key": "4b7e1c9a-2d3f-4a5b-8c6d-7e8f9a0b1c2d", + }; + const body = { takenAt: "2026-10-12T08:15:00.000Z" }; + await post("07a-header-key", body, { headers }); + await post("07b-header-key-replay", body, { headers }); + }); + + it("08 a native take", async () => { + await post( + "08-native", + { takenAt: "2026-10-11T20:10:00.000Z" }, + { auth: ownerBearer }, + ); + }); + + it("09 a skip over a recorded dose, by the owner and by a delegate", async () => { + await post( + "09a-delegate-skip-refused", + { skipped: true, scheduledFor: "2026-10-14T20:00:00.000Z" }, + { ...writer, settle: () => refusalRows(2) }, + ); + await post("09b-owner-skip-flip", { + skipped: true, + scheduledFor: "2026-10-14T20:00:00.000Z", + }); + }); + + it("10 a delegate's take on an open slot, a reader, and nobody", async () => { + await post( + "10a-writer-take", + { takenAt: "2026-10-11T08:20:00.000Z" }, + writer, + ); + await post("10b-reader", { takenAt: "2026-10-10T08:20:00.000Z" }, reader); + await post( + "10c-anonymous", + { takenAt: "2026-10-10T08:20:00.000Z" }, + { auth: anonymous }, + ); + }); + + it("11 the injection-site refusal, and a site with a dose override", async () => { + await post( + "11a-site-not-allowed", + { takenAt: "2026-10-15T09:10:00.000Z", injectionSite: "ABDOMEN_RIGHT" }, + { medicationId: med.pen }, + ); + await post( + "11b-site-and-dose", + { + takenAt: "2026-10-15T09:10:00.000Z", + injectionSite: "ABDOMEN_LEFT", + doseTaken: "0.5 pen", + }, + { medicationId: med.pen }, + ); + }); + + it("12 a one-shot dose", async () => { + await post( + "12-one-shot", + { takenAt: "2026-10-15T10:00:00.000Z" }, + { medicationId: med.oneShot }, + ); + }); + + it("13 a body the schema refuses", async () => { + await post("13-invalid", { + takenAt: "yesterday", + idempotencyKey: "", + }); + }); + + it("14 another account's medication, and a record-only one", async () => { + await post( + "14a-not-found", + { takenAt: "2026-10-15T08:30:00.000Z" }, + { medicationId: med.foreign }, + ); + await post( + "14b-record-only", + { takenAt: "2026-10-15T07:10:00.000Z" }, + { medicationId: med.recordOnly }, + ); }); }); - it("05 a backfill onto a named earlier slot", async () => { - await post("05-named-backfill", { - takenAt: "2026-10-15T11:30:00.000Z", - scheduledFor: "2026-10-13T08:00:00.000Z", + describe("GET /api/medications/{id}/intake parity", () => { + it("21 the default list", async () => { + await list("21-default"); }); - }); - it("06 a body idempotency key and its replay", async () => { - const body = { - takenAt: "2026-10-13T20:30:00.000Z", - idempotencyKey: "dose-key-0001", - }; - await post("06a-body-key", body); - await post("06b-body-key-replay", body); - }); - - it("07 an Idempotency-Key header and its replay", async () => { - const headers = { - "idempotency-key": "4b7e1c9a-2d3f-4a5b-8c6d-7e8f9a0b1c2d", - }; - const body = { takenAt: "2026-10-12T08:15:00.000Z" }; - await post("07a-header-key", body, { headers }); - await post("07b-header-key-replay", body, { headers }); - }); - - it("08 a native take", async () => { - await post( - "08-native", - { takenAt: "2026-10-11T20:10:00.000Z" }, - { auth: ownerBearer }, - ); - }); - - it("09 a skip over a recorded dose, by the owner and by a delegate", async () => { - await post( - "09a-delegate-skip-refused", - { skipped: true, scheduledFor: "2026-10-14T20:00:00.000Z" }, - { ...writer, settle: () => refusalRows(2) }, - ); - await post("09b-owner-skip-flip", { - skipped: true, - scheduledFor: "2026-10-14T20:00:00.000Z", + it("22 a completed page sorted by take time", async () => { + await list("22-completed-page", { + status: "completed", + sortBy: "takenAt", + sortDir: "asc", + limit: "3", + offset: "1", + }); }); - }); - it("10 a delegate's take on an open slot, a reader, and nobody", async () => { - await post( - "10a-writer-take", - { takenAt: "2026-10-11T08:20:00.000Z" }, - writer, - ); - await post("10b-reader", { takenAt: "2026-10-10T08:20:00.000Z" }, reader); - await post( - "10c-anonymous", - { takenAt: "2026-10-10T08:20:00.000Z" }, - { auth: anonymous }, - ); - }); - - it("11 the injection-site refusal, and a site with a dose override", async () => { - await post( - "11a-site-not-allowed", - { takenAt: "2026-10-15T09:10:00.000Z", injectionSite: "ABDOMEN_RIGHT" }, - { medicationId: med.pen }, - ); - await post( - "11b-site-and-dose", - { - takenAt: "2026-10-15T09:10:00.000Z", - injectionSite: "ABDOMEN_LEFT", - doseTaken: "0.5 pen", - }, - { medicationId: med.pen }, - ); - }); - - it("12 a one-shot dose", async () => { - await post( - "12-one-shot", - { takenAt: "2026-10-15T10:00:00.000Z" }, - { medicationId: med.oneShot }, - ); - }); - - it("13 a body the schema refuses", async () => { - await post("13-invalid", { - takenAt: "yesterday", - idempotencyKey: "", + it("23 the skips, and the takes", async () => { + await list("23a-skipped", { status: "skipped" }); + await list("23b-taken", { status: "taken", sortDir: "asc" }); }); - }); - it("14 another account's medication, and a record-only one", async () => { - await post( - "14a-not-found", - { takenAt: "2026-10-15T08:30:00.000Z" }, - { medicationId: med.foreign }, - ); - await post( - "14b-record-only", - { takenAt: "2026-10-15T07:10:00.000Z" }, - { medicationId: med.recordOnly }, - ); - }); -}); - -describe("GET /api/medications/{id}/intake parity", () => { - it("21 the default list", async () => { - await list("21-default"); - }); - - it("22 a completed page sorted by take time", async () => { - await list("22-completed-page", { - status: "completed", - sortBy: "takenAt", - sortDir: "asc", - limit: "3", - offset: "1", + it("24 a query the schema refuses", async () => { + await list("24-invalid", { limit: "lots", status: "maybe" }); }); - }); - - it("23 the skips, and the takes", async () => { - await list("23a-skipped", { status: "skipped" }); - await list("23b-taken", { status: "taken", sortDir: "asc" }); - }); - it("24 a query the schema refuses", async () => { - await list("24-invalid", { limit: "lots", status: "maybe" }); - }); - - it("25 another account's medication, a reading delegate, and nobody", async () => { - await list("25a-not-found", {}, { medicationId: med.foreign }); - await list("25b-reader", {}, reader); - await list("25c-anonymous", {}, { auth: anonymous }); + it("25 another account's medication, a reading delegate, and nobody", async () => { + await list("25a-not-found", {}, { medicationId: med.foreign }); + await list("25b-reader", {}, reader); + await list("25c-anonymous", {}, { auth: anonymous }); + }); }); }); diff --git a/tests/integration/routes/parity/medications.parity.test.ts b/tests/integration/routes/parity/medications.parity.test.ts index 0e3b15478..84b671309 100644 --- a/tests/integration/routes/parity/medications.parity.test.ts +++ b/tests/integration/routes/parity/medications.parity.test.ts @@ -138,197 +138,204 @@ afterAll(() => { vi.useRealTimers(); }); -describe("GET /api/medications parity", () => { - it("01 the owner's list", async () => { - await list("01-list"); - }); +// One walk: each request sees what the earlier ones stored, so the cases +// keep their order even when a run shuffles (`shuffle: false`). +describe("medications parity walk", { shuffle: false }, () => { + describe("GET /api/medications parity", () => { + it("01 the owner's list", async () => { + await list("01-list"); + }); - it("02 a reading delegate's view of it, and nobody's", async () => { - await list("02a-list-reader", reader); - await list("02b-list-anonymous", { auth: anonymous }); - }); -}); - -describe("POST /api/medications parity", () => { - it("11 a plain create", async () => { - await create("11-plain", { - name: "Plain tablet", - dose: "5mg", - schedules: [schedule("07:00", "09:00")], + it("02 a reading delegate's view of it, and nobody's", async () => { + await list("02a-list-reader", reader); + await list("02b-list-anonymous", { auth: anonymous }); }); }); - it("12 a full create with a custom category, a course and auto-take", async () => { - await create("12-full", { - name: "Full injection", - dose: "1 pen", - category: CUSTOM_KEY, - treatmentClass: "GLP1", - dosesPerUnit: 4, - unitsPerDose: 0.5, - reorderLeadDays: 7, - deliveryForm: "INJECTION", - trackInjectionSites: true, - allowedInjectionSites: ["ABDOMEN_LEFT", "THIGH_RIGHT"], - notificationsEnabled: false, - liveActivityEnabled: true, - criticalAlarmEnabled: true, - atcCode: "A10BJ06", - rxNormCode: "1991302", - startsOn: "2026-10-10", - endsOn: "2026-12-31", - autoMarkTaken: true, - schedules: [ - schedule("18:00", "20:00", { - label: "Evening", - dose: "0.5 pen", - unitsPerDose: 1, - timesOfDay: ["18:30"], - reminderGraceMinutes: 45, - rrule: "FREQ=WEEKLY;BYDAY=MO", - doseWindows: [{ timeOfDay: "18:30", start: "18:00", end: "20:00" }], - }), - ], + describe("POST /api/medications parity", () => { + it("11 a plain create", async () => { + await create("11-plain", { + name: "Plain tablet", + dose: "5mg", + schedules: [schedule("07:00", "09:00")], + }); }); - }); - it("13 an as-needed create, a one-shot one and a PRN schedule", async () => { - await create("13a-as-needed", { - name: "As needed", - dose: "400mg", - category: "PAIN_RELIEF", - asNeeded: true, + it("12 a full create with a custom category, a course and auto-take", async () => { + await create("12-full", { + name: "Full injection", + dose: "1 pen", + category: CUSTOM_KEY, + treatmentClass: "GLP1", + dosesPerUnit: 4, + unitsPerDose: 0.5, + reorderLeadDays: 7, + deliveryForm: "INJECTION", + trackInjectionSites: true, + allowedInjectionSites: ["ABDOMEN_LEFT", "THIGH_RIGHT"], + notificationsEnabled: false, + liveActivityEnabled: true, + criticalAlarmEnabled: true, + atcCode: "A10BJ06", + rxNormCode: "1991302", + startsOn: "2026-10-10", + endsOn: "2026-12-31", + autoMarkTaken: true, + schedules: [ + schedule("18:00", "20:00", { + label: "Evening", + dose: "0.5 pen", + unitsPerDose: 1, + timesOfDay: ["18:30"], + reminderGraceMinutes: 45, + rrule: "FREQ=WEEKLY;BYDAY=MO", + doseWindows: [{ timeOfDay: "18:30", start: "18:00", end: "20:00" }], + }), + ], + }); }); - await create("13b-one-shot", { - name: "One shot", - dose: "1 dose", - oneShot: true, - startsOn: "2026-10-20", - endsOn: "2026-11-20", - schedules: [schedule("09:00", "11:00")], - }); - await create("13c-prn-and-legacy-days", { - name: "Mixed schedules", - dose: "2mg", - trackIntake: false, - schedules: [ - schedule("06:00", "07:00", { scheduleType: "PRN" }), - schedule("21:00", "22:00", { daysOfWeek: [1, 3, 5], intervalWeeks: 2 }), - schedule("12:00", "13:00", { rollingIntervalDays: 3 }), - ], - }); - await create("13d-ended-window", { - name: "Ended window", - dose: "3mg", - endsOn: "2026-10-05", - schedules: [schedule("08:00", "09:00")], - }); - }); - it("14 the refusals", async () => { - await create("14a-unknown-category", { - name: "Unknown category", - dose: "1mg", - category: "custom:0a6c2d4e-1111-4222-8333-944455556666", - schedules: [schedule("08:00", "09:00")], + it("13 an as-needed create, a one-shot one and a PRN schedule", async () => { + await create("13a-as-needed", { + name: "As needed", + dose: "400mg", + category: "PAIN_RELIEF", + asNeeded: true, + }); + await create("13b-one-shot", { + name: "One shot", + dose: "1 dose", + oneShot: true, + startsOn: "2026-10-20", + endsOn: "2026-11-20", + schedules: [schedule("09:00", "11:00")], + }); + await create("13c-prn-and-legacy-days", { + name: "Mixed schedules", + dose: "2mg", + trackIntake: false, + schedules: [ + schedule("06:00", "07:00", { scheduleType: "PRN" }), + schedule("21:00", "22:00", { + daysOfWeek: [1, 3, 5], + intervalWeeks: 2, + }), + schedule("12:00", "13:00", { rollingIntervalDays: 3 }), + ], + }); + await create("13d-ended-window", { + name: "Ended window", + dose: "3mg", + endsOn: "2026-10-05", + schedules: [schedule("08:00", "09:00")], + }); }); - await create("14b-course-ends-before-start", { - name: "Backwards course", - dose: "1mg", - startsOn: "2026-10-20", - endsOn: "2026-10-10", - schedules: [schedule("08:00", "09:00")], - }); - await create("14c-one-shot-two-schedules", { - name: "One shot twice", - dose: "1mg", - oneShot: true, - startsOn: "2026-10-20", - schedules: [schedule("08:00", "09:00"), schedule("20:00", "21:00")], - }); - await create("14d-one-shot-recurrence", { - name: "One shot recurring", - dose: "1mg", - oneShot: true, - startsOn: "2026-10-20", - schedules: [schedule("08:00", "09:00", { rrule: "FREQ=DAILY" })], - }); - await create("14e-unstable-concept-id", { - name: "Phantom", - dose: "1mg", - externalSource: "APPLE_HEALTH", - externalId: "", - schedules: [schedule("08:00", "09:00")], - }); - await create("14f-invalid", { name: "", dose: 5, schedules: "daily" }); - }); - it("15 a mirrored create and its replay", async () => { - const mirrored = { - name: "Mirrored", - dose: "50mg", - externalSource: "APPLE_HEALTH", - externalId: "hk-concept-0001", - schedules: [schedule("08:00", "09:00")], - }; - await create("15a-mirror", mirrored); - await create("15b-mirror-replay", { ...mirrored, name: "Renamed" }); - }); - - it("16 the mirror cap", async () => { - const capped = await makeUser({ timezone: "UTC" }); - await prisma.medication.createMany({ - data: Array.from({ length: 30 }, (_, i) => ({ - userId: capped.id, - name: `Mirror ${String(i).padStart(2, "0")}`, + it("14 the refusals", async () => { + await create("14a-unknown-category", { + name: "Unknown category", dose: "1mg", - externalSource: "APPLE_HEALTH" as const, - externalId: `hk-cap-${String(i).padStart(2, "0")}`, - })), - }); - await create( - "16-mirror-cap", - { - name: "One too many", + category: "custom:0a6c2d4e-1111-4222-8333-944455556666", + schedules: [schedule("08:00", "09:00")], + }); + await create("14b-course-ends-before-start", { + name: "Backwards course", + dose: "1mg", + startsOn: "2026-10-20", + endsOn: "2026-10-10", + schedules: [schedule("08:00", "09:00")], + }); + await create("14c-one-shot-two-schedules", { + name: "One shot twice", + dose: "1mg", + oneShot: true, + startsOn: "2026-10-20", + schedules: [schedule("08:00", "09:00"), schedule("20:00", "21:00")], + }); + await create("14d-one-shot-recurrence", { + name: "One shot recurring", + dose: "1mg", + oneShot: true, + startsOn: "2026-10-20", + schedules: [schedule("08:00", "09:00", { rrule: "FREQ=DAILY" })], + }); + await create("14e-unstable-concept-id", { + name: "Phantom", dose: "1mg", externalSource: "APPLE_HEALTH", - externalId: "hk-cap-30", + externalId: "", schedules: [schedule("08:00", "09:00")], - }, - { auth: await makeSession(capped), userIds: [capped.id] }, - ); - }); + }); + await create("14f-invalid", { name: "", dose: 5, schedules: "daily" }); + }); - it("17 a retried create under one idempotency key", async () => { - const body = { - name: "Retried", - dose: "10mg", - schedules: [schedule("08:00", "09:00")], - }; - const headers = { - "idempotency-key": "6f9d2c4a-8b1e-4f3a-9d7c-5e2b1a0c9d8e", - }; - await create("17a-idempotent-first", body, { headers }); - await create("17b-idempotent-replay", body, { headers }); - }); + it("15 a mirrored create and its replay", async () => { + const mirrored = { + name: "Mirrored", + dose: "50mg", + externalSource: "APPLE_HEALTH", + externalId: "hk-concept-0001", + schedules: [schedule("08:00", "09:00")], + }; + await create("15a-mirror", mirrored); + await create("15b-mirror-replay", { ...mirrored, name: "Renamed" }); + }); - it("18 a delegate who may write, and one who may only read", async () => { - const body = { - name: "Brought by a delegate", - dose: "20mg", - schedules: [schedule("08:00", "09:00")], - }; - await create("18a-writer", body, writer); - await create("18b-reader", body, reader); - }); + it("16 the mirror cap", async () => { + const capped = await makeUser({ timezone: "UTC" }); + await prisma.medication.createMany({ + data: Array.from({ length: 30 }, (_, i) => ({ + userId: capped.id, + name: `Mirror ${String(i).padStart(2, "0")}`, + dose: "1mg", + externalSource: "APPLE_HEALTH" as const, + externalId: `hk-cap-${String(i).padStart(2, "0")}`, + })), + }); + await create( + "16-mirror-cap", + { + name: "One too many", + dose: "1mg", + externalSource: "APPLE_HEALTH", + externalId: "hk-cap-30", + schedules: [schedule("08:00", "09:00")], + }, + { auth: await makeSession(capped), userIds: [capped.id] }, + ); + }); - it("19 no credentials", async () => { - await create( - "19-anonymous", - { name: "Nobody", dose: "1mg" }, - { - auth: anonymous, - }, - ); + it("17 a retried create under one idempotency key", async () => { + const body = { + name: "Retried", + dose: "10mg", + schedules: [schedule("08:00", "09:00")], + }; + const headers = { + "idempotency-key": "6f9d2c4a-8b1e-4f3a-9d7c-5e2b1a0c9d8e", + }; + await create("17a-idempotent-first", body, { headers }); + await create("17b-idempotent-replay", body, { headers }); + }); + + it("18 a delegate who may write, and one who may only read", async () => { + const body = { + name: "Brought by a delegate", + dose: "20mg", + schedules: [schedule("08:00", "09:00")], + }; + await create("18a-writer", body, writer); + await create("18b-reader", body, reader); + }); + + it("19 no credentials", async () => { + await create( + "19-anonymous", + { name: "Nobody", dose: "1mg" }, + { + auth: anonymous, + }, + ); + }); }); }); diff --git a/tests/integration/routes/parity/mood-entries-bulk.parity.test.ts b/tests/integration/routes/parity/mood-entries-bulk.parity.test.ts index 6c4a79519..6f58473a1 100644 --- a/tests/integration/routes/parity/mood-entries-bulk.parity.test.ts +++ b/tests/integration/routes/parity/mood-entries-bulk.parity.test.ts @@ -214,7 +214,7 @@ afterAll(async () => { await prisma.moodTag.delete({ where: { id: SHORT_FACTOR_ID } }); }); -describe("POST /api/mood-entries/bulk parity", () => { +describe("POST /api/mood-entries/bulk parity", { shuffle: false }, () => { it("01 a first drain", async () => { await step("01-first-drain", { entries: firstDrain }); }); diff --git a/tests/integration/routes/parity/oidc-callback.parity.test.ts b/tests/integration/routes/parity/oidc-callback.parity.test.ts index 661d78770..c49bc1a85 100644 --- a/tests/integration/routes/parity/oidc-callback.parity.test.ts +++ b/tests/integration/routes/parity/oidc-callback.parity.test.ts @@ -270,7 +270,7 @@ afterAll(() => { } }); -describe("GET /api/auth/oidc/callback parity", () => { +describe("GET /api/auth/oidc/callback parity", { shuffle: false }, () => { it("01 SSO switched off", async () => { await step("01-disabled", { state: {} }); process.env.OIDC_ISSUER_URL = ISSUER; diff --git a/tests/integration/routes/parity/sync-changes.parity.test.ts b/tests/integration/routes/parity/sync-changes.parity.test.ts index d4d092ff9..c1c39b39e 100644 --- a/tests/integration/routes/parity/sync-changes.parity.test.ts +++ b/tests/integration/routes/parity/sync-changes.parity.test.ts @@ -298,7 +298,7 @@ afterAll(() => { vi.useRealTimers(); }); -describe("GET /api/sync/changes parity", () => { +describe("GET /api/sync/changes parity", { shuffle: false }, () => { let firstCursor: string; it("drains the feed in small pages to the end", async () => { diff --git a/tests/integration/routes/parity/tokens-mirror.parity.test.ts b/tests/integration/routes/parity/tokens-mirror.parity.test.ts index 141c811ec..5d69444d7 100644 --- a/tests/integration/routes/parity/tokens-mirror.parity.test.ts +++ b/tests/integration/routes/parity/tokens-mirror.parity.test.ts @@ -176,63 +176,67 @@ beforeAll(async () => { }); }); -describe("the Apple Health write-back of a measurement token", () => { - it("walks the cases in order", async () => { - await step("01-summary", { kind: "summary" }); - await step("02-on", { - kind: "patch", - id: scale.tokenId, - body: { mirrorToAppleHealth: true }, +describe( + "the Apple Health write-back of a measurement token", + { shuffle: false }, + () => { + it("walks the cases in order", async () => { + await step("01-summary", { kind: "summary" }); + await step("02-on", { + kind: "patch", + id: scale.tokenId, + body: { mirrorToAppleHealth: true }, + }); + await step("03-on-again", { + kind: "patch", + id: scale.tokenId, + body: { mirrorToAppleHealth: true }, + }); + await step("04-list", { kind: "list" }); + const revoked = await callRoute(revoke, { + method: "DELETE", + route: "/api/tokens/{id}", + params: { id: scale.tokenId }, + auth: owner, + }); + expect(revoked.status).toBe(200); + await step("05-off-revoked", { + kind: "patch", + id: scale.tokenId, + body: { mirrorToAppleHealth: false }, + }); + await step("06-wildcard", { + kind: "patch", + id: wildcard.tokenId, + body: { mirrorToAppleHealth: true }, + }); + await step("07-connector", { + kind: "patch", + id: connector.tokenId, + body: { mirrorToAppleHealth: true }, + }); + await step("08-another-account", { + kind: "patch", + id: theirs.tokenId, + body: { mirrorToAppleHealth: true }, + }); + await step("09-unknown", { + kind: "patch", + id: "missing-token", + body: { mirrorToAppleHealth: true }, + }); + await step("10-invalid-body", { + kind: "patch", + id: scale.tokenId, + body: { mirrorToAppleHealth: "yes", name: "Renamed" }, + }); + await step("11-bearer", { + kind: "patch", + id: scale.tokenId, + body: { mirrorToAppleHealth: true }, + auth: wildcard, + }); + await step("12-summary-bearer", { kind: "summary", auth: wildcard }); }); - await step("03-on-again", { - kind: "patch", - id: scale.tokenId, - body: { mirrorToAppleHealth: true }, - }); - await step("04-list", { kind: "list" }); - const revoked = await callRoute(revoke, { - method: "DELETE", - route: "/api/tokens/{id}", - params: { id: scale.tokenId }, - auth: owner, - }); - expect(revoked.status).toBe(200); - await step("05-off-revoked", { - kind: "patch", - id: scale.tokenId, - body: { mirrorToAppleHealth: false }, - }); - await step("06-wildcard", { - kind: "patch", - id: wildcard.tokenId, - body: { mirrorToAppleHealth: true }, - }); - await step("07-connector", { - kind: "patch", - id: connector.tokenId, - body: { mirrorToAppleHealth: true }, - }); - await step("08-another-account", { - kind: "patch", - id: theirs.tokenId, - body: { mirrorToAppleHealth: true }, - }); - await step("09-unknown", { - kind: "patch", - id: "missing-token", - body: { mirrorToAppleHealth: true }, - }); - await step("10-invalid-body", { - kind: "patch", - id: scale.tokenId, - body: { mirrorToAppleHealth: "yes", name: "Renamed" }, - }); - await step("11-bearer", { - kind: "patch", - id: scale.tokenId, - body: { mirrorToAppleHealth: true }, - auth: wildcard, - }); - await step("12-summary-bearer", { kind: "summary", auth: wildcard }); - }); -}); + }, +); diff --git a/tests/integration/routes/parity/workout-detail.parity.test.ts b/tests/integration/routes/parity/workout-detail.parity.test.ts index 3b5e8debf..bb4ee814b 100644 --- a/tests/integration/routes/parity/workout-detail.parity.test.ts +++ b/tests/integration/routes/parity/workout-detail.parity.test.ts @@ -248,64 +248,68 @@ afterAll(() => { vi.useRealTimers(); }); -describe("GET /api/workouts/{id} parity", () => { - it("the detail reads", async () => { - await step("get-01-canonical", { method: "GET", id: ids.run }); - await step("get-02-compact", { - method: "GET", - id: ids.run, - query: { compact: "1" }, +// One walk: each request sees what the earlier ones stored, so the cases +// keep their order even when a run shuffles (`shuffle: false`). +describe("workout-detail parity walk", { shuffle: false }, () => { + describe("GET /api/workouts/{id} parity", () => { + it("the detail reads", async () => { + await step("get-01-canonical", { method: "GET", id: ids.run }); + await step("get-02-compact", { + method: "GET", + id: ids.run, + query: { compact: "1" }, + }); + await step("get-03-hidden-twin", { method: "GET", id: ids.twin }); + await step("get-04-ride", { method: "GET", id: ids.ride }); + await step("get-05-other-account", { + method: "GET", + id: ids.run, + auth: stranger, + }); + await step("get-06-missing", { + method: "GET", + id: "cmissingworkout000000000", + }); }); - await step("get-03-hidden-twin", { method: "GET", id: ids.twin }); - await step("get-04-ride", { method: "GET", id: ids.ride }); - await step("get-05-other-account", { - method: "GET", - id: ids.run, - auth: stranger, - }); - await step("get-06-missing", { - method: "GET", - id: "cmissingworkout000000000", + + it("the module switched off", async () => { + await prisma.user.update({ + where: { id: userId }, + data: { modulePreferencesJson: { workouts: false } }, + }); + await step("get-07-module-off", { method: "GET", id: ids.run }); + await prisma.user.update({ + where: { id: userId }, + data: { modulePreferencesJson: {} }, + }); }); }); - it("the module switched off", async () => { - await prisma.user.update({ - where: { id: userId }, - data: { modulePreferencesJson: { workouts: false } }, + describe("DELETE /api/workouts/{id} parity", () => { + it("a synced workout, a missing one, another account's", async () => { + await step("delete-01-synced", { + method: "DELETE", + id: ids.ride, + readBack: true, + }); + await step("delete-02-missing", { + method: "DELETE", + id: "cmissingworkout000000000", + }); + await step("delete-03-other-account", { + method: "DELETE", + id: ids.walk, + auth: stranger, + readBack: true, + }); }); - await step("get-07-module-off", { method: "GET", id: ids.run }); - await prisma.user.update({ - where: { id: userId }, - data: { modulePreferencesJson: {} }, - }); - }); -}); -describe("DELETE /api/workouts/{id} parity", () => { - it("a synced workout, a missing one, another account's", async () => { - await step("delete-01-synced", { - method: "DELETE", - id: ids.ride, - readBack: true, - }); - await step("delete-02-missing", { - method: "DELETE", - id: "cmissingworkout000000000", - }); - await step("delete-03-other-account", { - method: "DELETE", - id: ids.walk, - auth: stranger, - readBack: true, - }); - }); - - it("a hand-entered workout and the record it set", async () => { - await step("delete-04-manual", { - method: "DELETE", - id: ids.walk, - readBack: true, + it("a hand-entered workout and the record it set", async () => { + await step("delete-04-manual", { + method: "DELETE", + id: ids.walk, + readBack: true, + }); }); }); }); diff --git a/tests/integration/routes/parity/workouts-batch.parity.test.ts b/tests/integration/routes/parity/workouts-batch.parity.test.ts index 66b2ff8b6..ae4e77588 100644 --- a/tests/integration/routes/parity/workouts-batch.parity.test.ts +++ b/tests/integration/routes/parity/workouts-batch.parity.test.ts @@ -238,7 +238,7 @@ afterAll(() => { vi.useRealTimers(); }); -describe("POST /api/workouts/batch parity", () => { +describe("POST /api/workouts/batch parity", { shuffle: false }, () => { it("01 a first batch", async () => { await step("01-first-batch", { workouts: FIRST_BATCH }); });