#!/usr/bin/env node // Dewey row 83: the base script (d13b9300) and the candidate, run side by side // in one scratch worktree (same REPO, same template) on every argument list // the base service tests use, without --send. Expected: stdout, stderr and // exit status identical, and every installed file identical, except the usage // text on exit 4 (it names [--send]), status's new send: line, and the // install message's start wording (lead decision 102). // Usage: identity.mjs , where scripts/webui-service-base.sh is the // base script and scripts/webui-service.sh the candidate. Nothing here reaches // the real systemd: install and uninstall run with --no-reload, status with a // stand-in systemctl. import { spawnSync } from 'node:child_process'; import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; const WT = process.argv[2]; const BASE = join(WT, 'scripts', 'webui-service-base.sh'); const CAND = join(WT, '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']; const scratch = mkdtempSync(join(tmpdir(), 'r83-identity-')); const bin = join(scratch, 'bin'); mkdirSync(bin); writeFileSync(join(bin, 'systemctl'), '#!/bin/sh\ncase "$*" in *is-enabled*) echo enabled ;; *is-active*) echo inactive ;; esac\nexit 0\n'); chmodSync(join(bin, 'systemctl'), 0o755); 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]; }; const U = join(scratch, 'units'); const cases = [ [], ['--port', '7340', '--business', 'mosaic.dev_1', '--board', 'http://127.0.0.1:7331', ...PREVIEW], PREVIEW, ['--business', 'mosaic'], [...PREVIEW, '--business', 'mosaic'], PREVIEW.slice(0, 2), PREVIEW.slice(2, 6), [...PREVIEW.slice(0, 2), ...PREVIEW.slice(4, 6)], ].map((a) => ['render', ...a]); for (const flag of ['--business', '--board', '--public-host', '--auth-header', '--allow-user']) { for (const bad of [' x', '%n', '\n', '$HOME', '"', '\\', '|', '&']) { cases.push(['render', ...set(flag, flag === '--board' ? `http://127.0.0.1:7331${bad}` : `a${bad}b`)]); } } for (const port of ['0', '65536', '7a', '', '-1', '999999', '7332', '65535']) cases.push(['render', '--port', 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')]) cases.push(['render', ...args]); for (const args of [[], ['start'], ['stop'], ['enable'], ['restart'], ['render', '--bogus'], ['render', '--port'], ['render', '--dir', scratch], ['render', '--no-reload'], ['uninstall', '--port', '7332'], ['status', '--business', 'mosaic'], ['status', '--no-reload'], ['--help']]) cases.push(args); // Install, then the same again (unchanged), a refusal over it, then uninstall. cases.push(['install', '--dir', U, '--no-reload', '--business', 'mosaic'], ['install', '--dir', U, '--no-reload', '--business', 'mosaic'], ['install', '--dir', U, '--no-reload', '--business', 'a b'], ['install', '--dir', join(scratch, 'never'), '--no-reload', '--port', '0'], ['install', '--dir', U, '--no-reload', ...PREVIEW, '--port', '7340'], ['status', '--dir', U], ['uninstall', '--dir', U, '--no-reload'], ['uninstall', '--dir', U, '--no-reload'], ['status', '--dir', U]); const env = { PATH: `${bin}:${NODE_DIR}:/usr/bin:/bin`, HOME: scratch, XDG_CONFIG_HOME: join(scratch, 'config') }; const files = (dir) => (existsSync(dir) ? readdirSync(dir).sort().map((f) => `${f}\n${readFileSync(join(dir, f), 'utf8')}`).join('\n') : ''); // Each script runs the whole sequence from a clean scratch, so install state carries the same way. function sequence(script) { rmSync(U, { recursive: true, force: true }); rmSync(join(scratch, 'never'), { recursive: true, force: true }); return cases.map((args) => { const r = spawnSync(script, args, { env, encoding: 'utf8' }); return { status: r.status, stdout: r.stdout, stderr: r.stderr, units: files(U), never: existsSync(join(scratch, 'never')) }; }); } const base = sequence(BASE); const cand = sequence(CAND); const usageBase = readFileSync(BASE, 'utf8').match(/^ FLAGS: .*"$/m)[0].slice(0, -1); const usageCand = readFileSync(CAND, 'utf8').match(/^ FLAGS: .*"$/m)[0].slice(0, -1); // The install message's lines before its command list. const message = (script) => { const s = readFileSync(script, 'utf8'); return s.slice(s.indexOf('cat < { const b = base[i], c = cand[i]; const label = JSON.stringify(args).replaceAll(scratch, '$S'); if (JSON.stringify(b) === JSON.stringify(c)) { same++; return; } const usageOnly = b.status === 4 && c.status === 4 && JSON.stringify({ ...b, stderr: b.stderr.replace(usageBase, usageCand) }) === JSON.stringify(c); const helpOnly = args[0] === '--help' && JSON.stringify({ ...b, stdout: b.stdout.replace(usageBase, usageCand) }) === JSON.stringify(c); const bl = b.stdout.split('\n'), cl = c.stdout.split('\n'); const sendLineOnly = args[0] === 'status' && b.status === c.status && b.stderr === c.stderr && b.units === c.units && JSON.stringify(cl.filter((l) => !/^send: off \(read-only\)$/.test(l))) === JSON.stringify(bl) && cl.length === bl.length + 1; const messageOnly = args[0] === 'install' && b.status === 0 && msgBase !== msgCand && b.stdout.includes(msgBase) && JSON.stringify({ ...b, stdout: b.stdout.replace(msgBase, msgCand) }) === JSON.stringify(c); if (messageOnly) { expected++; console.log(`start wording only ${label}`); return; } if (usageOnly || helpOnly) { expected++; console.log(`usage text only ${label}`); return; } if (sendLineOnly) { expected++; console.log(`send: line only ${label}: + ${cl.find((l) => l.startsWith('send:'))}`); return; } unexpected++; console.log(`DIFFERS ${label}\n base ${JSON.stringify(b)}\n cand ${JSON.stringify(c)}`); }); console.log(`${cases.length} cases: ${same} identical, ${expected} expected differences, ${unexpected} unexpected`); rmSync(scratch, { recursive: true, force: true }); process.exitCode = unexpected ? 1 : 0;