// 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'), 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']) { 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 sent = call('human', 'message.send', { to: 'pm', body: INTENT }); 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 { 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('bootstrap rechecks archive before reservation after asynchronous preparation', async t => { const f = await setup(t, { barrier: step => { if (step === 'begin') { const source = f.call('human', 'project.create', { name: 'Factory', repo: 'stack' }); f.call('human', 'project.archive', { project: source.id }); } } }); const state = await f.choose(); assert.equal(state.wait.code, 'project-archived'); assert.equal(existsSync(f.paths.product), false); assert.equal(f.additions().length, 0); }); test('archive during bootstrap blocks activation and preserves earlier effect receipts', async t => { const f = await setup(t, { barrier: step => { if (step === 'activate') { const project = f.rows().find(p => p.repo === 'timer'); f.call('human', 'project.archive', { project: project.id }); } } }); const state = await f.choose(); assert.equal(state.wait.code, 'project-archived'); assert.equal(state.phase, 'bootstrap'); assert.equal(f.additions().length, 1, 'already committed mapping remains'); assert.equal(f.bus.admission({ business: 'demo', project: 'timer' }).code, 'project-archived'); assert.equal(JSON.parse(readFileSync(join(f.paths.product, 'bootstrap.json'), 'utf8')).state, 'reserved'); }); test('bootstrap_connects_all_resources', async t => { const f = await setup(t), factoryHead = git(f.source, 'rev-parse', 'HEAD'), orgBefore = f.writer.read('demo'); const done = await f.choose('guided'); assert.equal(done.phase, 'discovery'); assert.equal(done.project, 'timer'); assert.equal(done.mode, 'guided'); assert.equal(done.wait.code, 'request-discovery-unavailable'); // The organization file: exactly one added mapping, everything else byte-for-byte the same document. const org = f.writer.read('demo'); assert.deepEqual(org.document.projects.timer, { root: f.paths.code, repo: f.paths.records }); assert.deepEqual(org.document.projects.stack, orgBefore.document.projects.stack); const { projects: a, ...restNow } = org.document, { projects: b, ...restBefore } = orgBefore.document; assert.deepEqual(restNow, restBefore); assert.equal(f.additions().length, 1); assert.equal(f.additions()[0].detail.plan_digest, done.plan_digest); assert.equal(f.additions()[0].after, org.revision); // The code checkout: the factory commit plus one seed commit that writes only the project file. assert.equal(git(f.paths.code, 'rev-parse', 'HEAD~1'), factoryHead); assert.equal(git(f.paths.code, 'diff', '--name-status', 'HEAD~1', 'HEAD'), 'M\t.mosaic/project.json'); assert.deepEqual(JSON.parse(readFileSync(join(f.paths.code, '.mosaic/project.json'), 'utf8')), { projectVersion: 1, id: 'timer', vars: { 'git.workingBranch': 'main' } }); assert.equal(git(f.paths.code, 'remote'), ''); assert.equal(git(f.paths.code, 'status', '--porcelain'), ''); // The records repository: layout commit, then the original intent as the owner's edit. assert.equal(git(f.paths.records, 'rev-list', '--count', 'HEAD'), '2'); assert.equal(git(f.paths.records, 'log', '-1', '--format=%an|%s'), 'jason|Edit spec/north-star.md'); assert.equal(git(f.paths.records, 'rev-parse', 'HEAD'), done.spec_commit); assert.equal(readFileSync(join(f.paths.records, 'spec/north-star.md'), 'utf8'), INTENT); // The chat association and the working repository. const chat = f.rows().find(p => p.name === 'Timer'); assert.equal(chat.repo, 'timer'); assert.equal(chat.id, done.chat_project); assert.equal(f.rows().filter(p => p.name === 'Timer').length, 1); assert.equal(git(workingRepo(f.f.dataRoot, 'demo', 'timer'), 'rev-parse', 'refs/heads/main'), git(f.paths.code, 'rev-parse', 'HEAD')); // The single tracker root stays; the selected board lives in the immutable plan and the admission projection. const plan = done.selection.bindings.find(x => x.digest === done.plan_digest).plan; assert.deepEqual(plan.tracker, { root: 1, project: 2 }); assert.deepEqual(await f.first.bus.admission({ business: 'demo', project: 'timer' }), { ready: true, revision: org.revision, code: null }); // The factory is untouched and the broker serves the new project without a restart. assert.equal(git(f.source, 'rev-parse', 'HEAD'), factoryHead); assert.equal(git(f.source, 'status', '--porcelain'), ''); assert.ok(f.call('human', 'project.create', { name: 'Side', repo: 'timer' }).id); // The next launch: a fresh boot lists the project, verifies it and still names the one tracker project. await f.first.bus.close(); const next = await f.boot(); assert.deepEqual(next.config.businesses.demo.projects.timer, { root: f.paths.code, repo: f.paths.records }); assert.equal(next.trackers.demo.project, 1); assert.deepEqual(next.warnings, []); assert.equal(loadBusiness('demo', { dir: join(f.root, 'config'), rolesDir: f.f.roles }).projects.timer.root, f.paths.code); }); test('a_product_is_not_admitted_until_every_step_passes', async t => { let seen; const f = await setup(t, { barrier: async step => { if (step === 'intent') seen = { admission: await f.first.bus.admission({ business: 'demo', project: 'timer' }) }; } }); await f.choose(); assert.equal(seen.admission.ready, false); assert.equal(typeof seen.admission.code, 'string'); assert.equal((await f.first.bus.admission({ business: 'demo', project: 'timer' })).ready, true); assert.deepEqual(await f.first.bus.admission({ business: 'demo', project: 'stack' }), { ready: true, revision: f.writer.read('demo').revision, code: null }); assert.deepEqual(await f.first.bus.admission({ business: 'demo', project: 'absent' }), { ready: false, revision: null, code: 'project-unknown' }); assert.deepEqual(await f.first.bus.admission({ business: 'other', project: 'timer' }), { ready: false, revision: null, code: 'project-unknown' }); }); // Lifecycle #800: an association or organization change can land after the last awaited read. function staleCase(name, change, { keep } = {}) { test(name, async t => { let armed = true; const f = await setup(t, { barrier: step => { if (armed && step === 'begin') { armed = false; change(f); } } }); const orgBefore = readFileSync(join(f.root, 'config/businesses/demo.json')); const out = await f.choose(); assert.equal(out.wait.code, 'request-plan-stale'); assert.equal(out.phase, 'selection'); assert.equal(out.project, null); assert.equal(out.plan_digest, null); assert.equal(out.selection, null); assert.equal(f.additions().length, 0); assert.equal(existsSync(f.paths.records), false); assert.equal(existsSync(join(f.paths.code, '.mosaic')), false); keep?.(f, orgBefore); // Nothing is retained that a fresh preview could trip over, and a re-run does not resume the old plan. assert.equal((await f.first.bus.requestLifecycle.resume('demo', f.request)).wait.code, 'request-plan-stale'); }); } staleCase('an_organization_change_after_confirmation_waits_without_effect', f => { const now = f.writer.read('demo'), next = structuredClone(now.document); next.vars['tracker.pollSeconds'] = 61; f.writer.write('demo', next, { revision: now.revision, actor: 'jason' }); }, { keep: f => assert.equal(existsSync(f.paths.product), false) }); staleCase('a_changed_source_commit_waits_without_effect', f => { writeFileSync(join(f.source, 'README.md'), 'changed\n'); git(f.source, 'commit', '-qam', 'moved'); }, { keep: f => assert.equal(existsSync(f.paths.product), false) }); staleCase('preexisting_unrelated_data_is_left_alone_and_the_plan_waits', f => { mkdirSync(f.paths.code, { recursive: true }); writeFileSync(join(f.paths.code, 'keep.txt'), 'mine\n'); }, { keep: f => assert.equal(readFileSync(join(f.paths.code, 'keep.txt'), 'utf8'), 'mine\n') }); staleCase('a_symlink_ancestor_is_refused_and_its_target_is_untouched', f => { const outside = join(f.root, 'outside'); mkdirSync(outside); symlinkSync(outside, join(f.f.dataRoot, 'products')); }, { keep: f => { assert.deepEqual(readdirSync(join(f.root, 'outside')), []); assert.ok(lstatSync(join(f.f.dataRoot, 'products')).isSymbolicLink()); } }); staleCase('a_path_overlap_with_a_declared_project_waits_without_effect', f => { const now = f.writer.read('demo'), next = structuredClone(now.document); next.projects.stack = { ...next.projects.stack, repo: join(f.paths.code, 'inner') }; writeFileSync(join(f.root, 'config/businesses/demo.json'), JSON.stringify(next, null, 2) + '\n'); }, { keep: f => assert.equal(existsSync(f.paths.product), false) }); // Kill (the barrier throws after the effect and before its receipt), reopen, recover exactly once. for (const step of ['mapping', 'intent']) { test(`kill_after_${step}_then_reopen_recovers_the_same_project_exactly_once`, async t => { let armed = true; const f = await setup(t, { barrier: s => { if (armed && s === step) { armed = false; throw new Error('killed'); } } }); const factoryHead = git(f.source, 'rev-parse', 'HEAD'); const stopped = await f.choose(); assert.equal(stopped.phase, 'bootstrap'); assert.equal(stopped.wait.code, 'bootstrap-uncertain'); assert.equal(f.writer.read('demo').document.projects.timer !== undefined, true); assert.equal((await f.first.bus.admission({ business: 'demo', project: 'timer' })).ready, false); const mapped = f.writer.read('demo').revision; await f.first.bus.close(); const next = await f.boot(); const human = next.bus.broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }); const done = await next.bus.broker.request(human, { verb: 'request.get', args: { request: f.request } }); assert.equal(done.phase, 'discovery'); if (step === 'mapping') assert.equal(f.writer.read('demo').revision, mapped, 'recovery adds no second mapping'); assert.equal(f.additions().length, 1); assert.equal(git(f.paths.records, 'rev-list', '--count', 'HEAD'), '2'); assert.equal(git(f.paths.records, 'rev-parse', 'HEAD'), done.spec_commit); assert.equal(git(f.paths.code, 'rev-list', '--count', 'HEAD'), '2'); assert.equal(git(f.paths.code, 'rev-parse', 'HEAD~1'), factoryHead); assert.equal(git(f.source, 'rev-parse', 'HEAD'), factoryHead); assert.equal(git(f.source, 'status', '--porcelain'), ''); assert.equal(next.bus.broker.request(human, { verb: 'project.list', args: {} }).filter(p => p.name === 'Timer').length, 1); assert.equal((await next.bus.admission({ business: 'demo', project: 'timer' })).ready, true); assert.equal(next.warnings.length, 0); }); } test('ordinary_organization_writers_still_refuse_every_project_change', async t => { const f = await setup(t), now = f.writer.read('demo'), next = structuredClone(now.document); next.projects.timer = { root: f.paths.code, repo: f.paths.records }; assert.throws(() => f.writer.write('demo', next, { revision: now.revision, actor: 'jason' }), { code: 'organization-projects-protected' }); assert.equal(f.writer.read('demo').revision, now.revision); }); // ---- Fix round for review r1 (#797 Task 2): C1, C2, I1 --------------------------------------------------------- const external = (root, name = 'external') => { const dir = join(root, name); mkdirSync(dir); return dir; }; const stateOf = async (f, bus = f.first.bus) => bus.broker.request(bus.broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }), { verb: 'request.get', args: { request: f.request } }); // C1. A committed `.mosaic` link is refused where the plan is prepared, so the owner never sees a plan that would write through it. test('c1_a_committed_mosaic_directory_link_is_refused_and_the_external_file_keeps_its_bytes', async t => { let victim, before; const f = await setup(t, { configureFactory: ({ root, source }) => { const outside = external(root); victim = join(outside, 'project.json'); before = JSON.stringify(PROJECT_FILE, null, 2) + '\n'; writeFileSync(victim, before); rmSync(join(source, '.mosaic'), { recursive: true }); symlinkSync(outside, join(source, '.mosaic')); git(source, 'add', '-A'); git(source, 'commit', '-qm', 'linked project metadata'); } }); assert.equal(f.proposeError?.code, 'request-project-refused'); assert.equal(f.proposed, null); assert.equal(readFileSync(victim, 'utf8'), before); assert.equal(existsSync(f.paths.product), false); assert.equal(f.additions().length, 0); }); // The config loader refuses a linked project file at boot, so this link is committed after boot: preparation is the guard. test('c1_a_committed_project_file_link_is_refused_at_preparation', async t => { let victim, before; const f = await setup(t, { beforePropose: ({ root, source }) => { const outside = external(root); victim = join(outside, 'project.json'); before = JSON.stringify(PROJECT_FILE, null, 2) + '\n'; writeFileSync(victim, before); rmSync(join(source, '.mosaic/project.json')); symlinkSync(victim, join(source, '.mosaic/project.json')); git(source, 'add', '-A'); git(source, 'commit', '-qm', 'linked project file'); } }); assert.equal(f.proposeError?.code, 'request-project-refused'); assert.equal(f.proposed, null); assert.equal(readFileSync(victim, 'utf8'), before); assert.equal(existsSync(f.paths.product), false); }); // The executor does not trust the plan: a link that appears in the checkout is refused before anything is written through it. for (const [what, link] of [ ['mosaic_directory', (code, outside) => { rmSync(join(code, '.mosaic'), { recursive: true }); symlinkSync(outside, join(code, '.mosaic')); }], ['project_file', (code, outside) => { rmSync(join(code, '.mosaic/project.json')); symlinkSync(join(outside, 'project.json'), join(code, '.mosaic/project.json')); }], ]) { test(`c1_a_${what}_link_in_the_checkout_is_refused_and_the_external_file_keeps_its_bytes`, async t => { let victim, before, f; f = await setup(t, { barrier: step => { if (step !== 'cloned') return; const outside = external(f.root); victim = join(outside, 'project.json'); before = JSON.stringify(PROJECT_FILE, null, 2) + '\n'; writeFileSync(victim, before); link(f.paths.code, outside); } }); const out = await f.choose(); assert.equal(out.phase, 'bootstrap'); assert.equal(out.wait.code, 'bootstrap-uncertain'); assert.equal(readFileSync(victim, 'utf8'), before); assert.equal(git(f.paths.code, 'rev-list', '--count', 'HEAD'), '1', 'no seed commit was made through the link'); assert.equal(f.additions().length, 0); assert.equal((await f.first.bus.admission({ business: 'demo', project: 'timer' })).ready, false); }); } test('c1_a_marker_temp_link_is_never_followed_and_the_product_is_not_admitted', async t => { let victim, f; f = await setup(t, { barrier: step => { if (step !== 'activate') return; victim = join(external(f.root), 'victim.txt'); writeFileSync(victim, 'mine\n'); symlinkSync(victim, join(f.paths.product, 'bootstrap.json.tmp')); } }); const out = await f.choose(); assert.equal(out.phase, 'bootstrap'); assert.equal(out.wait.code, 'bootstrap-uncertain'); assert.equal(readFileSync(victim, 'utf8'), 'mine\n'); assert.equal(JSON.parse(readFileSync(join(f.paths.product, 'bootstrap.json'), 'utf8')).state, 'reserved'); assert.equal((await f.first.bus.admission({ business: 'demo', project: 'timer' })).ready, false); }); test('c1_a_seed_temp_link_is_never_followed_and_the_seed_is_not_published', async t => { let victim, f; f = await setup(t, { barrier: step => { if (step !== 'reserve') return; victim = join(external(f.root), 'victim.txt'); writeFileSync(victim, 'mine\n'); symlinkSync(victim, join(f.paths.product, 'project.json.seed')); } }); const out = await f.choose(); assert.equal(out.wait.code, 'bootstrap-uncertain'); assert.equal(readFileSync(victim, 'utf8'), 'mine\n'); assert.equal(git(f.paths.code, 'rev-list', '--count', 'HEAD'), '1'); assert.equal(f.additions().length, 0); }); // C2. A path nobody claimed is never removed. test('c2_unrelated_code_tmp_contents_survive_and_the_step_is_uncertain', async t => { let f; f = await setup(t, { barrier: step => { if (step !== 'reserve') return; mkdirSync(join(f.paths.product, 'code.tmp')); writeFileSync(join(f.paths.product, 'code.tmp', 'keep.txt'), 'unrelated\n'); } }); const out = await f.choose(); assert.equal(readFileSync(join(f.paths.product, 'code.tmp', 'keep.txt'), 'utf8'), 'unrelated\n'); assert.deepEqual(readdirSync(join(f.paths.product, 'code.tmp')), ['keep.txt']); assert.equal(out.phase, 'bootstrap'); assert.equal(out.wait.code, 'bootstrap-uncertain'); assert.equal(existsSync(f.paths.code), false); assert.equal(f.additions().length, 0); const op = `bootstrap:${out.plan_digest}:seed`; assert.equal(f.first.bus.requestJournal?.effects?.('demo', f.request).filter(r => r.op === op).at(-1)?.stage ?? 'uncertain', 'uncertain'); }); // Ownership comes from the claim this plan wrote, never from how a directory looks. test('c2_an_unclaimed_empty_code_tmp_is_kept_and_not_adopted', async t => { let f; f = await setup(t, { barrier: step => { if (step === 'reserve') mkdirSync(join(f.paths.product, 'code.tmp')); } }); const out = await f.choose(); assert.equal(out.wait.code, 'bootstrap-uncertain'); assert.deepEqual(readdirSync(join(f.paths.product, 'code.tmp')), []); assert.equal(existsSync(f.paths.code), false); assert.equal(existsSync(join(f.paths.product, 'code.tmp.claim')), false); assert.equal(f.additions().length, 0); }); test('c2_an_unclaimed_clone_of_the_same_commit_is_kept_and_not_adopted', async t => { let f; f = await setup(t, { barrier: step => { if (step === 'reserve') git(f.root, 'clone', '-q', f.source, join(f.paths.product, 'code.tmp')); } }); const out = await f.choose(); assert.equal(out.wait.code, 'bootstrap-uncertain'); assert.equal(existsSync(join(f.paths.product, 'code.tmp/.git')), true); assert.equal(existsSync(f.paths.code), false); assert.equal(f.additions().length, 0); }); test('c2_a_claimed_clone_left_by_a_kill_is_adopted_exactly_once_on_retry', async t => { let armed = true; const f = await setup(t, { barrier: step => { if (armed && step === 'clone') { armed = false; throw new Error('killed'); } } }); const stopped = await f.choose(); assert.equal(stopped.wait.code, 'bootstrap-uncertain'); assert.equal(existsSync(join(f.paths.product, 'code.tmp/.git')), true); assert.equal(existsSync(f.paths.code), false); assert.equal(existsSync(join(f.paths.product, 'code.tmp.claim')), true); await f.first.bus.close(); const next = await f.boot(); const done = await stateOf(f, next.bus); assert.equal(done.phase, 'discovery'); assert.equal(existsSync(join(f.paths.product, 'code.tmp')), false); assert.equal(existsSync(join(f.paths.product, 'code.tmp.claim')), false); assert.equal(git(f.paths.code, 'rev-list', '--count', 'HEAD'), '2'); assert.equal(f.additions().length, 1); }); test('c2_a_claimed_clone_with_foreign_content_is_kept_and_reported_uncertain', async t => { let armed = true; const f = await setup(t, { barrier: step => { if (armed && step === 'clone') { armed = false; throw new Error('killed'); } } }); await f.choose(); await f.first.bus.close(); writeFileSync(join(f.paths.product, 'code.tmp/extra.txt'), 'foreign\n'); const next = await f.boot(); const out = await stateOf(f, next.bus); assert.equal(out.phase, 'bootstrap'); assert.equal(out.wait.code, 'bootstrap-uncertain'); assert.equal(readFileSync(join(f.paths.product, 'code.tmp/extra.txt'), 'utf8'), 'foreign\n'); assert.equal(existsSync(f.paths.code), false); assert.equal(f.additions().length, 0); }); // I1. The freshness check holds before every effect, not only the first one. test('i1_an_organization_change_after_reservation_waits_and_keeps_the_reserved_paths', async t => { let armed = true, f; f = await setup(t, { barrier: step => { if (armed && step === 'reserve') { armed = false; const now = f.writer.read('demo'), next = structuredClone(now.document); next.vars['tracker.pollSeconds'] = 61; f.writer.write('demo', next, { revision: now.revision, actor: 'jason' }); } } }); const out = await f.choose(); assert.equal(out.phase, 'bootstrap'); assert.equal(out.wait.code, 'request-plan-stale'); assert.equal(out.project, 'timer'); assert.equal(existsSync(join(f.paths.product, 'bootstrap.json')), true, 'completed evidence is kept'); assert.equal(existsSync(f.paths.code), false); assert.equal(existsSync(join(f.paths.product, 'code.tmp')), false); assert.equal(f.additions().length, 0); assert.deepEqual(readdirSync(f.paths.records), []); assert.equal((await f.first.bus.admission({ business: 'demo', project: 'timer' })).ready, false); assert.equal((await f.first.bus.requestLifecycle.resume('demo', f.request)).wait.code, 'request-plan-stale', 'a re-run performs no effect either'); assert.equal(existsSync(f.paths.code), false); }); test('i1_an_organization_change_after_the_mapping_stops_the_remaining_effects_across_a_restart', async t => { let armed = true, f; f = await setup(t, { barrier: step => { if (armed && step === 'mapping') { armed = false; const now = f.writer.read('demo'), next = structuredClone(now.document); next.vars['tracker.pollSeconds'] = 61; f.writer.write('demo', next, { revision: now.revision, actor: 'jason' }); } } }); const out = await f.choose(); assert.equal(out.phase, 'bootstrap'); assert.equal(out.wait.code, 'request-plan-stale'); assert.equal(f.additions().length, 1); assert.deepEqual(readdirSync(f.paths.records), [], 'no records were written on the moved organization'); await f.first.bus.close(); const next = await f.boot(); const again = await stateOf(f, next.bus); assert.equal(again.phase, 'bootstrap'); assert.equal(again.wait.code, 'request-plan-stale'); assert.deepEqual(readdirSync(f.paths.records), []); assert.equal(f.additions().length, 1); assert.equal(f.writer.read('demo').document.projects.timer !== undefined, true); assert.equal((await next.bus.admission({ business: 'demo', project: 'timer' })).ready, false); }); // I2. A product is classified by its layout, so deleting the marker cannot turn an unfinished one into an ordinary project. test('i2_a_missing_marker_does_not_admit_an_unfinished_product_after_reopen', async t => { let armed = true; const f = await setup(t, { barrier: step => { if (armed && step === 'mapping') { armed = false; throw new Error('killed'); } } }); const stopped = await f.choose(); assert.equal(stopped.wait.code, 'bootstrap-uncertain'); assert.equal(f.additions().length, 1); await f.first.bus.close(); rmSync(join(f.paths.product, 'bootstrap.json')); const entry = f.writer.read('demo').document.projects.timer; assert.equal(managedState({ dataRoot: f.f.dataRoot, business: 'demo', project: 'timer', entry }), 'reserved'); const next = await f.boot(); const admitted = await next.bus.admission({ business: 'demo', project: 'timer' }); assert.equal(admitted.ready, false); assert.equal(admitted.code, 'project-pending'); assert.equal(existsSync(join(f.paths.product, 'bootstrap.json')), false, 'recovery never rewrites a marker it did not write'); assert.equal((await stateOf(f, next.bus)).phase, 'bootstrap'); assert.equal(next.warnings.length, 0); }); // I3. Nothing is injected: the host builds its own preparation and admission from the organization, the source // checkout, the tracker settings it booted with and the staff the organization names. test('i3_a_normal_boot_reaches_a_bootstrap_selection_and_bootstraps_the_project', async t => { const f = await setup(t, { defaultServices: true }); assert.equal(f.proposeError, null); assert.equal(f.proposed.phase, 'selection'); assert.equal(f.proposed.wait.kind, 'decision'); const [auto, guided] = f.proposed.selection.bindings.map(b => b.plan); assert.deepEqual([auto.mode, guided.mode], ['autonomous', 'guided']); assert.equal(auto.source.commit, git(f.source, 'rev-parse', 'HEAD'), 'the displayed commit is the source checkout\'s'); assert.equal(auto.organization_revision, f.writer.read('demo').revision); assert.deepEqual(auto.tracker, { root: 1, project: 2 }); assert.deepEqual(auto.staff, { pm: 'pm', technical_reviewer: 'cto', coder: 'coder', reviewer: 'reviewer', chief: 'chief' }); assert.deepEqual([auto.code_root, auto.records_root], [f.paths.code, f.paths.records]); assert.equal(auto.intent.text, INTENT); const out = await f.choose(); assert.equal(out.phase, 'discovery'); assert.equal(f.additions().length, 1); assert.equal((await f.first.bus.admission({ business: 'demo', project: 'timer' })).ready, true); }); test('i3_a_missing_tracker_root_is_a_named_setup_wait_and_creates_nothing', async t => { const f = await setup(t, { defaultServices: true, withTrackers: false }); assert.equal(f.proposeError, null); assert.equal(f.proposed.phase, 'selection'); assert.equal(f.proposed.wait.code, 'request-tracker-unavailable'); assert.equal(f.proposed.selection ?? null, null, 'no plan and no decision were made up'); assert.equal(existsSync(f.paths.product), false); assert.equal(f.additions().length, 0); }); test('i3_a_staff_the_organization_does_not_name_is_a_named_setup_wait', async t => { const f = await setup(t, { defaultServices: true, configureFactory: ({ root }) => { const file = join(root, 'config/businesses/demo.json'), doc = JSON.parse(readFileSync(file, 'utf8')); doc.launch.instances = ['coder']; writeFileSync(file, JSON.stringify(doc, null, 2) + '\n'); } }); assert.equal(f.proposeError, null); assert.equal(f.proposed.wait.code, 'request-staff-unavailable'); assert.equal(f.proposed.selection ?? null, null); assert.equal(existsSync(f.paths.product), false); }); test('i3_host_admission_refuses_a_plan_whose_world_has_moved_with_a_named_code', async t => { const f = await setup(t, { defaultServices: true }), plan = f.proposed.selection.bindings[0].plan; const host = (over = {}) => hostPreparation({ dataRoot: f.f.dataRoot, organization: f.writer, trackers: { demo: { project: 1 } }, ...over }); assert.deepEqual(host().admitPlan(plan), { ready: true }); assert.deepEqual(host({ trackers: null }).admitPlan(plan), { ready: false, code: 'request-tracker-unavailable' }); assert.deepEqual(host({ trackers: { demo: { project: 7 } } }).admitPlan(plan), { ready: false, code: 'request-tracker-unavailable' }); assert.deepEqual(host({ organization: { read: () => { throw new Error('gone'); } } }).admitPlan(plan), { ready: false, code: 'request-organization-unavailable' }); assert.deepEqual(host().admitPlan({ ...plan, staff: { ...plan.staff, coder: 'reviewer' } }), { ready: false, code: 'request-staff-unavailable' }); const now = f.writer.read('demo'), next = structuredClone(now.document); next.vars['tracker.pollSeconds'] = 61; f.writer.write('demo', next, { revision: now.revision, actor: 'jason' }); assert.deepEqual(host().admitPlan(plan), { ready: false, code: 'request-organization-changed' }); assert.equal(staffFor(now.document, 'cto'), null, 'the PM cannot also be the technical reviewer'); assert.equal(staffFor({ ...now.document, launch: { ...now.document.launch, instances: ['coder', 'reviewer', 'reviewer'] } }, 'pm'), null); }); // ---- Fix round for review r2 (#802): N1, I1, I2 ------------------------------------------------------------------- // N1. The working repository is confined to the data root like every other bootstrap path. for (const [what, plant, kind] of [ ['the_working_directory_is_a_link', (f, outside) => { symlinkSync(outside, join(f.dataRoot, 'working')); return join(f.dataRoot, 'working'); }, 'link'], ['the_business_directory_under_working_is_a_link', (f, outside) => { mkdirSync(join(f.dataRoot, 'working')); symlinkSync(outside, join(f.dataRoot, 'working', 'demo')); return join(f.dataRoot, 'working', 'demo'); }, 'link'], ['the_working_directory_is_a_file', f => { writeFileSync(join(f.dataRoot, 'working'), 'not a directory\n'); return join(f.dataRoot, 'working'); }, 'file'], ]) { test(`n1_${what}_so_nothing_is_written_through_it_and_the_product_is_not_admitted`, async t => { let outside, planted; const f = await setup(t, { configureFactory: ({ root, f }) => { outside = external(root, 'external-working'); planted = plant(f, outside); } }); const out = await f.choose(); assert.equal(out.phase, 'bootstrap'); assert.equal(out.wait.code, 'bootstrap-uncertain'); assert.deepEqual(readdirSync(outside), [], 'nothing was made behind the link'); assert.equal(kind === 'link' ? lstatSync(planted).isSymbolicLink() : lstatSync(planted).isFile(), true, 'what was planted is left as it was'); assert.equal(f.additions().length, 0); assert.equal((await f.first.bus.admission({ business: 'demo', project: 'timer' })).ready, false); }); } test('n1_a_working_repository_already_there_on_the_first_attempt_is_kept_and_not_adopted', async t => { const f = await setup(t, { configureFactory: ({ f }) => { const repo = workingRepo(f.dataRoot, 'demo', 'timer'); mkdirSync(repo, { recursive: true }); writeFileSync(join(repo, 'keep.txt'), 'mine\n'); } }); const out = await f.choose(); assert.equal(out.phase, 'bootstrap'); assert.equal(out.wait.code, 'bootstrap-uncertain'); const repo = workingRepo(f.f.dataRoot, 'demo', 'timer'); assert.deepEqual(readdirSync(repo), ['keep.txt']); assert.equal(readFileSync(join(repo, 'keep.txt'), 'utf8'), 'mine\n'); assert.equal(f.additions().length, 0); assert.equal((await f.first.bus.admission({ business: 'demo', project: 'timer' })).ready, false); }); // The plan's own repository, left by a kill after the step's effect, is adopted on retry and the project completes. test('n1_a_working_repository_left_by_a_kill_is_adopted_on_retry_and_the_project_completes', async t => { let armed = true; const f = await setup(t, { barrier: step => { if (armed && step === 'working') { armed = false; throw new Error('killed'); } } }); const stopped = await f.choose(); assert.equal(stopped.wait.code, 'bootstrap-uncertain'); const repo = workingRepo(f.f.dataRoot, 'demo', 'timer'); assert.equal(git(repo, 'rev-parse', 'refs/heads/main'), git(f.paths.code, 'rev-parse', 'HEAD')); await f.first.bus.close(); const next = await f.boot(); const done = await stateOf(f, next.bus); assert.equal(done.phase, 'discovery'); assert.equal(f.additions().length, 1); assert.equal(git(repo, 'rev-parse', 'refs/heads/main'), git(f.paths.code, 'rev-parse', 'HEAD')); assert.equal((await next.bus.admission({ business: 'demo', project: 'timer' })).ready, true); }); test('n1_a_working_repository_with_a_foreign_ref_is_kept_and_not_adopted_on_retry', async t => { let armed = true; const f = await setup(t, { barrier: step => { if (armed && step === 'working') { armed = false; throw new Error('killed'); } } }); await f.choose(); await f.first.bus.close(); const repo = workingRepo(f.f.dataRoot, 'demo', 'timer'), head = git(repo, 'rev-parse', 'refs/heads/main'); git(repo, 'update-ref', 'refs/heads/foreign', head); const next = await f.boot(); const out = await stateOf(f, next.bus); assert.equal(out.phase, 'bootstrap'); assert.equal(out.wait.code, 'bootstrap-uncertain'); assert.equal(git(repo, 'rev-parse', 'refs/heads/foreign'), head, 'the foreign ref is kept'); assert.equal(f.additions().length, 0); assert.equal((await next.bus.admission({ business: 'demo', project: 'timer' })).ready, false); }); // I1. A protected write happens only while the plan is still fresh, with the check and the start of the write in one // synchronous run. These interleavings land an ordinary organization write after the step's last asynchronous read and // before its write, which is exactly where a bare check-then-await let a stale plan write. const moveOrganization = f => { const now = f.writer.read('demo'), next = structuredClone(now.document); next.vars['tracker.pollSeconds'] = 61; f.writer.write('demo', next, { revision: now.revision, actor: 'jason' }); }; // #802 C1: a refused foreign repository never becomes this attempt's on retry. for (const linked of [false, true]) { test(`c1_foreign_working_repo_stays_refused_on_retry_${linked ? 'linked_objects' : 'empty_refs'}`, async t => { let outside, repo; const f = await setup(t, { configureFactory: ({ root, f }) => { outside = external(root, 'external-objects'); repo = workingRepo(f.dataRoot, 'demo', 'timer'); mkdirSync(repo, { recursive: true }); git(repo, 'init', '-q', '--bare', '--template='); if (linked) { rmSync(join(repo, 'objects'), { recursive: true }); symlinkSync(outside, join(repo, 'objects')); } } }); assert.equal((await f.choose()).wait.code, 'bootstrap-uncertain'); assert.deepEqual(readdirSync(outside), []); // Even replacing the foreign repository with matching refs does not undo its refusal. if (!linked) git(repo, 'fetch', '-q', f.paths.code, 'HEAD:refs/heads/main', 'HEAD:refs/heads/root'); for (let retry = 0; retry < 2; retry++) { const again = await f.bus.requestLifecycle.resume('demo', f.request); assert.equal(again.phase, 'bootstrap'); assert.equal(again.wait.code, 'bootstrap-uncertain'); assert.deepEqual(readdirSync(outside), []); assert.equal(f.additions().length, 0); assert.equal(f.bus.admission({ business: 'demo', project: 'timer' }).ready, false); } }); } for (const refs of ['empty', 'matching']) { test(`c1_retry_refuses_a_replacement_working_repo_without_ownership_${refs}`, async t => { let armed = true; const f = await setup(t, { barrier: step => { if (armed && step === 'working') { armed = false; throw new Error('killed'); } } }); assert.equal((await f.choose()).wait.code, 'bootstrap-uncertain'); const repo = workingRepo(f.f.dataRoot, 'demo', 'timer'); rmSync(repo, { recursive: true }); mkdirSync(repo); git(repo, 'init', '-q', '--bare', '--template='); if (refs === 'matching') git(repo, 'fetch', '-q', f.paths.code, 'HEAD:refs/heads/main', 'HEAD:refs/heads/root'); const before = git(repo, 'for-each-ref', '--format=%(refname) %(objectname)'); const again = await f.bus.requestLifecycle.resume('demo', f.request); assert.equal(again.phase, 'bootstrap'); assert.equal(again.wait.code, 'bootstrap-uncertain'); assert.equal(git(repo, 'for-each-ref', '--format=%(refname) %(objectname)'), before); assert.equal(f.additions().length, 0); }); } test('c1_retry_checks_internal_links_before_inspecting_working_refs', async t => { let armed = true; const f = await setup(t, { barrier: step => { if (armed && step === 'working') { armed = false; throw new Error('killed'); } } }); assert.equal((await f.choose()).wait.code, 'bootstrap-uncertain'); const repo = workingRepo(f.f.dataRoot, 'demo', 'timer'), outside = external(f.root); symlinkSync(outside, join(repo, 'objects/info/linked')); let children = 0; const result = await withSpawnHook((binary, args) => { if (binary === 'git' && args.includes(`--git-dir=${repo}`)) children++; }, () => f.bus.requestLifecycle.resume('demo', f.request)); assert.equal(result.phase, 'bootstrap'); assert.equal(result.wait.code, 'bootstrap-uncertain'); assert.equal(children, 0); assert.deepEqual(readdirSync(outside), []); }); test('c1_retry_rechecks_internal_links_before_each_working_git_child', async t => { let armed = true; const f = await setup(t, { barrier: step => { if (armed && step === 'working') { armed = false; throw new Error('killed'); } } }); await f.choose(); const repo = workingRepo(f.f.dataRoot, 'demo', 'timer'), outside = external(f.root); let children = 0; const result = await withSpawnHook((binary, args) => { if (binary === 'git' && args.includes(`--git-dir=${repo}`)) { children++; if (args.includes('for-each-ref')) queueMicrotask(() => symlinkSync(outside, join(repo, 'objects/info/linked'))); } }, () => f.bus.requestLifecycle.resume('demo', f.request)); assert.equal(children, 1); assert.equal(result.wait.code, 'bootstrap-uncertain'); assert.deepEqual(readdirSync(outside), []); }); // #802 C2: these hooks run real organization writes at the review's two scheduling points. test('c2_intent_commit_holds_the_organization_lock_until_git_finishes', async t => { const f = await setup(t), revision = () => f.writer.read('demo').revision; let attempted = false, before, refusal; const result = await withSpawnHook((binary, args) => { if (!attempted && binary === 'git' && args.includes(f.paths.records) && args.includes('commit') && args.includes('Edit spec/north-star.md')) { attempted = true; before = revision(); assert.equal(readFileSync(join(f.paths.records, 'spec/north-star.md'), 'utf8'), INTENT); assert.equal(git(f.paths.records, 'rev-list', '--count', 'HEAD'), '1'); try { moveOrganization(f); } catch (e) { refusal = e.code; } } }, () => f.choose()); assert.equal(attempted, true); assert.equal(refusal, 'organization-busy'); assert.equal(revision(), before); assert.equal(result.phase, 'discovery'); assert.equal(git(f.paths.records, 'log', '-1', '--format=%s'), 'Edit spec/north-star.md'); moveOrganization(f); assert.notEqual(revision(), before, 'the lock is released after the effect'); }); test('c2_records_init_holds_the_organization_lock_across_awaits', async t => { const f = await setup(t), original = cp.spawn; let attempted = false, refusal, before; cp.spawn = function (binary, args, ...rest) { const child = original.call(this, binary, args, ...rest); if (!attempted && binary === 'git' && args.includes(f.paths.records) && args.includes('init')) { attempted = true; before = f.writer.read('demo').revision; queueMicrotask(() => { try { moveOrganization(f); } catch (e) { refusal = e.code; } }); } return child; }; syncBuiltinESMExports(); let result; try { result = await f.choose(); } finally { cp.spawn = original; syncBuiltinESMExports(); } assert.equal(attempted, true); assert.equal(refusal, 'organization-busy'); assert.equal(f.writer.read('demo').revision, before); assert.equal(result.phase, 'discovery'); assert.equal(git(f.paths.records, 'log', '--format=%s').split('\n')[1], 'Initialize project layout v1'); }); test('c2_intent_rollback_keeps_the_organization_lock_and_retry_completes', async t => { const f = await setup(t); let failed = false, refusal, before; const result = await withSpawnHook((binary, args) => { if (binary !== 'git' || !args.includes(f.paths.records)) return; if (!failed && args.includes('commit') && args.includes('Edit spec/north-star.md')) { failed = true; before = f.writer.read('demo').revision; throw new Error('injected commit spawn failure'); } if (failed && args.includes('reset')) { try { moveOrganization(f); } catch (e) { refusal = e.code; } } }, () => f.choose()); assert.equal(failed, true); assert.equal(refusal, 'organization-busy'); assert.equal(f.writer.read('demo').revision, before); assert.equal(result.wait.code, 'bootstrap-uncertain'); assert.equal(git(f.paths.records, 'rev-list', '--count', 'HEAD'), '1'); assert.equal(git(f.paths.records, 'status', '--porcelain'), ''); assert.notEqual(readFileSync(join(f.paths.records, 'spec/north-star.md'), 'utf8'), INTENT); assert.equal((await f.bus.requestLifecycle.resume('demo', f.request)).phase, 'discovery'); }); test('c3_broker_reload_microtask_only_serves_after_the_activate_receipt', async t => { const f = await setup(t), broker = f.bus.broker, original = broker.reloadBusiness; let observed; broker.reloadBusiness = function (...args) { const result = original.apply(this, args); queueMicrotask(() => { try { observed = { active: activations(f), admission: admission(f), made: f.call('human', 'project.create', { name: 'After receipt', repo: 'timer' }) }; } catch (error) { observed = { error }; } }); return result; }; let result; try { result = await f.choose(); } finally { broker.reloadBusiness = original; } assert.ok(observed); assert.ifError(observed.error); assert.equal(observed.active.length, 1); assert.equal((await observed.admission).ready, true); assert.ok(observed.made.id); assert.equal(result.phase, 'discovery'); }); test('c3_activate_receipt_io_failure_keeps_broker_fenced_until_retry', async t => { const f = await setup(t), original = DatabaseSync.prototype.prepare; let faulted = false, servedBeforeReceipt = false; DatabaseSync.prototype.prepare = function (sql, ...rest) { const statement = original.call(this, sql, ...rest); if (sql.startsWith('INSERT INTO request_effects(')) { const run = statement.run; statement.run = function (...args) { if (!faulted && String(args[4]).endsWith(':activate') && args[7] === 'committed') { faulted = true; try { f.call('human', 'project.create', { name: 'Before receipt', repo: 'timer' }); servedBeforeReceipt = true; } catch {} throw Object.assign(new Error('injected activation receipt I/O error'), { code: 'SQLITE_IOERR' }); } return run.apply(this, args); }; } return statement; }; let result; try { result = await f.choose(); } finally { DatabaseSync.prototype.prepare = original; } assert.equal(faulted, true); assert.equal(result.phase, 'bootstrap'); assert.equal(result.wait.code, 'bootstrap-uncertain'); assert.equal(marked(f), 'complete'); assert.deepEqual(activations(f), []); assert.equal((await admission(f)).ready, false); assert.throws(() => f.call('human', 'project.create', { name: 'After failed receipt', repo: 'timer' })); assert.equal(servedBeforeReceipt, false); assert.equal((await f.bus.requestLifecycle.resume('demo', f.request)).phase, 'discovery'); assert.equal(activations(f).length, 1); assert.equal((await admission(f)).ready, true); assert.ok(f.call('human', 'project.create', { name: 'After retry', repo: 'timer' }).id); }); // `before(binary, args)` runs just ahead of every child the code spawns while `during` runs. async function withSpawnHook(before, during) { const original = cp.spawn; cp.spawn = function (binary, args, ...rest) { before(binary, args); return original.call(this, binary, args, ...rest); }; syncBuiltinESMExports(); try { return await during(); } finally { cp.spawn = original; syncBuiltinESMExports(); } } test('i1_an_organization_write_during_the_intent_reads_leaves_no_intent_commit_under_the_stale_plan', async t => { let ready = false, changed = false, f; f = await setup(t, { barrier: step => { if (step === 'records') ready = true; } }); const out = await withSpawnHook((binary, args) => { if (ready && !changed && binary === 'git' && args.includes(f.paths.records) && args.includes('rev-list') && args.includes('--count')) { changed = true; moveOrganization(f); } }, () => f.choose()); assert.equal(changed, true, 'the organization write landed between the reads and the write'); assert.equal(out.phase, 'bootstrap'); assert.equal(out.wait.code, 'request-plan-stale'); const star = join(f.paths.records, 'spec/north-star.md'); assert.notEqual(existsSync(star) ? readFileSync(star, 'utf8') : null, INTENT, 'the intent was not written'); assert.equal(git(f.paths.records, 'rev-list', '--count', 'HEAD'), '1'); assert.equal(git(f.paths.records, 'log', '-1', '--format=%s'), 'Initialize project layout v1'); assert.equal(git(f.paths.records, 'status', '--porcelain'), ''); assert.equal((await f.first.bus.admission({ business: 'demo', project: 'timer' })).ready, false); }); test('i1_an_organization_write_during_the_seed_reads_leaves_the_checkout_unseeded', async t => { let changed = false, f; f = await setup(t); const out = await withSpawnHook((binary, args) => { if (!changed && binary === 'git' && args.includes(f.paths.code) && args.includes('status')) { changed = true; moveOrganization(f); } }, () => f.choose()); assert.equal(changed, true); assert.equal(out.phase, 'bootstrap'); assert.equal(out.wait.code, 'request-plan-stale'); assert.equal(git(f.paths.code, 'rev-list', '--count', 'HEAD'), '1', 'no seed commit was made'); assert.equal(readFileSync(join(f.paths.code, '.mosaic/project.json'), 'utf8'), JSON.stringify(PROJECT_FILE, null, 2) + '\n'); assert.equal(git(f.paths.code, 'status', '--porcelain'), ''); assert.equal(f.additions().length, 0); }); // I2. The complete marker is published before the broker admits the project, and admission needs both the marker and the // committed activation receipt, live and after a restart. const activations = f => { const database = new DatabaseSync(join(f.f.dataRoot, 'bus/bus.sqlite'), { readOnly: true }); try { return database.prepare('SELECT op, stage FROM request_effects WHERE business = ? AND request = ?').all('demo', f.request).filter(r => r.op.endsWith(':activate') && r.stage === 'committed'); } finally { database.close(); } }; const marked = f => JSON.parse(readFileSync(join(f.paths.product, 'bootstrap.json'), 'utf8')).state; const admission = f => f.first.bus.admission({ business: 'demo', project: 'timer' }); test('i2_a_failed_marker_publication_ends_the_step_before_the_broker_admits_the_project', async t => { const f = await setup(t), marker = join(f.paths.product, 'bootstrap.json'); const original = fs.renameSync; let faults = 1; fs.renameSync = function (from, to, ...rest) { if (faults && from === `${marker}.tmp` && to === marker) { faults--; throw Object.assign(new Error('injected final publication I/O failure'), { code: 'EIO' }); } return original.call(this, from, to, ...rest); }; syncBuiltinESMExports(); let out; try { out = await f.choose(); } finally { fs.renameSync = original; syncBuiltinESMExports(); } assert.equal(faults, 0, 'the fault fired'); assert.equal(out.phase, 'bootstrap'); assert.equal(out.wait.code, 'bootstrap-uncertain'); assert.equal(marked(f), 'reserved'); const pending = await admission(f); assert.equal(pending.ready, false); assert.equal(pending.code, 'project-pending'); assert.deepEqual(activations(f), [], 'no activation receipt was committed'); assert.throws(() => f.call('human', 'project.create', { name: 'Side', repo: 'timer' }), 'the broker does not serve the project'); // The fault is gone: a retry publishes the marker, admits the project and commits the receipt. const again = await f.first.bus.requestLifecycle.resume('demo', f.request); assert.equal(again.phase, 'discovery'); assert.equal(marked(f), 'complete'); assert.equal((await admission(f)).ready, true); assert.equal(activations(f).length, 1); assert.ok(f.call('human', 'project.create', { name: 'Side', repo: 'timer' }).id); }); // Defence in depth: even if the executor's marker write did nothing, the runtime refuses to admit a product whose marker // is not complete. test('i2_the_runtime_refuses_to_admit_a_product_whose_marker_is_not_complete', async t => { const f = await setup(t), marker = join(f.paths.product, 'bootstrap.json'); const original = fs.renameSync; let skipped = 0; fs.renameSync = function (from, to, ...rest) { if (from === `${marker}.tmp` && to === marker) { skipped++; return undefined; } return original.call(this, from, to, ...rest); }; syncBuiltinESMExports(); let out; try { out = await f.choose(); } finally { fs.renameSync = original; syncBuiltinESMExports(); } assert.ok(skipped >= 1, 'the marker rename was skipped'); assert.equal(out.phase, 'bootstrap'); assert.equal(marked(f), 'reserved'); assert.equal((await admission(f)).ready, false); assert.deepEqual(activations(f), []); assert.throws(() => f.call('human', 'project.create', { name: 'Side', repo: 'timer' })); }); test('i2_a_completed_product_whose_marker_is_removed_is_not_ready_live_or_after_reopen', async t => { const f = await setup(t), marker = join(f.paths.product, 'bootstrap.json'); assert.equal((await f.choose()).phase, 'discovery'); assert.equal((await admission(f)).ready, true); const bytes = readFileSync(marker); // Live: the broker is already serving the project, and admission still needs the marker. rmSync(marker); const live = await admission(f); assert.equal(live.ready, false); assert.equal(live.code, 'project-pending'); writeFileSync(marker, bytes, { mode: 0o600 }); assert.equal((await admission(f)).ready, true, 'the marker back makes it ready again'); // After a restart: the committed receipt alone admits nothing, and the marker is not rewritten. await f.first.bus.close(); rmSync(marker); const next = await f.boot(); const reopened = await next.bus.admission({ business: 'demo', project: 'timer' }); assert.equal(reopened.ready, false); assert.equal(reopened.code, 'project-pending'); assert.equal(existsSync(marker), false, 'recovery never rewrites a marker it did not write'); assert.equal(next.warnings.length, 0); await next.bus.close(); writeFileSync(marker, bytes, { mode: 0o600 }); const last = await f.boot(); assert.equal((await last.bus.admission({ business: 'demo', project: 'timer' })).ready, true); });