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.
79 lines
4.1 KiB
JavaScript
79 lines
4.1 KiB
JavaScript
// 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' } }] };
|
|
});
|
|
});
|