Files
stack/packages/processing-control/tests/task.test.mjs
T

166 lines
11 KiB
JavaScript

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');
});