import test from 'node:test'; import assert from 'node:assert/strict'; import { scopedFixture } from '../../tasks/tests/scoped-fixture.mjs'; const A = 'a'.repeat(40); const push = (f, task, project) => ({ task_ref: task.task_ref, project, branch: 'feat', ref: 'refs/heads/work', commit: A, previous: null }); test('checkPush binds a product before bytes and recordPush repeats the project check', async t => { const f = await scopedFixture(t), p = await f.product('timer'), task = await f.create(p.args); const b = f.bus.broker; for (const project of ['stack', undefined, 'missing']) assert.throws(() => b.checkPush(f.caps.coder, task.task_ref, project), { code: 'task-project-refused' }); b.checkPush(f.caps.coder, task.task_ref, 'timer'); assert.throws(() => b.recordPush(f.caps.coder, push(f, task, 'stack')), { code: 'task-project-refused' }); b.recordPush(f.caps.coder, push(f, task, 'timer')); const review = f.call('coder', 'review.request', { task_ref: task.task_ref, commit: A, to: 'reviewer' }); assert.equal(b.reviewFetch(f.caps.reviewer, { task_ref: task.task_ref, request: review.request }).project, 'timer'); await f.restart(); assert.throws(() => f.bus.checkPush(f.caps.coder, task.task_ref, 'stack'), { code: 'task-project-refused' }); f.bus.checkPush(f.caps.coder, task.task_ref, 'timer'); }); test('review, review export and remote export reject old cross-project evidence for a scoped task', async t => { const f = await scopedFixture(t), p = await f.product('timer'), task = await f.create(p.args), b = f.bus.broker; // Host-written historical evidence can predate scope admission; consumers must still check it. const { DatabaseSync } = await import('node:sqlite'); const db = new DatabaseSync(f.f.dataRoot + '/bus/bus.sqlite'); db.prepare("INSERT INTO events(id,at,business,actor_role,actor_run,kind,subject,body) VALUES('old-push',?,'demo','coder','coder-run','action.allowed',?,?)").run(new Date().toISOString(), task.task_ref, JSON.stringify({ action: 'git.push.working', project: 'stack', commit: A, ref: 'refs/heads/work' })); db.prepare("INSERT INTO events(id,at,business,actor_role,actor_run,kind,subject,body) VALUES('old-review',?,'demo','coder','coder-run','review.requested',?,?)").run(new Date().toISOString(), task.task_ref, JSON.stringify({ to: 'reviewer', project: 'stack', commit: A, ref: 'refs/heads/work' })); db.close(); assert.throws(() => f.call('coder', 'review.request', { task_ref: task.task_ref, commit: A, to: 'reviewer' }), { code: 'task-project-refused' }); assert.throws(() => b.reviewFetch(f.caps.reviewer, { task_ref: task.task_ref, request: 'old-review' }), { code: 'task-project-refused' }); assert.throws(() => b.checkPublish(f.caps.coder, { task_ref: task.task_ref, project: 'stack', branch: 'feat' }), { code: 'task-project-refused' }); }); test('legacy recorded tasks still admit a push without inventing scope', async t => { const f = await scopedFixture(t), task = { task_ref: 'vikunja:1/789' }, b = f.bus.broker; b.recordTask({ cap: f.caps.pm, business: 'demo', events: [{ kind: 'task.created', subject: task.task_ref, body: { request: f.instruction(), requirement: 'REQ-TASK-1' } }] }); b.checkPush(f.caps.coder, task.task_ref); b.recordPush(f.caps.coder, push(f, task, 'stack')); assert.equal(f.call('coder', 'trail', { subject: task.task_ref }).find(e => e.body?.action === 'git.push.working').body.scope, undefined); });