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.
105 lines
7.2 KiB
JavaScript
105 lines
7.2 KiB
JavaScript
// Lifecycle Task 2 (#797 / Gitea #1839): a confirmed new-project selection becomes a usable project.
|
|
// Real startBroker over a real organization file, a real git factory repo and a fixture data root.
|
|
// The test barrier stands in for a kill: it throws after a step's effect and before its receipt.
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import cp, { execFileSync } from 'node:child_process';
|
|
import fs, { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
import { syncBuiltinESMExports } from 'node:module';
|
|
import { DatabaseSync } from 'node:sqlite';
|
|
import { join } from 'node:path';
|
|
import { startBroker } from '../src/runtime.mjs';
|
|
import { prepareBootstrap } from '../src/request-bootstrap.mjs';
|
|
import { hostPreparation, staffFor } from '../src/request-host.mjs';
|
|
import { bootConfig, loadSystem } from '../../cli/src/config.mjs';
|
|
import { fixture, tmp } from '../../cli/tests/helpers.mjs';
|
|
import { organizationWriter } from '../../organization/src/organization.mjs';
|
|
import { loadBusiness } from '../../organization/src/organization-config.mjs';
|
|
import { managedState } from '../../organization/src/project-bootstrap.mjs';
|
|
import { workingRepo } from '../../seat/src/working.mjs';
|
|
|
|
const INTENT = 'Build a single-file timer.\nKeep these exact bytes.\n';
|
|
const GIT_ENV = { ...process.env, GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_SYSTEM: '/dev/null', GIT_AUTHOR_NAME: 'Factory', GIT_AUTHOR_EMAIL: '[email protected]',
|
|
GIT_COMMITTER_NAME: 'Factory', GIT_COMMITTER_EMAIL: '[email protected]' };
|
|
const git = (cwd, ...args) => execFileSync('git', args, { cwd, env: GIT_ENV, encoding: 'utf8' }).trim();
|
|
const PROJECT_FILE = { projectVersion: 1, id: 'stack', vars: { 'tracker.project': 1, 'git.workingBranch': 'main' } };
|
|
|
|
// The factory: the declared `stack` project is a git repository with a project file.
|
|
function factory(root) {
|
|
const dir = join(root, 'project');
|
|
mkdirSync(join(dir, '.mosaic'), { recursive: true });
|
|
writeFileSync(join(dir, '.mosaic/project.json'), JSON.stringify(PROJECT_FILE, null, 2) + '\n');
|
|
writeFileSync(join(dir, 'README.md'), 'factory\n');
|
|
git(dir, 'init', '-q', '-b', 'main'); git(dir, 'add', '-A'); git(dir, 'commit', '-q', '-m', 'factory');
|
|
return dir;
|
|
}
|
|
|
|
// `defaultServices` boots with nothing injected, so the host builds its own preparation and admission; `withTrackers` forwards the
|
|
// boot config's tracker settings as process.mjs does (the adapter itself is not built).
|
|
async function setup(t, { barrier, configureFactory = () => {}, beforePropose = () => {}, defaultServices = false, withTrackers = defaultServices } = {}) {
|
|
const root = tmp(t, 'bootstrap-'), f = fixture(root, 'demo',d=>{d.roles.assistant={definition:'assistant'};return d;}), source = factory(root);
|
|
configureFactory({ root, f, source });
|
|
const hooks = { barrier };
|
|
const system = loadSystem({ env: f.env });
|
|
const writer = organizationWriter({ dir: join(root, 'config'), rolesDir: f.roles });
|
|
const plans = (dataRoot, ready = () => true) => ({
|
|
preparePlans: (state, args, from) => Promise.all(['autonomous', 'guided'].map(mode => prepareBootstrap({
|
|
business: writer.read('demo').document, request: state.request, sourceProject: args.source_project, project: args.project, name: args.name,
|
|
mode, dataRoot, organizationRevision: writer.read('demo').revision, tracker: { root: 1, project: args.tracker_project },
|
|
staff: { pm: state.pm, technical_reviewer: 'cto', coder: 'coder', reviewer: 'reviewer', chief: 'chief' },
|
|
intent: { text: from.text, source_message: from.source_message } }))),
|
|
admitPlan: plan => plan.organization_revision === writer.read('demo').revision && ready() ? { ready: true } : { ready: false, code: 'organization-stale' },
|
|
barrier: step => hooks.barrier?.(step),
|
|
});
|
|
const boot = async () => {
|
|
const warnings = [];
|
|
const { trackers, ...config } = JSON.parse(JSON.stringify(bootConfig({ system, businessId: 'demo', env: f.env, warn: w => warnings.push(w) })));
|
|
const bus = await startBroker({ ...config, ...(withTrackers ? { trackers } : {}), requestServices: defaultServices ? {} : plans(f.dataRoot) });
|
|
t.after(() => bus.close().catch(() => {}));
|
|
return { bus, config, trackers, warnings };
|
|
};
|
|
const first = await boot();
|
|
const caps = { human: first.bus.broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }) };
|
|
for (const role of ['pm', 'cto', 'coder', 'chief']) {
|
|
caps[role] = first.bus.broker.bindLaunch({ business: 'demo', role, run: role + '-run', harness: 'pi' });
|
|
first.bus.broker.request(caps[role], { verb: 'role.claim' });
|
|
}
|
|
const call = (who, verb, args = {}) => first.bus.broker.request(caps[who], { verb, args });
|
|
const originProject=call('human','project.create',{name:'Origin intake'}).id;
|
|
const thread=call('human','thread.create',{project:originProject,name:'Intake'}).id;
|
|
caps.assistant=first.bus.broker.bindLaunch({business:'demo',role:'assistant',run:'assistant-run',harness:'pi',thread});
|
|
call('assistant','role.claim');
|
|
const sent=call('human','thread.send',{thread,body:INTENT});
|
|
const relay=call('assistant','message.send',{to:'chief',in_reply_to:sent.id,body:'Original request '+sent.request});
|
|
call('chief','message.send',{to:'pm',in_reply_to:relay.id,body:'Open original request '+sent.request});
|
|
const opened = await call('pm', 'request.open', { request: sent.request });
|
|
// A source the preparation refuses never reaches a decision; the refusal is the outcome, so setup hands it back.
|
|
let proposed = null, proposeError = null;
|
|
beforePropose({ root, f, source });
|
|
try {
|
|
proposed = await call('pm', 'request.propose', { request: sent.request, revision: opened.revision, op: 'plan-1', source_project: 'stack',
|
|
project: 'timer', name: 'Timer', tracker_project: 2 });
|
|
} catch (e) { proposeError = e; }
|
|
const choose = async (choice = 'guided') => {
|
|
call('human', 'decision.resolve', { id: proposed.selection.decision, choice });
|
|
return first.bus.requestLifecycle.resume('demo', sent.request);
|
|
};
|
|
const paths = { code: join(f.dataRoot, 'products/demo/timer/code'), records: join(f.dataRoot, 'projects/demo/timer'), product: join(f.dataRoot, 'products/demo/timer') };
|
|
const changes = () => { try { return readFileSync(writer.logFile('demo'), 'utf8').trim().split('\n').map(l => JSON.parse(l)); } catch { return []; } };
|
|
const additions = () => changes().filter(c => c.state === 'committed' && c.detail?.verb === 'project.add');
|
|
return { originProject, thread, root, f, system, source, writer, hooks, boot, bus: first.bus, first, call, caps, request: sent.request, proposed, proposeError, choose, paths, changes, additions,
|
|
rows: () => first.bus.broker.request(caps.human, { verb: 'project.list', args: {} }) };
|
|
}
|
|
|
|
|
|
test('archiving the originating chat during bootstrap must fence the next effect',async t=>{
|
|
const f=await setup(t,{barrier: step=>{
|
|
if(step==='reserve')f.call('human','project.archive',{project:f.originProject});
|
|
}});
|
|
const state=await f.choose();
|
|
console.log('ORIGIN_BOOTSTRAP',JSON.stringify({phase:state.phase,wait:state.wait,codeExists:existsSync(f.paths.code),additions:f.additions().length}));
|
|
assert.equal(state.wait?.code,'project-archived');
|
|
assert.equal(existsSync(f.paths.code),false);
|
|
assert.equal(f.additions().length,0);
|
|
});
|