mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-06 17:57:43 +02:00
[eric] refactor: trim the verifier and mcp comments to one line each per the updated convention
This commit is contained in:
+1
-16
@@ -1,20 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// A tiny MCP server that hands a Claude Code instance the "hand + eyes" to drive
|
||||
// the REAL packaged OpenSwarm app: launch it, click, type, screenshot, read the
|
||||
// live DOM/accessibility tree, run JS in the renderer, and tail backend.log.
|
||||
//
|
||||
// Why this exists: the deterministic scripts in scripts/ci/ cover boot/serve/
|
||||
// resilience/network/agent-turn (fast, free, the CI gate). This covers the part
|
||||
// scripts can't express - actual GUI behavior - by letting a CC instance you talk
|
||||
// to drive the app at Playwright (DOM) precision. The official @playwright/mcp is
|
||||
// browser-only, so we wrap Playwright's Electron (_electron) API ourselves.
|
||||
//
|
||||
// Register it (repo-root .mcp.json) and any CC instance auto-connects:
|
||||
// { "mcpServers": { "openswarm-gui": { "command": "node",
|
||||
// "args": ["e2e/mcp/electron-mcp.js"] } } }
|
||||
//
|
||||
// The launched ElectronApplication + main Page are held across calls, so a CC
|
||||
// session clicks through a single live app the way a person would.
|
||||
// MCP server giving a Claude Code instance a Playwright hand on the real packaged app (launch/click/type/screenshot/eval/read-log); wraps Electron _electron since @playwright/mcp is browser-only. App held across calls. See e2e/mcp/README.md.
|
||||
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
|
||||
+2
-9
@@ -1,11 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// Proves the openswarm-gui MCP server actually works, end-to-end, the way a CC
|
||||
// instance would use it: spawn the server over stdio, list its tools, then (unless
|
||||
// --no-launch) launch the real app, screenshot it, read the log, and close.
|
||||
//
|
||||
// node e2e/mcp/selftest.js [--no-launch]
|
||||
//
|
||||
// Exit 0 = the hand works. Exit 1 = it doesn't (prints why).
|
||||
// Proves the openswarm-gui MCP server works: spawn over stdio, list tools, then (unless --no-launch) launch the app, screenshot, assert the renderer painted, and close.
|
||||
|
||||
'use strict';
|
||||
const path = require('path');
|
||||
@@ -41,8 +35,7 @@ async function main() {
|
||||
if (!isImage) throw new Error('screenshot did not return a PNG');
|
||||
process.stdout.write(`screenshot -> ${shot.content[0].data.length} base64 bytes\n`);
|
||||
|
||||
// A PNG of the right size could still be a BLANK window, so prove the renderer
|
||||
// actually painted: assert the React root mounted children (the real render gate).
|
||||
// A PNG of the right size could still be a blank window, so prove the renderer painted: assert #root mounted children.
|
||||
const root = await client.callTool({ name: 'eval', arguments: { expression: "document.getElementById('root').childElementCount" } });
|
||||
const childCount = Number(root.content?.[0]?.text);
|
||||
if (!(childCount > 0)) throw new Error(`renderer #root has ${root.content?.[0]?.text} children (blank window, not a real paint)`);
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
'use strict';
|
||||
// Shared plumbing for the packaged-app verifiers in scripts/ci/. Every verifier
|
||||
// needs the same things: find the built artifact for this OS, launch it, read the
|
||||
// backend.log it writes, and kill it cleanly. Keeping that here means each verifier
|
||||
// stays small and single-purpose (boot, resilience, signature, network).
|
||||
//
|
||||
// These helpers THROW on misuse and return data on success; the calling script
|
||||
// owns the pass/fail print + exit code so the harness has no opinion on policy.
|
||||
// Shared plumbing for the scripts/ci/ verifiers: locate the built artifact, launch it, read its backend.log, kill it cleanly. Helpers throw on misuse; callers own pass/fail.
|
||||
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
@@ -29,11 +23,9 @@ function packagedAppPath(explicit) {
|
||||
return found;
|
||||
}
|
||||
|
||||
// The on-disk binary the OS actually signs/scans: the .exe on win, the .app
|
||||
// bundle dir on mac (codesign/spctl assess the bundle, not the inner MachO).
|
||||
// The on-disk thing the OS signs/scans: the .exe on win, the .app bundle on mac.
|
||||
function signableTarget(appExecutable) {
|
||||
if (process.platform === 'darwin') {
|
||||
// .../OpenSwarm.app/Contents/MacOS/OpenSwarm -> .../OpenSwarm.app
|
||||
const i = appExecutable.indexOf('.app');
|
||||
return i === -1 ? appExecutable : appExecutable.slice(0, i + 4);
|
||||
}
|
||||
@@ -47,8 +39,7 @@ function backendLogPath() {
|
||||
return path.join(xdg, 'OpenSwarm', 'data', 'backend.log');
|
||||
}
|
||||
|
||||
// The bearer token the shell writes before the HTTP bind; tests reuse it to call
|
||||
// the same authenticated API the app itself uses.
|
||||
// The bearer token the shell writes before bind; tests reuse it to call the authed API.
|
||||
function authTokenPath() {
|
||||
const dir = path.dirname(backendLogPath());
|
||||
return path.join(dir, 'auth.token');
|
||||
@@ -62,8 +53,7 @@ function readFileSafe(p) { try { return fs.readFileSync(p, 'utf8'); } catch { re
|
||||
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
||||
|
||||
function spawnApp(appPath, extraArgs = []) {
|
||||
// detached on posix so we can SIGKILL the whole process group (the app spawns
|
||||
// python + 9router children); on win we reap by image name instead.
|
||||
// detached on posix so we can SIGKILL the whole process group (python + 9router children); on win we reap by image name.
|
||||
return spawn(appPath, extraArgs, { detached: process.platform !== 'win32', stdio: 'ignore', cwd: path.dirname(appPath) });
|
||||
}
|
||||
|
||||
@@ -86,8 +76,7 @@ function healthCode(port, timeoutMs = 3000) {
|
||||
});
|
||||
}
|
||||
|
||||
// Authenticated JSON call to the running backend, the same way the app calls it.
|
||||
// Returns { status, json, text }; status 0 means the request never completed.
|
||||
// Authenticated JSON call to the running backend; returns { status, json, text } (status 0 = never completed).
|
||||
function apiRequest(port, { method = 'GET', path = '/', token = '', body = null, timeoutMs = 30000 } = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const data = body != null ? Buffer.from(JSON.stringify(body)) : null;
|
||||
@@ -106,9 +95,7 @@ function apiRequest(port, { method = 'GET', path = '/', token = '', body = null,
|
||||
});
|
||||
}
|
||||
|
||||
// Find an already-running app to reuse (so we exercise the user's logged-in
|
||||
// creds) by reading the token off disk and the port out of the last backend.log,
|
||||
// then confirming it actually answers. Returns { port, token } or null.
|
||||
// Reuse an already-running app (the user's logged-in creds): read the token + last logged port and confirm it answers. Returns { port, token } or null.
|
||||
async function attachToRunning() {
|
||||
const token = readFileSafe(authTokenPath()).trim();
|
||||
const m = readFileSafe(backendLogPath()).match(/Backend ready on port (\d+)/g);
|
||||
@@ -133,11 +120,7 @@ function parsePerfMarks(log) {
|
||||
return marks;
|
||||
}
|
||||
|
||||
// Pure verdict on a backend.log: the log-based half of the boot check (provenance
|
||||
// matches HEAD, the three perf marks exist, are ordered, and are not degenerate).
|
||||
// Kept pure + exported so it can be mutation-tested (selftest-gate.js feeds it
|
||||
// crafted broken logs and proves each guard fires) without launching the app.
|
||||
// Returns { failures: string[], sha, marks }; empty failures == the log half passed.
|
||||
// Pure, mutation-testable verdict on a backend.log (provenance == HEAD, perf marks present/ordered/non-degenerate); returns { failures, sha, marks }, empty failures == passed.
|
||||
function bootFailures({ log, headShort } = {}) {
|
||||
const failures = [];
|
||||
const sha = parseProvenanceSha(log || '');
|
||||
@@ -156,8 +139,7 @@ function bootFailures({ log, headShort } = {}) {
|
||||
return { failures, sha, marks };
|
||||
}
|
||||
|
||||
// Launch the app and poll its backend.log until it reports HTTP-ready (or time out).
|
||||
// Returns { child, log, port }. Caller is responsible for killApp(child).
|
||||
// Launch the app and poll backend.log until HTTP-ready (or time out); returns { child, log, port }. Caller calls killApp.
|
||||
async function launchAndWait({ appPath, timeoutMs = 180000, freshLog = true } = {}) {
|
||||
const logPath = backendLogPath();
|
||||
if (freshLog) {
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// "Test the tests" - mutation testing of the gate's own logic. For each guard in
|
||||
// the boot check, we feed a deliberately BROKEN backend.log and assert the guard
|
||||
// fires (and that a good log passes). If breaking the input doesn't turn the gate
|
||||
// red, the gate is theater; this script fails loudly when that happens.
|
||||
//
|
||||
// This covers the PURE, log-based assertions (provenance + perf). The live-process
|
||||
// guards (signature --require-signed, wrong-token auth, verify-all aggregation,
|
||||
// renderer paint) are fault-injected separately - see GATE_AUDIT.md.
|
||||
//
|
||||
// node scripts/ci/selftest-gate.js
|
||||
//
|
||||
// Exit 0 = every guard discriminates good from broken. Exit 1 = a guard is fake.
|
||||
// Test-the-tests: feeds broken backend.logs to the boot check and asserts each guard fires (a good log passes); if a break stops going red, the gate is theater. Live-process guards: see GATE_AUDIT.md.
|
||||
|
||||
'use strict';
|
||||
const h = require('./lib/app-harness');
|
||||
@@ -43,8 +32,7 @@ check('degenerate all-zero marks -> caught', caught(
|
||||
'[provenance] OpenSwarm 1 sha=abc123def456 channel=stable\n[perf] app-launch t=0\n[perf] first-paint t=0\n[perf] backend-http-ready t=0',
|
||||
HEAD, /> 0|degenerate/));
|
||||
|
||||
// And a stale build (old sha) must be caught even with all marks fine - the exact
|
||||
// real-world case we already saw fire live.
|
||||
// A stale build (old sha, all marks fine) must still be caught - the case we saw fire live.
|
||||
check('stale build (every mark fine, wrong sha) -> still caught', caught(GOOD.replace('abc123def456', 'deadbeef0000'), HEAD, /!= git HEAD/));
|
||||
|
||||
process.stdout.write('\nparse-function edge cases:\n');
|
||||
|
||||
@@ -1,23 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// Drives a REAL agent turn end-to-end against the packaged app, using whatever
|
||||
// provider the app is already logged into on THIS machine (your subscription /
|
||||
// 9router / API key) — no separate key, no second agent. It proves the full
|
||||
// round-trip: session launch -> message -> the model actually generated a reply.
|
||||
//
|
||||
// Why this is gated: a real turn spends a sliver of your LLM quota. So it only
|
||||
// runs when OPENSWARM_E2E_AGENT=1; otherwise it prints a skip and exits 0, which
|
||||
// keeps CI from silently billing you.
|
||||
//
|
||||
// How it proves a real reply (not a fake pass): it launches with NO tools (so the
|
||||
// agent can't trip an approval gate and hang) and a trivial prompt, then polls the
|
||||
// session until status is terminal and asserts the model produced output tokens
|
||||
// (tokens.output > 0) with no error. Output tokens can only come from a live
|
||||
// provider answering — they can't be faked by the harness.
|
||||
//
|
||||
// OPENSWARM_E2E_AGENT=1 node scripts/ci/verify-agent-turn.js [--app <path>] [--prompt "..."]
|
||||
//
|
||||
// Note: the [perf] first-agent-response mark is emitted by the RENDERER, so it is
|
||||
// asserted in the GUI walkthrough, not here (this path is API-driven by design).
|
||||
// Drives a REAL agent turn on the app's own login (gated by OPENSWARM_E2E_AGENT=1; spends quota, else skips). Launches tool-free (no approval-gate hang) and asserts terminal status + tokens.output>0, a live reply the harness cant fake.
|
||||
|
||||
'use strict';
|
||||
const h = require('./lib/app-harness');
|
||||
|
||||
@@ -1,22 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// Verifies the Windows NSIS installer (OpenSwarm-Setup-x64.exe). Two modes,
|
||||
// because the installer is oneClick + per-user to a FIXED dir
|
||||
// (%LOCALAPPDATA%\Programs\OpenSwarm), so a blind install/uninstall on a machine
|
||||
// that already has OpenSwarm would DESTROY the user's real install.
|
||||
//
|
||||
// default (safe, observational): validate the Setup.exe artifact, and if an
|
||||
// install is present, assert its invariants (dir layout, uninstaller registry
|
||||
// entry whose target actually exists, shortcuts). Touches nothing.
|
||||
//
|
||||
// --destructive: the real cycle - install /S, assert invariants, (optionally
|
||||
// launch + verify-all), uninstall /S, assert removal AND that user data
|
||||
// survived (deleteAppDataOnUninstall:false). REFUSES if an install already
|
||||
// exists unless --force, so it only runs where there is nothing to clobber
|
||||
// (a clean machine / CI runner). This is what the CI installer job runs.
|
||||
//
|
||||
// node scripts/ci/verify-installer.js [--setup <path>] [--destructive] [--force]
|
||||
//
|
||||
// Exit 0 = invariants hold. Exit 1 = a real installer/install problem.
|
||||
// Verifies the Windows NSIS installer: default safely observes an existing install (dir, an uninstaller that exists, shortcuts); --destructive runs install->verify->uninstall but refuses to clobber an existing install unless --force (clean machine / CI).
|
||||
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
@@ -47,8 +30,7 @@ function defaultSetup() {
|
||||
return exists(p) ? p : null;
|
||||
}
|
||||
|
||||
// The shipped installer must be a real, complete PE - not a 0-byte stub or a
|
||||
// truncated upload (a real CI failure mode).
|
||||
// The shipped installer must be a real, complete PE (catch a 0-byte stub or truncated upload).
|
||||
function checkSetupArtifact(setup) {
|
||||
process.stdout.write(`Setup.exe: ${setup}\n`);
|
||||
if (!exists(setup)) { bad(`Setup.exe missing at ${setup}`); return; }
|
||||
@@ -84,8 +66,7 @@ function findShortcuts() {
|
||||
return found;
|
||||
}
|
||||
|
||||
// Assert the on-disk + registry state a correct install produces. Used both to
|
||||
// observe an existing install (safe) and to verify a fresh one (destructive).
|
||||
// Assert the on-disk + registry state a correct install produces (used by both observe and destructive).
|
||||
function assertInstalled() {
|
||||
if (!exists(INSTALL_DIR)) { bad(`install dir missing: ${INSTALL_DIR}`); return; }
|
||||
ok(`install dir present: ${INSTALL_DIR}`);
|
||||
@@ -97,8 +78,7 @@ function assertInstalled() {
|
||||
else {
|
||||
ok(`uninstall registry entry: ${reg.split('|')[0]}`);
|
||||
const up = uninstallerPathFromReg(reg);
|
||||
// A registered uninstaller whose target does not exist is an orphaned entry -
|
||||
// a real, user-visible "can't uninstall" bug. Assert the target actually exists.
|
||||
// A registered uninstaller whose target is missing is an orphaned "can't uninstall" bug, so assert it exists.
|
||||
if (!up) bad('uninstall entry has no parseable uninstaller path');
|
||||
else if (!exists(up)) bad(`uninstaller registered but missing on disk: ${up}`);
|
||||
else ok(`uninstaller exists: ${up}`);
|
||||
|
||||
@@ -1,22 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// Exercises the network/auth plumbing the app depends on, using the SAME bearer
|
||||
// token and ports the running app uses (no mocks). Splits into two tiers:
|
||||
//
|
||||
// LOCAL (always hard-asserted, deterministic):
|
||||
// - auth.token exists on disk (the load-bearing pre-bind handshake, auth.py)
|
||||
// - an authed endpoint rejects with no token (401/403) and accepts with it
|
||||
// (proves the bearer wiring, not just that a port is open)
|
||||
// - the bundled 9router subprocess is listening on :20128 (LLM routing + the
|
||||
// OAuth authorize/exchange path both go through it)
|
||||
//
|
||||
// EXTERNAL (best-effort; --strict makes them hard):
|
||||
// - api.openswarm.com reachable (the fly-hosted proxy OAuth + sign-in use)
|
||||
// - the GitHub Releases feed reachable (what the auto-updater reads)
|
||||
//
|
||||
// Completing a real OAuth flow is inherently interactive (a browser), so it lives
|
||||
// in the GUI walkthrough; here we prove its DEPENDENCIES are reachable.
|
||||
//
|
||||
// node scripts/ci/verify-network.js [--app <path>] [--strict] [--timeout-ms 120000]
|
||||
// Exercises auth/network with the app's real bearer token: no-token and wrong-token rejected (401), real token 200, 9router on :20128. External reachability is best-effort unless --strict.
|
||||
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
@@ -84,9 +67,7 @@ async function main() {
|
||||
const authedPath = '/api/settings/default-system-prompt';
|
||||
const noTok = await httpStatus(port, authedPath, {});
|
||||
const withTok = await httpStatus(port, authedPath, { Authorization: `Bearer ${token}` });
|
||||
// Wrong-token MUST be rejected too. Without this, a backend that accepts ANY
|
||||
// Authorization header would pass no-token=401 + with-token=200 while auth is
|
||||
// actually broken. This probe is what makes the 200 mean "validated".
|
||||
// Wrong-token must also be rejected: else a backend accepting ANY Authorization header passes no-token+real-token while auth is broken. This makes the 200 mean "validated".
|
||||
const badTok = await httpStatus(port, authedPath, { Authorization: 'Bearer not-a-real-token-deadbeefcafe' });
|
||||
if (![401, 403].includes(noTok)) failures.push(`authed endpoint returned ${noTok} WITHOUT token (expected 401/403)`);
|
||||
if ([401, 403, 0].includes(withTok)) failures.push(`authed endpoint returned ${withTok} WITH the real token (expected it honored)`);
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// Deterministic "does the packaged app actually boot and serve" check — a plain
|
||||
// script, no browser/Electron automation (which is flaky: single-instance locks,
|
||||
// target-closed races). It launches the REAL built exe/app, waits for the backend,
|
||||
// and reads the same backend.log the shipped app writes to confirm the boot:
|
||||
//
|
||||
// - [provenance] line present and its sha == git rev-parse HEAD (right build)
|
||||
// - [perf] app-launch < first-paint < backend-http-ready (UI painted, ordered)
|
||||
// - the backend answers /api/health/check with 200 (it actually serves)
|
||||
//
|
||||
// first-paint coming from the log means we prove the renderer painted WITHOUT
|
||||
// scraping the DOM. Reserve Playwright for genuine GUI-click regressions; this
|
||||
// covers "did the artifact boot and serve" far more robustly.
|
||||
//
|
||||
// node scripts/ci/verify-packaged-app.js [--app <path>] [--timeout-ms 180000]
|
||||
//
|
||||
// Exit 0 = all good. Exit 1 = something didn't boot/serve/match (prints why).
|
||||
// Launches the built app and reads backend.log: provenance sha == git HEAD, perf marks ordered (proves first paint), health 200. No browser automation; Playwright is for GUI clicks.
|
||||
|
||||
'use strict';
|
||||
const h = require('./lib/app-harness');
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// Proves the packaged app survives the two machine-variance cases that most often
|
||||
// cause "worked on mine, broke on theirs" — both already handled in main.js, but
|
||||
// never verified:
|
||||
//
|
||||
// --locked-port: something else already holds the preferred backend port.
|
||||
// We bind 8324.. on 127.0.0.1, launch, and assert the app
|
||||
// still comes up on a DIFFERENT port and serves 200
|
||||
// (pickBackendPort fallback, main.js:551).
|
||||
// --multi-instance: the user double-clicks / a deep link relaunches the app.
|
||||
// We launch one instance, then a second; the second must exit
|
||||
// cleanly (single-instance lock at main.js:117) while the
|
||||
// first keeps serving.
|
||||
//
|
||||
// node scripts/ci/verify-resilience.js [--app <path>] [--locked-port] [--multi-instance]
|
||||
//
|
||||
// With no check flag, runs both. Exit 0 = all ran passed; exit 1 = a failure.
|
||||
// Proves the app survives a taken preferred port (--locked-port falls back, still serves 200) and a second launch (--multi-instance exits cleanly, first keeps serving). No flag runs both.
|
||||
|
||||
'use strict';
|
||||
const net = require('net');
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// Reports — and, with --require-signed, ENFORCES — the code-signing state of a
|
||||
// built artifact. Local dev builds are intentionally unsigned
|
||||
// (CSC_IDENTITY_AUTO_DISCOVERY=false), so by default this only REPORTS so you
|
||||
// always know what you're holding. The release workflows pass --require-signed
|
||||
// AFTER Azure/Apple have signed, turning it into the SmartScreen/Gatekeeper gate:
|
||||
//
|
||||
// Windows: Get-AuthenticodeSignature .Status == Valid (Authenticode -> no SmartScreen block)
|
||||
// macOS: codesign --verify --deep --strict AND spctl --assess == accepted (Gatekeeper)
|
||||
// plus a stapled notarization ticket (stapler validate)
|
||||
//
|
||||
// node scripts/ci/verify-signature.js [--target <path>] [--require-signed]
|
||||
//
|
||||
// Exit 0 = reported ok (or signed when required). Exit 1 = required but not signed.
|
||||
// Reports code-signing state; --require-signed fails unless signed (release CI gate, post-sign). Local builds are unsigned by design. Win: Authenticode Valid; mac: codesign + spctl + staple.
|
||||
|
||||
'use strict';
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
Reference in New Issue
Block a user