// #1873 CLI parity for the Console's Proxy page: status, unit, init-lock, init --generate-key and // fence clear --restart. Every verb runs only from a human shell; nothing prints a key or a path. import test from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, readFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { main } from '../src/cli.mjs'; import { proxyCommand } from '../src/model-proxy.mjs'; import { io } from './helpers.mjs'; const code = c => e => e.code === c; const fakeSystem = () => ({ dataRoot: join(mkdtempSync(join(tmpdir(), 'ps-cli-')), 'data') }); const STATUS = { release: { fetched: true, version: '8.0.42', panelVersion: '1.26.7', verified: true }, init: { initialized: true, partial: false, settings: { port: 18432 }, files: [] }, unit: { unit: 'mosaic-proxy.service', installed: true, active: 'active', pid: 77, port: 18432, listening: true }, fence: { set: false }, initLock: { present: false, clearable: false, blockers: [] }, ownProxy: { available: false, state: 'not built' }, next: ['accounts'], }; function fakeAdmin({ restart = { confirmed: true } } = {}) { const calls = []; return { calls, status: async () => { calls.push(['status']); return STATUS; }, render: async () => { calls.push(['render']); return { text: '[Unit]\nDescription=fixture\n' }; }, unitAction: async op => { calls.push(['unit', op]); return op === 'restart' ? { receipt: { op: 'unit.restart' }, evidence: restart } : { op, ok: true }; }, initLockClear: async () => { calls.push(['init-lock.clear']); return { cleared: true, receipt: { op: 'init-lock.clear' } }; }, fenceClear: async call => { calls.push(['fence.clear']); return call('proxy.fence.clear', { evidence: restart }); }, }; } const human = () => io(); test('every new setup verb refuses inside an agent shell and with --tester, before the controller runs', async () => { const admin = fakeAdmin(); for (const args of [['proxy', 'status'], ['proxy', 'status', '--json'], ['proxy', 'unit', 'render'], ['proxy', 'unit', 'start'], ['proxy', 'unit', 'restart'], ['proxy', 'init-lock', 'status'], ['proxy', 'init-lock', 'clear'], ['proxy', 'fence', 'clear', '--restart', '--business', 'demo'], ['proxy', 'init', '--generate-key', '--business', 'demo']]) { await assert.rejects(main(args, io({ CODEX_THREAD_ID: 'fixture' }), { system: fakeSystem(), proxyAdmin: admin, transport: () => () => { throw Error('must not call'); } }), code('human-required'), args.join(' ')); await assert.rejects(main([...args, '--tester'], human(), { system: fakeSystem(), proxyAdmin: admin }), code('human-required'), `${args.join(' ')} --tester`); } assert.deepEqual(admin.calls, []); }); test('proxy status prints the same facts the page shows, and --json the status object', async () => { const admin = fakeAdmin(); const x = human(); await main(['proxy', 'status'], x, { system: fakeSystem(), proxyAdmin: admin }); assert.match(x.out.text, /^release: 8\.0\.42 \(panel 1\.26\.7\) hashes verified$/m); assert.match(x.out.text, /^unit: mosaic-proxy\.service installed, active pid 77, port 18432 answering$/m); assert.match(x.out.text, /^own proxy: not built$/m); assert.match(x.out.text, /^next: accounts$/m); const y = human(); await main(['proxy', 'status', '--json'], y, { system: fakeSystem(), proxyAdmin: admin }); assert.deepEqual(JSON.parse(y.out.text), STATUS); await assert.rejects(main(['proxy', 'status', '--verbose'], human(), { system: fakeSystem(), proxyAdmin: admin }), code('proxy-usage')); }); test('proxy unit and init-lock verbs map one to one onto the controller; bad usage refuses', async () => { const admin = fakeAdmin(); const x = human(); await main(['proxy', 'unit', 'render'], x, { system: fakeSystem(), proxyAdmin: admin }); assert.equal(x.out.text, '[Unit]\nDescription=fixture\n'); for (const op of ['install', 'start', 'stop']) await main(['proxy', 'unit', op], human(), { system: fakeSystem(), proxyAdmin: admin }); await main(['proxy', 'unit', 'status'], human(), { system: fakeSystem(), proxyAdmin: admin }); await main(['proxy', 'init-lock', 'status'], human(), { system: fakeSystem(), proxyAdmin: admin }); const y = human(); await main(['proxy', 'init-lock', 'clear'], y, { system: fakeSystem(), proxyAdmin: admin }); assert.equal(JSON.parse(y.out.text).cleared, true); assert.deepEqual(admin.calls, [['render'], ['unit', 'install'], ['unit', 'start'], ['unit', 'stop'], ['status'], ['status'], ['init-lock.clear']]); for (const args of [['proxy', 'unit'], ['proxy', 'unit', 'reload'], ['proxy', 'unit', 'start', 'now'], ['proxy', 'init-lock'], ['proxy', 'init-lock', 'clear', '--force']]) await assert.rejects(main(args, human(), { system: fakeSystem(), proxyAdmin: admin }), code('proxy-usage'), args.join(' ')); }); test('a controller refusal reaches the shell as its proxy code only, with no path or detail', async () => { const admin = fakeAdmin(); admin.unitAction = async () => { throw Object.assign(Error('/home/owner/.config/mosaic-dev/proxy.json pa-cli-sentinel'), { code: 'proxy-unit-missing' }); }; admin.status = async () => { throw Error('pa-cli-sentinel'); }; await assert.rejects(main(['proxy', 'unit', 'start'], human(), { system: fakeSystem(), proxyAdmin: admin }), e => e.code === 'proxy-unit-missing' && !e.message.includes('pa-cli-sentinel') && !e.message.includes('/home/')); await assert.rejects(main(['proxy', 'status'], human(), { system: fakeSystem(), proxyAdmin: admin }), e => e.code === 'proxy-unit-failed' && !e.message.includes('pa-cli-sentinel')); }); test('proxy init --generate-key needs no terminal, never prompts, and sends generate to the bus', async () => { const x = human(); assert.notEqual(x.stdin.isTTY, true); const calls = [], system = fakeSystem(); await main(['proxy', 'init', '--generate-key', '--business', 'demo', '--port', '18440', '--rotate-grace-minutes', '5'], x, { system, promptProxyKey: () => { throw Error('must not prompt'); }, transport: b => async (verb, args) => { calls.push({ b, verb, args }); return { port: 18440, rotateGraceMinutes: 5, binary: '/fixture' }; } }); assert.deepEqual(calls, [{ b: 'demo', verb: 'proxy.init', args: { generate: true, port: 18440, rotateGraceMinutes: 5 } }]); assert.deepEqual(JSON.parse(x.out.text), { port: 18440, rotateGraceMinutes: 5, firstOutputSeconds: null, generated: true }); // The same init receipt the page writes, settings only. const [receipt] = readFileSync(join(system.dataRoot, 'state', 'proxy-admin.jsonl'), 'utf8').trim().split('\n').map(JSON.parse); assert.deepEqual({ ...receipt, time: 'T' }, { time: 'T', op: 'init', generated: true, port: 18440, rotateGraceMinutes: 5, firstOutputSeconds: null }); assert.match(x.err.text, /^receipt: \{"time":/m); await assert.rejects(main(['proxy', 'init', '--generate-key', '--generate-key', '--business', 'demo'], human(), { system: fakeSystem() }), code('proxy-usage')); await assert.rejects(main(['proxy', 'init', '--generate-key', '--port', '8317', '--business', 'demo'], human(), { system: fakeSystem() }), code('proxy-config-invalid')); }); test('proxy fence clear --restart restarts first and clears only on a confirmed restart', async () => { const admin = fakeAdmin(); const calls = []; const x = human(); await main(['proxy', 'fence', 'clear', '--restart', '--business', 'demo'], x, { system: fakeSystem(), proxyAdmin: admin, transport: b => async (verb, args) => { calls.push({ b, verb, args }); return { cleared: true, evidence: { after: { pid: 2 } } }; } }); assert.deepEqual(admin.calls, [['unit', 'restart'], ['fence.clear']]); assert.deepEqual(calls, [{ b: 'demo', verb: 'proxy.fence.clear', args: { evidence: { confirmed: true } } }]); assert.deepEqual(JSON.parse(x.out.text), { restart: { op: 'unit.restart' }, cleared: true, evidence: { after: { pid: 2 } } }); const unconfirmed = fakeAdmin({ restart: { confirmed: false } }); await assert.rejects(main(['proxy', 'fence', 'clear', '--restart', '--business', 'demo'], human(), { system: fakeSystem(), proxyAdmin: unconfirmed, transport: () => () => { throw Error('must not call'); } }), code('proxy-restart-unconfirmed')); assert.deepEqual(unconfirmed.calls, [['unit', 'restart']]); await assert.rejects(main(['proxy', 'fence', 'clear', '--restart', '--restart'], human(), { system: fakeSystem(), proxyAdmin: admin }), code('proxy-usage')); }); test('proxy fetch shows the page\'s progress steps and writes the same fetch receipt; stdout keeps its line', async () => { const system = fakeSystem(); const x = human(); await proxyCommand(['fetch'], x, { system, fetch: async ({ progress }) => { for (const step of ['download-archive', 'verify-archive', 'install']) progress(step); return { binary: 'fixture-binary', panel: 'fixture-panel' }; } }); assert.deepEqual(JSON.parse(x.out.text), { binary: 'fixture-binary', panel: 'fixture-panel' }); assert.match(x.err.text, /^fetch: download-archive\nfetch: verify-archive\nfetch: install\nreceipt: /); const receipts = readFileSync(join(system.dataRoot, 'state', 'proxy-admin.jsonl'), 'utf8').trim().split('\n').map(JSON.parse); assert.equal(receipts.length, 1); assert.equal(receipts[0].op, 'fetch'); assert.equal(receipts[0].verified, true); assert.match(receipts[0].archiveSha256, /^[0-9a-f]{64}$/); // A failed hash check writes no receipt and keeps its code. const failed = fakeSystem(); await assert.rejects(proxyCommand(['fetch'], human(), { system: failed, fetch: async () => { throw Object.assign(Error('mismatch'), { code: 'proxy-fetch-hash' }); } }), code('proxy-fetch-hash')); assert.throws(() => readFileSync(join(failed.dataRoot, 'state', 'proxy-admin.jsonl')), { code: 'ENOENT' }); });