Skip to content

feat(e2e): live-client smoke test harness (Phase 1 + Phase 2) - #45

Draft
jlowapik wants to merge 7 commits into
mainfrom
feat/e2e-live-clients
Draft

feat(e2e): live-client smoke test harness (Phase 1 + Phase 2)#45
jlowapik wants to merge 7 commits into
mainfrom
feat/e2e-live-clients

Conversation

@jlowapik

@jlowapik jlowapik commented May 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Lands the live-client e2e testing infrastructure in two phases on a single branch. Drives the real Claude Desktop and ChatGPT clients against the dev MCP server from a self-hosted Mac Studio GHA runner.

Phase 1 — scaffold (commits f25ed18, f3aa0eb, 66bcf3c)

  • e2e/ workspace with drivers (Appium-mac2 + Playwright CDP), runSmokeTest dispatch, forensics bundle.
  • First read smoke (readGoogleDoc).
  • .github/workflows/e2e-smoke.yml triggered on workflow_run of Deploy → Dev.
  • Mac Studio bootstrap (e2e/mac-studio/install.sh + launch agent plists).
  • Fix: prompt uses doc ID, not title.

Phase 2 — dual-connector read/write split (commit 6e408f5)

Expands from one read smoke to a per-tool regression harness:

  • Two MCP connectors registered via the dashboard (awesome-mcp-readonly + awesome-mcp-full), each bound to its own Google account via separate instanceId-tagged OAuth grants.
  • Read-only enforcement via manual write-tool unchecking in the Claude connector UI (no server code change). Escalation path documented if that proves fragile.
  • Direct Google API for write setup/teardown (e2e/setup/googleClient.ts mirrors src/userSession.ts:42-50). Scratch folder isolation; cleanupScratchFolder() safety net.
  • runSmokeTest extended with mode, setup, teardown, ctx-aware prompts and assertions.
  • First write smoke (appendToGoogleDoc.smoke.ts) with full setup → write → verify → teardown lifecycle.
  • e2e/tools.ts: source-of-truth READ_TOOLS (9) / WRITE_TOOLS (21), derived from src/google-docs/server.ts addTool calls. Used by the runbook as the canonical list of tools to disable on the readonly connector.
  • Nightly regression workflow (.github/workflows/e2e-regression.yml) runs the full glob at 03:00 UTC. Slack notify on failure (advisory — does not gate prod).
  • Smoke gate stays narrow (npm run test:gate = 1 read + 1 write). Phase 3 fills the remaining 28 tools.

Deferred (NOT in this PR)

  • create-tag.yml prod-gate modification. Flipping the gate before the runner is online blocks prod deploys. The exact diff lives at the bottom of e2e/runbook.md.
  • Mac Studio bootstrap, account provisioning, OAuth grants, fixture creation, GHA secrets. Operational sub-tickets; e2e/runbook.md and e2e/mac-studio/README.md document each step.
  • Server-side scope narrowing via a google-docs-readonly catalog entry. Documented as the escalation path if manual UI blocking proves fragile.

Required operator work before this PR is useful

  1. Mac Studio bootstrap (e2e/mac-studio/install.sh + manual GHA runner registration).
  2. Two Google accounts: mcp-e2e-readonly@… (pre-populated fixtures) and mcp-e2e-write@… (empty + e2e-scratch/ folder).
  3. Dashboard registration of two connections (one per Google account) → register both URLs in Claude/ChatGPT as awesome-mcp-readonly + awesome-mcp-full.
  4. Manual write-tool unchecking on the readonly connector in Claude (list: e2e/tools.ts WRITE_TOOLS).
  5. GHA secrets: E2E_WRITE_GOOGLE_REFRESH_TOKEN, E2E_GOOGLE_CLIENT_ID, E2E_GOOGLE_CLIENT_SECRET, E2E_SLACK_WEBHOOK_URL.
  6. GHA repo variables: E2E_FIXTURE_DOC_ID, E2E_FIXTURE_DOC_NEEDLE, E2E_SCRATCH_FOLDER_ID.

See e2e/fixtures/read.md, e2e/fixtures/write.md, e2e/runbook.md, e2e/mac-studio/README.md.

Verified locally

  • cd e2e && npm run typecheck — clean.
  • cd e2e && npm run test:gate against unset env — fails at the expected points (Appium not running locally, Google API rejects fake creds). Module graph resolves cleanly.
  • Main repo npm run typecheck and npm run lint unchanged.

