Files
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

65 lines
4.3 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { consoleReads } from '../src/reads.mjs';
import { recordBootRefused, recordBootOk } from '../../cli/src/boot-status.mjs';
import { createOrg, templateRender, showOrg, archiveOrg } from '../../organization/src/org-service.mjs';
import { busStopForDelete } from '../../cli/src/org-bus.mjs';
import { listOrgs, resolveOrg } from '../../organization/src/org-registry.mjs';
import { organizationConsole } from '../src/organizations.mjs';
test('admission errors are visible while a healthy organization remains usable', async t => {
const root = mkdtempSync(join(tmpdir(), 'org-admission-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
const configDir = join(root, 'config'), dataRoot = join(root, 'data');
mkdirSync(configDir, { mode: 0o700 }); mkdirSync(dataRoot, { mode: 0o700 });
const principal = { provider: 'local-dev', iss: 'local-dev', sub: 'local-dev-owner' };
const ctx = { configDir, dataRoot, instance: 'readtest', principal };
for (const id of ['alpha', 'beta']) await createOrg(ctx, id, await templateRender(id));
const file = join(configDir, 'org-runtime.json'), registry = JSON.parse(readFileSync(file, 'utf8'));
registry.orgs.alpha.digest = '0'.repeat(64); writeFileSync(file, JSON.stringify(registry));
const console = organizationConsole({ ctx, listOrgs, showOrg, resolveOrg, orgPaths: () => ({}), busStatus: async () => ({ healthy: true }), options: ({ id }) => ({ id }) });
const rows = await console.list(principal);
assert.equal(rows.find(row => row.id === 'alpha')?.invalid, true);
assert.match(rows.find(row => row.id === 'alpha')?.reason, /document and registry disagree/);
assert.equal((await console.resolve('beta', principal)).id, 'beta');
});
test('an owner whose only organization is archived gets an empty list, not an error', async t => {
const root = mkdtempSync(join(tmpdir(), 'org-archived-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
const configDir = join(root, 'config'), dataRoot = join(root, 'data');
mkdirSync(configDir, { mode: 0o700 }); mkdirSync(dataRoot, { mode: 0o700 });
const principal = { provider: 'local-dev', iss: 'local-dev', sub: 'local-dev-owner' };
const ctx = { configDir, dataRoot, instance: 'readtest', principal };
await createOrg(ctx, 'alpha', await templateRender('alpha'));
const { revision } = await showOrg(ctx, 'alpha');
await archiveOrg(ctx, 'alpha', { ifRevision: revision, busStop: busStopForDelete });
const console = organizationConsole({ ctx, listOrgs, showOrg, resolveOrg, orgPaths: () => ({}), busStatus: async () => ({ healthy: true }), options: ({ id }) => ({ id }) });
assert.deepEqual(await console.list(principal), []);
});
test('offline boot status comes from the selected organization root', async t => {
const root = mkdtempSync(join(tmpdir(), 'org-reads-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
const alpha = join(root, 'alpha'), beta = join(root, 'beta');
recordBootOk(alpha, { business: 'alpha', file: 'alpha.json' });
recordBootRefused(beta, { business: 'beta', file: 'beta.json', reason: 'bad config' });
const reads = orgRoot => consoleReads({ root, system: { dataRoot: root }, orgRoot, business: 'alpha' });
assert.equal((await reads(alpha).busStatus()).boot.business, 'alpha');
assert.equal((await reads(beta).busStatus()).boot.business, 'beta');
});
test('settings reads the notifier choice created under the selected organization root', async t => {
const root = mkdtempSync(join(tmpdir(), 'org-reads-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
const configDir = join(root, 'config'), dataRoot = join(root, 'data');
mkdirSync(configDir, { mode: 0o700 }); mkdirSync(dataRoot, { mode: 0o700 });
const ctx = { configDir, dataRoot, instance: 'readtest', principal: { provider: 'local-dev', iss: 'local-dev', sub: 'local-dev-owner' } };
const created = await createOrg(ctx, 'alpha', await templateRender('alpha'));
const reads = consoleReads({ root, configDir, system: { dataRoot }, orgRoot: created.root, business: 'alpha' });
assert.deepEqual((await reads.settings()).notifier, { binding: null });
});