mo
feat: enterprise capability expansion — app control, browser control, background execution
fce80d1 Download apps/desktop/test/control.test.mjs from aiagentmona/mona-agent: direct link, hf CLI and curl.
- Browser
- Download file 5.95 kB
-
https://huggingface.co/aiagentmona/mona-agent/resolve/main/apps/desktop/test/control.test.mjs
- Command line
-
hf download hf://aiagentmona/mona-agent/apps/desktop/test/control.test.mjs
-
curl -L -o control.test.mjs https://huggingface.co/aiagentmona/mona-agent/resolve/main/apps/desktop/test/control.test.mjs
5.95 kB
| // Control-channel tests: reconnect backoff, queue flush, terminal close codes. | |
| // Run: node --test test/control.test.mjs | |
| import { describe, it, before, after } from 'node:test'; | |
| import assert from 'node:assert/strict'; | |
| import { WebSocketServer } from 'ws'; | |
| const server = new WebSocketServer({ port: 0 }); | |
| const port = server.address().port; | |
| const url = `ws://127.0.0.1:${port}/ws?role=device`; | |
| // ControlChannel reads CLOUD.wsUrl at import time — import after env is set. | |
| process.env.MONA_CLOUD_WS = url; | |
| const { ControlChannel } = await import('../src/control.js'); | |
| let connections = 0; | |
| let closeCode = null; | |
| let lastHello = null; | |
| server.on('connection', (ws) => { | |
| connections++; | |
| ws.on('message', (raw) => { | |
| const msg = JSON.parse(raw.toString()); | |
| if (msg.type === 'hello') { | |
| lastHello = msg; | |
| ws.send(JSON.stringify({ type: 'pong', data: {} })); | |
| } | |
| }); | |
| if (closeCode !== null) { | |
| // 1006 is reserved and can never be sent as a close frame — terminate() | |
| // is what produces a real 1006 (abnormal closure) on the client side. | |
| if (closeCode === 1006) ws.terminate(); | |
| else ws.close(closeCode); | |
| } | |
| }); | |
| const reset = () => { connections = 0; lastHello = null; }; | |
| // terminate() produces an ECONNRESET on the client — expected in the 1006 test. | |
| const quiet = (ch) => ch.on('error', () => {}); | |
| describe('control channel', () => { | |
| before(() => { server.on('connection', () => {}); }); | |
| after(() => server.close()); | |
| it('emits auth-failed and stops on terminal close code 4001', async () => { | |
| reset(); | |
| closeCode = 4001; | |
| const ch = new ControlChannel('test-key', 'agent-1'); | |
| const events = []; | |
| quiet(ch); | |
| ch.on('auth-failed', (code) => events.push(['auth-failed', code])); | |
| await new Promise((resolve) => { | |
| ch.on('disconnected', () => setTimeout(resolve, 100)); | |
| ch.connect(); | |
| }); | |
| assert.equal(events.length, 1); | |
| assert.equal(events[0][1], 4001); | |
| assert.equal(ch.stopped, true); | |
| // No reconnect loop: allow one backoff cycle, assert no new connection. | |
| await new Promise((r) => setTimeout(r, 1500)); | |
| assert.equal(connections, 1); | |
| ch.close(); | |
| }); | |
| it('emits auth-failed and stops on terminal close code 4003 (revoked)', async () => { | |
| reset(); | |
| closeCode = 4003; | |
| const ch = new ControlChannel('test-key', 'agent-1'); | |
| let failed = false; | |
| quiet(ch); | |
| ch.on('auth-failed', () => { failed = true; }); | |
| await new Promise((resolve) => { | |
| ch.on('disconnected', () => setTimeout(resolve, 100)); | |
| ch.connect(); | |
| }); | |
| assert.equal(failed, true); | |
| assert.equal(ch.stopped, true); | |
| await new Promise((r) => setTimeout(r, 1500)); | |
| assert.equal(connections, 1); | |
| ch.close(); | |
| }); | |
| it('reconnects with backoff after a transient close (1006)', async () => { | |
| reset(); | |
| closeCode = 1006; | |
| const ch = new ControlChannel('test-key', 'agent-1'); | |
| quiet(ch); | |
| // A second disconnect can only happen after a reconnect — wait for it. | |
| let disconnects = 0; | |
| const reconnected = new Promise((resolve, reject) => { | |
| ch.on('disconnected', () => { | |
| if (++disconnects >= 2) resolve(true); | |
| }); | |
| ch.connect(); | |
| setTimeout(() => reject(new Error('no reconnect within 5s')), 5000); | |
| }); | |
| assert.equal(await reconnected, true); | |
| assert.ok(connections >= 2, `expected a reconnect, saw ${connections} connection(s)`); | |
| closeCode = null; | |
| ch.close(); | |
| }); | |
| it('queues messages until open, then flushes', async () => { | |
| reset(); | |
| closeCode = null; | |
| const ch = new ControlChannel('test-key', 'agent-1'); | |
| let flushed = false; | |
| quiet(ch); | |
| ch.connect(); | |
| ch.step('test', { n: 1 }); // sent before open queued | |
| await new Promise((resolve) => { | |
| const timer = setInterval(() => { | |
| if (connections === 1) { flushed = true; clearInterval(timer); resolve(); } | |
| }, 50); | |
| }); | |
| assert.equal(flushed, true); | |
| ch.close(); | |
| }); | |
| it('sends v:1 envelope and announces capabilities in hello', async () => { | |
| reset(); | |
| closeCode = null; | |
| const caps = { | |
| tools: [{ name: 'sysinfo', description: 'System information' }], | |
| shell: { allowlist: ['df', 'uptime'], unsafe: false, platform: 'darwin' }, | |
| }; | |
| const ch = new ControlChannel('test-key', 'agent-1', caps); | |
| quiet(ch); | |
| ch.connect(); | |
| await new Promise((resolve) => { | |
| const timer = setInterval(() => { | |
| if (lastHello) { clearInterval(timer); resolve(); } | |
| }, 25); | |
| }); | |
| assert.equal(lastHello.v, 1); | |
| assert.equal(lastHello.agentId, 'agent-1'); | |
| assert.ok(Array.isArray(lastHello.data.capabilities.tools)); | |
| assert.equal(lastHello.data.capabilities.tools[0].name, 'sysinfo'); | |
| assert.deepEqual(lastHello.data.capabilities.shell.allowlist, ['df', 'uptime']); | |
| assert.ok(lastHello.data.host); | |
| assert.ok(lastHello.data.version); | |
| ch.close(); | |
| }); | |
| it('streams extended metrics (cpu/mem/disk percent)', async () => { | |
| reset(); | |
| closeCode = null; | |
| const ch = new ControlChannel('test-key', 'agent-1', null, { metricsIntervalMs: 120 }); | |
| quiet(ch); | |
| const got = new Promise((resolve) => ch.on('metrics', resolve)); | |
| ch.connect(); | |
| const m = await Promise.race([ | |
| got, | |
| new Promise((_, reject) => setTimeout(() => reject(new Error('no metrics within 3s')), 3000)), | |
| ]); | |
| assert.equal(typeof m.cpuPercent, 'number'); | |
| assert.ok(m.cpuPercent >= 0 && m.cpuPercent <= 100); | |
| assert.ok(m.mem.percent >= 0 && m.mem.percent <= 100); | |
| assert.equal(typeof m.mem.used, 'number'); | |
| assert.equal(typeof m.diskPercent, 'number'); | |
| assert.equal(typeof m.uptimeSeconds, 'number'); | |
| assert.ok(m.cpuModel); | |
| // Backward-compatible fields the TUI depends on: | |
| assert.equal(typeof m.mem.total, 'number'); | |
| assert.equal(typeof m.mem.free, 'number'); | |
| assert.ok(Array.isArray(m.cpuLoad)); | |
| ch.close(); | |
| }); | |
| }); | |