import test from 'node:test'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, readdirSync, symlinkSync, truncateSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { fixture, tmp } from '../../cli/tests/helpers.mjs'; import { initializeControl, transition } from '../src/state.mjs'; const TASK = new URL('../../../scripts/mosaic-task.mjs', import.meta.url).pathname; function setup(t, stop = false) { const root = tmp(t), f = fixture(root), bin = join(root, 'bin'); mkdirSync(bin); const source = new URL('../src/state.mjs', import.meta.url).href; const runtimes = new URL('../src/runtimes.mjs', import.meta.url).href; writeFileSync(join(bin, 'docker'), `#!${process.execPath} (async()=>{const args=process.argv.slice(2),fs=require('node:fs');fs.writeFileSync(${JSON.stringify(join(root, 'called'))},JSON.stringify(args));fs.writeFileSync(${JSON.stringify(join(root, 'docker-data-root-env'))},process.env.MOSAIC_DATA_ROOT??''); ${stop ? `const {transition}=await import(${JSON.stringify(source)});const {updateRuntime}=await import(${JSON.stringify(runtimes)});const dataRoot=${JSON.stringify(f.dataRoot)};const s=transition(dataRoot,{business:'acme',human:'jason',halted:true});const tag=args.find(x=>x.startsWith('mosaic.runtime='));if(tag)updateRuntime(dataRoot,tag.slice(15),{haltGeneration:s.generation});process.exit(137);` : `console.log('normal response');`} })().catch(e=>{console.error(e.message);process.exit(1)});\n`, { mode: 0o755 }); f.env.PATH = bin + ':' + f.env.PATH; const task = join(root, 'task.json'); writeFileSync(task, JSON.stringify({ taskVersion: 1, id: 'one', prompt: 'request' })); const run = args => spawnSync(process.execPath, [TASK, ...args], { env: f.env, encoding: 'utf8', timeout: 10000 }); return { root, f, task, run }; } test('task halt keeps the true Docker exit status, writes kill-switch once and leaves snapshots unchanged', t => { const f = setup(t, true), result = f.run(['run', f.task]); assert.equal(result.status, 1, result.stderr); const runs = join(f.f.dataRoot, 'runs'), [id] = readdirSync(runs); const file = join(runs, id, 'result.json'), bytes = readFileSync(file), record = JSON.parse(bytes); assert.equal(record.reason, 'kill-switch'); assert.match(record.haltGeneration, /^[a-f0-9-]{36}$/); assert.equal(record.exitCode, 137); assert.equal(record.signal, null); assert.deepEqual(readFileSync(join(runs, id, 'task.json')), readFileSync(f.task)); const retry = f.run(['retry', id]); assert.equal(retry.status, 3, retry.stderr); assert.deepEqual(readFileSync(file), bytes); assert.deepEqual(readdirSync(runs), [id]); }); test('halt refuses direct task launch before Docker or a run record; clear allows a fresh normal result', t => { const f = setup(t), actor = { business: 'acme', human: 'jason' }; initializeControl(f.f.dataRoot); const halted = transition(f.f.dataRoot, { ...actor, halted: true }); const result = f.run(['run', f.task]); assert.equal(result.status, 3, result.stderr); assert.equal(existsSync(join(f.root, 'called')), false); assert.equal(existsSync(join(f.f.dataRoot, 'runs')), false); transition(f.f.dataRoot, { ...actor, halted: false, generation: halted.generation }); const normal = f.run(['run', f.task]); assert.equal(normal.status, 0, normal.stderr); const [id] = readdirSync(join(f.f.dataRoot, 'runs')); const record = JSON.parse(readFileSync(join(f.f.dataRoot, 'runs', id, 'result.json'))); assert.equal(record.reason, null); assert.equal(record.status, 'succeeded'); assert.equal(record.response, 'normal response'); assert.equal('haltGeneration' in record, false); }); test('task launch command exposes no data root, sibling evidence, sibling workspace/session or broker socket', t => { const f = setup(t); mkdirSync(join(f.f.dataRoot, 'user'), { recursive: true }); writeFileSync(join(f.f.dataRoot, 'user', 'USER.md'), '# User\n'); mkdirSync(join(f.f.dataRoot, 'runs', 'r-sibling'), { recursive: true }); writeFileSync(join(f.f.dataRoot, 'runs', 'r-sibling', 'result.json'), '{}\n'); mkdirSync(join(f.f.dataRoot, 'workspaces', 'sibling'), { recursive: true }); mkdirSync(join(f.f.dataRoot, 'sessions', 'sibling'), { recursive: true }); mkdirSync(join(f.f.dataRoot, 'sessions', 'base'), { recursive: true }); const forkSource = join(f.f.dataRoot, 'sessions', 'base', 'turn.jsonl'); const forkBytes = Buffer.from('{"type":"message","text":"remember me"}\n'); writeFileSync(forkSource, forkBytes); const outside = join(f.root, 'outside.txt'); writeFileSync(outside, 'must not replace the fork snapshot\n'); mkdirSync(join(f.f.dataRoot, 'bus'), { recursive: true }); writeFileSync(join(f.f.dataRoot, 'bus', 'broker.sock'), 'not reachable\n'); const mission = join(f.root, 'mission.json'); writeFileSync(mission, JSON.stringify({ missionVersion: 1, id: 'm-one', objective: 'bounded', directives: ['stay bounded'] })); writeFileSync(f.task, JSON.stringify({ taskVersion: 1, id: 'one', prompt: 'request', mission, workspace: 'selected', session: 'child', sessionForkFrom: 'base', })); writeFileSync(join(f.root, 'bin', 'docker'), `#!${process.execPath} const fs=require('node:fs'),args=process.argv.slice(2); fs.renameSync(${JSON.stringify(forkSource)},${JSON.stringify(forkSource + '.original')}); fs.symlinkSync(${JSON.stringify(outside)},${JSON.stringify(forkSource)}); fs.writeFileSync(${JSON.stringify(join(f.root, 'called'))},JSON.stringify(args)); fs.writeFileSync(${JSON.stringify(join(f.root, 'docker-data-root-env'))},process.env.MOSAIC_DATA_ROOT??''); console.log('normal response'); `, { mode: 0o755 }); const result = f.run(['run', f.task]); assert.equal(result.status, 0, result.stderr); assert.equal(readFileSync(join(f.root, 'docker-data-root-env'), 'utf8'), ''); const args = JSON.parse(readFileSync(join(f.root, 'called'), 'utf8')); const text = JSON.stringify(args); assert.doesNotMatch(text, new RegExp(JSON.stringify(f.f.dataRoot + ':/var/lib/mosaic').slice(1, -1))); for (const hidden of [join(f.f.dataRoot, 'runs', 'r-sibling'), join(f.f.dataRoot, 'workspaces', 'sibling'), join(f.f.dataRoot, 'sessions', 'sibling'), join(f.f.dataRoot, 'sessions', 'base'), join(f.f.dataRoot, 'bus', 'broker.sock'), outside]) assert.equal(text.includes(hidden), false, hidden); const valuesAfter = flag => args.flatMap((v, i) => v === flag ? [args[i + 1]] : []); const volumes = valuesAfter('--volume'); assert.ok(volumes.includes(`${join(f.f.dataRoot, 'control')}:/var/lib/mosaic/control:ro`)); assert.ok(volumes.some(v => /\/runs\/r-[^/]+\/task\.json:\/var\/lib\/mosaic\/input\/task\.json:ro$/.test(v))); assert.ok(volumes.some(v => /\/runs\/r-[^/]+\/mission\.json:\/var\/lib\/mosaic\/input\/mission\.json:ro$/.test(v))); assert.ok(volumes.includes(`${join(f.f.dataRoot, 'workspaces', 'selected')}:/workspace:rw`)); assert.ok(volumes.includes(`${join(f.f.dataRoot, 'sessions', 'child')}:/var/lib/mosaic/session:rw`)); const runId = readdirSync(join(f.f.dataRoot, 'runs')).find(name => name !== 'r-sibling'); const forkSnapshot = join(f.f.dataRoot, 'runs', runId, 'session-fork.jsonl'); assert.ok(volumes.includes(`${forkSnapshot}:/var/lib/mosaic/input/session-fork.jsonl:ro`)); assert.deepEqual(readFileSync(forkSnapshot), forkBytes); assert.equal(lstatSync(forkSource).isSymbolicLink(), true); assert.equal(readlinkSync(forkSource), outside); const passedEnv = valuesAfter('-e'); for (const value of [ 'MOSAIC_TASK_FILE=/var/lib/mosaic/input/task.json', 'MOSAIC_MISSION_FILE=/var/lib/mosaic/input/mission.json', 'MOSAIC_WORKSPACE=/workspace', 'MOSAIC_SESSION_DIR=/var/lib/mosaic/session', 'MOSAIC_SESSION_FORK=/var/lib/mosaic/input/session-fork.jsonl', 'MOSAIC_SYSTEM_PROMPT_FILE=/var/lib/mosaic/private/system-prompt.md', ]) assert.ok(passedEnv.includes(value), value); }); test('newest session fork symlink refuses before Docker without copying its target', t => { const f = setup(t), sourceDir = join(f.f.dataRoot, 'sessions', 'base'); mkdirSync(sourceDir, { recursive: true }); const outside = join(f.root, 'outside.txt'); writeFileSync(outside, 'host target bytes\n'); writeFileSync(join(sourceDir, 'a-older.jsonl'), '{"safe":"older"}\n'); symlinkSync(outside, join(sourceDir, 'z-newest.jsonl')); writeFileSync(f.task, JSON.stringify({ taskVersion: 1, id: 'one', prompt: 'request', session: 'child', sessionForkFrom: 'base', })); const result = f.run(['run', f.task]); assert.equal(result.status, 4, result.stderr); assert.match(result.stderr, /session fork source must be a regular, non-symbolic-link file/); assert.equal(existsSync(join(f.root, 'called')), false); const [runId] = readdirSync(join(f.f.dataRoot, 'runs')); assert.deepEqual(readdirSync(join(f.f.dataRoot, 'runs', runId)), ['task.json']); assert.equal(readFileSync(join(f.f.dataRoot, 'runs', runId, 'task.json'), 'utf8').includes('host target bytes'), false); }); test('oversized session fork refuses before Docker or snapshot publication', t => { const f = setup(t), sourceDir = join(f.f.dataRoot, 'sessions', 'base'); mkdirSync(sourceDir, { recursive: true }); const source = join(sourceDir, 'turn.jsonl'); writeFileSync(source, ''); truncateSync(source, 16 * 1024 * 1024 + 1); writeFileSync(f.task, JSON.stringify({ taskVersion: 1, id: 'one', prompt: 'request', session: 'child', sessionForkFrom: 'base', })); const result = f.run(['run', f.task]); assert.equal(result.status, 4, result.stderr); assert.match(result.stderr, /session fork source exceeds 16777216 bytes/); assert.equal(existsSync(join(f.root, 'called')), false); const [runId] = readdirSync(join(f.f.dataRoot, 'runs')); assert.equal(existsSync(join(f.f.dataRoot, 'runs', runId, 'session-fork.jsonl')), false); }); for (const [script, args] of [['hello.sh', []], ['verify.sh', []], ['agent.sh', ['fixture']]]) { test(`halt refuses ${script} before Docker, credentials or release alignment`, t => { const f = setup(t); initializeControl(f.f.dataRoot); transition(f.f.dataRoot, { business: 'acme', human: 'jason', halted: true }); const r = spawnSync('bash', [new URL('../../../scripts/' + script, import.meta.url).pathname, ...args], { env: f.f.env, encoding: 'utf8', timeout: 5000 }); assert.equal(r.status, 3, r.stderr); assert.match(r.stderr, /processing-halted/); assert.equal(existsSync(join(f.root, 'called')), false); }); } for (const starter of ['task', 'container-cli']) test(`a nonzero Docker client exit retains its unresolved runtime receipt (${starter})`, t => { const f = setup(t); writeFileSync(join(f.root, 'bin', 'docker'), `#!${process.execPath}\nprocess.exit(137);\n`, { mode: 0o755 }); const result = starter === 'task' ? f.run(['run', f.task]) : spawnSync(process.execPath, [new URL('../src/container-cli.mjs', import.meta.url).pathname, 'request'], { env: f.f.env, encoding: 'utf8', timeout: 10000 }); assert.equal(result.status, starter === 'task' ? 1 : 137, result.stderr); const files = readdirSync(join(f.f.dataRoot, 'control', 'runtimes')); assert.equal(files.length, 1, 'a failed Docker client cannot prove the container stopped'); const runtime = JSON.parse(readFileSync(join(f.f.dataRoot, 'control', 'runtimes', files[0]))); assert.notEqual(runtime.phase, 'ended'); });