From 7f021f9aaa26dd6ea1943dd3a9785f5621a6f70e Mon Sep 17 00:00:00 2001 From: Masaru Nemoto <16267290+nemolize@users.noreply.github.com> Date: Thu, 11 Jun 2026 11:17:46 +0900 Subject: [PATCH] docs(rules): fix live tool-verify procedure for Workers vitest env gap The Workers vitest pool isolate does not expose Node's process.env, so a test/_verify_*.test.js spec injecting a live PAT 401s. Switch the procedure to a standalone scripts/_verify_*.mjs run via `pnpm dlx tsx`, document the gotcha, and add an error-path approach for write tools (completed-run 409, no-trigger-workflow 422) to avoid real CI mutations. Discovered while verifying the #8 Phase-5 write tools. --- .claude/rules/tool-e2e-handler-capture.md | 78 ++++++++++++++++------- 1 file changed, 56 insertions(+), 22 deletions(-) diff --git a/.claude/rules/tool-e2e-handler-capture.md b/.claude/rules/tool-e2e-handler-capture.md index bf6704f..f884173 100644 --- a/.claude/rules/tool-e2e-handler-capture.md +++ b/.claude/rules/tool-e2e-handler-capture.md @@ -5,31 +5,61 @@ To verify a tool module (`registerXxxTools` in `src/tools/*.ts`) against the **real GitHub API** without standing up the full Cloudflare Workers + OAuth stack, drive the handlers directly through the same registration path the server uses, with a -**live** Octokit injected: - -1. Write a throwaway vitest spec, e.g. `test/_verify__live.test.js` (the - `_verify_` prefix marks it disposable — delete it after). -2. In it: `captureHandlers()` from `test/_helpers/tools.js`, then - `registerXxxTools(server, () => new Octokit({ auth: process.env.GITHUB_PERSONAL_ACCESS_TOKEN }))` - — a **real** network Octokit, not the test stub. -3. `invoke(handlers, "", { owner, repo, ... })` against a repo with real - data (e.g. `nemolize/remote-mcp-github`, which has both success and failure - workflow runs). Assert on the rendered Markdown and `console.log` the body so - the output is observable. -4. Run only that file: `pnpm exec vitest run test/_verify__live.test.js`. -5. **Delete the spec when done** — it is not part of the suite (it needs network - + a token and would fail in CI). +**live** Octokit injected. Run it as a **standalone script outside vitest**, not as +a `test/_verify_*.test.js` spec — see the gotcha below for why the vitest route does +not authenticate. + +1. Write a throwaway script, e.g. `scripts/_verify__live.mjs` (the `_verify_` + prefix marks it disposable — delete it after). +2. In it: build the handler map inline (`const h = new Map(); registerXxxTools({ + registerTool: (n, _c, fn) => h.set(n, fn) }, () => new Octokit({ auth: + process.env.GITHUB_PERSONAL_ACCESS_TOKEN }))`) — a **real** network Octokit, not + the test stub. (`captureHandlers` from `test/_helpers/tools.js` pulls in vitest's + `expect`, so inline the 3-line capture instead of importing it into a plain + script.) +3. Invoke a handler against a repo with real data (e.g. `nemolize/remote-mcp-github`, + which has both success and failure workflow runs), and assert / `console.log` the + rendered Markdown so the output is observable. +4. Run it with `pnpm dlx tsx scripts/_verify__live.mjs` (tsx resolves the + `.js`-extension imports inside the `.ts` source). `node --experimental-strip-types` + does **not** work — it does not remap `.js`→`.ts` specifiers, so the import of + `../src/mcp/response.js` fails with `ERR_MODULE_NOT_FOUND`. +5. **Delete the script when done** — it needs network + a token and is not part of + the suite. This is *not* an import-and-call unit test: `registerXxxTools` is the module's -public registration boundary, and `captureHandlers` invokes the exact handler the +public registration boundary, and the inline capture invokes the exact handler the running server registers — only the Octokit factory is swapped for a live one. -## Gotcha — sandboxed `pnpm exec vitest` fails with `listen EPERM` +### Verifying a write (mutating) tool — use the error path + +For write tools (`rerun_*`, `cancel_workflow_run`, `trigger_workflow_dispatch`, …) +a real mutation against this repo is unsafe (a dispatch on `deploy.yml` is a real +production deploy; a cancel disrupts live CI). Verify the **live binding** via the +error path instead — it exercises the real Octokit method, endpoint, request body, +and `wrapTool` error surfacing with **no side effect**: + +- `cancel_workflow_run` on an already-**completed** run → real `409 Cannot cancel a + workflow run that is completed`. +- `trigger_workflow_dispatch` on `ci.yml` (which has **no** `workflow_dispatch` + trigger) → real `422 Workflow does not have 'workflow_dispatch' trigger`. + +## Gotcha — the Workers vitest pool does NOT expose `process.env` to the isolate + +This repo's `vitest.config.mts` runs **every** spec in `@cloudflare/vitest-pool-workers`, +and the Workers isolate's `process.env` is **not** Node's process env — a handler doing +`new Octokit({ auth: process.env.GITHUB_PERSONAL_ACCESS_TOKEN })` gets `undefined` inside +the pool, so live calls fail with `401 Requires authentication`. Neither a shell-exported +PAT, adding the PAT to `secrets.required` in `wrangler.jsonc`, `CLOUDFLARE_INCLUDE_PROCESS_ENV=true`, +nor a temporary `.dev.vars` fixes it (`.dev.vars` maps to the Worker `env` binding, not +`process.env`). That is why a live-PAT verification must run as a **standalone script** +(step 4 above), where Node's real `process.env` carries the token — not as a vitest spec. -`@cloudflare/vitest-pool-workers` binds a `127.0.0.1` socket at pool startup, which -the Claude Code sandbox blocks (`Error: listen EPERM: operation not permitted -127.0.0.1`). Run the live spec with `dangerouslyDisableSandbox: true`. (This is the -network-socket axis, unrelated to `.dev.vars` — `secrets.required` does not fix it.) +(A `_verify_*.test.js` that uses only **stubbed** Octokit still works fine in the pool — +the limitation is specifically the *live-PAT* path. A sandboxed `pnpm exec vitest` also +hits a separate `listen EPERM` on the pool's `127.0.0.1` bind; run with +`dangerouslyDisableSandbox: true`. That socket axis is unrelated to `.dev.vars` / +`secrets.required`.) ## Where this sits vs the two-tier ADR @@ -46,6 +76,10 @@ behaviour/output, not the OAuth/transport plumbing the other two tiers cover. - Triggered when verifying a `src/tools/*.ts` module's real-API behaviour during development (e.g. confirming a new tool returns the right Markdown against live data) — before relying on the full manual OAuth harness, which is heavier. -- Use a throwaway `test/_verify_*.test.js`, live Octokit via PAT, sandbox-bypassed - run, then delete the spec. +- Use a throwaway **standalone** `scripts/_verify_*.mjs` (live Octokit via PAT from + Node's `process.env`), run with `pnpm dlx tsx`, then delete it. Do **not** use a + `test/_verify_*.test.js` spec for the *live-PAT* path — the Workers vitest pool + isolate has no `process.env` PAT, so it 401s (see the gotcha above). +- For a **write** tool, verify the live binding via the error path (completed-run + `409`, no-trigger-workflow `422`) rather than performing a real mutation. - For OAuth/consent/transport coverage, use the two ADR-0002 tiers instead.