git mv packages/business -> packages/organization (@mosaic/organization). The old package stays as one-line re-export shims of each moved module, with src/business.mjs re-exporting src/organization-config.mjs and src/cli.mjs forwarding to the moved executable. BusinessError is OrganizationConfigError (one constructor, name kept BusinessError); OrganizationError stays distinct. The bus adapter gains organization-context.mjs (busOrganization), with business.mjs (busBusiness) as the same function. Import consumers, manifests, the lockfile, scripts/mosaic, stack-copy.sh COPY_PATHS (both roots), seat sandbox RUNNER_FILES and the copy closure walker (organization-imports.mjs, both roots required) move to the new path. v1 serialization, v1 writers and the stage A fixtures are unchanged; the stage A contract tests stay where they were and pass as before.
77 lines
4.3 KiB
JavaScript
77 lines
4.3 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { organizationWriter, setReplyLimits } from '../src/organization.mjs';
|
|
import { validateBusinessDocument } from '../src/organization-config.mjs';
|
|
import { businessDoc, rolesCopy, tmp, writeJson } from './helpers.mjs';
|
|
|
|
function fixture(t, human = 'jason') {
|
|
const root = tmp(); t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
const doc = businessDoc(root); doc.human = human; doc.replyLimits = { requireCitation: false, citationsPerReply: 20 };
|
|
const dir = join(root, 'config'), file = writeJson(join(dir, 'businesses/acme.json'), doc), rolesDir = rolesCopy(root);
|
|
const writer = organizationWriter({ dir, rolesDir });
|
|
const next = structuredClone(doc); next.vars['tracker.pollSeconds'] = 61;
|
|
writer.write('acme', next, { revision: writer.read('acme').revision, actor: human });
|
|
return { root, file, writer, rolesDir, log: writer.logFile('acme') };
|
|
}
|
|
|
|
function refused(f, actor, change) {
|
|
const before = readFileSync(f.file), log = readFileSync(f.log), { document, revision } = f.writer.read('acme');
|
|
change(document);
|
|
assert.throws(() => f.writer.write('acme', document, { revision, actor }), { code: 'organization-reply-limits-human-only' });
|
|
assert.deepEqual(readFileSync(f.file), before); assert.deepEqual(readFileSync(f.log), log);
|
|
}
|
|
|
|
test('C2-06 M11 human replacement works; omitted settings return to defaults', t => {
|
|
const f = fixture(t);
|
|
setReplyLimits(f.writer, 'acme', { revision: f.writer.read('acme').revision, replyLimits: { requireCitation: true } }, 'jason');
|
|
assert.deepEqual(f.writer.read('acme').document.replyLimits, { requireCitation: true });
|
|
setReplyLimits(f.writer, 'acme', { revision: f.writer.read('acme').revision, replyLimits: {} }, 'jason');
|
|
assert.deepEqual(f.writer.read('acme').document.replyLimits, {});
|
|
});
|
|
|
|
test('C2-06 M11 agents and tester cannot change settings, including by changing human', t => {
|
|
const f = fixture(t);
|
|
for (const actor of ['chief-of-staff.chief', 'tester', 'coder.coder'])
|
|
refused(f, actor, d => { d.replyLimits.requireCitation = true; });
|
|
refused(f, 'chief-of-staff.chief', d => { d.human = 'chief-of-staff.chief'; d.replyLimits.requireCitation = true; });
|
|
const dotted = fixture(t, 'coder.coder');
|
|
refused(dotted, 'coder.coder', d => { d.replyLimits.citationsPerReply = 1; });
|
|
});
|
|
|
|
test('C2-06 M11 serialization cannot replace settings after the first fence', t => {
|
|
const f = fixture(t);
|
|
refused(f, 'chief-of-staff.chief', d => {
|
|
Object.defineProperty(d.replyLimits, 'toJSON', { value: () => ({ requireCitation: true }) });
|
|
});
|
|
});
|
|
|
|
// E1: unlike tester/role actors, mallory passes the other actor-shape exclusions.
|
|
// Comparing document.human instead of old.human must fail this case.
|
|
test('C2-06 M11 an unrelated actor cannot appoint itself human and change reply limits', t => {
|
|
const f = fixture(t);
|
|
refused(f, 'mallory', d => { d.human = 'mallory'; d.replyLimits.requireCitation = true; });
|
|
});
|
|
|
|
test('C2-06 invalid setter values preserve the business and changes.jsonl', t => {
|
|
const f = fixture(t), before = readFileSync(f.file), log = readFileSync(f.log);
|
|
for (const replyLimits of [{ citationsPerReply: 0 }, { citationsPerReply: 1001 }, { requireCitation: 'true' }, { invented: true }, []]) {
|
|
assert.throws(() => setReplyLimits(f.writer, 'acme', { revision: f.writer.read('acme').revision, replyLimits }, 'jason'), { code: 'invalid-request' });
|
|
assert.deepEqual(readFileSync(f.file), before); assert.deepEqual(readFileSync(f.log), log);
|
|
}
|
|
});
|
|
|
|
test('C2-06 validator accepts hand-edited settings; raw reader fails closed on invalid files', t => {
|
|
const f = fixture(t), doc = f.writer.read('acme').document;
|
|
for (const replyLimits of ['true', 1, null, [true], {}, { requireCitation: 'true' }, { citationsPerReply: 0 }]) {
|
|
const next = { ...doc, replyLimits };
|
|
assert.doesNotThrow(() => validateBusinessDocument(next, f.file, { rolesDir: f.rolesDir }));
|
|
writeJson(f.file, next); assert.deepEqual(f.writer.readRaw('acme').document.replyLimits, replyLimits);
|
|
}
|
|
for (const bad of ['{broken', 'null', '[]', '1']) {
|
|
writeFileSync(f.file, bad); assert.deepEqual(f.writer.readRaw('acme'), { readable: false });
|
|
}
|
|
rmSync(f.file); assert.deepEqual(f.writer.readRaw('acme'), { readable: false });
|
|
});
|