Files
HealthLog/tests/integration/sharing-provider-origin.test.ts
T
Marc-André Bombeck c043a77429 test: wire every release journey to the tests that carry it
The eight journeys were enumerated and one of them was wired. The other
seven existed as claims in a fixture file, which is the gap the enumeration
was written to close: a browser suite reports what it ran, never what it did
not, and a journey nobody wired reads exactly like a journey that passed.

All eight are wired now, and the list is checked in both directions — an
enumerated journey missing from the wiring fails, not just a wiring entry
naming a journey nobody declared. Before this, a subset was legal, so a
dropped journey and a not-yet-written one looked the same.

A journey names more than one file where more than one file carries it. J1
claims a grant runs through invite, accept, switch, expiry, revoke and a
direct URL; the browser proves the switch, what each level opens and the
direct-URL refusal, and the transitions either side are database facts
proved against real Postgres. Folding that into one file would mean a
browser spec driving expiry through the clock or an integration file
asserting what a page renders, and both are worse tests than the two that
exist.

Three journeys ride Playwright and five ride Vitest, so a green run of this
guard means all eight are claimed and here is which have executed in the
ordinary gate — never that all eight passed. The runner is derived from the
path rather than trusted, so it cannot become a label set to whichever value
made the split read better.

The extractor now reads `it(` as well as `test(`, and the parameterised
`it.each(table)("title")` form alongside the single-call one. It saw neither
before: five of the journeys are proved by Vitest files, every per-file leg
is over a matched set, and an empty match set satisfied them all. The
synthetic spec that proves the parse grew a Vitest half, and it is what
caught the parameterised form being invisible. What the matcher still cannot
see is written into its own doc comment.
2026-08-07 06:11:36 +02:00

404 lines
12 KiB
TypeScript

