Commit Graph
970 Commits
Author SHA1 Message Date
Sergey KozyrenkoandClaude Opus 4.8 cf28128fe3 fix(auth): cap passwords at the 72 bytes bcrypt can hash
Both sides advertised 8–100 characters, but bcrypt.GenerateFromPassword refuses
anything over 72 bytes — and it runs after validation has passed. Creating a user
with an 84-character password answered 500 Internal, and a 73-character password
change answered a generic "New password does not meet requirements" that no field
message explained.

Validation now rejects over-long passwords as input, in bytes rather than runes so
a 40-character multibyte passphrase cannot slip past a rune-counting limit and fail
inside bcrypt anyway. The zod schema and the documented policy follow the same 72.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 05:10:22 +07:00
Sergey KozyrenkoandClaude Opus 4.8 fe812a77e2 test(e2e): let the three editors' Save reach the wire
The prompt, template and knowledge editors all stopped at the same boundary: the
suite proved a document loads and survives an in-memory round trip, but no spec
ever let Save issue its mutation. That is the one operation that can destroy a
user's Go template, and the form→mutation hop — dirty tracking, raw/rich mode,
which field is sent — was covered by nothing.

Each spec now edits, saves, and asserts the request: updatePrompt/createPrompt
carries the edit plus every template atom (heading, {{.Target}}, the bash fence,
the table row), updateFlowTemplate the same for {{TARGET}}, and
updateKnowledgeDocument the edited content with the document id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 04:54:33 +07:00
Sergey KozyrenkoandClaude Opus 4.8 548cc12488 test(e2e): pin the provider create payload, including the fields that get dropped
The provider editor's whole write path had no oracle: createProvider,
updateProvider, deleteProvider and the two test mutations were absent from every
spec. The failure this guards is not hypothetical — the same class (a zero-ish
agent field silently lost between the form and the wire) was found and fixed in
the converter one round ago, with nothing left behind to keep it fixed.

The cassette's defaults now carry `json: true`, `n: 1` and a real maxTokens, and
the spec asserts the createProvider variables still hold them after the form
round trip, plus that all thirteen agents are serialised rather than only the
one the user touched. Dropping `n` from transformFormToGraphQL turns it red.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 04:48:08 +07:00
Sergey KozyrenkoandClaude Opus 4.8 7a56dc53aa test(e2e): pin every resources write verb and the replace guard
mkdir was the only write the suite drove; move, copy, delete and download had no
oracle at all. That matters more here than the usual coverage argument: these
paths also serve the SPA, so a wrong method answers 200 with HTML instead of
404 — the failure mode looks like success. Each new test pins the method and the
payload: rename → PUT /resources/move with the typed destination, copy → POST
/resources/copy, delete → DELETE with the row path in paths[], download → the
href the row builds.

The fifth case covers the guard that has to stand between a user and data loss:
copying onto an existing name must raise "Replace existing item?" and send
nothing at all until the second decision — asserted by counting requests, not by
reading the dialog.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 04:43:42 +07:00
Sergey KozyrenkoandClaude Opus 4.8 7a86cb0a44 refactor(e2e): address the message id by slot, not by a test id
data-testid existed in exactly one place in product code — the message-id badge
this suite added — while every other primitive in the app is addressed through
the shadcn `data-slot` attribute. Switch it to `data-slot="flow-message-id"` and
move the five specs and the helper onto a selector constant. The remaining
data-testid occurrences are test-local stubs inside *.test.tsx files, not
product surface.

130 mock-tier tests still pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 04:35:39 +07:00
Sergey KozyrenkoandClaude Opus 4.8 a7b1533ceb Merge branch 'feature/next-release' into ui/stage-1-non-flows
langchaingo v0.1.14-update.6 is published, so the local scaffolding is gone: the
path `replace` to the working copy, the Dockerfile's vendored-build guard and
the 187 MB backend/vendor tree. The branch already pins the released version and
carries the go.mod it needs (ollama v0.23, sergi/go-diff, jsonschema), so the
backend builds straight from the module proxy.

Merged clean — no conflicts. The two sides barely overlap: 180 files, all
backend/cmd/examples, against this branch's frontend, e2e and CI work. The four
agent-config fields added here (minP/n/json/responseMimeType) and their converter
mapping survive intact, and schema.graphqls is untouched by the merge, so the
generated frontend types stay fresh.

