Commit Graph
842 Commits
Author SHA1 Message Date
Sergey KozyrenkoandClaude Opus 4.8 ecfb2029ce fix(e2e): require E2E_BASE_URL for the local tier, like stand
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>
2026-07-24 02:41:02 +07:00
Sergey KozyrenkoandClaude Opus 4.8 aec236d32e fix(ci): gate the stand run on the label just added, not the whole label set
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>
2026-07-24 02:37:15 +07:00
Sergey KozyrenkoandClaude Opus 4.8 b919b7a839 test(e2e): assert the overview panel is unmounted, not merely hidden
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>
2026-07-24 02:36:00 +07:00
Sergey KozyrenkoandClaude Opus 4.8 e9c1c56b8b test(e2e): gate the reconnect test on resubscribe, not just the socket reconnect
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>
2026-07-24 02:28:52 +07:00
Sergey KozyrenkoandClaude Opus 4.8 b6e5564f49 fix(e2e): write a trace on mock-tier failure so the debug path exists
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>
2026-07-24 02:23:56 +07:00
Sergey KozyrenkoandClaude Opus 4.8 8a6eecd6e6 test(e2e): sweep tab panels in the palette gate, not just the default view
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>
2026-07-24 01:22:58 +07:00
Sergey KozyrenkoandClaude Opus 4.8 05e79902d8 fix(ci): read the visual container's Playwright version from the image, not npx
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>
2026-07-24 01:06:40 +07:00
Sergey KozyrenkoandClaude Opus 4.8 184c2b6b8c fix(ci): redact stand secrets from the e2e report before the public upload
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>
2026-07-24 01:04:32 +07:00
Sergey KozyrenkoandClaude Opus 4.8 0d68d1d622 test(e2e): cover the authz-denial redirect + fix vacuous header-order asserts
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>
2026-07-23 16:19:03 +07:00
Sergey KozyrenkoandClaude Opus 4.8 2de6386cb5 fix(templates): don't disable the detail actions on a background refetch
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>
2026-07-23 13:52:15 +07:00
Sergey KozyrenkoandClaude Opus 4.8 f0860a7858 fix(ui): don't treat an authz denial or a partial error as a missing record
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>
2026-07-23 13:52:02 +07:00
Sergey KozyrenkoandClaude Opus 4.8 3d5fc75f7b fix(ui): guard the detail loading branch too, not only the error branch
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>
2026-07-23 02:14:53 +07:00
Sergey KozyrenkoandClaude Opus 4.8 1308f2d010 fix(settings): don't blank a working settings view on a background refetch
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>
2026-07-23 01:05:14 +07:00
Sergey KozyrenkoandClaude Opus 4.8 66d9c835b7 test(e2e): wait for Submit before clicking in the real-flow spec
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>
2026-07-23 00:21:52 +07:00
Sergey KozyrenkoandClaude Opus 4.8 28ab3d2e61 fix(ui): render a retryable error on a detail load failure, not "not found"
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>
2026-07-23 00:21:33 +07:00
Sergey KozyrenkoandClaude Opus 4.8 f95c48b062 refactor(knowledges): give knowledgeId back to the route param
`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>
2026-07-22 18:23:00 +07:00
Sergey KozyrenkoandClaude Opus 4.8 e51f686e5c refactor: drop the header-order comment and the rest of the narration
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>
2026-07-22 18:16:04 +07:00
Sergey KozyrenkoandClaude Opus 4.8 f24b75f350 refactor: cut the comments that only explain the author
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>
2026-07-22 17:55:35 +07:00
Sergey KozyrenkoandClaude Opus 4.8 46457026e8 fix(ui): repair what the always-rendered header exposed on the other two pages
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>
2026-07-22 17:43:35 +07:00
Sergey KozyrenkoandClaude Opus 4.8 15319ef817 fix(ui): reserve the counter width on the label, not on the button
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>
2026-07-22 16:21:38 +07:00
Sergey KozyrenkoandClaude Opus 4.8 ee6ed49d48 fix(ui): keep the header actions in fixed slots instead of a pager prop
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>
2026-07-22 16:05:36 +07:00
Sergey KozyrenkoandClaude Opus 4.8 5b4ded1e3c fix(flows): stop the pager bouncing the user back to the flows list
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>
2026-07-22 16:05:22 +07:00
Sergey KozyrenkoandClaude Opus 4.8 3d04fcfff2 test(e2e): fail on a colour written outside the variant set
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>
2026-07-22 14:06:20 +07:00
Sergey KozyrenkoandClaude Opus 4.8 dfff92bef6 docs: correct two claims in the list/detail guide
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>
2026-07-22 14:06:09 +07:00
Sergey KozyrenkoandClaude Opus 4.8 ecb94b25e7 test(e2e): assert stand routes on data, not only on client-side state
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>
2026-07-22 14:06:09 +07:00
Sergey KozyrenkoandClaude Opus 4.8 04cc821bd6 test(e2e): stop the flows entries owning pages no route renders
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>
2026-07-22 14:06:09 +07:00
Sergey KozyrenkoandClaude Opus 4.8 d58f840702 test(e2e): probe every editor colour token, keyed off the stylesheet
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>
2026-07-22 14:05:55 +07:00
Sergey KozyrenkoandClaude Opus 4.8 908d35f8e4 test(e2e): make every dashboard period query serve its own dataset
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>
2026-07-22 14:05:55 +07:00
Sergey KozyrenkoandClaude Opus 4.8 054f46b0ed test(e2e): gate the reconnect delta and give the dedup assertion a source
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>
2026-07-22 14:05:42 +07:00
Sergey KozyrenkoandClaude Opus 4.8 f6bcff8672 test(e2e): scan the flow tab panels and the populated provider cards
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>
2026-07-22 13:41:35 +07:00
Sergey KozyrenkoandClaude Opus 4.8 ce1fc7049d test(e2e): cover the two detail routes whose editor loads server content
/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>
2026-07-22 13:18:20 +07:00
Sergey KozyrenkoandClaude Opus 4.8 f3311ecadf ci: scope the codegen-freshness check to its inputs and name the fix
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>
2026-07-22 12:59:14 +07:00
Sergey KozyrenkoandClaude Opus 4.8 fdf5c4ea90 test(e2e): close three latent assertion gaps
- 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>
2026-07-22 06:10:18 +07:00
Sergey KozyrenkoandClaude Opus 4.8 678a878855 test(e2e): assert the responsive and a11y-login claims their titles make
- 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>
2026-07-22 06:05:33 +07:00
Sergey KozyrenkoandClaude Opus 4.8 2615510a0d style(e2e): apply prettier to the matcher and auth-setup edits
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 06:00:24 +07:00
Sergey KozyrenkoandClaude Opus 4.8 abef4e29b8 test(e2e): fail the contrast probe on an unparseable colour
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>
2026-07-22 05:58:24 +07:00
Sergey KozyrenkoandClaude Opus 4.8 cb0ec25319 docs(e2e): correct the stand-tier fork guard and document its env vars
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>
2026-07-22 05:55:51 +07:00
Sergey KozyrenkoandClaude Opus 4.8 06f53c72a0 test(e2e): settle editor typing, resources input and the stand login detector
- 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>
2026-07-22 05:54:14 +07:00
Sergey KozyrenkoandClaude Opus 4.8 5803d5adc6 test(e2e): tighten the graphql-ws mock's id reuse, completion test and comment
- 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>
2026-07-22 05:49:12 +07:00
Sergey KozyrenkoandClaude Opus 4.8 b16d4a8b4a test(e2e): harden the e2e tooling and CI freshness checks
- 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>
2026-07-22 05:47:15 +07:00
Sergey KozyrenkoandClaude Opus 4.8 3400e1e7c1 ci(e2e): tighten the visual gate, CI signal integrity, and container guards
- 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>
2026-07-22 05:43:50 +07:00
Sergey KozyrenkoandClaude Opus 4.8 e4b8001499 test(e2e): harden the mock matcher's cursor, keys and stream completion
- 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>
2026-07-22 05:33:07 +07:00
Sergey KozyrenkoandClaude Opus 4.8 b0e69b2c7c test(e2e): seed a realistic session so gated branches enter the sweeps
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>
2026-07-22 05:25:23 +07:00
Sergey KozyrenkoandClaude Opus 4.8 101331c920 test(e2e): fix three cassette-fidelity gaps
- 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>
2026-07-22 05:19:20 +07:00
Sergey KozyrenkoandClaude Opus 4.8 be585b64f4 test(e2e): make the rejected-login cassette answer 401 like the backend
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>
2026-07-22 05:15:05 +07:00
Sergey KozyrenkoandClaude Opus 4.8 b565bf41c3 ci(e2e): stop a PR label from superseding a red required check
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>
2026-07-22 05:10:50 +07:00
Sergey KozyrenkoandClaude Opus 4.8 7b6c85c34f style(editor): trim narration from the code-block CSS comments
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>
2026-07-22 04:21:12 +07:00
Sergey KozyrenkoandClaude Opus 4.8 c635a9fe34 test(converter): cover per-agent extra_body round-trip
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>
2026-07-22 04:03:42 +07:00
Sergey KozyrenkoandClaude Opus 4.8 cae0295e8b feat(providers): expose per-agent extra_body in the provider config
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>
2026-07-22 03:57:23 +07:00
Sergey KozyrenkoandClaude Opus 4.8 648bd1a018 fix(ui): give the detail pager a fixed slot so header actions can't shift it
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>
2026-07-22 03:56:19 +07:00