readCandidate called readPrdMaterial with the org data root directly, so prd.submit and the Console PRD draft kept working while project.files refused the same conflict. It now resolves through recordArgs like the file adapters. Fixes the P2 from the E2E4 review.
71 lines
5.2 KiB
JavaScript
71 lines
5.2 KiB
JavaScript
import test from 'node:test';
|
|
import { DatabaseSync } from 'node:sqlite';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdirSync, readFileSync, 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 instanceRoot=join(root,'instance');mkdirSync(instanceRoot);
|
|
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,org:{id:'demo'},instanceDataRoot:instanceRoot,instanceConfigDir:join(root,'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, instanceRoot, request: sent.request, args, submit, approve, activate, boot, bind, config: bootConfig({ system, businessId: 'demo', env: f.env, warn: () => {} }), store, journal: createRequestStore(store) };
|
|
}
|
|
|
|
|
|
// E2E4 review P2: in an organization runtime the PRD reader resolves records through the same
|
|
// conflict guard as project.files, so records at both roots refuse and neither root changes.
|
|
test('org PRD submit refuses when records exist at both roots', async t => {
|
|
const f = await setup(t, 'guided');
|
|
const legacy = join(f.instanceRoot, 'projects/demo/timer');
|
|
mkdirSync(legacy, { recursive: true }); writeFileSync(join(legacy, 'keep'), 'legacy record evidence');
|
|
await assert.rejects(f.call('human', 'project.files', { project: 'timer' }), { code: 'project-records-conflict' });
|
|
const before = git(f.repo, 'rev-parse', 'HEAD');
|
|
const decisions = f.store.get("SELECT count(*) n FROM decisions").n;
|
|
await assert.rejects(f.submit(), { code: 'project-records-conflict' });
|
|
assert.equal(f.store.get("SELECT count(*) n FROM decisions").n, decisions, 'no approval decision is raised');
|
|
assert.equal(git(f.repo, 'rev-parse', 'HEAD'), before);
|
|
assert.equal(readFileSync(join(legacy, 'keep'), 'utf8'), 'legacy record evidence');
|
|
});
|
|
|
|
test('org Console PRD draft refuses when records exist at both roots', async t => {
|
|
const f = await setup(t, 'guided'); await f.submit();
|
|
const legacy = join(f.instanceRoot, 'projects/demo/timer');
|
|
mkdirSync(legacy, { recursive: true }); writeFileSync(join(legacy, 'keep'), 'legacy');
|
|
await assert.rejects(f.call('console', 'request.draft.console', { request: f.request }), { code: 'project-records-conflict' });
|
|
assert.equal(readFileSync(join(legacy, 'keep'), 'utf8'), 'legacy');
|
|
});
|