// C2-04 M9: use the writer directly, rather than relying on broker key filtering. import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { organizationWriter } from '../src/organization.mjs'; import { businessDoc, rolesCopy, tmp, writeJson } from './helpers.mjs'; import { mappingEntry } from '../../library/tests/helpers.mjs'; function fixture(t) { const root = tmp(); t.after(() => rmSync(root, { recursive: true, force: true })); const doc = businessDoc(root); const books = mappingEntry(); books.universes.hybrid = { kind: 'hybrid', name: 'An invented crossover', sources: ['harbor', 'second'], books: ['Hybrid Book'], playground: [], constraints: [], notes: 'Fixture only' }; doc.projects = { books, ...doc.projects }; doc.roles.chief.credentials = { gitea: { ...doc.roles.coder.credentials.gitea } }; const dir = join(root, 'config'), file = writeJson(join(dir, 'businesses/acme.json'), doc); const writer = organizationWriter({ dir, rolesDir: rolesCopy(root) }); // Give both snapshots an existing ordinary committed receipt to preserve. doc.vars['tracker.pollSeconds'] = 61; writer.write('acme', doc, { revision: writer.read('acme').revision, actor: 'jason' }); const log = writer.logFile('acme'); return { root, file, doc, writer, log }; } function protectedWrite(f, change) { const before = readFileSync(f.file), logBefore = readFileSync(f.log); const next = structuredClone(f.writer.read('acme').document); change(next); assert.throws(() => f.writer.write('acme', next, { revision: f.writer.read('acme').revision, actor: 'jason' }), { code: 'organization-projects-protected' }); assert.deepEqual(readFileSync(f.file), before); assert.deepEqual(readFileSync(f.log), logBefore); } test('C2-04 M9: changed playground and hybrid entries leave business and changes.jsonl byte-identical', t => { const f = fixture(t); protectedWrite(f, next => { next.projects.books.universes.harbor.playground = ['Book One/New Playground/']; }); protectedWrite(f, next => { next.projects.books.universes.hybrid.notes = 'Changed hybrid rule'; }); }); test('C2-04 M9: code projects, additions, deletions and array changes are protected too', t => { const f = fixture(t); protectedWrite(f, next => { next.projects.stack.root = join(f.root, 'different-code'); }); protectedWrite(f, next => { next.projects.extra = { root: join(f.root, 'extra') }; }); protectedWrite(f, next => { delete next.projects.stack; }); protectedWrite(f, next => { next.projects.books.universes.hybrid.sources.reverse(); }); }); test('C2-04 M9: canonical key order is allowed and a non-project setter still records its change', t => { const f = fixture(t), old = f.writer.read('acme'); const next = structuredClone(old.document); next.projects = Object.fromEntries(Object.entries(next.projects).reverse()); next.vars['tracker.pollSeconds'] = 62; f.writer.write('acme', next, { revision: old.revision, actor: 'jason' }); assert.equal(f.writer.read('acme').document.vars['tracker.pollSeconds'], 62); const receipt = JSON.parse(readFileSync(f.log, 'utf8').trim().split('\n').at(-1)); assert.equal(receipt.state, 'committed'); assert.deepEqual(receipt.changes.map(c => c.path), ['vars.tracker.pollSeconds']); }); test('C2-04 M9: a serialization hook cannot change projects after the canonical comparison', t => { const f = fixture(t); protectedWrite(f, next => { const changed = structuredClone(next.projects); changed.books.universes.hybrid.notes = 'Serialized change'; Object.defineProperty(next.projects, 'toJSON', { value: () => changed }); }); }); test('C2-15: an organization write that adds a hybrid universe with exclude and pin constraints is protected', t => { const f = fixture(t); protectedWrite(f, next => { next.projects.books.universes.cross = { kind: 'hybrid', name: 'An invented crossover', sources: ['harbor', 'second'], books: ['Crossover'], playground: [], notes: 'Fixture only', constraints: [{ exclude: 'second/Characters' }, { pin: { entity: 'Book One/Characters/Mara Vell.md', field: 'name', value: 'Mara Vell' } }] }; }); });