mirror of
https://github.com/ruvnet/RuView.git
synced 2026-10-18 21:00:22 -05:00
- ui://ruview/console-v1.html: one self-contained widget for MCP Apps hosts
and ChatGPT (ui.resourceUri + openai/outputTemplate). Renders node
streams, device scans and doctor results; Refresh re-runs the tool via
window.openai.callTool or an MCP Apps tools/call. Empty CSP allowlist, no
network, textContent only, parent-frame messages only.
- MCP: protocol negotiation (2025-06-18/2025-03-26/2024-11-05),
structuredContent + compact JSON text (17-33% smaller, MEASURED),
resources/list|read, tool titles, canonical tool name in result _meta.
Transport-neutral handleRpc + shared FIFO dispatcher.
- `mcp start --http`: Streamable HTTP for ChatGPT/remote clients. Bearer or
secret-path token (constant-time), loopback default, Origin allowlist,
256 KiB cap, no batches, write grants never honoured over HTTP.
- CLI: formatted TTY output (tables, rate bars, alerts first, NO_COLOR),
JSON for pipes/--json, capture countdown, `esp32 --watch` with per-node
sparklines, lazy module loading (--help ~20% faster, MEASURED).
- monitor detects Ameba CSI report-buffer starvation ("lack of csi buf"),
the root cause of the Realtek heartbeat-only stall seen live.
- Size budget 320 -> 384 KiB (package ~339 kB).
Verified: 160 tests + 83 security tests; headless-Chrome e2e of the MCP
Apps flow against live boards (refresh round trip 108 -> 105 packets);
HTTP security probes; clean tarball smoke (stdio + HTTP).
Co-Authored-By: claude-flow <ruv@ruv.net>
97 lines
5.8 KiB
JavaScript
97 lines
5.8 KiB
JavaScript
// ADR-375: terminal rendering. Pure functions, pinned without color; JSON stays
|
|
// the contract for pipes. Monitor starvation detection runs the real monitor
|
|
// script against a stand-in serial module.
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { bar, colorEnabled, renderResult, sparkline, table, createStyle } from '../src/cli-ui.js';
|
|
import { MONITOR_SCRIPT } from '../src/tools.js';
|
|
|
|
const capture = {
|
|
ok: true, listen: '0.0.0.0:5005', seconds: 15, packets: 5199, decodedPackets: 5199, csiNodes: 2,
|
|
evidence: 'MEASURED: live UDP packets received and decoded on this host',
|
|
heartbeatOnlySenders: [{ address: '192.168.1.67', heartbeats: 50 }],
|
|
nodes: [
|
|
{ source: 'esp32', nodeId: 42, csiRateHz: 4.73, csiLossFraction: 0, rssiMean: -63.1, csi: { shape: '1x64' }, csiShapes: [{}, {}], packets: { csi: 71 } },
|
|
{ source: 'realtek', nodeId: 3, csiRateHz: 324.67, csiLossFraction: 0.1, rssiMean: -39.3, csi: { shape: '1x52', synthetic: false }, seqReordered: 37, seqStrays: 7, packets: { csi: 4870 } },
|
|
],
|
|
analysis: { ok: true, backend: 'wasm', integrity: 'verified', input: { frames: 12549, sampleRateHz: 313.7 }, summary: { last: { heart: { bpm: 99.06, status: 'unreliable' }, respiratory: { status: 'unavailable' } } }, note: 'no reference measurement' },
|
|
};
|
|
|
|
test('capture renders nodes, alerts and analysis without color codes', () => {
|
|
const out = renderResult('ruview_esp32_capture', capture, { color: false });
|
|
assert.doesNotMatch(out, /\x1b\[/);
|
|
assert.match(out, /^RUVIEW \/ NODE STREAM {3}● MEASURED/);
|
|
assert.match(out, /heartbeat-only: 192\.168\.1\.67 \(50 heartbeats, no CSI\)/);
|
|
assert.match(out, /esp32 +42 +4\.7 Hz/);
|
|
assert.match(out, /realtek +3 +324\.7 Hz +████████/);
|
|
assert.match(out, /10\.0%/);
|
|
assert.match(out, /1x64 \+1/);
|
|
assert.match(out, /37r 7s/);
|
|
assert.match(out, /heart 99\.1 bpm \(unreliable\) {3}breathing unavailable/);
|
|
const lines = out.split('\n');
|
|
const header = lines.find((l) => l.startsWith('SOURCE'));
|
|
const row = lines.find((l) => l.startsWith('realtek'));
|
|
assert.equal(row.indexOf('324.7'), header.indexOf('CSI RATE'), 'columns align');
|
|
});
|
|
|
|
test('failures lead with the reason and the fix; color is opt-in', () => {
|
|
const out = renderResult('ruview_esp32_capture', { ok: false, reason: 'heartbeat_only', remedy: 'reset the board', packets: 3 }, { color: false });
|
|
assert.match(out, /✖ heartbeat only/);
|
|
assert.match(out, /fix {2}reset the board/);
|
|
assert.match(renderResult('ruview_esp32_capture', capture, { color: true }), /\x1b\[38;5;155m/);
|
|
assert.equal(colorEnabled({ isTTY: true }, {}), true);
|
|
assert.equal(colorEnabled({ isTTY: true }, { NO_COLOR: '' }), false);
|
|
assert.equal(colorEnabled({ isTTY: true }, { TERM: 'dumb' }), false);
|
|
assert.equal(colorEnabled({ isTTY: false }, {}), false);
|
|
});
|
|
|
|
test('devices, monitor, flash and generic views', () => {
|
|
const dev = renderResult('ruview_devices_scan', { ok: true, devices: [{ port: 'COM10', bridge: 'Prolific PL2303GC', likelyRoles: ['realtek'], confirmWith: ['ruview monitor --port COM10 --baud 1500000'] }, { port: 'COM3', builtin: true }], note: 'hints' }, { color: false });
|
|
assert.match(dev, /COM10 +Prolific PL2303GC +realtek +ruview monitor --port COM10 --baud 1500000/);
|
|
assert.doesNotMatch(dev, /COM3/, 'built-in ports hidden');
|
|
const mon = renderResult('ruview_node_monitor', { ok: false, reason: 'csi_buffer_starvation', hint: 'reset', csi_callbacks: 0, lines: 629, baud: 1500000, reset_on_open: false }, { color: false });
|
|
assert.match(mon, /✖ csi buffer starvation/);
|
|
assert.match(mon, /reset on open: no/);
|
|
const flash = renderResult('ruview_firmware_plan', { ok: true, dryRun: true, plan: { chip: 'ESP32-C6', flashSize: '4MB', port: 'COM16', checksums: 'SHA256SUMS.txt', images: [{ offset: '0x0', role: 'bootloader', bytes: 19568, integrity: 'verified' }] } }, { color: false });
|
|
assert.match(flash, /RUVIEW \/ FLASH PLAN/);
|
|
assert.match(flash, /0x0 +bootloader +19568 +verified/);
|
|
const gen = renderResult('ruview_claim_check', { ok: true, verdict: 'PASS', findings: [] }, { color: false });
|
|
assert.match(gen, /RUVIEW \/ CLAIM CHECK/);
|
|
assert.match(gen, /verdict +PASS/);
|
|
assert.match(gen, /use --json/);
|
|
});
|
|
|
|
test('bar, sparkline and table primitives', () => {
|
|
assert.equal(bar(1, 4), '████');
|
|
assert.equal(bar(0, 4), ' ');
|
|
assert.equal(bar(0.5, 4).length, 4);
|
|
assert.equal(sparkline([1, 2, 3, 4, 5, 6, 7, 8]), '▁▂▃▄▅▆▇█');
|
|
assert.equal(sparkline([5, 5]), '▄▄');
|
|
assert.equal(sparkline([]), '');
|
|
assert.equal(table([['A', 'B'], ['xx', 'y']], createStyle(false)), 'A B\nxx y');
|
|
});
|
|
|
|
test('monitor script flags Ameba CSI buffer starvation', (t) => {
|
|
const py = ['python3', 'python'].find((p) => spawnSync(p, ['--version'], { stdio: 'ignore' }).status === 0);
|
|
if (!py) { t.skip('python not available'); return; }
|
|
const fake = mkdtempSync(join(tmpdir(), 'ruview-fakeserial-'));
|
|
try {
|
|
writeFileSync(join(fake, 'serial.py'), [
|
|
'class Serial:',
|
|
' def __init__(self, *a, **k): self.lines = [b"[WLAN-W] lack of csi buf!\\n", b"[WLAN-W] csi buf not enough\\n", b"RUVIEW_CSI: heartbeat tick=9\\n"]',
|
|
' def open(self): pass',
|
|
' def readline(self): return self.lines.pop(0) if self.lines else b""',
|
|
' def close(self): pass',
|
|
].join('\n'));
|
|
const r = spawnSync(py, ['-c', MONITOR_SCRIPT, 'COM1', '0.4', '1500000'], { env: { PATH: process.env.PATH, SYSTEMROOT: process.env.SYSTEMROOT || '', PYTHONPATH: fake }, encoding: 'utf8' });
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.match(r.stdout, /LINES=3 CSI=0 STARVED=2/);
|
|
} finally {
|
|
rmSync(fake, { recursive: true, force: true });
|
|
}
|
|
});
|