Verified on the merge result: backend builds against the published module,
go test passes everywhere except cmd/installer, which fails the same three
cases on feature/next-release alone (pre-existing, not from this merge).
TestConvertModels_OffCapability, which failed while the local fork was in
place, now passes. Frontend: tsc -b, eslint, prettier, 1104 vitest, 130
mock-tier e2e.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 00:32:03 +07:00
Sergey KozyrenkoandClaude Opus 4.8 4e6fd844f4 fix(e2e): type the codegen-gate fixture shas so tsc -b passes
The Record<string, string> index type made every sha string | undefined under
noUncheckedIndexedAccess, which vitest and eslint did not see but tsc -b did.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 00:08:29 +07:00
Sergey KozyrenkoandClaude Opus 4.8 ae1d600c2c fix(providers): stop the GraphQL round trip from dropping four agent options
The converter maps 13 keys of pconfig.AgentConfig; min_p, n, json and
response_mime_type were never among them, and CreateProvider/UpdateProvider
replace the whole row, so saving a provider through the UI persisted a config
with those keys gone. The one that bites out of the box is `json`: openai's
shipped simple_json default carries it, pconfig turns key-presence into
llms.WithJSONMode(), and a user-defined provider built from that same default
therefore called the LLM without JSON mode while the built-in one did not.

Expose the four on AgentConfig/AgentConfigInput and map them both ways. From
GraphQL a zero must be written as an absent key, not as a zero value —
BuildOptions gates on presence, so `json: false` would otherwise switch JSON
mode ON and `n: 0` would emit an invalid request parameter. The form carries
them through untouched; none is user-editable yet.

The behavioural test takes openai's shipped default through the round trip and
asserts JSON mode still reaches the call — it fails on the old converter.

Note: TestConvertModels_OffCapability fails in this working tree both before and
after this change; it depends on the local langchaingo checkout, not on this.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 00:08:13 +07:00
Sergey KozyrenkoandClaude Opus 4.8 d904a6997a docs: correct two claims the code does not honour
e2e.md promised "unmatched calls fail the test" without qualification, but only
HTTP is gated: an unmatched subscription is deliberately answered with silence
and merely attached as a diagnostic, because a flow page opens ~15 of them and
erroring on the unmocked ones would poison the client. A typo'd subscription key
therefore surfaces as a UI timeout, which is the opposite of what the doc led a
contributor to expect. Say which half is gated and where the missed operations
show up.

list_detail_pages.md named /settings/api-tokens as the sole exception to the
list/detail pattern, so a reader looking to extend /settings/providers/:id would
grep for a use-provider-detail-navigation.ts that never existed — as would one
looking at /settings/prompts/:name. Both are single-entity forms; name them, and
add the two phantom hooks to the doc's own do-not-reintroduce table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 00:00:32 +07:00
Sergey KozyrenkoandClaude Opus 4.8 b6e6a307b3 test(e2e): assert the expiry an api-token create actually sends
The cassette pins the mutation on `input.name` only, because ttl is derived from
the clock and cannot be matched exactly — and the subset matcher then accepts any
ttl at all. Every post-submit assertion reads cassette-fixed data, so a
calculateTTL regressed to its 60-second floor (or off by a factor of 1000) still
passed. Assert the value on the request instead, with the band derived from
CASSETTE_EPOCH and the date the picker clicks rather than hardcoded. Clamping
calculateTTL to 60 now fails the spec with 60 vs >387800.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 23:59:54 +07:00
Sergey KozyrenkoandClaude Opus 4.8 ef53946607 test(e2e): pin the REST re-hydration the reconnect spec is supposed to own
After a socket drop the GraphQL sweep skips the resources slot — it is
cache-only, and refetchObservableQueries filters that policy out — so the whole
re-hydration rides on the `ws:reconnected` event. The spec asserted the GraphQL
delta and nothing else, and deleting the production dispatch left the entire
suite green. Count the GETs to /api/v1/resources across the drop and require
exactly one more; counting rather than arming a one-shot expectation matters,
because the mount-time hydration would otherwise satisfy it on its own. With the
dispatch removed the poll now fails 1 vs 2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 23:59:54 +07:00
Sergey KozyrenkoandClaude Opus 4.8 17198bf073 fix(e2e): sweep the panels the manifest claims to cover, and own the shared provider
Three holes in the route manifest, one class: a gate that reports green on a
surface it never mounts.

- FLOW_DETAIL_TABS listed 8 of the flow detail's 10 desktop panels. Terminal is
  the default and gets scanned anyway, but Automation was mounted by nothing —
  no axe scan, no palette scan — so a regression confined to it stayed invisible.
