Files
stack/packages/organization/tests/library-projects-protected.test.mjs
jason.woltje a8d9eecf65 organization: move packages/business to packages/organization (rename r2 stage B1)
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.
2026-11-09 11:47:48 -06:00

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