Compare commits

...
2 Commits
33 changed files with 482 additions and 63 deletions

No files matched your search

+9 -3
View File
@@ -100,9 +100,15 @@ function sessionPairs(pairs) {
// Safety net for a removed-outer-bound change. It is fixture cleanup, never
// product supervision. The guard reads only fixture records and checks sid
// immediately before every kill. Normal outer-bound cases return before it.
export function fixtureGuard(t,files,milliseconds=9500) {
const program=`const fs=require('node:fs');setTimeout(()=>{for(const file of JSON.parse(process.argv[1])){let rows=[];try{rows=fs.readFileSync(file,'utf8').trim().split(String.fromCharCode(10))}catch{}for(const row of rows){const [pid,sid]=row.trim().split(' ').map(Number);if(!(pid>0&&sid>0))continue;try{const raw=fs.readFileSync('/proc/'+pid+'/stat','utf8'),f=raw.slice(raw.lastIndexOf(') ')+2).split(' ');if(Number(f[3])===sid&&f[0]!=='Z')process.kill(pid,'SIGKILL')}catch{}}}},Number(process.argv[2]));`;
const child=spawn(process.execPath,['-e',program,JSON.stringify(files),String(milliseconds)],{stdio:'ignore'});
export function fixtureGuard(t,files,milliseconds=9500,startFile) {
// P10 already records the instant its observed probe starts. Arm its safety
// net there, so unrelated host-fixture setup cannot consume the probe budget.
// Callers without an observer retain the original immediate-start behavior.
const program=`const fs=require('node:fs');
function cleanup(){for(const file of JSON.parse(process.argv[1])){let rows=[];try{rows=fs.readFileSync(file,'utf8').trim().split(String.fromCharCode(10))}catch{}for(const row of rows){const [pid,sid]=row.trim().split(' ').map(Number);if(!(pid>0&&sid>0))continue;try{const raw=fs.readFileSync('/proc/'+pid+'/stat','utf8'),f=raw.slice(raw.lastIndexOf(') ')+2).split(' ');if(Number(f[3])===sid&&f[0]!=='Z')process.kill(pid,'SIGKILL')}catch{}}}}
function arm(){setTimeout(cleanup,Number(process.argv[2]));}
if(!process.argv[3])arm();else{const poll=setInterval(()=>{let text='';try{text=fs.readFileSync(process.argv[3],'utf8')}catch{}if(/^begin [0-9]+[.][0-9]{2}\\n/.test(text)){clearInterval(poll);arm();}},10);}`;
const child=spawn(process.execPath,['-e',program,JSON.stringify(files),String(milliseconds),...(startFile?[startFile]:[])],{stdio:'ignore'});
const stop=async()=>{
if(child.exitCode!==null||child.signalCode!==null)return;
const exited=new Promise(resolve=>child.once('exit',resolve));
@@ -0,0 +1,33 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { once } from 'node:events';
import { mkdtempSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { fixtureGuard } from './case-sudo-fixture.mjs';
test('fixture cleanup guard starts its budget at the probe marker', async t => {
const dir = mkdtempSync(join(tmpdir(), 'guard-'));
t.after(() => rmSync(dir, { recursive: true, force: true }));
const records = join(dir, 'pids'), startFile = join(dir, 'probe');
writeFileSync(startFile, '');
const child = spawn(process.execPath, ['-e', 'setInterval(()=>{},1000)'], { stdio: 'ignore' });
const exited = once(child, 'exit');
t.after(() => { if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL'); });
const raw = readFileSync(`/proc/${child.pid}/stat`, 'utf8');
const sid = Number(raw.slice(raw.lastIndexOf(') ') + 2).split(' ')[3]);
writeFileSync(records, `${child.pid} ${sid}\n`);
const stop = fixtureGuard(t, [records], 100, startFile);
// An empty marker is setup, not the start of the observed probe. This wait
// exceeds the cleanup budget deliberately; no production bound is changed.
await new Promise(resolve => setTimeout(resolve, 500));
assert.equal(child.signalCode, null, 'setup must not consume the probe cleanup budget');
writeFileSync(startFile, 'begin 100.00\n');
const terminal = await Promise.race([exited, new Promise((_, reject) => {
const timer = setTimeout(() => reject(new Error('armed fixture guard did not clean up')), 3000);
timer.unref();
})]);
assert.deepEqual(terminal, [null, 'SIGKILL']);
await stop();
});
+2 -1
View File
@@ -429,7 +429,7 @@ for(const fault of ['inner','outer','overflow'])test(`row253 C4 P10 ${fault} rea
assert.equal(baseline.status,0,baseline.out+baseline.err);
assert.deepEqual(await pollFixtureGone(files),[],'baseline leaves no session');
}
const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files,7000);
const capture=cleanupFixture(t,files);
if(fault==='inner')put(join(h.hc,'peer-hang'),'');
if(fault==='outer'){put(join(h.hc,'stall-probe'),'');put(join(h.hc,'stall-output'),'200 997 997\n');}
if(fault==='overflow'){put(join(h.hc,'peer-output'),'200 997 997\n'+'X'.repeat(256));put(join(h.hc,'peer-status'),'0');}
@@ -440,6 +440,7 @@ for(const fault of ['inner','outer','overflow'])test(`row253 C4 P10 ${fault} rea
put(join(h.dir,'selector.sh'),read(join(REPO,'scripts','selector.sh')),0o644);
assert.equal(removeP10Observers(read(observedScript)),read(SCRIPT),'observer reversal equals a fresh shipped script read');
}
const stop=fixtureGuard(t,files,7000,timingFile);
const start=performance.now(),r=run(h,timingFile?{ROW299_TIMING_FILE:timingFile}:{},undefined,observedScript),seconds=(performance.now()-start)/1000;capture();
assert.equal(r.status,0,r.out+r.err);
const reason=fault==='inner'?'inner-timeout':fault==='outer'?'outer-timeout':'output-limit';
@@ -0,0 +1,60 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
const secret = 'sk-test-NOT-A-SECRET-1234';
// JSON.parse quotes a prefix near this fault on Node 26; check partial leaks too.
const malformed = secret;
function scratch(t) {
const root = mkdtempSync(join(tmpdir(), 'r3-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
return root;
}
function write(file, text = malformed) {
mkdirSync(dirname(file), { recursive: true });
writeFileSync(file, text, { mode: 0o600 });
return file;
}
import { spawnSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { fixture } from './helpers.mjs';
const repo = join(import.meta.dirname, '../../..');
test('task file JSON refusal preserves exit 2 and file without parser bytes', t => {
const file = write(join(scratch(t), 'task.json'));
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(file), r.stderr);
assert.match(r.stderr, /task file is not valid JSON/);
assert.doesNotMatch(r.stdout + r.stderr, /sk-test|Unexpected token/);
});
test('task file read refusal retains exit 2, file and filesystem code', t => {
const file = write(join(scratch(t), 'task.json'), '{}');
// A directory is refused by lstat before the read. Inject a read failure
// after that unchanged regular-file gate, including on root-run CI hosts.
const preload = `import fs from 'node:fs';
const read = fs.readFileSync;
fs.readFileSync = function(path, ...args) {
if (path === ${JSON.stringify(file)}) throw Object.assign(new Error(${JSON.stringify(secret)}), { code: 'EIO' });
return read.call(this, path, ...args);
};`;
const r = spawnSync(process.execPath, ['--import', 'data:text/javascript,' + encodeURIComponent(preload), 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(file), r.stderr);
assert.match(r.stderr, /task file is not valid JSON.*\(EIO\)/);
assert.doesNotMatch(r.stderr, /sk-test/);
});
test('case2 precheck business helper omits parser bytes in its BUSINESS fact', t => {
const root = scratch(t), f = fixture(root);
const file = write(join(root, 'config/businesses/acme.json'));
// Execute the production facts helper, without host or credential checks.
const shell = readFileSync(join(repo, 'scripts/case2-precheck.sh'), 'utf8');
const start = shell.indexOf('facts="$(cd "$REPO" && node --input-type=module -e \'') + 'facts="$(cd "$REPO" && node --input-type=module -e \''.length;
assert.ok(start > 100);
const end = shell.indexOf("' \"$REPO\" \"$BUSINESS\"", start);
assert.ok(end > start);
const r = spawnSync(process.execPath, ['--input-type=module', '-e', shell.slice(start, end), repo, 'acme'], { encoding: 'utf8', env: { ...f.env, HOME: process.env.HOME, TMPDIR: process.env.TMPDIR, PATH: process.env.PATH } });
assert.equal(r.status, 0, r.stderr);
assert.ok(r.stdout.includes(`BUSINESS\tbad\tcannot read ${file}:`), r.stdout);
assert.doesNotMatch(r.stdout + r.stderr, /sk-test|Unexpected token/);
});
+7 -6
View File
@@ -13,6 +13,7 @@
// exception is <boardDir>/seen.json, which holds Jason's "seen" marks and is
// only changed when he clicks; a scan reads it and never rewrites it.
import { parseJsonFileText } from "../../organization/src/util.mjs";
import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, writeFileSync, renameSync } from "node:fs";
import { join, basename, isAbsolute, resolve } from "node:path";
import { homedir } from "node:os";
@@ -35,9 +36,9 @@ export function loadConfig(path = defaultConfigPath()) {
if (!existsSync(path)) throw new ConfigError(`config not found: ${path}`);
let raw;
try {
raw = JSON.parse(readFileSync(path, "utf8"));
} catch (err) {
throw new ConfigError(`config is not valid JSON: ${path} (${err.message})`);
raw = parseJsonFileText(readFileSync(path, "utf8"), path);
} catch (error) {
throw new ConfigError(`config is not valid JSON: ${path}${error.code ? ` (${error.code})` : ""}`);
}
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new ConfigError(`config is not an object: ${path}`);
if (typeof raw.dataRoot !== "string" || !isAbsolute(raw.dataRoot)) {
@@ -227,9 +228,9 @@ export function loadSeen(boardDir) {
if (!existsSync(path)) return {};
let parsed;
try {
parsed = JSON.parse(readFileSync(path, "utf8"));
} catch (err) {
throw new ConfigError(`seen marks file is not valid JSON: ${path} (${err.message})`);
parsed = parseJsonFileText(readFileSync(path, "utf8"), path);
} catch (error) {
throw new ConfigError(`seen marks file is not valid JSON: ${path}${error.code ? ` (${error.code})` : ""}`);
}
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new ConfigError(`seen marks file must be a JSON object: ${path}`);
for (const [k, v] of Object.entries(parsed)) {
@@ -0,0 +1,44 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
const secret = 'sk-test-NOT-A-SECRET-1234';
// JSON.parse quotes a prefix near this fault on Node 26; check partial leaks too.
const malformed = secret;
function scratch(t) {
const root = mkdtempSync(join(tmpdir(), 'r3-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
return root;
}
function write(file, text = malformed) {
mkdirSync(dirname(file), { recursive: true });
writeFileSync(file, text, { mode: 0o600 });
return file;
}
function safe(label, ErrorType, properties = {}) {
return error => {
assert.ok(error instanceof ErrorType);
for (const [key, value] of Object.entries(properties)) assert.equal(error[key], value);
assert.ok(error.message.includes(label), error.message);
assert.doesNotMatch(error.message, /sk-test|Unexpected token/);
assert.doesNotMatch(error.stack, /sk-test/);
assert.equal(error.cause, undefined);
return true;
};
}
import { ConfigError, loadConfig, loadSeen } from '../src/scan.mjs';
for (const kind of ['config', 'seen']) test(`${kind} JSON refusal names only the file`, t => {
const root = scratch(t), file = write(join(root, kind === 'config' ? 'config.json' : 'seen.json'));
assert.throws(() => kind === 'config' ? loadConfig(file) : loadSeen(root), safe(file, ConfigError));
});
for (const kind of ['config', 'seen']) test(`${kind} read refusal retains the filesystem code`, t => {
const root = scratch(t), file = join(root, kind === 'config' ? 'config.json' : 'seen.json');
mkdirSync(file);
assert.throws(() => kind === 'config' ? loadConfig(file) : loadSeen(root), error => {
safe(file, ConfigError)(error);
assert.match(error.message, /\(EISDIR\)$/);
assert.doesNotMatch(error.message, /illegal operation/);
return true;
});
});
+4 -3
View File
@@ -15,6 +15,7 @@
// The per-message budget resets on agent_start, the event pi emits once per
// prompt run, so a follow-up message gets a fresh budget.
import { parseJsonFileText } from "../../organization/src/util.mjs";
import { Type } from "typebox";
import { TOOLS_ENV, TOOL_DESCRIPTIONS, READ_MAX_LINES, loadToolsConfig, createToolSet, enabledToolNames } from "../src/tools.mjs";
import { COMMIT_MESSAGE_MAX, COMMIT_PATHS_MAX, VAULT_PREFIXES } from "../src/git.mjs";
@@ -141,9 +142,9 @@ export default function (pi) {
if (typeof raw !== "string" || raw.length === 0) throw new Error(`${TOOLS_ENV} is not set; the connector sets it from the binding's tools key`);
let parsed;
try {
parsed = JSON.parse(raw);
} catch (err) {
throw new Error(`${TOOLS_ENV} is not valid JSON: ${err.message}`);
parsed = parseJsonFileText(raw, TOOLS_ENV);
} catch {
throw new Error(`${TOOLS_ENV} is not valid JSON`);
}
const config = loadToolsConfig(parsed);
const tools = createToolSet(config);
+4 -3
View File
@@ -19,6 +19,7 @@
// Ids in this file are private to the binding's data directory (0600),
// like every other journal.
import { parseJsonFileText } from "../../organization/src/util.mjs";
import { appendFileSync, existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { DiscordError } from "./errors.mjs";
@@ -104,9 +105,9 @@ export function readApprovals(dir) {
for (const [i, line] of readFileSync(path, "utf8").split("\n").entries()) {
if (line.length === 0) continue;
try {
out.push(JSON.parse(line));
} catch (err) {
throw new DiscordError(`${path}:${i + 1}: not valid JSON (${err.message})`);
out.push(parseJsonFileText(line, `${path}:${i + 1}`));
} catch {
throw new DiscordError(`${path}:${i + 1}: not valid JSON`);
}
}
return out;
+7 -6
View File
@@ -18,6 +18,7 @@
// Absent means no tools and a launch exactly as before. It is a fixed
// key: the extension reads it at pi start.
import { parseJsonFileText } from "../../organization/src/util.mjs";
import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
import { loadWebConfig } from "./web.mjs";
import { loadSetsparkConfig } from "./setspark.mjs";
@@ -59,9 +60,9 @@ export function loadDataRoot(path = defaultConfigPath()) {
if (!existsSync(path)) throw new DiscordError(`config not found: ${path}`);
let raw;
try {
raw = JSON.parse(readFileSync(path, "utf8"));
} catch (err) {
throw new DiscordError(`config is not valid JSON: ${path} (${err.message})`);
raw = parseJsonFileText(readFileSync(path, "utf8"), path);
} catch (error) {
throw new DiscordError(`config is not valid JSON: ${path}${error.code ? ` (${error.code})` : ""}`);
}
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new DiscordError(`config is not an object: ${path}`);
if (typeof raw.dataRoot !== "string" || !isAbsolute(raw.dataRoot)) throw new DiscordError(`config.dataRoot must be an absolute path: ${path}`);
@@ -292,9 +293,9 @@ export function loadBinding(path) {
checkPrivateFile(path, "binding");
let raw;
try {
raw = JSON.parse(readFileSync(path, "utf8"));
} catch (err) {
throw new DiscordError(`binding is not valid JSON: ${path} (${err.message})`);
raw = parseJsonFileText(readFileSync(path, "utf8"), path);
} catch (error) {
throw new DiscordError(`binding is not valid JSON: ${path}${error.code ? ` (${error.code})` : ""}`);
}
return validateBinding(raw, `binding ${path}`);
}
+4 -3
View File
@@ -20,6 +20,7 @@
// supervised form for a service unit's pre-start
// Directories are 0700, files 0600. Lines are appended, never rewritten.
import { parseJsonFileText } from "../../organization/src/util.mjs";
import {
appendFileSync, closeSync, constants, existsSync, fstatSync, fsyncSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, renameSync, rmdirSync, rmSync,
unlinkSync, writeFileSync, writeSync,
@@ -47,9 +48,9 @@ function readLines(path) {
for (const [i, line] of text.split("\n").entries()) {
if (line.length === 0) continue;
try {
out.push(JSON.parse(line));
} catch (err) {
throw new DiscordError(`${path}:${i + 1}: not valid JSON (${err.message})`);
out.push(parseJsonFileText(line, `${path}:${i + 1}`));
} catch {
throw new DiscordError(`${path}:${i + 1}: not valid JSON`);
}
}
return out;
@@ -0,0 +1,59 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
const secret = 'sk-test-NOT-A-SECRET-1234';
// JSON.parse quotes a prefix near this fault on Node 26; check partial leaks too.
const malformed = secret;
function scratch(t) {
const root = mkdtempSync(join(tmpdir(), 'r3-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
return root;
}
function write(file, text = malformed) {
mkdirSync(dirname(file), { recursive: true });
writeFileSync(file, text, { mode: 0o600 });
return file;
}
function safe(label, ErrorType, properties = {}) {
return error => {
assert.ok(error instanceof ErrorType);
for (const [key, value] of Object.entries(properties)) assert.equal(error[key], value);
assert.ok(error.message.includes(label), error.message);
assert.doesNotMatch(error.message, /sk-test|Unexpected token/);
assert.doesNotMatch(error.stack, /sk-test/);
assert.equal(error.cause, undefined);
return true;
};
}
import { DiscordError } from '../src/errors.mjs';
import { loadDataRoot, loadBinding } from '../src/binding.mjs';
import { readApprovals } from '../src/approvals.mjs';
import { readInboxIds } from '../src/journal.mjs';
import extension from '../extension/tools.mjs';
import { TOOLS_ENV } from '../src/tools.mjs';
for (const [name, read, relative] of [
['config', (file) => loadDataRoot(file), 'config.json'],
['binding', (file) => loadBinding(file), 'binding.json'],
['approvals', (_file, root) => readApprovals(root), 'approvals.jsonl'],
['journal', (_file, root) => readInboxIds(root), 'inbox.jsonl'],
]) test(`${name} JSON refusal omits parser bytes`, t => {
const root = scratch(t), ledger = name === 'approvals' || name === 'journal';
const file = write(join(root, relative), ledger ? '{}\n' + malformed : malformed);
assert.throws(() => read(file, root), safe(ledger ? `${file}:2` : file, DiscordError, { exitCode: 2 }));
});
test('tools environment JSON refusal names the config key without its bytes', () => {
const previous = process.env[TOOLS_ENV];
process.env[TOOLS_ENV] = malformed;
try { assert.throws(() => extension({}), safe(TOOLS_ENV, Error)); }
finally { if (previous === undefined) delete process.env[TOOLS_ENV]; else process.env[TOOLS_ENV] = previous; }
});
test('config read refusal retains the filesystem code', t => {
const file = scratch(t);
assert.throws(() => loadDataRoot(file), error => {
safe(file, DiscordError, { exitCode: 2 })(error);
assert.match(error.message, /\(EISDIR\)$/);
return true;
});
});
+1 -1
View File
@@ -36,7 +36,7 @@ export function webPlan(resolved, harness, builtinWeb) {
throw new BundleError(`instance ${resolved.instance}: a ${resolved.definition} role fetches only through the broker, not search-source "harness-builtin"`, "web-source-not-allowed");
}
if (granted && source === "harness-builtin" && !builtinWeb) {
throw new BundleError(`instance ${resolved.instance}: search-source "harness-builtin" has no built-in web tool on ${harness}`);
throw new BundleError(`instance ${resolved.instance}: search-source "harness-builtin" has no built-in web tool on the configured harness`);
}
return { source, builtin: granted && source === "harness-builtin" };
}
+1 -1
View File
@@ -440,7 +440,7 @@ test("search-source harness-builtin: claude-code gets WebSearch and WebFetch and
test("search-source harness-builtin is refused for a granted pi role, which has no built-in web tool", (t) => {
const r = resolvedFor(t, "coder", (d) => (d.roles.coder.vars = { harness: "pi", model: "claude-sonnet-5-5" }));
assert.throws(() => build(t, withSource(granted(r), "harness-builtin")), (e) => e instanceof BundleError && /no built-in web tool on pi/.test(e.message));
assert.throws(() => build(t, withSource(granted(r), "harness-builtin")), (e) => e instanceof BundleError && /no built-in web tool on the configured harness/.test(e.message));
// Without the grant the source doesn't matter on pi.
assert.ok(!build(t, withSource(r, "harness-builtin")).manifest.builtins.some((x) => /web/i.test(x)));
});
@@ -0,0 +1,19 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
const secret = 'sk-test-NOT-A-SECRET-1234';
// JSON.parse quotes a prefix near this fault on Node 26; check partial leaks too.
function safe(label, ErrorType, properties = {}) {
return error => {
assert.ok(error instanceof ErrorType);
for (const [key, value] of Object.entries(properties)) assert.equal(error[key], value);
assert.ok(error.message.includes(label), error.message);
assert.doesNotMatch(error.message, /sk-test|Unexpected token/);
assert.doesNotMatch(error.stack, /sk-test/);
assert.equal(error.cause, undefined);
return true;
};
}
import { BundleError, webPlan } from '../src/bundle.mjs';
test('harness web refusal names the harness field without the configured value', () => {
assert.throws(() => webPlan({ instance: 'builder', definition: 'coder', webSource: 'harness-builtin', limits: { tools: ['web'] } }, secret, false), safe('harness', BundleError, { code: 'bundle-refused' }));
});
+3
View File
@@ -65,6 +65,9 @@ function fixture(t) {
cpSync(source, path.join(root, 'packages/ledger/src'), { recursive: true });
// The queue section imports the queue validator and the seat registry.
for (const pkg of ['queue', 'seat']) cpSync(path.join(source, '../..', pkg, 'src'), path.join(root, 'packages', pkg, 'src'), { recursive: true });
mkdirSync(path.join(root, 'packages/organization/src'), { recursive: true });
for (const file of ['util.mjs', 'errors.mjs', 'vocabulary.mjs'])
cpSync(path.join(source, '../../organization/src', file), path.join(root, 'packages/organization/src', file));
// The fake serves ISSUES for every page, or page N of the ISSUE_PAGES array
// (an empty page past its end). API_FAIL fails every call, or only FAIL_PAGE.
const api = put('bin/gitea-api.sh', `#!/usr/bin/env node
@@ -337,6 +337,9 @@ function cliFixture(t, rows = MAP) {
const repo = scratchRepo(t, { map: mapText(rows, { retired: [], highWater: Math.max(...rows.map(r => r.id)) }) });
genesisCommitted(repo);
for (const pkg of ['ledger', 'seat']) cpSync(path.join(PACKAGES, pkg, 'src'), path.join(repo.root, 'packages', pkg, 'src'), { recursive: true });
mkdirSync(path.join(repo.root, 'packages/organization/src'), { recursive: true });
for (const file of ['util.mjs', 'errors.mjs', 'vocabulary.mjs'])
cpSync(path.join(PACKAGES, 'organization/src', file), path.join(repo.root, 'packages/organization/src', file));
const dataRoot = path.join(repo.base, 'data');
const config = path.join(repo.base, 'config.json');
writeFileSync(config, JSON.stringify({ dataRoot }));
+3 -3
View File
@@ -112,10 +112,10 @@ function checkScope(value, where, check) {
function checkExpires(value, where) {
if (typeof value !== "string" || !ISO_TIME.test(value) || Number.isNaN(Date.parse(value))) {
refuse(`${where} must be an ISO time such as 2026-12-31T00:00:00Z (got ${JSON.stringify(value)})`);
refuse(`${where} must be an ISO time such as 2026-12-31T00:00:00Z`);
}
const iso = new Date(value).toISOString();
if (iso.slice(0, 19) !== value.slice(0, 19)) refuse(`${where} is not a real time: ${value}`);
if (iso.slice(0, 19) !== value.slice(0, 19)) refuse(`${where} is not a real time`);
}
function checkGrant(grant, at, ctx, users, seenGrants) {
@@ -133,7 +133,7 @@ function checkGrant(grant, at, ctx, users, seenGrants) {
if (typeof p !== "string" || !UUID.test(p)) refuse(`${where} must be "*" or a bus project id, a lowercase UUID (got ${JSON.stringify(p)})`);
});
checkScope(grant.instances, `${at}.instances`, (x, where) => {
if (typeof x !== "string" || !business.instances.has(x)) refuse(`${where} must be "*" or a declared instance of ${grant.business} (got ${JSON.stringify(x)})`);
if (typeof x !== "string" || !business.instances.has(x)) refuse(`${where} must be "*" or a declared instance of the grant's business`);
});
if (!LEVELS.includes(grant.level)) refuse(`${at}.level must be one of ${LEVELS.join(", ")} (got ${JSON.stringify(grant.level)})`);
if (grant.expires !== undefined) checkExpires(grant.expires, `${at}.expires`);
+4 -4
View File
@@ -20,7 +20,7 @@ export function rejectUnknownKeys(object, allowed, where) {
export function requireId(value, where) {
if (typeof value !== "string" || !ID_PATTERN.test(value)) {
refuse(`${where} must match ${ID_PATTERN} (got ${JSON.stringify(value)})`);
refuse(`${where} must match ${ID_PATTERN}`);
}
return value;
}
@@ -33,7 +33,7 @@ export function requireString(value, where, { max = 200 } = {}) {
}
export function requirePositiveInt(value, where) {
if (!Number.isSafeInteger(value) || value < 1) refuse(`${where} must be a positive integer (got ${JSON.stringify(value)})`);
if (!Number.isSafeInteger(value) || value < 1) refuse(`${where} must be a positive integer`);
return value;
}
@@ -52,9 +52,9 @@ export function requireDistinctList(value, where, check, { nonEmpty = false } =
// Calendar date YYYY-MM-DD that exists (2026-02-30 refuses).
export function requireDate(value, where) {
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value)) refuse(`${where} must be a date YYYY-MM-DD (got ${JSON.stringify(value)})`);
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value)) refuse(`${where} must be a date YYYY-MM-DD`);
const d = new Date(`${value}T00:00:00Z`);
if (Number.isNaN(d.getTime()) || d.toISOString().slice(0, 10) !== value) refuse(`${where} is not a real date: ${value}`);
if (Number.isNaN(d.getTime()) || d.toISOString().slice(0, 10) !== value) refuse(`${where} is not a real date`);
return value;
}
+1 -1
View File
@@ -161,7 +161,7 @@ test("grants: projects is [\"*\"] or distinct lowercase UUIDs", () => {
});
test("grants: instances is [\"*\"] or distinct declared instances of that business", () => {
refuses(() => checkAccess(mutated((d) => { d.grants[0].instances = ["coder"]; }), context()), /grants\[0\]\.instances\[0\] must be "\*" or a declared instance of creative-hub \(got "coder"\)/);
refuses(() => checkAccess(mutated((d) => { d.grants[0].instances = ["coder"]; }), context()), /grants\[0\]\.instances\[0\] must be "\*" or a declared instance of the grant's business/);
refuses(() => checkAccess(mutated((d) => { d.grants[0].instances = ["*", "writer"]; }), context()), /grants\[0\]\.instances must be \["\*"\] alone/);
refuses(() => checkAccess(mutated((d) => { d.grants[0].instances = ["writer", "writer"]; }), context()), /duplicate entry in grants\[0\]\.instances/);
refuses(() => checkAccess(mutated((d) => { d.grants[0].instances = []; }), context()), /grants\[0\]\.instances must not be empty/);
@@ -0,0 +1,39 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
const secret = 'sk-test-NOT-A-SECRET-1234';
// JSON.parse quotes a prefix near this fault on Node 26; check partial leaks too.
function safe(label, ErrorType, properties = {}) {
return error => {
assert.ok(error instanceof ErrorType);
for (const [key, value] of Object.entries(properties)) assert.equal(error[key], value);
assert.ok(error.message.includes(label), error.message);
assert.doesNotMatch(error.message, /sk-test|Unexpected token/);
assert.doesNotMatch(error.stack, /sk-test/);
assert.equal(error.cause, undefined);
return true;
};
}
import { BusinessError } from '../src/errors.mjs';
import { requireId, requirePositiveInt, requireDate } from '../src/util.mjs';
import { checkAccess } from '../src/access.mjs';
for (const [field, check, value] of [
['id', requireId, secret], ['revision', requirePositiveInt, secret], ['rotateBy', requireDate, secret],
// Arbitrary sentinel bytes cannot pass the date-shape check to reach this branch.
['rotateBy', requireDate, '2026-02-30'],
]) test(`${field} rejects ${check.name} input without its value`, () => {
assert.throws(() => check(value, field), error => {
safe(field, BusinessError, { exitCode: 2 })(error);
assert.ok(!error.message.includes(value), error.message);
return true;
});
});
function access() { return { accessVersion: 1, revision: 1, users: [{ id: 'bean', home: 'local', subjects: [{ provider: 'test', sub: 'user' }] }], grants: [{ id: 'grant', user: 'bean', business: 'acme', projects: ['*'], instances: ['coder'], level: 'viewer' }] }; }
const context = { providers: ['test'], peers: [], businesses: { acme: { human: 'jason', instances: ['coder'] } }, ssoSubjects: [] };
for (const [field, value] of [['expires', secret], ['expires', '2026-02-30T00:00:00Z'], ['instances', [secret]]]) test(`grant ${field} refusal omits its value ${JSON.stringify(value)}`, () => {
const doc = access(); doc.grants[0][field] = value;
assert.throws(() => checkAccess(doc, context), error => {
safe(`grants[0].${field}`, BusinessError, { exitCode: 2 })(error);
assert.ok(!error.message.includes(Array.isArray(value) ? value[0] : value), error.message);
return true;
});
});
+1 -1
View File
@@ -27,7 +27,7 @@ function jsonProblems(kind, text, relative, problems) {
const value = JSON.parse(text);
for (const problem of validateRecord(kind, value)) problems.push(`${relative}: ${problem}`);
return value;
} catch (e) { problems.push(`${relative}: invalid JSON (${e.message})`); return null; }
} catch { problems.push(`${relative}: invalid JSON`); return null; }
}
async function checkLayout(context) {
const repo = projectRepo(context), problems = [];
@@ -0,0 +1,28 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
const secret = 'sk-test-NOT-A-SECRET-1234';
// JSON.parse quotes a prefix near this fault on Node 26; check partial leaks too.
const malformed = secret;
function scratch(t) {
const root = mkdtempSync(join(tmpdir(), 'r3-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
return root;
}
function write(file, text = malformed) {
mkdirSync(dirname(file), { recursive: true });
writeFileSync(file, text, { mode: 0o600 });
return file;
}
import { checkProject, initProject } from '../src/index.mjs';
test('project record JSON problems name the file without parser bytes', async t => {
const root = scratch(t), context = { business: { id: 'acme', human: 'jason', projects: { stack: { root: join(root, 'code') } } }, project: 'stack', dataRoot: join(root, 'data') };
const { repo } = await initProject(context);
write(join(repo, 'requirements/REQ-1.json'));
const result = await checkProject(context);
assert.equal(result.ok, false);
assert.ok(result.problems.some(p => p.includes('requirements/REQ-1.json: invalid JSON')), JSON.stringify(result.problems));
assert.doesNotMatch(JSON.stringify(result), /sk-test|Unexpected token/);
});
+2 -2
View File
@@ -1025,7 +1025,7 @@ export function parseMigrationMap(text) {
const blocks = [...text.matchAll(/^```json queue-map\n([\s\S]*?)^```$/gm)];
if (blocks.length !== 1) throw refuse("the migration map must hold exactly one ```json queue-map block");
let map;
try { map = JSON.parse(blocks[0][1]); } catch (err) { throw refuse(`the migration map's queue-map block is not JSON: ${err.message}`); }
try { map = JSON.parse(blocks[0][1]); } catch { throw refuse(`the migration map's queue-map block is not JSON`); }
keysExactly(map, ["rows", "retired", "highWater"], "migration map");
if (!Array.isArray(map.rows)) throw refuse("migration map rows must be a list");
map.rows.forEach((r) => {
@@ -1126,7 +1126,7 @@ export function loadDoc(bytes) {
throw refuse("queue.json is not valid UTF-8");
}
let doc;
try { doc = JSON.parse(text); } catch (err) { throw refuse(`queue.json is not JSON: ${err.message}`); }
try { doc = JSON.parse(text); } catch { throw refuse(`queue.json is not JSON`); }
keysExactly(doc, DOC_KEYS, "queue.json");
if (serialize(doc) !== text) throw refuse("queue.json does not re-serialize byte for byte (a hand edit?)");
if (doc.version !== VERSION) throw refuse(`queue.json version ${doc.version} is not ${VERSION}`);
+4 -1
View File
@@ -1,6 +1,6 @@
// Scratch repositories for the queue tests. Every test works under
// os.tmpdir(): a fresh repository holding a copy of packages/queue/src and
// the two discord helpers it imports, so the code-under-root check (8.3)
// the discord helpers and safe JSON parser they import, so the code-under-root check (8.3)
// passes there and nothing touches this checkout's .git.
import { spawnSync } from "node:child_process";
import { cpSync, mkdirSync, mkdtempSync, realpathSync, rmSync, statfsSync, writeFileSync } from "node:fs";
@@ -91,6 +91,9 @@ export function scratchRepo(t, { map = mapText(), commitMap = true, name = "repo
cpSync(PKG_SRC, join(root, "packages/queue/src"), { recursive: true });
mkdirSync(join(root, "packages/discord/src"), { recursive: true });
for (const f of ["journal.mjs", "errors.mjs"]) cpSync(join(DISCORD_SRC, f), join(root, "packages/discord/src", f));
mkdirSync(join(root, "packages/organization/src"), { recursive: true });
for (const f of ["util.mjs", "errors.mjs", "vocabulary.mjs"])
cpSync(join(DISCORD_SRC, "../../organization/src", f), join(root, "packages/organization/src", f));
for (const s of ["darkwing", "dewey", "filbert", "rocko", "sage"]) {
mkdirSync(join(root, "agents", s, "work"), { recursive: true });
writeFileSync(join(root, "agents", s, ".keep"), "");
@@ -0,0 +1,24 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
const secret = 'sk-test-NOT-A-SECRET-1234';
// JSON.parse quotes a prefix near this fault on Node 26; check partial leaks too.
const malformed = secret;
function safe(label, ErrorType, properties = {}) {
return error => {
assert.ok(error instanceof ErrorType);
for (const [key, value] of Object.entries(properties)) assert.equal(error[key], value);
assert.ok(error.message.includes(label), error.message);
assert.doesNotMatch(error.message, /sk-test|Unexpected token/);
assert.doesNotMatch(error.stack, /sk-test/);
assert.equal(error.cause, undefined);
return true;
};
}
import { loadDoc, parseMigrationMap } from '../src/queue.mjs';
import { QueueError } from '../src/errors.mjs';
for (const [label, parse] of [
['queue.json', () => loadDoc(Buffer.from(malformed))],
["migration map's queue-map block", () => parseMigrationMap('```json queue-map\n' + malformed + '\n```')],
]) test(`${label} refusal omits parser bytes`, () => {
assert.throws(parse, safe(label, QueueError, { code: 2 }));
});
+2 -2
View File
@@ -31,8 +31,8 @@ export function readActivePointer(dataRoot) {
let pointer;
try {
pointer = JSON.parse(state.text);
} catch (error) {
throw new RunsError(`release pointer is not valid JSON (${state.file}): ${error.message}`, 2);
} catch {
throw new RunsError(`release pointer is not valid JSON (${state.file})`, 2);
}
const invalid = (why) => new RunsError(`release pointer ${why} (${state.file})`, 2);
if (typeof pointer !== "object" || pointer === null || Array.isArray(pointer)) throw invalid("must be a JSON object");
@@ -0,0 +1,34 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
const secret = 'sk-test-NOT-A-SECRET-1234';
// JSON.parse quotes a prefix near this fault on Node 26; check partial leaks too.
const malformed = secret;
function scratch(t) {
const root = mkdtempSync(join(tmpdir(), 'r3-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
return root;
}
function write(file, text = malformed) {
mkdirSync(dirname(file), { recursive: true });
writeFileSync(file, text, { mode: 0o600 });
return file;
}
function safe(label, ErrorType, properties = {}) {
return error => {
assert.ok(error instanceof ErrorType);
for (const [key, value] of Object.entries(properties)) assert.equal(error[key], value);
assert.ok(error.message.includes(label), error.message);
assert.doesNotMatch(error.message, /sk-test|Unexpected token/);
assert.doesNotMatch(error.stack, /sk-test/);
assert.equal(error.cause, undefined);
return true;
};
}
import { RunsError, readActivePointer } from '../src/index.mjs';
test('release pointer JSON refusal names only the file', t => {
const root = scratch(t), file = write(join(root, 'state/active.json'));
assert.throws(() => readActivePointer(root), safe(file, RunsError, { exitCode: 2 }));
});
+7 -6
View File
@@ -15,6 +15,7 @@
// Sessions live in <seatDir>/.pi/agent/sessions.
// Anything else is layout "unknown" and records nulls; nothing is guessed.
import { parseJsonFileText } from "../../organization/src/util.mjs";
import { existsSync, readFileSync, mkdirSync, writeFileSync, renameSync, statSync, realpathSync } from "node:fs";
import { join, basename, dirname, isAbsolute, resolve } from "node:path";
import { homedir } from "node:os";
@@ -53,9 +54,9 @@ export function loadDataRoot(path = defaultConfigPath()) {
if (!existsSync(path)) throw new SeatError(`config not found: ${path}`);
let raw;
try {
raw = JSON.parse(readFileSync(path, "utf8"));
} catch (err) {
throw new SeatError(`config is not valid JSON: ${path} (${err.message})`);
raw = parseJsonFileText(readFileSync(path, "utf8"), path);
} catch (error) {
throw new SeatError(`config is not valid JSON: ${path}${error.code ? ` (${error.code})` : ""}`);
}
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new SeatError(`config is not an object: ${path}`);
if (typeof raw.dataRoot !== "string" || !isAbsolute(raw.dataRoot)) throw new SeatError(`config.dataRoot must be an absolute path: ${path}`);
@@ -202,9 +203,9 @@ export function readRegistration(seats, seat, layout) {
if (!existsSync(path)) return null;
let parsed;
try {
parsed = JSON.parse(readFileSync(path, "utf8"));
} catch (err) {
throw new SeatError(`registration is not valid JSON: ${path} (${err.message})`);
parsed = parseJsonFileText(readFileSync(path, "utf8"), path);
} catch (error) {
throw new SeatError(`registration is not valid JSON: ${path}${error.code ? ` (${error.code})` : ""}`);
}
try {
const record = validateRegistration(parsed);
@@ -0,0 +1,43 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
const secret = 'sk-test-NOT-A-SECRET-1234';
// JSON.parse quotes a prefix near this fault on Node 26; check partial leaks too.
const malformed = secret;
function scratch(t) {
const root = mkdtempSync(join(tmpdir(), 'r3-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
return root;
}
function write(file, text = malformed) {
mkdirSync(dirname(file), { recursive: true });
writeFileSync(file, text, { mode: 0o600 });
return file;
}
function safe(label, ErrorType, properties = {}) {
return error => {
assert.ok(error instanceof ErrorType);
for (const [key, value] of Object.entries(properties)) assert.equal(error[key], value);
assert.ok(error.message.includes(label), error.message);
assert.doesNotMatch(error.message, /sk-test|Unexpected token/);
assert.doesNotMatch(error.stack, /sk-test/);
assert.equal(error.cause, undefined);
return true;
};
}
import { SeatError, loadDataRoot, readRegistration, registrationPath } from '../src/seat.mjs';
for (const kind of ['config', 'registration']) test(`${kind} JSON refusal names only the file`, t => {
const root = scratch(t), file = write(kind === 'config' ? join(root, 'config.json') : registrationPath(root, 'builder', 'repo'));
assert.throws(() => kind === 'config' ? loadDataRoot(file) : readRegistration(root, 'builder', 'repo'), safe(file, SeatError, { exitCode: 2 }));
});
for (const kind of ['config', 'registration']) test(`${kind} read refusal retains the filesystem code`, t => {
const root = scratch(t), file = kind === 'config' ? join(root, 'config.json') : registrationPath(root, 'builder', 'repo');
mkdirSync(file, { recursive: true });
assert.throws(() => kind === 'config' ? loadDataRoot(file) : readRegistration(root, 'builder', 'repo'), error => {
safe(file, SeatError, { exitCode: 2 })(error);
assert.match(error.message, /\(EISDIR\)$/);
return true;
});
});
+2 -1
View File
@@ -193,9 +193,10 @@ try {
readNotifyConfig(system.dataRoot, id);
say("NOTIFY", "ok");
} catch (e) { say("NOTIFY", "bad", e.message.split("\n")[0]); }
const { parseJsonFileText } = await import(`${repo}/packages/organization/src/util.mjs`);
const file = businessFilePath(id);
let raw;
try { raw = JSON.parse(readFileSync(file, "utf8")); } catch (e) { say("BUSINESS", "bad", `cannot read ${file}: ${e.code ?? e.message}`); process.exit(0); }
try { raw = parseJsonFileText(readFileSync(file, "utf8"), file); } catch (e) { say("BUSINESS", "bad", `cannot read ${file}: ${e.code ?? "not valid JSON"}`); process.exit(0); }
const library = Object.entries(raw?.projects ?? {}).filter(([, p]) => p && p.kind === "library").map(([k]) => k);
if (library.length) say("LIBRARY", library.join(","));
let b;
+3 -2
View File
@@ -27,6 +27,7 @@
* - No secrets in mission/task data or run records.
*/
import { parseJsonFileText } from "../packages/organization/src/util.mjs";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
@@ -74,9 +75,9 @@ function readJsonFile(file, what) {
}
let document;
try {
document = JSON.parse(fs.readFileSync(file, "utf8"));
document = parseJsonFileText(fs.readFileSync(file, "utf8"), file, what);
} catch (error) {
fail(2, `${what} is not valid JSON (${file}): ${error.message}`);
fail(2, `${what} is not valid JSON (${file})${error.code ? ` (${error.code})` : ""}`);
}
return document;
}
+22 -13
View File
@@ -32,6 +32,9 @@ for (const agent of ['darkwing', 'dewey', 'filbert', 'researcher', 'sage']) test
// fixture is its own data root so no registration reaches a real one.
cpSync(join(source, 'scripts/mosaic'), join(root, 'scripts/mosaic'));
cpSync(join(source, 'packages/seat/src'), join(root, 'packages/seat/src'), { recursive: true });
mkdirSync(join(root, 'packages/organization/src'), { recursive: true });
for (const file of ['util.mjs', 'errors.mjs', 'vocabulary.mjs'])
cpSync(join(source, 'packages/organization/src', file), join(root, 'packages/organization/src', file));
mkdirSync(join(root, '.git'));
writeFileSync(join(root, 'config.json'), JSON.stringify({ configVersion: 1, dataRoot: root }));
const registration = join(root, `seats/repo/${agent}/registration.json`);
@@ -149,40 +152,46 @@ test('default agent entry retains container setup and refuses missing seat/auth'
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.
// Observe the current processing-control handoff; its Docker behavior has
// its own package tests. Every runtime effect stays inside this fixture.
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; }
processing_guard() { echo processing >> 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
mkdirSync(join(root, 'packages/processing-control/src'), { recursive: true });
writeFileSync(join(root, 'packages/processing-control/src/container-cli.mjs'), `
import fs from 'node:fs';
fs.writeFileSync('container.json', JSON.stringify({
args: process.argv.slice(2), session: process.env.MOSAIC_SESSION_DIR ?? null,
workspace: process.env.MOSAIC_WORKSPACE ?? null, tools: process.env.MOSAIC_TOOLS,
name: process.env.MOSAIC_AGENT_NAME, interactive: process.env.MOSAIC_INTERACTIVE,
sessionSource: process.env.MOSAIC_SESSION_SOURCE, workspaceSource: process.env.MOSAIC_WORKSPACE_SOURCE
}));
`, { 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'
assert.deepEqual(readFileSync(join(root, 'trace'), 'utf8').trim().split('\n'), ['config', 'release', 'bootstrap', 'processing', 'alignment']);
assert.deepEqual(JSON.parse(readFileSync(join(root, 'container.json'))), {
args: ['--tty'], session: null,
workspace: null, tools: 'read,bash', name: 'fixture', interactive: '1',
sessionSource: join(root, 'runtime/sessions/agent-fixture'), workspaceSource: join(root, 'runtime/workspaces/fixture')
});
const captured = readFileSync(join(root, 'docker.json'), 'utf8');
const captured = readFileSync(join(root, 'container.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(readFileSync(join(root, 'container.json'), 'utf8'), captured);
assert.equal(existsSync(join(root, '.pi')), false);
} finally {
rmSync(root, { recursive: true, force: true });
+3
View File
@@ -23,6 +23,9 @@ test('Rocko uses Sonnet, isolated resume IDs, current context and launch refusal
// Seat registration (packages/seat): the fixture is its own data root.
cpSync(join(source, 'scripts/mosaic'), join(root, 'scripts/mosaic'));
cpSync(join(source, 'packages/seat/src'), join(root, 'packages/seat/src'), { recursive: true });
mkdirSync(join(root, 'packages/organization/src'), { recursive: true });
for (const file of ['util.mjs', 'errors.mjs', 'vocabulary.mjs'])
cpSync(join(source, 'packages/organization/src', file), join(root, 'packages/organization/src', file));
mkdirSync(join(root, '.git'));
writeFileSync(join(root, 'config.json'), JSON.stringify({ configVersion: 1, dataRoot: root }));
const registration = join(root, 'seats/repo/rocko/registration.json');