The dashboard laziness check asserted the Overview heading was unattached
straight after goto. The route is lazy, so that is satisfied by "nothing has
rendered yet": mounting the panel eagerly with forceMount still passed. Wait
for the Analytics panel first — with the gate in place forceMount now fails it.
The flow-detail a11y waivers matched `button[aria-label`, which waives
button-name and target-size for every labelled button on the route rather than
the file-manager controls that actually violate them. Anchored both on the
offending nodes; the real violations stay waived and a nameless icon button or
an undersized labelled button no longer does.
The rejected-login smoke test asserted only the disabled half of the behaviour
its name describes. Change a field and assert Sign in comes back.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`maxDiffPixelRatio: 0.01` allows 9,216 differing pixels on a 1280x720 baseline
— more than the area any single foreground token covers — so the gate could not
fail on the palette regression it was tightened for. Verified: swapping the
primary token from blue to green passed all 20 baselines before this change and
fails 6 of them after.
Both baselines and runs render in the pinned container, so the remaining noise
is glyph anti-aliasing; 200 pixels covers it with 20/20 still matching.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Concurrency is evaluated for the whole run before the job's `if`, so a run
started by any other label joined the same group and cancelled an approved,
in-flight stand run — then skipped its own job, leaving nothing in its place.
Key the group on the label as well.
The file's header promises fork PRs get Tier 1 only, but nothing enforced it.
GitHub withholds secrets from fork `pull_request` runs, so a labelled fork PR
held reviewers for an environment approval and then failed on empty
credentials. Require a non-fork head.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sampling window.location on a timer measured the host's speed: on an unloaded
machine the sibling landed before the third sample, so the guard that the
samples spanned the switch failed 3 runs in 4. Weakening it to a length check
made it vacuous instead — the sampling loop always runs its full count.
Record every pushState/replaceState the app makes and assert the exact
sequence, which is what "without passing through the list" claims. Verified by
routing the pager through the list: the trail assert names the detour.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The pager keeps FlowProvider mounted and only swaps subscription variables,
which is the one path where a superseded stream can stay open. Asserting the
leaked message is absent from the DOM cannot catch that: messageLogs is keyed
by flowId, so a frame carrying the old flow's id is written to the old flow's
cache slot and is never rendered under the new one, leaked or not.
Assert the mock's live subscriber count for the superseded stream instead, and
export the stream-key builder so the spec cannot drift from the mock's format.
Verified by holding a stale subscription open in the provider: the new assert
fails while every DOM assert stays green.
Also restores the class member ordering the linter requires.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The a11y and palette sweeps clicked a tab and scanned immediately, so the two
round-trip panels could be scanned while still skeletons. Skeletons carry no
axe or palette violations, so those scans passed on an empty panel instead of
the content they exist to check. Tabs now carry a readiness locator beside the
name and both sweeps wait for it.
Ordering is part of the same defect: the flow auto-opens the Assistant panel
when it has no message logs, so an Assistant-first sweep clicked a tab that was
already open and asserted a marker that predated the click. Dashboard leads the
left-hand pair, and the sweep asserts each panel is absent before its own click
so a future reordering that makes an iteration a no-op fails loudly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test.setTimeout(240_000) was below the sum of the sequential step ceilings
(60+30+90+90+90+60 = 420s), so a legitimately slow-but-passing real run was killed
mid-step with a generic timeout that masked the real failure — the exact reason the
config's own globalTimeout comment gives. Raised to 450_000.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The malformed-body guard only rejects non-object top-level payloads, so a body like
{"tools":5} passed it and reached `(payload.tools ?? []).map(...)` — `.map` on a
number throws outside the try/catch and kills the process, dropping any in-flight SSE
streams and violating the guard's stated contract. Guards on Array.isArray before
mapping. Proven: the old expression throws on {"tools":5}, the new one yields "" and
still maps a real tools array.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The local tier fell back to https://localhost:8443 when E2E_BASE_URL was unset,
while stand fail-fasts. Both tiers bake the real (paid) flow-run specs against a
real backend, so a bare `E2E_TIER=local pnpm e2e` silently ran a real flow against
the developer's dev stack — a paid LLM call, a junk flow, and a sandbox
container/volume the wrapper cleanup never removes. Requires E2E_BASE_URL for local
too; run-local-tier.sh already supplies it, so the legitimate path is unaffected.
Verified: bare local now throws, local + E2E_BASE_URL loads clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The job triggers on `pull_request: [labeled]`, but the guard tested
`contains(labels.*.name, 'e2e:stand')` — the label *set*, not the label that fired
the event. So adding ANY label to a PR that already carries `e2e:stand` re-triggered
the run, and `cancel-in-progress` then killed the approved, in-flight stand run and
re-pinged the environment reviewers. Gates on `github.event.label.name` instead;
workflow_dispatch is unchanged, and the job only listens to labeled + dispatch so the
event always carries a label name.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The lazy-mount check used toBeHidden(), which passes for a detached node AND for a
mounted-but-hidden one — so it did not actually verify the "not mounted while
Analytics is active" claim it documents; an eager mount (all overview queries
firing behind a hidden panel) would still pass. Switches to not.toBeAttached().
Confirmed green: the panel is genuinely unmounted (Radix drops the inactive tab).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
graphql-ws resubscribes active sinks in a microtask after the retry connect, so
the reconnect test gated on the wrong signal: after `retries == [false, true]` it
raised the flag immediately, and a poll landing in the gap delivered seq:2 to an
empty subscriber set — the no-replay contract then lost the frame, timing out. The
report flagged it as CI-load-dependent (0/25 local repro); the mechanism is a real
ordering gap regardless.
Adds MockWorld.subscriberCount(streamKey) and waits for the resubscribe to
re-register the sink before raising the flag. Not a repro of the flake (it does not
reproduce locally), but it closes the ordering gap the flake rides on. 4/4 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mock PR gate runs retries:0, but trace was 'on-first-retry' — so it never
recorded a trace on that tier, while docs/e2e.md ("Debugging a red CI run") and
the auto-posted PR comment both tell you to open trace.zip from the mock tier's
e2e-report artifact. Every red gate run dead-ended the advertised debug path.
Switches trace to 'retain-on-failure' (matching video on the same line), keeping
the stand tier at 'off' for the session-cookie privacy reason. Proven on a clean
host: a failing mock test (retries:0) now writes trace.zip + video.webm.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The palette gate scanned only each route's default view, so off-palette colours
behind a tab (which Radix unmounts while inactive) were never checked — the a11y
gate iterates tabs, this one did not, and they had drifted. The flow Files tab
carries a live off-palette node (file-manager's expand-all control,
hover:text-blue-400) that went green purely because the panel was unmounted.
Adds a per-tab scan mirroring the a11y gate, with tab-scoped waivers keyed
`${path} [${tab}]`. The file-manager control is waived on the Files tab under the
same "goes with the design pass" rationale it already carries on /resources.
Proven: the Files-tab scan passes against the exact waived offender (a non-empty
`toEqual`), so the scan reaches the panel — the old default-view scan could not.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The version guard compared @playwright/test against `npx playwright --version`, but
the pinned playwright container ships no global playwright package and the step runs
before pnpm install — so npx fetched the registry latest and compared the package
against that, not against the container. Green only while latest == the pin; the next
Playwright release fails every run telling you to bump the tag to the value it already
is, and a pin bump without a re-tag passes despite real drift.
Reads driverVersion from the image's own /ms-playwright/.docker-info instead. Verified
first-hand inside v1.61.1-noble: no global playwright, .docker-info reports 1.61.1, and
the fixed check reads pkg=container=1.61.1 with the repo mounted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The stand job runs against a URL, user and password held as repo secrets, and
Playwright's results.json embeds the resolved page URL (baseURL) in navigation and
toHaveURL error messages, plus the user in locator text, on any failing run. The
`if: always()` upload then publishes results.json as a public-repo artifact for 3
days. GitHub masks secrets in logs but never in artifacts, so a red stand run leaked
the stand URL and user. The comment beside the upload claimed results.json carried
none of those — false exactly when the upload matters.
Adds a redact step (node split/join, literal — safe for password metacharacters)
that replaces each secret with <redacted> before upload; proven locally to strip a
URL + user from a sample results.json while keeping it valid JSON.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two e2e gaps surfaced by an A/B review of this branch:
- knowledges detail gains an authz-denial case: the backend denial string
"requested permission '<perm>' not found" contains "not found", and a naive
not-found match would bounce a user who merely lacks access to the list. The
spec asserts it stays on the route behind Retry. Proven: reverting errors.ts to
the pre-fix predicate turns this red (page bounces to the list).
- template-detail and pager header-order specs used findIndex, which returns -1
for an absent label; -1 < any real index, so "Save left of Previous" passed even
if the Save/Next button had vanished. Both now assert every referenced button is
present before ordering them, so a missing-button regression fails the spec.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
isTemplatePending ORed the raw Apollo `loading` flag, gating Save/Rename/Delete.
Commit 3d5fc75 fixed the sibling *render* gate (`isLoadingTemplate && !template`)
but left this `disabled` gate one level down still keyed on raw loading, so a
background revalidation greyed out the actions on a form the user had already
edited. flow.tsx uses the entity-guarded `isFlowLoading`; template was the only
detail page reading raw loading here. The render gates above (both branches carry
`!template`) already make the form unreachable without a loaded template, so the
loading term only added a dead disabled window — dropped it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two ways a detail page redirected the user off to the list when it should not have,
both surfaced by an A/B review of this branch:
- isNotFoundError matched /not found/i, but the backend's authz failure
"requested permission '<perm>' not found" (graph/context.go) also contains
"not found". A user who merely lacked a permission was silently bounced to the
list instead of seeing the denial. Authz strings now read as real failures.
- flow-provider's isFlowMissing dropped the `!flow` guard that its two siblings
(flowLoadError and the not-found toast) apply: under errorPolicy:'all' a partial
not-found error rides alongside a flow that loaded fine, so the redirect fired on
a flow that had rendered correctly. The disjunct is gated on `!flowData?.flow`
again, extracted to a pure `deriveFlowMissing` so the regression is unit-tested.
errors.test gains the real authz strings (revert the predicate -> red);
flow-provider.test covers the partial-error-with-loaded-flow case (revert -> red).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The B1-B3/B6/B7 pass guarded the settings detail pages' error branch with
`&& !data`, but each detail page has an `if (loading)` branch that runs FIRST,
and it was left unguarded — so the fix it was meant to deliver never applied.
The queries are cache-and-network, so a background revalidation (a list→detail
navigation into a warm cache, or a post-save refetchQueries) reports loading
true with cached data present and blanks the edit form to the full-page spinner
before the guarded error branch is ever reached.
- settings-prompt.tsx / settings-provider.tsx: `if (loading)` -> `&& !data`
- template.tsx spinner: `if (!isNew && isLoadingTemplate)` -> `&& !template`,
which also realigns it with knowledge.tsx (fixed in 28ab3d2 to gate on the
entity, not raw loading) — the two had silently diverged.
- docs/list_detail_pages.md: the "canonical render gate" recipe still taught the
unguarded `if (isLoading)` it tells new pages to copy; both branches now gate.
settings-provider.test gains the loading-with-cached-data case (revert -> red);
the detail loading branch had zero coverage before. Found by the adversarial
review of the previous fix pass — the guard I applied was one line short.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The five settings surfaces guard their loading and error branches
inconsistently. Their queries are cache-and-network, so a subscription- or
mutation-driven refetch flips `loading` (and, on a failure, `error`) to true
while the cached data is still on screen. Where the guard omits `&& !data`,
that refetch replaces a populated list — or a provider/prompt edit form with
unsaved changes — with the full-page spinner or error screen for the duration
of the round-trip.
Each branch now matches the one beside it in the same file, which already
carried the guard and the comment "a failed background refetch must not blank
a working list":
- api-tokens / providers / prompts lists: `if (isLoading)` -> `&& !data`
- prompt / provider detail: `if (error)` -> `&& !data`
Proven by runtime repro, one per class: settings-provider.test asserts the
form survives an error arriving with cached data (revert -> red), and
settings-providers.test asserts the populated table survives loading:true with
cached rows (revert -> red). The other three are the identical one-line guard
against the same cache-and-network behaviour.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On a cold Tier-2 stack the new-flow form stays invalid — and Submit disabled —
until the providers query lands, so clicking it straight away burned the whole
240s test timeout waiting for a disabled control. Wait for it to enable first.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The template and knowledge detail pages dropped `error` from their query and
inferred "this record does not exist" from the absence of data. A real load
failure — a network drop, a 5xx, a cold-cache backend error on a deep link —
therefore rendered "Template not found" / bounced to the list with a toast,
offering no way back in short of retyping the URL. Only a genuine 404 should
do that; a transient failure should keep the user on the route behind Retry.
Both now split the two outcomes the way flow already does: a real error →
in-page ErrorState + Retry; a settled-empty result or a not-found error →
the existing redirect/not-found card. The `no rows`/`not found` predicate that
flow-provider kept privately becomes the shared `lib/errors.ts#isNotFoundError`
now that three call sites need it, and flow-provider moves onto it.
Proven by a runtime repro, not by reading: knowledge.test.tsx asserts the
in-page error + no redirect on a real failure and the redirect on a genuine
not-found — reverting the fix drops it to a failure. errors.test.ts pins the
predicate's two sides. e2e repros on both detail routes drive it through the
production bundle for CI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`knowledgeId` means the route param everywhere else — the route is declared as
`knowledges/:knowledgeId`, and both knowledge.tsx and the route-title resolver
destructure it under that name. This header had taken the name for the loaded
document's id and left the route param as `routeKnowledgeId`, so the one file
that reads both used the project's vocabulary backwards.
The document's id needed no binding of its own: `handleRenameSave` two functions
above already guards on `knowledge` and passes `knowledge.id`, so `handleDelete`
now does the same and the rename input keys off `knowledge?.id`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The comment on AppHeaderActions claimed the component pins itself to the right.
It does not — the sibling content block takes `flex-1` and pushes it there — and
the rule it stated was an instruction to call sites, not a fact about the twelve
characters below it. The convention now lives in docs/list_detail_pages.md, next
to the detail-page recipe that needs it.
Two more went the same way, by making the code carry the point instead: the
knowledge header's two ids are now `documentId` and `routeKnowledgeId`, so
there is nothing to warn about, and the separator that doubles up is gated on a
named `hasViewRow`. In the pager spec the injected delay is held by an assertion
that counts the samples taken before the sibling appears — removing the delay
now fails the test instead of quietly emptying it.
Across the e2e specs, six comments that only explained why a test exists are
gone and five more are down to one line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Seven blocks went: three justified a test's existence or the way an assertion
was written, one repeated the fact stated two hunks above it, one restated a
guard that a unit test now enforces, and two paraphrased the identifier sitting
next to them. The five that stay each name a consequence that is invisible from
the line: a delay whose removal makes the surrounding assertion vacuous, the
leftward-growth contract of a shared header primitive, a route id that must not
be taken from the entity beside it, a separator that doubles for callers without
a mode toggle, and a flag read by a subscription gate forty lines away.
The counter's width reservation gets a unit test over three set sizes instead of
prose, so its scaling is pinned by the suite rather than asserted in a comment.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three defects the review found, all in surfaces the flow page does not share.
The knowledge pager took its current id from the loaded document, so now that
the cluster no longer unmounts it sat there reading "–/N" with both arrows
dead for the whole document fetch — and again after every step. It takes the
route's id, like the flow and template pages already did.
The knowledge actions menu drew a doubled divider while loading: the View row
between the two groups only exists when a mode toggle is passed, which the
loading shell does not do.
The isLoading prop threaded down to that header could not change any output —
the only caller that passed it also passed a null document, so the flag it fed
was already true. Removed rather than left as a signal that looks live.
Also: the template not-found card reuses the page header, which since the
redesign offered a Save aimed at a form that is not on that screen and a pager
for an id absent from the list. The actions are gone from that branch; the
loading branch keeps them, which is the point of the convention.
The two flow baselines are regenerated: they were captured mid-series, before
the counter's width reservation moved off the button, and the visual gate could
not see the drift — the diff sits seven times under its ratio. The template
header's order now has a spec assertion; that route has no baseline at all.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The reservation was computed as digits-of-total × 2 + 1 and applied to the
button, which is border-box: 18px of padding and border ate it, so the
counter still grew from 65 to 83px across a digit boundary and Previous
still slid 18px out from under the cursor — measured, both before and after.
It now reserves the width of the widest label the set can produce, on the
label itself, and the button holds 83px through every position. Building
that label instead of deriving its length also drops the arithmetic that
made the intent unreadable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reverts the `pager` slot on AppHeaderActions. The block is right-pinned, so
ordering alone gives the guarantee the slot was reaching for: put the controls
that come and go at the start of the children and everything after them keeps
its position. Flow header, right to left: actions menu, pager, favourite,
report — the report being the one that waits on the task list.
The controls that are always meaningful for a route are now always rendered
and disabled from an explicit loading flag rather than unmounted when the
entity object is falsy. Stepping used to collapse the whole cluster to a lone
star for the length of the fetch, and the pager — which needs the sibling list,
not the current entity — went with it, so a second step meant waiting.
Templates and knowledge get the same treatment; knowledge had no loading
signal at all, so one is threaded down from the page.
Two side effects of dropping the entity gates: on phones the flows row and the
favourite toggle survive an unloaded list (they were nested behind it), and the
position counter reserves the widest label its total can produce, so stepping
across a digit boundary no longer slides Previous out from under the cursor.
The pixel baselines cannot pin any of this — the cluster is far below the
visual project's diff ratio — so the order is asserted in the spec instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stepping to a sibling flow landed on /flows. The detail page inferred "this
flow does not exist" from three absences — not loading, no flow, no error —
and an Apollo variables change satisfies all three: it reports networkStatus
setVariables, not loading, while the new flow's data is still undefined.
Instrumenting history showed both hops: replaceState to /flows/2861, then
straight back out to /flows.
The provider now publishes a positive isFlowMissing (the query settled with
no flow, or failed as a not-found) and the redirect reads only that, so
retuning the loading flag cannot silently break navigation again — which is
how this shipped. isLoading itself becomes "in flight with nothing to show",
which also keeps the Retry button on a failed load from ejecting the user.
Nothing in the suite pressed Prev or Next, so the new spec does: it samples
the DOM through a delayed fetch, proving the URL never passes through the
list and the pager stays mounted while the sibling loads.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The contrast gate mounts probes from cva output, so a colour written as a
raw utility in a page — the shape of the badge defect that shipped — was
outside it by construction. This walks the rendered DOM of every swept
route instead: each badge and button must draw its colour from the variant
set or from a semantic token.
It immediately found one, a hard-coded blue hover on the file manager's
expand-all control, waived by its exact node string until the design pass
takes it (the colour change moves baselined pixels).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The intro left /settings/prompts out of the list pages built on these
pieces and implied every one of them has a detail page; api-tokens edits
in place. The removed/renamed table pointed at stale mentions of the
deleted writer hook in two files that no longer contain any.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every assertion was client-side — the URL, a route-derived breadcrumb and
pageerror, which a production React build does not emit for a failed
query — so all five passed against a backend erroring on every request.
Verified against the live stand: healthy, 5/5 pass; with GraphQL forced to
error, the URL and breadcrumb assertions still pass and only the new one
fails. Each route now proves its query resolved, accepting the empty state
as well since a stand may hold no rows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both flow entries claimed the whole src/pages/flows dir, which also holds
the report and create pages. A diff touching only the report page scoped
the run to two routes that cannot render it — the same ownership class
already fixed elsewhere in the manifest. Each entry now names its own
page file, so those two pages fall through to the conservative full run
and the detail/list diffs scope more precisely than before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two of the five --editor-* tokens were probed and nothing made the list
rot-proof, unlike the badge matrix. The remaining three hang off element
selectors rather than classes, so the probe mount now takes a tag per
probe; the accent and code pairs clear AA in both themes. A new assertion
reads the token declarations out of the stylesheet, so a newly declared
token has to be probed or exempted.
The mount guard moved from "composites to transparent" to "still wears the
surface's colour", which every token rule overrides — the transparent form
could not cover an element-selector probe with no chip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three of the four by-period queries returned one dataset regardless of the
period, so the period switch could only be asserted on the token chart —
the other three cards would have rendered identically had the app never
rewired them. Each now has week and month variants keyed on the period
variable, and the spec asserts the swap across all four cards plus the
execution breakdown, which carries no dates and so differs by flow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The post-reconnect flow(5) response was ungated, so any spec that fetched
flow 5 twice consumed it and rendered a message that never streamed. It now
serves only after a drop, behind a flag `dropAndReconnect` raises.
The no-duplicate assertion beside it had nothing that could produce a
duplicate: every id reached the page exactly once by construction. The
resubscribe now replays the id the refetch already delivered — the real
server behaviour the client dedups — followed by a sentinel that proves
the replay arrived rather than merely being awaited.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The a11y sweep only ever saw each route's default view. Radix unmounts
inactive tab panels, so seven of the flow-detail tabs were never scanned,
and /settings/providers was scanned exclusively in its empty state — the
provider cards, where the badge-contrast defect lived, went unlooked at.
The manifest entry also claimed to own the file-manager, dashboard and
resources dirs while seeding empty collections, so the swept form of the
route rendered none of them. It now runs on the populated cassette, which
required the six per-flow stats queries the Dashboard tab issues.
Scanning the panels surfaced five real defects (unnamed progressbar,
unnamed icon buttons, under-size targets, screenshot-title contrast,
unfocusable scroll regions); each is waived by rule and node so the rest
of the panel still fails on anything new.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
/settings/prompts/:promptId and /templates/:templateId had no test on any tier,
and they are where MarkdownEditorField loads content from the server — the
prompts list spec only expands a row into a <pre>, and the templates spec only
exercises create mode. The editor's one shipped crash reproduced solely in a
production build, which is exactly what the mock tier runs.
Each route now loads a non-trivial body (headings, list, fenced command, table,
and the {{.Var}} / {{PLACEHOLDER}} atoms the backend parses) and asserts both
halves: the raw view matches the loaded source byte-exact, and after an edit in
the rich editor every atom survives its serialization.
Also close the hole that let them stay uncovered: route builders are functions,
so the manifest's static path walk never saw them. Every builder must now
declare where it is covered or why it is not, and the check fails when a new one
appears undeclared.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The check ran on every push and failed with a bare diff, so a backend schema
change surfaced as an unexplained red frontend job.
Run it only when a codegen input moved — the backend schema, the operations
document, the codegen config, or the lockfile (a codegen bump can change the
output, and skipping it there would let types.ts go stale and fail someone
else's later push). When the compare range can't be resolved (new branch,
force-push, tag) it still runs. On failure it now says which command to run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Watch for uncaught errors on the two auth-path smoke tests (they destructured
no error log before); the rejected-login path asserts no uncaught JS exception
while tolerating the 401's expected browser console line.
- Assert the thinking body is collapsed before the toggle, so an always-expanded
regression fails.
- Assert the dashboard overview metrics are absent until the tab is selected, so
the "loads lazily" title is actually covered.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- The mobile test now asserts a mobile-shell-owned fact (sidebar nav collapsed to
the off-canvas sheet) and the split-boundary test asserts the tab rows go 2→1,
so both cover the width-sensitive half their titles promise.
- The a11y login scan gets the same theme-class guard as the manifest scan, so a
dead dark seed can't re-scan the light page under a dark label.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
paint() set fillStyle to black, then to the requested colour — but an
unparseable colour is a silent no-op on fillStyle, leaving black, which measures
as a spurious ~21:1 pass. Assign the colour against two different sentinels and
throw when it doesn't land on the same value, so a broken colour surfaces the
gate instead of clearing it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The stand tier's fork/secret protection is the protected Environment's reviewer
gate, not the job's label condition — a fork PR can carry the label but blocks
on a human before any secret. Say so, note the tier now lives in its own
workflow, and document the E2E_STAND_* secrets and their E2E_* tool names.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Route both the knowledge and template editor inputs through a shared
typeIntoEditor helper (pressSequentially), instead of fill() on one and a
documented ProseMirror-race workaround on the other.
- Make the mkdir spec type a name distinct from the dialog default and pin it, so
a broken input→payload binding no longer matches the default.
- Derive the stand login-readiness locator from E2E_USER instead of a hardcoded
admin@ (and a "flows" button that does not exist).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Release a subscription id's prior handler before overwriting it, so a reused
live id can't leak both subscribers onto one id.
- Assert a stream without `complete` delivers frames but stays open, so flipping
the completion default would fail the protocol suite instead of passing.
- Correct the connection_init comment: a pre-ack frame closes the socket as a
protocol violation, it does not cause a reconnect storm.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Reject a non-object JSON body in the mock-LLM (JSON.parse('null') parsed but
reading .tools then threw and killed the process).
- Pin serve-dist's PORT from the config so an ambient PORT can't move it off the
port Playwright waits on.
- Fail lint on any warning, so a Playwright test with no assertion (expect-expect
is a warning) can no longer lint clean.
- Add a CI check that regenerates src/graphql/types.ts and diffs it, so the
compiled operations can't drift from the codegen input the stand validates.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Drop the screenshot threshold to 0.02 (baselines and CI both render in the
pinned container, so there is no host rasterisation noise to absorb) so a
palette change now fails the visual gate; verified 20/20 still match.
- Gate the visual guard on a container marker set by run-visual.sh and the CI
job, not the host OS, so an --update on a Linux workstation cannot overwrite
baselines with host-font pixels.
- retries:0 on the hermetic mock tier so a retry-recovered race fails instead of
merging green; wire @quarantine via grepInvert as the escape hatch.
- Scope globalTimeout to the mock tier so a real-tier run is not aborted mid-retry.
- Verify the visual container against @playwright/test's actual version instead
of a hardcoded literal.
- Key the report's "snapshots differ" advisory on the diffs artifact (uploaded
only on a real snapshot-step failure), not the visual job conclusion; and stop
a transient jobs-API error from silently leaving a stale sticky comment.
- Drop the unconsumed blob report that doubled every uploaded trace.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Track sequenced-response consumption by entry identity, not a cursor into the
current eligible subset: a second raised flag grew the subset, reset the cursor
to 0, and replayed the previous flag's entry once before advancing.
- Restrict isPlainObject to true plain objects, so a non-plain pin (a Date) can no
longer recurse into empty own-keys and match any object-shaped value.
- Key subscription streams on the request's variables and REST sequences on the
request body, so entries without variables no longer merge onto one cursor.
- Complete a late subscriber that joins after a complete:true stream has drained.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>