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>
The seeded /info returned privileges:[] with no OAuth providers — a state the
backend never returns. Every privilege-gated control (e.g. the knowledge
Anonymize action) and the login page's OAuth buttons rendered in their denied
variant in every spec and tier, so a regression there shipped unseen.
Seed the admin privilege list and the OAuth providers (authenticated and guest),
assert the login page now renders its OAuth buttons, and refresh the /flows/5
baseline, which now shows the flow controls in their real admin state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- The knowledge list query is withContent:false, where the backend returns an
empty content string; the entry served "Content for …".
- The generator and refiner prompt pairs were inverted vs the backend
(System/Human swapped), teaching the wrong contract on two of fifteen agents.
- The GraphQL mock dropped an entry's data when it also carried errors, so a
partial-error response could not be mocked with its data half.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The entry answered HTTP 200 with `{error, status}`, but the backend returns 401
with `{status, code, msg}` (develop mode adds the `error` key). The two take
different app branches: the 200 shape resolves and shows "Invalid login or
password"; the real 401 is caught by the axios interceptor, which clears the
auth key and rejects, so the form shows "Login failed. Please try again." — the
branch a real wrong password takes, which no spec exercised.
Serve 401 with the real envelope, pin the submitted credentials with bodySubset,
and assert the message the 401 path actually produces.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The gate subscribed to `labeled` so applying `e2e:stand` could start the stand
job, but every gate job carried `if: github.event.action != 'labeled'`. A label
event then ran with e2e-mock skipped, and GitHub counts a skipped required check
as satisfied on the same head SHA — so adding any label to a PR whose e2e-mock
was red published a merge-permitting check over the red one.
Move the stand tier into its own workflow (e2e-stand.yml) that owns the
`labeled` trigger; e2e.yml now fires only on real code events (opened /
synchronize / reopened) and its gate jobs never skip. The stand workflow keeps
the protected environment + reviewer gate, and uploads results.json only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Keep only the two invisible contracts — pre defers its colours to .hljs, and
data-language is DOM-only and never serialized — and drop the prose that
restated the CSS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Guards the two-way mapping added with the extra_body field: a JSON object
survives GraphQL→pconfig→GraphQL intact, and an absent one stays nil rather
than becoming an empty map.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The backend already threaded a per-agent `extra_body` map into the LLM request
(pconfig.AgentConfig, openai.WithExtraBody), but it was reachable only from YAML
provisioning — the GraphQL type, the converter, and the settings form all
dropped it. Editing a YAML-provisioned provider through the UI therefore round-
tripped its config and silently stripped extra_body.
Add `extraBody: Map` to AgentConfig / AgentConfigInput, carry it through the
converter in both directions, and give each agent an "Extra Body" JSON field in
the provider form (validated as an object, serialized on load, parsed on save).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The prev/next stepping control sat first in the header's right-anchored action
cluster, so a per-entity action that appears after load or differs between
entities (e.g. the flow Report button) pushed the pager sideways — clicking Next
repeatedly landed on whatever slid under the cursor.
AppHeaderActions now takes a `pager` slot rendered as the cluster's trailing
child: its right edge is pinned to the header edge and its width is constant, so
variable actions grow leftward without moving it. flow / template / knowledge
detail headers route their DetailNavigationToolbar through the slot.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fenced code in the editor rendered as one flat grey block with no colour and no
visible language, even though the fence language was already captured into the
node and round-tripped. Swap the plain StarterKit codeBlock for CodeBlockLowlight
(lowlight + highlight.js are already deps; the tiptap wrapper is the one add),
keeping the byte-fidelity parseMarkdown/renderMarkdown tuning verbatim — the
highlighting is a view-only ProseMirror decoration and never touches the
serialized markdown. renderHTML stamps the language onto the `<pre>` as
data-language so a CSS caption bar names the block, matching the read-only
viewer's atom-one-dark theme.
Verified: all 337 existing editor round-trip tests still pass, plus a new one
asserting the fence language reaches attrs.language and survives save for
ts/python/bash/json/go.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mock world's matchers carried five explanatory comments and no test. Cover
them directly — order-independent variable matching, nested-object subset,
sequenced-then-repeated entries, a mismatched REST body staying unmatched, and
flag-gated visibility — and delete the prose they replace. The order test is
built key-by-key on purpose: a literal would be re-sorted by the linter, which
silently defeated the first version (a stripped key-sort still passed until the
value was moved inside an array, where stableStringify actually normalises it).
Sweep the rest of e2e for comments that only narrate the test or restate the
code — step labels, "happy path", cassette descriptions, field docstrings — and
remove them. Kept: framework gotchas whose violation is silent (a probe reading
a colour mid-transition, react-hook-form's disabled Submit, a pre-ack frame that
storms graphql-ws), security notes, magic-value and source-of-truth pointers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each sandbox gets a `<container>-data` volume created straight on the docker
socket, outside the compose project, so removing the container left the volume
behind and every Tier-2 run added one more. Confirmed on this machine:
pentagi-terminal-90001-data and -90002-data outlived their containers.
Swept with the same anchored 9xxxx filter the container removal uses, so a dev
stack's volumes are left alone.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three ways these gates could go green while measuring nothing:
The editor probes name their classes by hand — the one place in the contrast
spec that does not import them from the app. A rename, or the rule moving out
from under its ancestor chain, would mount a span matching no rule, and the
measurement would fall back to the page's default text pair, which clears AA.
The probes now reject a transparent background, which only an unmatched class
produces (the check lives with the editor probes, not in the shared measure —
the outline badge legitimately paints no background).
The dark sweeps seed a theme key that mirrors a *default parameter value* in the
provider, so it can stop matching without any test file changing; the run would
then repeat the light theme under a dark label. Both sweeps now assert the theme
actually applied.
The resources target-size waiver matched `.rounded` unanchored, so it also
covered every `rounded-*` utility — a too-broad waiver never fails, it only
hides. Anchored to the class it documents.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`toHaveClass(/destructive/)` on the delete confirm matched every shadcn Button —
the base class string carries `border-destructive` and `ring-destructive/*` for
the invalid state — so the one guard against an action losing its destructive
variant, a regression this repo already shipped once, was dead. Anchored on the
variant's own `bg-destructive` fill instead.
The message-alignment check took `.items-end` first-match anywhere on the page
and never asserted the contrasting case; it now binds to the input message and
pins the count, so an inverted ternary fails on both.
`not.toHaveClass(/dark/)` also holds before the provider has applied any class,
so the light half of the theme switch proved nothing; the provider sets an
explicit `light`, so assert that. Same shape as the menu-item absence check,
which now anchors on an item that stays.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Keying the waivers by route path made the spec restate the manifest's sample
flow id, so renaming that id would silently unhook the waiver and redden a route
over debt that was already accepted. The waivers now live on the manifest entry
and travel with it; scanA11y takes the list directly, which also drops the
route-keyed map the hand-written sweep needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The sweep hand-listed six scans while the manifest holds nine routes, so
/dashboard, /resources, /settings/prompts and /settings/providers were never
scanned in either theme — and since the pixel matrix cannot see a colour change
either, those pages had no automated accessibility instrument at all. It now
iterates the manifest, so a route added to the sweep is scanned by construction.
The four newly covered routes surfaced real debt on the first run. One was a
genuine invalid-ARIA bug and is fixed in its own commit; the rest are recorded as
named waivers: the dashboard period switcher points aria-controls at a tab panel
it never renders, and the resources tree has 80%-muted row metadata under AA plus
an expand toggle and row checkboxes under the 24px pointer-target floor. Each
waiver names one defect and matches only its own nodes, so the same rule still
fires anywhere else on the page.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Seven payload-bearing cassette entries answered success no matter what the app
sent: the four flow mutations, both create mutations, and the mkdir POST. Since
each assertion then reads canned cassette data, a wrong payload — a stale closure
deleting flow 6 while flow 5 is open, an unbound name field — repainted exactly
the asserted UI and the suite stayed green. Verified by flipping a pinned flow id
and a pinned path: both specs now fail on the unmatched request, and both passed
before.
Matching had to grow a level for this: `bodySubset` documented a deep subset but
compared nested values whole, so `{ input: { name } }` demanded the entire input.
It now recurses, which is also what makes the API-token pin possible — its `ttl`
is derived from the clock at submit time and drifts a second between runs, so
only the operator-entered name is pinned.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The listing is a `tree`, not a grid, so there is no columnheader for `aria-sort`
to live on — axe rates it a critical `aria-allowed-attr` violation on every
sortable column. The sort state is already announced through the button's label
("Sort by name (ascending)"), so removing the attribute loses nothing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The probes carry `transition-colors`, so reading the computed background right
after hover() sampled a colour still interpolating away from the rest state —
the half of the gate that exists to catch hover regressions could not catch one.
Disabling the transition on the probes fixes it: verified by giving one variant a
deliberately unreadable hover fill, which the assertion now reports as "badge
blue on hover" and previously let through.
Key the probe list off the variant union so a new badge variant fails to compile
until it is probed, rather than silently going unmeasured.
Also correct two route-manifest ownerships that misdirect diff-scoping: the flows
provider is mounted by the detail route as well as the list, and the provider
detail page was attributed to the list route that never renders it.
Doc fixes: the route sweep is tagged @cross, not @coverage; the licenses link in
CONTRIBUTING resolved outside the repository; and the frontend prettierignore
duplicated entries prettier already reads from gitignore.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Four independent ways the signal misled:
Adding a label to a PR skips every gate job, yet the report still ran, found no
results file and overwrote a correct green comment with a red "no results". It
now returns early when the run carries no completed mock job.
A run aborted by the global timeout still uploads a results file whose failure
count is zero, so the header could go green over a red job; the verdict now
requires the mock job's own conclusion too.
The report's concurrency key was the branch name alone, so a fork PR opened from
a branch named `main` lost its comment to upstream `main` activity. The gate's
key split `opened` from `synchronize`, leaving both runs live so the older one
could finish last.
Stand failure screenshots carried the stand URL and the login into a public
artifact — screenshots are now tier-aware like traces and videos already were.
Also guard the Tier-2 teardown: the trap is the script's last statement, so a
teardown hiccup turned a passing run red.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "Success" badge and the all-passed accordion badge were assembled from raw
utility classes (`text-green-600` on a green tint, no dark override) instead of
the shared variant, so they kept the pre-unification shade and render at about
3:1 in light theme — under the 4.5:1 floor every other colored badge now meets.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The sticky PR comment took its header from the whole-workflow conclusion, so a
visual-snapshot diff — advisory, never a required check — rendered a red ❌ over a
"62 passed, 0 failed" mock-tier stats line, reading as a gate failure it isn't.
Track the header on the mock gate instead (✅ pass / ❌ real failure) and surface a
differing visual job as a separate ⚠️ line pointing at the diff artifact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A used template variable read as green in the Available-variables panel (its
"used" badge) but violet in the editor highlight — the same entity, two colours.
Swap the editor variable to green and the <xml-tag> highlight to violet so it
stays distinct. A pure hue swap: lightness and chroma are untouched, and every
token still clears WCAG AA in both themes (tightest is the dark tag at 5.36).
Guard the editor palette in cross/contrast.spec.ts alongside the badges, mounting
the real .template-* rule so the shipped CSS is measured, not a copy — verified to
fail when a token is pushed under AA.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The last two uncovered detail tabs. Files turned out not to be REST-backed at all:
it runs its own flowFiles query, separate from FlowDocument, so it needed a keyed
cassette entry and paths under the manager's uploads/resources prefixes — a file
seeded outside those roots renders nowhere. Screenshots reads FlowDocument like the
other four; only its image is fetched over REST.
That image is the part worth guarding. It mounts behind an intersection observer
and loads lazily, so a spec that merely opens the tab never requests it — the first
version passed with the cassette entry removed. It now scrolls the card into view
and asserts the decoded width, since toBeVisible is satisfied by an element that
failed to decode. Removing the entry fails it twice over: zero width, and the
unmatched-call gate.
Serving those bytes needed the REST mock to stop assuming JSON, so entries can now
declare a content type and have their body passed through verbatim.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Nothing enforced the container requirement — only a comment mentioned it. Running
the visual project on a developer machine writes `*-visual-darwin.png` next to the
linux baselines: new files CI never reads, so it stays red while the repo collects
junk and nothing says why. Fail up front with the command to use instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>