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); });