agentsclimarketplace

Goose gui e2e

Skill Miyamura80/MCP-Template/.agents/skills/goose-gui-e2e

🏡️ MCP Server with CLI interface ready for distribution

Install
npx -y skills add Miyamura80/MCP-Template --skill goose-gui-e2e

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Drive the real Goose desktop GUI (Electron) as an MCP client via Playwright to end-to-end test this template's MCP Apps - asserting the ui:// iframe actually renders and round-trips in a real host. Use to run, add, or debug MCP-App e2e scenarios.

SKILL.md

12.0 KB, as published. Nobody here has run it

Goose-GUI end-to-end testing of the MCP Apps

Goose (Block's open-source agent, an Agentic AI Foundation reference MCP client) renders MCP Apps natively - it fetches a tool's ui:// resource and mounts the HTML in a sandboxed iframe. This skill stands up the real Goose desktop app (Electron), wires this template's /mcp mount into it as a streamable-HTTP extension, drives a chat scenario with Playwright-Electron (DOM selectors, no pixel coordinates), and decides PASS/FAIL from two records the mock LLM cannot fake: the rendered iframe DOM and the tool-call round-trip log.

This closes the L4 "rendered iframe in a real host" gap from issue #37 - the layer tests/test_mcp_e2e.py (wire) and the per-app vitest (component) deliberately don't cover. It's ported from Edison-Watch's goose-gui-e2e harness; the Goose build under $E2E_HOME/goose_src is repo-agnostic and shared with it.

   scenario (JSON: prompt + tool plan + expect)
        β”‚  Playwright-Electron drives the DOM
        β–Ό
   mock LLM ──emits webhook_settings──► Goose desktop GUI ──/mcp──► template server
   (scenario engine)                    (Electron, real)            (FastMCP + enhancer
        β–²                                     β”‚  renders ui://       attaches ui://mymcp/settings)
        β”‚                                     β–Ό
        └──────────────────────────  sandboxed iframe (Settings app)
                                             β”‚
                     assert: iframe DOM (pw_result) + tool round-trip (toolcalls.jsonl)

Quickstart

cd .agents/skills/goose-gui-e2e/scripts     # or the symlink under .claude/skills/
export E2E_HOME="$HOME/goose-e2e"           # working dir (default; override freely)

bash setup.sh                # ONE TIME: build goose + Electron + bundles (~6-8 min). Idempotent.
bash up.sh                   # seed e2e DB + API key; start template server + mock + vite (idempotent)
bash run_test.sh settings_render   # drive the GUI + assert -> PASS/FAIL, screenshot in $E2E_HOME
bash run_all.sh              # run every scenario, print a summary
bash down.sh                 # stop test services  (down.sh --reset also drops the e2e DB + key)

make test_apps_e2e runs up.sh && run_all.sh. Each run_test.sh writes a screenshot of the real Goose GUI (with the Settings app rendered) to $E2E_HOME/shot_<scenario>.png.

Where this runs in the suite

Heavy tier - not in make ci (which stays Node/Rust/Electron-free):

  • Local / make: make test_apps_e2e (after the one-time setup.sh).
  • Full pytest suite: tests/test_apps_e2e.py is a collectable member, skipped by default; opt in with RUN_APPS_E2E=1 (e.g. RUN_APPS_E2E=1 uv run pytest tests/test_apps_e2e.py --no-cov). It shells out to up.sh + run_all.sh. (--no-cov keeps a single-file run from tripping pytest.ini's coverage gate.)
  • CI: .github/workflows/apps_e2e.yaml - workflow_dispatch + weekly cron. It builds Goose via setup.sh (open egress, so the npmmirror preconditions below don't apply on GitHub runners) and runs the guarded pytest entry.

Preconditions (verify before setup)

  • npmmirror on the network allowlist. Electron's binary is fetched from a mirror because GitHub release assets are egress-blocked in the sandbox. In the environment's Network access β†’ Custom, add registry.npmmirror.com and cdn.npmmirror.com (keep the default package-manager list checked). setup.sh preflights this and fails loudly if it's missing.
  • Sandbox provides: cargo, pnpm/node, bun, uv, xvfb-run. setup.sh uses git clone (allowed) + the mirror - none of the GitHub-blocked paths.

How a scenario works

A scenario is JSON in scripts/scenarios/<name>.json:

{
  "prompt": "Open my settings.",
  "plan": [ { "match": "webhook_settings" } ],
  "final": "Opened your settings.",
  "app_uri": "ui://mymcp/settings",
  "expect": {
    "tool_called": "webhook_settings",
    "round_trip": true,
    "app_rendered": true,
    "dom_contains": ["Settings", "Gmail"]
  }
}
  • prompt - the user message typed into Goose.
  • plan - the tool-call sequence the mock emits. match is a substring of the tool name Goose offers (Goose may namespace extension tools, so substring not exact). A plan step may carry "args" (e.g. {"match": "gmail_get_thread", "args": {"thread_id": "t-1001"}}) which the mock passes through as the tool call's arguments. Pick an app-returning tool: webhook_settings β†’ ui://mymcp/settings renders offline with zero external deps, and gmail_get_thread β†’ ui://mymcp/gmail_inbox renders offline because the stack sets GMAIL_FAKE_BACKEND=1 (see below).
  • expect - checked by mcp_probe.py: tool_called + round_trip (from the tool-call log) and app_rendered + dom_contains (from the rendered iframe).

Rendering the Gmail apps offline (GMAIL_FAKE_BACKEND)

The gmail_inbox / gmail_composer apps normally need a linked Google account: gmail_get_thread β†’ _get_gmail_client() raises GmailNotConnectedError (ConnectRequiredError) with no token row, so the tool errors before send_app and the iframe never mounts. To render them offline, up.sh starts the server with GMAIL_FAKE_BACKEND=1, which makes _get_gmail_client return a fixture-serving fake (services/_gmail_fake_backend.py) instead of hitting Google - no linked account, OAuth, or network. The fixtures are raw-Gmail-API shaped, so the real service parsing β†’ Pydantic GmailThread runs end-to-end; only the network boundary is faked. The flag is hard-refused under DEV_ENV=prod (guard in services.gmail_svc._maybe_fake_gmail_client), so it can never stand in for a real mailbox in production.

Why the fake also serves the curated inbox: the gmail_inbox app registers its ontoolresult handler in a mount effect, and if the host delivers the thread result before that runs, the app misses it and falls back to a curated-inbox refresh (gmail_inbox.refresh β†’ gmail_curate_inbox) after ~800ms. So a thread-open renders the reader when it wins that race and the curated list when it loses. The fake serves both paths from the same fixture thread, and gmail_thread_render asserts on content present in both views, so the scenario is deterministic instead of ~30% flaky. gmail_get_thread's round-trip is still proven independently via the tool-call log regardless of which view renders.

Optional: a click β†’ callServerTool β†’ re-render step

A scenario may add an interact block to drive a real control inside the app iframe after it renders, exercising a user-initiated server round-trip (the settings_subscribe fixture does this - clicks "Add endpoint" β†’ settings.subscribe):

"expect": { "...": "...", "interaction_rendered": true },
"interact": {
  "fill":  { "selector": "input[type=url]", "value": "https://webhook.example.com/e2e" },
  "click": { "text": "Add endpoint" },
  "expect_dom_contains": ["Signing secret", "webhook.example.com"]
}

The iframe's callServerTool goes app β†’ Goose β†’ /mcp directly, bypassing the mock LLM, so this round-trip never appears in the tool-call log - the re-rendered DOM (expect_dom_contains, here the returned signing secret) is the only proof, and only the server's real response can produce it. For post-interaction DOM that plain text can't see, interact also accepts "expect_selectors": ["css", ...] - CSS selectors that must each match at least one element (e.g. img[src^="data:image/png;base64,"] to prove a fetched image's data URI was written into the iframe DOM - presence, not paint). interaction_rendered: true makes mcp_probe.py require it, and pw_scenario.mjs fails the drive if the post-click DOM never matches. This needs the "Add endpoint" control, which renders only when push_available is true - up.sh sets GMAIL_PUBSUB_TOPIC to ungate it (no Pub/Sub is ever contacted).

Why the mock cannot fake a PASS

The mock only follows the plan and returns canned narration; it never inspects tool results. The verdict is computed by mcp_probe.py from two records the mock doesn't control:

  • the rendered iframe - pw_scenario.mjs frameLocators into the real Goose DOM and asserts the Settings app's text. The iframe only exists if the server returned the app resource and Goose fetched + mounted it - a full round-trip.
  • the tool-call log - the calls Goose actually issued to /mcp and the results the server returned.

Two gates stop a no-op run from passing on stale data:

  • Drive must render. pw_scenario.mjs exits non-zero unless the app iframe rendered with the expected DOM; run_test.sh checks it (PIPESTATUS[0]).
  • Log must be new. run_test.sh snapshots the tool-call-log line count before driving; mcp_probe.py FAILs if no new entries were appended for this run.

Mock vs. real LLM

  • Mock (default): deterministic, offline, CI-friendly. configure_goose.sh mock.
  • Real LLM: configure_goose.sh real anthropic $KEY (model via $GOOSE_REAL_MODEL). The agent decides tool calls from the prompt; requires an API key and the provider host on the allowlist. The render/round-trip assertions are unchanged.

Sandbox gotchas already handled (do not re-discover)

Baked into the scripts; listed so you recognize them if something drifts:

SymptomCause β†’ handled by
Electron binary 403GitHub release assets blocked β†’ mirror + checksum (setup.sh)
@electron/node-gyp 403 on pnpm installgit tarball on codeload blocked β†’ registry override (setup.sh)
Electron SIGSEGV "Missing X server"needs a display β†’ xvfb-run owns Xvfb per test
Electron crash under Xvfb--no-sandbox --disable-gpu (NOT --in-process-gpu)
CDP /json empty / debug-port hangPlaywright-Electron launches over a CDP pipe
bg service dies next tool callsetsid ... & disown (start_detached); re-run up.sh to recover
/mcp 401the extension must send the seeded X-API-KEY header (configure_goose.sh)
app iframe not foundassert only on an app-returning tool; the Settings app renders offline

Files

FileRole
lib.shpaths/ports + bounded-wait/setsid helpers
setup.shone-time provisioning: build goose, Electron via mirror, pnpm install, dev bundle
seed.pycreate the e2e SQLite schema + one API key; prints the raw key
up.sh / down.shidempotent stack bring-up / teardown
configure_goose.shwrite Goose config for mock/real, wiring the mymcp extension at /mcp
mock_llm.pyscenario-engine mock LLM (follows the plan, logs tool calls)
pw_scenario.mjsPlaywright-Electron driver + rendered-iframe DOM assertion
mcp_probe.pyassertion oracle - rendered iframe (pw_result) + tool round-trip (toolcalls.jsonl)
run_test.sh / run_all.shdrive + assert one / all scenarios
scenarios/settings_render.jsonscenario 1: LLM opens the Settings app; assert it renders
scenarios/settings_subscribe.jsonscenario 2: click "Add endpoint" β†’ settings.subscribe β†’ assert the re-render
scenarios/gmail_thread_render.jsonscenario 3: LLM opens a thread via gmail_get_thread; assert the gmail_inbox reader iframe renders (needs GMAIL_FAKE_BACKEND)
scenarios/gmail_remote_images.jsonscenario 4: remote-image pipeline - assert the blocked-by-default "Show images" banner renders, click it, and assert the gmail_inbox.fetch_image round-trip re-renders as "Retry" (fixture URL is .invalid, so the SSRF guard rejects it deterministically offline and in CI)

Also outside the skill: tests/test_apps_e2e.py (guarded pytest entry) and .github/workflows/apps_e2e.yaml (opt-in CI).

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.