Repository navigation
scripts: add a browser verification loop for the admin console - #1272
Conversation
|
Thanks for opening this pull request! A SPACE maintainer will take a look. CONTRIBUTING.md explains how we review outside contributions. |
zibi-pplx
left a comment
There was a problem hiding this comment.
A useful review aid. Questions about scope and cost:
-
[medium] Should the "add a step" guidance point to typed
space_smokescenarios?
infra/space/kind/console-loop/README.md:282
steps.tsalready has 43c.rec.checkassertions against a running Kind stack, for example "the role change persists", "searching a full sbx_ ID finds the sandbox", and lifecycle actions reaching a state. The README asks contributors to "add a step tosrc/steps.tsfor each behavior a change introduces … and keep it if it fails before and passes after", which is regression-test guidance. The same README says smoke tests against a deployment are typedspace_smokescenarios, with console scenarios as a follow-up. Should this bullet send behaviour checks that don't need a browser to those scenarios? Steps would then stay for what only a browser can show. That would keep the follow-up from re-implementing the same checks. -
[low] Editing the tool runs the full required Kind smoke
ci/kind-pull-paths.txt:10
The tool lives underinfra/space/kind/**, so any change to it runs the six Kind shards and the producer in pull CI. If #1225 merges, the Direct lane runs too. Meanwhile nothing in CI runs itstypecheckornpm test. Would a location outside that glob fit better (for example besideui/), withnpm run typecheckadded to an existing lint job? -
[low] The seed's recovery scope
infra/space/kind/console-loop/src/seed.ts
About 780 lines handle idempotent replay of interrupted creates, 409 and 422 classification, and operations the API garbage-collects after a week. The README asks for a disposable stack. Could recovery fall back to "deleteout/seed-<prefix>.jsonand reseed", so the seeder shrinks to what a fresh stack needs?
Withdrawing this approval for now. Any feedback above still stands.
infra/space/kind/console-loop/ is a small Node package that checks the admin console (ui/) in a browser against the local Kind stack: - `npm run seed` creates synthetic organizations, users, API keys and sandboxes in each resting state through the public and admin APIs, under a name prefix, and `npm run clean` deletes exactly those. Seeded users get a generated password, kept only in the git-ignored state. - `npm run loop` signs in through the login page, visits every console route, runs the operator flows (create and delete an organization, a user, an API key, and a sandbox through its lifecycle), and records a screenshot, console and page errors, failed and slow requests, error toasts and its own checks for every step, in a JSON summary. - `--compare` (or `npm run compare`) compares two runs by error type and exits 1 when the later run, say a branch, has one the earlier run, say main, does not. - `npm run capture` records pages as a video, and a GIF with ffmpeg, for a pull request description. The README covers the fast loop (`next dev` against the port-forwarded backends), the Tilt-image loop, comparing a branch with main, and using runs as evidence. The Kind runbook, the installation guide and the space-validation local-development reference point to it, and the plugin version is bumped for the reference change. Playwright, TypeScript and the Node types are the package's only dependencies, pinned exactly, outside ui/'s dependencies and image. The scripts run with Node's type stripping. Its node_modules and output are git-ignored and kept out of root-context Docker builds. It is a development tool, not a CI gate; the typed space_smoke console scenarios are a separate follow-up.
- seed, clean, and loop runs that create things refuse an API or console URL that is not loopback unless --allow-remote is passed, and print the target they act on. - clean deletes exactly what seed.json records: the seed and the loop now record every object as soon as it exists. It waits for each sandbox delete operation and counts anything but success as a failure, refuses to delete an org that still holds a child org, user, API key or live sandbox it did not record, reports what it could not delete, keeps it recorded, and exits 1. - Seeding looks every object up by its prefixed name before creating it, scoped to its org, so a rerun after an interruption duplicates nothing; it fails loudly when an org has too many sandboxes for the admin list to tell, and checks every recorded fixture by ID. The loop's first step does the same. Org display names carry the prefix, so two prefixes can share a stack. - The API-key step hides the new key's secret before its dialog opens and checks that it stays hidden, and the recorder replaces anything shaped like a key in what it writes. - Each event belongs to the step that issued its request; a response that lands later is a note, not an event of the next step. - Comparisons keep the step in failed checks and exceptions, and list an error type as "not covered", not resolved, when the later run did not run every step it appeared in. - Development warnings are classified as React 19 and antd print them (format strings with `%s` placeholders, "Warning: …"), and the README says so, along with the loop's scope: development evidence, not a pass/fail gate; smoke tests stay typed space_smoke scenarios.
Every mutating run now reads the cluster row's id through the console's /admin and /auth proxies and through the API, and refuses when they differ or differ from the seed state. The state is per prefix, records each create before it is submitted (with the sandbox idempotency key) and every operation, and clean waits for those operations before it forgets anything. Clean keeps organizations unless --delete-orgs is given, since deleting one cascades to its keys, credentials and grants. The API-key step replaces the secret in the create response with page.route, so the browser never holds a live key. Seeding fails when a fixture cannot be set up and verifies the full set, --only and --skip take exact step names or tags instead of regular expressions, and a late response body is checked against its step again after it is read.
The API deletes finished operations after a week, and the loop treated the resulting 404 as an error, so seed and clean failed for good. A vanished operation is now recorded as gone and the object is judged by its ID; every outcome is recorded once seen and not asked for again. An unanswered seed create is resubmitted with its recorded request, unchanged, so a later --template cannot turn its key into a conflict. A browser sandbox create is marked as submitted from page.route before it leaves, and clean keeps one that left with no answer and no sandbox of its name as unresolved, unless --forget-unresolved is given. Browser lifecycle and delete operations are recorded as they arrive. The API-key step routes every POST without following redirects, replaces any secret in an answer, and clears the page, fails, and deletes a --video recording when a secret could have reached the page another way. Clean now deletes the orgs loop runs created and keeps only the seeded ones, and a state from a recreated stack is refused with its remedy.
A sandbox create the API refused before admitting it (a 4xx other than 409) left no operation, so every rerun replayed the same bad body and clean never dropped it. It is now recorded as rejected: a rerun makes a new create with the current options and a new key, and clean drops the intent; the browser create handler records the same. A 409 stays unresolved, replayed unchanged with its key. When the API-key step clears the page for a possible live key, the flag is set before it navigates, the run stops after that step, and the browser is closed and the recording deleted in a finally path. The README says an interrupted run can still leave a raw recording.
The seeder journaled create intents, replayed unanswered creates with their recorded keys, classified 409 and 422 answers, kept unresolved browser creates and recorded every operation outcome, including ones the API had since garbage-collected. The README already requires a disposable stack, so recovery is now to delete the seed state and reseed, or recreate the stack, and the README and errors say so. Each object is still recorded as soon as the API answers its create, and clean still deletes only what was recorded, waits for each delete and exits 1 on failures. The stack identity checks, the per-prefix state, the org handling and the API-key secret routing are unchanged.
The README told contributors to add a loop step for every behavior a change introduces and keep it if it failed before and passed after, which is regression-test guidance. Behavior checks that need no browser belong in typed space_smoke scenarios; loop steps cover what only a browser shows. The loop stays development and review evidence, not a pass/fail gate.
Under infra/space/kind/ every edit to the tool selected the six-shard Kind smoke, while CI ran neither its type check nor its unit tests. It now lives in scripts/console-loop/, which ci/kind-pull-paths.txt does not match and ui/'s build does not read, and the SPACE Guide job, the pull job that already sets up Node, runs its npm ci, typecheck and tests with a separate npm cache. The Kind README no longer links it, so this change touches no Kind path; docs/installation.md and the space-validation reference point at the new location.
The console loop's pointer in the space-validation reference changes shipped plugin content. main already moved the plugin to 0.2.45 for its own skill change, which absorbed this branch's identical bump on rebase.
The SPACE Guide step for the console loop used an empty npm cache of its own, so a required job fetched every package from the registry on each run, and a registry outage would fail every pull request. The tool's lockfile is now one of the npm cache family's inputs, the trim keeps the packages of every lockfile it is given, and the step uses the job's restored cache.
A create's answer names the sandbox while its operation can still be queued, before the sandbox exists. clean treated that first 404 as gone and dropped the record, so the sandbox appeared later untracked, and a reseed in that window created a second one and replaced the record by name. Each sandbox create's operation is now recorded with its answer. clean and the reseed wait, up to ten minutes, for a recorded create operation to finish before they read a missing sandbox again; past that the record stays and the sandbox counts as a failure. An absent sandbox is still never deleted.
b1766a2 to
5d27ebe
Compare
Summary
Adds
scripts/console-loop/, a Playwright loop for the admin console (ui/) on the local Kind stack. It seeds synthetic organizations, users, API keys and sandboxes through the public and admin APIs. It signs in through the real login page, visits every console route and runs the operator flows. For each step it records a screenshot, console and page errors, failed and slow requests, error toasts and the step's own checks, and it compares two runs (for examplemainagainst a branch), failing on new error types. It is development and review evidence, not a CI gate. Behavior checks that need no browser belong in typedspace_smokescenarios (thespace-validationsmoke-testing reference); loop steps are for what only a browser shows: rendering, navigation, dialogs and console errors. CI runs the tool's own type check and unit tests in the existing SPACE Guide job, never the loop itself. The step installs from the job's restored npm cache: the tool's lockfile is an input of the npm cache family inci/scripts/package-cache.py, next todocs/api/package-lock.json, and the trim before main saves the cache keeps both lockfiles' packages.Safety and cleanup:
seed,cleanand everylooprun that creates things refuse a non-loopback API or console URL unless--allow-remoteis passed. They also read the stack's identity, the cluster row'sidandcreated_at(generated with the database;managing_org_idis the same on every stack), from/admin/api/v1/clusterand/auth/v1/cluster, through both the API URL and the console's origin. They refuse when the readings differ, or differ from the identity the seed recorded, and the refusal names the state file and the remedy for a recreated stack. The tool's own API requests and the browser requests it intercepts never follow a redirect; the console's own requests and navigations do. The README says what this cannot prove: the console's/apiand data-plane routes, browser requests to other origins, proxies that route differently later, and stacks that share a database.out/seed-<prefix>.json, andseed,loopandcleanrefuse a state recorded for another prefix. Mutating loop runs require a seed state.cleanand a reseed wait, up to ten minutes, for a recorded create operation to finish before they treat a missing sandbox as gone; on timeout the record stays and counts as a failure. A pending create is never replaced by name, and an absent sandbox is never force-deleted. If a rerun does not recover, deleteout/seed-<prefix>.jsonand reseed, or recreate the stack; the README and the error messages say so.cleandeletes only what was recorded, waits for each delete, and exits 1 on failures.clean, which the next seed reuses by slug; the organizations loop runs create are deleted.--delete-orgsalso deletes the seeded ones. Deleting an organization cascades to its API keys, credentials, credential references, master keys, template grants and more (listed in the README).cleanrefuses an organization that still holds a child organization, a user, an API key or a sandbox that is not deleted; the other cascaded rows are not checked and go with it.page.route, withmaxRedirects: 0and any 3xx refused. A create records its key's ID, and everysecretin a JSON answer is replaced, so the dialog showsconsole-loop-placeholder-not-a-key. It fails closed: if a secret could have reached the page another way, the dialog opened before the route answered, or the dialog shows anything key-shaped, the step opensabout:blankbefore any screenshot and fails, and the run stops after it (exit 1); the browser is closed and a--videorecording deleted in afinallypath, and the summary says so. A run killed or interrupted while it closes can still leave a raw recording under its run directory's.video/; the README says to delete that before sharing a run. The recorder also redacts 64-hex strings in everything it writes.--skip-sandboxes) each sandbox at rest and the deleted one gone. The loop'sfixturesstep checks the same set through the console.--onlyand--skiptake comma-separated exact step names or tags; no regular expression is built from them.The fixtures are synthetic, but screenshots show everything the console account can see on the stack. The README asks for a disposable stack and a look at each image before sharing it.
Validation
On a local Kind stack (
infra/space/kind/scripts/up.sh), at the head commit:npm run typecheckpasses, andnpm testpasses 13 unit tests (selection, normalizer, secret replacement, per-prefix state, state merging, stack identity, pending creates). The CI step, run as the workflow runs it (clean checkout, Node 22.23.3, the registry blocked), installs from a filled cache and passes;package-cache.py's tests pass, including the two-lockfile trim. The pplx-space plugin moves to 0.2.46 for the skill-reference change, andclaude plugin validatepasses on it.ci/scripts/kind-pull-needed.shreturns false for this head and for a change only to the tool, so editing it no longer selects the Kind smoke.npm run seedcreated and verified 18 fixtures, a fullnpm run loop(47 steps) ran, andnpm run cleandeleted everything recorded, including the--delete-orgsrefusal for an organization holding an unrecorded user. The previous version of the tool gave the same per-step results on the same stack, andcomparefound 0 new error types; the 17 steps with events are existing console findings (snapshots 404 on sandbox pages, 403s for tenant roles, the unknown-org page, the users list's default org).out/holds a 64-hex string.cleanwaited, then deleted the sandbox once it appeared. The timeout case is covered by a unit test.Visual confirmation
A short run against
main: sign-in, then the console's routes and the sandbox search steps:Video: loop-run.webm.
The API-key step answers the create request itself, so the dialog shows a placeholder and the browser never holds a live key:
Severity and risk
Severity: does not apply. This is a development tool.
Risk: low. Nothing in CI or in any image runs the loop, and it changes no service, chart or
ui/file; CI only type-checks and unit-tests it in the existing SPACE Guide job. The seed writes only through the public and admin APIs, as the console account, and only to a stack on this host unless--allow-remoteis passed;cleandeletes only what the state file recorded, and keeps the seeded organizations unless--delete-orgsis passed. It refuses to delete an organization that still holds a child organization, a user, an API key or a sandbox that is not deleted; anything else the organization holds (credentials, credential references, master keys, template grants) is deleted with it. Mutating runs refuse a target whose stack identity differs between the API, the console and the seed state. The tool lives outside the paths inci/kind-pull-paths.txt, so changes to it do not require the Kind smoke. Roll back by reverting the commits.Not covered
space_smokeconsole scenarios: operator invariants (lookup by every ID form, detail completeness per state, cross-org actions, destructive-action confirmation, role gating, link integrity), an allowlisted error baseline next to the scenarios, and selection so the Kind smoke shards run them only whenui/or the admin routes change.main. Steps for behavior a PR adds belong with that PR.AI assistance: Claude Code wrote the code, the README and this description, and ran the validation; a maintainer reviews it.