Skip to content

feat: Percy feature testbed — one-command build seeder (Units 1–10) - #1

Open
AkashBrowserStack wants to merge 17 commits into
mainfrom
feat/implement-testbed
Open

feat: Percy feature testbed — one-command build seeder (Units 1–10)#1
AkashBrowserStack wants to merge 17 commits into
mainfrom
feat/implement-testbed

Conversation

@AkashBrowserStack

@AkashBrowserStack AkashBrowserStack commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

What & why

One command — seed-testbed --profile <env> — populates a Percy environment with real renderer-backed builds exercising every Percy feature, for manual QA. Default is a full pass (every account tier × every feature); --only <feature|tier> narrows it for targeted re-runs.

Builds render actual DOM via percy snapshot. The earlier approach uploaded solid-colour PNGs, which proved create project → token → build → diff and nothing else: the AI classifier had nothing to classify and region rules had nothing to reason about, so both were smoke builds by construction.

Origin: brainstorm → plan (docs/plans/2026-07-27-001-feat-percy-feature-testbed-plan.md in the percy hub).

Verified live

Every build below is on prod org 9560f98d, produced by this branch's generators.

Region rules ✅ — percy-testbed-rich-rgd7733

Build Rule Diffs
52700854 ignore on the carousel, carousel-only change 0 ✅ as expected
52700911 controlstandard, same pair 4 ✅ as expected
52700952 ignore on carousel, all 4 zones changed 4 expected — 3 zones still differ
52700994 layout on a displaced sidebar 4 ⚠️ see below
52701059 controlstandard, same pair 0 ⚠️ see below

The first pair is the proof. Identical fixtures, the carousel is the only thing that changed: standard flags it, ignore takes the build to zero. Region config demonstrably reaches Percy and does what it says.

That pair exists because the earlier version of this test could not fail — it changed four zones and ignored one, leaving three still changed, so the count stayed at 4 whether or not the rule fired. An unfalsifiable test reads as "the feature is broken" when it is really saying nothing. Isolating the change fixed that.

⚠️ Layout runs opposite to expectation. On an identical pair the layout rule flags 4 and standard flags 0. The rule may be designed to detect layout change rather than suppress it — that is not confirmed, so the build asks QA to compare the pair rather than asserting a direction.

AI ✅ — all three R12 capabilities — percy-testbed-rich-bug322a

Build Snapshots Diffs AI reduced
52700252visual bug classification 2 4 8
52700352AI build summary, 8 realistic page pairs 8 24 10
52700474reduce-diff 1 4

ai-details.total-diffs-reduced was null on every build before this branch's fixes; it now carries real values. The bug-classification build ships five deliberate defects — unreadable carousel text, banner copy clipped mid-word, a misaligned price row with an overlapping value, a failed ad image, and the sidebar overlapping the main column — because Percy tags such regions visual_quality: 'irregularity', which needs something that looks broken rather than merely changed.

Core review states ✅ — percy-testbed-rich-rich0180

Build Branch Snapshots Diffs
52695689 baseline (approved) master 2 16/16
52695709 changed feature/seed-change 2 4/16

IntelliIgnore ⚠️ — runs, but not yet distinguishable — percy-testbed-rich-fix4b7b

Build Rule Diffs AI reduced
52699608 intelliignore, 4 noise zones 4
52699656 controlstandard 4 4
52699692 intelliignore, noise + price change 4 4
52699720 / 52699755 sensitivity 0 / 4 4 / 4 8 / —

AI reduction happens, but the pattern does not track the rules — the standard control also shows reduction, suggesting project-level IntelliIgnore reduces across all builds independently of the per-region algorithm. These scenarios need the same isolated-change treatment the ignore test just got before they prove anything.

