183 lines
13 KiB
JavaScript
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);
|
|
});
|