test(plugin-chatbot): pin what a free-plan 429 renders after the quota envelope moved - #8131
Conversation
…a envelope moved Cloud PR #1852 (rolling 7-day free quota) left `error.details.resetsTonight` a boolean and changed what it means: the free plan now sends `false` where it sent `true`, with an ISO `resetsAt` beside it that `tool-display.ts` has no read for. Measured before choosing a repair, which is the half the card had not taken: nothing reads `resetsTonight` once `parseAiQuotaError` populates it. `useAiQuotaCopy` builds the banner from `message` / `messageEn` / `topUp`, and the other two `parseAiQuotaError` call sites use the parse as a predicate. So the answer is "renders nothing", not "renders a false promise" -- the banner is the server's own sentence passed through verbatim, and the inversion changes no rendered byte. The existing pin is about the field's TYPE, and the type did not move. These pin the consequence instead: - the banner's text by exact accounting (our title + the server sentence + the CTA, and nothing else), so reset copy of our own cannot appear unnoticed; - the flag as inert: the same envelope with `resetsTonight` true and false renders identical HTML, with a control asserting the two inputs really differ; - the parse as silent about the instant: the free-plan envelope's `resetsAt` is absent from the parsed shape, so no consumer can render it today. `resetsAt` is deliberately still unread: with no renderer for it, a read would pin a wire position this repo cannot measure and add a second unread field. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Landing — ACCEPTBy the dispatching seat ( CI: 32 of 32 check runs enumerated (returned count equals ⛔ Standing down on What lands, and what it deliberately does not do+193 / −0, and the production diff is comment-only — one new pin file, ⭐ It pins the absence rather than inventing a behaviour. ⛔ The obvious repair — read
Flipping ready and enqueuing. Generated by Claude Code |
Fixes #7587
The card asked for a repair and said, in its own words, that it had not measured whether anything renders the field — and that the difference between renders nothing and renders a false promise changes the priority. So that was measured first, and it decided the shape of this PR.
The measurement: renders nothing
packages/plugin-chatbot/src/tool-display.tsstill populatesresetsTonight(:221declared,:373read — both line numbers re-taken oncf1d29e2fand still exact). After that, nothing reads it:parseAiQuotaErroruseAiQuotaCopy(ChatbotEnhanced.tsx:4322)message/messageEn/topUp— the only consumer of the parsed fieldsChatbotEnhanced.tsx:1939ChatbotEnhanced.tsx:3313parseAiQuotaError/AiQuotaErrorare not in the package barrel either, so there is no external consumer to inherit the field.resetsAtis still 0 occurrences repo-wide inplugin-chatbot.What a free-plan user actually sees, captured from the rendered DOM:
Three parts, each with a named source. Nothing in it is derived from
resetsTonight, and nothing fromresetsAt. Thetrue->falseinversion changes not one rendered byte, and the only sentence about when the allowance returns is the server's, passed straight through. The false-promise risk, if it exists, lives in that server-owned prose — in a repo that is not in this session's scope.Why this is a pin and not a feature
Reading
resetsAtand rendering a reset time was the other candidate repair. Against it, measured here:objectstack-ai/cloudis out of scope; thatresetsAtsits inerror.detailsrather than top-level is an inference from the cloud seat's prose, not a measurement. A reader built on a guessed position fails silently — it never fires, and a fixture written to match the guess pins the guess as if it were fact.useAiQuotaCopyexists to prevent (objectui#7253): "one 429 can never be described by two different sentences on the same screen." Our own reset line would sit beside the server's.So the field stays unread, deliberately and in writing, and what gets pinned is the consequence — which is what the existing pin could not do. #6385 left a pin about this field's type; the type did not move. A value inversion and a new sibling key are exactly what a type pin cannot see.
What is pinned
packages/plugin-chatbot/src/__tests__/ChatbotEnhanced.quotaResetPromise-7587.test.tsxtextContentequals title + server sentence + CTA, plus explicit assertions that the ISO instant never reaches the DOM and no reset copy of ours stands in for it. Reset copy of our own cannot appear unnoticed.resetsTonighttrueandfalserenders byte-identical HTML, guarded by a control that asserts the two parsed inputs really differ (so a green here is a measurement, not a fixture that forgot to vary its input).packages/plugin-chatbot/src/tool-display.test.ts'resetsAt' in parsedisfalse. Stated separately from thetoEqualbecausetoEqualtreats an explicitundefinedproperty as absent.Plus the measurement itself, recorded at the field in
tool-display.tswhere the next reader lands, and an empty-frontmatter changeset (test + comment only; nothing released).Reverse verification — each pin proven able to fail
Three ablations, each committed-then-mutated, each with the on-disk change proven by marker count and the restore proven by
git diff HEADempty plus a blob-hash match againstHEAD.useAiQuotaCopyappends' Resets tonight.'driven by the flaguseAiQuotaCopyappends' Resets tonight.'unconditionallyresetsAt: asText(details?.resetsAt)The first two are complementary on purpose: each catches precisely what the other cannot, and the flag-driven one is the regression the card actually feared — copy promising a reset, driven by a boolean that is now
falsefor exactly the plan that needs the answer.The third is the card's lesson landing twice. Under it, the suite's pre-existing whole-shape
toEqualassertions stay green: their fixtures send noresetsAt, so an exact-equality assertion still cannot see a sibling key that is not in the fixture. Only a fixture that carries the field can.Gates
pnpm exec vitest run packages/plugin-chatbot/— 39 files / 461 tests passed.type-check(both tsconfigs) exit 0 after building the dependency closure.check:control-bytes,check:published-dist,check:published-tsconfig-exclude,check:handler-key-reads,check:sdui-registration-pins,check-changeset-presence— all exit 0. Package lint: 0 errors, 90 pre-existing warnings, none in the changed files.Scope
packages/app-shellis untouched. ItsuseAiUsage/AiUsageIndicatorread aresetsAtbelonging toGET /api/v1/ai/usage— a different envelope on a different surface, and no evidence at all about the 429's field.The follow-on decision this PR deliberately does not take — wire
resetsAtto a renderer, or retireresetsTonightas declared-but-unread — is named in the dev report on #7587.🤖 Generated with Claude Code
https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
Generated by Claude Code