// 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: 'f@example.test', GIT_COMMITTER_NAME: 'Factory', GIT_COMMITTER_EMAIL: 'f@example.test' }; 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); });