// RR (#1873): the file writer refuses arbiters without their domain's right. 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 } from '../src/organization.mjs'; import { loadRole, validateBusinessDocument } from '../src/index.mjs'; import { businessDoc, REPO_ROLES, rolesCopy, tmp, writeJson } from './helpers.mjs'; for (const [domain, instance, definition] of [['technical', 'cto', 'cto'], ['delivery', 'pm', 'pm']]) { test(`RR: naming ${instance} without decision.resolve.${domain} refuses when written`, t => { const root = tmp(); t.after(() => rmSync(root, { recursive: true, force: true })); const rolesDir = rolesCopy(root), file = join(root, 'businesses/acme.json'); writeJson(file, businessDoc(root)); const writer = organizationWriter({ dir: root, rolesDir }); const { document, revision } = writer.read('acme'), before = readFileSync(file, 'utf8'); const roleFile = join(rolesDir, `${definition}.json`), role = JSON.parse(readFileSync(roleFile, 'utf8')); role.authority.withinRole = role.authority.withinRole.filter(a => a !== `decision.resolve.${domain}`); writeJson(roleFile, role); assert.throws(() => writer.write('acme', document, { revision, actor: 'jason' }), error => { assert.match(error.message, new RegExp(`arbiters\\.${domain}.*${instance}.*role definition ${definition}.*decision\\.resolve\\.${domain}`)); return true; }); assert.equal(readFileSync(file, 'utf8'), before); }); } test('RR: a coder named as arbiter refuses when written', t => { const root = tmp(); t.after(() => rmSync(root, { recursive: true, force: true })); const file = join(root, 'businesses/acme.json'); writeJson(file, businessDoc(root)); const writer = organizationWriter({ dir: root, rolesDir: REPO_ROLES }); const { document, revision } = writer.read('acme'), before = readFileSync(file, 'utf8'); document.arbiters.delivery = 'coder'; assert.throws(() => writer.write('acme', document, { revision, actor: 'jason' }), /arbiters\.delivery.*coder.*role definition coder.*decision\.resolve\.delivery/); assert.equal(readFileSync(file, 'utf8'), before); }); test('RR: a cross-role resolve grant cannot qualify an arbiter', t => { const root = tmp(); t.after(() => rmSync(root, { recursive: true, force: true })); const rolesDir = rolesCopy(root), roleFile = join(rolesDir, 'cto.json'); const role = JSON.parse(readFileSync(roleFile, 'utf8')); role.authority.withinRole = role.authority.withinRole.filter(a => a !== 'decision.resolve.technical'); role.authority.crossRole.push('decision.resolve.technical'); writeJson(roleFile, role); assert.throws(() => validateBusinessDocument(businessDoc(root), join(root, 'businesses/acme.json'), { rolesDir }), /cto.*decision\.resolve\.technical/); }); test('RR: a version 2 contract without its Authority section refuses', t => { const root = tmp(); t.after(() => rmSync(root, { recursive: true, force: true })); const rolesDir = rolesCopy(root); writeFileSync(join(rolesDir, 'coder.md'), '# Role contract: coder\n\n## Duties\nRead only.\n'); assert.throws(() => loadRole(rolesDir, 'coder'), /coder.*## Authority/); }); for (const [domain, instance] of [['technical', 'cto'], ['delivery', 'pm']]) { for (const layer of ['agent', 'business', 'intersection']) { test(`RR: ${layer} limits removing ${instance}'s ${domain} resolve right refuse when written`, t => { const root = tmp(); t.after(() => rmSync(root, { recursive: true, force: true })); const file = join(root, 'businesses/acme.json'); writeJson(file, businessDoc(root)); const writer = organizationWriter({ dir: root, rolesDir: REPO_ROLES }); const { document, revision } = writer.read('acme'), before = readFileSync(file, 'utf8'); const right = `decision.resolve.${domain}`; const rights = ['message.send', 'decision.resolve.technical', 'decision.resolve.delivery']; if (layer === 'agent') document.roles[instance].vars['limits.authority'] = ['message.send']; else document.vars['limits.authority'] = rights.filter(value => value !== right); if (layer === 'intersection') document.roles[instance].vars['limits.authority'] = rights; assert.throws(() => writer.write('acme', document, { revision, actor: 'jason' }), error => { assert.match(error.message, new RegExp(`arbiters\\.${domain}.*${instance}.*role definition ${instance}.*decision\\.resolve\\.${domain}`)); return true; }); assert.equal(readFileSync(file, 'utf8'), before); }); } } test('RR: narrowed arbiters retaining their domain rights can be written', t => { const root = tmp(); t.after(() => rmSync(root, { recursive: true, force: true })); const file = join(root, 'businesses/acme.json'); writeJson(file, businessDoc(root)); const writer = organizationWriter({ dir: root, rolesDir: REPO_ROLES }); const { document, revision } = writer.read('acme'); document.vars['limits.authority'] = ['message.send', 'decision.resolve.technical', 'decision.resolve.delivery']; for (const [domain, instance] of [['technical', 'cto'], ['delivery', 'pm']]) { document.roles[instance].vars['limits.authority'] = ['message.send', `decision.resolve.${domain}`]; } writer.write('acme', document, { revision, actor: 'jason' }); assert.deepEqual(writer.read('acme').document.arbiters, document.arbiters); });