@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,696 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
3
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { dirname, join, resolve } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
|
+
const EXPECTED_VERSION = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version;
|
|
12
|
+
const BRIDGE = process.env.BRIDGE_SMOKE_URL || 'http://127.0.0.1:3006';
|
|
13
|
+
const INSTALLER = process.env.BRIDGE_SMOKE_INSTALLER || join(ROOT, 'build', 'dist', 'Empir3Setup.exe');
|
|
14
|
+
const STAMP = new Date().toISOString().replace(/[:.]/g, '-');
|
|
15
|
+
const OUT_DIR = resolve(process.env.BRIDGE_SMOKE_OUT || join(ROOT, 'build', `live-smoke-${STAMP}`));
|
|
16
|
+
const REPORT_PATH = join(OUT_DIR, 'report.json');
|
|
17
|
+
const SUMMARY_PATH = join(OUT_DIR, 'summary.md');
|
|
18
|
+
|
|
19
|
+
mkdirSync(OUT_DIR, { recursive: true });
|
|
20
|
+
|
|
21
|
+
const report = {
|
|
22
|
+
startedAt: new Date().toISOString(),
|
|
23
|
+
bridge: BRIDGE,
|
|
24
|
+
installer: INSTALLER,
|
|
25
|
+
outDir: OUT_DIR,
|
|
26
|
+
checks: [],
|
|
27
|
+
artifacts: {},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const wait = (ms) => new Promise((resolveWait) => setTimeout(resolveWait, ms));
|
|
31
|
+
|
|
32
|
+
function log(message) {
|
|
33
|
+
console.log(`[live-smoke] ${message}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function compact(value, depth = 0) {
|
|
37
|
+
if (value == null) return value;
|
|
38
|
+
if (typeof value === 'string') {
|
|
39
|
+
if (/^[A-Za-z0-9+/=]{1200,}$/.test(value)) return `[base64 ${value.length} chars]`;
|
|
40
|
+
return value.length > 900 ? `${value.slice(0, 900)}... (${value.length} chars)` : value;
|
|
41
|
+
}
|
|
42
|
+
if (typeof value !== 'object') return value;
|
|
43
|
+
if (Buffer.isBuffer(value)) return `[buffer ${value.length} bytes]`;
|
|
44
|
+
if (Array.isArray(value)) {
|
|
45
|
+
const first = value.slice(0, 8).map((item) => compact(item, depth + 1));
|
|
46
|
+
if (value.length > 8) first.push(`... ${value.length - 8} more`);
|
|
47
|
+
return first;
|
|
48
|
+
}
|
|
49
|
+
if (depth > 5) return '[object]';
|
|
50
|
+
const out = {};
|
|
51
|
+
for (const [key, item] of Object.entries(value)) {
|
|
52
|
+
if (/data|base64|thumbnail|screenshot/i.test(key) && typeof item === 'string') {
|
|
53
|
+
out[key] = `[base64 ${item.length} chars]`;
|
|
54
|
+
} else {
|
|
55
|
+
out[key] = compact(item, depth + 1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function withTimeout(promise, label, timeoutMs = 45000) {
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const timer = setTimeout(() => controller.abort(`${label} timed out after ${timeoutMs}ms`), timeoutMs);
|
|
64
|
+
try {
|
|
65
|
+
return await promise(controller.signal);
|
|
66
|
+
} finally {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function requestJson(method, url, body, timeoutMs = 45000) {
|
|
72
|
+
return withTimeout(async (signal) => {
|
|
73
|
+
const res = await fetch(url, {
|
|
74
|
+
method,
|
|
75
|
+
headers: body === undefined ? {} : { 'Content-Type': 'application/json' },
|
|
76
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
77
|
+
signal,
|
|
78
|
+
});
|
|
79
|
+
const text = await res.text();
|
|
80
|
+
let parsed = null;
|
|
81
|
+
try { parsed = JSON.parse(text); } catch {}
|
|
82
|
+
if (!res.ok) throw new Error(`${method} ${url} failed ${res.status}: ${text.slice(0, 500)}`);
|
|
83
|
+
return parsed ?? text;
|
|
84
|
+
}, `${method} ${url}`, timeoutMs);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function getJson(path, timeoutMs) {
|
|
88
|
+
return requestJson('GET', `${BRIDGE}${path}`, undefined, timeoutMs);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function postJson(path, body, timeoutMs) {
|
|
92
|
+
return requestJson('POST', `${BRIDGE}${path}`, body, timeoutMs);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function command(cmd, timeoutMs = 45000) {
|
|
96
|
+
const body = await postJson('/api/command', cmd, timeoutMs);
|
|
97
|
+
if (!body?.ok) throw new Error(body?.error || `Command failed: ${JSON.stringify(body)}`);
|
|
98
|
+
const result = body.result;
|
|
99
|
+
if (result?.success === false) throw new Error(result.error || result.stderr || `Command returned success=false: ${JSON.stringify(compact(result))}`);
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function commandAllowFailure(cmd, timeoutMs = 45000) {
|
|
104
|
+
const body = await postJson('/api/command', cmd, timeoutMs);
|
|
105
|
+
return body;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function step(name, fn, options = {}) {
|
|
109
|
+
const started = Date.now();
|
|
110
|
+
log(`${name} ...`);
|
|
111
|
+
try {
|
|
112
|
+
const detail = await fn();
|
|
113
|
+
const check = { name, ok: true, elapsedMs: Date.now() - started, detail: compact(detail) };
|
|
114
|
+
report.checks.push(check);
|
|
115
|
+
log(`PASS ${name}`);
|
|
116
|
+
writeReport();
|
|
117
|
+
return detail;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
const check = { name, ok: false, elapsedMs: Date.now() - started, error: error?.stack || error?.message || String(error) };
|
|
120
|
+
report.checks.push(check);
|
|
121
|
+
log(`FAIL ${name}: ${error?.message || error}`);
|
|
122
|
+
writeReport();
|
|
123
|
+
if (options.fatal) throw error;
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function writeReport() {
|
|
129
|
+
report.updatedAt = new Date().toISOString();
|
|
130
|
+
report.ok = report.checks.every((check) => check.ok);
|
|
131
|
+
writeFileSync(REPORT_PATH, JSON.stringify(report, null, 2));
|
|
132
|
+
const passed = report.checks.filter((check) => check.ok).length;
|
|
133
|
+
const failed = report.checks.filter((check) => !check.ok);
|
|
134
|
+
const lines = [
|
|
135
|
+
'# Empir3 Bridge Live Smoke',
|
|
136
|
+
'',
|
|
137
|
+
`- Started: ${report.startedAt}`,
|
|
138
|
+
`- Updated: ${report.updatedAt}`,
|
|
139
|
+
`- Bridge: ${BRIDGE}`,
|
|
140
|
+
`- Checks: ${passed}/${report.checks.length} passed`,
|
|
141
|
+
'',
|
|
142
|
+
];
|
|
143
|
+
if (failed.length) {
|
|
144
|
+
lines.push('## Failures', '');
|
|
145
|
+
for (const check of failed) lines.push(`- ${check.name}: ${String(check.error || '').split('\n')[0]}`);
|
|
146
|
+
lines.push('');
|
|
147
|
+
}
|
|
148
|
+
lines.push('## Checks', '');
|
|
149
|
+
for (const check of report.checks) {
|
|
150
|
+
lines.push(`- ${check.ok ? 'PASS' : 'FAIL'} ${check.name} (${check.elapsedMs}ms)`);
|
|
151
|
+
}
|
|
152
|
+
writeFileSync(SUMMARY_PATH, `${lines.join('\n')}\n`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function ps(command, timeoutMs = 30000) {
|
|
156
|
+
return execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', command], {
|
|
157
|
+
encoding: 'utf8',
|
|
158
|
+
timeout: timeoutMs,
|
|
159
|
+
windowsHide: true,
|
|
160
|
+
}).trim();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function listenerProvenance() {
|
|
164
|
+
try {
|
|
165
|
+
const json = ps(`
|
|
166
|
+
$ports = 3006,9867
|
|
167
|
+
$listeners = Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Where-Object { $_.LocalPort -in $ports } | Select-Object LocalAddress,LocalPort,OwningProcess
|
|
168
|
+
$procs = foreach ($p in ($listeners | Select-Object -ExpandProperty OwningProcess -Unique)) {
|
|
169
|
+
Get-Process -Id $p -ErrorAction SilentlyContinue | Select-Object Id,ProcessName,Path
|
|
170
|
+
}
|
|
171
|
+
[pscustomobject]@{ listeners=$listeners; processes=$procs } | ConvertTo-Json -Depth 6
|
|
172
|
+
`, 15000);
|
|
173
|
+
return JSON.parse(json || '{}');
|
|
174
|
+
} catch (error) {
|
|
175
|
+
return { error: error?.message || String(error) };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function liveTestHtml(label = 'direct') {
|
|
180
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>Empir3 Bridge Live Smoke</title>
|
|
181
|
+
<style>
|
|
182
|
+
body{margin:0;font-family:Segoe UI,Arial,sans-serif;background:#09111f;color:#e6edf7;min-height:2200px}
|
|
183
|
+
header{position:sticky;top:0;z-index:10;background:#111d31;border-bottom:1px solid #294263;padding:16px 22px}
|
|
184
|
+
h1{font-size:22px;margin:0 0 4px}.sub{color:#9db0ca;font-size:13px}
|
|
185
|
+
main{padding:24px;display:grid;grid-template-columns:minmax(340px,640px) 1fr;gap:20px}
|
|
186
|
+
.panel{border:1px solid #294263;background:#111a2b;padding:16px;border-radius:8px}
|
|
187
|
+
button,input,textarea{font:16px Segoe UI,Arial,sans-serif;border-radius:6px;border:1px solid #3d5778;padding:10px 12px}
|
|
188
|
+
button{background:#e8f1ff;color:#08111f;font-weight:700;cursor:pointer;margin:4px}
|
|
189
|
+
input,textarea{background:#07101d;color:#e6edf7;display:block;margin:8px 0;width:90%;max-width:480px}
|
|
190
|
+
#log{white-space:pre-wrap;min-height:260px;max-height:600px;overflow:auto;background:#07101d;border:1px solid #294263;border-radius:8px;padding:12px}
|
|
191
|
+
.target{width:240px;height:110px;border:2px solid #48d597;display:grid;place-items:center;margin:18px 0;border-radius:8px;background:rgba(72,213,151,.1)}
|
|
192
|
+
.spacer{height:900px}
|
|
193
|
+
</style></head><body>
|
|
194
|
+
<header><h1>Empir3 Bridge Live Smoke - ${label}</h1><div class="sub">You should see clicks, typing, highlights, scrolling, chat overlay, and recording/playback happen here.</div></header>
|
|
195
|
+
<main>
|
|
196
|
+
<section class="panel">
|
|
197
|
+
<button id="selectorBtn">Selector button</button>
|
|
198
|
+
<button id="refBtn">Ref button</button>
|
|
199
|
+
<button id="xyBtn">XY target</button>
|
|
200
|
+
<button id="recordBtn">Record target</button>
|
|
201
|
+
<input id="selectorInput" aria-label="Selector Input" placeholder="selector input">
|
|
202
|
+
<input id="refInput" aria-label="Ref Input" placeholder="ref input">
|
|
203
|
+
<input id="mcpInput" aria-label="MCP Input" placeholder="mcp input">
|
|
204
|
+
<textarea id="notes" aria-label="Notes" placeholder="notes"></textarea>
|
|
205
|
+
<div class="target" id="highlightMe">highlight target</div>
|
|
206
|
+
<div class="target" id="dragTarget">browser visible target</div>
|
|
207
|
+
</section>
|
|
208
|
+
<section class="panel"><h2>Live event log</h2><div id="log">ready</div></section>
|
|
209
|
+
</main>
|
|
210
|
+
<div class="spacer"></div>
|
|
211
|
+
<script>
|
|
212
|
+
window.smokeState={selectorClicks:0,refClicks:0,xyClicks:0,recordClicks:0,plays:0,typed:[]};
|
|
213
|
+
function line(msg){const log=document.getElementById('log');log.textContent+='\\n'+new Date().toLocaleTimeString()+' '+msg;log.scrollTop=log.scrollHeight;}
|
|
214
|
+
function wire(id,key){document.getElementById(id).addEventListener('click',e=>{window.smokeState[key]++;line(id+' clicked trusted='+e.isTrusted+' count='+window.smokeState[key]);});}
|
|
215
|
+
wire('selectorBtn','selectorClicks');wire('refBtn','refClicks');wire('xyBtn','xyClicks');wire('recordBtn','recordClicks');
|
|
216
|
+
for (const id of ['selectorInput','refInput','mcpInput','notes']) document.getElementById(id).addEventListener('input',e=>{window.smokeState.typed.push(id+':'+e.target.value);line(id+' input='+e.target.value);});
|
|
217
|
+
window.__smokeSummary=()=>({state:window.smokeState,scrollY:window.scrollY,title:document.title,url:location.href});
|
|
218
|
+
</script></body></html>`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function dataUrl(html) {
|
|
222
|
+
return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function flattenSnapshot(snapshot) {
|
|
226
|
+
if (!snapshot) return [];
|
|
227
|
+
if (Array.isArray(snapshot)) return snapshot;
|
|
228
|
+
if (Array.isArray(snapshot.elements)) return snapshot.elements;
|
|
229
|
+
if (Array.isArray(snapshot.nodes)) return snapshot.nodes;
|
|
230
|
+
if (Array.isArray(snapshot.tree)) return snapshot.tree;
|
|
231
|
+
if (typeof snapshot === 'object') {
|
|
232
|
+
const found = [];
|
|
233
|
+
const walk = (node) => {
|
|
234
|
+
if (!node || typeof node !== 'object') return;
|
|
235
|
+
if (node.ref || node.role || node.name) found.push(node);
|
|
236
|
+
for (const key of ['children', 'nodes', 'items']) {
|
|
237
|
+
if (Array.isArray(node[key])) node[key].forEach(walk);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
walk(snapshot);
|
|
241
|
+
return found;
|
|
242
|
+
}
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function assert(condition, message) {
|
|
247
|
+
if (!condition) throw new Error(message);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function getBrowserSummary() {
|
|
251
|
+
const result = await command({ type: 'evaluate', script: 'window.__smokeSummary && window.__smokeSummary()' });
|
|
252
|
+
return typeof result?.result === 'string' ? JSON.parse(result.result) : result?.result;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function browserDirectSmoke() {
|
|
256
|
+
const url = dataUrl(liveTestHtml('direct api'));
|
|
257
|
+
await step('browser show controlled window', async () => command({ type: 'desktop:browse:show', params: { url } }));
|
|
258
|
+
await wait(1200);
|
|
259
|
+
await step('browser status', async () => command({ type: 'status' }));
|
|
260
|
+
await step('browser navigate', async () => command({ type: 'navigate', url }));
|
|
261
|
+
await wait(1000);
|
|
262
|
+
await step('browser snapshot direct', async () => {
|
|
263
|
+
const result = await command({ type: 'snapshot', filter: 'all', format: 'json' });
|
|
264
|
+
const elements = flattenSnapshot(result.snapshot);
|
|
265
|
+
assert(elements.length > 0, 'snapshot returned no elements');
|
|
266
|
+
return { count: elements.length, first: elements.slice(0, 6) };
|
|
267
|
+
});
|
|
268
|
+
await step('browser text direct', async () => {
|
|
269
|
+
const result = await command({ type: 'text' });
|
|
270
|
+
assert(/Empir3 Bridge Live Smoke/.test(result.text || ''), 'text did not include live smoke title');
|
|
271
|
+
return { length: result.text.length };
|
|
272
|
+
});
|
|
273
|
+
await step('browser type selector direct', async () => command({ type: 'type', selector: '#selectorInput', text: 'selector direct typed' }));
|
|
274
|
+
await step('browser click selector direct', async () => command({ type: 'click', selector: '#selectorBtn' }));
|
|
275
|
+
await step('browser press direct', async () => command({ type: 'press', text: 'Tab' }));
|
|
276
|
+
await step('browser scroll direct', async () => command({ type: 'scroll', y: 420, x: 0 }));
|
|
277
|
+
await step('browser cursor move direct', async () => command({ type: 'cursor_move', x: 180, y: 180 }));
|
|
278
|
+
await step('browser click xy direct', async () => {
|
|
279
|
+
await command({ type: 'evaluate', script: 'window.scrollTo(0,0); true' });
|
|
280
|
+
await wait(250);
|
|
281
|
+
const pos = await command({
|
|
282
|
+
type: 'evaluate',
|
|
283
|
+
script: `(() => { const r = document.querySelector('#xyBtn').getBoundingClientRect(); return { x: Math.round(r.left + r.width/2), y: Math.round(r.top + r.height/2) }; })()`,
|
|
284
|
+
});
|
|
285
|
+
const point = typeof pos.result === 'string' ? JSON.parse(pos.result) : pos.result;
|
|
286
|
+
const clicked = await command({ type: 'click_xy', x: point.x, y: point.y });
|
|
287
|
+
await wait(300);
|
|
288
|
+
const summary = await getBrowserSummary();
|
|
289
|
+
assert(summary.state.xyClicks >= 1, `xy click did not register: ${JSON.stringify(summary)}`);
|
|
290
|
+
return { point, clicked, xyClicks: summary.state.xyClicks };
|
|
291
|
+
});
|
|
292
|
+
await step('browser evaluate direct', async () => {
|
|
293
|
+
const summary = await getBrowserSummary();
|
|
294
|
+
assert(summary.state.selectorClicks >= 1, 'selector click did not register');
|
|
295
|
+
assert(summary.state.typed.some((item) => item.includes('selector direct typed')), 'selector type did not register');
|
|
296
|
+
return summary;
|
|
297
|
+
});
|
|
298
|
+
await step('browser highlight direct', async () => command({ type: 'highlight', selector: '#highlightMe' }));
|
|
299
|
+
await step('browser screenshot direct', async () => {
|
|
300
|
+
const result = await command({ type: 'screenshot' });
|
|
301
|
+
assert(result.path || result.screenshot, 'browser screenshot did not return a path/name');
|
|
302
|
+
return result;
|
|
303
|
+
});
|
|
304
|
+
await step('browser refresh direct', async () => command({ type: 'refresh' }));
|
|
305
|
+
await wait(1200);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function browserAliasSmoke() {
|
|
309
|
+
const url = dataUrl(liveTestHtml('desktop:browse aliases'));
|
|
310
|
+
await step('agent-browser show alias', async () => command({ type: 'desktop:agent-browser:show', params: { url } }));
|
|
311
|
+
await wait(1000);
|
|
312
|
+
await step('browse status alias', async () => command({ type: 'desktop:browse:status' }));
|
|
313
|
+
await step('browse navigate alias', async () => command({ type: 'desktop:browse:navigate', url }));
|
|
314
|
+
await wait(900);
|
|
315
|
+
await step('browse type selector alias', async () => command({ type: 'desktop:browse:type_selector', selector: '#selectorInput', text: 'alias selector typed' }));
|
|
316
|
+
await step('browse click selector alias', async () => command({ type: 'desktop:browse:click_selector', selector: '#selectorBtn' }));
|
|
317
|
+
await step('browse selector verify alias', async () => {
|
|
318
|
+
const result = await command({ type: 'desktop:browse:evaluate', script: 'window.__smokeSummary()' });
|
|
319
|
+
const summary = typeof result.result === 'string' ? JSON.parse(result.result) : result.result;
|
|
320
|
+
assert(summary.state.selectorClicks >= 1, 'alias selector click did not register');
|
|
321
|
+
assert(summary.state.typed.some((item) => item.includes('alias selector typed')), 'alias selector type did not register');
|
|
322
|
+
return summary;
|
|
323
|
+
});
|
|
324
|
+
const refs = await step('browse snapshot refs alias', async () => {
|
|
325
|
+
const result = await command({ type: 'desktop:browse:snapshot', filter: 'all', format: 'json' });
|
|
326
|
+
const elements = flattenSnapshot(result.snapshot);
|
|
327
|
+
const exact = await command({
|
|
328
|
+
type: 'desktop:browse:evaluate',
|
|
329
|
+
script: `(() => ({
|
|
330
|
+
inputRef: document.querySelector('#refInput')?.getAttribute('data-empir3-ref'),
|
|
331
|
+
buttonRef: document.querySelector('#refBtn')?.getAttribute('data-empir3-ref')
|
|
332
|
+
}))()`,
|
|
333
|
+
});
|
|
334
|
+
const refs = typeof exact.result === 'string' ? JSON.parse(exact.result) : exact.result;
|
|
335
|
+
assert(refs?.inputRef && refs?.buttonRef, `missing exact refs after snapshot: ${JSON.stringify(compact({ refs, elements: elements.slice(0, 12) }))}`);
|
|
336
|
+
return { inputRef: refs.inputRef, buttonRef: refs.buttonRef, count: elements.length };
|
|
337
|
+
});
|
|
338
|
+
if (refs) {
|
|
339
|
+
await step('browse type ref alias', async () => command({ type: 'desktop:browse:type_ref', ref: refs.inputRef, text: 'alias ref typed' }));
|
|
340
|
+
await step('browse click ref alias', async () => command({ type: 'desktop:browse:click_ref', ref: refs.buttonRef }));
|
|
341
|
+
await step('browse ref verify alias', async () => {
|
|
342
|
+
const result = await command({ type: 'desktop:browse:evaluate', script: 'window.__smokeSummary()' });
|
|
343
|
+
const summary = typeof result.result === 'string' ? JSON.parse(result.result) : result.result;
|
|
344
|
+
assert(summary.state.refClicks >= 1, 'alias ref click did not register');
|
|
345
|
+
assert(summary.state.typed.some((item) => item.includes('alias ref typed')), 'alias ref type did not register');
|
|
346
|
+
return summary;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
await step('browse screenshot alias bytes', async () => {
|
|
350
|
+
const result = await command({ type: 'desktop:browse:screenshot', quality: 65 });
|
|
351
|
+
assert(result.base64 && result.mimeType === 'image/jpeg' && result.bytes > 1000, `bad screenshot shape: ${JSON.stringify(compact(result))}`);
|
|
352
|
+
return { mimeType: result.mimeType, bytes: result.bytes };
|
|
353
|
+
});
|
|
354
|
+
await step('browse text alias', async () => command({ type: 'desktop:browse:text' }));
|
|
355
|
+
await step('browse press alias', async () => command({ type: 'desktop:browse:press', key: 'Tab' }));
|
|
356
|
+
await step('browse scroll alias', async () => command({ type: 'desktop:browse:scroll', amount: 300 }));
|
|
357
|
+
await step('browse highlight alias', async () => command({ type: 'desktop:browse:highlight', selector: '#highlightMe' }));
|
|
358
|
+
await step('browse chat alias', async () => command({ type: 'desktop:browse:chat', message: `Visible live smoke chat ${STAMP}` }));
|
|
359
|
+
await step('browse read_chat alias', async () => command({ type: 'desktop:browse:read_chat' }));
|
|
360
|
+
|
|
361
|
+
let recordingName = '';
|
|
362
|
+
await step('browse record start alias', async () => command({ type: 'desktop:browse:record_start', name: `live-smoke-${STAMP}` }));
|
|
363
|
+
await step('browse record action click', async () => command({ type: 'desktop:browse:click_selector', selector: '#recordBtn' }));
|
|
364
|
+
await wait(700);
|
|
365
|
+
await step('browse record stop alias', async () => {
|
|
366
|
+
const result = await command({ type: 'desktop:browse:record_stop', name: `live-smoke-${STAMP}` });
|
|
367
|
+
recordingName = result.saved || `live-smoke-${STAMP}.json`;
|
|
368
|
+
assert(result.actionCount >= 1, `recording captured no actions: ${JSON.stringify(result)}`);
|
|
369
|
+
return result;
|
|
370
|
+
});
|
|
371
|
+
await step('browse recordings alias', async () => command({ type: 'desktop:browse:recordings' }));
|
|
372
|
+
if (recordingName) {
|
|
373
|
+
await step('browse play alias', async () => command({ type: 'desktop:browse:play', recording: recordingName, speed: 2 }));
|
|
374
|
+
await step('browser delete recording direct', async () => command({ type: 'delete_recording', recording: recordingName }));
|
|
375
|
+
}
|
|
376
|
+
await step('agent-browser close alias', async () => command({ type: 'desktop:agent-browser:close' }));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function companionSmoke() {
|
|
380
|
+
await step('capabilities quick', async () => command({ type: 'desktop:capabilities:quick' }, 60000));
|
|
381
|
+
await step('capabilities full scan', async () => command({ type: 'desktop:capabilities:scan' }, 90000));
|
|
382
|
+
await step('capabilities check_cli node', async () => {
|
|
383
|
+
const result = await command({ type: 'desktop:capabilities:check_cli', name: 'node' });
|
|
384
|
+
assert(result?.name === 'node' || result?.success, 'check_cli node did not return success');
|
|
385
|
+
return result;
|
|
386
|
+
});
|
|
387
|
+
for (const query of ['overview', 'processes', 'disk', 'network', 'battery', 'installed']) {
|
|
388
|
+
await step(`sysinfo ${query}`, async () => command({ type: `desktop:sysinfo:${query}` }, query === 'installed' ? 90000 : 45000));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
let originalClipboard = null;
|
|
392
|
+
await step('clipboard read backup', async () => {
|
|
393
|
+
const result = await command({ type: 'desktop:clipboard:read' });
|
|
394
|
+
originalClipboard = result.text || '';
|
|
395
|
+
return { length: result.length || 0 };
|
|
396
|
+
});
|
|
397
|
+
try {
|
|
398
|
+
await step('clipboard write', async () => command({ type: 'desktop:clipboard:write', text: `Empir3 live smoke ${STAMP}` }));
|
|
399
|
+
await step('clipboard read verify', async () => {
|
|
400
|
+
const result = await command({ type: 'desktop:clipboard:read' });
|
|
401
|
+
assert((result.text || '').includes('Empir3 live smoke'), 'clipboard did not contain smoke text');
|
|
402
|
+
return { length: result.length };
|
|
403
|
+
});
|
|
404
|
+
await step('clipboard clear', async () => command({ type: 'desktop:clipboard:clear' }));
|
|
405
|
+
} finally {
|
|
406
|
+
await step('clipboard restore', async () => {
|
|
407
|
+
if (originalClipboard) return command({ type: 'desktop:clipboard:write', text: originalClipboard });
|
|
408
|
+
return command({ type: 'desktop:clipboard:clear' });
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
await step('execute powershell', async () => {
|
|
413
|
+
const result = await command({ type: 'desktop:execute:run', command: 'Write-Output "empir3-powershell-smoke"', shell: 'powershell', timeout: 10 });
|
|
414
|
+
assert(/empir3-powershell-smoke/.test(result.stdout || ''), 'powershell stdout mismatch');
|
|
415
|
+
return result;
|
|
416
|
+
});
|
|
417
|
+
await step('execute cmd', async () => {
|
|
418
|
+
const result = await command({ type: 'desktop:execute:run', command: 'echo empir3-cmd-smoke', shell: 'cmd', timeout: 10 });
|
|
419
|
+
assert(/empir3-cmd-smoke/.test(result.stdout || ''), 'cmd stdout mismatch');
|
|
420
|
+
return result;
|
|
421
|
+
});
|
|
422
|
+
await step('execute timeout handling', async () => {
|
|
423
|
+
const result = await commandAllowFailure({ type: 'desktop:execute:run', command: 'Start-Sleep -Seconds 3', shell: 'powershell', timeout: 1 }, 10000);
|
|
424
|
+
const body = result?.result || result;
|
|
425
|
+
assert(result.ok && body.timedOut === true && body.exitCode === -2, `timeout did not report correctly: ${JSON.stringify(compact(result))}`);
|
|
426
|
+
return body;
|
|
427
|
+
});
|
|
428
|
+
await step('execute destructive blocklist', async () => {
|
|
429
|
+
const result = await commandAllowFailure({ type: 'desktop:execute:run', command: 'Remove-Item -Recurse -Force C:\\', shell: 'powershell', timeout: 5 }, 10000);
|
|
430
|
+
const body = result?.result || result;
|
|
431
|
+
assert(result.ok && body.blocked === true, `destructive command was not blocked: ${JSON.stringify(compact(result))}`);
|
|
432
|
+
return body;
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
await step('notify toast visible', async () => command({ type: 'desktop:notify:show', title: 'Empir3 Bridge Smoke', message: `Visible toast from live smoke ${STAMP}` }));
|
|
436
|
+
|
|
437
|
+
const fileText = `Empir3 live smoke file ${STAMP}`;
|
|
438
|
+
let pushedPath = '';
|
|
439
|
+
await step('file push', async () => {
|
|
440
|
+
const result = await command({
|
|
441
|
+
type: 'desktop:file',
|
|
442
|
+
action: 'push',
|
|
443
|
+
filename: 'empir3-live-smoke.txt',
|
|
444
|
+
subfolder: 'live-smoke',
|
|
445
|
+
data: Buffer.from(fileText, 'utf8').toString('base64'),
|
|
446
|
+
});
|
|
447
|
+
pushedPath = result.savedPath;
|
|
448
|
+
return result;
|
|
449
|
+
});
|
|
450
|
+
await step('file pull', async () => {
|
|
451
|
+
const result = await command({ type: 'desktop:file:pull', path: pushedPath, maxSizeMB: 1 });
|
|
452
|
+
assert(Buffer.from(result.data, 'base64').toString('utf8') === fileText, 'pulled file content mismatch');
|
|
453
|
+
return { filename: result.filename, sizeBytes: result.sizeBytes, sourcePath: result.sourcePath };
|
|
454
|
+
});
|
|
455
|
+
await step('project file write', async () => command({ type: 'desktop:project:file', projectName: 'live-smoke', path: 'project-file.txt', content: fileText }));
|
|
456
|
+
await step('sync push text', async () => command({ type: 'desktop:sync:push', projectName: 'live-smoke', path: 'sync-file.txt', content: fileText }));
|
|
457
|
+
await step('sync push base64', async () => command({ type: 'desktop:sync:push', projectName: 'live-smoke', path: 'sync-file-b64.txt', content: Buffer.from(fileText).toString('base64'), encoding: 'base64' }));
|
|
458
|
+
await step('sync complete', async () => command({ type: 'desktop:sync:complete', totalPushed: 2 }));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async function launchNotepad(label) {
|
|
462
|
+
const before = await command({ type: 'desktop:app:is_running', name: 'notepad' });
|
|
463
|
+
const beforeIds = new Set((before.processes || []).map((item) => Number(item.pid)));
|
|
464
|
+
await command({ type: 'desktop:app:launch', name: 'notepad' });
|
|
465
|
+
await wait(1800);
|
|
466
|
+
let after = null;
|
|
467
|
+
for (let i = 0; i < 12; i++) {
|
|
468
|
+
after = await command({ type: 'desktop:app:is_running', name: 'notepad' });
|
|
469
|
+
const newProc = (after.processes || []).find((item) => !beforeIds.has(Number(item.pid)));
|
|
470
|
+
if (newProc) {
|
|
471
|
+
const listed = await command({ type: 'desktop:window:list', title: 'Notepad' });
|
|
472
|
+
const win = (listed.windows || [])[0];
|
|
473
|
+
return { label, pid: Number(newProc.pid), title: win?.title || 'Notepad', window: win };
|
|
474
|
+
}
|
|
475
|
+
await wait(700);
|
|
476
|
+
}
|
|
477
|
+
throw new Error(`Notepad did not launch: ${JSON.stringify(after)}`);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function desktopSmoke() {
|
|
481
|
+
await step('app is_running explorer', async () => command({ type: 'desktop:app:is_running', name: 'explorer' }));
|
|
482
|
+
await step('app list_running', async () => command({ type: 'desktop:app:list_running' }, 60000));
|
|
483
|
+
await step('window list', async () => command({ type: 'desktop:window:list' }));
|
|
484
|
+
await step('window active', async () => command({ type: 'desktop:window:active' }));
|
|
485
|
+
await step('gui monitors', async () => command({ type: 'desktop:gui:monitors' }));
|
|
486
|
+
await step('gui screenshot primary', async () => {
|
|
487
|
+
const result = await command({ type: 'desktop:gui:screenshot', monitor: 'primary', quality: 65 }, 90000);
|
|
488
|
+
assert(result.bytes > 1000 || result.savedPath, 'desktop gui screenshot missing bytes/path');
|
|
489
|
+
return result;
|
|
490
|
+
});
|
|
491
|
+
await step('desktop monitors direct', async () => command({ type: 'desktop_monitors' }));
|
|
492
|
+
await step('desktop screenshot direct', async () => command({ type: 'desktop_screenshot', monitor: 'primary' }, 90000));
|
|
493
|
+
await step('desktop cursor position direct', async () => command({ type: 'desktop_cursor_position' }));
|
|
494
|
+
await step('desktop screen size direct', async () => command({ type: 'desktop_screen_size' }));
|
|
495
|
+
await step('gui cursor position relay', async () => command({ type: 'desktop:gui:position' }));
|
|
496
|
+
await step('gui screen size relay', async () => command({ type: 'desktop:gui:screensize' }));
|
|
497
|
+
|
|
498
|
+
const blank = await step('app launch notepad for window close', async () => launchNotepad('blank close'), { fatal: true });
|
|
499
|
+
if (blank) {
|
|
500
|
+
await step('window focus', async () => command({ type: 'desktop:window:focus', title: blank.title }));
|
|
501
|
+
await step('window minimize', async () => command({ type: 'desktop:window:minimize', title: blank.title }));
|
|
502
|
+
await wait(500);
|
|
503
|
+
await step('window restore', async () => command({ type: 'desktop:window:restore', title: blank.title }));
|
|
504
|
+
await step('window maximize', async () => command({ type: 'desktop:window:maximize', title: blank.title }));
|
|
505
|
+
await wait(500);
|
|
506
|
+
await step('window resize', async () => command({ type: 'desktop:window:resize', title: blank.title, x: 120, y: 120, width: 820, height: 520 }));
|
|
507
|
+
await step('window close blank notepad', async () => command({ type: 'desktop:window:close', title: blank.title }));
|
|
508
|
+
await wait(1000);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const active = await step('app launch notepad for gui tests', async () => launchNotepad('gui'), { fatal: true });
|
|
512
|
+
if (!active) return null;
|
|
513
|
+
await step('window focus gui notepad', async () => command({ type: 'desktop:window:focus', title: active.title }));
|
|
514
|
+
await step('window resize gui notepad', async () => command({ type: 'desktop:window:resize', title: active.title, x: 160, y: 160, width: 880, height: 540 }));
|
|
515
|
+
await wait(700);
|
|
516
|
+
const listed = await command({ type: 'desktop:window:list', title: 'Notepad' });
|
|
517
|
+
const win = (listed.windows || [])[0] || active.window || { left: 160, top: 160, width: 880, height: 540 };
|
|
518
|
+
const x = Math.round(Number(win.left) + Math.min(380, Number(win.width) / 2));
|
|
519
|
+
const y = Math.round(Number(win.top) + Math.min(220, Number(win.height) / 2));
|
|
520
|
+
report.artifacts.notepadTarget = { pid: active.pid, title: active.title, x, y, window: compact(win) };
|
|
521
|
+
writeReport();
|
|
522
|
+
|
|
523
|
+
await step('desktop hover direct on notepad', async () => command({ type: 'desktop_hover', x, y }));
|
|
524
|
+
await step('desktop click direct on notepad', async () => command({ type: 'desktop_click', x, y, button: 'left' }));
|
|
525
|
+
await step('gui type into notepad', async () => command({ type: 'desktop:gui:type', text: `Empir3 live GUI smoke ${STAMP}` }));
|
|
526
|
+
await step('gui hotkey select all', async () => command({ type: 'desktop:gui:hotkey', keys: ['Control', 'a'] }));
|
|
527
|
+
await step('gui type replace text', async () => command({ type: 'desktop:gui:type', text: `Empir3 live GUI replacement ${STAMP}` }));
|
|
528
|
+
await step('gui move cursor', async () => command({ type: 'desktop:gui:move', x: x + 80, y: y + 40 }));
|
|
529
|
+
await step('gui click', async () => command({ type: 'desktop:gui:click', x, y }));
|
|
530
|
+
await step('gui doubleclick', async () => command({ type: 'desktop:gui:doubleclick', x: x + 40, y }));
|
|
531
|
+
await step('gui scroll', async () => command({ type: 'desktop:gui:scroll', x, y, clicks: -1 }));
|
|
532
|
+
await step('desktop drag direct on notepad', async () => command({ type: 'desktop_drag', x, y, toX: x + 140, toY: y, durationMs: 450, steps: 12 }));
|
|
533
|
+
|
|
534
|
+
return { pid: active.pid, title: active.title, x, y };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async function mcpSmoke(desktopTarget) {
|
|
538
|
+
if (!existsSync(INSTALLER)) throw new Error(`Installer not found for MCP smoke: ${INSTALLER}`);
|
|
539
|
+
const transport = new StdioClientTransport({
|
|
540
|
+
command: INSTALLER,
|
|
541
|
+
args: ['--mcp'],
|
|
542
|
+
env: { ...process.env, BRIDGE_URL: BRIDGE },
|
|
543
|
+
});
|
|
544
|
+
const client = new Client({ name: 'empir3-live-smoke', version: '1.0.0' });
|
|
545
|
+
await client.connect(transport);
|
|
546
|
+
const calls = [];
|
|
547
|
+
async function call(name, args = {}) {
|
|
548
|
+
const started = Date.now();
|
|
549
|
+
try {
|
|
550
|
+
const result = await client.callTool({ name, arguments: args });
|
|
551
|
+
if (result?.isError) throw new Error(JSON.stringify(compact(result)));
|
|
552
|
+
calls.push({ name, ok: true, elapsedMs: Date.now() - started, detail: compact(result) });
|
|
553
|
+
return result;
|
|
554
|
+
} catch (error) {
|
|
555
|
+
calls.push({ name, ok: false, elapsedMs: Date.now() - started, error: error?.message || String(error) });
|
|
556
|
+
throw error;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
try {
|
|
561
|
+
await step('mcp list tools', async () => {
|
|
562
|
+
const tools = await client.listTools();
|
|
563
|
+
const names = tools.tools.map((tool) => tool.name).sort();
|
|
564
|
+
const expected = [
|
|
565
|
+
'browser_status', 'bridge_reliability_status', 'bridge_reliability_smoke', 'bridge_action_log',
|
|
566
|
+
'bridge_safety_status', 'bridge_revoke_control',
|
|
567
|
+
'browser_navigate', 'browser_click', 'browser_click_ref', 'browser_click_xy',
|
|
568
|
+
'browser_type', 'browser_type_ref', 'browser_press', 'browser_scroll',
|
|
569
|
+
'browser_screenshot', 'desktop_monitors', 'desktop_cursor_position', 'desktop_screenshot', 'desktop_click',
|
|
570
|
+
'desktop_hover', 'desktop_drag', 'browser_snapshot', 'browser_text', 'browser_evaluate',
|
|
571
|
+
'browser_highlight', 'browser_chat', 'browser_read_chat', 'browser_record_start',
|
|
572
|
+
'browser_record_stop', 'browser_play', 'browser_recordings', 'browser_refresh',
|
|
573
|
+
];
|
|
574
|
+
const missing = expected.filter((name) => !names.includes(name));
|
|
575
|
+
assert(missing.length === 0, `missing MCP tools: ${missing.join(', ')}`);
|
|
576
|
+
return { count: names.length, names };
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
const url = dataUrl(liveTestHtml('mcp stdio tools'));
|
|
580
|
+
await step('mcp browser tools', async () => {
|
|
581
|
+
await call('browser_status');
|
|
582
|
+
await call('browser_navigate', { url });
|
|
583
|
+
await wait(1000);
|
|
584
|
+
await call('browser_snapshot', { filter: 'all' });
|
|
585
|
+
await call('browser_text');
|
|
586
|
+
await call('browser_type', { selector: '#mcpInput', text: 'mcp selector typed' });
|
|
587
|
+
await call('browser_click', { selector: '#selectorBtn' });
|
|
588
|
+
await call('browser_press', { key: 'Tab' });
|
|
589
|
+
await call('browser_click_xy', { x: 120, y: 120 });
|
|
590
|
+
await call('browser_scroll', { y: 260, x: 0 });
|
|
591
|
+
await call('browser_evaluate', { script: 'window.__smokeSummary()' });
|
|
592
|
+
await call('browser_highlight', { selector: '#highlightMe' });
|
|
593
|
+
await call('browser_screenshot');
|
|
594
|
+
await call('browser_chat', { message: `MCP visible smoke chat ${STAMP}` });
|
|
595
|
+
await call('browser_read_chat', { limit: 10 });
|
|
596
|
+
await call('browser_record_start');
|
|
597
|
+
await call('browser_click', { selector: '#recordBtn' });
|
|
598
|
+
const stopped = await call('browser_record_stop', { name: `mcp-live-smoke-${STAMP}` });
|
|
599
|
+
await call('browser_recordings');
|
|
600
|
+
await call('browser_refresh');
|
|
601
|
+
return { calls: calls.filter((item) => item.name.startsWith('browser_')), stopped: compact(stopped) };
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
await step('mcp desktop tools', async () => {
|
|
605
|
+
await call('desktop_monitors');
|
|
606
|
+
await call('desktop_cursor_position');
|
|
607
|
+
await call('desktop_screenshot', { monitor: 'primary' });
|
|
608
|
+
if (desktopTarget?.x && desktopTarget?.y) {
|
|
609
|
+
await call('desktop_hover', { x: desktopTarget.x, y: desktopTarget.y });
|
|
610
|
+
await call('desktop_click', { x: desktopTarget.x, y: desktopTarget.y, button: 'left' });
|
|
611
|
+
await call('desktop_drag', {
|
|
612
|
+
x: desktopTarget.x,
|
|
613
|
+
y: desktopTarget.y,
|
|
614
|
+
toX: desktopTarget.x + 120,
|
|
615
|
+
toY: desktopTarget.y,
|
|
616
|
+
durationMs: 350,
|
|
617
|
+
steps: 8,
|
|
618
|
+
button: 'left',
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
return { calls: calls.filter((item) => item.name.startsWith('desktop_')) };
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
await step('mcp reliability and safety tools', async () => {
|
|
625
|
+
await call('bridge_reliability_status');
|
|
626
|
+
await call('bridge_reliability_smoke');
|
|
627
|
+
await call('bridge_action_log');
|
|
628
|
+
await call('bridge_safety_status');
|
|
629
|
+
return { calls: calls.filter((item) => item.name.startsWith('bridge_')) };
|
|
630
|
+
});
|
|
631
|
+
} finally {
|
|
632
|
+
await client.close().catch(() => {});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async function safetyLockdownRestoreSmoke() {
|
|
637
|
+
await step('safety status before lockdown', async () => getJson('/api/safety'));
|
|
638
|
+
const configBefore = await step('config backup before lockdown', async () => getJson('/api/config'));
|
|
639
|
+
if (!configBefore?.enabledTools) throw new Error('Cannot run lockdown restore smoke without enabledTools backup');
|
|
640
|
+
await step('safety lockdown command', async () => command({ type: 'safety_lockdown' }));
|
|
641
|
+
await step('safety restore config', async () => postJson('/api/config', { enabledTools: configBefore.enabledTools }));
|
|
642
|
+
await step('safety status after restore', async () => {
|
|
643
|
+
const status = await getJson('/api/safety');
|
|
644
|
+
assert(status.state === 'write_controls_enabled', `write controls did not restore: ${JSON.stringify(status)}`);
|
|
645
|
+
return status;
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async function cleanupNotepad(target) {
|
|
650
|
+
if (!target?.pid) return;
|
|
651
|
+
await step('app kill smoke notepad by pid', async () => command({ type: 'desktop:app:kill', pid: target.pid }));
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async function main() {
|
|
655
|
+
writeReport();
|
|
656
|
+
await step('bridge api status and provenance', async () => {
|
|
657
|
+
const status = await getJson('/api/status');
|
|
658
|
+
assert(status.running === true, 'bridge status is not running');
|
|
659
|
+
assert(status.version === EXPECTED_VERSION, `expected bridge ${EXPECTED_VERSION}, got ${status.version}`);
|
|
660
|
+
return { status, provenance: listenerProvenance() };
|
|
661
|
+
}, { fatal: true });
|
|
662
|
+
await step('bridge wrapper health', async () => {
|
|
663
|
+
const status = await getJson('/api/status');
|
|
664
|
+
const healthUrl = String(status.bridgeUrl || 'http://localhost:9867').replace('localhost', '127.0.0.1') + '/health';
|
|
665
|
+
const health = await requestJson('GET', healthUrl, undefined, 15000);
|
|
666
|
+
assert(health.status === 'connected' || health.ok || health.ready, `unexpected wrapper health: ${JSON.stringify(health)}`);
|
|
667
|
+
return health;
|
|
668
|
+
});
|
|
669
|
+
await browserDirectSmoke();
|
|
670
|
+
await browserAliasSmoke();
|
|
671
|
+
await companionSmoke();
|
|
672
|
+
const desktopTarget = await desktopSmoke();
|
|
673
|
+
await mcpSmoke(desktopTarget);
|
|
674
|
+
await cleanupNotepad(desktopTarget);
|
|
675
|
+
await safetyLockdownRestoreSmoke();
|
|
676
|
+
await step('action log available', async () => command({ type: 'action_log' }));
|
|
677
|
+
await step('reliability status final', async () => command({ type: 'reliability_status' }));
|
|
678
|
+
await step('reliability smoke final', async () => {
|
|
679
|
+
const result = await command({ type: 'reliability_smoke' }, 90000);
|
|
680
|
+
assert(result.ok === true || (Array.isArray(result.checks) && result.checks.every((check) => check.ok)), 'reliability smoke reported failure');
|
|
681
|
+
return result;
|
|
682
|
+
});
|
|
683
|
+
writeReport();
|
|
684
|
+
const failed = report.checks.filter((check) => !check.ok);
|
|
685
|
+
log(`summary: ${report.checks.length - failed.length}/${report.checks.length} passed`);
|
|
686
|
+
log(`report: ${REPORT_PATH}`);
|
|
687
|
+
log(`summary: ${SUMMARY_PATH}`);
|
|
688
|
+
if (failed.length) process.exitCode = 1;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
main().catch((error) => {
|
|
692
|
+
report.fatal = error?.stack || error?.message || String(error);
|
|
693
|
+
writeReport();
|
|
694
|
+
console.error(report.fatal);
|
|
695
|
+
process.exit(1);
|
|
696
|
+
});
|