Test plan

  • Mac Studio runner online with labels [self-hosted, macOS, mac-studio].
  • Both connectors registered in Claude (and ChatGPT) with write tools unchecked on readonly.
  • Local smoke gate green: CLIENT=claude-desktop npm run test:gate from the Mac Studio.
  • Local smoke gate green for CLIENT=chatgpt-web.
  • No-op push to main triggers ci → deploy-dev → e2e-smoke chain; both gate jobs green; forensics artifacts uploaded.
  • Manually dispatch e2e-regression.yml; confirm full glob runs and Slack notifies on injected failure.
  • After ~10 stable runs, apply create-tag.yml diff per runbook to gate prod tags on Claude Desktop smoke.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/e2e-live-clients

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…accounts

Expands the e2e suite from a single read smoke into a per-tool regression
harness that exercises two MCP connectors against two Google accounts:

- awesome-mcp-readonly  → mcp-e2e-readonly@ (rich fixtures, never modified)
- awesome-mcp-full      → mcp-e2e-write@   (empty; scratch resources only)

Both connectors register through the dashboard against the SAME catalog
slug (google-docs) but each gets a unique instanceId-bound session, which
binds the connector to its own Google OAuth grant. Read-only enforcement
is by manually unchecking write tools on the readonly connector in Claude
Desktop UI (see runbook.md). If that proves fragile, the escalation path
is a google-docs-readonly catalog entry with .readonly OAuth scopes - not
in this PR.

What lands:
- e2e/tools.ts: source-of-truth READ_TOOLS / WRITE_TOOLS arrays
  (9 reads, 21 writes derived from src/google-docs/server.ts addTool calls)
- e2e/promptTemplates.ts: preface(mode) helper that names the connector
  in each test prompt
- e2e/setup/googleClient.ts: direct Google API client built from a
  refresh token (mirrors src/userSession.ts:42-50)
- e2e/setup/scratchFactory.ts: createScratchDoc, createScratchSheet,
  trashFile, cleanupScratchFolder - all scoped to e2e-scratch/ folder
- e2e/runSmokeTest.ts: adds mode, setup, teardown, ctx-aware prompts
  and assertions
- e2e/tests/read/readGoogleDoc.smoke.ts: updated to use mode:'readonly'
  + preface('readonly'); doubles as fixture-integrity safety net
- e2e/tests/write/appendToGoogleDoc.smoke.ts: first write smoke with
  full setup/teardown lifecycle + cleanupScratchFolder safety net in
  the test file's after() hook

CI:
- e2e-smoke.yml (deploy gate): now scoped to npm run test:gate which
  runs only the 1 read + 1 write smoke. New env wiring for the write
  side (E2E_WRITE_GOOGLE_REFRESH_TOKEN, E2E_GOOGLE_CLIENT_ID/SECRET,
  E2E_SCRATCH_FOLDER_ID).
- e2e-regression.yml (NEW): nightly cron at 03:00 UTC running the full
  tests/**/*.smoke.ts glob. Slack notification on failure (advisory -
  does not gate prod).

Docs:
- e2e/fixtures/read.md (renamed from fixtures.md, expanded): readonly
  account spec, fixture inventory for Phase 3 to come, GHA variables.
- e2e/fixtures/write.md (new): write account spec, OAuth grant
  procedure, scratch folder conventions, GHA secrets.
- e2e/runbook.md: two-connector model, manual write-tool blocking
  procedure, account rotation including OAuth and fixture drift.

Phase 2 lands two tests. Phase 3 fills out the remaining 28 tools
as data-driven cases on the same harness.

Verified:
- e2e/ typecheck passes (tsc --noEmit clean)
- Module graph resolves at runtime (test:gate fails at expected
  points: Appium not running + fake Google creds)
- Main repo typecheck unchanged

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jlowapik jlowapik changed the title feat(e2e): scaffold live-client smoke test harness feat(e2e): live-client smoke test harness (Phase 1 + Phase 2) Jun 1, 2026
@sonarqubecloud

sonarqubecloud Bot commented Jun 1, 2026

Copy link
Copy Markdown

Євген Овчинніков and others added 6 commits July 3, 2026 22:05
Enables driving the e2e runner bootstrap over the tailnet after a
one-time console visit. Also ignores .claude/ since its local settings
can embed OAuth credentials in permission allowlist entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 built the read/write dual-connector pattern for google-docs only.
Generalize it to every server under src/: google-drive, google-gmail,
google-calendar, google-sheets, google-slides, clickup, slack, slack-user.

