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 }); });