Files
stack/packages/tasks/tests/scoped-fixture.mjs

69 lines
5.7 KiB
JavaScript

import assert from 'node:assert/strict';
import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { startBroker } from '../../bus/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 { FakeVikunja } from '../src/fake.mjs';
import { tasksAdapter } from '../src/adapter.mjs';
import { SCOPES } from './world.mjs';
// Real product repositories, approval decisions and file-backed bus; only tracker HTTP is fake.
export async function scopedFixture(t) {
const root = tmp(t, 'scoped-'), 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, 'tracker.descendants': true } }));
git(source, 'init', '-qb', 'main'); git(source, 'add', '-A'); git(source, 'commit', '-qm', 'Factory');
let config = bootConfig({ system: loadSystem({ env: f.env }), businessId: 'demo', env: f.env });
const fake = new FakeVikunja(), owner = fake.user('owner'), bots = {};
for (let i = 2; i <= 9; i++) {
const role = ({3:'pm',4:'cto',5:'coder',6:'reviewer',9:'sync'})[i];
const id = fake.user(role ?? `unused${i}`, { owner }); if (role) bots[role] = id;
}
const board = fake.project('Root', owner), child = fake.project('Products', owner, { parent: board });
for (const p of [board, child]) { fake.install(p); for (const [role, bot] of Object.entries(bots)) fake.share(p, bot, role === 'sync' ? 0 : 1); }
const tokens = Object.fromEntries(Object.entries(bots).map(([role, bot]) => [role, fake.token(bot, role === 'sync'
? { ...SCOPES.sync, projects: [...SCOPES.sync.projects, 'read_all'] } : role === 'pm' ? SCOPES.pm : SCOPES.worker)]));
const credentials = { status: () => Object.keys(bots).map(role => ({ service: 'vikunja', instance: `demo/${role === 'sync' ? '@sync' : role}`, state: 'valid' })),
use: (instance, service, fn) => fn(tokens[instance.split('/')[1].replace('@','')]), useFor: (s, service, fn) => fn(tokens[s.role]) };
const factory = tasksAdapter({ trackers: { demo: { baseUrl: 'http://vikunja.test', project: board, descendants: true } }, fetch: fake.fetch, autostart: false, log: () => {} });
let bus, adapter, generation = 0;
const caps = {};
const boot = async () => {
config = bootConfig({ system: loadSystem({ env: f.env }), businessId: 'demo', env: f.env });
bus = await startBroker(config); generation++;
adapter = await factory({ broker: bus.broker, credentials, businesses: config.businesses });
await adapter.start('demo'); assert.equal(adapter.status()[0].state, 'ready');
caps.human = bus.broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
caps.console = bus.broker.bindConsole({ business: 'demo', human: 'jason', subject: { provider: 'test', iss: 'test', sub: 'owner' } });
for (const role of ['pm','cto','coder','reviewer']) {
if (generation > 1) bus.broker.recoverLaunch({ business: 'demo', role, run: `${role}-run-${generation - 1}`, harness: 'pi' });
caps[role] = bus.broker.bindLaunch({ business: 'demo', role, run: `${role}-run-${generation}`, harness: 'pi' });
bus.broker.request(caps[role], { verb: 'role.claim' });
}
};
const close = async () => { await adapter?.close(); await bus?.close(); };
t.after(close); await boot();
const call = (role, verb, args = {}) => bus.broker.request(caps[role], { verb, args });
const instruction = () => call('human', 'message.send', { to: 'pm', body: 'Build a product.' }).request;
const product = async (project, { approve = true, activate = true, tracker_project = child } = {}) => {
const request = instruction(); let state = await call('pm', 'request.open', { request });
state = await call('pm', 'request.propose', { request, revision: state.revision, op: 'plan', source_project: 'stack', project, name: project, tracker_project });
call('human', 'decision.resolve', { id: state.selection.decision, choice: 'autonomous' });
state = await bus.requestLifecycle.resume('demo', request);
const p = await prdFixture(t, { dataRoot: f.dataRoot, business: { id: 'demo', human: 'jason', projects: { [project]: { root: join(f.dataRoot, 'products/demo', project, 'code') } } }, project });
const requirement = 'REQ-POMO-1';
const records_commit = p.save(`requirements/${requirement}.json`, JSON.stringify({ recordVersion: 1, id: requirement, title: 'Timer', text: `${project} timer behavior.`, status: 'draft' }), 'pm.pm');
const submission = await call('pm', 'prd.submit', { request, revision: state.revision, op: 'prd', prd_version: '1.0', records_commit, prd_path: p.args.prd_path, requirement_ids: [requirement] });
if (approve) call('cto', 'decision.resolve', { id: submission.decision.id, choice: 'yes' });
const snapshot = approve && activate ? await bus.requirementCatalog.activate({ request, decision: submission.decision.id }) : null;
return { ...p, request, submission, snapshot, args: { title: project, request, requirement, project_key: project, requirement_snapshot: snapshot?.id ?? 'unapproved', op: 'create-' + project } };
};
const create = args => bus.broker.requestTask(caps.pm, { verb: 'task.create', args }, adapter.handle);
const posts = () => fake.requests.filter(r => r.method === 'POST' && /^\/projects\/\d+\/tasks$/.test(r.path)).length;
return { f, get config() { return config; }, fake, board, child, caps, call, product, create, instruction, posts,
get bus() { return bus; }, get adapter() { return adapter; },
restart: async () => { await close(); await boot(); } };
}