- tools.ts -> tools/<service>.ts + barrel. Lists are derived from each
  server's `annotations.readOnlyHint`; the derivation reproduces the
  hand-curated google-docs split exactly, which is what validates it.
- promptTemplates: per-service connector pairs, `awesome-mcp-<service>-<mode>`.
  runSmokeTest now prepends preface(service, mode) itself so a test cannot
  name the wrong connector.
- Per-backend direct-API clients + scratch factories. googleClient gains the
  slides/gmail/calendar namespaces (same OAuth plumbing); clickup and slack
  get their own thin clients. Every sweep is double-scoped: container plus
  the shared `[e2e]` name prefix.
- 18 smoke tests (1 read + 1 write per service).
- test:gate is an explicit manifest, not a glob. GATE is keyed on
  WriteTestedService, computed from the WRITE_TOOLS arrays, so a new
  write-capable service fails typecheck until it is gated.
- Wire the new secrets/vars through both e2e workflows.

Also fixes a pre-existing bug: `--test "tests/**/*.smoke.ts"` does not glob
on Node 20 (workflows pin 20), so the nightly regression matched zero files
and reported success. test:full now discovers files explicitly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brings PR #45 up to date with main, which had moved ahead by five commits
touching the same files the e2e harness owns.

Conflict resolutions:

- .gitignore — took main's. #45 broadened the rule to a blanket `.claude/`,
  but main has since checked in `.claude/skills/**` on purpose ("skills are
  checked in"). Main's narrower rule still ignores settings.local.json, which
  is the file that actually embeds credentials in allowlist entries, so #45's
  security intent is preserved without untracking the skills.

- .github/workflows/e2e-regression.yml — pinned actions/checkout, setup-node
  and upload-artifact to the same commit SHAs main uses in e2e-smoke.yml.
  Not a textual conflict: e2e-regression.yml is new in #45, so it never
  received 0ff451f's pinning pass and would have landed unpinned against a
  repo where 7 of 9 workflows are pinned.

Carried in from main unchanged: the claude-desktop / chatgpt-web
waitForResponseComplete race fixes, persist-credentials: false on checkout,
and the Appium --base-path removal (the runbook reference went with it).

Verified on the merge result: root typecheck clean, root lint 0 errors,
e2e typecheck clean, test:gate and test:full both resolve 18 smoke tests and
fail only on unset credentials.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The per-service tool lists omitted both shared tools for all nine services.
The pattern that generates them matches literal `addTool({name, annotations})`
blocks in src/<service>/server.ts; the shared tools arrive via
registerMintRestBearerForCurl()/registerListRestEndpoints() helpers, so it
never saw them.

That is not cosmetic. mintRestBearerForCurl has readOnlyHint: false, and
runbook.md tells operators to uncheck every tool in a service's WRITE_TOOLS on
its -readonly connector. Absent from every list, it stayed CHECKED on every
readonly connector. The bearer it mints is scoped to the USER, not to a service
and not to reads — createServiceAuth resolves the token to a user and then
looks up that user's connection per route — so a "readonly" connector could
mint a bearer reaching WRITE endpoints on every service the account has
connected. That defeats the entire point of the two-connector split.

mintRestBearerForCurl -> WRITE_TOOLS, listRestEndpoints -> READ_TOOLS, with a
comment in each file recording why they are hand-maintained, and a callout in
runbook.md's blocking step since this is the one checkbox that matters most.

Merging main in also surfaced real drift that had accumulated since these lists
were written, so all nine were regenerated from current source:
- clickup +11 tools (tags, task events, doc images)
- slack-user +7 tools (search, channel events)
- downloadFile was missing from BOTH slack lists: the extraction pattern
  required `annotations:` to immediately follow `name:`, and both Slack servers
  put an explanatory comment between them. The pattern now tolerates that, and
  the addTool-count assertion that caught it is what proved the lists complete.

Counts now: docs 30, drive 15, gmail 14, calendar 6, sheets 12, slides 6,
clickup 45, slack 8, slack-user 15 — each plus the 2 shared.

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

Copy link
Copy Markdown

jlowapik added a commit that referenced this pull request Aug 30, 2026
The setup instructions pointed at tests/read/readGoogleDoc.smoke.ts, which does
not exist on main — that layout arrives with PR #45. On this branch the single
smoke test is tests/readGoogleDoc.smoke.ts, so the command as written would have
failed with 'Could not find' on the first thing anyone tried.

Also notes that the 18-test figures in the Cost section describe the post-#45
state rather than this branch.

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.

1 participant