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

97 lines
6.1 KiB
JavaScript

// Row 176: full fixture config, credential sentinels and Git project; never the user's config.
import { mkdirSync, readFileSync, writeFileSync, symlinkSync } from 'node:fs';
import { join } from 'node:path';
import { execFileSync } from 'node:child_process';
import { DatabaseSync } from 'node:sqlite';
import { fixture, tmp } from '../../cli/tests/helpers.mjs';
import { loadSystem, bootConfig } from '../../cli/src/config.mjs';
import { loadBusiness } from '../../organization/src/index.mjs';
import { writeJson } from '../../organization/tests/helpers.mjs';
import { initProject } from '../../project/src/index.mjs';
import { startBroker } from '../../bus/src/runtime.mjs';
import { Broker, BusError } from '../../bus/src/broker.mjs';
import { Store } from '../../bus/src/store.mjs';
import { BusReadError } from '../src/bus.mjs';
import { views } from '../../bus/src/views.mjs';
import { startTesterSocket } from '../src/tester-socket.mjs';
import { createChat } from '../src/chat.mjs';
import { close } from './fixture.mjs';
export const TOKEN = 'row176-fixture-token-sentinel';
export const VARIABLE = 'ROW176_FIXTURE_CREDENTIAL_VARIABLE';
export const OUTSIDE = 'row176-outside-target-must-never-be-read';
export async function testerFixture(t, { enabled = true } = {}) {
const root = tmp(t, 'console-tester-');
const f = fixture(root, 'acme', doc => {
doc.vars['bus.tester'] = enabled;
doc.projects.stack.repo = join(root, 'records');
return doc;
});
const file = join(root, 'config/businesses/acme.json');
const tokenPath = f.doc.roles.pm.credentials.gitea.file;
writeFileSync(tokenPath, TOKEN, { mode: 0o600 });
const old = process.env[VARIABLE]; process.env[VARIABLE] = TOKEN;
t.after(() => { if (old === undefined) delete process.env[VARIABLE]; else process.env[VARIABLE] = old; });
f.doc.roles.coder.credentials.gitea = { env: VARIABLE, rotateBy: '2099-01-01' };
writeJson(file, f.doc);
const business = loadBusiness('acme', { dir: join(root, 'config'), rolesDir: f.roles });
await initProject({ business, project: 'stack', dataRoot: f.dataRoot });
writeFileSync(join(root, 'outside.md'), OUTSIDE);
symlinkSync(join(root, 'outside.md'), join(root, 'records/docs/out.md'));
execFileSync('git', ['-c', 'core.hooksPath=/dev/null', 'add', 'docs/out.md'], { cwd: join(root, 'records') });
execFileSync('git', ['-c', 'core.hooksPath=/dev/null', '-c', 'maintenance.auto=false', '-c', 'gc.auto=0', 'commit', '-m', 'Fixture symlink'], {
cwd: join(root, 'records'), env: { ...process.env, GIT_AUTHOR_NAME: 'jason', GIT_AUTHOR_EMAIL: 'fixture@invalid', GIT_COMMITTER_NAME: 'jason', GIT_COMMITTER_EMAIL: 'fixture@invalid' },
});
mkdirSync(f.dataRoot, { recursive: true, mode: 0o700 });
const runtime = await startBroker(bootConfig({ system: loadSystem({ env: f.env }), businessId: 'acme', env: f.env }));
t.after(() => runtime.close());
const calls = [];
const call = async (verb, args = {}) => {
calls.push([verb, args]);
try { return structuredClone(await runtime.broker.request(runtime.broker.bindTester({ business: 'acme' }), { verb, args })); }
catch (err) { if (err instanceof BusError) throw new BusReadError(err.code); throw err; }
};
const bus = { ...views({ call }), names: () => call('names'), instances: () => call('instances'), routes: () => call('routes') };
const lines = [];
const web = await startTesterSocket({ dataRoot: f.dataRoot, board: 'http://127.0.0.1:9', tester: true, testerAuthHeader: 'x-user', bus,
reads: { settings: async () => ({ business: 'acme', release: 'fixture', notifier: {}, launch: await call('limits') }) },
projectFiles: args => call('project.files', args), projectFile: args => call('project.file', args),
instancesWrite: args => call('instances.set', args), routesWrite: args => call('routes.set', args), nameWrite: args => call('name.set', args),
log: line => lines.push(line),
});
t.after(() => close(web));
const history = () => readFileSync(join(root, 'config/businesses/.history/acme/changes.jsonl'), 'utf8').trim().split('\n').map(JSON.parse);
const nameRows = () => {
const db = new DatabaseSync(join(f.dataRoot, 'bus/bus.sqlite'), { readOnly: true });
try { return db.prepare('SELECT name,by FROM instance_names').all(); } finally { db.close(); }
};
return { root, f, file, tokenPath, runtime, calls, call, web, lines, history, nameRows };
}
// Row 177 (#1675): a tester listener whose chat calls a plain broker as the tester. The shared
// fixture has no assistant role, and other tests count its roles, so the chat gets its own business.
export async function testerChat(t, { enabled = true } = {}) {
const root = tmp(t, 'console-tester-chat-');
const store = new Store(root);
t.after(() => store.close());
const none = { authority: { withinRole: ['message.send'], crossRole: [] } };
const broker = new Broker({ store, businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' },
vars: { 'bus.tester': enabled }, roles: { assistant: { definition: 'assistant', ...none }, chief: { definition: 'chief-of-staff', ...none } },
definitions: { assistant: {}, 'chief-of-staff': {} } } } });
const human = (verb, args = {}) => broker.request(broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }), { verb, args });
const calls = [];
const chat = createChat({ call: async (verb, args) => {
calls.push(verb);
try {
// Bound at each call, as the tester CLI is, so bus.tester off refuses before any read.
const tester = broker.bindTester({ business: 'demo' });
if (verb === 'plugins') return { business: 'demo', revision: 'a'.repeat(64), plugins: { channel: ['cli', 'webui'] } };
return structuredClone(broker.request(tester, { verb, args }));
} catch (err) { if (err instanceof BusError) throw new BusReadError(err.code); throw err; }
} });
const lines = [];
const web = await startTesterSocket({ dataRoot: root, board: 'http://127.0.0.1:9', tester: true, testerAuthHeader: 'x-user', chat, log: line => lines.push(line) });
t.after(() => close(web));
return { store, broker, human, calls, web, lines };
}