webui: four refusal cases in the Console service tests (#1592)

Row 98, the row 77 follow-up. Researcher's build (agents/researcher/work/queue-98/), approved by Filbert
in round 1 on manifest 352a3034 (comment 28110). Tests only: three new mv and rename cases in
pinned-copy.test.mjs beside the fixture repo they need, and added assertions in service.test.mjs.
Sage's gate rerun on 47a8190d: all 14 package node suites and all 9 scripts/test-*.sh green, webui 107 pass.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-13 09:32:45 -05:00
co-authored by Claude Opus 5.5
parent 097dec3314
commit bb45f75ed2
2 changed files with 71 additions and 2 deletions
+47
View File
@@ -169,6 +169,53 @@ test('a copy that loses the rename race leaves no read-only temp directory behin
assert.deepEqual(readdirSync(copies), [head], 'the temp directory is removed'); assert.deepEqual(readdirSync(copies), [head], 'the temp directory is removed');
}); });
// Row 98 (#1592). The rename's two other outcomes: it fails and no target
// appears (a copy that was never written must not print exists:), it
// meets a target that holds files (mv fails and nothing lands in the
// target), and it meets an empty target directory. Only the last tells mv -T
// from plain mv: the temp copy's directories are 0555, so plain mv cannot move
// it into a target at all (Permission denied) and fails exactly as -T does on a
// full one, but -T may replace an empty directory by rename.
async function copyWithMv(t, mvBody) {
const { dir, script, head } = fixtureRepo(t);
const copies = join(dir, 'copies');
const bin = join(dir, 'bin');
mkdirSync(bin);
writeFileSync(join(bin, 'mv'), `#!/bin/sh\n${mvBody}\n`, { mode: 0o755 });
const env = { PATH: `${bin}:${NODE_DIR}:/usr/bin:/bin`, HOME: dir, XDG_CONFIG_HOME: join(dir, 'config') };
const child = spawn(script, ['copy', 'refactor', '--copies', copies], { 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 { copies, target: join(copies, head), head, status, stdout, stderr };
}
test('a copy whose rename fails with no target refuses, writes no copy and leaves no temp directory', async (t) => {
const r = await copyWithMv(t, 'exit 1');
assert.equal(r.status, 1);
assert.equal(r.stderr, `webui-service: could not rename the copy into ${r.target}\n`);
assert.equal(r.stdout, '');
assert.deepEqual(readdirSync(r.copies), [], 'no target and no temp directory');
});
test('a copy that meets a target holding files says exists: and moves nothing into it', async (t) => {
// The stand-in makes the target appear with a file in it, then runs the real mv with the script's flags.
const r = await copyWithMv(t, 'for a; do t="$a"; done\nmkdir "$t"\n: > "$t/other"\nexec /usr/bin/mv "$@"');
assert.equal(r.status, 0, r.stderr);
assert.equal(r.stdout, `exists: ${r.target}\n`);
assert.deepEqual(readdirSync(r.copies), [r.head], 'the temp directory is removed');
assert.deepEqual(readdirSync(r.target), ['other'], 'nothing was moved into the target');
});
test('a copy that meets an empty target directory replaces it by rename, not by moving into it', async (t) => {
const r = await copyWithMv(t, 'for a; do t="$a"; done\nmkdir "$t"\nexec /usr/bin/mv "$@"');
assert.equal(r.status, 0, r.stderr);
assert.equal(r.stdout, `copied: ${r.target}\ninstall it with: scripts/webui-service.sh install --copy ${r.head}, with the unit's flags\n`);
assert.deepEqual(readdirSync(r.copies), [r.head], 'the temp directory is gone');
assert.ok(existsSync(join(r.target, 'packages', 'webui', 'README.md')), 'the copy is the target itself');
});
// serve from a copy: /healthz, the config check, roles and RELEASE from the // serve from a copy: /healthz, the config check, roles and RELEASE from the
// copy, and the queue read from --checkout or, without it, from the copy. // copy, and the queue read from --checkout or, without it, from the copy.
async function serve(t, cli, args, env) { async function serve(t, cli, args, env) {
+24 -2
View File
@@ -71,19 +71,22 @@ const BUS_UNITS = '[email protected] loaded active running Mosaic bus host
// to stderr. The bus calls install makes are answered apart: `busUnits` for // to stderr. The bus calls install makes are answered apart: `busUnits` for
// the mosaic-bus@ list-units, and `bus` for show, by default a bus started // the mosaic-bus@ list-units, and `bus` for show, by default a bus started
// from the run's own copy ($HOME, as run() sets it). `busFails` makes both // from the run's own copy ($HOME, as run() sets it). `busFails` makes both
// bus calls fail. // bus calls fail. `busOther` answers show for [email protected]
function standIn(dir, lines = '', { listFails = false, bus = null, busUnits = BUS_UNITS, busFails = false } = {}) { // apart from `bus`, so a second loaded unit can differ from the first.
function standIn(dir, lines = '', { listFails = false, bus = null, busUnits = BUS_UNITS, busFails = false, busOther = null } = {}) {
const bin = join(dir, 'bin'); const bin = join(dir, 'bin');
mkdirSync(bin); mkdirSync(bin);
writeFileSync(join(dir, 'list-units.txt'), lines); writeFileSync(join(dir, 'list-units.txt'), lines);
writeFileSync(join(dir, 'bus-units.txt'), busUnits); writeFileSync(join(dir, 'bus-units.txt'), busUnits);
if (bus !== null) writeFileSync(join(dir, 'bus-show.txt'), bus); if (bus !== null) writeFileSync(join(dir, 'bus-show.txt'), bus);
if (busOther !== null) writeFileSync(join(dir, 'bus-show-other.txt'), busOther);
const fail = 'echo "Failed to connect to bus" >&2; exit 1'; const fail = 'echo "Failed to connect to bus" >&2; exit 1';
const show = bus !== null ? `cat "${dir}/bus-show.txt"` : `r="$HOME/.local/share/mosaic-console/${COPY_ID}"; printf '%s' "${busShow('$r')}"`; const show = bus !== null ? `cat "${dir}/bus-show.txt"` : `r="$HOME/.local/share/mosaic-console/${COPY_ID}"; printf '%s' "${busShow('$r')}"`;
writeFileSync(join(bin, 'systemctl'), `#!/bin/sh writeFileSync(join(bin, 'systemctl'), `#!/bin/sh
echo "$*" >> "${dir}/calls.txt" echo "$*" >> "${dir}/calls.txt"
case "$*" in case "$*" in
*list-units*mosaic-bus@*) ${busFails ? fail : `cat "${dir}/bus-units.txt"`} ;; *list-units*mosaic-bus@*) ${busFails ? fail : `cat "${dir}/bus-units.txt"`} ;;
*show*[email protected]*) ${busFails ? fail : busOther !== null ? `cat "${dir}/bus-show-other.txt"` : show} ;;
*show*mosaic-bus@*) ${busFails ? fail : show} ;; *show*mosaic-bus@*) ${busFails ? fail : show} ;;
*list-units*) ${listFails ? fail : `cat "${dir}/list-units.txt"`} ;; *list-units*) ${listFails ? fail : `cat "${dir}/list-units.txt"`} ;;
*is-enabled*) echo enabled ;; *is-enabled*) echo enabled ;;
@@ -538,10 +541,21 @@ test('install refuses unless the bus unit runs from the same copy, naming both p
assert.equal((await install(s.bin)).status, 0); assert.equal((await install(s.bin)).status, 0);
assert.match(readFileSync(join(s.d, 'calls.txt'), 'utf8'), /show mosaic-bus@other\.service -p LoadState -p ExecStart/); assert.match(readFileSync(join(s.d, 'calls.txt'), 'utf8'), /show mosaic-bus@other\.service -p LoadState -p ExecStart/);
rmSync(unitDir, { recursive: true }); rmSync(unitDir, { recursive: true });
// The second loaded unit is off the copy: refused, naming the second unit and its path.
const two = `${BUS_UNITS}[email protected] loaded active running Mosaic bus host (other)\n`;
s = sys({ busUnits: two, bus: busShow(copy), busOther: busShow(REPO) });
refused(await install(s.bin), `[email protected] runs from ${REPO}/scripts/mosaic, not the copy ${copy}; the bus accepts only its own human-cli.mjs, so every Console bus call would fail human-required until the bus runs from the same copy`);
assert.match(readFileSync(join(s.d, 'calls.txt'), 'utf8'), /show mosaic-bus@acme\.service -p LoadState -p ExecStart\n.*show mosaic-bus@other\.service -p LoadState -p ExecStart/s);
// No bus unit installed for the business: refused. // No bus unit installed for the business: refused.
s = sys({ bus: 'LoadState=not-found\nExecStart=\n' }); s = sys({ bus: 'LoadState=not-found\nExecStart=\n' });
refused(await install(s.bin, ['--business', 'acme']), `no bus unit [email protected] is installed (LoadState not-found); the Console on the copy ${copy} needs its bus on that copy`); refused(await install(s.bin, ['--business', 'acme']), `no bus unit [email protected] is installed (LoadState not-found); the Console on the copy ${copy} needs its bus on that copy`);
// Only loaded counts: a unit systemd shows at any other LoadState is not the bus, even with an
// ExecStart on the copy.
for (const state of ['bad-setting', 'error', 'masked']) {
s = sys({ bus: busShow(copy).replace('LoadState=loaded', `LoadState=${state}`) });
refused(await install(s.bin, ['--business', 'acme']), `no bus unit [email protected] is installed (LoadState ${state}); the Console on the copy ${copy} needs its bus on that copy`);
}
// No --business and no mosaic-bus@ unit loaded: refused. // No --business and no mosaic-bus@ unit loaded: refused.
s = sys({ busUnits: '' }); s = sys({ busUnits: '' });
refused(await install(s.bin), `no mosaic-bus@ unit is loaded, and the Console on the copy ${copy} needs its bus on that copy; name the business with --business ID`); refused(await install(s.bin), `no mosaic-bus@ unit is loaded, and the Console on the copy ${copy} needs its bus on that copy; name the business with --business ID`);
@@ -644,6 +658,14 @@ test('status names the copy and whether refactor has moved past it, and says whe
r = await run(dir, ['status', '--dir', unitDir], bin); r = await run(dir, ['status', '--dir', unitDir], bin);
assert.match(r.stdout, new RegExp(`^copy: none; the unit runs from ${esc(REPO)}$`, 'm')); assert.match(r.stdout, new RegExp(`^copy: none; the unit runs from ${esc(REPO)}$`, 'm'));
assert.ok(!/^refactor:/m.test(r.stdout)); assert.ok(!/^refactor:/m.test(r.stdout));
// Only a whole 40-hex last part is a copy: 40 hex plus a character, or 41 hex, is not.
for (const last of [`${COPY_ID}x`, `x${COPY_ID}`, `${COPY_ID}0`]) {
unit(join(dir, 'copies', last));
r = await run(dir, ['status', '--dir', unitDir], bin);
assert.equal(r.status, 0, r.stderr);
assert.match(r.stdout, new RegExp(`^copy: none; the unit runs from ${esc(join(dir, 'copies', last))}$`, 'm'), last);
assert.ok(!/^refactor:/m.test(r.stdout), last);
}
// status reads; it calls nothing that changes the unit. // status reads; it calls nothing that changes the unit.
assert.deepEqual(readFileSync(join(dir, readdirSync(dir).find((d) => d.startsWith('si-')), 'calls.txt'), 'utf8').split('\n').filter(Boolean).filter((c) => !/^--user (is-enabled|is-active|show) mosaic-webui\.service( |$)/.test(c)), []); assert.deepEqual(readFileSync(join(dir, readdirSync(dir).find((d) => d.startsWith('si-')), 'calls.txt'), 'utf8').split('\n').filter(Boolean).filter((c) => !/^--user (is-enabled|is-active|show) mosaic-webui\.service( |$)/.test(c)), []);
}); });