81 lines
5.6 KiB
JavaScript
81 lines
5.6 KiB
JavaScript
// #797 / #1839 Task 3: the owner's Console controls over the request journal, and the PM's question.
|
|
// The webui side is packages/webui/tests/request-http.test.mjs; this file drives the broker verbs
|
|
// those routes call, with a real capability for every caller.
|
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { createHash } from 'node:crypto';
|
|
import { Store } from '../../../bus/src/store.mjs';
|
|
import { Broker } from '../../../bus/src/broker.mjs';
|
|
import { createRequestStore } from '../../../bus/src/request-store.mjs';
|
|
import { createRequestLifecycle } from '../../../bus/src/request-lifecycle.mjs';
|
|
|
|
const hash = x => createHash('sha256').update(x).digest('hex');
|
|
const intent = 'Build a single-file timer.\nKeep these exact bytes.\n';
|
|
const SUBJECT = { provider: 'oidc', iss: 'https://id.test', sub: 'jason' };
|
|
function setup(t, overrides = {}) {
|
|
const root = mkdtempSync(join(tmpdir(), 'request-console-'));
|
|
const store = new Store(root);
|
|
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
|
|
const business = { id: 'demo', human: 'jason', vars: { 'bus.tester': true }, arbiters: { technical: 'cto', delivery: 'pm' },
|
|
roles: Object.fromEntries(['pm', 'cto', 'coder', 'otherpm', 'chat'].map(key => [key, {
|
|
definition: key === 'otherpm' ? 'pm' : key === 'chat' ? 'assistant' : key, authority: { withinRole: ['message.send'], crossRole: [] },
|
|
}])), projects: { factory: { root: join(root, 'factory') }, old: { root: join(root, 'old'), repo: join(root, 'old-code') },
|
|
library: { kind: 'library', root: join(root, 'library') }, unknown: { root: join(root, 'unknown') } } };
|
|
const plans = (state, args, source, existing) => ['autonomous', 'guided'].map(mode => ({
|
|
version: 1, business: state.business, request: state.request, project: args.project, name: args.name, chat_project: null, mode,
|
|
organization_revision: 'org-1', source: { project: args.source_project, commit: 'a'.repeat(40), remote: null, forks: [] },
|
|
code_root: existing ? business.projects[args.project].root : join(root, 'products/demo', args.project, 'code'),
|
|
records_root: existing ? business.projects[args.project].repo : join(root, 'projects/demo', args.project), tracker: { root: 1, project: args.tracker_project },
|
|
staff: { pm: state.pm, technical_reviewer: 'cto', coder: 'coder', reviewer: 'cto', chief: 'cto' },
|
|
intent: { text: source.text, sha256: hash(source.text), source_message: source.source_message, generated: false },
|
|
project_vars: {}, existing,
|
|
}));
|
|
const host = {
|
|
listProjects: () => [
|
|
{ business: 'demo', project: 'old', name: 'Old', tracker: { root: 1, project: 2 } },
|
|
{ business: 'demo', project: 'library', name: 'Library', tracker: { root: 1, project: 2 } },
|
|
{ business: 'foreign', project: 'old', name: 'Foreign', tracker: { root: 1, project: 2 } },
|
|
{ business: 'demo', project: 'unknown', name: 'Unknown', tracker: null },
|
|
],
|
|
preparePlans: (state, args, source) => plans(state, args, source, false),
|
|
prepareExisting: (state, args, source) => plans(state, args, source, true)[0],
|
|
admitPlan: () => ({ ready: true }),
|
|
};
|
|
let lifecycle, journal;
|
|
const b = new Broker({ store, businesses: { demo: business }, requestLifecycle: services => {
|
|
journal = createRequestStore(store); lifecycle = createRequestLifecycle({ journal, services: { ...host, ...overrides, ...services } }); return lifecycle;
|
|
} });
|
|
const caps = { human: b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }),
|
|
reader: b.bindReader({ business: 'demo' }), tester: b.bindTester({ business: 'demo' }),
|
|
console: b.bindConsole({ business: 'demo', human: 'jason', subject: SUBJECT }) };
|
|
for (const role of ['pm', 'cto', 'coder', 'otherpm']) {
|
|
caps[role] = b.bindLaunch({ business: 'demo', role, run: role + '-run', harness: 'pi' });
|
|
b.request(caps[role], { verb: 'role.claim' });
|
|
}
|
|
const call = async (who, verb, args = {}) => b.request(caps[who], { verb, args });
|
|
const originProject=b.request(caps.human,{verb:'project.create',args:{name:'Origin chat'}});
|
|
const originThread=b.request(caps.human,{verb:'thread.create',args:{project:originProject.id,name:'pm',instance:'chat'}});
|
|
const source=b.request(caps.human,{verb:'thread.send',args:{thread:originThread.id,body:intent}});
|
|
caps.chat=b.bindLaunch({business:'demo',role:'chat',run:'chat-run',harness:'pi',thread:originThread.id});
|
|
b.request(caps.chat,{verb:'role.claim'});
|
|
b.request(caps.chat,{verb:'message.send',args:{to:'pm',body:intent,in_reply_to:source.id}});
|
|
const propose = state => ({ request: source.request, revision: state.revision, op: 'plan-1', source_project: 'factory', project: 'timer', name: 'Timer', tracker_project: 2 });
|
|
const existing = (state, op) => ({ request: state.request, revision: state.revision, op, source_project: 'old', project: 'old', name: 'Old', tracker_project: 2 });
|
|
return { root, b, store, call, source, propose, existing, business, get journal() { return journal; }, get lifecycle() { return lifecycle; } };
|
|
}
|
|
const n = (x, sql, ...p) => x.store.get(sql, ...p).n;
|
|
const get = (x, who = 'console') => x.call(who, 'request.get.console', { request: x.source.request });
|
|
async function atSelection(t) {
|
|
const x = setup(t), opened = await x.call('pm', 'request.open', { request: x.source.request });
|
|
x.state = await x.call('pm', 'request.propose', x.propose(opened));
|
|
return x;
|
|
}
|
|
async function atPicker(t) {
|
|
const x = await atSelection(t);
|
|
await x.call('human', 'decision.resolve', { id: x.state.selection.decision, choice: 'existing' });
|
|
x.state = await x.lifecycle.resume('demo', x.source.request);
|
|
return x;
|
|
}
|
|
export { atPicker };
|