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.
869 lines
54 KiB
JavaScript
869 lines
54 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'), 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);
|
|
});
|