← Commit history
verb battery: fixture page + runner + battery-parity gate
5 files changed
+320
demo/sample-download.txtadded+1@@ -0,0 +1 @@+nb-battery-sample-download MARKER-7f3a9
demo/sample-upload.txtadded+1@@ -0,0 +1 @@+nb-battery-sample-upload MARKER-c41d2
demo/test-harness.htmladded+51@@ -0,0 +1,51 @@+<!doctype html><meta charset="utf-8"><title>nb verb battery harness</title>+<style>+ body{background:#0d1117;color:#e6edf3;font:16px/1.5 system-ui;margin:0;padding:28px;max-width:900px}+ h1{font-size:20px} section{border:1px solid #21262d;border-radius:8px;padding:14px;margin:12px 0}+ button,input,select{font:inherit;padding:6px 12px;margin:4px;background:#161b22;color:#e6edf3;border:1px solid #30363d;border-radius:6px}+ #hover-menu{display:none;padding:8px;background:#161b22;border:1px solid #2ea043}+ #hover-zone:hover + #hover-menu{display:block}+ .spacer{height:1200px;background:linear-gradient(#0d1117,#161b22)}+ #ctx-target{user-select:none}+</style>+<h1 id="page-title">nb verb battery harness</h1>+<p>Deterministic fixtures for tools/verb-battery.mjs. Every element here is load-bearing for a test.</p>++<section><h2>Click targets</h2>+ <button id="btn-one" onclick="this.textContent='Clicked Once OK'">Click Me Once</button>+ <button id="dbl-btn" ondblclick="this.textContent='Double OK'">Double Click Me</button>+ <button id="ctx-target" oncontextmenu="this.textContent='Context OK';return false">Right Click Me</button>+ <button onclick="document.getElementById('amb-note').textContent='amb-A'">Duplicate</button>+ <button onclick="document.getElementById('amb-note').textContent='amb-B'">Duplicate</button>+ <span id="amb-note"></span>+</section>++<section><h2>Inputs</h2>+ <input id="text-input" placeholder="type here">+ <select id="select-input"><option value="">pick</option><option value="alpha">Alpha</option><option value="beta">Beta</option></select>+ <input id="date-input" type="date">+ <input id="file-input" type="file">+ <span id="file-note"></span>+ <script>document.getElementById('file-input').addEventListener('change',e=>{document.getElementById('file-note').textContent='file:'+(e.target.files[0]||{}).name})</script>+</section>++<section><h2>Hover</h2>+ <div id="hover-zone">Hover me</div><div id="hover-menu">revealed-by-hover</div>+</section>++<section><h2>Async + dialogs + errors</h2>+ <button id="spawn-late" onclick="setTimeout(()=>{const d=document.createElement('div');d.id='late-el';d.textContent='late-element-arrived';document.body.appendChild(d)},1200)">Spawn Late</button>+ <button id="confirm-btn" onclick="document.getElementById('dlg-note').textContent='confirm:'+window.confirm('battery confirm?')">Open Confirm</button>+ <span id="dlg-note"></span>+ <button id="err-btn" onclick="console.error('battery-console-error');setTimeout(()=>{throw new Error('battery-thrown-error')},0)">Throw Error</button>+ <button id="title-btn" onclick="document.title='battery-title-changed'">Change Title</button>+</section>++<section><h2>Spawn + download + login fixture</h2>+ <a id="blank-link" href="nb.html" target="_blank">Open child window</a><br>+ <a id="download-link" href="sample-download.txt" download="nb-battery-download.txt">Download sample</a>+ <form id="login-form" onsubmit="return false"><input id="lf-user" autocomplete="username" placeholder="user"><input id="lf-pass" type="password" autocomplete="current-password" placeholder="pass"><button type="submit">Sign in</button></form>+</section>++<div class="spacer"></div>+<div id="bottom-marker">bottom-of-page</div>
tools/check-guard-coverage.mjs+11@@ -65,5 +65,16 @@ const ghostDescribed = described.filter((v) => !metaKeys.includes(v)); if (undescribed.length) errors.push(`UNDESCRIBED in nbrowser_describe (no AI can discover them): ${undescribed.join(', ')}`); if (ghostDescribed.length) errors.push(`DESCRIBED but not implemented (no VERB_META): ${ghostDescribed.join(', ')}`); +// BATTERY PARITY. tools/verb-battery.mjs is the daily end-to-end test; its COVERAGE map must decide+// a fate (live / guard / skip:reason) for EVERY implemented verb, or a new verb ships untested and the+// daily run reads green while a hole grows. Same doctrine as the manifest/describe gates: the list+// that can silently rot gets a gate instead of a convention.+const battery = readFileSync(join(root, 'tools/verb-battery.mjs'), 'utf8');+const covStart = battery.indexOf('export const COVERAGE');+const covBlock = battery.slice(covStart, battery.indexOf('};', covStart));+const covered = new Set([...covBlock.matchAll(/([a-z_0-9]+):\s*'/g)].map((m) => m[1]));+const unbattried = metaKeys.map((v) => v.replace(/^nbrowser_/, '')).filter((v) => !covered.has(v));+if (unbattried.length) errors.push(`NO BATTERY COVERAGE (add to tools/verb-battery.mjs COVERAGE as live/guard/skip:reason): ${unbattried.join(', ')}`);+ if (errors.length) { console.error('guard-coverage FAILED:\n ' + errors.join('\n ')); process.exit(1); } console.log(`guard-coverage OK: ${dispatched.length} verbs classified, ${Object.values(policy).filter((c) => c === 'mutating').length} mutating verbs gated, ${dispatched.filter((v) => policy[v] !== 'alias').length} verbs with non-empty hint + pitfalls, ${manifest.verbs.length} verbs declared to ab in bridge.json`);
tools/verb-battery.mjsadded+256@@ -0,0 +1,256 @@+#!/usr/bin/env node+// nb verb battery — the DAILY end-to-end test of every nbrowser_* verb, run exactly the way users+// run them: through the `adom-bridge` CLI over the relay to a real desktop, against the real+// signed-in browser, using the fixture page + sample files in demo/. Run:+//+// node tools/verb-battery.mjs --target AdomLapper --profile chrome:[email protected]+//+// EVERY verb in VERB_META must appear in COVERAGE below as one of:+// live:<fn> — executed against the desktop, strict assertions+// guard:<fn> — executed expecting a SPECIFIC refusal (guards are the product; a guard that+// stops refusing is a worse regression than a feature that stops working)+// skip:"reason" — consciously not auto-tested (destructive / human-gated), reason printed+// tools/check-guard-coverage.mjs fails the build if a verb is missing from this map.+import { execFileSync } from 'node:child_process';+import { readFileSync, writeFileSync } from 'node:fs';+import { dirname, join } from 'node:path';+import { fileURLToPath } from 'node:url';++const root = join(dirname(fileURLToPath(import.meta.url)), '..');+const argv = Object.fromEntries(process.argv.slice(2).map((a, i, arr) => a.startsWith('--') ? [a.slice(2), arr[i + 1]] : []).filter(Boolean));+const TARGET = argv.target || 'AdomLapper';+const PROFILE = argv.profile || 'chrome:[email protected]';+const THREAD = 'nb verb battery';+const SID = 'nbbattery';+const HARNESS = 'https://wiki.adom.inc/blob/app/nb-bridge/demo/test-harness.html';+const CB = Date.now();++function call(verb, args = {}, timeoutMs = 90000) {+ try {+ const out = execFileSync('adom-bridge', ['--target', TARGET, '--ai-thread', THREAD, verb, JSON.stringify(args)], { encoding: 'utf8', timeout: timeoutMs, maxBuffer: 32 * 1024 * 1024 });+ let d; try { d = JSON.parse(out); } catch { return { _raw: out, _parse: 'outer' }; }+ if (typeof d.output === 'string' && d.output.trim().startsWith('{')) { try { return { ...d, ...JSON.parse(d.output) }; } catch {} }+ return d;+ } catch (e) {+ const out = String(e.stdout || '');+ try { const d = JSON.parse(out); if (typeof d.output === 'string') { try { return { ...d, ...JSON.parse(d.output) }; } catch {} } return d; } catch {}+ return { _err: String(e.message).slice(0, 200), _raw: out.slice(0, 300) };+ }+}+const evalIn = (expr, extra = {}) => call('nbrowser_eval', { sessionId: SID, expression: expr, ...extra });++const results = []; let ctx = {};+function record(verb, mode, ok, note) {+ results.push({ verb, mode, ok, note: String(note || '').slice(0, 220) });+ console.log(`${ok ? 'PASS' : 'FAIL'} ${verb.padEnd(30)} [${mode}] ${note || ''}`);+}+const A = (cond, note, bad) => cond ? [true, note] : [false, bad];++// ─── the checks ──────────────────────────────────────────────────────────────+const T = {+ // lifecycle / read-only+ status: () => { const r = call('nbrowser_status'); return A(r.ok === true && r.ext === 'nbe', `ext:${r.ext} v${r.version}`, JSON.stringify(r).slice(0, 160)); },+ readiness: () => { const r = call('nbrowser_readiness'); ctx.ready = r.ready; return A(typeof r.ready === 'boolean' && r.claudeExtension !== undefined, `ready:${r.ready} state:${r.state}`, JSON.stringify(r).slice(0, 160)); },+ ping: () => { const r = call('nbrowser_ping'); return A(r.ok === true && r.pong === true, 'pong', JSON.stringify(r).slice(0, 120)); },+ whoami: () => { const r = call('nbrowser_whoami'); return A(r.ok !== false, `keys:${Object.keys(r).slice(0, 5)}`, JSON.stringify(r).slice(0, 120)); },+ profiles: () => { const r = call('nbrowser_profiles'); const ps = r.profiles || []; ctx.profileCount = ps.length; return A(ps.length > 0, `${ps.length} profiles`, JSON.stringify(r).slice(0, 140)); },+ resolve_profiles: () => { const r = call('nbrowser_resolve_profiles'); return A(r.ok !== false, 'resolved', JSON.stringify(r).slice(0, 120)); },+ chrome_status: () => { const r = call('nbrowser_chrome_status'); return A(r.installed === true, `chrome installed, phase:${r.phase}`, JSON.stringify(r).slice(0, 140)); },+ describe: () => { const r = call('nbrowser_describe'); const n = (r.verbs || []).length; const meta = metaCount(); return A(n === meta, `describe:${n} == VERB_META:${meta}`, `describe:${n} != VERB_META:${meta} — regenerate!`); },+ activity: () => { const r = call('nbrowser_activity', { limit: 5 }); return A(Array.isArray(r.entries), `${(r.entries || []).length} entries`, JSON.stringify(r).slice(0, 120)); },+ os_windows: () => { const r = call('nbrowser_os_windows'); return A(r.ok === true && typeof r.count === 'number', `${r.count} windows`, JSON.stringify(r).slice(0, 140)); },+ wake_profile: () => { const r = call('nbrowser_wake_profile', { profile: PROFILE }); return A(r.ok === true, `woke:${r.woke}`, JSON.stringify(r).slice(0, 160)); },+ rescan: () => { const r = call('nbrowser_rescan'); return A(r.ok !== false, 'rescanned', JSON.stringify(r).slice(0, 120)); },+ resync_sessions: () => { const r = call('nbrowser_resync_sessions'); return A(r.ok !== false, 'resynced', JSON.stringify(r).slice(0, 120)); },++ // window lifecycle (the spine — everything after rides ctx.sid)+ open_window: () => {+ const g = call('nbrowser_open_window', { sessionId: SID + '-noreg', url: HARNESS });+ if (g.errorCode !== 'session_unregistered' && !/session_unregistered|thread.*purpose|registration/i.test(JSON.stringify(g))) return [false, 'registration guard MISSING: unregistered open was not refused: ' + JSON.stringify(g).slice(0, 140)];+ const r = call('nbrowser_open_window', { sessionId: SID, thread: THREAD, purpose: 'daily verb battery run', profile: PROFILE, url: `${HARNESS}?cb=${CB}` });+ ctx.hwnd = r.hwnd; ctx.windowId = r.windowId;+ return A(r.ok === true && r.sessionId === SID, `opened hwnd:${r.hwnd} (+ registration guard verified)`, JSON.stringify(r).slice(0, 200));+ },+ pin_session: () => { const r = call('nbrowser_pin_session', { sessionId: SID }); return A(r.ok !== false, 'pinned', JSON.stringify(r).slice(0, 120)); },+ use_profile: () => { const r = call('nbrowser_use_profile', { profile: PROFILE }); return A(r.ok !== false, `active:${PROFILE}`, JSON.stringify(r).slice(0, 120)); },+ list_windows: () => { const r = call('nbrowser_list_windows'); const mine = JSON.stringify(r).includes(SID); return A(mine, 'my session listed', 'battery session missing from list_windows'); },+ window_state: () => { const r = call('nbrowser_window_state', { sessionId: SID, width: 1200, height: 860 }); return A(r.ok !== false, '1200x860', JSON.stringify(r).slice(0, 140)); },+ switch_window: () => { const r = call('nbrowser_switch_window', { sessionId: SID }); return A(r.ok !== false, 'switched to own', JSON.stringify(r).slice(0, 120)); },++ // navigation+ navigate: () => { const r = call('nbrowser_navigate', { sessionId: SID, url: `${HARNESS}?cb=${CB}&nav=2` }); return A(r.ok !== false, 'navigated', JSON.stringify(r).slice(0, 140)); },+ reload: () => { const r = call('nbrowser_reload', { sessionId: SID }); return A(r.ok !== false, 'reloaded', JSON.stringify(r).slice(0, 120)); },+ back: () => { call('nbrowser_wait', { sessionId: SID, ms: 800 }); const r = call('nbrowser_back', { sessionId: SID }); call('nbrowser_wait', { sessionId: SID, ms: 800 }); const u = evalIn('location.search'); return A(String(u.result).includes('cb=' + CB) && !String(u.result).includes('nav=2'), 'went back', `url after back: ${u.result}`); },+ forward: () => { const r = call('nbrowser_forward', { sessionId: SID }); call('nbrowser_wait', { sessionId: SID, ms: 800 }); const u = evalIn('location.search'); return A(String(u.result).includes('nav=2'), 'went forward', `url after forward: ${u.result}`); },+ wait: () => { const t0 = Date.now(); call('nbrowser_wait', { sessionId: SID, ms: 700 }); return A(Date.now() - t0 >= 650, `waited ${Date.now() - t0}ms`, 'returned too fast'); },+ wait_for: () => { call('nbrowser_click', { sessionId: SID, selector: '#spawn-late' }); const r = call('nbrowser_wait_for', { sessionId: SID, selector: '#late-el', timeoutMs: 6000 }); return A(r.ok === true || r.found === true, 'late element appeared', JSON.stringify(r).slice(0, 140)); },+ keepalive: () => { const r = call('nbrowser_keepalive', { sessionId: SID, stop: true }); return A(r.ok !== false, 'keepalive verb responds', JSON.stringify(r).slice(0, 120)); },++ // reading + JS+ eval: () => { const r = evalIn('document.getElementById("page-title").textContent'); return A(String(r.result).includes('battery harness'), 'read page title', JSON.stringify(r).slice(0, 140)); },+ evaluate: () => { const r = call('nbrowser_evaluate', { sessionId: SID, expression: '1+41' }); return A(String(r.result) === '42', 'alias works', JSON.stringify(r).slice(0, 120)); },+ screenshot: () => { const r = call('nbrowser_screenshot', { sessionId: SID }); return A(r.ok === true && (r.width || 0) > 100, `${r.width}x${r.height}`, JSON.stringify(r).slice(0, 140)); },+ screenshot_full_res: () => { const r = call('nbrowser_screenshot_full_res', { sessionId: SID, toFile: true }); return A(r.ok === true && !!r.path, `saved ${String(r.path).slice(-40)}`, JSON.stringify(r).slice(0, 140)); },+ errors: () => { call('nbrowser_click', { sessionId: SID, selector: '#err-btn' }); call('nbrowser_wait', { sessionId: SID, ms: 500 }); const r = call('nbrowser_errors', { sessionId: SID }); return A(JSON.stringify(r).includes('battery-'), 'console error captured', JSON.stringify(r).slice(0, 160)); },+ fetch_url: () => { const r = call('nbrowser_fetch_url', { sessionId: SID, url: 'https://wiki.adom.inc/blob/app/nb-bridge/demo/sample-download.txt' }); return A(JSON.stringify(r).includes('MARKER-7f3a9'), 'fetched sample content', JSON.stringify(r).slice(0, 140)); },++ // input (isTrusted family)+ click: () => {+ const g = call('nbrowser_click', { sessionId: SID, text: 'Duplicate' });+ if (g.errorCode !== 'ambiguous_target' && !/ambiguous/i.test(JSON.stringify(g))) return [false, 'ambiguous_target guard MISSING: ' + JSON.stringify(g).slice(0, 140)];+ const r = call('nbrowser_click', { sessionId: SID, text: 'Click Me Once' });+ const v = evalIn('document.getElementById("btn-one").textContent');+ return A(String(v.result) === 'Clicked Once OK', 'clicked by text (+ ambiguity guard verified)', `btn text: ${v.result}`);+ },+ double_click: () => { call('nbrowser_double_click', { sessionId: SID, selector: '#dbl-btn' }); const v = evalIn('document.getElementById("dbl-btn").textContent'); return A(String(v.result) === 'Double OK', 'dblclick landed', `text: ${v.result}`); },+ right_click: () => { call('nbrowser_right_click', { sessionId: SID, selector: '#ctx-target' }); const v = evalIn('document.getElementById("ctx-target").textContent'); return A(String(v.result) === 'Context OK', 'contextmenu fired', `text: ${v.result}`); },+ hover: () => { call('nbrowser_hover', { sessionId: SID, selector: '#hover-zone' }); const v = evalIn('getComputedStyle(document.getElementById("hover-menu")).display'); return A(String(v.result) === 'block', 'hover menu revealed', `display: ${v.result}`); },+ type: () => { call('nbrowser_click', { sessionId: SID, selector: '#text-input' }); call('nbrowser_type', { sessionId: SID, selector: '#text-input', text: 'battery-typed' }); const v = evalIn('document.getElementById("text-input").value'); return A(String(v.result) === 'battery-typed', 'typed into input', `value: ${v.result}`); },+ press_key: () => { call('nbrowser_type', { sessionId: SID, selector: '#text-input', text: 'x' }); const r = call('nbrowser_press_key', { sessionId: SID, key: 'Backspace' }); return A(r.ok !== false, 'key dispatched', JSON.stringify(r).slice(0, 120)); },+ select: () => { call('nbrowser_select', { sessionId: SID, selector: '#select-input', value: 'beta' }); const v = evalIn('document.getElementById("select-input").value'); return A(String(v.result) === 'beta', 'option selected', `value: ${v.result}`); },+ set_date: () => { call('nbrowser_set_date', { sessionId: SID, selector: '#date-input', value: '2026-08-14' }); const v = evalIn('document.getElementById("date-input").value'); return A(String(v.result) === '2026-08-14', 'date set', `value: ${v.result}`); },+ input_dispatch: () => { const r = call('nbrowser_input_dispatch', { sessionId: SID, type: 'click', selector: '#btn-one' }); return A(r.ok !== false, 'low-level dispatch ok', JSON.stringify(r).slice(0, 140)); },+ set_file_input: () => {+ const up = call('send_files', { reason: 'verb battery: staging the sample upload file', filePaths: [join(root, 'demo', 'sample-upload.txt')], dest: 'C:/Users/john/.adom/native-browser' });+ if (up.success === false) return [false, 'send_files staging failed: ' + JSON.stringify(up).slice(0, 120)];+ call('nbrowser_set_file_input', { sessionId: SID, selector: '#file-input', filePaths: ['C:/Users/john/.adom/native-browser/sample-upload.txt'] });+ const v = evalIn('document.getElementById("file-note").textContent');+ return A(String(v.result).includes('sample-upload'), 'file attached to input', `note: ${v.result}`);+ },++ // presentation + signals+ caption: () => { const r = call('nbrowser_caption', { sessionId: SID, text: 'verb battery running', durationMs: 1500 }); return A(r.ok === true, 'caption painted', JSON.stringify(r).slice(0, 120)); },+ cursor: () => { const r = call('nbrowser_cursor', { sessionId: SID, x: 300, y: 300, label: 'battery' }); return A(r.ok === true, 'cursor shown', JSON.stringify(r).slice(0, 120)); },+ focus: () => { const r = call('nbrowser_focus', { sessionId: SID }); return A(r.ok !== false && r.foregrounded !== true, 'tab selected, no foreground steal', JSON.stringify(r).slice(0, 140)); },+ flash: () => { const r = call('nbrowser_flash', { sessionId: SID }); return A(r.ok !== false, 'flashed', JSON.stringify(r).slice(0, 120)); },+ flash_stop: () => { const r = call('nbrowser_flash_stop', { sessionId: SID }); return A(r.ok !== false, 'flash stopped', JSON.stringify(r).slice(0, 120)); },+ flash_mode: () => { const r = call('nbrowser_flash_mode', { mode: 'quiet' }); return A(r.ok !== false && r.mode === 'quiet', 'mode:quiet', JSON.stringify(r).slice(0, 120)); },+ taskbar: () => { const a = call('nbrowser_taskbar', { sessionId: SID, progress: { state: 'indeterminate' } }); const b = call('nbrowser_taskbar', { sessionId: SID, progress: { state: 'none' } }); return A(a.ok !== false && b.ok !== false, 'progress on/off', JSON.stringify(a).slice(0, 120)); },+ fullscreen: () => { const a = call('nbrowser_fullscreen', { sessionId: SID, on: true }); const b = call('nbrowser_fullscreen', { sessionId: SID, on: false }); return A(a.ok !== false && b.ok !== false, 'fullscreen roundtrip', JSON.stringify(a).slice(0, 120)); },++ // tabs+ open_tab: () => { const r = call('nbrowser_open_tab', { sessionId: SID, url: `${HARNESS}?cb=${CB}&tab=2` }); ctx.tab2 = r.tabId; return A(r.ok !== false, `tab:${r.tabId}`, JSON.stringify(r).slice(0, 140)); },+ list_tabs: () => { const r = call('nbrowser_list_tabs', { sessionId: SID }); const n = (r.tabs || []).length; return A(n >= 2, `${n} tabs`, JSON.stringify(r).slice(0, 140)); },+ switch_tab: () => { const r = call('nbrowser_switch_tab', { sessionId: SID, tabId: ctx.tab2 }); return A(r.ok !== false, 'switched', JSON.stringify(r).slice(0, 120)); },+ close_tab: () => { const r = call('nbrowser_close_tab', { sessionId: SID, tabId: ctx.tab2 }); const l = call('nbrowser_list_tabs', { sessionId: SID }); return A((l.tabs || []).length === 1, 'tab closed', JSON.stringify(r).slice(0, 120)); },++ // recording (WGC, background-safe)+ record_start: () => { const r = call('nbrowser_record_start', { method: 'wgc', hwnd: ctx.hwnd, fps: 30 }); ctx.recId = r.recordingId; return A(!!r.recordingId, `rec:${r.recordingId}`, JSON.stringify(r).slice(0, 160)); },+ record_status: () => { const r = call('nbrowser_record_status', { method: 'wgc', recordingId: ctx.recId }); return A(r.ok !== false, JSON.stringify(r).slice(0, 80), JSON.stringify(r).slice(0, 140)); },+ record_stop: () => { call('nbrowser_wait', { sessionId: SID, ms: 1500 }); const r = call('nbrowser_record_stop', { method: 'wgc', recordingId: ctx.recId }); return A(JSON.stringify(r).includes('.mp4') || r.ok === true, 'mp4 produced', JSON.stringify(r).slice(0, 160)); },+ record_list: () => { const r = call('nbrowser_record_list'); return A(r.ok !== false, 'listed', JSON.stringify(r).slice(0, 120)); },++ // downloads+ download: () => { const r = call('nbrowser_download', { sessionId: SID, url: 'https://wiki.adom.inc/blob/app/nb-bridge/demo/sample-download.txt', filename: 'nb-battery-download.txt' }); return A(r.ok !== false, `downloadId:${r.downloadId}`, JSON.stringify(r).slice(0, 140)); },+ download_wait: () => { const r = call('nbrowser_download_wait', { sessionId: SID, sinceMs: 60000, timeoutMs: 20000 }); const done = JSON.stringify(r).includes('nb-battery-download'); if (done) call('delete_file', { reason: 'verb battery cleanup: removing the test download from Downloads', path: 'C:/Users/john/Downloads/nb-battery-download.txt' }); return A(done, 'download completed + cleaned', JSON.stringify(r).slice(0, 160)); },++ // spawn tracking + events + dialogs+ events_and_owned: () => {+ call('nbrowser_click', { sessionId: SID, selector: '#blank-link' });+ call('nbrowser_wait', { sessionId: SID, ms: 1500 });+ const own = call('nbrowser_owned', { sessionId: SID });+ const spawned = (own.spawned || []).length;+ const ev = call('nbrowser_events');+ const sawSpawn = JSON.stringify(ev).includes('spawned_window') || spawned > 0;+ ctx.spawned = spawned;+ return A(sawSpawn, `spawn tracked (${spawned} owned.spawned)`, `no spawn recorded: owned=${JSON.stringify(own).slice(0, 120)} events=${JSON.stringify(ev).slice(0, 120)}`);+ },+ handle_dialog: () => { const r = call('nbrowser_handle_dialog', { sessionId: SID, action: 'accept' }); call('nbrowser_click', { sessionId: SID, selector: '#confirm-btn' }); call('nbrowser_wait', { sessionId: SID, ms: 600 }); const v = evalIn('document.getElementById("dlg-note").textContent'); return A(String(v.result).startsWith('confirm:'), `dialog handled: ${v.result}`, JSON.stringify(r).slice(0, 140)); },+ cleanup: () => { const r = call('nbrowser_cleanup', { sessionId: SID }); const own = call('nbrowser_owned', { sessionId: SID }); return A((own.spawned || []).length === 0, `spawns closed (was ${ctx.spawned})`, JSON.stringify(r).slice(0, 140)); },++ // credentials family — CONTRACT tests only, never real secrets+ login_state: () => { const r = call('nbrowser_login_state', { sessionId: SID, host: 'wiki.adom.inc' }); return A(r.ok !== false, 'login_state responds', JSON.stringify(r).slice(0, 120)); },+ login_form: () => { const r = call('nbrowser_login_form', { sessionId: SID }); return A(r.ok !== false && JSON.stringify(r).length > 20, `detected form:${r.hasForm}`, JSON.stringify(r).slice(0, 140)); },+ credentials: () => { const r = call('nbrowser_credentials', { host: 'battery-nonexistent.invalid' }); const leaked = /password"\s*:\s*"[^"]/.test(JSON.stringify(r)); return A(!leaked, 'no secret material in response', 'PASSWORD FIELD IN RESPONSE — contract broken'); },+ totp: () => { const r = call('nbrowser_totp', { host: 'battery-nonexistent.invalid' }); const leaked = /seed|secret"\s*:\s*"[^"]/.test(JSON.stringify(r)); return A(!leaked && r.ok !== true, 'refused unknown host, no seed leak', JSON.stringify(r).slice(0, 140)); },+ clear_cookies: () => { const r = call('nbrowser_clear_cookies', { host: 'battery-nonexistent.invalid' }); return A(r.ok !== false && (r.removed === 0 || typeof r.removed === 'number'), `removed:${r.removed}`, JSON.stringify(r).slice(0, 140)); },+ add_extension_origin: () => { const r = call('nbrowser_add_extension_origin', { id: 'nncjgceobmlenfliamnibncjojpedbcc' }); return A(r.ok !== false, 'idempotent re-add ok', JSON.stringify(r).slice(0, 140)); },++ open_os_window: () => {+ const r = call('nbrowser_open_os_window', { sessionId: SID + '-l1', thread: THREAD, purpose: 'verb battery: layer-1 open + attested close', profile: PROFILE, url: `${HARNESS}?layer1=${CB}` });+ if (r.ok !== true || r.launched !== true) return [false, 'layer-1 open failed: ' + JSON.stringify(r).slice(0, 160)];+ if (!r.hwnd) return [true, 'launched (grouped into existing window - no hwnd to close, per contract)'];+ const one = call('nbrowser_force_close', { hwnd: r.hwnd });+ const ticket = (JSON.stringify(one).match(/"overrideTicket":"([^"]+)"/) || [])[1];+ if (!ticket) return [false, 'no overrideTicket from force_close step 1: ' + JSON.stringify(one).slice(0, 160)];+ const two = call('nbrowser_force_close', { hwnd: r.hwnd, overrideTicket: ticket, attestOrphan: true, reason: 'verb battery cleanup of its own layer-1 test window' });+ return A(JSON.stringify(two).includes('closed') || two.ok === true, 'layer-1 open + minimized + two-step attested close', JSON.stringify(two).slice(0, 160));+ },+ // guards-as-tests+ guard_autologin: () => { const r = call('nbrowser_autologin', { sessionId: SID, host: 'wiki.adom.inc' }); return A(r.ok !== true && r.loggedIn !== true, 'refused without confirm:true', 'AUTOLOGIN RAN WITHOUT CONFIRM: ' + JSON.stringify(r).slice(0, 120)); },+ guard_close_all: () => { const r = call('nbrowser_close'); return A(r.ok !== true || /confirm/i.test(JSON.stringify(r)), 'close-all demands confirmCloseAll', 'CLOSE-ALL RAN UNCONFIRMED: ' + JSON.stringify(r).slice(0, 120)); },+ guard_record_fallback: () => { const r = call('nbrowser_record_start', { method: 'getdisplaymedia', sessionId: SID }); return A(r.ok !== true, 'picker fallback refused without fallbackConfirmed', 'FALLBACK RAN UNCONFIRMED: ' + JSON.stringify(r).slice(0, 140)); },+ guard_force_close: () => { const r = call('nbrowser_force_close', { hwnd: ctx.hwnd }); const two = /attestation|overrideTicket/i.test(JSON.stringify(r)); return A(two && r.closed !== true, 'two-step attestation demanded', 'FORCE_CLOSE SKIPPED ATTESTATION: ' + JSON.stringify(r).slice(0, 140)); },+ profile_block: () => { const b = call('nbrowser_profile_block', { profile: PROFILE }); const g = call('nbrowser_open_tab', { sessionId: SID, url: `${HARNESS}?blocked=1` }); const u = call('nbrowser_profile_unblock', { profile: PROFILE }); const blockedWorked = g.ok !== true || /block/i.test(JSON.stringify(g)); return A(b.ok !== false && u.ok !== false && blockedWorked, 'block refused a drive; unblock restored', `block:${JSON.stringify(b).slice(0, 80)} drive-under-block:${JSON.stringify(g).slice(0, 80)}`); },+ profile_unblock: () => { const r = call('nbrowser_open_tab', { sessionId: SID, url: `${HARNESS}?after-unblock=1&cb=${CB}` }); if (r.tabId) call('nbrowser_close_tab', { sessionId: SID, tabId: r.tabId }); return A(r.ok !== false, 'driving works again after unblock', JSON.stringify(r).slice(0, 140)); },++ // teardown (must be LAST live test)+ close_window: () => {+ const r = call('nbrowser_close_window', { sessionId: SID, includeSpawned: true });+ const own = call('nbrowser_owned', { sessionId: SID });+ const clean = (own.opened || []).length === 0 && (own.spawned || []).length === 0;+ return A(clean, 'window closed, zero litter', `still owned: ${JSON.stringify(own).slice(0, 140)}`);+ },+ owned: () => [true, 'exercised inside events_and_owned + close_window'],+};++// ─── coverage map: EVERY VERB_META verb decides its fate here ───────────────+export const COVERAGE = {+ status: 'live', readiness: 'live', ping: 'live', whoami: 'live', profiles: 'live',+ resolve_profiles: 'live', chrome_status: 'live', describe: 'live', activity: 'live',+ os_windows: 'live', wake_profile: 'live', rescan: 'live', resync_sessions: 'live',+ open_window: 'live', pin_session: 'live', use_profile: 'live', list_windows: 'live',+ window_state: 'live', switch_window: 'live', navigate: 'live', reload: 'live',+ back: 'live', forward: 'live', wait: 'live', wait_for: 'live', keepalive: 'live',+ eval: 'live', evaluate: 'live', screenshot: 'live', screenshot_full_res: 'live',+ errors: 'live', fetch_url: 'live', click: 'live', double_click: 'live',+ right_click: 'live', hover: 'live', type: 'live', press_key: 'live', select: 'live',+ set_date: 'live', input_dispatch: 'live', set_file_input: 'live', caption: 'live',+ cursor: 'live', focus: 'live', flash: 'live', flash_stop: 'live', flash_mode: 'live',+ taskbar: 'live', fullscreen: 'live', open_tab: 'live', list_tabs: 'live',+ switch_tab: 'live', close_tab: 'live', record_start: 'live', record_status: 'live',+ record_stop: 'live', record_list: 'live', download: 'live', download_wait: 'live',+ handle_dialog: 'live', login_state: 'live', login_form: 'live', credentials: 'live',+ totp: 'live', clear_cookies: 'live', add_extension_origin: 'live', owned: 'live',+ cleanup: 'live', close_window: 'live', profile_block: 'live', profile_unblock: 'live',+ open_os_window: 'live',+ autologin: 'guard', close: 'guard', force_close: 'guard',+ events: 'live:events_and_owned',+ install_chrome: 'skip:starts a real Chrome install on the desktop — exercised manually on fresh boxes',+ register_totp: 'skip:writes a real TOTP seed into the vault — contract covered by totp read-path test',+ dev_reload: 'skip:restarts the SW + native host and is throttled 1/90s — disruptive to live sessions',+ uninstall: 'skip:the ab-2.0.35 cascade hook — deletes host registrations; tested at release time only',+};++function metaCount() {+ const s = readFileSync(join(root, 'bridge', 'server.js'), 'utf8');+ const i = s.indexOf('const VERB_META = {'); let d = 0, e = -1;+ for (let j = s.indexOf('{', i); j < s.length; j++) { if (s[j] === '{') d++; else if (s[j] === '}') { d--; if (!d) { e = j; break; } } }+ return [...s.slice(i, e + 1).matchAll(/^ {2}nbrowser_[a-z0-9_]+:\s*\{/gm)].length;+}++if (import.meta.url === `file://${process.argv[1]}`) {+ console.log(`nb verb battery — target:${TARGET} profile:${PROFILE} harness:${HARNESS}\n`);+ const order = ['status','readiness','ping','whoami','profiles','resolve_profiles','chrome_status','describe','activity','os_windows','wake_profile','rescan','resync_sessions',+ 'open_window','pin_session','use_profile','list_windows','window_state','switch_window',+ 'navigate','reload','back','forward','wait','wait_for','keepalive',+ 'eval','evaluate','screenshot','screenshot_full_res','errors','fetch_url',+ 'click','double_click','right_click','hover','type','press_key','select','set_date','input_dispatch','set_file_input',+ 'caption','cursor','focus','flash','flash_stop','flash_mode','taskbar','fullscreen',+ 'open_tab','list_tabs','switch_tab','close_tab',+ 'record_start','record_status','record_stop','record_list',+ 'download','download_wait','login_state','login_form','credentials','totp','clear_cookies','add_extension_origin',+ 'open_os_window','guard_autologin','guard_record_fallback','guard_force_close','guard_close_all','profile_block','profile_unblock',+ 'events_and_owned','handle_dialog','cleanup','owned','close_window'];+ for (const name of order) {+ const fn = T[name]; if (!fn) { record(name, '??', false, 'no test fn'); continue; }+ try { const [ok, note] = fn(); record(name, name.startsWith('guard') ? 'guard' : 'live', ok, note); }+ catch (e) { record(name, 'live', false, 'threw: ' + String(e.message).slice(0, 160)); }+ }+ for (const [v, mode] of Object.entries(COVERAGE)) if (mode.startsWith('skip:')) record(v, 'skip', true, mode.slice(5));+ const fails = results.filter((r) => !r.ok);+ const report = { at: new Date().toISOString(), target: TARGET, profile: PROFILE, pass: results.length - fails.length, fail: fails.length, results };+ writeFileSync(join(root, 'tools', 'verb-battery-report.json'), JSON.stringify(report, null, 1));+ console.log(`\n${report.pass}/${results.length} passed, ${fails.length} failed — report: tools/verb-battery-report.json`);+ process.exit(fails.length ? 1 : 0);+}