@empir3/empir3-bridge 0.3.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1531 -0
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +75 -0
- package/LICENSE +21 -0
- package/README.md +464 -0
- package/SECURITY.md +130 -0
- package/assets/accuracy-lab.html +2639 -0
- package/assets/api-clis-real.jpg +0 -0
- package/assets/bridge-console-hero.jpg +0 -0
- package/assets/browser-privacy.svg +151 -0
- package/assets/demo-orchestration.svg +74 -0
- package/assets/desktop-select-region.jpg +0 -0
- package/assets/in-page-chat.gif +0 -0
- package/assets/orchestration-hero.svg +126 -0
- package/assets/social-preview.png +0 -0
- package/assets/zara-accent.png +0 -0
- package/build/bootstrap.js +548 -0
- package/build/build.js +680 -0
- package/build/payload-entry.js +649 -0
- package/build/payload-signing-pub.json +7 -0
- package/docs/AGENT_GUIDE.md +259 -0
- package/docs/RELEASE.md +106 -0
- package/docs/SAFETY.md +112 -0
- package/docs/TESTING.md +181 -0
- package/installer/server.js +231 -0
- package/installer/ui/app.js +278 -0
- package/installer/ui/index.html +24 -0
- package/installer/ui/styles.css +146 -0
- package/package.json +95 -0
- package/scripts/bootstrap-e2e.mjs +650 -0
- package/scripts/certify-bridge.mjs +636 -0
- package/scripts/check-companion-surface.mjs +118 -0
- package/scripts/extract-welcome.mjs +64 -0
- package/scripts/gh-route-handler-check.mjs +57 -0
- package/scripts/gh-wire-test.mjs +107 -0
- package/scripts/publish-downloads.mjs +180 -0
- package/scripts/smoke-all-tools.mjs +509 -0
- package/scripts/smoke-live-bridge.mjs +696 -0
- package/scripts/splice-welcome.mjs +63 -0
- package/scripts/welcome-body.txt +2733 -0
- package/src/anthropic-client.ts +192 -0
- package/src/bootstrap-exe.ts +69 -0
- package/src/bridge.ts +2444 -0
- package/src/chat.ts +345 -0
- package/src/cli-runner.ts +239 -0
- package/src/cli.ts +649 -0
- package/src/config.ts +199 -0
- package/src/desktop-overlay.ps1 +121 -0
- package/src/executable-resolver.ts +330 -0
- package/src/handlers/agy-imagegen.ts +179 -0
- package/src/handlers/github-cli.ts +399 -0
- package/src/handlers/higgsfield-cli.ts +783 -0
- package/src/launch.js +337 -0
- package/src/mcp-server.ts +1265 -0
- package/src/pair-claim.ts +218 -0
- package/src/payload-daemon.ts +168 -0
- package/src/server.ts +21036 -0
- package/src/tool-defaults.ts +230 -0
- package/src/update-check.js +136 -0
- package/tray/build.py +76 -0
- package/tray/requirements.txt +2 -0
- package/tray/tray.py +1843 -0
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Full-coverage smoke driver for every tool in TOOL_META.
|
|
3
|
+
//
|
|
4
|
+
// Calls each tool via /api/command, captures pass/fail + a short response
|
|
5
|
+
// excerpt + any screenshot path it produced, and writes a single markdown
|
|
6
|
+
// report.
|
|
7
|
+
//
|
|
8
|
+
// Tiers:
|
|
9
|
+
// 1 — read/status (always safe; no side effects)
|
|
10
|
+
// 2 — browser navigate (visible, but only on the bridge welcome page)
|
|
11
|
+
// 3 — browser interact (safe on the bridge welcome page)
|
|
12
|
+
// 4 — desktop interact (moves the user's mouse / clicks real coords) —
|
|
13
|
+
// only runs with --tier4 flag. User must be near the keyboard.
|
|
14
|
+
// 5 — recordings (creates files; safe)
|
|
15
|
+
// 6 — eval (default-off; only with --eval)
|
|
16
|
+
//
|
|
17
|
+
// Usage:
|
|
18
|
+
// node scripts/smoke-all-tools.mjs # tiers 1-3 + 5
|
|
19
|
+
// node scripts/smoke-all-tools.mjs --tier4 # add desktop interact
|
|
20
|
+
// node scripts/smoke-all-tools.mjs --eval # add browser_evaluate
|
|
21
|
+
// node scripts/smoke-all-tools.mjs --only=read # subset by tier name
|
|
22
|
+
//
|
|
23
|
+
// Report: build/smoke-all-<stamp>/report.md
|
|
24
|
+
|
|
25
|
+
import { mkdirSync, writeFileSync, readFileSync, copyFileSync, existsSync } from 'node:fs';
|
|
26
|
+
import { createServer } from 'node:http';
|
|
27
|
+
import { join, basename, resolve, dirname } from 'node:path';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
|
|
30
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
31
|
+
const BRIDGE = process.env.BRIDGE_SMOKE_URL || 'http://127.0.0.1:3006';
|
|
32
|
+
const STAMP = new Date().toISOString().replace(/[:.]/g, '-');
|
|
33
|
+
const OUT_DIR = resolve(process.env.BRIDGE_SMOKE_OUT || join(ROOT, 'build', `smoke-all-${STAMP}`));
|
|
34
|
+
const SHOTS_DIR = join(OUT_DIR, 'shots');
|
|
35
|
+
mkdirSync(SHOTS_DIR, { recursive: true });
|
|
36
|
+
|
|
37
|
+
const args = new Set(process.argv.slice(2));
|
|
38
|
+
const onlyArg = process.argv.slice(2).find(a => a.startsWith('--only='));
|
|
39
|
+
const ONLY = onlyArg ? onlyArg.split('=')[1].split(',') : null;
|
|
40
|
+
const RUN_TIER4 = args.has('--tier4');
|
|
41
|
+
const RUN_EVAL = args.has('--eval');
|
|
42
|
+
|
|
43
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
44
|
+
|
|
45
|
+
async function command(body, { timeoutMs = 20000 } = {}) {
|
|
46
|
+
const ctrl = new AbortController();
|
|
47
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch(`${BRIDGE}/api/command`, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { 'Content-Type': 'application/json' },
|
|
52
|
+
body: JSON.stringify(body),
|
|
53
|
+
signal: ctrl.signal,
|
|
54
|
+
});
|
|
55
|
+
const text = await res.text();
|
|
56
|
+
let json;
|
|
57
|
+
try { json = JSON.parse(text); } catch (e) {
|
|
58
|
+
// Sanitize unescaped control chars (some PS responses include raw newlines in element names)
|
|
59
|
+
try { json = JSON.parse(text.replace(/[\u0000-\u001F]/g, ' ')); }
|
|
60
|
+
catch { json = { ok: false, error: `JSON parse failed: ${e.message}`, _raw_len: text.length }; }
|
|
61
|
+
}
|
|
62
|
+
return { status: res.status, body: json };
|
|
63
|
+
} finally { clearTimeout(t); }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function summarise(value, limit = 500) {
|
|
67
|
+
if (value == null) return String(value);
|
|
68
|
+
if (typeof value !== 'object') return String(value).slice(0, limit);
|
|
69
|
+
try {
|
|
70
|
+
const s = JSON.stringify(value, (k, v) => {
|
|
71
|
+
if (typeof v === 'string' && /^[A-Za-z0-9+/=]{600,}$/.test(v)) return `[base64 ${v.length}b]`;
|
|
72
|
+
if (typeof k === 'string' && /(screenshot|base64|thumbnail|data)/i.test(k) && typeof v === 'string' && v.length > 200) {
|
|
73
|
+
return `[base64 ${v.length}b]`;
|
|
74
|
+
}
|
|
75
|
+
return v;
|
|
76
|
+
});
|
|
77
|
+
return s.length > limit ? s.slice(0, limit) + `... (${s.length} chars total)` : s;
|
|
78
|
+
} catch { return String(value).slice(0, limit); }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function extractScreenshotPath(result) {
|
|
82
|
+
const paths = [];
|
|
83
|
+
const walk = (v) => {
|
|
84
|
+
if (!v || typeof v !== 'object') return;
|
|
85
|
+
if (Array.isArray(v)) { v.forEach(walk); return; }
|
|
86
|
+
for (const [k, val] of Object.entries(v)) {
|
|
87
|
+
if (typeof val === 'string' && /\.(png|jpg|jpeg)$/i.test(val) && /[\\/]/.test(val)) {
|
|
88
|
+
if (existsSync(val)) paths.push(val);
|
|
89
|
+
} else if (val && typeof val === 'object') walk(val);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
walk(result);
|
|
93
|
+
return paths;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const results = [];
|
|
97
|
+
let snapshotRef = null; // browser ref captured for interact tests
|
|
98
|
+
let desktopRef = null; // desktop UIA ref captured for tier4
|
|
99
|
+
|
|
100
|
+
async function runTest(t) {
|
|
101
|
+
if (ONLY && !ONLY.includes(t.tier)) return;
|
|
102
|
+
if (t.tier === '4' && !RUN_TIER4) {
|
|
103
|
+
results.push({ name: t.name, tier: t.tier, status: 'SKIP', note: 'tier 4 (desktop interact) — pass --tier4 to enable' });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (t.tier === 'eval' && !RUN_EVAL) {
|
|
107
|
+
results.push({ name: t.name, tier: t.tier, status: 'SKIP', note: 'eval — pass --eval to enable' });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const started = Date.now();
|
|
111
|
+
try {
|
|
112
|
+
const out = await t.run();
|
|
113
|
+
const ms = Date.now() - started;
|
|
114
|
+
const result = out?.result ?? out;
|
|
115
|
+
const shots = extractScreenshotPath(result);
|
|
116
|
+
let copied = [];
|
|
117
|
+
for (const s of shots.slice(0, 2)) {
|
|
118
|
+
const dst = join(SHOTS_DIR, `${t.name}-${basename(s)}`);
|
|
119
|
+
try { copyFileSync(s, dst); copied.push(dst); } catch {}
|
|
120
|
+
}
|
|
121
|
+
const pass = out?.status ? (out.status < 400) : true;
|
|
122
|
+
const okBody = out?.body ? out.body.ok !== false : true;
|
|
123
|
+
results.push({
|
|
124
|
+
name: t.name,
|
|
125
|
+
tier: t.tier,
|
|
126
|
+
status: pass && okBody ? 'PASS' : 'FAIL',
|
|
127
|
+
ms,
|
|
128
|
+
summary: summarise(result),
|
|
129
|
+
shots: copied,
|
|
130
|
+
error: pass && okBody ? null : (out?.body?.error || `HTTP ${out?.status}`),
|
|
131
|
+
});
|
|
132
|
+
} catch (e) {
|
|
133
|
+
results.push({ name: t.name, tier: t.tier, status: 'FAIL', ms: Date.now() - started, error: e?.message || String(e) });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ────────────────── TESTS ──────────────────
|
|
138
|
+
|
|
139
|
+
const tests = [];
|
|
140
|
+
|
|
141
|
+
// ── TIER 1: read / status ──
|
|
142
|
+
// Wire protocol uses short type names (status, text, navigate, click...) —
|
|
143
|
+
// the tool-meta name is the user-facing label; we tag results by tool-meta
|
|
144
|
+
// but POST with the wire name.
|
|
145
|
+
const READ = [
|
|
146
|
+
['browser_status', () => command({ type: 'status' })],
|
|
147
|
+
['browser_text', async () => {
|
|
148
|
+
await openBrowserFixture();
|
|
149
|
+
return command({ type: 'text' });
|
|
150
|
+
}],
|
|
151
|
+
['browser_snapshot', async () => {
|
|
152
|
+
await openBrowserFixture();
|
|
153
|
+
const r = await command({ type: 'snapshot' });
|
|
154
|
+
const nodes = r.body?.result?.nodes || r.body?.result?.snapshot?.nodes;
|
|
155
|
+
const first = nodes?.[0]?.ref;
|
|
156
|
+
if (first) snapshotRef = first;
|
|
157
|
+
return r;
|
|
158
|
+
}],
|
|
159
|
+
['browser_screenshot', async () => {
|
|
160
|
+
await openBrowserFixture();
|
|
161
|
+
return command({ type: 'screenshot' });
|
|
162
|
+
}],
|
|
163
|
+
['desktop_monitors', () => command({ type: 'desktop_monitors' })],
|
|
164
|
+
['desktop_screenshot', () => command({ type: 'desktop_screenshot', monitor: 'primary' })],
|
|
165
|
+
['desktop_screenshot_zoom', async () => {
|
|
166
|
+
const mon = await command({ type: 'desktop_monitors' });
|
|
167
|
+
const m = mon.body?.result?.monitors?.[0] || { bounds: { x: 0, y: 0, width: 1920, height: 1080 }};
|
|
168
|
+
const cx = m.bounds.x + Math.round(m.bounds.width / 2);
|
|
169
|
+
const cy = m.bounds.y + Math.round(m.bounds.height / 2);
|
|
170
|
+
return command({ type: 'desktop_screenshot_zoom', x: cx, y: cy, radius: 60 });
|
|
171
|
+
}],
|
|
172
|
+
['desktop_cursor_position', () => command({ type: 'desktop_cursor_position' })],
|
|
173
|
+
['desktop_snapshot', async () => {
|
|
174
|
+
const r = await command({ type: 'desktop_snapshot', scope: 'foreground', maxElements: 30 });
|
|
175
|
+
const first = r.body?.result?.elements?.[0]?.ref;
|
|
176
|
+
if (first) desktopRef = first;
|
|
177
|
+
return r;
|
|
178
|
+
}],
|
|
179
|
+
['desktop_focus_status', () => command({ type: 'desktop_focus_status' })],
|
|
180
|
+
['desktop_pointer_status', () => command({ type: 'desktop_pointer_status' })],
|
|
181
|
+
['desktop_calibration_status', () => command({ type: 'desktop_calibration_status' })],
|
|
182
|
+
['desktop_snapshot_som', () => command({ type: 'desktop_snapshot_som', region: { x: 0, y: 0, width: 800, height: 600 } })],
|
|
183
|
+
];
|
|
184
|
+
for (const [name, run] of READ) tests.push({ name, tier: '1', run });
|
|
185
|
+
|
|
186
|
+
// ── TIER 2: browser navigate (safe — bridge welcome) ──
|
|
187
|
+
const WELCOME = `${BRIDGE}/welcome`;
|
|
188
|
+
const BROWSER_FIXTURE_HTML = '<!doctype html><title>Smoke</title><button id=b aria-label="Smoke Click Target">Click Target</button><input id=i aria-label="Smoke Input"><div style="height:1400px">Scroll</div><script>window.smokeClicked=0;window.lastKey="";b.onclick=function(){window.smokeClicked++};document.onkeydown=function(e){window.lastKey=e.key}</script>';
|
|
189
|
+
let browserFixtureSeq = 0;
|
|
190
|
+
let browserFixtureBasePromise = null;
|
|
191
|
+
let browserFixtureServer = null;
|
|
192
|
+
|
|
193
|
+
function browserFixtureBase() {
|
|
194
|
+
if (browserFixtureBasePromise) return browserFixtureBasePromise;
|
|
195
|
+
browserFixtureBasePromise = new Promise((resolveBase, reject) => {
|
|
196
|
+
const server = createServer((req, res) => {
|
|
197
|
+
const url = new URL(req.url || '/', 'http://127.0.0.1');
|
|
198
|
+
if (url.pathname !== '/fixture') {
|
|
199
|
+
res.writeHead(404, { 'Content-Type': 'text/plain', 'Connection': 'close' });
|
|
200
|
+
res.end('not found');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const nonce = url.searchParams.get('nonce') || '';
|
|
204
|
+
const html = `${BROWSER_FIXTURE_HTML}<script>window.smokeNonce=${JSON.stringify(nonce)}</script>`;
|
|
205
|
+
res.writeHead(200, {
|
|
206
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
207
|
+
'Content-Length': Buffer.byteLength(html),
|
|
208
|
+
'Connection': 'close',
|
|
209
|
+
});
|
|
210
|
+
res.end(html);
|
|
211
|
+
});
|
|
212
|
+
server.on('error', reject);
|
|
213
|
+
server.listen(0, '127.0.0.1', () => {
|
|
214
|
+
browserFixtureServer = server;
|
|
215
|
+
server.unref();
|
|
216
|
+
const address = server.address();
|
|
217
|
+
if (!address || typeof address === 'string') {
|
|
218
|
+
reject(new Error('fixture server did not bind to a TCP port'));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
resolveBase(`http://127.0.0.1:${address.port}`);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
return browserFixtureBasePromise;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function openBrowserFixture() {
|
|
228
|
+
let lastError = 'fixture did not open';
|
|
229
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
230
|
+
const nonce = `smoke-${Date.now()}-${++browserFixtureSeq}`;
|
|
231
|
+
const base = await browserFixtureBase();
|
|
232
|
+
const url = `${base}/fixture?nonce=${encodeURIComponent(nonce)}`;
|
|
233
|
+
try {
|
|
234
|
+
const r = await command({ type: 'navigate', url }, { timeoutMs: 50000 });
|
|
235
|
+
const ready = await waitForBrowserFixture(nonce, 20000);
|
|
236
|
+
if (ready.ok) return r;
|
|
237
|
+
lastError = ready.error;
|
|
238
|
+
} catch (e) {
|
|
239
|
+
lastError = e?.message || String(e);
|
|
240
|
+
}
|
|
241
|
+
if (attempt < 3) await sleep(1000);
|
|
242
|
+
}
|
|
243
|
+
return { status: 500, body: { ok: false, error: lastError } };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function waitForBrowserFixture(nonce, timeoutMs) {
|
|
247
|
+
const deadline = Date.now() + timeoutMs;
|
|
248
|
+
let last = null;
|
|
249
|
+
while (Date.now() < deadline) {
|
|
250
|
+
await sleep(500);
|
|
251
|
+
try {
|
|
252
|
+
const r = await command({
|
|
253
|
+
type: 'evaluate',
|
|
254
|
+
script: `JSON.stringify({ href: location.href, ready: document.readyState, nonce: window.smokeNonce })`,
|
|
255
|
+
}, { timeoutMs: 5000 });
|
|
256
|
+
last = r;
|
|
257
|
+
const raw = r.body?.result?.result;
|
|
258
|
+
const result = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
259
|
+
if (result?.nonce === nonce && result?.ready !== 'loading') return { ok: true };
|
|
260
|
+
} catch (e) {
|
|
261
|
+
last = e;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return { ok: false, error: `fixture did not become ready for nonce ${nonce}: ${summarise(last)}` };
|
|
265
|
+
}
|
|
266
|
+
tests.push({ name: 'browser_navigate', tier: '2', run: () => openBrowserFixture() });
|
|
267
|
+
tests.push({ name: 'browser_scroll', tier: '2', run: async () => {
|
|
268
|
+
await openBrowserFixture();
|
|
269
|
+
return command({ type: 'scroll', y: 200 });
|
|
270
|
+
}});
|
|
271
|
+
tests.push({ name: 'browser_refresh', tier: '2', run: async () => {
|
|
272
|
+
await openBrowserFixture();
|
|
273
|
+
return command({ type: 'refresh' });
|
|
274
|
+
}});
|
|
275
|
+
|
|
276
|
+
// ── TIER 3: browser interact (welcome page has labeled buttons) ──
|
|
277
|
+
tests.push({ name: 'browser_click', tier: '3', run: async () => {
|
|
278
|
+
await openBrowserFixture();
|
|
279
|
+
const clicked = await command({ type: 'click', selector: '#b' });
|
|
280
|
+
const check = await command({ type: 'evaluate', script: 'window.smokeClicked' });
|
|
281
|
+
if (check.body?.result?.result !== 1) return { status: 500, body: { ok: false, error: `click did not update fixture counter: ${summarise(check)}` }};
|
|
282
|
+
return clicked;
|
|
283
|
+
}});
|
|
284
|
+
tests.push({ name: 'browser_click_ref', tier: '3', run: async () => {
|
|
285
|
+
await openBrowserFixture();
|
|
286
|
+
const snap = await command({ type: 'snapshot' });
|
|
287
|
+
const nodes = snap.body?.result?.nodes || snap.body?.result?.snapshot?.nodes || [];
|
|
288
|
+
const ref = nodes.find(n => n.role === 'button' && /click target/i.test(n.name || ''))?.ref || nodes.find(n => n.role === 'button')?.ref;
|
|
289
|
+
if (!ref) return { status: 500, body: { ok: false, error: `no button ref on page (snapshot returned ${nodes.length} nodes)` }};
|
|
290
|
+
const clicked = await command({ type: 'click_ref', ref });
|
|
291
|
+
const check = await command({ type: 'evaluate', script: 'window.smokeClicked' });
|
|
292
|
+
if (check.body?.result?.result !== 1) return { status: 500, body: { ok: false, error: `click_ref did not update fixture counter: ${summarise(check)}` }};
|
|
293
|
+
return clicked;
|
|
294
|
+
}});
|
|
295
|
+
tests.push({ name: 'browser_click_xy', tier: '3', run: async () => {
|
|
296
|
+
await openBrowserFixture();
|
|
297
|
+
const clicked = await command({ type: 'click_xy', x: 35, y: 18 });
|
|
298
|
+
const check = await command({ type: 'evaluate', script: 'window.smokeClicked' });
|
|
299
|
+
if (check.body?.result?.result !== 1) return { status: 500, body: { ok: false, error: `click_xy did not update fixture counter: ${summarise(check)}` }};
|
|
300
|
+
return clicked;
|
|
301
|
+
}});
|
|
302
|
+
tests.push({ name: 'browser_type', tier: '3', run: async () => {
|
|
303
|
+
await openBrowserFixture();
|
|
304
|
+
const typed = await command({ type: 'type', selector: '#i', text: 'smoke-test' });
|
|
305
|
+
const check = await command({ type: 'evaluate', script: 'document.querySelector("#i")?.value' });
|
|
306
|
+
if (check.body?.result?.result !== 'smoke-test') return { status: 500, body: { ok: false, error: `type did not update fixture input: ${summarise(check)}` }};
|
|
307
|
+
return typed;
|
|
308
|
+
}});
|
|
309
|
+
tests.push({ name: 'browser_type_ref', tier: '3', run: async () => {
|
|
310
|
+
await openBrowserFixture();
|
|
311
|
+
const snap = await command({ type: 'snapshot' });
|
|
312
|
+
const nodes = snap.body?.result?.nodes || snap.body?.result?.snapshot?.nodes || [];
|
|
313
|
+
const ref = nodes.find(n => (n.role === 'input' || n.role === 'textbox') && /smoke input/i.test(n.name || ''))?.ref
|
|
314
|
+
|| nodes.find(n => n.role === 'input' || n.role === 'textbox')?.ref;
|
|
315
|
+
if (!ref) return { status: 500, body: { ok: false, error: `no input ref on fixture (snapshot returned ${nodes.length} nodes)` }};
|
|
316
|
+
const typed = await command({ type: 'type_ref', ref, text: 'smoke-test-ref' });
|
|
317
|
+
const check = await command({ type: 'evaluate', script: 'document.querySelector("#i")?.value' });
|
|
318
|
+
if (check.body?.result?.result !== 'smoke-test-ref') return { status: 500, body: { ok: false, error: `type_ref did not update fixture input: ${summarise(check)}` }};
|
|
319
|
+
return typed;
|
|
320
|
+
}});
|
|
321
|
+
tests.push({ name: 'browser_press', tier: '3', run: async () => {
|
|
322
|
+
await openBrowserFixture();
|
|
323
|
+
await command({ type: 'click', selector: '#i' });
|
|
324
|
+
const pressed = await command({ type: 'press', key: 'Escape' });
|
|
325
|
+
const check = await command({ type: 'evaluate', script: 'window.lastKey' });
|
|
326
|
+
if (check.body?.result?.result !== 'Escape') return { status: 500, body: { ok: false, error: `press did not update fixture key: ${summarise(check)}` }};
|
|
327
|
+
return pressed;
|
|
328
|
+
}});
|
|
329
|
+
tests.push({ name: 'browser_highlight', tier: '3', run: async () => {
|
|
330
|
+
await openBrowserFixture();
|
|
331
|
+
return command({ type: 'highlight', selector: '#b' });
|
|
332
|
+
}});
|
|
333
|
+
|
|
334
|
+
// ── TIER 4: desktop interact (--tier4) ──
|
|
335
|
+
const D = (name, body) => tests.push({ name, tier: '4', run: () => command(body) });
|
|
336
|
+
D('desktop_click', { type: 'desktop_click', x: 5, y: 5, space: 'desktop' });
|
|
337
|
+
D('desktop_hover', { type: 'desktop_hover', x: 50, y: 50, space: 'desktop' });
|
|
338
|
+
D('desktop_drag', { type: 'desktop_drag', x: 50, y: 50, toX: 60, toY: 60, space: 'desktop' });
|
|
339
|
+
tests.push({ name: 'desktop_click_ref', tier: '4', run: async () => {
|
|
340
|
+
if (!desktopRef) {
|
|
341
|
+
const s = await command({ type: 'desktop_snapshot', scope: 'foreground' });
|
|
342
|
+
desktopRef = s.body?.result?.elements?.[0]?.ref;
|
|
343
|
+
}
|
|
344
|
+
if (!desktopRef) return { status: 500, body: { ok: false, error: 'no desktop ref' }};
|
|
345
|
+
return command({ type: 'desktop_click_ref', ref: desktopRef });
|
|
346
|
+
}});
|
|
347
|
+
tests.push({ name: 'desktop_hover_ref', tier: '4', run: async () => {
|
|
348
|
+
if (!desktopRef) {
|
|
349
|
+
const s = await command({ type: 'desktop_snapshot', scope: 'foreground' });
|
|
350
|
+
desktopRef = s.body?.result?.elements?.[0]?.ref;
|
|
351
|
+
}
|
|
352
|
+
if (!desktopRef) return { status: 500, body: { ok: false, error: 'no desktop ref' }};
|
|
353
|
+
return command({ type: 'desktop_hover_ref', ref: desktopRef });
|
|
354
|
+
}});
|
|
355
|
+
D('desktop_overlay', { type: 'desktop_overlay', show: false });
|
|
356
|
+
D('desktop_pointer_show', { type: 'desktop_pointer_show', x: 200, y: 200, label: 'smoke', space: 'desktop' });
|
|
357
|
+
D('desktop_pointer_move', { type: 'desktop_pointer_move', x: 220, y: 220, space: 'desktop' });
|
|
358
|
+
D('desktop_pointer_pulse', { type: 'desktop_pointer_pulse' });
|
|
359
|
+
D('desktop_pointer_hide', { type: 'desktop_pointer_hide' });
|
|
360
|
+
// focus-dependent tools — only attempt when a focus region exists
|
|
361
|
+
tests.push({ name: 'desktop_click_cell', tier: '4', run: async () => {
|
|
362
|
+
const f = await command({ type: 'desktop_focus_status' });
|
|
363
|
+
if (!f.body?.result?.active) return { status: 200, body: { ok: true, note: 'no agent-focus active (acceptable skip)' }};
|
|
364
|
+
return command({ type: 'desktop_click_cell', col: 2, row: 2 });
|
|
365
|
+
}});
|
|
366
|
+
tests.push({ name: 'desktop_pointer_cell', tier: '4', run: async () => {
|
|
367
|
+
const f = await command({ type: 'desktop_focus_status' });
|
|
368
|
+
if (!f.body?.result?.active) return { status: 200, body: { ok: true, note: 'no agent-focus active (acceptable skip)' }};
|
|
369
|
+
return command({ type: 'desktop_pointer_cell', col: 2, row: 2 });
|
|
370
|
+
}});
|
|
371
|
+
tests.push({ name: 'desktop_focus_grid', tier: '4', run: async () => {
|
|
372
|
+
const f = await command({ type: 'desktop_focus_status' });
|
|
373
|
+
if (!f.body?.result?.active) return { status: 200, body: { ok: true, note: 'no agent-focus active (acceptable skip)' }};
|
|
374
|
+
const on = await command({ type: 'desktop_focus_grid', action: 'show' });
|
|
375
|
+
await sleep(300);
|
|
376
|
+
await command({ type: 'desktop_focus_grid', action: 'hide' });
|
|
377
|
+
return on;
|
|
378
|
+
}});
|
|
379
|
+
// region select + pick point + calibrate are USER-INTERACTIVE — skip in auto tier4
|
|
380
|
+
tests.push({ name: 'desktop_select_region', tier: '4', run: async () => ({ status: 200, body: { ok: true, note: 'user-interactive — skipped in auto smoke (would block on user click)' }})});
|
|
381
|
+
tests.push({ name: 'desktop_release_focus', tier: '4', run: () => command({ type: 'desktop_release_focus' })});
|
|
382
|
+
tests.push({ name: 'desktop_calibrate_pointer', tier: '4', run: async () => ({ status: 200, body: { ok: true, note: 'user-interactive — skipped in auto smoke' }})});
|
|
383
|
+
tests.push({ name: 'desktop_pick_point', tier: '4', run: async () => ({ status: 200, body: { ok: true, note: 'user-interactive — skipped in auto smoke' }})});
|
|
384
|
+
|
|
385
|
+
// ── TIER 5: recordings ──
|
|
386
|
+
tests.push({ name: 'browser_recordings', tier: '5', run: async () => {
|
|
387
|
+
const r = await fetch(`${BRIDGE}/api/recordings`);
|
|
388
|
+
return { status: r.status, body: { ok: r.ok, result: { recordings: await r.json() }}};
|
|
389
|
+
}});
|
|
390
|
+
tests.push({ name: 'browser_record_start', tier: '5', run: () => command({ type: 'record_start', name: `smoke-${STAMP}` }) });
|
|
391
|
+
tests.push({ name: 'browser_record_stop', tier: '5', run: async () => { await sleep(300); return command({ type: 'record_stop' }); } });
|
|
392
|
+
tests.push({ name: 'browser_play', tier: '5', run: async () => {
|
|
393
|
+
const r = await fetch(`${BRIDGE}/api/recordings`);
|
|
394
|
+
const list = await r.json();
|
|
395
|
+
const first = list?.[0]?.name;
|
|
396
|
+
if (!first) return { status: 200, body: { ok: true, note: 'no saved recordings — skip' }};
|
|
397
|
+
return { status: 200, body: { ok: true, note: `would play "${first}" (skipped to avoid state changes)` }};
|
|
398
|
+
}});
|
|
399
|
+
tests.push({ name: 'browser_chat', tier: '5', run: () => command({ type: 'chat', message: 'smoke-test' }) });
|
|
400
|
+
tests.push({ name: 'browser_read_chat', tier: '5', run: async () => {
|
|
401
|
+
const r = await fetch(`${BRIDGE}/api/chat?limit=5`);
|
|
402
|
+
return { status: r.status, body: { ok: r.ok, result: { messages: await r.json() }}};
|
|
403
|
+
}});
|
|
404
|
+
|
|
405
|
+
// ── TIER 6: eval (--eval) ──
|
|
406
|
+
tests.push({ name: 'browser_evaluate', tier: 'eval', run: async () => {
|
|
407
|
+
await openBrowserFixture();
|
|
408
|
+
const r = await command({ type: 'evaluate', script: '1+1' });
|
|
409
|
+
if (r.body?.result?.result !== 2) return { status: 500, body: { ok: false, error: `evaluate returned unexpected result: ${summarise(r)}` }};
|
|
410
|
+
return r;
|
|
411
|
+
}});
|
|
412
|
+
|
|
413
|
+
// ────────────────── RUN ──────────────────
|
|
414
|
+
console.log(`[smoke-all] bridge=${BRIDGE} out=${OUT_DIR}`);
|
|
415
|
+
console.log(`[smoke-all] tier4=${RUN_TIER4} eval=${RUN_EVAL} total=${tests.length}`);
|
|
416
|
+
|
|
417
|
+
const enableNeeded = [
|
|
418
|
+
'browser_click','browser_click_ref','browser_click_xy','browser_type','browser_type_ref',
|
|
419
|
+
'browser_press','browser_highlight','browser_evaluate',
|
|
420
|
+
'desktop_click','desktop_hover','desktop_drag','desktop_click_ref','desktop_hover_ref',
|
|
421
|
+
'desktop_overlay','desktop_select_region','desktop_release_focus','desktop_pointer_show',
|
|
422
|
+
'desktop_pointer_move','desktop_pointer_pulse','desktop_pointer_hide','desktop_calibrate_pointer',
|
|
423
|
+
'desktop_click_cell','desktop_pointer_cell','desktop_focus_grid','desktop_pick_point',
|
|
424
|
+
'browser_record_start','browser_record_stop','browser_play','browser_recordings',
|
|
425
|
+
'browser_chat','browser_read_chat',
|
|
426
|
+
];
|
|
427
|
+
// Snapshot current settings, force-enable per-tool toggles AND global read/write/execute,
|
|
428
|
+
// restore at end. globalSafety lives under bridge.globalSafety, enabledTools under chat.
|
|
429
|
+
const stateRes = await fetch(`${BRIDGE}/api/settings/state`).then(r => r.json()).catch(() => ({}));
|
|
430
|
+
const enabledBefore = stateRes?.chat?.enabledTools || {};
|
|
431
|
+
const safetyBefore = stateRes?.bridge?.globalSafety || { read: true, write: true, execute: true };
|
|
432
|
+
const overrides = {};
|
|
433
|
+
for (const n of enableNeeded) overrides[n] = true;
|
|
434
|
+
await fetch(`${BRIDGE}/api/settings/state`, {
|
|
435
|
+
method: 'POST',
|
|
436
|
+
headers: { 'Content-Type': 'application/json' },
|
|
437
|
+
body: JSON.stringify({
|
|
438
|
+
chat: { enabledTools: { ...enabledBefore, ...overrides }},
|
|
439
|
+
bridge: { globalSafety: { read: true, write: true, execute: true }},
|
|
440
|
+
}),
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
for (const t of tests) {
|
|
445
|
+
if (ONLY && !ONLY.includes(t.tier)) continue;
|
|
446
|
+
process.stdout.write(`[${t.tier}] ${t.name} ... `);
|
|
447
|
+
await runTest(t);
|
|
448
|
+
const last = results[results.length - 1];
|
|
449
|
+
console.log(last.status === 'PASS' ? `\u2713 ${last.ms}ms` : last.status === 'SKIP' ? 'skip' : `\u2717 ${last.error}`);
|
|
450
|
+
}
|
|
451
|
+
} finally {
|
|
452
|
+
// Restore enabled state even when an individual smoke crashes or times out.
|
|
453
|
+
await fetch(`${BRIDGE}/api/settings/state`, {
|
|
454
|
+
method: 'POST',
|
|
455
|
+
headers: { 'Content-Type': 'application/json' },
|
|
456
|
+
body: JSON.stringify({
|
|
457
|
+
chat: { enabledTools: enabledBefore },
|
|
458
|
+
bridge: { globalSafety: safetyBefore },
|
|
459
|
+
}),
|
|
460
|
+
}).catch(() => {});
|
|
461
|
+
if (browserFixtureServer) {
|
|
462
|
+
await new Promise(resolve => browserFixtureServer.close(resolve)).catch(() => {});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ────────────────── REPORT ──────────────────
|
|
467
|
+
const pass = results.filter(r => r.status === 'PASS').length;
|
|
468
|
+
const fail = results.filter(r => r.status === 'FAIL').length;
|
|
469
|
+
const skip = results.filter(r => r.status === 'SKIP').length;
|
|
470
|
+
|
|
471
|
+
const lines = [];
|
|
472
|
+
lines.push(`# Bridge full-tool smoke — v${JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version}`);
|
|
473
|
+
lines.push('');
|
|
474
|
+
lines.push(`Generated: ${new Date().toISOString()} · Bridge: ${BRIDGE}`);
|
|
475
|
+
lines.push(`Total ${results.length} · PASS ${pass} · FAIL ${fail} · SKIP ${skip}`);
|
|
476
|
+
lines.push('');
|
|
477
|
+
const tiers = [['1', 'Read / status'], ['2', 'Browser navigate'], ['3', 'Browser interact'], ['4', 'Desktop interact'], ['5', 'Recordings'], ['eval', 'Eval (JS)']];
|
|
478
|
+
for (const [t, label] of tiers) {
|
|
479
|
+
const sub = results.filter(r => r.tier === t);
|
|
480
|
+
if (!sub.length) continue;
|
|
481
|
+
lines.push(`## Tier ${t} — ${label}`);
|
|
482
|
+
lines.push('');
|
|
483
|
+
lines.push('| Tool | Result | Latency | Notes |');
|
|
484
|
+
lines.push('|---|---|---|---|');
|
|
485
|
+
for (const r of sub) {
|
|
486
|
+
const icon = r.status === 'PASS' ? '✓' : r.status === 'FAIL' ? '✗' : '–';
|
|
487
|
+
const ms = r.ms != null ? `${r.ms}ms` : '';
|
|
488
|
+
const note = r.error ? `error: ${r.error}` : (r.summary ? r.summary.slice(0, 180) : '');
|
|
489
|
+
lines.push(`| \`${r.name}\` | ${icon} ${r.status} | ${ms} | ${note.replace(/\|/g, '\\|')} |`);
|
|
490
|
+
}
|
|
491
|
+
lines.push('');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const allShots = results.flatMap(r => (r.shots || []).map(s => `- \`${r.name}\` → [${basename(s)}](shots/${basename(s)})`));
|
|
495
|
+
if (allShots.length) {
|
|
496
|
+
lines.push('## Captured screenshots');
|
|
497
|
+
lines.push('');
|
|
498
|
+
lines.push(...allShots);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const reportPath = join(OUT_DIR, 'report.md');
|
|
502
|
+
writeFileSync(reportPath, lines.join('\n'));
|
|
503
|
+
writeFileSync(join(OUT_DIR, 'report.json'), JSON.stringify(results, null, 2));
|
|
504
|
+
|
|
505
|
+
console.log('');
|
|
506
|
+
console.log(`[smoke-all] PASS ${pass} FAIL ${fail} SKIP ${skip}`);
|
|
507
|
+
console.log(`[smoke-all] report: ${reportPath}`);
|
|
508
|
+
|
|
509
|
+
process.exit(fail > 0 ? 1 : 0);
|