build.patch againstd13b9300(brief blobf8d14d70, rev 630), candidate manifest, identity and mutant harnesses with results, gate summary. Co-Authored-By: Claude Opus 5.5 <[email protected]>
89 lines
6.5 KiB
JavaScript
89 lines
6.5 KiB
JavaScript
#!/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 <worktree>, 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') : '<absent>');
|
|
// 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 <<MSG\n') + 10, s.indexOf(' systemctl --user enable --now mosaic-webui start')); };
|
|
const msgBase = message(BASE), msgCand = message(CAND);
|
|
let same = 0, expected = 0, unexpected = 0;
|
|
cases.forEach((args, i) => {
|
|
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;
|