fix(settings): never report a failed consent read as a withdrawal

The AI consent card turned any failed read of the receipt (a rate limit, a
network drop) into "Withdrawn" and offered a grant. It now says the read
failed, with a retry. The Coach history note is one sentence.
This commit is contained in:
Marc-André Bombeck committed 2026-09-24 11:36:29 +02:00
1 parent a6c65bd37e
commit e72441743f
9 files changed
+52 -9

No files matched your search

@@ -75,6 +75,35 @@ describe("AiConsentCard", () => {
expect(html).toContain('data-slot="ai-consent-grant"');
});
it("never reports a failed read as a withdrawal", () => {
// A refused or failed read (the consent bucket's rate limit, a network
// drop) says nothing about the decision. Reporting it as "Withdrawn" and
// offering a grant would misstate the person's own choice.
const client = new QueryClient({
defaultOptions: { queries: { retry: false, retryOnMount: false } },
});
client
.getQueryCache()
.build(client, { queryKey: receiptKey })
.setState({
status: "error",
error: new Error("read failed"),
data: undefined,
fetchStatus: "idle",
});
const html = renderToStaticMarkup(
<QueryClientProvider client={client}>
<I18nProvider initialLocale="en">
<AiConsentCard isAuthenticated />
</I18nProvider>
</QueryClientProvider>,
);
expect(html).not.toContain("Withdrawn.");
expect(html).not.toContain('data-slot="ai-consent-grant"');
expect(html).not.toContain('data-slot="ai-consent-withdraw"');
expect(html).toContain('data-slot="ai-consent-load-error"');
});
it("never shows the grant control while consent stands", () => {
const html = renderWith({
id: "rcpt-1",
+16 -2
View File
@@ -35,6 +35,7 @@ import { Loader2, ShieldCheck, ShieldOff } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { QueryErrorRow } from "@/components/ui/query-error-row";
import { apiFetchRaw } from "@/lib/api/api-fetch";
import { formatDateTime } from "@/lib/format";
import { useTranslations } from "@/lib/i18n/context";
@@ -56,11 +57,13 @@ export function AiConsentCard({
const queryClient = useQueryClient();
const [confirming, setConfirming] = useState(false);
const { data, isLoading } = useQuery({
const { data, isLoading, isError, refetch } = useQuery({
queryKey: queryKeys.aiConsentReceipt("ai_full"),
queryFn: async () => {
const res = await apiFetchRaw("/api/consent/ai/latest?kind=ai_full");
if (!res.ok) return null;
// A failed read is not a withdrawal: throw so the card says it could
// not load instead of reporting the person's decision wrongly.
if (!res.ok) throw new Error("consent read failed");
const json = await res.json();
return (json.data?.receipt ?? null) as ConsentReceiptWire;
},
@@ -133,6 +136,17 @@ export function AiConsentCard({
);
}
if (isError) {
return (
<section className="space-y-3" data-slot="ai-consent">
<QueryErrorRow
slot="ai-consent-load-error"
onRetry={() => void refetch()}
/>
</section>
);
}
// Narrow to the receipt itself rather than a boolean, so the branches
// below can read its fields without an assertion.
const activeReceipt = data && data.revokedAt === null ? data : null;