- /dashboard declared no tabs at all although it is a two-tab Radix Tabs, and
  Radix unmounts the inactive panel: Overview, four Cards and two usage tables,
  was never scanned. The same regression in Analytics would have been caught.
- Its `ready` was a ChartCard heading rendered above the component's own loading
  branch, which is exactly what the tabs contract forbids: the sweep could start
  against spinners. Gate on a data row instead.
- templates-provider.tsx was owned by /templates alone while flow-form.tsx
  consumes it from /flows/5, so affected-routes scoped a provider change away
  from the route that renders the consumer.

All four panels pass axe and palette in both themes as-is. The ownership case is
pinned against the real manifest, not the fake one, and fails before the fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 23:56:04 +07:00
Sergey KozyrenkoandClaude Opus 4.8 b56833c2ea fix(e2e): key the GraphQL cursor on the matched entries, like the REST half
The REST cursor was moved off the raw request body because a per-call volatile
field fragmented the sequence into a fresh cursor every time, so step one was
re-served forever. The GraphQL half kept keying on the raw variables and has the
same hole — an operation like createAPIToken, whose ttl is computed from the
clock, cannot be sequenced today. Key on the selected entries' own pins instead;
variable-pinned sequences (the dashboard's week/month stats) stay separated
because their entries pin different variables. The first new case fails at HEAD
with step one served twice; the second pins the property the key must not break.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 23:56:04 +07:00
Sergey KozyrenkoandClaude Opus 4.8 9f4edc64c0 build: keep Playwright output and tsbuildinfo out of the docker context
The Tier-2 runner builds with the repo root as context, and the same script
rewrites e2e/.auth/user.json and playwright-report/ on every run, so `COPY
frontend/ .` missed cache every time and re-ran pnpm install, license-checker
and the frontend build. frontend/.gitignore already enumerates the four output
directories; mirror them, plus the tsbuildinfo files a local typecheck leaves.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 23:53:21 +07:00
Sergey KozyrenkoandClaude Opus 4.8 37058c8991 fix(ci): surface a visual failure when the baseline is missing, not only when it differs
Playwright writes a -diff.png when a snapshot differs, but a missing baseline
takes handleMissing, which writes only -actual.png. The detection step looked
for -diff.png alone, so a PR that adds a manifest route without regenerating
baselines uploaded no artifact and the sticky comment carried no advisory at
all. Match -actual.png too — both names are written exclusively by the snapshot
matcher, so a non-snapshot failure (which leaves test-failed-N.png) still
reports nothing — and say in the comment that the missing case uploads actual
only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 23:53:21 +07:00
Sergey KozyrenkoandClaude Opus 4.8 948f4742b9 fix(ci): gate the stand job on repo identity, not on a nullable fork flag
`head.repo.fork == false` is not null-safe: a PR whose head fork was deleted
arrives with `head.repo: null`, and GitHub coerces null and false alike to 0, so
the guard passed and a labelled fork PR would enter the protected environment,
hold a reviewer, then fail on the secrets GitHub withholds from fork runs — the
exact outcome the guard's own comment says it prevents. Comparing head.repo
full_name to github.repository yields false for a live fork and for a deleted
one, and it also survives this repo ever becoming a fork itself.

Evaluated both expressions with @actions/expressions against the real workflow
file: the only row that moves is the deleted fork, true -> false.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 23:53:21 +07:00
Sergey KozyrenkoandClaude Opus 4.8 18abc46162 fix(settings): stop a background refetch from wiping unsaved agent edits
keepDirtyValues covers reset(), and the create form seeds its agents with
setValue, which ignores it. So on /settings/providers/new?type=… every field
under agents.* snapped back to the type's defaults whenever a settingsProviders
result landed that differed from the rendered one — a reconnect refetch, another
session touching a provider, a changed model catalogue — while the dirty name
and type survived, making the loss look arbitrary. Skip the re-seed when the
type has not changed and the agents subtree is already dirty; a real type switch
still re-seeds, since the previous type's models and defaults are wrong for it.

The create-path twin of the existing edit-path test fails on the old code with
the edited temperature back at the default; a second case pins the type switch,
which a plain dirty-guard would have broken.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 23:50:34 +07:00
Sergey KozyrenkoandClaude Opus 4.8 0030bfb6cd fix(ui): give link text its own token instead of reusing the fill colour
--primary is a fill paired with white --primary-foreground, and as text on dark
it measures 3.12:1 on --card (2.89:1 through the upload overlay's tint) — below
AA. There is no lightness that clears AA in both roles: text needs L>=0.59 and
white-on-fill needs L<=0.545, so the window is empty. Add --link, measured live
at 6.34:1 on --card in dark and 15.59:1 in light, and point the link button
variant, the table/file-manager/prompt hover links and the two drag overlays at
it. The icon at flow-assistant-messages.tsx keeps --primary: it is a graphic and
clears the 3:1 bar.

The gate probed exactly one button variant, so the failure had been living under
a waiver instead: probe every variant off the union the way the badge half
already does, and drop the color-contrast waiver on the Screenshots source URL —
that node is the link variant, and axe passes it now. The dark link row fails at
3.12 against the previous colour.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 23:46:51 +07:00
Sergey KozyrenkoandClaude Opus 4.8 7fcecdc6b4 fix(e2e): give the review sandbox a root node_modules can be hardlinked into
--with-deps hardlinks node_modules in, and a hardlink cannot cross filesystems,
so on a host whose $TMPDIR is its own mount the flag was unusable: `cp -al`
fails and the ERR trap tears the half-built worktree down, leaving the caller
with an empty $SANDBOX. Reproduced on a real second filesystem (attached image,
st_dev 16777238 vs 16777234): the old script dies with "Cross-device link" at
rc=1, the new one probes with a real link first, reports the fallback on stderr
and builds the sandbox beside the repo. Same-filesystem hosts keep using
$TMPDIR untouched, and PENTAGI_SANDBOX_ROOT overrides both.

`clean` accepts a path under either root, so a sandbox built through the
fallback can still be removed by the containment check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 22:54:42 +07:00
Sergey KozyrenkoandClaude Opus 4.8 aa5378b9b2 fix(e2e): contain what the review sandbox tool is willing to delete
`clean <path>` ran `git worktree remove --force || rm -rf` on whatever it was
handed. git exits 128 both for the main working tree and for any directory that
is not a worktree, so the rm fallback is what runs for every wrong argument, and
sitting left of `||` it never trips `set -e` — `clean <repo-root>` erased the
checkout including .git, and `clean <unrelated dir>` erased it silently at rc=0
because the git error was redirected away. Resolve the target and refuse
anything that is not a child of the sandbox root, and let git's refusal print.

The no-argument form was the other half: it wiped the whole root, which every
agent on the host shares, so the documented copy-paste flow with an unset
$SANDBOX took out concurrent live sandboxes. The sweep is now the explicit
`clean --all` and only removes what has been untouched for two hours; a bare
`clean` is an error.

Four of the six new cases fail against the old script.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 22:51:02 +07:00
Sergey KozyrenkoandClaude Opus 4.8 b58a40dd72 fix(ci): pick the codegen-freshness range from the PR base, not the last push
On pull_request, `github.event.before` is the PR's previous head, so the second
push to a PR diffs a range that no longer contains the commit that edited the
schema: the detector emits changed=false, the freshness step is skipped by its
`if:`, and lint-and-test goes green on a stale src/graphql/types.ts. Every
later push that does not itself touch one of the five inputs repeats it, forks
included — which is the case the trigger was added for. Nothing else covers the
gap: tsc reads the committed file as truth, eslint ignores it, and prettier
passes on codegen's own output.

Diff from pull_request.base.sha on PR events and keep `before` for push. The
range selection moves out of the YAML into .github/scripts so it can be driven
over real git fixtures: on the three-commit fixture the old range yields
changed=false and the new one changed=true.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 22:46:19 +07:00
Sergey KozyrenkoandClaude Opus 4.8 24e0529379 perf(editor): stop auto-detecting the language of unlabelled code blocks
CodeBlockLowlight had no defaultLanguage, so a fence with no info string fell
to highlightAuto — a scan of all 37 `common` grammars — and the plugin redoes
every code block in the document whenever the doc changes with the caret inside
one. Measured on the stand, per keystroke: 12 ms at 20 lines, 33 ms at 60,
62 ms at 120, and 33 ms for three 20-line blocks (it is the document total that
counts, not the block). With the plaintext grammar the same edits cost 0.6-0.9 ms.

The trade is that an undeclared fence now renders monochrome instead of being
coloured by a guess, which matches how the language caption already behaves.
Declared fences are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 22:40:51 +07:00
Sergey KozyrenkoandClaude Opus 4.8 a6fc854e0b fix(editor): keep highlight tokens readable inside code blocks
Syntax highlighting handed the code-block surface to atom-one-dark, which is
#282c34 in both themes, but the `{{.Var}}` / `<tag>` view decorations kept
their theme tokens — dark ink meant for a light ground. Measured on the stand:
a tag inside a fence sits at 1.16:1 in light theme (a variable at 1.72), and
dark had slipped to 4.05, below AA. Outside a fence both are unaffected.
Re-point the two tokens at the dark ground inside `pre`, where they measure
5.77 / 5.30 in both themes.

The contrast gate could not see this: it mounts probes on a synthetic --card
surface and composited only the probe's immediate parent, so a span inside a
transparent `code` measured against nothing. Walk to the first opaque ancestor
instead, and pin the real rendered spans in the prompt-detail editor — the
cassette's bash fence now carries a tag as well as a variable. Both new rows
fail on the previous CSS with exactly the numbers above.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 22:36:23 +07:00
Dmitry Ng c257dabc57 feat(config): update hcnsec provider configuration and test report
- Replaced all instances of `Qwen3-Coder-Next-FP8` with `MiniMax-M2.7` in the hcnsec provider configuration, adjusting model settings and pricing accordingly.
- Updated the test report to reflect the new model configurations, including success rates and average latencies for various agents using `MiniMax-M2.7`.
- Enhanced the overall structure of the configuration file for clarity and consistency.
2026-07-25 15:02:49 +03:00
Dmitry Ng 2d87a1f8fe feat(retries): implement callWithSetupRetries for transient error handling
- Introduced `callWithSetupRetries` function to enhance error resilience during LLM prompt calls, allowing for retries on transient errors with backoff.
- Updated `NewFlowProvider` and `NewAssistantProvider` methods to utilize `callWithSetupRetries` instead of direct calls to `prv.Call`, improving stability in flow and assistant creation.
- Added comprehensive unit tests for `callWithSetupRetries`, covering immediate success, transient error handling, and context cancellation scenarios.
- Enhanced error messages in Graphiti search tools to provide actionable feedback for missing or malformed parameters.
- Updated templates to clarify search type requirements and taxonomy references.
2026-07-25 13:30:12 +03:00
Sergey KozyrenkoandClaude Opus 4.8 a7c0c19b53 chore(e2e): trim narration and self-justifying comments; drop a stale one
An audit of the comments added since the feature base flagged a batch of
justification openers ("Not a copy-paste", "Live entry not dead weight"),
"there's a test for it" cross-refs, and how-we-found-it war-stories. Cut the
narration and keep the load-bearing fact in each. One was actively wrong:
playwright.config.ts still described the `maxDiffPixelRatio` anti-aliasing
budget that 5cec81b replaced with an absolute `maxDiffPixels` — removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:12:46 +07:00
Dmitry Ng d9b28ce8bf chore(dependencies): update langchaingo version and adjust model configurations
- Updated the `langchaingo` dependency in `go.mod` from version `v0.1.15-0.20260723091023-35da5c0f0620` to `v0.1.14-update.6`.
- Modified the model configurations in `config.yml` and `models.yml` to replace `claude-opus-4-8` with `claude-opus-5` for both the generator and refiner sections.
- Updated the model descriptions and pricing in `models.yml` to reflect the new `claude-opus-5` model and added new models `claude-sonnet-4-6` and `claude-haiku-4-5`.
- Adjusted the test report in `anthropic-report.md` to reflect the updated model names and improved average latencies across various tests.
2026-07-25 02:52:04 +03:00
Dmitry Ng 5a4f1f54dc feat(search): introduce internal analytics engine for web search
- Added configuration options for an optional internal analytics engine in `.env.example`, `docker-compose.yml`, and related files.
- Updated documentation to include details about the new internal analytics engine, which scrapes and summarizes pages as a fallback for the web_search tool.
- Enhanced the `ftester` to support testing the new internal engine alongside existing search engines.
- Adjusted various components to integrate the new engine, including configuration forms and validation logic.

This feature allows users to enable a browser-based fallback for analytic queries, enhancing the flexibility of the web search capabilities.
2026-07-25 01:16:18 +03:00
Sergey KozyrenkoandClaude Opus 4.8 9649e7f313 fix(ci): route the sticky comment by PR identity on the primary path too
280d7b9 filtered the fork-fallback PR lookup by head identity but left the
primary same-repo path taking `run.pull_requests[0]` — one commit can belong to
several same-repo PRs, so the first listed could collect another branch's
report. Apply one identity filter to both sources. workflow_run.pull_requests
carries head.repo.name but no full_name, so the repo is enforced only when
present, keeping the primary path matching on head sha + ref.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 04:13:26 +07:00
Sergey KozyrenkoandClaude Opus 4.8 dd5b4e04d5 docs(e2e): correct the stand-tier trigger and gate model
The doc told readers to dispatch the workflow "with tier: stand", an input
fc15ec8 removed — workflow_dispatch takes none — and claimed the protected
Environment is what stops a fork or mislabeled run from reaching secrets. It is
not: the job's `if` (label name plus a non-fork head, added in 7b1ecfd) skips
before the Environment is ever evaluated. Describe the real two-gate model: the
`if` first, then the Environment for a run that clears it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 04:12:02 +07:00
Sergey KozyrenkoandClaude Opus 4.8 551418136b test(e2e): key the REST cursor on the selected entries, not the raw body
The sequencing cursor embedded the whole request body, so an endpoint whose
body carries a per-call volatile field — an idempotency key, a timestamp — got
a different cursor key on every call and re-served entry one forever, never
advancing the sequence. Key on a signature of the matched entries' pins instead:
two genuinely different sequences still separate (their entries pin different
subsets), but a volatile field no longer fragments a single sequence. Unit test
covers it; it fails on the old body-keyed cursor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 04:11:13 +07:00
Sergey KozyrenkoandClaude Opus 4.8 9bdd41c63b test(e2e): assert the resources empty-state upload CTA, not just the title
The "shows the upload call to action" test asserted only the "No resources yet"
title — the sole empty-state surface in the suite — so dropping the drop-zone's
actionLabel kept it green. Assert the CTA button, scoped to the drop zone (its
size hint is unique) so the toolbar's Upload button is not mistaken for it.
Removing actionLabel now fails the test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 04:08:42 +07:00
Sergey KozyrenkoandClaude Opus 4.8 bdce2bfb04 test(e2e): require the button tag on the /resources Select waiver
The target-size waiver matched a bare `aria-label="Select `, so any control on
the page labelled "Select …" — not just the file-manager row-select buttons —
was waived. axe emits those buttons as `button[aria-label="Select <name>"]` with
no parent path, so the label is the only anchor available; requiring the button
tag at least stops a future non-button "Select …" control from slipping the
rule. /resources a11y stays green; a div/anchor "Select" is now caught.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 04:04:00 +07:00
Sergey KozyrenkoandClaude Opus 4.8 3746029825 style(e2e): drop a duplicated waiver comment and apply prettier
Removes a stale "revisit with the design pass" comment left above the rewritten
scoped-waiver block in routes.ts, and applies the formatting the repo's own
`pnpm run prettier` wants across the a11y, palette and provider-form files —
per-file prettier checks were missing a plugin the package script loads, so a
few of this session's commits landed formatting the gate would flag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 04:01:37 +07:00
Sergey KozyrenkoandClaude Opus 4.8 74099d2bf4 fix(e2e): carry binaries and untracked files into the review sandbox
`create --dirty` piped `git diff HEAD` without --binary and copied no untracked
files, so a regenerated PNG baseline aborted the patch (leaving a half-built
worktree) and a new spec never reached the sandbox — and because the failure
printed onto stdout, the caller captured an error string as the sandbox path.
Use --binary, copy untracked non-ignored files separately, and trap ERR to tear
the worktree down and report on stderr. Verified with a modified baseline plus a
new untracked spec: both now arrive in the sandbox.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 04:00:57 +07:00
Sergey KozyrenkoandClaude Opus 4.8 5f7a2285be test(e2e): count palette offenders instead of deduping them
The gate compared `[...new Set(offenders)]` against ACCEPTED, so a waiver that
names one node absorbed any number of nodes carrying the same token — a second
element reusing an already-waived off-palette class stayed green, against the
"waive one node, not a rule" contract. Compare the sorted offender list
directly: two nodes with a waived token now fail where one is accepted. All 18
scans stay green (each waived token appears exactly once today).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 03:57:53 +07:00
Sergey KozyrenkoandClaude Opus 4.8 4867ad2d6c test(e2e): make the palette gate see absolute colours
PALETTE_UTILITY matched only numbered hues and arbitrary values, so text-white,
text-black and bg-transparent were invisible to it: an overlay like
`cn(badgeVariants({variant:'blue'}), 'text-white')` on a badge went unmeasured.
Add white/black/transparent (with optional opacity) to the colour alternation.
No badge or button variant uses them, so all 18 scans stay green; injecting a
text-white overlay on a rendered badge now fails the gate as an off-palette node.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 03:53:54 +07:00
Sergey KozyrenkoandClaude Opus 4.8 44d471f290 test(e2e): give the stand smoke data assertion a real-backend timeout
The only data assertion on the stand tier ran on Playwright's 5s default expect
timeout — the config overrides only the screenshot timeout — so a healthy but
slow stand backend serving the list a little late would fail the smoke falsely.
Give it a 30s budget, matching the real-backend nature of the tier.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 03:48:24 +07:00
Sergey KozyrenkoandClaude Opus 4.8 dc7eedfae4 fix(ci): run the lint/codegen/test gate on pull requests, not only pushes
ci.yml triggered only on push, but a fork PR's commits never push to this repo,
so its lint-and-test job (eslint, vitest, and the GraphQL codegen-freshness
check) never ran for forks. e2e.yml, the only workflow a fork PR did trigger,
builds with tsc — which reads the committed types.ts and cannot see it go stale.
So a fork editing schema.graphqls without regenerating, or shipping a lint/test
failure, could merge green.

Add a pull_request trigger (lint-and-test uses no secrets and is fork-safe) and
narrow push to main + tags, which is all docker-build — gated to those refs
already — needs. A concurrency group drops superseded PR-sync runs. The
codegen-detect step already treats an empty `github.event.before` as "check
anyway", so pull_request events always run the freshness check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 03:47:12 +07:00
Sergey KozyrenkoandClaude Opus 4.8 0d4232a1d5 test(e2e): fill the provider cassettes with the shape the backend emits
The settings-providers cassettes left nine of eleven default configs null and
every model catalog empty. Verified against the live backend that this is a
shape it never sends: gemini/deepseek/ollama all return a non-null default and a
populated model catalog even with zero keys configured. The empty catalogs made
the create-form's model-dropdown seeding path structurally unreachable — no spec
could reach it.

Give every type a default config and a two-entry catalog, and add a spec that
picks a type and opens an agent's model dropdown: it asserts a catalog-only
model appears there. That option is gone when the catalog is empty, so the spec
fails on the old backend-impossible shape — the coverage that was missing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 03:45:20 +07:00
Sergey KozyrenkoandClaude Opus 4.8 b4f50ba3f0 test(e2e): scope flow-detail a11y waivers to the tab that owns the debt
The waivers were passed to all nine /flows/5 scans (base + 8 tabs), so a
Files-tab waiver silenced its rule on every other tab: a new violation of the
same shape on Assistant would stay green. Add an optional `tabs` to A11yWaiver —
omitted means route-wide (the by-design message metadata), a list means only
those tabs — and filter per scan.

The gate itself supplied the real ownership once scoped: the progressbar-name
waiver belongs to Tasks, the truncate-contrast one to Screenshots, and
table-container scrollable to Dashboard as well as Files — all previously masked.
Proven by injecting a Files-shaped table-container into the Assistant panel: it
now fails the Assistant scan where the route-wide waiver used to absorb it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 03:28:34 +07:00
Sergey KozyrenkoandClaude Opus 4.8 fab990d9b9 fix(ci): don't post a green e2e comment when the run's result is unconfirmed
The sticky comment computed `mockPassed` by falling back to results.json alone
whenever the jobs API was unreachable. A run killed by the global timeout writes
its results.json before the abort is observed — the un-run tests land in
`skipped`, so `unexpected` stays 0 — so that fallback posted a  on a run that
never finished.

Model three states instead of a boolean: pass (zero failures AND the job
confirmed success), fail, and unknown (jobs API unreachable, so the job cannot
be confirmed). Unknown renders ⚠️ with a note to check the run, never .
Verified by driving the extracted script over a timeout-shaped results.json with
a rejecting jobs API: it now yields ⚠️ unknown where it used to yield .

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 03:19:04 +07:00
Sergey KozyrenkoandClaude Opus 4.8 81f3d06ebd fix(settings): keep unsaved provider-form edits across a background refetch
The provider detail form was the only one of the three detail forms created
without `resetOptions: { keepDirtyValues: true }`. Its seeding effect keys on
`data`, and settingsProviders is cache-and-network + replaceWithIncoming, so a
background refetch (list→detail race, or a provider* subscription) delivers a
fresh `data` reference mid-edit and the plain reset() silently wipes whatever
the user was typing. settings-prompt and template already guard this, the
latter with a comment naming the exact hazard.

Live-reproduced on the docker branch image: an edit typed during a delayed
refetch reverted to the server value. Regression test delivers a fresh data
reference after an edit and asserts it survives — it fails without the option.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 03:17:47 +07:00
Sergey KozyrenkoandClaude Opus 4.8 e31c725cc0 style(e2e): apply prettier to the pager-switch cassette and spec
`pnpm run prettier` (which CI gates on) failed on these two files — line
wrapping only, in edits from the M5 pager-switch work. The branch tip was red
until this. No behaviour change; `prettier --check` over the {src,e2e} glob is
clean now.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 03:14:45 +07:00
Sergey KozyrenkoandClaude Opus 4.8 280d7b9231 fix(ci): route the sticky comment by PR identity, not by list order
The fork fallback took `prs[0]` from the commit's PR associations without
checking any of them is still this run's head, so a stale association could
collect another branch's report. Match on head sha, ref and repository, and skip
when nothing matches. Verified against a synthesised association list where the
stale PR is listed first: the old lookup picks it, the new one does not.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 19:34:09 +07:00
Sergey KozyrenkoandClaude Opus 4.8 162c84d99a test(e2e): let a cassette entry pin the request's query string
The REST gate matched on method and path alone, so an endpoint that carries its
payload in the query and sends no body could not be pinned — `bodySubset` has
nothing to bite on, and the path hit answered success whatever the request
named. Unmatched-call diagnostics dropped the query too, so a miss printed a
path that looked right.

Add `querySubset`, the counterpart to `bodySubset`, thread the query into the
matcher and the sequencing cursor, and report it on a miss. No spec exercises a
query-carrying endpoint yet; the unit test pins the behaviour and fails when the
new filter is removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 19:34:09 +07:00
Sergey KozyrenkoandClaude Opus 4.8 fadde5ad5a fix(e2e): composite text colour onto its own ground before measuring contrast
`paint()` clears to transparent and the label was painted alone, so a
translucent text token kept its opaque base: 30% black on white measured 21:1
instead of 2.12:1 — a failing token reported as perfect contrast. Paint the
label on the same layer stack as the surface.

No probed token is translucent today, so every current reading is unchanged
(7/7 still pass); this stops the helper lying about the next one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 19:34:09 +07:00
Sergey KozyrenkoandClaude Opus 4.8 dbfdbe0e8d test(e2e): pair the terminal message with the type the backend emits
The cassette gave entry 503 a Terminal body under type Report — a combination
`getMessageType` never produces, since the terminal tool's output carries the
terminal type. It mattered: only a report auto-expands, so the spec's two
xterm assertions passed on a panel that was open for the wrong reason.

With the realistic pairing the message starts collapsed, so the spec now walks
the path a user does — assert one xterm, open the details, assert the second
and its content.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 19:34:08 +07:00
Sergey KozyrenkoandClaude Opus 4.8 c906539a3d test(e2e): give review agents a sandbox instead of the live checkout
Verifying a claim about a gate means breaking something on purpose, and review
runs kept doing that in the working tree: probe specs left behind under src/ and
e2e/, tracked files edited mid-run, and once an `rm -rf` of a directory the run
had not created. Nothing survived to the commits, but only because each run
happened to clean up after itself.

`review-sandbox.sh` hands out a detached `git worktree` under $TMPDIR, optionally
carrying the uncommitted diff and a hardlinked node_modules so pnpm, vitest and
playwright run there. Verified by doing the damage on purpose inside one — an
edited world.ts, a stray probe spec, a deleted frontend/docs — and confirming
the real tree came back untouched.

node_modules is hardlinked rather than symlinked because pnpm treats a symlinked
modules dir as foreign and tries to purge and reinstall it, which would reach
through the link into the real tree; it aborted here only for want of a TTY.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 19:21:00 +07:00
Sergey KozyrenkoandClaude Opus 4.8 0a8ca104e0 test(e2e): cover the flag-outranks-sequence rule in the cassette matcher
Deleting `eligible`'s second line — the one that makes a newly-enabled flagged
entry outrank the unflagged candidates — left the whole unit tier green. The
existing cases use two entries, where the consumed-index cursor lands on the
flagged entry anyway, so sequencing alone reproduced every asserted progression.

Add a three-candidate case where the two disagree: it passes as written and
fails with that line removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 19:04:48 +07:00