92 lines
7.5 KiB
JavaScript
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 };
|
|
}
|