From a620a730b90b0cc7314f39e1342400971f3d06c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 6 Aug 2026 02:04:04 +0200 Subject: [PATCH] test: extend sharing boundary guards Cover provider work authority and import actor attribution. --- src/__tests__/sharing-surface-guard.test.ts | 115 ++++++++++++++++++-- 1 file changed, 105 insertions(+), 10 deletions(-) diff --git a/src/__tests__/sharing-surface-guard.test.ts b/src/__tests__/sharing-surface-guard.test.ts index 8f1ecd56d..2b86a2efe 100644 --- a/src/__tests__/sharing-surface-guard.test.ts +++ b/src/__tests__/sharing-surface-guard.test.ts @@ -1148,10 +1148,18 @@ const DELEGABLE_ROUTES: Record = { domain: "medications", why: "Importing a dose history for one medication. Reached only through this module's MANAGE arm; the argument for admitting it is the manage literal's reason line, and the entry here is what leg (a) freezes and leg (f) checks the section against.", }, + "app/api/medications/[id]/intake/import/[jobId]/status/route.ts": { + domain: "medications", + why: "Polling the one-medication import the manager admitted. The row stays scoped to the record and medication, so its progress is part of the same MANAGE capability rather than an actor-only job lookup.", + }, "app/api/medications/intake/dose-history-import/route.ts": { domain: "medications", why: "The whole-regimen form of the same import, same worker, same properties. Reached only through this module's MANAGE arm; the argument for admitting it is the manage literal's reason line, and the entry here is what leg (a) freezes and leg (f) checks the section against.", }, + "app/api/medications/intake/dose-history-import/[jobId]/status/route.ts": { + domain: "medications", + why: "Polling the account-wide import the manager admitted. The row remains record-scoped, while the null medication scope prevents resolving a one-medication job through this route.", + }, "app/api/nutrients/water/route.ts": { domain: "measurements", why: "Logging hydration. Reached only through this module's MANAGE arm; the argument for admitting it is the manage literal's reason line, and the entry here is what leg (a) freezes and leg (f) checks the section against.", @@ -1553,11 +1561,21 @@ const DELEGABLE_MANAGE_ROUTES: Record = { conditions: ["C1", "C9"], why: "Importing a dose history for one medication. Additive with duplicates skipped and honest provenance. C1, C9.", }, + "app/api/medications/[id]/intake/import/[jobId]/status/route.ts": { + domain: "medications", + conditions: [], + why: "Polling a one-medication import requires the same record MANAGE capability that admitted it, but creates no data or audit row.", + }, "app/api/medications/intake/dose-history-import/route.ts": { domain: "medications", conditions: ["C1", "C9"], why: "The whole-regimen form of the same import, same worker, same properties. C1, C9.", }, + "app/api/medications/intake/dose-history-import/[jobId]/status/route.ts": { + domain: "medications", + conditions: [], + why: "Polling an account-wide import requires the same record MANAGE capability that admitted it, but creates no data or audit row.", + }, "app/api/nutrients/water/route.ts": { domain: "measurements", conditions: ["C1", "C4"], @@ -1765,9 +1783,10 @@ const ACTOR_ROUTES: Record = { * literal is still empty, so the number moves again when it fills. It is one * number rather than one per list on purpose: filling either of the new * literals collides here by construction, so two people filling them at once - * find out from the merge rather than from production. + * find out from the merge rather than from production. Import-status polling + * adds two record entries and two MANAGE entries: 190 → 194. */ -const FROZEN_ENTRY_COUNT = 190; +const FROZEN_ENTRY_COUNT = 194; /** * The two surfaces that authenticate a Bearer token outside `requireAuth` — @@ -2697,14 +2716,11 @@ describe("(g) the MANAGE route set is frozen", () => { } }); - it("C5 is enforced at the enqueue, not at the ten call sites", () => { - // The condition that costs the owner money if it is wrong, and the one - // place it is implemented. Ten frozen entries carry C5; nine of them - // reach the provider through `resolveReadOnlyStatusMiss` and the tenth - // (the narrative) enqueues in its own handler. Asserting the choke point - // rather than the ten modules is deliberate — a generator added later - // inherits the suppression without knowing it exists — so the anchor has - // to be exactly here. + it("C5 keeps its read-path suppression and queue authority boundary", () => { + // The C5 request-path check still prevents work before an old status or + // narrative caller reaches the queue. The queue now repeats that refusal + // with a bounded authority envelope, so an invalidation path or injected + // payload cannot turn a delegated record mutation into provider egress. expect(manageModulesWith("C5").length).toBe(10); const miss = functionSource( @@ -2729,6 +2745,85 @@ describe("(g) the MANAGE route set is frozen", () => { ); expect(resolver.length).toBeGreaterThan(0); expect(resolver).toContain("setDelegatedGenerationSuppressed"); + expect(resolver).toContain("setProviderWorkAuthority"); + }); + + it("provider work validates every admission and dispatch boundary", () => { + // Status invalidation is a mutation-only path outside the status-cache + // miss. It must use the same shared admission point, not send directly. + expect( + callsSymbol( + "lib/insights/status-invalidation.ts", + "enqueueStatusGeneration", + ), + ).toBe(true); + + const statusAdmission = functionSource( + "lib/jobs/insight-status-generate-shared.ts", + "enqueueStatusGeneration", + ); + expect(statusAdmission.length).toBeGreaterThan(0); + expect(statusAdmission).toContain("providerWorkAuthorityForRecord"); + expect(statusAdmission).toContain("mayEnqueueProviderWork"); + expect(statusAdmission).toContain("authority"); + + const statusDispatch = functionSource( + "lib/jobs/insight-status-generate.ts", + "runInsightStatusGenerate", + ); + expect(statusDispatch.length).toBeGreaterThan(0); + expect(statusDispatch).toContain("mayDispatchProviderWork"); + expect(statusDispatch).toContain("withProviderWorkAuthority"); + + const narrativeDispatch = functionSource( + "lib/jobs/period-narrative-warm.ts", + "warmOneNarrative", + ); + expect(narrativeDispatch.length).toBeGreaterThan(0); + expect(narrativeDispatch).toContain("mayDispatchProviderWork"); + expect(narrativeDispatch).toContain("withProviderWorkAuthority"); + + // A shared comprehensive read calls the resolver even before it decides + // whether there is enough context to generate. Credential policy belongs + // inside every resolver helper so a delegate or managed system job cannot + // select a personal BYOK key or custom base URL first. + expect( + callsSymbol("app/api/insights/comprehensive/route.ts", "resolveProvider"), + ).toBe(true); + for (const name of [ + "resolveProvider", + "resolveProviderChain", + "hasAnyConfiguredProvider", + "resolveProviderAvailability", + ]) { + const resolver = functionSource("lib/ai/provider.ts", name); + expect(resolver.length, `${name} not found`).toBeGreaterThan(0); + expect(resolver, `${name} ignores sharing authority`).toContain( + "providerCredentialPolicy", + ); + } + }); + + it("import completion retains bounded record and actor attribution", () => { + const admission = functionSource( + "lib/medications/intake-import-admission.ts", + "admitIntakeImportJob", + ); + expect(admission.length).toBeGreaterThan(0); + expect(admission).toContain("recordUserId"); + expect(admission).toContain("actorUserId"); + + const completion = functionSource( + "lib/jobs/medication-intake-import.ts", + "processNextChunk", + ); + expect(completion.length).toBeGreaterThan(0); + expect(completion).toContain('auditLog("medication.intake.import"'); + expect(completion).toContain("client: tx"); + expect(completion).toContain("recordUserId: row.recordUserId"); + expect(completion).toContain("actorUserId: row.actorUserId"); + expect(completion).toContain("jobId: row.id"); + expect(completion).not.toContain("tx.auditLog.create"); }); it("the remaining conditions are carried where they were argued", () => {