import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; const secret = 'sk-test-NOT-A-SECRET-1234'; function scratch(t) { const root = mkdtempSync(join(tmpdir(), 'r4-')); t.after(() => rmSync(root, { recursive: true, force: true })); return root; } function safe(label, Type, props = {}, omitted = secret) { return error => { assert.ok(error instanceof Type, error.stack); for (const [key, value] of Object.entries(props)) assert.equal(error[key], value); assert.ok(error.message.includes(label), error.message); assert.doesNotMatch(error.message + error.stack, /sk-test|NOT-A-SECRET|Unexpected token/); assert.ok(!error.message.includes(omitted), error.message); assert.equal(error.cause, undefined); return true; }; } import { spawnSync } from 'node:child_process'; import { fixture } from './helpers.mjs'; import { runConfigCommand } from '../../organization/src/config-command.mjs'; const repo = join(import.meta.dirname, '../../..'); const task = () => ({ taskVersion: 1, id: 'valid', prompt: 'hello' }); for (const [field, edit] of [ ['task id', d => { d.id = secret; }], ['taskVersion', d => { d.taskVersion = secret; }], ['tool', d => { d.capabilities = { tools: [secret] }; }], ['mission id', (d, root) => { d.mission = 'mission.json'; writeFileSync(join(root, d.mission), JSON.stringify({ missionVersion: 1, id: secret, objective: 'hello' })); }], ['missionVersion', (d, root) => { d.mission = 'mission.json'; writeFileSync(join(root, d.mission), JSON.stringify({ missionVersion: secret, id: 'valid', objective: 'hello' })); }], ['mission tool', (d, root) => { d.mission = 'mission.json'; writeFileSync(join(root, d.mission), JSON.stringify({ missionVersion: 1, id: 'valid', objective: 'hello', capabilities: { tools: [secret] } })); }], ]) test(`task validation ${field} omits file value and keeps exit 2`, t => { const root = scratch(t), d = task(); edit(d, root); const file = join(root, 'task.json'); writeFileSync(file, JSON.stringify(d)); const r = spawnSync(process.execPath, [join(repo, 'scripts/mosaic-task.mjs'), 'validate', file], { encoding: 'utf8', env: { PATH: process.env.PATH, HOME: process.env.HOME, TMPDIR: process.env.TMPDIR } }); assert.equal(r.status, 2, r.stderr); assert.ok(r.stderr.includes(field), r.stderr); assert.doesNotMatch(r.stderr, /sk-test|NOT-A-SECRET/); }); const configCases = [ ...['configVersion', 'environment', 'dataRoot'].map(k => [k, d => { d[k] = secret; }]), ...['backend', 'provider', 'model', 'adapter'].map(k => [`execution.${k}`, d => { d.execution[k] = secret + '\n'; }]), ['federation.self', d => { d.federation = { self: secret, peers: [] }; }], ['federation.peers[0].id', d => { d.federation = { self: 'self', peers: [{ id: secret, origin: 'https://peer.example', label: 'Peer' }] }; }], ['federation.peers[0].origin', d => { d.federation = { self: 'self', peers: [{ id: 'peer', origin: secret, label: 'Peer' }] }; }], ]; for (const [field, edit] of configCases) test(`config ${field} omits the configured value`, t => { const root = scratch(t), f = fixture(root), d = JSON.parse(readFileSync(f.config, 'utf8')); edit(d); writeFileSync(f.config, JSON.stringify(d)); const r = spawnSync(process.execPath, [join(repo, 'scripts/mosaic-config.mjs'), 'validate'], { encoding: 'utf8', env: { ...f.env, PATH: process.env.PATH, HOME: process.env.HOME, TMPDIR: process.env.TMPDIR } }); assert.equal(r.status, 2, r.stderr); assert.ok(r.stderr.includes(field), r.stderr); assert.doesNotMatch(r.stderr, /sk-test|NOT-A-SECRET/); }); test('config-command refuses system config without relaying stderr file values', t => { const root = scratch(t), f = fixture(root), d = JSON.parse(readFileSync(f.config, 'utf8')); d.execution.model = secret + '\n'; writeFileSync(f.config, JSON.stringify(d)); let err = ''; const status = runConfigCommand(['validate', 'acme'], { env: f.env, stdout: { write() {} }, stderr: { write(s) { err += s; } } }); assert.equal(status, 3); assert.match(err, /system config problem \(mosaic-config exit 2\)/); assert.ok(!err.includes(f.config), err); assert.match(err, /the system config MOSAIC_CONFIG names/); assert.doesNotMatch(err, /sk-test|NOT-A-SECRET/); assert.match(err, /execution.model/); assert.match(err, /expected a 1-200 character name/); }); test('project id mismatch omits the file value in config-command', t => { const root = scratch(t), f = fixture(root), file = join(f.doc.projects.stack.root, '.mosaic', 'project.json'); mkdirSync(join(f.doc.projects.stack.root, '.mosaic')); writeFileSync(file, JSON.stringify({ projectVersion: 1, id: secret.toLowerCase() })); let err = ''; const status = runConfigCommand(['validate', 'acme'], { env: f.env, stdout: { write() {} }, stderr: { write(s) { err += s; } } }); assert.equal(status, 2); assert.ok(!err.includes(file), err); assert.match(err, /project file of project stack has id that does not match the declared project key/); assert.doesNotMatch(err, /sk-test|NOT-A-SECRET/); }); for (const [field, edit] of [ ['dataRoot', d => { d.dataRoot = `/x/${secret}/../y`; }], ['federation.peers[0].id', d => { d.federation = { self: secret.toLowerCase(), peers: [{ id: secret.toLowerCase(), origin: 'https://peer.example', label: 'Peer' }] }; }], ]) test(`config ${field} repeated/canonical value ${field.endsWith(".id") ? "names the checked id" : "is omitted"}`, t => { const root = scratch(t), f = fixture(root), d = JSON.parse(readFileSync(f.config, 'utf8')); edit(d); writeFileSync(f.config, JSON.stringify(d)); const r = spawnSync(process.execPath, [join(repo, 'scripts/mosaic-config.mjs'), 'validate'], { encoding: 'utf8', env: { ...f.env, PATH: process.env.PATH, HOME: process.env.HOME, TMPDIR: process.env.TMPDIR } }); assert.equal(r.status, 2); assert.ok(r.stderr.includes(field), r.stderr); if (field.endsWith(".id")) assert.ok(r.stderr.includes(secret.toLowerCase()), r.stderr); else assert.doesNotMatch(r.stderr, /sk-test|NOT-A-SECRET/); }); for (const mission of [false, true]) test(`duplicate ${mission ? 'mission' : 'task'} tool names the validated value`, t => { const root = scratch(t), d = task(); if (mission) { d.mission = 'mission.json'; writeFileSync(join(root, d.mission), JSON.stringify({ missionVersion: 1, id: 'valid', objective: 'hello', capabilities: { tools: ['read', 'read'] } })); } else d.capabilities = { tools: ['read', 'read'] }; const file = join(root, 'task.json'); writeFileSync(file, JSON.stringify(d)); const r = spawnSync(process.execPath, [join(repo, 'scripts/mosaic-task.mjs'), 'validate', file], { encoding: 'utf8', env: { PATH: process.env.PATH, HOME: process.env.HOME, TMPDIR: process.env.TMPDIR } }); assert.equal(r.status, 2); assert.match(r.stderr, /duplicate tool in/); assert.match(r.stderr, /read/); }); for (const childStderr of [secret, `mosaic-config: execution.model must match ${secret}`]) test(`config-command does not relay arbitrary child stderr${childStderr.startsWith('mosaic-config:') ? ' after a known field' : ''}`, t => { const root = scratch(t), file = join(root, 'config.json'); const code = `import cp from 'node:child_process'; import { syncBuiltinESMExports } from 'node:module'; cp.spawnSync = () => ({ status: 2, stderr: ${JSON.stringify(childStderr)}, stdout: '' }); syncBuiltinESMExports(); const { runConfigCommand } = await import(${JSON.stringify(new URL('../../organization/src/config-command.mjs', import.meta.url).href)}); process.exitCode = runConfigCommand(['validate', 'acme'], { env: { MOSAIC_CONFIG: ${JSON.stringify(file)} } });`; const r = spawnSync(process.execPath, ['--input-type=module', '-e', code], { encoding: 'utf8', env: { PATH: process.env.PATH, HOME: process.env.HOME, TMPDIR: process.env.TMPDIR } }); assert.equal(r.status, 3, r.stderr); assert.ok(!r.stderr.includes(file), r.stderr); assert.match(r.stderr, /: the system config MOSAIC_CONFIG names\n$/); assert.doesNotMatch(r.stderr, /sk-test|NOT-A-SECRET/); }); import { bootConfig, loadSystem } from '../src/config.mjs'; import { CliError } from '../src/errors.mjs'; test('CLI boot project id mismatch omits the configured value and keeps exit 3', t => { const root = scratch(t), f = fixture(root), file = join(f.doc.projects.stack.root, '.mosaic', 'project.json'); mkdirSync(join(f.doc.projects.stack.root, '.mosaic')); writeFileSync(file, JSON.stringify({ projectVersion: 1, id: secret.toLowerCase() })); assert.throws(() => bootConfig({ system: loadSystem({ env: f.env }), businessId: 'acme', env: f.env }), safe(file, CliError, { exitCode: 3 }, secret.toLowerCase())); }); for (const mission of [false, true]) test(`duplicate ${mission ? 'mission' : 'task'} tool rejects an unknown value before naming it`, t => { const root = scratch(t), d = task(); if (mission) { d.mission = 'mission.json'; writeFileSync(join(root, d.mission), JSON.stringify({ missionVersion: 1, id: 'valid', objective: 'hello', capabilities: { tools: [secret, secret] } })); } else d.capabilities = { tools: [secret, secret] }; const file = join(root, 'task.json'); writeFileSync(file, JSON.stringify(d)); const r = spawnSync(process.execPath, [join(repo, 'scripts/mosaic-task.mjs'), 'validate', file], { encoding: 'utf8', env: { PATH: process.env.PATH, HOME: process.env.HOME, TMPDIR: process.env.TMPDIR } }); assert.equal(r.status, 2); assert.match(r.stderr, /unsupported.*tool/); assert.doesNotMatch(r.stderr, /sk-test|NOT-A-SECRET/); });