Files
stack/packages/bus/tests/request-delivery.test.mjs

183 lines
13 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { requestFixture } from './request-fixture.mjs';
test('delivery requires the delivery phase even with a valid typed request', async t => {
const f = await requestFixture(t);
const s = f.journal.readRequest('demo', f.request);
await assert.rejects(Promise.resolve().then(() => f.call('pm', 'request.deliver', { request: f.request, revision: s.revision, op: 'delivery-1', task_ref: 'vikunja:2/41', verdict: 'verdict-1', path: 'timer.html', verification_record: { path: 'docs/verification.md', commit: 'a'.repeat(40) } })), /request-phase-refused/);
});
import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { createHash } from 'node:crypto';
import { git } from '../../project/tests/prd-fixture.mjs';
import { ready } from './delivery-fixture.mjs';
test('approved delivery binds exact blob, verification, task and review, and retries once', async t => {
const f = await ready(t);
const outcome = await f.call('pm', 'request.deliver', f.args);
assert.equal(outcome.commit, f.commit); assert.equal(outcome.task_ref, f.task_ref);
assert.equal(outcome.review_request, f.review.request); assert.equal(outcome.verdict, f.verdict.verdict);
assert.equal(outcome.artifact.blob, git(join(f.f.dataRoot, 'working/demo/timer.git'), 'rev-parse', `${f.commit}:timer.html`));
assert.equal(outcome.artifact.sha256, createHash('sha256').update(f.bytes).digest('hex'));
assert.equal(outcome.verification.record_commit, f.verification.commit);
assert.deepEqual(await f.call('pm', 'request.deliver', f.args), outcome);
assert.equal(f.journal.readRequest('demo', f.request).phase, 'complete');
assert.equal(f.journal.effects('demo', f.request).filter(e => e.kind === 'delivery' && e.stage === 'committed').length, 1);
assert.equal(f.journal.effects('demo', f.request).filter(e => e.kind === 'continuation' && e.stage === 'uncertain' && e.result?.intent?.step === 'send_message').length, 1);
const artifact = await f.call('console', 'request.artifact.console', { request: f.request, delivery: outcome.id });
assert.deepEqual(Buffer.from(artifact.base64, 'base64'), f.bytes);
});
test('a different operation for the same reviewed artifact returns the original delivery', async t => {
const f = await ready(t); const first = await f.call('pm', 'request.deliver', f.args);
const again = await f.call('pm', 'request.deliver', { ...f.args, op: 'retry-different-op', revision: f.args.revision + 1 });
assert.deepEqual(again, first);
assert.equal(f.store.get("SELECT count(*) n FROM request_events WHERE request=? AND kind='advanced' AND json_extract(body,'$.phase')='complete'", f.request).n, 1);
});
test('a newer push or wrong task cannot borrow an earlier verdict', async t => {
const f = await ready(t);
await assert.rejects(f.call('pm', 'request.deliver', { ...f.args, op: 'wrong-task', task_ref: 'vikunja:2/42' }), /request-evidence-required/);
f.push('c'.repeat(40));
await assert.rejects(f.call('pm', 'request.deliver', f.args), /request-evidence-required/);
assert.equal(f.journal.readRequest('demo', f.request).phase, 'delivery');
});
test('a forged verification record cannot borrow the reviewer receipt', async t => {
const f = await ready(t);
const fake = await f.call('pm', 'record.write', { project: 'timer', path: 'docs/forged.md', record: JSON.stringify({ version: 1, task_ref: f.task_ref, review_request: f.review.request, commit: f.commit, results: 'Acceptance: timer starts and stops offline; passed.' }) });
await assert.rejects(f.call('pm', 'request.deliver', { ...f.args, verification_record: { path: fake.path, commit: fake.commit } }), /request-verification-refused/);
});
test('delivery and chief intent roll back together when the notification cannot be recorded', async t => {
const f = await ready(t);
const { DatabaseSync } = await import('node:sqlite');
const db = new DatabaseSync(join(f.f.dataRoot, 'bus/bus.sqlite')); t.after(() => db.close());
db.exec("CREATE TRIGGER fail_delivery_notice BEFORE INSERT ON messages WHEN NEW.from_role='_bus' AND NEW.to_role='chief' AND NEW.body LIKE 'Request continuation %' BEGIN SELECT RAISE(ABORT,'delivery-notice-failed'); END");
await assert.rejects(f.call('pm', 'request.deliver', f.args), /delivery-notice-failed|request-failed|ERR_SQLITE_ERROR/);
assert.equal(f.journal.readRequest('demo', f.request).phase, 'delivery');
assert.equal(f.journal.effects('demo', f.request).filter(e => e.kind === 'delivery' && e.stage === 'committed').length, 0);
db.exec('DROP TRIGGER fail_delivery_notice');
const delivered = await f.call('pm', 'request.deliver', f.args);
const intent = f.journal.effects('demo', f.request).find(e => e.kind === 'continuation' && e.stage === 'uncertain' && e.result?.intent?.step === 'send_message');
assert.equal(delivered.message, intent.result.message);
});
test('verification results must equal those cited in the review verdict', async t => {
const f = await ready(t, { results: 'Different results' });
await assert.rejects(f.call('pm', 'request.deliver', f.args), /request-verification-refused/);
assert.equal(f.journal.readRequest('demo', f.request).phase, 'delivery');
});
test('reviewed downloaded HTML opens offline and matches the committed blob', async t => {
const f = await ready(t); const d = await f.call('pm', 'request.deliver', f.args);
const { requestHTTP } = await import('../../webui/src/request-http.mjs');
const { browser } = await import('../../webui/tests/browser.mjs');
const { pathToFileURL } = await import('node:url');
const session = { name: 'jason', provider: 'test', iss: 'test', sub: 'owner' };
const res = { writeHead(status, headers) { this.status = status; this.headers = headers; }, end(data) { this.data = data; } };
await requestHTTP({ method: 'GET', headersDistinct: { 'sec-fetch-site': ['same-origin'] } }, res, {
path: `/api/requests/${f.request}/artifact/${d.id}`, session, live: () => session, origin: 'https://console.test', log() {}, requestWrite: null,
requestRead: (verb, args) => f.call('console', verb, args),
});
assert.equal(res.status, 200); assert.deepEqual(res.data, f.bytes);
const downloaded = join(f.f.dataRoot, 'downloaded-timer.html'); writeFileSync(downloaded, res.data);
const b = await browser({ machine: false }); t.after(() => b.close());
await b.call('Network.enable');
await b.call('Network.emulateNetworkConditions', { offline: true, latency: 0, downloadThroughput: 0, uploadThroughput: 0 });
await b.navigate(pathToFileURL(downloaded).href);
assert.equal(await b.evaluate('navigator.onLine'), false);
assert.equal(await b.evaluate("document.querySelector('#start').click(); document.querySelector('#clock').textContent"), 'Running');
assert.equal(createHash('sha256').update(res.data).digest('hex'), d.artifact.sha256);
assert.equal(git(join(f.f.dataRoot, 'working/demo/timer.git'), 'rev-parse', `${d.commit}:timer.html`), d.artifact.blob);
});
test('a later changes verdict cannot deliver the previous approval', async t => {
const f = await ready(t); const review = f.call('coder', 'review.request', { task_ref: f.task_ref, commit: f.commit, to: 'reviewer' });
f.call('reviewer', 'review.verdict', { task_ref: f.task_ref, request: review.request, verdict: 'changes', body: 'Needs work' });
await assert.rejects(f.call('pm', 'request.deliver', f.args), /request-evidence-required/);
});
test('delivery rejects traversal, symlinks and oversized blobs', async t => {
const f = await ready(t, { invalidPaths: true });
for (const [i, path] of ['../timer.html', 'link.html', 'large.html'].entries()) {
await assert.rejects(f.call('pm', 'request.deliver', { ...f.args, op: `invalid-path-${i}`, path }), /artifact-path-invalid|artifact-not-regular|artifact-too-large/);
}
assert.equal(f.journal.readRequest('demo', f.request).phase, 'delivery');
});
test('delivery remains readable and notification recoverable when the assistant is unavailable', async t => {
const f = await ready(t);
f.bus.broker.endLaunch({ business: 'demo', run: 'assistant-run', reason: 'exit', exitCode: 1 });
const d = await f.call('pm', 'request.deliver', f.args);
const result = await f.call('console', 'request.artifact.console', { request: f.request, delivery: d.id });
assert.deepEqual(Buffer.from(result.base64, 'base64'), f.bytes);
for (let i = 0; i < 2; i++) await f.bus.requestLifecycle.recover();
const pending = f.journal.effects('demo', f.request).filter(e => e.kind === 'continuation' && e.stage === 'uncertain' && e.result?.intent?.step === 'send_message');
assert.equal(pending.length, 1);
assert.equal(pending[0].result.message, d.message);
});
test('changed PRD records cannot authorize a new delivery', async t => {
const f = await ready(t);
await f.call('pm', 'record.write', { project: 'timer', path: 'docs/prd-1.0.md', record: '# Changed PRD\n' });
await assert.rejects(f.call('pm', 'request.deliver', f.args), /prd-candidate-stale|requirement-snapshot-revoked/);
assert.equal(f.journal.effects('demo', f.request).filter(e => e.kind === 'delivery' && e.stage === 'committed').length, 0);
});
test('real task.close through the tracker adapter supplies the closure used by delivery', async t => {
const f = await ready(t, { realClose: true });
assert.ok(f.fake, 'the delivery fixture uses the real task adapter and fake tracker');
assert.ok(f.fake.requests.some(r => r.method === 'PUT' && /^\/projects\/\d+\/views\/\d+\/buckets\/\d+\/tasks$/.test(r.path)), 'task.close wrote to the tracker');
const d = await f.call('pm', 'request.deliver', f.args);
assert.equal(d.verdict, f.verdict.verdict);
assert.equal(f.journal.readRequest('demo', f.request).phase, 'complete');
});
async function newerUnclosedReview(f) {
const { bootConfig, loadSystem } = await import('../../cli/src/config.mjs');
const config = bootConfig({ system: loadSystem({ env: f.f.env }), businessId: 'demo', env: f.f.env, warn: () => {} });
const business = config.businesses.demo;
business.roles.reviewer2 = structuredClone(business.roles.reviewer);
business.roles.reviewer2.tracker = { bot: 'bot-demo-reviewer2', botId: f.fake.user('reviewer2') };
business.launch.instances.push('reviewer2');
f.bus.broker.reloadBusiness('demo', business);
const cap = f.bus.broker.bindLaunch({ business: 'demo', role: 'reviewer2', run: 'reviewer2-run', harness: 'pi' });
f.bus.broker.request(cap, { verb: 'role.claim' });
const review = f.call('coder', 'review.request', { task_ref: f.task_ref, commit: f.commit, to: 'reviewer2' });
const results = 'Reviewer two independently passed acceptance.';
const verification = await f.bus.broker.request(cap, { verb: 'record.write', args: { project: 'timer', path: 'docs/verification2.md',
record: JSON.stringify({ version: 1, task_ref: f.task_ref, review_request: review.request, commit: f.commit, results }) } });
const verdict = f.bus.broker.request(cap, { verb: 'review.verdict', args: { task_ref: f.task_ref, request: review.request, verdict: 'approve',
body: JSON.stringify({ verification_record: { path: verification.path, commit: verification.commit }, results }) } });
return { review, verdict, verification };
}
test('refuses a newer approval whose verdict was never cited by the task closure', async t => {
const f = await ready(t, { realClose: true }), v2 = await newerUnclosedReview(f);
const closures = f.store.all("SELECT body FROM events WHERE business=? AND kind='task.closed' AND subject=?", 'demo', f.task_ref);
assert.equal(closures.length, 1);
assert.equal(JSON.parse(closures[0].body).verdict, f.verdict.verdict);
await assert.rejects(f.call('pm', 'request.deliver', { ...f.args, verdict: v2.verdict.verdict,
verification_record: { path: v2.verification.path, commit: v2.verification.commit } }), { code: 'request-evidence-required' });
assert.equal(f.journal.readRequest('demo', f.request).phase, 'delivery');
assert.equal(f.store.get("SELECT count(*) n FROM request_events WHERE business=? AND request=? AND kind='advanced' AND json_extract(body,'$.phase')='complete'", 'demo', f.request).n, 0);
assert.equal(f.journal.effects('demo', f.request).filter(e => e.kind === 'continuation' && e.stage === 'uncertain' && e.result?.intent?.step === 'send_message').length, 0);
assert.equal(f.journal.effects('demo', f.request).filter(e => e.kind === 'delivery' && e.stage === 'committed').length, 0);
});
test('delivers the closed planned review even after another reviewer gives a newer unclosed approval', async t => {
const f = await ready(t, { realClose: true }), v2 = await newerUnclosedReview(f);
const d = await f.call('pm', 'request.deliver', f.args);
assert.equal(d.review_request, f.review.request);
assert.equal(d.verdict, f.verdict.verdict);
assert.notEqual(d.verdict, v2.verdict.verdict);
assert.equal(d.verification.record_commit, f.verification.commit);
assert.equal(f.journal.readRequest('demo', f.request).phase, 'complete');
assert.equal(f.journal.effects('demo', f.request).filter(e => e.kind === 'continuation' && e.stage === 'uncertain' && e.result?.intent?.step === 'send_message').length, 1);
});