Files
stack/scripts/test-rocko-launch.mjs
T
jason.woltjeandClaude Fable 5.1 17153fe140 control board: stale registrations and a launch-test leak (#1504)
Two defects in 69f99323, reported by the professor session and verified.

The darkwing launch test's flock-contention spawn ran without the fixture
config, so launch.sh re-entered scripts/mosaic against the real data root
and wrote fixture records for darkwing, dewey and filbert there. That
spawn now names the fixture config, and both launch test files set
MOSAIC_CONFIG to a nonexistent path and clear MOSAIC_LAUNCH_REGISTERED
process-wide, so a spawn that forgets fails instead of polluting.

A registration is written before the launch script's own checks, so a
refused launch left a record with a dead pid that the board honoured. The
scanner now probes the recorded pid (pidAlive, signal 0); a gone pid makes
the record stale: still on the Registered line with alive false, derived
task, project and workspace win, index gains registrationStale, CLI
summary gains a stale count.

Fleet launchers marked not planned per Jason. Board 90/90, seat 15/15,
launch scripts 5/5. Sonnet review APPROVED.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-12 11:04:49 -05:00

101 lines
6.2 KiB
JavaScript

// Offline Claude launcher checks; no authentication or model calls.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, cpSync, existsSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { spawnSync } from 'node:child_process';
const source = resolve(import.meta.dirname, '..');
// Every spawn below must name the fixture config explicitly. Anything that
// forgets inherits these and fails instead of registering into a real data root.
process.env.MOSAIC_CONFIG = join(tmpdir(), 'mosaic-launch-test-no-config.json');
process.env.MOSAIC_LAUNCH_REGISTERED = '';
test('Rocko uses Sonnet, isolated resume IDs, current context and launch refusals', () => {
const root = mkdtempSync(join(tmpdir(), 'rocko launch '));
try {
for (const dir of ['agents', 'scripts', 'contracts', 'user', 'bin']) mkdirSync(join(root, dir), { recursive: true });
cpSync(join(source, 'agents/rocko'), join(root, 'agents/rocko'), { recursive: true });
for (const file of ['contracts/CONSTITUTION.md', 'contracts/STANDARDS.md', 'user/USER.md', 'AGENTS.md']) {
writeFileSync(join(root, file), `fixture ${file}\n`);
}
writeFileSync(join(root, 'scripts/common.sh'), 'load_config() { MOSAIC_DATA_ROOT="$PWD"; }\n');
// Seat registration (packages/seat): the fixture is its own data root.
cpSync(join(source, 'scripts/mosaic'), join(root, 'scripts/mosaic'));
cpSync(join(source, 'packages/seat/src'), join(root, 'packages/seat/src'), { recursive: true });
mkdirSync(join(root, '.git'));
writeFileSync(join(root, 'config.json'), JSON.stringify({ configVersion: 1, dataRoot: root }));
const registration = join(root, 'seats/repo/rocko/registration.json');
writeFileSync(join(root, 'bin/claude'), `#!/usr/bin/env node
const fs = require('node:fs');
if (process.argv[2] === '--version') { console.log('fixture Claude'); process.exit(0); }
fs.writeFileSync('capture.json', JSON.stringify({args: process.argv.slice(2), cwd: process.cwd(), agent: process.env.MOSAIC_AGENT_NAME, incarnation: process.env.MOSAIC_LAUNCH_INCARNATION, registered: process.env.MOSAIC_LAUNCH_REGISTERED}));
if (process.env.FAKE_CLAUDE_FAIL) process.exit(7);
`, { mode: 0o755 });
const env = { ...process.env, PATH: `${join(root, 'bin')}:${process.env.PATH}`, MOSAIC_LAUNCH_INCARNATION: 'do-not-inherit', MOSAIC_CONFIG: join(root, 'config.json'), MOSAIC_LAUNCH_REGISTERED: '' };
const run = (args = '', tty = true, extraEnv = {}) => spawnSync(tty ? 'script' : 'bash', tty
? ['-q', '-e', '-c', `bash agents/rocko/launch.sh ${args}`, '/dev/null']
: [join(root, 'agents/rocko/launch.sh'), ...args.split(' ').filter(Boolean)],
{ cwd: tty ? root : tmpdir(), env: { ...env, ...extraEnv }, encoding: 'utf8', timeout: 10000 });
const ok = result => assert.equal(result.status, 0, result.stdout + result.stderr);
const capture = () => JSON.parse(readFileSync(join(root, 'capture.json')));
const state = join(root, '.pi/state/rocko');
ok(run('--check', false));
assert.equal(existsSync(state), false);
assert.equal(existsSync(join(root, 'capture.json')), false);
assert.equal(existsSync(join(root, 'seats')), false);
ok(run());
let captured = capture();
assert.equal(captured.cwd, root);
assert.equal(captured.agent, 'rocko');
// Registered before Claude started; the record names the Claude harness.
assert.equal(captured.registered, registration);
const record = JSON.parse(readFileSync(registration, 'utf8'));
assert.equal(record.seat, 'rocko');
assert.equal(record.harness, 'claude-code');
assert.equal(record.layout, 'repo');
assert.equal(record.workspace, root);
assert.ok(Number.isInteger(record.pid) && record.pid > 0);
assert.equal(captured.incarnation, undefined);
assert.equal(captured.args[captured.args.indexOf('--model') + 1], 'sonnet');
assert.equal(captured.args[captured.args.indexOf('--name') + 1], 'Rocko');
assert.equal(captured.args.includes('--continue'), false);
assert.equal(captured.args.includes('--dangerously-skip-permissions'), false);
assert.equal(captured.args.includes('--agent'), false);
const firstId = readFileSync(join(state, 'session-id'), 'utf8').trim();
assert.equal(captured.args[captured.args.indexOf('--session-id') + 1], firstId);
const prompt = captured.args[captured.args.indexOf('--append-system-prompt') + 1];
for (const value of ['You are Rocko', 'Darkwing', 'fixture AGENTS.md', 'fixture user/USER.md', 'ROCKO NATIVE DEVELOPMENT CONTEXT']) assert.ok(prompt.includes(value));
writeFileSync(join(root, 'agents/rocko/SOUL.md'), 'You are Rocko. Updated context.\n');
ok(run());
captured = capture();
assert.equal(captured.args[captured.args.indexOf('--resume') + 1], firstId);
assert.equal(captured.args.includes('--session-id'), false);
assert.match(captured.args[captured.args.indexOf('--append-system-prompt') + 1], /Updated context/);
const failed = run('', true, { FAKE_CLAUDE_FAIL: '1' });
assert.equal(failed.status, 7);
assert.equal(readFileSync(join(state, 'session-id'), 'utf8').trim(), firstId);
ok(run('--fresh'));
const secondId = readFileSync(join(state, 'session-id'), 'utf8').trim();
assert.notEqual(secondId, firstId);
assert.equal(capture().args.includes('--resume'), false);
writeFileSync(join(state, 'session-id'), 'corrupt\n');
assert.notEqual(run().status, 0);
ok(run('--fresh'));
assert.notEqual(run('--unexpected', false).status, 0);
assert.notEqual(run('--user missing.md --check', false).status, 0);
assert.notEqual(run('', false).status, 0);
const locked = spawnSync('script', ['-q', '-e', '-c',
'flock .pi/state/rocko/launch.lock bash agents/rocko/launch.sh --fresh', '/dev/null'],
{ cwd: root, env, encoding: 'utf8', timeout: 10000 });
assert.notEqual(locked.status, 0);
assert.match(locked.stdout, /another rocko TUI/);
writeFileSync(join(root, 'alternate.md'), 'alternate user context\n');
ok(run('--fresh --user alternate.md'));
captured = capture();
assert.match(captured.args[captured.args.indexOf('--append-system-prompt') + 1], /alternate user context/);
} finally {
rmSync(root, { recursive: true, force: true });
}
});