Files
stack/packages/organization/tests/org-service.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

101 lines
9.8 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { templateRender, validateOrg } from '../src/org-service.mjs';
test('local template validates with the shipped business loader', async () => { const doc = await templateRender('alpha', { fixture: 'local' }); assert.equal(doc.human, 'local-dev-owner'); assert.deepEqual(await validateOrg(doc), { ok: true, errors: [] }); });
import { mkdtempSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { createOrg, showOrg, updateOrg, queueInit, queueShow, projectRepoInit, projectRepoShow, notificationShow } from '../src/org-service.mjs';
import { loadBusiness } from '../src/organization-config.mjs';
import { REPO_ROLES } from './helpers.mjs';
import { organizationWriter } from '../src/organization.mjs';
import { watch } from 'node:fs';
import { writeFile } from 'node:fs/promises';
function context() { const root = mkdtempSync(join(tmpdir(), 'org-service-')); const configDir = join(root, 'config'), dataRoot = join(root, 'data'); mkdirSync(configDir, { mode: 0o700 }); mkdirSync(dataRoot, { mode: 0o700 }); return { configDir, dataRoot, instance: 'test', principal: { provider: 'local-dev', iss: 'local-dev', sub: 'local-dev-owner' } }; }
test('fixture template includes the runnable local model route', async () => {
const doc = await templateRender('alpha', { fixture: 'local' });
assert.deepEqual(doc.plugins, { channel: ['cli', 'webui'], harness: ['pi'], 'model-provider': ['fixture'] });
assert.deepEqual(doc.roles.assistant.vars, { harness: 'pi', model: 'fixture-deterministic', thinking: 'off' });
assert.equal(doc.launch.models['fixture-deterministic'].module, 'fixture');
assert.equal(doc.vars['bus.tester'], true);
});
test('creation records the explicit no-notifications choice privately', async () => {
const c = context();
const result = await createOrg(c, 'alpha', await templateRender('alpha'));
const file = join(result.root, 'notify/alpha/notify.json');
assert.deepEqual(JSON.parse(readFileSync(file, 'utf8')), { notifyVersion: 1, binding: null });
assert.equal(statSync(file).mode & 0o777, 0o600);
assert.equal(result.notifications, 'none');
assert.equal((await notificationShow(c, 'alpha')).currentNotifications, 'none');
writeFileSync(file, JSON.stringify({ notifyVersion: 1, binding: 'alerts' }));
const later = await notificationShow(c, 'alpha');
assert.equal(later.notifications, 'none');
assert.equal(later.currentNotifications, 'configured');
});
test('create loads using existing loader and revisions fence updates', async () => { const c = context(); const doc = await templateRender('alpha', { fixture: 'local' }); await createOrg(c, 'alpha', doc); assert.equal(loadBusiness('alpha', { dir: c.configDir, rolesDir: REPO_ROLES }).projects.work.root, join(c.dataRoot, 'org-runtime/alpha/projects/work')); const current = await showOrg(c, 'alpha'); await updateOrg(c, 'alpha', current.doc, { ifRevision: current.revision }); await assert.rejects(updateOrg(c, 'alpha', current.doc, { ifRevision: current.revision }), { code: 'revision-conflict' }); });
test('git setup requires explicit risk and refuses overwrite', async () => { const c = context(); await createOrg(c, 'alpha', await templateRender('alpha')); await assert.rejects(queueInit(c, 'alpha', { mode: 'local-only' })); const q = await queueInit(c, 'alpha', { mode: 'local-only', acceptLocalLossRisk: true }); assert.equal(q.riskAccepted, true); assert.equal((await queueShow(c, 'alpha')).root, q.root); await assert.rejects(queueInit(c, 'alpha', { mode: 'local-only', acceptLocalLossRisk: true })); const p = await projectRepoInit(c, 'alpha', 'work', { mode: 'local-only', acceptLocalLossRisk: true }); assert.equal((await projectRepoShow(c, 'alpha', 'work')).root, p.root); await assert.rejects(projectRepoInit(c, 'alpha', 'work', { mode: 'local-only', acceptLocalLossRisk: true })); });
test('invalid paths and unrecognized principals refuse', async () => { const c = context(); const doc = await templateRender('alpha'); doc.projects.work.root = '/etc'; await assert.rejects(createOrg(c, 'alpha', doc)); c.principal = { provider: 'x', iss: 'x', sub: 'someone' }; await assert.rejects(createOrg(c, 'beta', await templateRender('beta'))); });
test('validation identifies an arbitrary project root', async () => { const doc = await templateRender('alpha'); doc.projects.work.root = '/etc'; const result = await validateOrg(doc); assert.equal(result.ok, false); assert.equal(result.errors[0].path, '$.projects.work.root'); });
test('remote setup records the URL and never pushes', async () => { const c = context(); await createOrg(c, 'alpha', await templateRender('alpha')); const remote = 'https://example.invalid/team/repo'; const q = await queueInit(c, 'alpha', { mode: 'remote', remote }); assert.equal((await queueShow(c, 'alpha')).remote, remote); assert.equal(q.mode, 'remote'); const p = await projectRepoInit(c, 'alpha', 'work', { mode: 'remote', remote }); assert.equal((await projectRepoShow(c, 'alpha', 'work')).remote, remote); assert.equal(p.mode, 'remote'); });
test('a prior receipt fences reinitialization even when the repository disappears', async () => { const c = context(); await createOrg(c, 'alpha', await templateRender('alpha')); const created = await queueInit(c, 'alpha', { mode: 'local-only', acceptLocalLossRisk: true }); const { rmSync } = await import('node:fs'); rmSync(created.root, { recursive: true }); await assert.rejects(queueInit(c, 'alpha', { mode: 'local-only', acceptLocalLossRisk: true })); });
test('broker organization writer updates the registry revision and remains readable', async () => {
const c = context(); await createOrg(c, 'alpha', await templateRender('alpha', { fixture: 'local' }));
const before = await showOrg(c, 'alpha');
const writer = organizationWriter({ dir: c.configDir, rolesDir: REPO_ROLES });
const { document, revision } = writer.read('alpha'); document.plugins.channel = ['cli'];
assert.ok(writer.write('alpha', document, { revision, actor: c.principal.sub }).transaction);
const after = await showOrg(c, 'alpha');
assert.deepEqual(after.doc.plugins.channel, ['cli']); assert.equal(after.revision, before.revision + 1);
});
test('reading beta does not contend with an alpha document writer', async () => {
const c = context();
for (const id of ['alpha', 'beta']) await createOrg(c, id, await templateRender(id, { fixture: 'local' }));
const writer = organizationWriter({ dir: c.configDir, rolesDir: REPO_ROLES });
const { document, revision } = writer.read('alpha'); document.plugins.channel = ['cli'];
let attempted = false, error;
const attempt = () => { if (attempted) return; attempted = true; try { writer.write('alpha', document, { revision, actor: c.principal.sub }); } catch (e) { error = e; } };
const watcher = watch(c.configDir, (_event, name) => { if (name === 'org-runtime.lock') attempt(); });
try { await showOrg(c, 'beta'); attempt(); } finally { watcher.close(); }
if (error) throw error;
assert.deepEqual((await showOrg(c, 'alpha')).doc.plugins.channel, ['cli']);
});
test('setup never overwrites a broker write committed while git initializes', async () => {
const c = context(); await createOrg(c, 'alpha', await templateRender('alpha', { fixture: 'local' }));
const writer = organizationWriter({ dir: c.configDir, rolesDir: REPO_ROLES });
const dir = join(c.dataRoot, 'git-wrapper'); mkdirSync(dir, { mode: 0o700 });
const ready = join(dir, 'ready'), release = join(dir, 'release');
const wrapper = `#!${process.execPath}\nimport {writeFileSync,existsSync,watch} from 'node:fs';\nimport {spawnSync} from 'node:child_process';\nif(process.argv.slice(2).includes('init')){writeFileSync(${JSON.stringify(ready)},'ready');if(!existsSync(${JSON.stringify(release)}))await new Promise(resolve=>{const w=watch(${JSON.stringify(dir)},()=>{if(existsSync(${JSON.stringify(release)})){w.close();resolve()}});if(existsSync(${JSON.stringify(release)})){w.close();resolve()}})}\nconst p=spawnSync('/usr/bin/git',process.argv.slice(2),{stdio:'inherit'});process.exit(p.status??1);\n`;
writeFileSync(join(dir, 'git'), wrapper, { mode: 0o700 });
const oldPath = process.env.PATH; process.env.PATH = `${dir}:${oldPath}`;
let watcher; const waiting = new Promise(resolve => { watcher = watch(dir, () => { try { readFileSync(ready); watcher.close(); resolve(); } catch {} }); });
const pending = projectRepoInit(c, 'alpha', 'work', { mode: 'local-only', acceptLocalLossRisk: true });
try {
await Promise.race([waiting, new Promise((_, reject) => setTimeout(() => reject(Error('git init timeout')), 10000).unref())]);
const { document, revision } = writer.read('alpha'); document.plugins.channel = ['cli'];
assert.ok(writer.write('alpha', document, { revision, actor: c.principal.sub }).transaction);
await writeFile(release, 'release');
await pending.catch(e => { if (e.code !== 'revision-conflict') throw e; });
assert.deepEqual((await showOrg(c, 'alpha')).doc.plugins.channel, ['cli']);
} finally { process.env.PATH = oldPath; watcher?.close(); await writeFile(release, 'release'); await pending.catch(() => {}); }
});
test('service updates wait for the broker writer asynchronous document hold', async () => {
const c = context(); await createOrg(c, 'alpha', await templateRender('alpha'));
const current = await showOrg(c, 'alpha');
const writer = organizationWriter({ dir: c.configDir, rolesDir: REPO_ROLES });
let release; const gate = new Promise(resolve => { release = resolve; });
const held = writer.holdAsync('alpha', () => gate);
let settled = false;
const update = updateOrg(c, 'alpha', current.doc, { ifRevision: current.revision }).finally(() => { settled = true; });
try {
await new Promise(resolve => setTimeout(resolve, 100));
assert.equal(settled, false, 'service must not publish during a broker document hold');
} finally { release(); await held; }
await update;
});