Files
stack/packages/bus/tests/delivery-fixture.mjs

92 lines
7.5 KiB
JavaScript

import { createGitSocket } from '../../cli/src/git-socket.mjs';
import { pushWorking, fetchReview } from '../../harness/src/git-tools.mjs';
import { mkdirSync, symlinkSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { git } from '../../project/tests/prd-fixture.mjs';
import assert from 'node:assert/strict';
import { requestFixture } from './request-fixture.mjs';
import { scopedFixture } from '../../tasks/tests/scoped-fixture.mjs';
import { DatabaseSync } from 'node:sqlite';
import { createRequestStore } from '../src/request-store.mjs';
const advance = async (f, op = 'next') => { const s = f.journal.readRequest('demo', f.request); return f.call('pm', 'request.advance', { request: f.request, revision: s.revision, op }); };
export async function planned(t, threaded = false) {
const f = await requestFixture(t, 'autonomous', null, threaded), sub = await f.submit(); f.approve(sub); await f.activate(sub);
const { createDigest, createMarker } = await import('../src/broker.mjs');
const s = f.journal.readRequest('demo', f.request), task_ref = 'vikunja:2/41';
const prepared = { op: 'timer-task', request: f.request, requirement: 'REQ-POMO-1', project_key: 'timer', requirement_snapshot: s.snapshot,
project: 2, creator: 3, decision: null, view: { ids: { todo: 1, review: 2, done: 3 } }, labels: [], relations: [] };
prepared.body = { title: 'Timer', description: createMarker(prepared) };
prepared.body_digest = createDigest(prepared.body);
const { receipt } = f.bus.broker.prepareTaskCreate(f.caps.pm, prepared);
let fields = { ...prepared.body, project_id: 2, bucket: 1, done: false, assignees: [], due_date: null, priority: 0 };
const snapshot = () => ({ task_ref, fields, digest: createDigest(fields), updated: new Date().toISOString() });
f.bus.broker.recordRecoveredCreate({ receipt, task_ref, snapshot: { ...snapshot(), creator: 3 } });
f.bus.broker.recordTask({ cap: f.caps.pm, business: 'demo', snapshots: [snapshot()], events: [] });
await assert.rejects(advance(f), /request-evidence-required/);
fields = { ...fields, assignees: [5] };
f.bus.broker.recordTask({ cap: f.caps.pm, business: 'demo', snapshots: [snapshot()], events: [{ kind: 'task.assigned', subject: task_ref, body: { role: 'coder', previous: [] } }] });
const push = commit => f.bus.broker.recordPush(f.caps.coder, { task_ref, project: 'timer', branch: 'timer', ref: 'work/vikunja-2-41', commit, previous: null });
const review = commit => f.call('coder', 'review.request', { task_ref, commit, to: 'reviewer' });
// Lightweight continuation tests simulate the tracker receipt; plannedWithRealClose uses the actual adapter.
return { ...f, task_ref, push, review, closeTask: verdict => {
fields = { ...fields, done: true, bucket: 3 };
f.bus.broker.recordTask({ cap: f.caps.pm, business: 'demo', snapshots: [snapshot()], events: [{ kind: 'task.closed', subject: task_ref, body: { verdict } }] });
} };
}
async function plannedWithRealClose(t) {
const f = await scopedFixture(t), product = await f.product('timer');
const task = await f.create(product.args), task_ref = task.task_ref;
await f.bus.broker.requestTask(f.caps.pm, { verb: 'task.assign', args: { task_ref, role: 'coder' } }, f.adapter.handle);
const db = new DatabaseSync(join(f.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) };
return { ...f, request: product.request, task_ref, store, journal: createRequestStore(store),
push: commit => f.bus.broker.recordPush(f.caps.coder, { task_ref, project: 'timer', branch: 'timer', ref: 'work/vikunja-2-41', commit, previous: null }),
review: commit => f.call('coder', 'review.request', { task_ref, commit, to: 'reviewer' }),
closeTask: verdict => f.bus.broker.requestTask(f.caps.pm, { verb: 'task.close', args: { task_ref, verdict } }, f.adapter.handle) };
}
export async function ready(t, options = {}) {
const f = options.realClose ? await plannedWithRealClose(t) : await planned(t, true);
const advance = async op => { const s = f.journal.readRequest('demo', f.request); return f.call('pm', 'request.advance', { request: f.request, revision: s.revision, op }); };
await advance('start');
const code = join(f.f.dataRoot, 'products/demo/timer/code');
const sandbox = join(f.f.dataRoot, 'test-workspaces');
const coder = join(sandbox, 'coder'), reviewer = join(sandbox, 'reviewer'), temp = join(sandbox, 'tmp');
for (const dir of [coder, reviewer, temp, join(f.f.dataRoot, 'bus-host')]) mkdirSync(dir, { recursive: true });
git(coder, 'clone', '-q', code, 'timer'); git(reviewer, 'clone', '-q', code, 'timer');
const clone = join(coder, 'timer'); git(clone, 'checkout', '-qb', 'feat');
const bytes = Buffer.from(`<!doctype html><title>Timer</title><output id="clock">Ready</output><button id="start">Start</button><script>document.querySelector('#start').onclick=()=>{document.querySelector('#clock').textContent='Running';};</script>`);
writeFileSync(join(clone, 'timer.html'), bytes);
if (options.invalidPaths) { symlinkSync('timer.html', join(clone, 'link.html')); writeFileSync(join(clone, 'large.html'), Buffer.alloc(1024 * 1024 + 1, 65)); }
git(clone, 'add', '-A'); git(clone, 'commit', '-qm', 'Build timer');
const commit = git(clone, 'rev-parse', 'HEAD');
const host = { op: async r => {
if (r.op === 'identity') return f.bus.identity(r.cap);
if (r.op === 'checkPush') return f.bus.checkPush(r.cap, r.task_ref, r.project);
if (r.op === 'recordPush') return f.bus.recordPush(r.cap, r.push);
if (r.op === 'reviewFetch') return f.bus.reviewFetch(r.cap, r.args);
if (r.op === 'refuse') return;
throw new Error('unexpected socket operation');
} };
const socket = createGitSocket({ host, business: { id: 'demo', projects: { timer: { root: code } } }, dataRoot: f.f.dataRoot, env: f.f.env });
await socket.listen(); t.after(() => socket.close());
const pushed = await pushWorking({ path: socket.path, cap: f.caps.coder, workspace: coder, task_ref: f.task_ref, project: 'timer', branch: 'feat', tmp: temp, env: f.f.env });
assert.equal(pushed.commit, commit);
const review = f.review(commit); await advance('review');
const fetched = await fetchReview({ path: socket.path, cap: f.caps.reviewer, workspace: reviewer, task_ref: f.task_ref, request: review.request, tmp: temp, env: f.f.env });
assert.equal(fetched.commit, commit);
assert.equal(git(fetched.clone, 'show', `${fetched.commit}:timer.html`), bytes.toString());
const results = 'Acceptance: timer starts and stops offline; passed.';
const record = JSON.stringify({ version: 1, task_ref: f.task_ref, review_request: review.request, commit, results });
const verification = await f.call('reviewer', 'record.write', { project: 'timer', path: 'docs/verification.md', record });
const verdictBody = JSON.stringify({ verification_record: { path: verification.path, commit: verification.commit }, results: options.results ?? results });
const verdict = f.call('reviewer', 'review.verdict', { task_ref: f.task_ref, request: review.request, verdict: 'approve', body: verdictBody });
await advance('delivery'); await f.closeTask(verdict.verdict);
const s = f.journal.readRequest('demo', f.request);
const args = { request: f.request, revision: s.revision, op: 'deliver-1', task_ref: f.task_ref, verdict: verdict.verdict, path: 'timer.html', verification_record: { path: verification.path, commit: verification.commit } };
return { ...f, advance, bytes, commit, review, verdict, verification, args };
}