Bugs this work surfaced

  • Settings writes were silent no-ops. editProject sent snake_case keys; Percy answers 200 for unknown keys and ignores them. Verified against prod: ignore_carousels_enabled → stays false, ignore-carousels-enabled → true, both 200. Keys are now dash-cased, and editProject reads the response back and throws when a value did not stick. The same bug was already in setAutoApprove, so auto-approve had been quietly doing nothing — and two existing tests asserted the same wrong key the code sent, which is why it survived.
  • ai-enabled defaults to false, so AI and IntelliIgnore never ran. Both generators now enable it.
  • Approve returns 409 on orgs whose default branch auto-approves; reviewBuild treated that as failure, which would break every generator that approves a baseline.
  • Percy's default 375px width left the fixed-width fixture's price table outside the render, so a genuine change produced no diff at all. Snapshots are pinned to 1280.

What's in this PR

  • Fixtures (src/fixtures/) — a zoned storefront generated from one source so variants cannot drift, with the zones Percy's algorithmConfiguration knows about (carousel, ad, banner, dynamic timestamp) plus a price table as the signal and a sidebar for layout. Deterministic: no Date.now, no randomness, so re-runs reproduce pixels.
  • Generators — new intelli-ignore, regions covering ignore/layout with controls, ai covering all three R12 capabilities (build summary and reduce-diff render the upstream percy_playwright/test_bed/ai pages in place, no vendoring; bug classification uses in-repo fixtures because upstream's point at external sites).
  • GeneratedBuild.expectation — every build states what QA should see, printed in the run summary.
  • Removed src/images.ts — dead with the upload path.

Tests

82 unit tests (mocked HTTP + shell — nothing hits the network), tsc --noEmit clean. npm test.

Known limitations

  • IntelliIgnore scenarios are not yet decisive (above).
  • Layout-rule direction unconfirmed (above).
  • AI output quality is not asserted — the requirements doc exempts AI from determinism.
  • App Percy isn't wired to a working capture (needs a BrowserStack app + BS_APP_ID).
  • A full seed:all across tiers still assumes the seeded account matrix exists on the target env.
  • npm run build output is not runnable (pre-existing): tsc emits extensionless ESM imports, so dist/main.js — the declared bin — fails to resolve. Everything runs through tsx.

Notes

  • Upstreams are used by shelling out / reading in place — no vendoring, so nothing drifts.
  • No secrets committed — only profiles/*.example.js templates.
  • Builds live in throwaway prod projects and Percy ages them out; re-run the seeder if they expire before review.

🤖 Generated with Claude Code

AkashBrowserStack and others added 13 commits July 27, 2026 13:23
resolveProfile (pure) validates the allow-list (prod never targetable, preprod
gated), derives PERCY_CLIENT_API_URL, and resolves declared secret refs from env
(fail-fast, naming any missing var). Fixes test discovery to recurse.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pletion (Unit 3)

Injectable HttpClient + auth helpers (user principal vs project write/read token
vs Basic-auth). createProject/editProject/setAutoApprove/fetchProjectToken;
createBuild/finalize/reviewBuild/getBuildState/waitForBuildFinished (bails on
terminal state). Auth split verified against percy-api policies. Shared test fakes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Injectable Runner + parseFinalizedBuild; captureWeb shell-out helper (always
injects PERCY_CLIENT_API_URL, nonces the branch, supports capture.js DIFF_MODE
and the YAML path). generateCoreStates: approved baseline -> changed vs baseline,
awaiting completion between.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…al (Units 5+6)

Feature generators (R9-R14): visual-git + A/B variant (target-branch), recurring
diff (2 consecutive), AI fixture pair, regions pair, plus approval.ts with
auto-finalization, supersede (same branch + skipCache), and the NET-NEW
auto-approve generator (editProject branch rule via user principal + matching/
control builds). Shared generator-context test fake.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y (Units 7-10)

- app-percy: shells app_capture.py (baseline+changed), prod-hub creds + BS_APP_ID
- preconditions: reserved-substring name guard + declared-flag/app-cred gate
- provisioning: local seed-rake shell + createProject/fetch write+read tokens
- orchestrator: one dependency-ordered pass over tiers x features, per-feature
  isolation, --only filter, AI skipped on ai_off
- report: feature-labeled, token-free run summary
- wired realDispatch into the CLI (live provisioning throws a Phase-0 TODO)

All 51 unit tests green (mocked; live validation is Unit 0, pending tokens).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…visionProject

Per public Percy docs (browserstack.com/docs/percy): create project = POST
/api/v1/projects with HTTP Basic auth (BrowserStack user:key), org inferred from
creds — not an org-scoped path or a bearer user token. editProject/setAutoApprove
likewise Basic auth. fetchProjectToken via the tokens relationship (flagged
verify-on-first-run — not fully public-documented). provisionProject now REAL
(create project -> fetch write+read tokens) and wired into run.ts, replacing the
Phase-0 throw. Builds still come from the CLI capture (no public create-build REST).
51 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…percy.io)

Live validation against the public API confirmed create-project (POST /api/v1/projects,
Basic auth) and token fetch both work; the tokens endpoint returns roles
write_only / master / read_only. Corrected fetchProjectToken + provisionProject to
use 'read_only' (was 'read', which would 404 the read token).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Live run confirmed the tokens/self links use the full-slug (orgSlug/projectSlug);
createProject now returns full-slug so fetchProjectToken/editProject build the
correct path. End-to-end create->fetch->build validated against percy.io
(build #52251139).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the browser-capture path with the image-upload path proven live:
- src/images.ts: static PNG generator + variant sets (baseline/unchanged/changed/
  new/removed) driving review states
- captureWeb now writes a variant image set to a temp dir and runs
  'npx @percy/cli upload <dir>' (no browser), injecting PERCY_CLIENT_API_URL +
  branch/target-branch/skip-cache
- generators (core/visual-git/recurring/ai/regions/supersede) switched from the
  YAML/browser path to diffMode image sets; ai/regions labeled as degraded on the
  upload path
- @percy/cli added as a dependency
- provisionProject (create project via Basic auth -> fetch write+read tokens) was
  already validated live; orchestrator now drives real builds end to end

So 'seed-testbed --profile <env>' reproduces the hand-proven flow. 54 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
save-exact + audit; commented placeholder for an approved internal registry
(auth via env var, no secrets committed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the exact directives required by the BrowserStack supply-chain audit
(SC-12282/Enigma): ignore-scripts, strict-ssl, save-exact, audit-level=high,
engine-strict, legacy-peer-deps=false, access=restricted. Relaxed engines to
>=18.19 (code runs there) so engine-strict doesn't block installs. Install +
54 tests verified green with the hardened config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reflects the validated flow: prerequisites (Node 18.19+, BS user:key), CLI usage
(--profile / --only), what each build type produces, the create-project -> fetch-token
-> percy upload mechanics, a manual prod recipe (CLI refuses prod), profiles/secrets,
known limitations (AI/regions smoke builds, App Percy, archive-not-delete), and dev commands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a --tier flag (scope a run to one account/project) that combines with --only,
and one-liner npm scripts per feature suite: npm run core / ab / recurring-diff /
ai / regions / approval / app / seed:all. Each defaults to --profile canary
--tier paid (override via PROFILE=/TIER= env). README documents them. 58 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rishigupta1599

Copy link
Copy Markdown

Testbed is very basic just color change, it should have complex dom to verify, to get proper AI diffs intelli ignore diffs etc....

AkashBrowserStack and others added 4 commits August 7, 2026 18:52
Replaces the solid-colour PNG upload path with `percy snapshot` over generated
fixture pages. Colour blocks proved the create-project -> token -> build plumbing
but gave the AI classifier nothing to classify and region rules nothing to reason
about, so those features were smoke builds by construction.

- src/fixtures/pages.ts: one zoned storefront page rendered from a single source,
  with the zones Percy's algorithmConfiguration actually knows about (carousel,
  ad, banner, dynamic timestamp) plus a price table as the SIGNAL and a sidebar
  for layout. Deterministic — no Date.now/random — so re-runs reproduce pixels.
- src/fixtures/sets.ts: file sets per review state (new adds a page, removed drops
  one); filenames stable so snapshots line up build to build.
- src/fixtures/rules.ts: generates the Percy config carrying regions[] with
  algorithm standard|layout|ignore|intelliignore + configuration. Pins width to
  1280 — at the default 375px the fixed-width fixture's price table falls outside
  the render and a real change silently produces no diff.
- generators/intelli-ignore.ts (new): every rule build is paired with a `standard`
  CONTROL over the same fixture pair, because "0 diffs" is otherwise
  indistinguishable from a no-op. Also enables the noise classes on the project —
  they default to off, which a live run showed makes the region config inert.
- generators/regions.ts: covers ignore + layout, each with its control.
- generators/ai.ts: renders the upstream percy_playwright test_bed/ai pages in
  place (no vendoring), 8 baseline/changed pairs + reduce-diff.
- GeneratedBuild.expectation + run summary: states what QA should SEE, since rich
  DOM makes "looks fine" a judgement nobody can make unaided.
- build-api: tolerate 409 on approve. Orgs whose default branch auto-approves
  answer 409 for every baseline, which would fail every generator.
- drops src/images.ts (dead with the upload path).

Validated live against prod: rendered builds finalize, and `algorithm` demonstrably
changes outcomes (same layout-shift pair = 0 diffs under standard, 4 under layout).
NOT yet verified: that `ignore` and `intelliignore` suppress what they should —
total-comparisons-diff is per comparison, not per region, so it cannot show whether
a rule fired. Recorded in README limitations; needs per-region comparison data.

80 tests green, tsc clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rishi's review caught that no AI or IntelliIgnore build had actually run. Three
causes, all fixed here.

1. editProject sent snake_case attribute keys. Percy answers 200 for unknown keys
   and ignores them, so every settings write reported success and changed nothing.
   Verified directly against prod: ignore_carousels_enabled -> stays false,
   ignore-carousels-enabled -> true, both 200. Keys are now dash-cased, and
   editProject reads the response back and throws when a value did not stick, so a
   silent 200 cannot hide a no-op write again.

   The same bug was already in setAutoApprove (auto_approve_branch_filter), so the
   auto-approve generator has been quietly doing nothing. Two existing tests asserted
   the same wrong key the code sent, which is exactly why it survived — both corrected.

2. ai-enabled defaulted to false on fresh projects. IntelliIgnore is AI-backed, so
   the rule never ran at all and every build matched its standard control, with
   ai-details.total-diffs-reduced null. Both the intelli-ignore and ai generators now
   enable it. After the fix the field carries real values.

3. AI bug classification (the third R12 capability) was never wired. Upstream's
   fixtures point at external sites — a random-content joke page and a GitHub Pages
   demo — which cannot give a testbed a reproducible diff, so this adds an in-repo
   `visual-bugs` variant instead: five distinct defects, one per zone (unreadable
   carousel text, clipped banner copy, misaligned price row with an overlapping
   value, failed ad image, sidebar overlapping the main column). Percy marks such
   regions visual_quality: 'irregularity', so the diff has to look like something
   broke rather than like a content edit.

82 tests green, tsc clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The existing ignore scenario could not fail. It changed four zones and ignored
one, so three changed zones remained, the snapshot differed either way, and
total-comparisons-diff stayed at 4 whether or not the rule fired. That is an
unfalsifiable test, and it is why the earlier region builds looked like "ignore
does nothing" — the result was inconclusive by construction, not negative.

Adds a `carousel-only` variant where exactly one zone differs, and a pair over it:
ignore on the carousel (expect ZERO diffs) against a standard control (expect the
carousel flagged). Now the rule build can actually fail, and a zero means the rule
worked rather than meaning nothing changed. The four-zone version is kept, relabelled
to say why it is the weaker test.

Correction to an earlier claim: the previous region builds were NOT invalidated by
the snake_case settings bug — generateRegions never calls editProject, and
regions.ts did not change in that commit. They were inconclusive for this reason
instead.

82 tests green, tsc clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Live runs give layout=4 diffs and standard=0 on an identical layout-shift pair —
the opposite of the "layout suppresses pure movement" claim the expectation made.
The rule may be designed to DETECT layout change rather than ignore it, but that
is not confirmed, so the build now tells QA which pair to compare instead of
asserting a direction that the evidence contradicts.

Verified in the same run: the isolated ignore pair works exactly as labelled —
carousel-only change gives 0 diffs under `ignore` and 4 under `standard`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants