// Offline launcher checks. Fake Pi captures argv; script(1) supplies a real PTY. 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, '..'); for (const agent of ['darkwing', 'dewey', 'filbert']) test(`${agent} launch context, resources, session recovery, and refusals`, () => { const root = mkdtempSync(join(tmpdir(), `${agent} launch `)); try { for (const dir of ['agents', 'scripts', 'contracts', 'user', 'node_modules/.bin', 'extensions/goal']) { mkdirSync(join(root, dir), { recursive: true }); } // Copy only runtime inputs. Agent work/scratch may be large or actively // authored and is unrelated to isolated launcher verification. mkdirSync(join(root, `agents/${agent}`), { recursive: true }); for (const file of ['launch.sh', 'SOUL.md', 'CONTEXT.md', 'validate-sessions.mjs']) { cpSync(join(source, `agents/${agent}`, file), join(root, `agents/${agent}`, file)); } assert.equal(existsSync(join(root, `agents/${agent}/work`)), false); assert.equal(existsSync(join(root, `agents/${agent}/scratch`)), false); cpSync(join(source, 'scripts/agent-host-dev.sh'), join(root, 'scripts/agent-host-dev.sh')); cpSync(join(source, 'scripts/agent.sh'), join(root, 'scripts/agent.sh')); cpSync(join(source, 'skills'), join(root, 'skills'), { recursive: true }); writeFileSync(join(root, 'package.json'), JSON.stringify({ dependencies: { '@earendil-works/pi-coding-agent': '0.84.4' } })); 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, 'extensions/goal/index.ts'), '// fixture\n'); writeFileSync(join(root, 'scripts/common.sh'), 'load_config() { MOSAIC_DATA_ROOT="$PWD"; MOSAIC_PROVIDER=test; MOSAIC_MODEL=test-model; MOSAIC_ADAPTER=pi; }\n'); writeFileSync(join(root, 'scripts/sync-dev-extensions.sh'), '#!/bin/sh\nexit 0\n', { mode: 0o755 }); writeFileSync(join(root, 'node_modules/.bin/pi'), `#!/usr/bin/env node const fs = require('node:fs'); if (process.argv[2] === '--version') { console.log('0.84.4'); process.exit(0); } fs.writeFileSync('capture.json', JSON.stringify({args: process.argv.slice(2), agent: process.env.MOSAIC_AGENT_NAME, incarnation: process.env.MOSAIC_LAUNCH_INCARNATION})); `, { mode: 0o755 }); const launch = (args = '', tty = true) => spawnSync(tty ? 'script' : 'bash', tty ? ['-q', '-e', '-c', `bash agents/${agent}/launch.sh ${args}`, '/dev/null'] : [`agents/${agent}/launch.sh`, ...args.split(' ').filter(Boolean)], { cwd: root, env: { ...process.env, MOSAIC_LAUNCH_INCARNATION: 'must-not-inherit' }, encoding: 'utf8', timeout: 10000 }); const ok = (result) => assert.equal(result.status, 0, result.stdout + result.stderr); ok(launch('--check', false)); const direct = spawnSync('bash', [join(root, 'scripts/agent.sh'), '--host-dev', agent, '--check'], { cwd: tmpdir(), encoding: 'utf8', timeout: 10000 }); ok(direct); assert.match(direct.stdout, /configuration checks passed/); assert.notEqual(launch('--auth account --check', false).status, 0); assert.equal(existsSync(join(root, '.pi/state')), false); ok(launch()); let captured = JSON.parse(readFileSync(join(root, 'capture.json'))); assert.equal(captured.incarnation, undefined); assert.equal(captured.agent, agent); assert.equal(captured.args[captured.args.indexOf('--provider') + 1], agent === 'filbert' ? 'openai-codex' : 'test'); assert.equal(captured.args[captured.args.indexOf('--model') + 1], agent === 'filbert' ? 'gpt-6-astra' : 'test-model'); if (agent === 'filbert') assert.equal(captured.args[captured.args.indexOf('--thinking') + 1], 'low'); assert.equal(captured.args[captured.args.indexOf('--session-dir') + 1], join(root, `.pi/state/${agent}/sessions`)); assert.equal(captured.args.includes('--continue'), false); assert.ok(captured.args.includes('--no-context-files')); assert.ok(captured.args.includes('--no-skills')); assert.equal(captured.args.filter(arg => arg === '--skill').length, 10); assert.ok(captured.args[captured.args.indexOf('--tools') + 1].includes('goal_report')); const firstPrompt = captured.args[captured.args.indexOf('--append-system-prompt') + 1]; const context = readFileSync(firstPrompt, 'utf8'); assert.match(context, /fixture contracts\/CONSTITUTION.md/); assert.match(context, /fixture user\/USER.md/); assert.match(context, /fixture AGENTS.md/); assert.match(context, new RegExp(`You are ${agent[0].toUpperCase() + agent.slice(1)}`)); const sessions = join(root, `.pi/state/${agent}/sessions`); const session = join(sessions, 'history.jsonl'); const history = JSON.stringify({ type: 'session', version: 3, id: 'test', cwd: root, timestamp: new Date().toISOString() }) + '\n'; writeFileSync(session, history); ok(launch()); captured = JSON.parse(readFileSync(join(root, 'capture.json'))); assert.ok(captured.args.includes('--continue')); assert.notEqual(captured.args[captured.args.indexOf('--append-system-prompt') + 1], firstPrompt); ok(launch('--fresh')); assert.equal(JSON.parse(readFileSync(join(root, 'capture.json'))).args.includes('--continue'), false); assert.equal(readFileSync(session, 'utf8'), history); writeFileSync(session, 'broken JSON\n'); assert.notEqual(launch().status, 0); ok(launch('--fresh')); assert.notEqual(launch('--user missing.md --check', false).status, 0); assert.notEqual(launch('--unexpected', false).status, 0); assert.notEqual(launch('', false).status, 0); const locked = spawnSync('script', ['-q', '-e', '-c', `flock .pi/state/${agent}/launch.lock bash agents/${agent}/launch.sh --fresh`, '/dev/null'], { cwd: root, encoding: 'utf8', timeout: 10000 }); assert.notEqual(locked.status, 0); assert.match(locked.stdout, new RegExp(`another ${agent} TUI`)); writeFileSync(join(root, 'alternate.md'), 'alternate user context\n'); ok(launch('--fresh --user alternate.md')); captured = JSON.parse(readFileSync(join(root, 'capture.json'))); assert.match(readFileSync(captured.args[captured.args.indexOf('--append-system-prompt') + 1], 'utf8'), /alternate user context/); } finally { rmSync(root, { recursive: true, force: true }); } }); test('default agent entry retains container setup and refuses missing seat/auth', () => { const root = mkdtempSync(join(tmpdir(), 'mosaic container entry ')); try { for (const dir of ['scripts', 'bin', 'runtime/user']) mkdirSync(join(root, dir), { recursive: true }); cpSync(join(source, 'scripts/agent.sh'), join(root, 'scripts/agent.sh')); // All runtime effects remain in this fixture; Docker and release helpers are doubles. writeFileSync(join(root, 'scripts/common.sh'), ` load_config() { MOSAIC_DEV_DIR="$PWD/runtime"; MOSAIC_ADAPTER=pi; MOSAIC_MODEL=test; echo config >> trace; } load_release() { echo release >> trace; } bootstrap_runtime_dir() { echo bootstrap >> trace; } ensure_release_aligned() { echo alignment >> trace; } `); writeFileSync(join(root, 'runtime/user/USER.md'), 'fixture user\n'); writeFileSync(join(root, 'bin/docker'), `#!/usr/bin/env node require('node:fs').writeFileSync('docker.json', JSON.stringify({ args: process.argv.slice(2), session: process.env.MOSAIC_SESSION_DIR, workspace: process.env.MOSAIC_WORKSPACE, tools: process.env.MOSAIC_TOOLS, name: process.env.MOSAIC_AGENT_NAME, interactive: process.env.MOSAIC_INTERACTIVE })); `, { mode: 0o755 }); const env = { ...process.env, PATH: join(root, 'bin') + ':' + process.env.PATH }; for (const name of ['MOSAIC_AGENTS_DIR', 'MOSAIC_ROLES_DIR', 'MOSAIC_SKILLS', 'MOSAIC_AGENT_ROLE']) delete env[name]; const run = (args, overrides = {}) => spawnSync('bash', [join(root, 'scripts/agent.sh'), ...args], { cwd: tmpdir(), env: { ...env, ...overrides }, encoding: 'utf8', timeout: 10000 }); let result = run(['fixture', '--tools', 'read,bash']); assert.equal(result.status, 0, result.stdout + result.stderr); assert.deepEqual(readFileSync(join(root, 'trace'), 'utf8').trim().split('\n'), ['config', 'release', 'bootstrap', 'alignment']); assert.deepEqual(JSON.parse(readFileSync(join(root, 'docker.json'))), { args: ['compose', 'run', '--rm', 'mosaic-agent'], session: '/var/lib/mosaic/sessions/agent-fixture', workspace: '/var/lib/mosaic/workspaces/fixture', tools: 'read,bash', name: 'fixture', interactive: '1' }); const captured = readFileSync(join(root, 'docker.json'), 'utf8'); result = run(['fixture'], { MOSAIC_AGENTS_DIR: join(root, 'missing-seats') }); assert.equal(result.status, 4); assert.match(result.stderr, /no seat definition/); result = run(['fixture', '--auth', 'missing']); assert.equal(result.status, 4); assert.match(result.stderr, /no mosaic-managed credential/); assert.equal(readFileSync(join(root, 'docker.json'), 'utf8'), captured); assert.equal(existsSync(join(root, '.pi')), false); } finally { rmSync(root, { recursive: true, force: true }); } });