fix(onboarding): the AI panel claims an outage only when one was seen

The done screen folded the `unknown` provider verdict into `unhealthy` and
painted "This instance's provider is not answering right now." `unknown` is
an empty `provider_health` table, which is the state of every instance until
its first AI call — so the most common path through the screen made a claim
about something nobody had measured. It now falls to the neutral panel, which
says nothing either way; the offer is withheld in both cases as before.

The projection itself drew the same unearned conclusion from staleness alone:
a quiet instance whose last success was two days old read `unhealthy` without
any failure behind it. A verdict now needs an observed one — a row whose last
outcome was a failure — and a merely old success reads `unknown`.
This commit is contained in:
Marc-André Bombeck committed 2026-09-11 10:53:41 +02:00
1 parent 1885b7867d
commit 65a62350ea
6 files changed
+92 -41

No files matched your search

@@ -186,24 +186,41 @@ describe("<DoneScreen> shared-provider offer", () => {
expect(html).not.toContain('data-slot="onboarding-ai-unavailable"');
});
it.each(["unhealthy", "unknown"] as const)(
"says the provider is not answering on %s, and offers no button",
(health) => {
aiProviderState.data = {
managedBy: "server",
serverProviderHealth: health,
serverProviderOffer: false,
serverProviderConsent: false,
};
const html = render();
expect(html).toContain('data-slot="onboarding-ai-unavailable"');
expect(html).not.toContain('data-slot="onboarding-ai-offer-grant"');
expect(html).not.toContain('data-slot="onboarding-ai-shared-key"');
// The rest of the panel is untouched in every branch.
expect(html).toContain('data-slot="onboarding-ai-keyless"');
expect(html).toContain('href="/settings/ai"');
},
);
it("says the provider is not answering only when a failure earned it", () => {
aiProviderState.data = {
managedBy: "server",
serverProviderHealth: "unhealthy",
serverProviderOffer: false,
serverProviderConsent: false,
};
const html = render();
expect(html).toContain('data-slot="onboarding-ai-unavailable"');
expect(html).not.toContain('data-slot="onboarding-ai-offer-grant"');
expect(html).not.toContain('data-slot="onboarding-ai-shared-key"');
// The rest of the panel is untouched in every branch.
expect(html).toContain('data-slot="onboarding-ai-keyless"');
expect(html).toContain('href="/settings/ai"');
});
it("claims nothing either way when nobody has measured the provider", () => {
// `unknown` is an empty `provider_health` table — the state of every
// instance until its first AI call, and the most common way through this
// screen. Saying "not answering" there is a claim about something nobody
// measured, which is the one thing this panel exists not to do.
aiProviderState.data = {
managedBy: "server",
serverProviderHealth: "unknown",
serverProviderOffer: false,
serverProviderConsent: false,
};
const html = render();
expect(html).not.toContain('data-slot="onboarding-ai-unavailable"');
expect(html).not.toContain('data-slot="onboarding-ai-offer-grant"');
expect(html).not.toContain('data-slot="onboarding-ai-shared-key"');
// The neutral panel is the whole panel, minus any claim about a provider.
expect(html).toContain('data-slot="onboarding-ai-keyless"');
expect(html).toContain('href="/settings/ai"');
});
it("keeps the shared-key note for the one state it was ever true of", () => {
aiProviderState.data = {
+14 -9
View File
@@ -35,8 +35,9 @@ interface AiProviderStatus {
/** Origin of the provider that would serve this user, if any. */
managedBy?: "user" | "local" | "server" | null;
/**
* v1.38.19 (wave D) — the instance-wide tri-state. Fail closed: `unknown`
* is treated exactly like `unhealthy`, here and everywhere.
* v1.38.19 (wave D) — the instance-wide tri-state. Fail closed for the
* offer: neither `unhealthy` nor `unknown` earns one. Only `unhealthy` is
* a verdict this screen may state out loud.
*/
serverProviderHealth?: "healthy" | "unhealthy" | "unknown";
/** Whether the shared provider may honestly be offered in one tap. */
@@ -174,17 +175,21 @@ export function DoneScreen({ state }: { state: OnboardingStateDto }) {
operatorProvides &&
!consentOnFile &&
aiProvider?.serverProviderOffer === true;
// Only a health verdict paints the "not answering" card. When the offer is
// refused for a reason that is not health — the operator switched the
// assistant surfaces off, or this is a managed profile whose provider is
// the operator's by definition — the panel falls back to its neutral form
// and claims nothing either way.
// Only a health verdict somebody EARNED paints the "not answering" card —
// `unhealthy`, which means the provider was tried and the last thing that
// happened was a failure. `unknown` is not that verdict: it is an empty
// ledger, which is the state of every instance until its first AI call and
// therefore the most common way through this screen. It falls to the
// neutral panel, as does every other non-health reason the offer is
// withheld (the assistant surfaces switched off, a managed profile whose
// provider is the operator's by definition). The neutral panel claims
// nothing either way, which is the only honest thing to say about a state
// nobody measured.
const sharedUnavailable =
operatorProvides &&
!consentOnFile &&
!offerShared &&
(aiProvider?.serverProviderHealth === "unhealthy" ||
aiProvider?.serverProviderHealth === "unknown");
aiProvider?.serverProviderHealth === "unhealthy";
return (
<section