Files
stack/packages/bus/tests/prd-activation.test.mjs
jason.woltje e6ab8bc1fd feat(bus,plugins,organization,cli,webui): SP9 remove system types outright, bus schema 3v-3w drops project_system_types (#1873)
Jason's 2026-11-12 ruling: the only deployed system is the dev environment on
dragon-lin, so the system-type plug-in kind goes with no retired kind, fence,
conversion or new refusal code.

- plugins: system-type leaves KINDS; system-types.mjs and the five modules are
  deleted. An org file naming the kind refuses unknown-kind.
- organization: the plugins.system-type check, systemTypesFence and the
  system-types.console right are removed.
- bus: system-types.console, project.system-types[.set], the system-type half
  of the tester check, the runtime reads and system-type-not-enabled are
  removed. Step 3v-3w drops project_system_types, its index, its four guards
  and its rows; SCHEMA_VERSION is 3w (32 tables, 126 triggers, 5 views).
- cli: mosaic org system-types and mosaic project system-types[-set] are
  removed; stack-release COUNTED and the budget supervisor's closed list move
  to 3w.
- channel: the project.system-types verbs are removed.
- webui: the System types page, route, dashboard row, chips and project
  selection are removed.
- docs: REQ-SYS-1/2 now describe starter packs; README system-type text and
  the requirements titles follow.
2026-11-12 21:02:46 -06:00

335 lines
26 KiB
JavaScript

import test from 'node:test';
import { DatabaseSync } from 'node:sqlite';
import assert from 'node:assert/strict';
import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { startBroker } from '../src/runtime.mjs';
import { bootConfig, loadSystem } from '../../cli/src/config.mjs';
import { fixture, tmp } from '../../cli/tests/helpers.mjs';
import { prdFixture, git } from '../../project/tests/prd-fixture.mjs';
import { createRequestStore } from '../src/request-store.mjs';
async function setup(t, mode = 'autonomous') {
const root = tmp(t, 'activation-'), f = fixture(root, 'demo');
const source = join(root, 'project'); mkdirSync(join(source, '.mosaic'), { recursive: true });
writeFileSync(join(source, '.mosaic/project.json'), JSON.stringify({ projectVersion: 1, id: 'stack', vars: { 'tracker.project': 1 } }));
git(source, 'init', '-qb', 'main'); git(source, 'add', '-A'); git(source, 'commit', '-qm', 'Factory');
const system = loadSystem({ env: f.env });
const boot = async () => {
const config = bootConfig({ system, businessId: 'demo', env: f.env, warn: () => {} });
const bus = await startBroker(config); t.after(() => bus.close().catch(() => {})); return bus;
};
const bus = await boot(), caps = {};
const db = new DatabaseSync(join(f.dataRoot, 'bus/bus.sqlite'), { readOnly: true }); t.after(() => db.close());
const store = { get: (sql, ...a) => db.prepare(sql).get(...a), all: (sql, ...a) => db.prepare(sql).all(...a) };
const bind = b => {
caps.human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
caps.console = b.bindConsole({ business: 'demo', human: 'jason', subject: { provider: 'test', iss: 'test', sub: 'owner' } });
caps.reader = b.bindReader({ business: 'demo' });
for (const role of ['pm','cto','coder']) { caps[role] = b.bindLaunch({ business: 'demo', role, run: role + '-run', harness: 'pi' }); b.request(caps[role], { verb: 'role.claim' }); }
};
bind(bus.broker);
const call = (who, verb, args = {}) => bus.broker.request(caps[who], { verb, args });
const sent = call('human', 'message.send', { to: 'pm', body: 'Build a timer.\n' });
let state = await call('pm', 'request.open', { request: sent.request });
state = await call('pm', 'request.propose', { request: sent.request, revision: state.revision, op: 'plan', source_project: 'stack', project: 'timer', name: 'Timer', tracker_project: 2 });
call('human', 'decision.resolve', { id: state.selection.decision, choice: mode });
state = await bus.requestLifecycle.resume('demo', sent.request);
const context = { business: { id: 'demo', human: 'jason', projects: { timer: { root: join(f.dataRoot, 'products/demo/timer/code') } } }, project: 'timer', dataRoot: f.dataRoot };
const p = await prdFixture(t, context);
const args = { request: sent.request, revision: state.revision, op: 'prd1', prd_version: '1.0', records_commit: p.args.records_commit, prd_path: p.args.prd_path, requirement_ids: p.args.requirement_ids };
const submit = () => call('pm', 'prd.submit', args);
const approve = (submission, who = mode === 'guided' ? 'human' : 'cto') => call(who, 'decision.resolve', { id: submission.decision.id, choice: 'yes' });
const activate = submission => bus.requirementCatalog.activate({ request: sent.request, decision: submission.decision.id });
return { ...p, f, bus, caps, call, state, request: sent.request, args, submit, approve, activate, boot, bind, config: bootConfig({ system, businessId: 'demo', env: f.env, warn: () => {} }), store, journal: createRequestStore(store) };
}
test('autonomous PM submits technical PRD to distinct CTO, no task before atomic idempotent activation', async t => {
const f = await setup(t), submission = await f.submit();
assert.equal(submission.decision.action, 'prd.approve'); assert.equal(submission.decision.route_to, 'cto');
assert.equal(submission.decision.authorization.target.startsWith('prd-'), true);
assert.equal(submission.decision.spec_commit, submission.candidate.spec_commit); assert.equal(submission.decision.blocking, false);
assert.equal(f.store.get('SELECT count(*) n FROM task_snapshots').n, 0);
await assert.rejects(f.activate(submission), { code: 'prd-not-approved' });
assert.throws(() => f.call('pm', 'decision.resolve', { id: submission.decision.id, choice: 'yes' }), { code: 'not-resolver' });
f.approve(submission);
f.call('cto', 'role.release');
const snap = await f.activate(submission);
assert.equal(snap.requirements[0].text, 'A working timer.'); assert.equal(snap.origin, 'product');
assert.deepEqual(await f.activate(submission), snap);
assert.equal(f.store.get('SELECT count(*) n FROM request_snapshots').n, 1);
assert.equal(f.journal.readRequest('demo', f.request).phase, 'planning');
assert.equal(f.journal.effects('demo', f.request).filter(e => e.kind === 'prd-activate' && e.stage === 'committed').length, 1);
assert.equal((await f.bus.broker.requirementsFor(f.caps.pm, { project_key: 'timer', requirement_snapshot: snap.id, requirement: 'REQ-POMO-1' })).id, snap.id);
});
test('guided routes the same PRD action to owner without a routing citation', async t => {
const f = await setup(t, 'guided'), sub = await f.submit();
assert.equal(sub.decision.route_to, 'human'); assert.equal(sub.decision.spec_commit, null);
assert.throws(() => f.approve(sub, 'cto'), { code: 'human-required' });
f.approve(sub); assert.equal((await f.activate(sub)).approval.by, 'jason');
});
test('submit is PM-only, strict, revision checked, idempotent and size rejected before a decision', async t => {
const f = await setup(t);
for (const who of ['coder','cto','human','reader']) await assert.rejects(Promise.resolve().then(() => f.call(who, 'prd.submit', f.args)));
await assert.rejects(f.call('pm', 'prd.submit', { ...f.args, prd_blob: 'forged' }));
await assert.rejects(f.call('pm', 'prd.submit', { ...f.args, revision: 1 }), { code: 'request-revision-conflict' });
const count = f.store.get('SELECT count(*) n FROM decisions').n;
const records_commit = f.save('docs/prd-1.0.md', 'x'.repeat(32769));
await assert.rejects(f.call('pm', 'prd.submit', { ...f.args, records_commit }), { code: 'prd-candidate-size' });
assert.equal(f.store.get('SELECT count(*) n FROM decisions').n, count);
});
for (const path of ['docs/prd-1.0.md','requirements/REQ-POMO-1.json','spec/north-star.md']) test(`activation rereads current selected content and intent: ${path}`, async t => {
const f = await setup(t), sub = await f.submit(); f.approve(sub);
f.save(path, path.endsWith('.json') ? JSON.stringify({ recordVersion: 1, id: 'REQ-POMO-1', title: 'Other', text: 'Other', status: 'accepted' }) : 'Changed\n');
await assert.rejects(f.activate(sub));
assert.equal(f.store.get('SELECT count(*) n FROM request_snapshots').n, 0);
});
test('forged accepted status is no authority, wrong project and business cannot borrow a snapshot', async t => {
const f = await setup(t);
f.args.records_commit = f.save('requirements/REQ-POMO-1.json', JSON.stringify({ recordVersion: 1, id: 'REQ-POMO-1', title: 'Timer', text: 'A working timer.', status: 'accepted' }), 'pm.pm');
await assert.rejects(f.bus.broker.requirementsFor(f.caps.pm, { project_key: 'timer' }), { code: 'requirement-snapshot-required' });
const sub = await f.submit(); f.approve(sub); const snap = await f.activate(sub);
await assert.rejects(f.bus.broker.requirementsFor(f.caps.pm, { project_key: 'stack', requirement_snapshot: snap.id }));
await assert.rejects(f.bus.requirementCatalog.resolve({ business: 'other', project: 'timer', snapshot: snap.id }));
});
test('fresh runtime resolves identical approved snapshot from committed records and journal alone', async t => {
const f = await setup(t), sub = await f.submit(); f.approve(sub); const snap = await f.activate(sub);
await f.bus.close(); const next = await f.boot();
assert.deepEqual(await next.requirementCatalog.resolve({ business: 'demo', project: 'timer', snapshot: snap.id }), snap);
});
test('owner-only explicit revocation rejects stale revisions and makes pending work wait for replacement', async t => {
const f = await setup(t), sub = await f.submit(); f.approve(sub); const snap = await f.activate(sub);
const args = { project: 'timer', snapshot: snap.id, revision: 1, reason: 'Replace requirements', op: 'revoke1' };
for (const who of ['pm','human','reader']) assert.throws(() => f.call(who, 'prd.revoke.console', args), { code: 'human-required' });
await assert.rejects(f.call('console', 'prd.revoke.console', { ...args, revision: 0 }));
const revoked = await f.call('console', 'prd.revoke.console', args);
assert.deepEqual(await f.call('console', 'prd.revoke.console', args), revoked);
await assert.rejects(f.bus.broker.requirementsFor(f.caps.pm, { project_key: 'timer', requirement_snapshot: snap.id }), { code: 'requirement-snapshot-revoked' });
assert.equal(f.journal.readRequest('demo', f.request).wait.code, 'requirement-snapshot-revoked');
});
test('mode and spec changes invalidate admissions and active snapshots', async t => {
const f = await setup(t), sub = await f.submit(); f.approve(sub);
let state = f.journal.readRequest('demo', f.request);
await f.call('console', 'request.mode.console', { request: f.request, revision: state.revision, mode_revision: state.mode_revision, mode: 'guided', op: 'mode1' });
await assert.rejects(f.activate(sub));
state = f.journal.readRequest('demo', f.request);
const next = await f.call('pm', 'prd.submit', { ...f.args, revision: state.revision, op: 'prd2' });
f.approve(next, 'human'); const snap = await f.activate(next);
const spec = await f.call('human', 'project.file', { project: 'timer', path: 'spec/north-star.md' });
await f.call('human', 'project.file.save', { project: 'timer', path: spec.path, base: spec.commit, content: 'Changed intent\n' });
await assert.rejects(f.bus.requirementCatalog.resolve({ business: 'demo', project: 'timer', snapshot: snap.id }), { code: 'requirement-snapshot-revoked' });
});
for (const fault of ['missing-authority','self-instance','same-definition']) test(`autonomous arbiter ${fault} is a setup wait without a decision`, async t => {
const f = await setup(t), b = structuredClone(f.config.businesses.demo);
if (fault === 'missing-authority') b.roles.cto.authority.withinRole = b.roles.cto.authority.withinRole.filter(a => a !== 'decision.resolve.technical');
if (fault === 'self-instance') b.arbiters.technical = 'pm';
if (fault === 'same-definition') b.roles.cto.definition = 'pm';
f.bus.broker.reloadBusiness('demo', b);
const count = f.store.get('SELECT count(*) n FROM decisions').n, result = await f.submit();
assert.equal(result.wait.kind, 'setup'); assert.match(result.wait.code, /^prd-(arbiter-unavailable|self-approval)$/);
assert.equal(f.store.get('SELECT count(*) n FROM decisions').n, count);
});
test('activation rechecks current role authority and cannot use another candidate decision', async t => {
const f = await setup(t), sub = await f.submit(); f.approve(sub);
await assert.rejects(f.bus.requirementCatalog.activate({ request: f.request, decision: f.state.selection.decision }), { code: 'prd-candidate-required' });
const b = structuredClone(f.config.businesses.demo); b.roles.cto.authority.withinRole = b.roles.cto.authority.withinRole.filter(a => a !== 'decision.resolve.technical');
f.bus.broker.reloadBusiness('demo', b);
await assert.rejects(f.activate(sub), { code: 'prd-arbiter-unavailable' });
assert.equal(f.store.get('SELECT count(*) n FROM request_snapshots').n, 0);
});
test('submission replay has one decision and rejects a changed operation binding', async t => {
const f = await setup(t), sub = await f.submit();
assert.deepEqual(await f.submit(), sub);
await assert.rejects(f.call('pm', 'prd.submit', { ...f.args, prd_version: '2.0' }), { code: 'request-operation-conflict' });
});
test('changed current intent is refused before submission creates a decision', async t => {
const f = await setup(t), count = f.store.get('SELECT count(*) n FROM decisions').n;
f.save('spec/north-star.md', 'Changed spec\n');
await assert.rejects(f.submit(), { code: 'stale-spec' });
assert.equal(f.store.get('SELECT count(*) n FROM decisions').n, count);
});
test('activation rollback leaves no snapshot, receipt or continuation after an insert failure', async t => {
const f = await setup(t), sub = await f.submit();
// A host fixture trigger injects a failure after the snapshot insert, before its activation event.
const db = new DatabaseSync(join(f.f.dataRoot, 'bus/bus.sqlite'));
db.exec("CREATE TRIGGER reject_activation BEFORE INSERT ON request_snapshot_events BEGIN SELECT RAISE(ABORT,'fixture-interruption'); END");
f.approve(sub);
await assert.rejects(f.activate(sub));
assert.equal(f.store.get('SELECT count(*) n FROM request_snapshots').n, 0);
assert.equal(f.journal.effects('demo', f.request).filter(e => e.kind === 'prd-activate').length, 0);
assert.notEqual(f.journal.readRequest('demo', f.request).phase, 'planning');
db.exec('DROP TRIGGER reject_activation'); db.close();
});
test('owner draft read shows exact bound PRD and requirements without a generic file endpoint', async t => {
const f = await setup(t), sub = await f.submit();
const draft = await f.call('console', 'request.draft.console', { request: f.request });
assert.deepEqual(draft.candidate, sub.candidate); assert.equal(draft.prd, '# Timer 1.0\nA timer.\n'); assert.equal(draft.requirements[0].id, 'REQ-POMO-1');
for (const who of ['pm','human','reader']) assert.throws(() => f.call(who, 'request.draft.console', { request: f.request }), { code: 'human-required' });
});
test('owner draft snapshot counter follows activation and revoke independently of the request revision', async t => {
const f = await setup(t);
const read = () => f.call('console', 'request.draft.console', { request: f.request });
assert.deepEqual(await read(), { candidate: null, prd: null, requirements: [], snapshot: { id: null, revision: 0 } });
const sub = await f.submit();
assert.deepEqual((await read()).snapshot, { id: null, revision: 0 });
f.approve(sub);
const snap = await f.activate(sub), active = await read();
assert.deepEqual(active.snapshot, { id: snap.id, revision: 1 });
assert.notEqual(active.snapshot.revision, f.journal.readRequest('demo', f.request).revision);
const args = { project: 'timer', snapshot: active.snapshot.id, revision: active.snapshot.revision, reason: 'Replace this version', op: 'revoke-read' };
await assert.rejects(f.call('console', 'prd.revoke.console', { ...args, revision: f.journal.readRequest('demo', f.request).revision }), { code: 'request-revision-conflict' });
const receipt = await f.call('console', 'prd.revoke.console', args);
assert.equal(receipt.kind, 'revoked');
assert.deepEqual(await read(), { candidate: null, prd: null, requirements: [], snapshot: { id: null, revision: 2 } });
await assert.rejects(f.call('console', 'prd.revoke.console', { ...args, op: 'stale-read' }), { code: 'request-revision-conflict' });
for (const who of ['pm', 'cto', 'coder', 'human', 'reader'])
assert.throws(() => f.call(who, 'request.draft.console', { request: f.request }), { code: 'human-required' });
// The next submission sees the existing project counter even though it has no active snapshot yet.
await f.call('pm', 'prd.submit', { ...f.args, revision: f.journal.readRequest('demo', f.request).revision, op: 'prd-again' });
assert.deepEqual((await read()).snapshot, { id: null, revision: 2 });
});
test('3p through 3s migration preserves existing request rows and the append-only guards', async t => {
const { readFileSync, chmodSync } = await import('node:fs');
const { Store, schemaDigest } = await import('../src/store.mjs');
const root = tmp(t, 'prd-migration-'); mkdirSync(join(root, 'bus'), { mode: 0o700 });
const path = join(root, 'bus/bus.sqlite'), ddl = readFileSync(new URL('../schema.sql', import.meta.url), 'utf8')
// 3s-3t (#797) adds project_archives; the 3p fixture predates it and the replayed chain recreates it.
.replace(readFileSync(new URL('../src/migrations/3s-3t.sql', import.meta.url), 'utf8'), '')
// 3u-3v (#1873, U4a) adds store_tasks; the same holds for it.
.replace(readFileSync(new URL('../src/migrations/3u-3v.sql', import.meta.url), 'utf8'), '')
.replace(",'prd.submitted','prd.activated','prd.invalidated','question.asked','question.answered','attach.selected','mode.changed','advanced'", '');
const db = new DatabaseSync(path); db.exec(ddl);
const at = '2026-11-03T00:00:00.000Z';
db.prepare('INSERT INTO meta VALUES(?,?)').run('schema_version', '3p');
db.prepare('INSERT INTO meta VALUES(?,?)').run('schema_digest', schemaDigest(db));
db.prepare('INSERT INTO meta VALUES(?,?)').run('comment_cursor_epoch', at);
db.prepare("INSERT INTO messages(id,at,business,from_role,from_run,to_role,class,body) VALUES('m',?,'demo','human','human','pm','INFO','Timer')").run(at);
db.prepare("INSERT INTO events(id,at,business,kind,subject,body) VALUES('r',?,'demo','human.input','m',?)").run(at, JSON.stringify({ kind: 'instruction', message: 'm' }));
db.prepare("INSERT INTO request_events(business,request,revision,kind,at,body) VALUES('demo','r',1,'opened',?,'{}')").run(at);
const before = db.prepare('SELECT * FROM request_events').all();
assert.throws(() => db.prepare("INSERT INTO request_events(business,request,revision,kind,at,body) VALUES('demo','r',2,'prd.submitted',?,'{}')").run(at));
db.close(); chmodSync(path, 0o600);
assert.throws(() => new Store(root), /schema-mismatch/);
const store = new Store(root, { migrateSchema: true }); t.after(() => store.close());
assert.deepEqual(store.all('SELECT * FROM request_events'), before);
assert.equal(store.get("SELECT value FROM meta WHERE key='schema_version'").value, '3w');
store.run("INSERT INTO request_events(business,request,revision,kind,at,body) VALUES('demo','r',2,'prd.submitted',?,'{}')", at);
assert.throws(() => store.run('DELETE FROM request_events'), /append-only/);
assert.throws(() => store.run("UPDATE request_events SET body='{}'"), /append-only/);
assert.throws(() => store.run("INSERT INTO request_events(business,request,revision,kind,at,body) VALUES('demo','r',4,'prd.activated',?,'{}')", at), /revision-conflict/);
});
test('guided Console approval uses the existing confirmation digest and attributed resolution evidence', async t => {
const f = await setup(t, 'guided'), sub = await f.submit();
const preview = f.call('console', 'decision.preview.console', { id: sub.decision.id });
f.call('console', 'decision.resolve.console', { id: sub.decision.id, choice: 'yes', digest: preview.digest });
const snap = await f.activate(sub); assert.equal(snap.approval.via, 'console');
assert.equal(f.store.get("SELECT count(*) n FROM messages WHERE decision=? AND class='DECISION'", sub.decision.id).n, 1);
});
test('declined draft returns to discovery and mode change revokes an active snapshot', async t => {
const f = await setup(t), sub = await f.submit();
f.call('cto', 'decision.resolve', { id: sub.decision.id, choice: 'no' });
const discovery = await f.bus.requestLifecycle.resume('demo', f.request);
assert.equal(discovery.phase, 'discovery'); assert.equal(discovery.wait.code, 'prd-changes-requested');
const next = await f.call('pm', 'prd.submit', { ...f.args, revision: discovery.revision, op: 'revision2' });
f.approve(next); const snap = await f.activate(next);
const state = f.journal.readRequest('demo', f.request);
await f.call('console', 'request.mode.console', { request: f.request, revision: state.revision, mode_revision: state.mode_revision, mode: 'guided', op: 'mode2' });
await assert.rejects(f.bus.requirementCatalog.resolve({ business: 'demo', project: 'timer', snapshot: snap.id }), { code: 'requirement-snapshot-revoked' });
});
const archiveProduct = f => {
const p = f.store.get("SELECT id FROM projects WHERE business='demo' AND repo='timer'");
assert.ok(p);
f.call('human', 'project.archive', { project: p.id });
return p.id;
};
test('archive fences product admission and PRD submission by repository key', async t => {
const f = await setup(t), count = f.store.get('SELECT count(*) n FROM decisions').n;
const p = archiveProduct(f);
assert.equal(f.bus.admission({ business: 'demo', project: 'timer' }).code, 'project-archived');
assert.equal(f.bus.admission({ business: 'demo', project: 'timer' }).ready, false);
assert.equal(f.bus.admission({ business: 'demo', project: 'stack' }).ready, true);
await assert.rejects(f.submit(), { code: 'project-archived' });
assert.equal(f.store.get('SELECT count(*) n FROM decisions').n, count);
assert.equal(f.store.get('SELECT count(*) n FROM request_snapshots').n, 0);
f.call('human', 'project.unarchive', { project: p });
assert.equal(f.bus.admission({ business: 'demo', project: 'timer' }).ready, true);
assert.ok(await f.submit());
});
test('archive after PRD approval refuses activation without publishing a snapshot', async t => {
const f = await setup(t), sub = await f.submit();
f.approve(sub); archiveProduct(f);
await assert.rejects(f.activate(sub), { code: 'project-archived' });
assert.equal(f.store.get('SELECT count(*) n FROM request_snapshots').n, 0);
});
test('archived products refuse task targets and requirements admission but retain snapshot reads and cancellation', async t => {
const f = await setup(t), sub = await f.submit(); f.approve(sub);
const snap = await f.activate(sub);
archiveProduct(f);
const args = { request: f.request, project_key: 'timer', requirement_snapshot: snap.id, op: 'new-task', requirement: 'REQ-POMO-1' };
assert.throws(() => f.bus.broker.taskCreateTarget(f.caps.pm, args), { code: 'project-archived' });
await assert.rejects(f.bus.broker.requirementsFor(f.caps.pm, args), { code: 'project-archived' });
await assert.rejects(f.bus.broker.requirementsFor(f.caps.pm, { project_key: 'timer', requirement_snapshot: snap.id }), { code: 'project-archived' });
assert.equal((await f.bus.requirementCatalog.resolve({ business: 'demo', project: 'timer', snapshot: snap.id })).id, snap.id);
const state = await f.call('console', 'request.get.console', { request: f.request });
const cancelled = await f.call('console', 'request.cancel.console', { request: f.request, revision: state.revision, op: 'cancel-archived' });
assert.equal(cancelled.phase, 'cancelled');
assert.deepEqual(await f.bus.requestLifecycle.resume('demo', f.request), cancelled, 'archive recovery must not rewrite terminal request state');
});
test('archived products disappear from the request picker and refuse attach/propose explicitly', async t => {
const f = await setup(t);
const sent = f.call('human', 'message.send', { to: 'pm', body: 'New work.' });
let state = await f.call('pm', 'request.open', { request: sent.request });
state = await f.call('pm', 'request.propose', { request: sent.request, revision: state.revision, op: 'another-plan', source_project: 'stack', project: 'new-timer', name: 'New Timer', tracker_project: 3 });
f.call('human', 'decision.resolve', { id: state.selection.decision, choice: 'existing' });
state = await f.bus.requestLifecycle.resume('demo', sent.request);
assert.ok((await f.call('console', 'request.projects.console', { request: sent.request })).some(p => p.project === 'timer'));
archiveProduct(f);
assert.ok(!(await f.call('console', 'request.projects.console', { request: sent.request })).some(p => p.project === 'timer'));
await assert.rejects(f.call('console', 'request.attach.console', { request: sent.request, revision: state.revision, project: 'timer', op: 'archived-pick' }), { code: 'project-archived' });
await assert.rejects(f.call('pm', 'request.propose', { request: sent.request, revision: state.revision, op: 'archived-plan', source_project: 'timer', project: 'timer', name: 'Timer', tracker_project: 2 }), { code: 'project-archived' });
});
test('archive stops new continuations and request-bound launch/manifest recovery', async t => {
const f = await setup(t);
const before = f.store.get('SELECT count(*) n FROM messages').n;
archiveProduct(f);
await f.bus.requestLifecycle.resume('demo', f.request);
assert.equal(f.store.get('SELECT count(*) n FROM messages').n, before);
assert.ok(!f.bus.requestLaunches('demo').some(i => i.project === 'timer'));
assert.throws(() => f.bus.requestManifest({ business: 'demo', instance: 'pm', project: 'timer', run: 'pm-run' }), { code: 'project-archived' });
});
test('a pending resource plan cannot schedule intake from an archived source product', async t => {
const f = await setup(t);
const sent = f.call('human', 'message.send', { to: 'pm', body: 'Build another timer.' });
let state = await f.call('pm', 'request.open', { request: sent.request });
state = await f.call('pm', 'request.propose', { request: sent.request, revision: state.revision, op: 'pending-plan',
source_project: 'timer', project: 'another-timer', name: 'Another timer', tracker_project: 3 });
archiveProduct(f);
f.call('human', 'decision.resolve', { id: state.selection.decision, choice: 'guided' });
const before = f.store.get('SELECT count(*) n FROM messages').n;
state = await f.bus.requestLifecycle.resume('demo', sent.request);
assert.equal(state.wait.code, 'project-archived');
assert.equal(f.store.get('SELECT count(*) n FROM messages').n, before);
assert.ok(!f.bus.requestLaunches('demo').some(i => i.request === sent.request));
});
test('activation rechecks archive after asynchronous candidate reads', async t => {
const f = await setup(t), sub = await f.submit(); f.approve(sub);
const activating = f.activate(sub);
archiveProduct(f);
await assert.rejects(activating, { code: 'project-archived' });
assert.equal(f.store.get('SELECT count(*) n FROM request_snapshots').n, 0);
});
test('requirements admission rechecks archive after asynchronous catalog reads', async t => {
const f = await setup(t), sub = await f.submit(); f.approve(sub);
const snap = await f.activate(sub);
const admission = f.bus.broker.requirementsFor(f.caps.pm, { project_key: 'timer', requirement_snapshot: snap.id });
archiveProduct(f);
await assert.rejects(admission, { code: 'project-archived' });
});