Skip to content

Add Claude-driven manual test workflow for PRs - #1160

Open
sugh01 wants to merge 1 commit into
mainfrom
feat/claude-manual-test-workflow
Open

Add Claude-driven manual test workflow for PRs#1160
sugh01 wants to merge 1 commit into
mainfrom
feat/claude-manual-test-workflow

Conversation

@sugh01

@sugh01 sugh01 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Runs an exploratory manual test on every PR: boots the local ENS stack, drives the real app in a browser via the Playwright MCP server, and posts a report as a PR comment.

An MCP-driven browser has none of the Playwright fixtures the e2e suite relies on, so init-page.cjs restores the two that matter via MCP's --init-page hook: it injects the same mock wallet the e2e suite uses (@ensdomains/headless-web3-provider, every request kind pre-authorized so nothing needs manual approval) and installs the page clock at the anvil block timestamp, since local chain time is not real time and every name would otherwise look expired. It also exposes a control server on :8546 for switching accounts and travelling in time from the shell.

Supporting pieces:

  • wait-for-stack.mjs gates on contracts actually having code on chain and the subgraph reaching the chain head. A leftover .env.local is not proof the deploy finished.
  • smoke.mjs boots a real MCP server, loads the app and asserts the wallet, clock and control server work, before any Claude usage is spent. It spawns from the same MCP config the session gets, so the two cannot drift.
  • pnpm seed seeds fixtures through the existing makeName helper, without launching a browser.
  • The run book always exercises register and extend, so a PR with no user-facing change still gets meaningful coverage.

Long-running processes are started with setsid: each step is its own shell session, and a merely backgrounded child is torn down with it.

Runs an exploratory manual test on every PR: boots the local ENS stack,
drives the real app in a browser via the Playwright MCP server, and posts
a report as a PR comment.

An MCP-driven browser has none of the Playwright fixtures the e2e suite
relies on, so init-page.cjs restores the two that matter via MCP's
--init-page hook: it injects the same mock wallet the e2e suite uses
(@ensdomains/headless-web3-provider, every request kind pre-authorized so
nothing needs manual approval) and installs the page clock at the anvil
block timestamp, since local chain time is not real time and every name
would otherwise look expired. It also exposes a control server on :8546
for switching accounts and travelling in time from the shell.

Supporting pieces:
- wait-for-stack.mjs gates on contracts actually having code on chain and
  the subgraph reaching the chain head. A leftover .env.local is not
  proof the deploy finished.
- smoke.mjs boots a real MCP server, loads the app and asserts the wallet,
  clock and control server work, before any Claude usage is spent. It
  spawns from the same MCP config the session gets, so the two cannot
  drift.
- pnpm seed seeds fixtures through the existing makeName helper, without
  launching a browser.
- The run book always exercises register and extend, so a PR with no
  user-facing change still gets meaningful coverage.

Long-running processes are started with setsid: each step is its own
shell session, and a merely backgrounded child is torn down with it.
@sonarqubecloud

Copy link
Copy Markdown

- name: Run manual test session
id: claude
if: ${{ !env.ACT || env.CLAUDE_IN_ACT == 'true' }}
uses: anthropics/claude-code-action@v1

- name: Install Playwright browsers
run: |
pnpm exec playwright install --with-deps chromium
run: |
pnpm exec playwright install --with-deps chromium
# the MCP server ships its own playwright, which needs its own browser build
npx -y @playwright/mcp@latest install-browser chrome-for-testing
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying ens-app-v3 with  Cloudflare Pages  Cloudflare Pages

Latest commit: 6d87a2b
Status: ✅  Deploy successful!
Preview URL: https://fa437ab2.ens-app-v3.pages.dev
Branch Preview URL: https://feat-claude-manual-test-work.ens-app-v3.pages.dev

View logs

@github-actions

Copy link
Copy Markdown

Claude manual test report — PR #1160

1. Verdict

looks good — Phase 0 (register, extend) both PASS, and the PR's own surface (the manual-test
harness itself: wallet injection, page clock, control server, seeder) all behaved exactly as the PR
description and instructions claim. 8 PASS / 0 FAIL / 0 BLOCKED.

