import { test } from 'node:test'; import assert from 'node:assert/strict'; import { spawn } from 'node:child_process'; import { createServer } from 'node:http'; import { once } from 'node:events'; import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; // scripts/webui-service.sh (row 75, #1569). Nothing here starts a unit or // reaches the real systemd: install and uninstall run with --no-reload or a // stand-in systemctl on PATH, and XDG_CONFIG_HOME points into a temp dir. const REPO = fileURLToPath(new URL('../../..', import.meta.url)).replace(/\/$/, ''); const SCRIPT = join(REPO, 'scripts', 'webui-service.sh'); const NODE_DIR = dirname(process.execPath); const PREVIEW = ['--public-host', 'console.example.com', '--auth-header', 'X-authentik-uid', '--allow-user', 'jason']; function tmp(t) { const dir = mkdtempSync(join(tmpdir(), 'webui-service-')); t.after(() => rmSync(dir, { recursive: true, force: true })); return dir; } // A stand-in systemctl that prints `lines` for list-units and logs every call. function standIn(dir, lines = '') { const bin = join(dir, 'bin'); mkdirSync(bin); writeFileSync(join(dir, 'list-units.txt'), lines); writeFileSync(join(bin, 'systemctl'), `#!/bin/sh echo "$*" >> "${dir}/calls.txt" case "$*" in *list-units*) cat "${dir}/list-units.txt" ;; *is-enabled*) echo enabled ;; *is-active*) echo active ;; esac exit 0 `); chmodSync(join(bin, 'systemctl'), 0o755); return bin; } // Async, so an in-process server can answer the script's /healthz request. async function run(dir, args, bin = null) { const env = { PATH: `${bin ? `${bin}:` : ''}${NODE_DIR}:/usr/bin:/bin`, HOME: dir, XDG_CONFIG_HOME: join(dir, 'config') }; const child = spawn(SCRIPT, args, { env }); let stdout = '', stderr = ''; child.stdout.on('data', (d) => { stdout += d; }); child.stderr.on('data', (d) => { stderr += d; }); const [status] = await once(child, 'close'); return { status, stdout, stderr }; } const execStart = (unit) => unit.split('\n').find((l) => l.startsWith('ExecStart=')); test('render prints the unit without the preview flags', async (t) => { const dir = tmp(t); const r = await run(dir, ['render']); assert.equal(r.status, 0, r.stderr); assert.match(r.stdout, new RegExp(`^WorkingDirectory=${REPO.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm')); assert.match(r.stdout, /^Environment=PATH=\S*\/usr\/local\/bin:\/usr\/bin:\/bin$/m); assert.equal(execStart(r.stdout), `ExecStart=${process.execPath} ${REPO}/packages/webui/src/cli.mjs serve --port 7332`); for (const line of ['Restart=on-failure', 'RestartPreventExitStatus=2', 'NoNewPrivileges=yes', 'WantedBy=default.target']) { assert.ok(r.stdout.split('\n').includes(line), line); } assert.doesNotMatch(r.stdout, /@[A-Z]+@/, 'every placeholder is filled'); }); test('render carries every serve flag, the preview set included', async (t) => { const dir = tmp(t); const r = await run(dir, ['render', '--port', '7340', '--business', 'mosaic.dev_1', '--board', 'http://127.0.0.1:7331', ...PREVIEW]); assert.equal(r.status, 0, r.stderr); assert.equal(execStart(r.stdout), `ExecStart=${process.execPath} ${REPO}/packages/webui/src/cli.mjs serve --port 7340 --business mosaic.dev_1 --board http://127.0.0.1:7331 --public-host console.example.com --auth-header X-authentik-uid --allow-user jason`); }); test('a partial preview set is refused', async (t) => { const dir = tmp(t); for (const args of [PREVIEW.slice(0, 2), PREVIEW.slice(2, 6), [...PREVIEW.slice(0, 2), ...PREVIEW.slice(4, 6)]]) { const r = await run(dir, ['render', ...args]); assert.equal(r.status, 1, args.join(' ')); assert.equal(r.stdout, ''); assert.match(r.stderr, /all three or none/); } }); test('a value the unit cannot carry is refused, for every flag', async (t) => { const dir = tmp(t); const set = (flag, value) => { const preview = PREVIEW.slice(); const i = preview.indexOf(flag); if (i >= 0) { preview[i + 1] = value; return preview; } return [flag, value]; }; for (const flag of ['--business', '--board', '--public-host', '--auth-header', '--allow-user']) { for (const bad of [' x', '%n', '\n', '$HOME', '"', '\\', '|', '&']) { const value = flag === '--board' ? `http://127.0.0.1:7331${bad}` : `a${bad}b`; const r = await run(dir, ['render', ...set(flag, value)]); assert.equal(r.status, 1, `${flag} ${JSON.stringify(value)}`); assert.equal(r.stdout, '', `${flag} ${JSON.stringify(value)}`); } } for (const port of ['0', '65536', '7a', '', '-1', '999999']) { const r = await run(dir, ['render', '--port', port]); assert.equal(r.status, 1, `port ${JSON.stringify(port)}`); } for (const args of [['--business', 'Mosaic'], ['--business', '-x'], ['--business', 'a'.repeat(65)], ['--board', 'https://127.0.0.1:7331'], ['--board', '127.0.0.1:7331'], set('--public-host', 'console.example.com:443'), set('--public-host', 'https://console.example.com'), set('--auth-header', 'X_uid')]) { const r = await run(dir, ['render', ...args]); assert.equal(r.status, 1, args.join(' ')); } }); test('the rendered unit names none of the human proof agent markers and sets no agent variable', async (t) => { const dir = tmp(t); const source = readFileSync(join(REPO, 'packages', 'bus', 'src', 'human.mjs'), 'utf8'); const markers = [...source.match(/const MARKERS = \[([^\]]*)\]/)[1].matchAll(/'([A-Z_]+)'/g)].map((m) => m[1]); assert.ok(markers.length >= 7 && markers.includes('MOSAIC_BUS_CAP') && markers.includes('CLAUDECODE'), markers.join(' ')); for (const args of [[], [...PREVIEW, '--business', 'mosaic']]) { const r = await run(dir, ['render', ...args]); assert.equal(r.status, 0, r.stderr); for (const marker of markers) assert.ok(!r.stdout.includes(marker), marker); const envLines = r.stdout.split('\n').filter((l) => /^\s*(Environment|EnvironmentFile|PassEnvironment|UnsetEnvironment)\s*=/.test(l)); assert.deepEqual(envLines.map((l) => l.split('=')[1]), ['PATH']); assert.doesNotMatch(r.stdout, /^[^#]*\b(MOSAIC_|CLAUDE|CODEX|PI_)/m); } }); test('install into --dir with --no-reload writes the file whole, then reports unchanged', async (t) => { const dir = tmp(t); const unitDir = join(dir, 'units'); const bin = standIn(dir); const rendered = await run(dir, ['render', '--business', 'mosaic']); const r = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--business', 'mosaic'], bin); assert.equal(r.status, 0, r.stderr); const target = join(unitDir, 'mosaic-webui.service'); assert.match(r.stdout, new RegExp(`^written: ${target}$`, 'm')); assert.match(r.stdout, /systemctl --user enable --now mosaic-webui/); assert.equal(readFileSync(target, 'utf8'), rendered.stdout); assert.equal(statSync(target).mode & 0o777, 0o644); assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service'], 'no temp file left behind'); const again = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--business', 'mosaic'], bin); assert.equal(again.status, 0, again.stderr); assert.match(again.stdout, new RegExp(`^unchanged: ${target}$`, 'm')); assert.ok(!existsSync(join(dir, 'calls.txt')), 'no systemctl call with --no-reload'); // A refused value writes nothing and leaves the installed unit alone. const bad = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--business', 'a b'], bin); assert.equal(bad.status, 1); assert.equal(readFileSync(target, 'utf8'), rendered.stdout); assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service']); const fresh = join(dir, 'never'); assert.equal((await run(dir, ['install', '--dir', fresh, '--no-reload', '--port', '0'], bin)).status, 1); assert.ok(!existsSync(fresh), 'a refusal creates no directory'); }); test('install without --no-reload runs daemon-reload and nothing that starts the unit', async (t) => { const dir = tmp(t); const bin = standIn(dir); const r = await run(dir, ['install', '--dir', join(dir, 'units')], bin); assert.equal(r.status, 0, r.stderr); assert.match(r.stdout, /^daemon-reload done$/m); assert.equal(readFileSync(join(dir, 'calls.txt'), 'utf8'), '--user daemon-reload\n'); }); test('uninstall refuses while the unit is active, and removes it once it is not', async (t) => { const dir = tmp(t); const unitDir = join(dir, 'units'); const target = join(unitDir, 'mosaic-webui.service'); for (const state of ['active', 'activating', 'deactivating']) { const bin = standIn(mkdtempSync(join(dir, 'si-')), `mosaic-webui.service loaded ${state} running Mosaic Console\n`); assert.equal((await run(dir, ['install', '--dir', unitDir, '--no-reload'])).status, 0); const r = await run(dir, ['uninstall', '--dir', unitDir], bin); assert.equal(r.status, 1, state); assert.match(r.stderr, /refusing: mosaic-webui\.service is still running/); assert.ok(existsSync(target), `still installed while ${state}`); } const bin = standIn(mkdtempSync(join(dir, 'si-')), 'mosaic-webui.service loaded inactive dead Mosaic Console\n'); const r = await run(dir, ['uninstall', '--dir', unitDir], bin); assert.equal(r.status, 0, r.stderr); assert.match(r.stdout, new RegExp(`^removed: ${target}$`, 'm')); assert.match(r.stdout, /^daemon-reload done$/m); assert.ok(!existsSync(target)); const absent = await run(dir, ['uninstall', '--dir', unitDir, '--no-reload'], bin); assert.equal(absent.status, 0); assert.match(absent.stdout, /^absent: /m); }); test('status prints the unit state and GET /healthz on the installed port', async (t) => { const dir = tmp(t); const unitDir = join(dir, 'units'); const bin = standIn(dir); const server = createServer((req, res) => { if (req.url === '/healthz') { res.writeHead(200, { 'content-type': 'application/json' }); res.end('{"ok":true}\n'); return; } res.writeHead(404); res.end(); }); server.listen(0, '127.0.0.1'); await once(server, 'listening'); t.after(() => server.close()); const { port } = server.address(); assert.equal((await run(dir, ['install', '--dir', unitDir, '--no-reload', '--port', String(port)])).status, 0); const r = await run(dir, ['status', '--dir', unitDir], bin); assert.equal(r.status, 0, r.stderr); assert.match(r.stdout, /^ {2}enabled: enabled$/m); assert.match(r.stdout, /^ {2}active: {2}active$/m); assert.match(r.stdout, new RegExp(`^healthz: http://127\\.0\\.0\\.1:${port}/healthz 200 \\{"ok":true\\}$`, 'm')); server.close(); await once(server, 'close'); const down = await run(dir, ['status', '--dir', unitDir], bin); assert.equal(down.status, 0, down.stderr); assert.match(down.stdout, /^healthz: \S+ unreachable \(ECONNREFUSED\)$/m); const none = await run(dir, ['status', '--dir', join(dir, 'empty')], bin); assert.match(none.stdout, /^healthz: not checked; no unit at /m); }); test('usage: an unknown verb or flag, and start or enable, exit 4', async (t) => { const dir = tmp(t); for (const args of [[], ['start'], ['stop'], ['enable'], ['restart'], ['render', '--bogus'], ['render', '--port'], ['render', '--dir', dir], ['render', '--no-reload'], ['uninstall', '--port', '7332'], ['status', '--business', 'mosaic'], ['status', '--no-reload']]) { const r = await run(dir, args); assert.equal(r.status, 4, args.join(' ')); assert.match(r.stderr, /^usage: scripts\/webui-service\.sh/); } });