redact6. A refusal names the field, the file or the instance. It never quotes a
config value, a host path, or a raw e.message or stderr that carries one.
- organization: checkCredentialRef names the field and each forbidden root by
label; OrgError keeps .path only for a document field ($...); the registry,
fence and lock refusals use the quiet helpers (call and code).
- cli: loadSystem never relays mosaic-config stderr: instance refusals keep
their code and names, everything else reads as a field and its constraint
against a labelled config. Project-key, access warning, halt gate, sessions
file, launch socket, harness relays and the workspace rename name labels.
Launch-log reasons read a Node system error as its call and code.
- scripts/mosaic-config: configuration not found names where the config came from.
- discord, seat, control-board: config, token, context, tool-root, journal,
binding, registration and seen-marks refusals name labels.
Exit codes, refusal codes and check order are unchanged. Each new or changed
test fails on BASE be698e602.
108 lines
9.6 KiB
JavaScript
108 lines
9.6 KiB
JavaScript
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 }), error => {
|
|
// #1873: the refusal names the project by its key, never the project file's path.
|
|
safe('project file of project stack has id that does not match the declared project key', CliError, { exitCode: 3 }, secret.toLowerCase())(error);
|
|
assert.ok(!error.message.includes(file) && !error.message.includes(f.doc.projects.stack.root), error.message);
|
|
return true;
|
|
});
|
|
});
|
|
|
|
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/);
|
|
});
|