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>
Strict pixel comparison was stable across seven local container runs and failed on
the first CI run: /flows differed by 332 and 384 pixels, identically on both
attempts, so it is a systematic host difference in text rasterisation rather than
jitter. The pinned image fixes the renderer, not the machine under it.
A budget wide enough to absorb 400 pixels cannot notice the ~70 a palette change
moves, so this trade has no useful setting — pixels are the wrong instrument for
colour. Back to the default tolerance, with the reasoning recorded so the next
attempt starts from the CI measurement rather than a local one.
The masking-race fix from the same commit stays: it was a real defect.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The suite was carrying Playwright's 0.2 per-pixel default, a tolerance meant for
cross-machine rendering noise that the pinned container removes by construction.
It cost the gate its sensitivity: a real palette change passed while three
baselines silently went stale.
Compare strictly and bound the differing-pixel count instead. Measured over
repeated container runs, glyph antialiasing jitters by exactly 2 pixels, while the
palette change that slipped through moved 71 — so threshold 0 with a 20-pixel
allowance tolerates the noise and catches the class of change that got away.
Strict comparison also exposed a real defect the loose one hid: the flow detail's
ready locator was the header button, but the visual spec masks the terminal, which
mounts later. Capturing in that window compared live terminal pixels against a
masked baseline — a 261k-pixel diff, roughly a third of the frame, appearing at
random. The route now counts as rendered when its terminal exists.
Seven consecutive container runs are clean at the new settings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Green sat a step darker than its siblings because green-700 measures 4.4991
against its tint — imperceptibly short of AA, but short. Carrying that as a
one-variant exception meant an odd shade number in the map, and the only ways to
let green back to 700 were to lower the gate for all eleven variants or to waive
it in the spec, which is the same exception wearing a different hat.
Move all seven to 800 instead. One rule, no exception, and the whole set clears
the bar with room: 6.39–7.83 on light, against 4.61–6.49 before — orange and
yellow had been sitting close enough that a later tint tweak would have pushed
them under. Dark is unchanged.
Three visual baselines were quietly stale: the earlier 600→700 move changed blue
text that the pixel comparison tolerated, since its YIQ metric under-weights a
blue-channel shift with little luminance change. Regenerated; two clean runs
confirm they are deterministic.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The palette fixes had no guard: no scanned page renders a colored badge or a
destructive fill, so a shade edit could silently drop a variant under AA and
nothing would fail. Two comments were standing in for that coverage.
Mount one probe per variant from the component's own cva — a class list copied
into the spec would only ever re-test itself — and measure what the browser
actually paints, so oklch tokens and translucent fills are read as rendered.
Every variant is checked at rest and on hover, in both themes.
The measurement is deliberately unrounded: green-700 comes out at 4.4991, which
two decimals report as a passing "4.5". With rounding in place the guard passed
against a deliberately regressed green; without it, it fails and names the
variant, the state and the ratio.
Specs can now import component modules, so the e2e tsconfig gains jsx.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three comments added with the contrast fixes were mostly narration: two restated
the class strings they sat above, and two defended the implementation rather than
naming a contract. The one fact that is not visible from the code — green sits a
step darker than its siblings — belongs in a test that fails when it regresses,
not in prose asking the reader not to touch it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four states were under AA, not one: the dark badge and button at rest (3.71), the
light badge on hover (4.34) and the dark button on hover (4.40), plus the dark
destructive menu item (4.01). Badge and button had also drifted apart — one faded
to 80% on hover, the other to 90% — so each failed in a different theme.
The token itself cannot be retuned: sweeping its lightness finds no value that
serves both roles, because the same red is a fill under white text and the text
colour on the page. Darkening it for the fill would break the twenty-four
text-destructive call sites.
Tune the fill's opacity per theme instead, keeping white text and adding no tokens.
A translucent red composites toward what is behind it, so the same fade darkens it
on the dark ground and lightens it on the light one: full strength on light fading
to 90% on hover, 80% on dark dropping to 70%. Hover now raises contrast in both
themes. All four states land between 5.04 and 6.28, verified on a built bundle
against every dark surface token (worst case 5.13 on secondary).
The dark destructive menu item drops its focus wash from 20% to 10%, matching light
and restoring the label to 4.52 at the cost of a fainter highlight.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every colored Badge variant carried a single text shade for both themes, and no
one shade clears WCAG AA on both grounds. Measured against the current tints,
five of the seven failed on light and four on dark, with red and pink failing on
both — the badge text is 12px semibold, so the bar is 4.5:1.
Use the darker shade on the light tint and the lighter one on dark:
text-{color}-700 with a dark:text-{color}-400 override. Green takes 800 instead
of 700 because a shade number is not a constant lightness across hues — green-700
measures 4.4991, just under the bar.
Every variant now lands between 4.6 and 9.7. The knowledges a11y waiver is gone
and both theme scans pass without it; the knowledges dark visual baseline is
regenerated for the lighter blue.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Measuring the variants (browser-resolved sRGB, the 10% tint composited over the
real card surface, WCAG formula) showed the defect is not dark-only: five of the
seven colored variants fail AA on light and four on dark, and no single text shade
clears both grounds. Blue happens to pass on light and fail on dark, which is why
only the dark scan flags it here. The waiver itself is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Only the Terminal tab had a spec; Tasks, Agents, Searches and Vector Store — all
fed by the FlowDocument query — were uncovered. Add a populated flow cassette (one
entry per tab) and a spec that opens each tab and asserts its seeded content
renders. Files and Screenshots are REST-backed and left for a follow-up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The route-manifest visual sweep only captured the empty providers state. Add a
populated cassette (enabled defaults + a user-defined provider row) and a dedicated
visual spec with light/dark baselines, generated in the pinned container. Dates
render deterministically because the visual project inherits the mock clock via the
backend fixture.
Also bind --update-snapshots=all in run-visual.sh so a trailing spec-file filter is
not swallowed as the option's optional [mode] argument.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The suite tags every spec but the docs only hinted at "@smoke, …". List the real
tags (@smoke/@flows/@crud/@coverage/@cross/@visual/@real/@stand) with their meaning
and how to filter, and spell out the two conventions the mock gate reserves:
@quarantine to isolate a flaky spec (excluded via --grep-invert) and @generated for
recorder-derived cassettes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
axe only ran in the light theme, so dark-only contrast regressions were invisible.
Parametrize every a11y scan over light and dark. The dark pass surfaced a real,
pre-existing issue: colored Badge variants use text-{color}-600 with no dark
override and fail contrast on the dark background. Waive it narrowly on the
knowledges list (the blue badge) as named debt — a palette fix is queued
separately — so the rest of the dark surface is now gated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The period-switch test only proved the usageStatsByPeriod refetch fired; it would
still pass if the UI rendered stale data. Key the cassette's usageStatsByPeriod by
period (week vs month) with distinct date ranges, and assert the Token Usage chart
x-axis shows the month range after the switch and drops the week range.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The comment claimed no non-backslash byte is lost, but GFM legitimately trims
cell-edge and collapsed whitespace, so the test asserts the weaker true invariant:
every byte other than backslashes and that trimmed whitespace survives in order.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The base compose file pins fixed network names, so any network left un-overridden
in this e2e override joins the stack to the developer's live observability and
langfuse networks. Give all three networks e2e-scoped names. Add a healthcheck to
the mock-LLM service and make pentagi depend on service_healthy, so a crash-looping
mock can never read as a started dependency and silently mask itself.
Update frontend/docs/e2e.md to match reality: the stand tier runs @stand only
(flow-run drives a real paid agent and stays local), every mock run rebuilds its
own bundle by design, and the trend/affected-routes notes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The co-located components/shared/README.md documented hooks that no longer
exist. `usePagination` was never a hook, and the writer `useTableQueryFilter`
was replaced by the unified `useTableState`, so the copy-paste recipe it shipped
would not compile. Detail pages had likewise moved from spreading a `toolbarProps`
object to passing a `controller` prop.
Rewrite it as frontend/docs/list_detail_pages.md, matching the flat
per-subsystem layout backend/docs already uses: exact signatures for every
hook, component and storage helper, the wiring the five live list pages
actually use, and an explicit table of removed names so they are not
reintroduced. Drop the source-adjacent README — no other directory under
frontend/src or backend/pkg carries one.
Fix the comments that still named the removed hook, plus two claims in them
that contradict the code: the controlled filter is URL-backed only (DataTable
persists sorting, column visibility, page size and search columns, never the
filter), and `busy` on InlineEditInput disables the Save and Cancel buttons
while leaving the input editable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The dashboard partial-errors spec matched cards via getByTestId('dashboard-card'),
but that attribute lives only in an uncommitted working-tree change, so on a clean
checkout the locator matched nothing and the spec failed. Match the data-slot="card"
attribute the shadcn Card primitive already emits. Also lift the inline xterm buffer
type in the message-rendering spec into named types so it satisfies prettier and the
union/module sort lint rules.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The smoke only exercised a successful login. Adds a cassette whose
/auth/login answers an error envelope and asserts the surfaced error plus
that Sign in stays disabled (react-hook-form holds it until a field
changes) — the negative path a live run confirmed but no spec covered.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The message cassettes were all Answer-typed, so flow-message.tsx's
type-specific branches never ran under test. A live QA run against a real
Bedrock flow showed real streams emit thoughts/report/terminal/input.
Adds a cassette with one message per path and asserts each: the
thinking toggle (thinking + message), a report that auto-expands its
details, a Terminal-format report that mounts the xterm renderer with
its output, and an input message's right alignment.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Real-tier (Tier 2/3) fixes, the blocking set:
- stand-smoke asserts a per-route anchor (breadcrumb title span) instead
of a shared sidebar link that 0-matches on /settings/* and double-matches
on /templates — the smoke was structurally red on 3 of 5 routes.
- stand-tier trace/video forced off (they embedded the session cookie and
the stand password in a public-repo artifact) and the stand project
selects @stand via config grep (the CI step selected zero tests).
- run-local-tier's sandbox filter is anchored to the seeded 9xxxx range
(`^/?pentagi-terminal-9[0-9]{4,}$`) so it can't force-remove a dev's
flow-9 sandbox; sandbox cleanup moved behind the keep-stack gate; compose
logs are captured before teardown.
- mock-LLM wraps the request handler (a non-JSON body crashed the process);
its transcript now issues a real terminal exec via a delegate chain and
flow-run asserts the terminal output; OpenAI-shaped error envelope.
Mock-gate rigor + coverage:
- order-insensitive deep-subset variable matching; unmatched WS subscribes
are now accounted at teardown; REST/GraphQL entries can pin a body/vars
subset; favorite-star asserts the persisted (not optimistic) value.
- new flows/create smoke (the primary journey had no mock coverage), a
graphql-ws protocol unit test against the real client, and a
route-manifest completeness test.
- affected-routes no longer under-scopes shared component dirs.
Hygiene: .mjs tools now linted/prettier'd; dead gitignore line; JSDoc/
sheet aria-label/schema-compat/trend/serve-dist/report-comment fixes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
new RegExp(route.replace(/\//g,'\\/')) tripped CodeQL
js/incomplete-sanitization (only slashes escaped). The routes are
constants, so it was a false positive — but the predicate form is
cleaner and asserts the exact pathname.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
schema-compat set NODE_TLS_REJECT_UNAUTHORIZED=0 process-wide (CodeQL
js/disabling-certificate-validation, high). A real stand has a valid
cert; only the local self-signed Tier-2 stack needs it, so the operator
now opts in via their own shell env, never in code.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lays the Phase-3 substrate on top of the three tiers.
- schema-compat pre-flight (e2e/tools/schema-compat.mjs): introspects a
target backend's live GraphQL schema and validates every frontend
operation against it, so deploy skew (a renamed/removed field) fails
once, readably, instead of as dozens of red specs. Verified against the
live Tier-2 backend both ways: 105 operations pass, an injected bogus
field is caught with the exact location.
- trend aggregation (trend.mjs): turns a run's results.json into one JSONL
record (p50/p95 spec duration, slowest three, pass/flaky/fail) so slow
regressions are visible, not just green/red; CI appends it to a
90-day-retained artifact.
- diff-scoping (affected-routes.ts + affected.ts): maps a diff to the
manifest routes it touches via each route's owning sources — the
substrate for selective runs and for scoping the exploratory agent.
Pure mapping fn, unit-tested (backend-only → none; shared infra → all;
owned → that route).
- CI: an e2e-stand job (label-gated + a protected Environment whose
reviewers approve before secrets are exposed; schema-compat runs first),
a trend step, and an LLM-independent @stand smoke.
- docs: the stand tier, trend/affected tools, and the LLM advisory recipe
(playwright-mcp + init-agents + guardrails) — the recipe the
deterministic tiers plug into, not a bespoke bot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
18 baselines (9 manifest routes × light/dark) compared only inside
mcr.microsoft.com/playwright:v<version>-noble, so pixels are identical on
every machine and CI; the wrapper derives the tag from the installed
@playwright/test version, making the pin drift-proof, and a CI guard
fails loudly if the workflow's container tag falls behind.
macOS hosts cannot run the visual project directly (parallel darwin
baselines) nor mount their node_modules into the container (native vite
binaries): the wrapper builds dist on the host and the container serves
it with a dependency-free static server — route mocks intercept API
calls before the network, so no proxy is needed. The xterm canvas is
masked (SwiftShader pixels are driver-dependent). Determinism proven by
back-to-back container runs. The e2e-visual CI job is advisory, never a
required check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
specs/real/** now exercise the actual agent loop end to end: create flow
-> image/language/title -> tool-call-ID sampling -> subtask_list plan ->
done barrier -> subtask_patch refine -> report_result — with messages
streamed over the real GraphQL websocket and the flow settling in
Waiting. The mock LLM is ~150 lines of Node driven by a deterministic
first-match transcript; the custom provider env seam means zero backend
changes.
The stack is fully isolated from a developer machine:
- own compose project/network/ports (8444/5433), coexists with a dev stack
- --env-file /dev/null so the developer's .env (live keys, DOCKER_HOST,
config paths) never leaks into the e2e backend
- flow ids seeded from 90001: sandbox containers are named
pentagi-terminal-<flowId> on the shared docker daemon, so the range
keeps them clear of dev flows and makes runner cleanup unambiguous
(down -v cannot remove them — the backend spawns them outside compose)
- pentagi healthcheck via busybox wget (the alpine image has no curl),
since the base service has none and up --wait returns too early
Auth for live tiers moved to the canonical setup-project + storageState;
the localStorage seed is now mock-tier-only (a forged client session has
no cookie behind it and the first 401 wipes it). CI gains a nightly/
dispatch-only e2e-local job.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The suite's generative tests run with timeout 30000 — the new one missed
it and tripped vitest's 5s default on slower CI runners.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
setup-go's cache:true already caches the build and module caches keyed
on go.sum; the extra actions/cache step restored the same read-only
~/go/pkg/mod on top and failed with "Cannot open: File exists" whenever
both caches hit. Also sets the workflow's GITHUB_TOKEN to contents:read.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
marked's cell splitter honors \| only after an odd backslash run and
truncates rows that split into extra columns, so escaping the pipe alone
in text like a\|b produced \\| — a live delimiter that dropped the
trailing cells on the next load. An odd run + pipe has no exact GFM
encoding: the serializer now pads the run by one backslash, trading a
one-character gain inside the cell for structural integrity, byte-stable
from the first save. Covered by direct round-trips and a random \/|
payload property test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Completes the scenario-catalog coverage on the mock tier (48 specs).
- dashboard: analytics/overview tabs (lazy mount), period switch verified
on the wire, and the per-card degradation path — a failed stats query
renders "Couldn't load" in its own card while neighbours stay live
(first use of error-response cassette entries)
- settings: prompts (the largest typed cassette — 15 agent + 12 tool
prompt configs), providers empty state, create form opens
- resources: REST-seeded file tree, and the mkdir journey proving the
full chain — REST mutation raises a world flag, the flag releases a
resourceAdded frame, the subscription cache merge grows the tree
- e2e/routes.ts: route manifest (paths imported from src/lib/routes so
a rename breaks compile) driving a data-driven NAV sweep over 9
routes; entries carry owning source dirs for future diff-scoping
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The token-reveal dialog (and any dialog with an explicit Close/Cancel)
exposed two controls named "Close" — ambiguous for screen readers and
for role-based locators in strict mode. The shared X is now "Dismiss
dialog".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Builds on the mock gate: 26 new specs across flows, CRUD, and cross
concerns, all driven by typed cassettes against the production bundle.
- flows: no-duplicate and exact-set-disjoint message streaming across
concurrent flows (ID sets attached to the report), reconnect that
reconciles the missed delta exactly once, and the rename/finish/
favorite/delete lifecycle chained mutation->subscription-frame
- mock engine: streams move to one-driver-broadcast (flowUpdated is
opened by two providers at once; reconnect re-joins past the cursor,
delta-only) and frames can wait on world flags
- CRUD: templates, knowledges (required-validation proves no mutation
fired via the zero-unmatched teardown), api-tokens (inline create,
one-time secret reveal)
- cross: themes (seeded + toggled), responsive (Pixel 7 touch profile,
1279/1280 split boundary), a11y with a policy - critical/serious
floor plus a committed per-route allowlist
- terminal content is asserted through the xterm buffer (the WebGL
canvas has no DOM text): the hook now exposes the instance on its
host element
- app fixes surfaced by the sweep: the rows-per-page select had no
accessible name; the favorite star is useOptimistic and only
persists via the settingsUserUpdated frame - the spec now asserts
the real persistence path
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The default tier runs the production bundle (vite build + preview)
against a cassette-driven mock of the whole API surface — GraphQL over
HTTP, graphql-transport-ws subscriptions, and REST — so the suite needs
no backend, no keys, and no secrets, and fork PRs can run it.
- mock engine: (operationName, variables) matching with sequenced
entries and world flags (login flips /info guest->user without
call-order coupling); unmatched calls answer 501 and fail the test,
so nothing leaks through the vite preview proxy to a live backend
- ws mock follows the graphql-transport-ws contract the app's client
needs: immediate ack, nothing before ack, streams stay open, delta
cursors survive reconnects, drops use retryable close codes
- clock and timezone pinned on the mock tier: formatDate branches on
isToday/isThisYear, so unpinned cassette dates rot within a day
- cassettes are TS modules typed against the generated GraphQL types,
so schema/operation drift fails the existing tsc gate at compile time
- CI: fork-safe e2e.yml (read-only token, no secrets, no write steps)
plus e2e-report.yml posting a sticky PR comment via workflow_run,
resolving fork PRs by head SHA
- smoke specs (login redirect, form login, authenticated /flows render)
green on the mock tier; e2e/ wired into tsc, eslint, and prettier
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
List pages and the flow detail page no longer replace live data with a
full-page error on a failed background refetch — the error/redirect branches
now gate on the absence of data, not on the error alone. On a cold-cache load
failure the flow detail page renders an in-page ErrorState + Retry instead of
silently redirecting to the list, while a genuine not-found still redirects.
Retrying after a failed initial load shows the spinner rather than flashing
the empty state.
Reconnect reconcile: catch the aggregate refetch rejection (the per-query
wrapper doesn't cover it) so a transient failure during the sweep can't
surface as an unhandledrejection, and re-hydrate the REST-backed resources
slot — which the observable-query sweep skips — via a ws:reconnected event.
The flow provider gates its blocking spinner and subscription teardown to the
initial load only, so a background reconcile no longer overlays the page or
bounces the live subscriptions.
Terminal: clear() cancels any in-flight chunked write so its trailing chunks
can't land after the buffer was cleared; the search Clear (X) resets only the
query and keeps the task/subtask filter.
Smaller fixes: role="alert" on ErrorState, a keyboard-operable per-agent Test
control, the correct prompt-validation Alert variant, and dashboard error
props. Shared LoadingState and DashboardError components collapse duplicated
loading/error blocks; terminal write cancellation and the Apollo cache
policies both gain test coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The GraphQL subscriptions are delta-only: the server registers a subscriber
for future events and never replays what it published while a client was
disconnected (and it drops events for a disconnected subscriber outright).
Nothing on the client refetched after the socket came back, so a websocket
drop during an active flow — a network blip, a laptop waking, a proxy timeout
— left a permanent hole in the streamed logs/messages/status until the user
manually reloaded the page.
Refetch the active queries on reconnect: graphql-ws hands `wasRetry` to the
`connected` handler, so on a retry (not the initial connect) call
`refetchObservableQueries()`, which re-runs the flow's queries and merges the
full current state back into the cache.
Verified live with a faithful drop (patched WebSocket, real `ws.close()` mid-
flow while the agent kept producing): before the fix the messages created
during the outage stayed missing after reconnect and only a reload recovered
them; with the fix they reappear automatically on reconnect, no reload. (A
CDP "offline" emulation does NOT reproduce this — it buffers the socket rather
than closing it, so use a real close when testing.) 1006 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Round out the subscription coverage with createStreamingLink — the piece that
coalesces streamed assistant token updates. Export it (test-only) and drive it
with a controllable source plus a mocked clock.
Locks the three behaviors that make token streaming feel right: append parts
inside the 50ms throttle window are accumulated but not re-emitted (so the UI
isn't hammered), the next emission past the window carries the full running
message rather than the latest delta, and the final non-append part flushes the
accumulated total and clears the per-id cache so the same id can stream again.
Also confirms a non-assistant-log result passes straight through untouched.
1006 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The link that folds live subscription events into the Apollo cache
(updateCacheForSubscription) had no coverage — MockedProvider replaces the
whole link chain, so it can't exercise this. Export it and drive it directly
against a real InMemoryCache configured like production (flow-scoped list
fields keyed by flowId).
Locks the routing rules: *Added appends + de-dups by id, *Created prepends
(newest first — not re-sorted by id), *Deleted removes by id, *Updated merges
an entity's fields in place without reordering (and appends if it's not yet
cached). Also covers the two subtle bits: flowId variant isolation (an event
for one flow leaves another flow's list untouched) and type-tolerant id
de-dup (a numeric subscription id matches a string id from REST hydration).
Also exports createSubscriptionCacheLink for future frame-level tests. Pure
additive exports, no runtime change; app verified still loading. 1003 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The flow detail query runs with errorPolicy:'all', so a failed sibling field
(the flow's log lists are separate, nullable root query fields — terminalLogs,
messageLogs, etc.) surfaces an Apollo error while `flow` itself resolves fine.
Both the redirect effect and the provider's load-error toast keyed off "any
error present", so one flaky log resolver would kick the user back to /flows
and toast "Failed to load flow" over a flow that had actually loaded.
Gate both on the flow genuinely being absent (`!flowData?.flow`) instead of on
the presence of an error. A missing/invalid flow still redirects and toasts
(its non-null `flow` field propagates to a null result); a partial failure now
renders the flow and lets the affected panel show its own empty state.
Reachability confirmed from the schema (sibling log fields are `[X!]`, nullable;
`flow` is `Flow!`, non-null) — end-to-end confirmation folded into the P2-S5
live run. 996 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>