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