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.
335 lines
26 KiB
JavaScript
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' });
|
|
});
|