2. What I understood the PR to do

  • Adds a new GitHub Actions workflow (.github/workflows/claude-manual-test.yaml) that boots the
    local ENS stack, starts a Playwright MCP server, and has a Claude session drive the real app in a
    browser to manually test each PR, posting a report as a PR comment.
  • init-page.cjs is an MCP --init-page hook that, per tab: injects the same mock wallet the e2e
    suite uses (@ensdomains/headless-web3-provider, announced as "Headless Web3 Provider", every
    request kind pre-authorized), installs a page clock pinned to the anvil block timestamp, and starts
    a control server on 127.0.0.1:8546 (/state, /account, /time/advance, /clock/sync).
  • smoke.mjs is a fast pre-flight check (spawns a real MCP server, loads the app, asserts wallet +
    clock + control server work) so a broken harness fails before any Claude usage is spent.
  • wait-for-stack.mjs blocks on contracts actually having code on chain and the subgraph reaching
    chain head, rather than trusting a possibly-stale .env.local.
  • e2e/seed/seed.spec.ts (pnpm seed) is a new Playwright "seed" project that seeds fixtures via
    the existing makeName helper without a browser, for use by manual-testing sessions.
  • .github/claude/manual-test-instructions.md is the run book this very report follows.
  • No src/ application code changed — this PR's entire diff is CI/test-harness tooling. There is
    no new user-facing app behaviour to test beyond the harness itself, which is what Phase 0 + the
    scenarios below exercise.
  • Stated non-goals: none of this claims to change app behaviour; the README explicitly notes
    ACT=true guards, and act-only rehearsal steps are out of scope for a real PR run (this run used
    the real GitHub Actions path, not act).

3. Test plan

Since the diff is 100% harness/CI code with no src/ changes, Phase 0 is the PR-specific test here.
On top of the mandated register/extend flows, I added scenarios that exercise the specific pieces the
diff adds (wallet injection, control server, seeder), each with an explicit oracle.

# Scenario Oracle
1 Register a fresh name end-to-end Profile resolves post-registration; expiry = registration block timestamp + 1 calendar year; owner = user's address; name appears under My Names
2 Extend an existing legacy name by 1 year New expiry = old expiry advanced by exactly 1 calendar year (not +31536000s); price is the real quoted rent, not a placeholder; profile + My Names both reflect the new expiry after subgraph sync
3 Wallet is injected as "Headless Web3 Provider" Connect modal lists it under "Installed"; eip6963:announceProvider reports the name (this is exactly what smoke.mjs asserts)
4 Control server /account switches the active wallet account without reconnecting Header identity in an already-open tab updates to the new account's address/primary name with no new connect prompt
5 Control server /account rejects an unknown user POST /account {"user":"nope"} → HTTP 400 with an {"error": ...} body, active account unchanged
6 Seeder produces a grace-period name (duration: -86400) Profile page shows the "has expired" banner with a grace-period end date; the seeder's own time travel (needed to push the name past expiry) is reflected in a later /state call
7 Seeder appends a unique suffix and prints SEEDED: [...] Registered name differs from the requested label by a numeric suffix; that exact name is resolvable in the app
8 /time/advance + /clock/sync unstall a live commit countdown After advancing chain time past the commit-reveal window and reloading, the "Almost there" countdown is replaced by the "Complete registration" step

4. Results table

