// #1873 A2: `mosaic agents root` and `mosaic project agents`, end to end through scripts/mosaic on a // bootstrapped named instance (the org CLI's own setup, org.test.mjs). import { test } from 'node:test'; import assert from 'node:assert/strict'; import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { tmpdir } from 'node:os'; import { spawnSync } from 'node:child_process'; const repo = resolve(import.meta.dirname, '../../..'); const SECRET = 'sk-test-NOT-A-SECRET-1234'; const ABSENT = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; function instance() { const home = mkdtempSync(join(tmpdir(), 'a2cli-')); const config = join(home, '.config/mosaic-atwo/config.json'); const env = { PATH: process.env.PATH, LANG: 'C.UTF-8', TMPDIR: process.env.TMPDIR ?? tmpdir(), HOME: home, MOSAIC_CONFIG: config }; const boot = spawnSync(join(repo, 'scripts/bootstrap.sh'), ['--instance', 'atwo'], { cwd: repo, env, encoding: 'utf8' }); assert.equal(boot.status, 0, boot.stderr); const run = (args, extra = {}) => spawnSync(join(repo, 'scripts/mosaic'), args, { cwd: repo, env: { ...env, ...extra }, encoding: 'utf8' }); const ok = (...args) => { const r = run(args); assert.equal(r.status, 0, `${args.join(' ')}: ${r.stderr}`); return r.stdout; }; const file = join(home, 'alpha.json'); ok('org', 'template', 'render', 'alpha', '--fixture', 'local', '--output', file); ok('org', 'create', 'alpha', '--from', file); const project = Object.keys(JSON.parse(readFileSync(join(home, '.config/mosaic-atwo/businesses/alpha.json'), 'utf8')).projects)[0]; return { home, env, run, ok, project, configDir: join(home, '.config/mosaic-atwo') }; } test('agents root list, set, remove: each write prints its receipt, values stay out of receipts and refusals', () => { const { run, ok, configDir } = instance(); assert.deepEqual(JSON.parse(ok('agents', 'root', 'list', '--json')), { revision: ABSENT, state: 'absent', agents: [] }); assert.match(ok('agents', 'root', 'list'), /^root agents: none \(no file\)/); const added = JSON.parse(ok('agents', 'root', 'set', 'researcher', '--definition', 'research', '--var', 'model=claude-opus-5-5')); assert.equal(added.verb, 'root.agent.set'); assert.equal(added.agent, 'researcher'); assert.deepEqual(added.changed, ['added']); assert.equal(added.revisionBefore, ABSENT); const patched = run(['agents', 'root', 'set', 'researcher', '--var', 'limits.network=none', '--exclude-orgs', 'home']); assert.equal(patched.status, 0, patched.stderr); assert.deepEqual(JSON.parse(patched.stdout).changed, ['vars.limits.network', 'excludeOrgs']); assert.ok(!patched.stdout.includes('none') && !patched.stdout.includes('claude-opus')); const listed = JSON.parse(ok('agents', 'root', 'list', '--json')); assert.equal(listed.state, 'present'); assert.deepEqual(listed.agents, [{ name: 'researcher', definition: 'research', model: 'claude-opus-5-5', varKeys: ['limits.network'], excludeOrgs: ['home'] }]); // A stale --revision refuses and the file is unchanged. const before = readFileSync(join(configDir, 'agents.json'), 'utf8'); const stale = run(['agents', 'root', 'set', 'researcher', '--var', 'limits.network=api-only', '--revision', added.revisionAfter]); assert.equal(stale.status, 2); assert.match(stale.stderr, /revision-conflict/); // An invalid value refuses with the key named and the value absent. const bad = run(['agents', 'root', 'set', 'researcher', '--var', `model=${SECRET.repeat(10)}`]); assert.equal(bad.status, 2); assert.match(bad.stderr, /agents-invalid: .*model/); assert.ok(!bad.stderr.includes(SECRET), bad.stderr); const badJson = run(['agents', 'root', 'set', 'researcher', '--var-json', `limits.tools=${SECRET}`]); assert.equal(badJson.status, 4); assert.match(badJson.stderr, /var limits\.tools is not valid JSON/); assert.ok(!badJson.stderr.includes(SECRET)); const chief = run(['agents', 'root', 'set', 'boss', '--definition', 'chief-of-staff']); assert.equal(chief.status, 2); assert.match(chief.stderr, /chief-of-staff can't be a root agent/); assert.equal(readFileSync(join(configDir, 'agents.json'), 'utf8'), before); // An agent run is refused before anything is read. for (const marker of ['MOSAIC_RUN_ID', 'MOSAIC_BUS_CAP', 'MOSAIC_AGENT_RUN']) { const inside = run(['agents', 'root', 'remove', 'researcher'], { [marker]: '1' }); assert.equal(inside.status, 3, `${marker}: ${inside.stderr}`); } assert.equal(readFileSync(join(configDir, 'agents.json'), 'utf8'), before); const removed = JSON.parse(ok('agents', 'root', 'remove', 'researcher')); assert.equal(removed.verb, 'root.agent.remove'); assert.deepEqual(removed.changed, ['removed']); const receipts = readFileSync(join(configDir, 'agents-receipts.jsonl'), 'utf8'); assert.equal(receipts.trim().split('\n').length, 3); assert.ok(!receipts.includes('claude-opus') && !receipts.includes(SECRET)); // Usage refusals never echo option values. const unknown = run(['agents', 'root', 'set', 'researcher', `--${SECRET}`]); assert.equal(unknown.status, 4); assert.ok(!unknown.stderr.includes(SECRET)); }); test('a filesystem failure prints its code and a fixed line, never a host path', () => { const { run, configDir, home } = instance(); // A directory where the receipt file goes: opening it fails EISDIR, whose message holds the path. mkdirSync(join(configDir, 'agents-receipts.jsonl')); const r = run(['agents', 'root', 'set', 'researcher', '--definition', 'research']); assert.equal(r.status, 2, r.stderr); assert.match(r.stderr, /EISDIR: the operation failed; the error text is not shown/); assert.ok(!r.stderr.includes(home), r.stderr); assert.equal(existsSync(join(configDir, 'agents.json')), false); }); test('a missing, malformed or unclaimed instance config refuses before anything is read or written', () => { const verbs = [['agents', 'root', 'set', 'researcher', '--definition', 'research'], ['agents', 'root', 'list'], ['project', 'agents', 'work', '--org', 'alpha', 'exclude', 'coder']]; const check = (env, dir, why) => { for (const args of verbs) { const r = spawnSync(join(repo, 'scripts/mosaic'), args, { cwd: repo, env, encoding: 'utf8' }); assert.equal(r.status, 3, `${why} ${args.join(' ')}: ${r.stderr}`); assert.match(r.stderr, /system config problem/); } assert.equal(existsSync(join(dir, 'agents.json')), false, why); assert.equal(existsSync(join(dir, 'agents-receipts.jsonl')), false, why); }; // Missing: the instance's config directory exists, its config.json does not. const home = mkdtempSync(join(tmpdir(), 'a2cfg-')); const dir = join(home, '.config/mosaic-atwo'); mkdirSync(dir, { recursive: true, mode: 0o700 }); const env = { PATH: process.env.PATH, LANG: 'C.UTF-8', TMPDIR: process.env.TMPDIR ?? tmpdir(), HOME: home, MOSAIC_CONFIG: join(dir, 'config.json') }; check(env, dir, 'missing'); writeFileSync(join(dir, 'config.json'), '{', { mode: 0o600 }); check(env, dir, 'malformed'); // Unclaimed: a bootstrapped instance whose data root marker names another instance. const booted = instance(); const { dataRoot } = JSON.parse(readFileSync(booted.env.MOSAIC_CONFIG, 'utf8')); writeFileSync(join(dataRoot, 'instance.json'), JSON.stringify({ instanceVersion: 1, name: 'other' })); check(booted.env, booted.configDir, 'marker'); }); test('the default instance refuses the agent level verbs', () => { const home = mkdtempSync(join(tmpdir(), 'a2def-')); const env = { PATH: process.env.PATH, LANG: 'C.UTF-8', TMPDIR: process.env.TMPDIR ?? tmpdir(), HOME: home }; for (const args of [['agents', 'root', 'list'], ['project', 'agents', 'work', '--org', 'alpha', 'list']]) { const r = spawnSync(join(repo, 'scripts/mosaic'), args, { cwd: repo, env, encoding: 'utf8' }); assert.equal(r.status, 2, r.stderr); assert.match(r.stderr, /named instance/); } assert.equal(existsSync(join(home, '.config/mosaic-dev/agents.json')), false); }); test('project agents: set, narrow, exclude and remove print receipts; a widening narrow and the chief refuse', () => { const { run, ok, project, home } = instance(); ok('agents', 'root', 'set', 'researcher', '--definition', 'research'); ok('agents', 'root', 'set', 'scout', '--definition', 'research'); const p = (...args) => ['project', 'agents', project, '--org', 'alpha', ...args]; assert.deepEqual(JSON.parse(ok(...p('list', '--json'))).agents, []); const defined = JSON.parse(ok(...p('set', 'checker', '--definition', 'fact-checker', '--var', 'model=claude-sonnet-5-5'))); assert.deepEqual({ ...defined, revisionBefore: undefined, revisionAfter: undefined, transaction: undefined }, { verb: 'project.agents.set', organization: 'alpha', project, agent: 'checker', form: 'define', revisionBefore: undefined, revisionAfter: undefined, transaction: undefined }); assert.notEqual(defined.revisionBefore, defined.revisionAfter); assert.ok(!JSON.stringify(defined).includes(home), 'no host path in the receipt'); assert.equal(JSON.parse(ok(...p('narrow', 'researcher', '--var', 'limits.network=none'))).form, 'narrow'); assert.equal(JSON.parse(ok(...p('exclude', 'scout'))).form, 'exclude'); assert.deepEqual(JSON.parse(ok(...p('list', '--json'))).agents, [ { name: 'checker', form: 'define', definition: 'fact-checker', varKeys: ['model'] }, { name: 'researcher', form: 'narrow', varKeys: ['limits.network'] }, { name: 'scout', form: 'exclude' }, ]); const file = join(home, '.config/mosaic-atwo/businesses/alpha.json'); const before = readFileSync(file, 'utf8'); const widen = run(p('narrow', 'scout', '--var', `model=${SECRET}`)); assert.equal(widen.status, 2); assert.match(widen.stderr, /can only narrow limits\.tools, limits\.network, limits\.authority, not "model"/); assert.ok(!widen.stderr.includes(SECRET)); const chief = run(p('exclude', 'chief')); assert.equal(chief.status, 2); assert.match(chief.stderr, /project-agents-invalid/); const stale = run(p('remove', 'checker', '--revision', defined.revisionBefore)); assert.equal(stale.status, 2); assert.match(stale.stderr, /revision-conflict/); const inside = run(p('remove', 'checker'), { MOSAIC_RUN_ID: 'r1' }); assert.equal(inside.status, 3); assert.equal(readFileSync(file, 'utf8'), before); for (const name of ['checker', 'researcher', 'scout']) assert.equal(JSON.parse(ok(...p('remove', name))).form, 'remove'); assert.deepEqual(JSON.parse(ok(...p('list', '--json'))).agents, []); const missing = run(['project', 'agents', 'nope', '--org', 'alpha', 'exclude', 'scout']); assert.equal(missing.status, 2); assert.match(missing.stderr, /project-not-found/); });