// #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 };