Scenario Expected Actual Verdict
1. Register claude-reg-8830.eth Profile resolves; expiry 2026-07-30 + 1yr = 2027-07-30; owner = user (0xf39Fd6...92266); shows under My Names Registered via full 3-step flow (Begin → 60s+ timer → Complete registration). Congratulations screen showed "Name expires: July 30, 2027". Profile page confirmed owner/manager = claude-reg-8830.eth (primary name of 0xf39...92266), expiry "July 30, 2027". My Names list shows it, "Expires in 1 year", Manager+Owner badges PASS
2. Extend claude-ext-9931-1785402023.eth by 1yr Old expiry 2027-07-30 → new expiry 2028-07-30 (crosses the 2028-02-29 leap day per the instructions' own worked example — correctly 366 days, not a bug); price = real quoted rent (0.0033 ETH) Pre-extend expiry: July 30, 2027. Confirm dialog showed "New expiry: July 30, 2028", cost "0.0033 ETH + fees". Post-tx profile expiry: July 30, 2028. My Names: "Expires in 2 years" PASS
3. Wallet announced as "Headless Web3 Provider" Listed under "Installed" in connect modal Confirmed in connect modal snapshot; clicking it connected instantly, no popup PASS
4. /account switch live-updates header Header shows new account, no reconnect prompt Switched to user2 (0x70997970...c79C8); header updated to 0x709...c79C8 on next navigation with zero reconnect friction PASS
5. /account rejects unknown user HTTP 400, {"error":"unknown user: nope"} Got exactly that PASS
6. Seed grace-period name "has expired" banner + grace period end date claude-grace-2201-1785402098.eth showed: "claude-grace-2201-1785402098.eth has expired. The grace period for this name ends on November 25, 2026, 09:08:28 UTC." PASS
7. Seeder unique suffix + SEEDED output Real name differs from requested label, is resolvable All 3 seed calls printed SEEDED: "<label>-<suffix>.eth"; every seeded name resolved correctly in the app PASS
8. /time/advance + /clock/sync unstall countdown Countdown clears after chain-time advance + reload Advanced chain 170s during the live "Almost there" 60s+ countdown, ran /clock/sync, reloaded — page immediately showed "Complete registration" (23 hours remaining), no stall PASS

5. Issues found

None — no functional defects in the harness or the app.

Two pieces of console noise observed, both pre-existing/unrelated to this diff (verified no src/
file touching these areas is in the PR):

  • Warning: Unsupported style property ... var(--rdjm151) — a pre-existing @ensdomains/thorin
    dev-mode CSS warning, present on every page load regardless of this PR.
  • WalletConnect relay JsonRpcProvider errors ("JWT Token is not yet valid: basic.iat ...") — appear
    after large time jumps (e.g. after seeding the grace-period name, which itself advances the chain by
    ~29 days). This is a side effect of the harness's own page-clock manipulation (WalletConnect's relay
    validates a JWT iat against real wall-clock time, and the page clock is now far in the future) —
    not a functional blocker since the harness's flows use the injected mock wallet, not WalletConnect,
    but worth knowing if a future scenario exercises the WalletConnect connector after a large time
    jump: expect relay-connection errors in the console that are not real regressions.
  • Anvil's testClient.increaseTime + mine does not land exactly the requested delta (e.g. asking
    for 60s produced 65–107s actual deltas across two calls). This is inherent anvil/JSON-RPC round-trip
    behaviour, not a bug in init-page.cjs's thin wrapper — noting it so a future run doesn't mistake it
    for a harness bug.

6. Not covered

  • smoke.mjs and wait-for-stack.mjs were not run directly — their behaviour was exercised
    indirectly and successfully (the stack was up, the wallet/clock/control-server all worked from the
    first navigation), which is what they gate on, but I did not invoke them as standalone scripts.
  • The act-local-rehearsal path (MANUAL_TEST_DRY_RUN, MANUAL_TEST_SCOPE=baseline, the ACT
    guards in the workflow yaml) was not exercised — this run went through the real GitHub Actions
    workflow with a real PR number, not act. MANUAL_TEST_DRY_RUN was false here, consistent with
    that.
  • The "Comment on failure" fallback step and artifact upload were not exercised since nothing failed.
  • Wrapped-name and subname seeding options (type: wrapped, subnames, fuses, etc.) were not
    seeded/tested — out of scope since nothing in the diff touches name-type-specific behaviour and
    Phase 0 only calls for legacy names.

Screenshots (workflow artifacts)

  • phase0-register-profile-claude-reg-8830.png — profile page after registration
  • phase0-my-names-list.png — My Names after registration
  • phase0-my-names-after-extend.png — My Names after extension, showing updated expiry
  • harness-grace-period-name-and-user2.png — grace-period banner + live /account switch to user2

@TwistedCrafts TwistedCrafts left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good how's it working

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.

3 participants