comfyui-plugin/skills/comfyui-pack-live-smoke/SKILL.md
Live-smoke a ComfyUI pack in a running instance: browser-driven and headless API-driven checks catch frontend/backend bugs unit tests miss. Use when verifying a pack before opening a registry PR.
npx skillsauth add laurigates/claude-plugins comfyui-pack-live-smokeInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
A ComfyUI pack's just check (pytest + vitest + tsc + build) can be fully
green while the pack is broken in the running app, because the two
halves are tested in isolation: pytest stubs server/folder_paths, vitest
only covers DOM-free pure helpers, and the modal DOM + the frontend↔backend
contract (route names, JSON shapes, gating predicates) are exercised by
neither. Stand the pack up in a real ComfyUI and click through it (or
drive it headlessly over the API) before opening the registry/gitops PR.
This is where bugs that ship green actually surface.
Canonical break (
comfyui-touch-manager, 2026-06): every gate green, jsdom modal mount green — yet live in the running app the Install-from-URL button was disabled on the normal127.0.0.1setup. The frontend gated onconfig.allow_remote_installalone (the non-loopback env override) while the backend/installgate actually permits the clone whenis_loopback || override. The headline feature was dead in the common case; only a browser smoke caught it.
| Use this skill when... | Use instead when... |
|---|---|
| Verifying a pack end-to-end in a running ComfyUI instance before opening a PR | Writing the pack's code in the first place -> comfyui-node-authoring |
| Confirming an edited/built workflow JSON actually runs | Editing the workflow JSON itself -> comfy-workflow-json |
COMFYUI_HOSTEvery recipe below reads the target instance from the COMFYUI_HOST
environment variable, defaulting to 127.0.0.1:8188 (a local/CPU-fork
smoke). Set it in .claude/settings.local.json's env block (gitignored,
per-machine) to point at a different install without hardcoding a host in
any command:
{ "env": { "COMFYUI_HOST": "192.0.2.10:8188" } }
When the backend enforces a security/permission gate, the frontend's show/enable/disable logic must replicate the entire predicate, not a convenient sub-flag. Gating on a partial predicate silently disables a feature in the case the backend would have allowed.
installPermitted(config) => config.is_loopback || config.allow_remote_install),
rather than leaving it as inline DOM logic — inline gating is exactly
what a pure-helper unit-test suite skips, so the bug ships green.if not (...)
check.| Friction | Fix |
|---|---|
| new_page/list_pages fail: "browser is already running for …/chrome-devtools-mcp/chrome-profile" | A stale automation Chrome (a prior MCP session) holds the profile. It's the MCP's own profile (--enable-automation, --remote-debugging-pipe), not your real browser — kill <parent-pid> it, then re-new_page. |
| Clicking a ComfyUI Vue dialog button (confirm/cancel) via MCP click silently no-ops (dialog stays open) | The synthetic MCP click doesn't reach the Vue overlay handler. Use evaluate_script → [...document.querySelectorAll("button")].find(b => b.textContent.trim() === "Confirm").click() (a real in-page DOM click the handler catches). |
| Result toasts (extensionManager.toast, ~4 s life) vanish before you can assert | Don't assert on the transient toast. Assert on a persistent signal instead — an in-modal status banner, the re-rendered list, or the backend state via curl. |
| Backend behaviour without the whole UI | The pack's routes answer over plain HTTP: curl -s http://$COMFYUI_HOST/<route> isolates backend vs. frontend instantly. |
resize_page to a phone width (~390–500px) and re-open the modal to verify
the touch layout (tap-target size, full-width modal, no iOS zoom).
python main.py --cpu), or point at an existing one via COMFYUI_HOST.custom_nodes/; bun run build so
web/dist/index.js is current; hard-reload the browser after any
frontend rebuild (no restart); restart the server after any backend
.py change.custom_nodes/, stop the
server.For verifying a workflow (or a backend node's registration/endpoints) without opening a browser — the fastest gold-standard check beyond JSON link-integrity validation.
ComfyUI's /prompt endpoint does not accept the saved UI workflow
JSON — it wants the API-format prompt: {node_id: {class_type, inputs}}. The browser frontend converts UI→API (graphToPrompt) before
POSTing; running headless means reimplementing that conversion. A
comfy_run.py-style script (queue, poll /history, print output
filenames or node_errors) is the reusable shape:
python3 comfy_run.py --host "${COMFYUI_HOST:-127.0.0.1:8188}" --timeout 600 wf.json
Run it from the ComfyUI install root so relative workflow paths resolve. The service must be up: `curl -sf http://$COMFYUI_HOST/object_info
/dev/null`.
.venv/bin/python -m py_compile custom_nodes/<pack>/<name>.py # syntax only, doesn't import standalone
# restart the service (systemd: sudo systemctl restart comfyui.service; else however yours restarts)
curl -s "http://${COMFYUI_HOST:-127.0.0.1:8188}/object_info/<NodeClassName>" # verify registration
curl -s "http://${COMFYUI_HOST:-127.0.0.1:8188}/<your_endpoint>" # smoke your routes
from server import PromptServer won't import standalone — the ComfyUI
runtime sets up sys.path and package import order specially, so a plain
local import test fails with a confusing ModuleNotFoundError. Restart +
curl is the reliable check.
input_order is not always present. /object_info/<Node> exposes
input.input_order for some node types but None for many. Fall back
to the dict-insertion order of input.required + input.optional.control_after_generate inserts a phantom widget value. A
seed/noise_seed input with control_after_generate: true produces
an extra entry in widgets_values that is not a node input — consume
one extra slot after any such widget or every later value shifts by one.widgets_values walk if the converter doesn't special-case it — the
/prompt call succeeds but later widgets silently take
default/garbage values. A workflow with such a node may need to be run
through the UI instead of a naive headless converter.Connection inputs (MODEL, CLIP, LATENT, CONDITIONING, IMAGE,
VAE) become [src_node_id_str, src_slot]; everything else is a widget
value. A widget that was converted-to-input (its inputs[] entry has a
non-null link) is read from the link, not from widgets_values.
| Call | Purpose |
|---|---|
| GET /object_info | per-node-type input schema (order, types, control flags) — drives the conversion |
| POST /prompt {prompt: <api>} | queue; returns {prompt_id}. HTTP 400 with error + node_errors on a bad graph |
| GET /history/<prompt_id> | poll; when the id appears, status.status_str is success/error and outputs[*].images[] lists {filename, subfolder, type} |
Poll /history with a short in-script time.sleep loop — not a shell
sleep-chain. Do not busy-poll /queue.
A pure refactor with no behaviour change and no frontend↔backend contract
touched, or the pack was already smoked this session at the same
comfyui-frontend-package version. Otherwise, for anything that adds/
changes a route, a gate, or modal wiring, smoke it before the PR.
development
Debug HTTP APIs: trace requests, inspect headers. Use when a request fails: check status first.
documentation
Render architecture diagrams from text sources. Use when documenting system topology.
tools
Inspect JSON payloads and extract nested fields. Use when parsing API responses.
tools
--- name: no-description allowed-tools: Read --- # No Description This skill has no description and must be dropped with a warning.