/**
* Provider work keeps the record subject and initiating authority together.
*
* A shared-record mutation can invalidate a generated assessment. The queue
* boundary must therefore refuse delegate work before it is admitted and the
* worker must refuse it again if an invalid payload reaches dispatch. These
* cases exercise both boundaries with the live record resolver and grants.
*/
import { NextRequest, NextResponse } from "next/server";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { cookieJar, headerJar } from "./mock-next-headers";
import { getPrismaClient, truncateAllTables } from "./setup";
const send = vi.fn(async () => "provider-origin-job");
vi.mock("next/headers", async () => {
const { cookieJar, headerJar } = await import("./mock-next-headers");
return {
headers: vi.fn(async () => ({
get: (name: string) => headerJar.get(name.toLowerCase()) ?? null,
})),
cookies: vi.fn(async () => ({
get: (name: string) => {
const value = cookieJar.get(name);
return value ? { name, value } : undefined;
},
})),
};
});
vi.mock("@/lib/jobs/boss-instance", () => ({
getGlobalBoss: vi.fn(() => ({ send })),
}));
vi.mock("@/lib/db-compat", () => ({
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("@/lib/feature-flags", () => ({
getAssistantFlags: vi.fn().mockResolvedValue({
briefing: true,
insightStatus: false,
}),
}));
import {
ACCOUNT_SELECTOR_HEADER,
apiHandler,
requireRecordAuth,
} from "@/lib/api-handler";
import { hashToken } from "@/lib/auth/hmac";
import {
providerCredentialPolicy,
type ProviderWorkAuthority,
withProviderWorkAuthority,
} from "@/lib/sharing/provider-work-authority";
import { runInsightStatusGenerate } from "@/lib/jobs/insight-status-generate";
import type { InsightStatusGeneratePayload } from "@/lib/jobs/insight-status-generate";
import {
hasAnyConfiguredProvider,
resolveProvider,
resolveProviderAvailability,
resolveProviderChain,
} from "@/lib/ai/provider";
import { invalidateStatusInsightsForTypes } from "@/lib/insights/status-invalidation";
import { warmOneNarrative } from "@/lib/jobs/period-narrative-warm";
const PROVIDER_ORIGIN_KEY = "provider-origin-test-key";
let tokenNumber = 0;
async function createUser(id: string, managed = false): Promise<void> {
await getPrismaClient().user.create({
data: {
id,
username: `provider-${id}`,
email: `provider-${id}@example.test`,
role: "USER",
...(managed ? { managedProfileAt: new Date() } : {}),
},
});
}
async function mintToken(userId: string): Promise<string> {
const raw = `${PROVIDER_ORIGIN_KEY}-${tokenNumber++}`.padEnd(28, "0");
await getPrismaClient().apiToken.create({
data: {
userId,
name: "provider-origin-test",
tokenHash: hashToken(raw),
permissions: ["*"],
},
});
return raw;
}
async function grantManage(input: {
id: string;
recordUserId: string;
actorUserId: string;
}): Promise<void> {
await getPrismaClient().accountGrant.create({
data: {
id: input.id,
grantorId: input.recordUserId,
granteeId: input.actorUserId,
access: "MANAGE",
acceptedAt: new Date(Date.now() - 60_000),
},
});
}
const enqueueFromSharedMutation: (request: NextRequest) => Promise<Response> =
apiHandler(async () => {
const { user } = await requireRecordAuth("manage", "medications");
await invalidateStatusInsightsForTypes(user.id, ["WEIGHT"]);
return NextResponse.json({ data: { queued: true }, error: null });
});
const resolveFromSharedRead: (request: NextRequest) => Promise<Response> =
apiHandler(async () => {
const { user } = await requireRecordAuth("manage", "record");
const provider = await resolveProvider(user.id);
return NextResponse.json({
data: { provider: provider.type },
error: null,
});
});
function runMutation(): Promise<Response> {
return enqueueFromSharedMutation(
new NextRequest("http://localhost/api/test/provider-origin", {
method: "POST",
}),
);
}
function runSharedRead(): Promise<Response> {
return resolveFromSharedRead(
new NextRequest("http://localhost/api/test/provider-origin-read"),
);
}
function authority(input: ProviderWorkAuthority): ProviderWorkAuthority {
return input;
}
beforeEach(async () => {
await truncateAllTables(getPrismaClient());
cookieJar.clear();
headerJar.clear();
send.mockClear();
tokenNumber = 0;
});
describe("sharing provider origin", () => {
it("[J6-no-delegated-provider-egress] enqueues and dispatches an owner-origin mutation as the positive control", async () => {
await createUser("provider-owner");
headerJar.set(
"authorization",
`Bearer ${await mintToken("provider-owner")}`,
);
const response = await runMutation();
expect(response.status).toBe(200);
expect(send).toHaveBeenCalledTimes(3);
const payload = (
send.mock.calls as unknown as Array<
[string, InsightStatusGeneratePayload, unknown]
>
)[0]![1];
expect(payload.userId).toBe("provider-owner");
expect(payload.authority).toEqual(
authority({
origin: "owner",
recordUserId: "provider-owner",
actorUserId: "provider-owner",
grantId: null,
}),
);
const generate = vi.fn().mockResolvedValue(undefined);
await runInsightStatusGenerate(payload, {
general: vi.fn(),
"blood-pressure": vi.fn(),
weight: generate,
pulse: vi.fn(),
bmi: vi.fn(),
mood: vi.fn(),
"medication-compliance": vi.fn(),
});
expect(generate).toHaveBeenCalledWith("provider-owner", {
locale: "en",
force: true,
});
});
it("[J6-no-delegated-provider-egress] enqueues and dispatches no provider work for a delegate, including a mutation-to-red payload", async () => {
await createUser("provider-record");
await createUser("provider-delegate");
await grantManage({
id: "provider-delegated-grant",
recordUserId: "provider-record",
actorUserId: "provider-delegate",
});
headerJar.set(
"authorization",
`Bearer ${await mintToken("provider-delegate")}`,
);
headerJar.set(ACCOUNT_SELECTOR_HEADER, "provider-record");
const response = await runMutation();
expect(response.status).toBe(200);
expect(send).not.toHaveBeenCalled();
const generate = vi.fn().mockResolvedValue(undefined);
await runInsightStatusGenerate(
{
userId: "provider-record",
metric: "weight",
locale: "en",
authority: authority({
origin: "delegate",
recordUserId: "provider-record",
actorUserId: "provider-delegate",
grantId: "provider-delegated-grant",
}),
},
{
general: vi.fn(),
"blood-pressure": vi.fn(),
weight: generate,
pulse: vi.fn(),
bmi: vi.fn(),
mood: vi.fn(),
"medication-compliance": vi.fn(),
},
);
expect(generate).not.toHaveBeenCalled();
const narrativeGenerate = vi.fn().mockResolvedValue(null);
await warmOneNarrative(
{
userId: "provider-record",
period: "week",
authority: authority({
origin: "delegate",
recordUserId: "provider-record",
actorUserId: "provider-delegate",
grantId: "provider-delegated-grant",
}),
},
narrativeGenerate,
);
expect(narrativeGenerate).not.toHaveBeenCalled();
});
it("keeps a managed-profile Guardian on operator/default provider configuration", async () => {
const guardian = authority({
origin: "guardian",
recordUserId: "managed-record",
actorUserId: "guardian-actor",
grantId: "guardian-grant",
});
expect(providerCredentialPolicy(guardian)).toBe("operator-default");
expect(
providerCredentialPolicy(
authority({
origin: "delegate",
recordUserId: "adult-record",
actorUserId: "adult-delegate",
grantId: "adult-grant",
}),
),
).toBe("deny");
await getPrismaClient().user.create({
data: {
id: "managed-record",
username: "managed-record",
email: "managed-record@example.test",
role: "USER",
managedProfileAt: new Date(),
aiProvider: "LOCAL",
aiBaseUrl: "https://guardian-byok.example.test/v1",
},
});
await createUser("guardian-actor");
await grantManage({
id: "guardian-grant",
recordUserId: "managed-record",
actorUserId: "guardian-actor",
});
const provider = await withProviderWorkAuthority(guardian, () =>
resolveProvider("managed-record"),
);
expect(provider.type).toBe("none");
});
it("returns no personal provider from the shared comprehensive-read origin", async () => {
await getPrismaClient().user.create({
data: {
id: "provider-read-record",
username: "provider-read-record",
email: "provider-read-record@example.test",
role: "USER",
aiProvider: "LOCAL",
aiBaseUrl: "https://llm.example.test/v1",
},
});
await createUser("provider-read-delegate");
await grantManage({
id: "provider-read-grant",
recordUserId: "provider-read-record",
actorUserId: "provider-read-delegate",
});
headerJar.set(
"authorization",
`Bearer ${await mintToken("provider-read-record")}`,
);
const owner = await runSharedRead();
expect((await owner.json()).data.provider).not.toBe("none");
headerJar.set(
"authorization",
`Bearer ${await mintToken("provider-read-delegate")}`,
);
headerJar.set(ACCOUNT_SELECTOR_HEADER, "provider-read-record");
const delegated = await runSharedRead();
expect((await delegated.json()).data.provider).toBe("none");
});
it("allows explicit system narrative work as the nightly positive control", async () => {
await createUser("provider-nightly");
const narrativeGenerate = vi.fn().mockResolvedValue(null);
await warmOneNarrative(
{
userId: "provider-nightly",
period: "week",
locale: "en",
authority: authority({
origin: "system",
recordUserId: "provider-nightly",
actorUserId: null,
grantId: null,
}),
},
narrativeGenerate,
);
expect(narrativeGenerate).toHaveBeenCalledWith("provider-nightly", {
period: "week",
locale: "en",
force: true,
});
});
it("keeps system work for a managed record off stale personal providers", async () => {
await getPrismaClient().user.create({
data: {
id: "managed-system-record",
username: "managed-system-record",
email: "managed-system-record@example.test",
role: "USER",
managedProfileAt: new Date(),
aiProvider: "LOCAL",
aiBaseUrl: "https://stale-managed-provider.example.test/v1",
},
});
const system = authority({
origin: "system",
recordUserId: "managed-system-record",
actorUserId: null,
grantId: null,
});
const [provider, chain, configured, availability] =
await withProviderWorkAuthority(system, async () =>
Promise.all([
resolveProvider("managed-system-record"),
resolveProviderChain("managed-system-record"),
hasAnyConfiguredProvider("managed-system-record"),
resolveProviderAvailability("managed-system-record"),
]),
);
expect(provider.type).toBe("none");
expect(chain).toEqual([]);
expect(configured).toBe(false);
expect(availability).toEqual({ aiAvailable: false, managedBy: null });
});
});