diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index bc6ab4873..13cf43e0f 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -44505,7 +44505,8 @@ components: description: "Whether the shared provider may honestly be offered to this caller in one tap. True only when all of: the operator's provider is the one that would serve them (`managedBy: \"server\"`), `serverProviderHealth` is `healthy`, the operator's assistant master + coach flags are on, the acting record holds its own credentials - (a managed profile never does), and the caller holds no receipt yet. Anything unknown makes it false." + (a managed profile never does), the caller holds no receipt yet, and the instance is not running in demo + mode (where the grant the tap makes is refused at the edge). Anything unknown makes it false." serverProviderConsent: type: boolean description: Whether this caller already holds an active `ai_full` / `ai_coach` receipt — the grant `POST diff --git a/src/app/api/user/ai-provider/__tests__/route.test.ts b/src/app/api/user/ai-provider/__tests__/route.test.ts index fbef38c9c..865ac3180 100644 --- a/src/app/api/user/ai-provider/__tests__/route.test.ts +++ b/src/app/api/user/ai-provider/__tests__/route.test.ts @@ -563,4 +563,26 @@ describe("GET /api/user/ai-provider — the shared-provider offer", () => { expect(vi.mocked(readServerProviderHealth)).toHaveBeenCalledTimes(1); expect(vi.mocked(readServerProviderHealth)).toHaveBeenCalledWith(); }); + + it("never offers on a demo instance, where the grant cannot be made", async () => { + // The one tap posts `POST /api/consent/ai/web`, which the demo's edge + // allowlist refuses — and should: the demo is one shared account, so a + // receipt minted by one visitor would turn the operator's provider on + // for every later one. Offering a button that can only 403 into a + // generic toast is the misleading failure this screen exists to remove, + // so the offer is withheld and the screen falls to its neutral panel. + const previous = process.env.DEMO_MODE; + process.env.DEMO_MODE = "true"; + try { + serverManaged(); + vi.mocked(readServerProviderHealth).mockResolvedValue("healthy"); + await expect(read()).resolves.toMatchObject({ + serverProviderHealth: "healthy", + serverProviderOffer: false, + }); + } finally { + if (previous === undefined) delete process.env.DEMO_MODE; + else process.env.DEMO_MODE = previous; + } + }); }); diff --git a/src/app/api/user/ai-provider/route.ts b/src/app/api/user/ai-provider/route.ts index 9fbe13e47..120e84bd9 100644 --- a/src/app/api/user/ai-provider/route.ts +++ b/src/app/api/user/ai-provider/route.ts @@ -87,7 +87,15 @@ export const GET = apiHandler(async () => { providerWorkAuthorityForRecord(user.id), u?.managedProfileAt ?? null, ); + // A demo instance can show the offer but cannot honour it: the one tap + // posts `POST /api/consent/ai/web`, which the demo's edge allowlist + // refuses — and should, since the demo is one shared account and a receipt + // one visitor minted would turn the operator's provider on for every later + // one. A button that can only 403 into a generic toast is precisely the + // misleading failure this surface exists to remove, so there is no button. + const demoInstance = process.env.DEMO_MODE === "true"; const serverProviderOffer = + !demoInstance && managedBy === "server" && serverProviderHealth === "healthy" && assistantFlags.enabled && diff --git a/src/lib/openapi/routes/profile.ts b/src/lib/openapi/routes/profile.ts index 246e215cf..067555113 100644 --- a/src/lib/openapi/routes/profile.ts +++ b/src/lib/openapi/routes/profile.ts @@ -572,7 +572,7 @@ const aiProviderResponse = z serverProviderOffer: z .boolean() .describe( - "Whether the shared provider may honestly be offered to this caller in one tap. True only when all of: the operator's provider is the one that would serve them (`managedBy: \"server\"`), `serverProviderHealth` is `healthy`, the operator's assistant master + coach flags are on, the acting record holds its own credentials (a managed profile never does), and the caller holds no receipt yet. Anything unknown makes it false.", + "Whether the shared provider may honestly be offered to this caller in one tap. True only when all of: the operator's provider is the one that would serve them (`managedBy: \"server\"`), `serverProviderHealth` is `healthy`, the operator's assistant master + coach flags are on, the acting record holds its own credentials (a managed profile never does), the caller holds no receipt yet, and the instance is not running in demo mode (where the grant the tap makes is refused at the edge). Anything unknown makes it false.", ), serverProviderConsent: z .boolean()