// Row 137 (#1631): the broker's side of an agent's landing. The git socket asks // checkLand before it touches the working repository, then ends the landing // with recordLand, which spends the merge decision, or landRefused. landHeld is // the launcher's question whether to offer land_work at all. // // The lander is merge-gate, granted git.merge.protected cross-role (PRD 1.5 // REQ-LAND-1), and its raise goes to the arbiter pm (REQ-DEC-2). The coder // pushes; its own merge raise is gated, and gated never lands. import test from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { fork } from 'node:child_process'; import { once } from 'node:events'; import { Store } from '../src/store.mjs'; import { Broker } from '../src/broker.mjs'; import { readLanding } from '../src/landing.mjs'; import { fixture } from './spec-fixtures.mjs'; import { createUsage, turnRecords } from '../src/usage.mjs'; const TASK = 'vikunja:1/2'; const MERGE = 'git.merge.protected'; const A = 'a'.repeat(40); const B = 'b'.repeat(40); const C = 'c'.repeat(40); const roles = { pm: { authority: { withinRole: ['message.send', 'decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: ['git.push.working', 'review.request', 'message.send'], crossRole: [] } }, reviewer: { authority: { withinRole: ['review.verdict', 'message.send'], crossRole: [] } }, 'merge-gate': { authority: { withinRole: [], crossRole: [MERGE] } }, }; const demo = (arbiters = { technical: 'pm', delivery: 'pm' }, r = roles) => ({ id: 'demo', human: 'jason', arbiters, projects: { stack: { root: '/nonexistent' } }, roles: r, }); const options = [ { key: 'yes', text: 'Land the parser fix\nIt is reviewed.' }, { key: 'no', text: 'Not yet' }, ]; const merge = { action: MERGE, target: TASK, question: 'Squash-merge work/vikunja-1-2 into main?', options, recommendation: 'yes', blocking: false }; // `extra(store, root, businesses)` adds Broker options, as the usage collector and limit checks. function setup(t, { arbiters, roles: r, extra = () => ({}) } = {}) { const root = mkdtempSync(join(tmpdir(), 'bus-land-')); const store = new Store(root); const businesses = { demo: demo(arbiters, r) }; const b = new Broker({ store, businesses, ...extra(store, root, businesses) }); t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); }); const caps = { human: b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }) }; for (const role of Object.keys(businesses.demo.roles)) { caps[role] = b.bindLaunch({ business: 'demo', role, run: `${role}-run`, harness: 'pi' }); b.request(caps[role], { verb: 'role.claim', args: {} }); } const call = (who, verb, args = {}) => b.request(caps[who], { verb, args }); const push = (commit, previous = null) => { b.checkPush(caps.coder, TASK); return b.recordPush(caps.coder, { task_ref: TASK, project: 'stack', branch: 'feat', ref: 'work/vikunja-1-2', commit, previous }); }; const approve = (commit) => { const r = call('coder', 'review.request', { task_ref: TASK, commit, to: 'reviewer' }); return call('reviewer', 'review.verdict', { task_ref: TASK, request: r.request, verdict: 'approve', body: 'good' }); }; const raise = (who = 'merge-gate', extra = {}) => call(who, 'decision.raise', { ...merge, ...extra }); // The arbiter resolves by default; a decision routed to the human needs who = 'human'. const resolve = (d, choice = 'yes', who = 'pm') => call(who, 'decision.resolve', { id: d.id, choice }); // A pushed, approved task with a merge decision its arbiter approved. const ready = () => { push(A); const v = approve(A); const d = raise(); resolve(d); return { d, v }; }; const land = { commit: B, previous: C, reviewed: A, base: C, patch_id: 'd'.repeat(40) }; const events = (kind) => store.all('SELECT * FROM events WHERE kind=? ORDER BY seq', kind).map((e) => ({ ...e, body: JSON.parse(e.body) })); const merges = () => events('action.allowed').filter((e) => e.body.action === MERGE && e.body.operation !== 'decision.raise'); // The landing's RESULTs; a #notice is a RESULT too. const results = () => store.all("SELECT * FROM messages WHERE class='RESULT' AND (body LIKE 'Landed %' OR body LIKE 'Landing of %') ORDER BY at, id").map((m) => ({ ...m })); const held = (role = 'merge-gate', thread = null) => b.landHeld({ business: 'demo', role, thread }); const check = (args) => b.checkLand(caps['merge-gate'], args); const routed = (d) => ({ ...store.get('SELECT class, route_to FROM decisions WHERE id=?', d.id) }); return { b, store, root, businesses, caps, call, push, approve, raise, resolve, ready, land, events, merges, results, held, check, routed }; } test('REQ-DEC-2: a merge-gate raise its arbiter resolved lands through the host op: checkLand answers the facts and a ticket, records nothing, and recordLand spends the decision once', (t) => { const w = setup(t); const { d, v } = w.ready(); assert.deepEqual(w.routed(d), { class: 'cross-role', route_to: 'pm' }); assert.deepEqual({ ...w.store.get('SELECT "by", via FROM decision_events WHERE decision=? AND op=\'resolved\'', d.id) }, { by: 'pm-run', via: 'broker' }); // land-work's copy of the check, without `arbiter`, still wants the human. assert.throws(() => readLanding(w.store, w.businesses.demo, d.id, { into: 'main' }), { reason: 'not-human' }); const r = w.check({ task_ref: TASK, decision: d.id }); const { ticket, resolved_at, ...facts } = r; assert.match(ticket, /^[0-9a-f-]{36}$/); assert.match(resolved_at, /^\d{4}-/); assert.deepEqual(facts, { task_ref: TASK, work_ref: 'work/vikunja-1-2', decision: d.id, project: 'stack', ref: 'work/vikunja-1-2', commit: A, verdict: v.verdict, subject: 'Land the parser fix', choice: 'yes', by: 'pm-run', via: 'broker', }); assert.deepEqual(w.merges(), []); assert.deepEqual(w.events('action.refused'), []); assert.deepEqual(w.b.recordLand(ticket, w.land), { task_ref: TASK, commit: B }); const [e] = w.merges(); assert.equal(e.subject, TASK); assert.equal(e.actor_role, 'merge-gate'); assert.deepEqual(e.body, { project: 'stack', ref: 'refs/heads/main', commit: B, previous: C, reviewed: A, base: C, patch_id: 'd'.repeat(40), verdict: v.verdict, action: MERGE, class: 'cross-role', decision: d.id, target: TASK, }); const [m] = w.results(); assert.equal(m.to_role, 'merge-gate'); assert.equal(m.decision, d.id); assert.match(m.body, new RegExp(`^Landed ${TASK} on stack main under decision ${d.id}: main moved from ${C} to ${B}`)); // Spent: a second landing, and the old ticket, refuse. assert.throws(() => w.check({ task_ref: TASK, decision: d.id }), /decision-consumed/); assert.throws(() => w.b.recordLand(ticket, w.land), /land-unknown/); assert.equal(w.held(), false); }); test('checkLand refuses on the decision and the review', (t) => { const w = setup(t); const { check } = w; assert.throws(() => check({ task_ref: TASK }), /invalid-request/); assert.throws(() => check({ task_ref: 'gitea:1', decision: 'x' }), /invalid-request/); assert.throws(() => check({ task_ref: TASK, decision: 'x', extra: 1 }), /invalid-request/); // No decision yet, then one that isn't resolved. assert.throws(() => check({ task_ref: TASK, decision: 'no-such' }), /decision-not-found/); w.push(A); const open = w.raise(); assert.throws(() => check({ task_ref: TASK, decision: open.id }), /decision-not-approved/); w.resolve(open, 'no'); assert.throws(() => check({ task_ref: TASK, decision: open.id }), /decision-not-approved/); // Approved, but by another role's raise, or for another target. pm's raise is gated and // uncited, so it goes to the human. const pmRaise = w.raise('pm'); w.resolve(pmRaise, 'yes', 'human'); assert.throws(() => check({ task_ref: TASK, decision: pmRaise.id }), /decision-mismatch/); const other = w.raise('merge-gate', { target: 'vikunja:1/3' }); w.resolve(other); assert.throws(() => check({ task_ref: TASK, decision: other.id }), /decision-mismatch/); // Approved for this task, but nothing reviewed it yet. const d = w.raise(); w.resolve(d); assert.throws(() => check({ task_ref: TASK, decision: d.id }), /no-verdict/); // The reviewer approved A, then the coder pushed B. w.approve(A); w.push(B, A); assert.throws(() => check({ task_ref: TASK, decision: d.id }), /ref-moved/); assert.deepEqual(w.merges(), []); }); // PRD 1.5 REQ-LAND-1. The coder has no cross-role grant, so its merge raise is gated: cited, it // goes to the arbiter (row 139); uncited, to the human. Approved either way, it never lands. test('REQ-LAND-1: a gated merge decision refuses not-authorized, cited and approved by the arbiter or uncited and approved by the human', async (t) => { const f = await fixture(t, { roles, arbiters: { technical: 'pm', delivery: 'pm' } }); const pm = f.agent('pm'); f.broker.checkPush(f.coder, TASK); f.broker.recordPush(f.coder, { task_ref: TASK, project: 'stack', branch: 'feat', ref: 'work/vikunja-1-2', commit: A, previous: null }); const r = f.call(f.coder, 'review.request', { task_ref: TASK, commit: A, to: 'reviewer' }); f.call(f.reviewer, 'review.verdict', { task_ref: TASK, request: r.request, verdict: 'approve', body: 'good' }); const d = await f.call(f.coder, 'decision.raise', { ...merge, project: 'stack', spec_path: 'spec/north-star.md', spec_commit: f.commit }); assert.deepEqual({ ...f.store.get('SELECT class, route_to FROM decisions WHERE id=?', d.id) }, { class: 'gated', route_to: 'pm' }); f.call(pm, 'decision.resolve', { id: d.id, choice: 'yes' }); assert.throws(() => f.broker.checkLand(f.coder, { task_ref: TASK, decision: d.id }), /not-authorized/); // The shared check refuses it too: its arbiter path takes only a cross-role decision. assert.throws(() => readLanding(f.store, f.business, d.id, { into: 'main', arbiter: true }), { reason: 'not-human' }); assert.equal(f.broker.landHeld({ business: 'demo', role: 'coder', thread: null }), false); const w = setup(t); w.push(A); w.approve(A); const human = w.raise('coder'); assert.deepEqual(w.routed(human), { class: 'gated', route_to: 'human' }); w.resolve(human, 'yes', 'human'); assert.throws(() => w.b.checkLand(w.caps.coder, { task_ref: TASK, decision: human.id }), /not-authorized/); assert.deepEqual([...w.merges(), ...w.events('action.refused')], []); }); // Round 4 (sage-0585): a role granted git.merge.protected within-role resolves its own merge // raise. That isn't the cross-role grant REQ-LAND-1 asks for, so it refuses at the land too. test('REQ-LAND-1: a within-role merge grant refuses not-authorized at the land', (t) => { const w = setup(t, { roles: { ...roles, 'merge-gate': { authority: { withinRole: [MERGE], crossRole: [] } } }, }); w.push(A); w.approve(A); const d = w.raise('merge-gate', { choice: 'yes' }); assert.deepEqual(w.routed(d), { class: 'within-role', route_to: 'merge-gate' }); assert.deepEqual({ ...w.store.get('SELECT op, choice, via FROM decision_events WHERE decision=?', d.id) }, { op: 'resolved', choice: 'yes', via: 'broker' }); assert.throws(() => w.check({ task_ref: TASK, decision: d.id }), /not-authorized/); assert.equal(w.held(), false); assert.deepEqual([...w.merges(), ...w.events('action.refused')], []); }); test('a cross-role merge that routes to the human, because the cto lander is its own arbiter, lands on the human\'s CLI answer', (t) => { const w = setup(t, { arbiters: { technical: 'cto', delivery: 'pm' }, roles: { ...roles, cto: { authority: { withinRole: ['decision.resolve.technical'], crossRole: [MERGE] } } } }); w.push(A); w.approve(A); const d = w.raise('cto'); assert.deepEqual(w.routed(d), { class: 'cross-role', route_to: 'human' }); assert.throws(() => w.resolve(d), /human-required/); w.resolve(d, 'yes', 'human'); const { ticket, by, via } = w.b.checkLand(w.caps.cto, { task_ref: TASK, decision: d.id }); assert.deepEqual([by, via], ['jason', 'cli']); w.b.recordLand(ticket, w.land); assert.equal(w.merges()[0].body.class, 'cross-role'); }); test('a merge option with no first line refuses no-subject, as land-work does', (t) => { const w = setup(t); w.push(A); w.approve(A); const d = w.raise('merge-gate', { options: [ { key: 'yes', text: '\nLand it' }, { key: 'no', text: 'Not yet' }, ], }); w.resolve(d); assert.throws(() => w.check({ task_ref: TASK, decision: d.id }), /no-subject/); }); // The codes readLanding gives once #checkAuthority has passed, on the 'main' // side. scripts/test-land-work.sh runs every code on the 'refactor' side. // checkLand never gives not-human: #landFacts refuses every class but // cross-role with not-authorized first, and the broker resolves a cross-role // decision only the two ways readLanding takes with `arbiter`, the human's CLI // answer or its arbiter's decision.resolve. The REQ-LAND-1 test calls // readLanding for not-human. test("checkLand refuses with land-work's codes past the authority check, and a decision raised for refactor never lands", (t) => { const w = setup(t); const check = (d) => () => w.check({ task_ref: TASK, decision: d.id }); w.push(A); w.approve(A); const approved = (extra) => { const d = w.raise('merge-gate', extra); w.resolve(d); return d; }; assert.throws(check(approved({ question: 'Squash-merge work/vikunja-1-2 and work/vikunja-1-3 into main?' })), /work-ref/); assert.throws(check(approved({ question: 'Squash-merge work/vikunja-1-2 into main?', task_ref: 'vikunja:1/3' })), /work-ref/); // The raise targets this task, but the question names another. assert.throws(check(approved({ question: 'Squash-merge work/vikunja-1-3 into main?' })), /target-branch/); // land-work's kind of decision: the authority check wants the task as the target. assert.throws(check(approved({ target: 'refactor' })), /decision-mismatch/); // A later merge decision on the same work ref supersedes an approved one. const first = approved(); w.raise(); assert.throws(check(first), /superseded/); // A later verdict asks for changes on the same commit. const v = setup(t); v.push(A); v.approve(A); const d = v.raise(); v.resolve(d); const r = v.call('coder', 'review.request', { task_ref: TASK, commit: A, to: 'reviewer' }); v.call('reviewer', 'review.verdict', { task_ref: TASK, request: r.request, verdict: 'changes', body: 'one more thing' }); assert.throws(() => v.check({ task_ref: TASK, decision: d.id }), /not-approved/); assert.deepEqual([...w.merges(), ...v.merges()], []); }); test('a ticket outlives the run that asked, and recordLand checks the task again', (t) => { const w = setup(t); const { d } = w.ready(); const { ticket } = w.check({ task_ref: TASK, decision: d.id }); // A bad record refuses and keeps the ticket. assert.throws(() => w.b.recordLand(ticket, { ...w.land, commit: 'x' }), /invalid-request/); const { patch_id, ...short } = w.land; assert.throws(() => w.b.recordLand(ticket, short), /invalid-request/); assert.throws(() => w.b.recordLand(ticket, { ...w.land, reviewed: C }), /ref-moved/); // The run ends while the gate runs; the landing is still recorded as merge-gate's. w.b.endLaunch({ business: 'demo', run: 'merge-gate-run', reason: 'exited' }); assert.deepEqual(w.b.recordLand(ticket, w.land), { task_ref: TASK, commit: B }); assert.equal(w.merges()[0].actor_role, 'merge-gate'); assert.throws(() => w.b.recordLand('no-such-ticket', w.land), /land-unknown/); assert.throws(() => w.b.landRefused(ticket, { code: 'gate-failed' }), /land-unknown/); }); // Row 241 (#1747): one decision lands once. Two tickets on one decision can't both reach // recordLand through the git socket (land-busy, git-socket.mjs), so this pins the broker's own // guard: recordLand runs #landFacts again, and a decision spent by the first refuses the second. test('two tickets on one decision: the first recordLand lands, the second refuses decision-consumed and records nothing', (t) => { const w = setup(t); const { d } = w.ready(); const first = w.check({ task_ref: TASK, decision: d.id }).ticket; const second = w.check({ task_ref: TASK, decision: d.id }).ticket; assert.notEqual(first, second); assert.deepEqual(w.b.recordLand(first, w.land), { task_ref: TASK, commit: B }); assert.throws(() => w.b.recordLand(second, { ...w.land, commit: C, previous: B }), /decision-consumed/); assert.equal(w.merges().length, 1); assert.equal(w.merges()[0].body.commit, B); assert.equal(w.results().length, 1); assert.deepEqual(w.events('action.refused'), []); // The git socket then ends the second ticket with the broker's code, as it does any record refusal. assert.equal(w.b.landRefused(second, { code: 'decision-consumed', notify: true }), 'decision-consumed'); assert.deepEqual(w.events('action.refused').map((e) => e.body), [{ code: 'decision-consumed' }]); assert.equal(w.merges().length, 1); assert.throws(() => w.b.recordLand(second, w.land), /land-unknown/); }); test('landRefused files the code on the task, tells the role only with notify, and ends the ticket', (t) => { const w = setup(t); const { d } = w.ready(); const first = w.check({ task_ref: TASK, decision: d.id }); assert.throws(() => w.b.landRefused(first.ticket, { code: 'Not A Code' }), /invalid-request/); assert.throws(() => w.b.landRefused(first.ticket, { code: 'x', detail: '' }), /invalid-request/); assert.throws(() => w.b.landRefused(first.ticket, { code: 'x', notify: 'yes' }), /invalid-request/); assert.equal(w.b.landRefused(first.ticket, { code: 'unknown-project' }), 'unknown-project'); assert.deepEqual(w.results(), []); const second = w.check({ task_ref: TASK, decision: d.id }); w.b.landRefused(second.ticket, { code: 'gate-failed', detail: 'pkg-bus failed, see x.log', notify: true }); const [m] = w.results(); assert.equal(m.to_role, 'merge-gate'); assert.equal(m.body, `Landing of ${TASK} under decision ${d.id} refused: gate-failed.\npkg-bus failed, see x.log`); assert.deepEqual( w.events('action.refused').map((e) => [e.subject, e.actor_role, e.body.code]), [ [TASK, 'merge-gate', 'unknown-project'], [TASK, 'merge-gate', 'gate-failed'], ], ); // A refusal spends nothing: the decision still lands. assert.equal(w.held(), true); const third = w.check({ task_ref: TASK, decision: d.id }); w.b.recordLand(third.ticket, w.land); assert.throws(() => w.b.landRefused(second.ticket, { code: 'x' }), /land-unknown/); }); test('landHeld is true only for an approved, unspent cross-role merge decision this role raised for a task in this thread', (t) => { const w = setup(t); assert.equal(w.held(), false); w.push(A); const d = w.raise(); assert.equal(w.held(), false, 'open'); w.resolve(d, 'no'); assert.equal(w.held(), false, 'declined'); const notTask = w.raise('merge-gate', { target: 'main' }); w.resolve(notTask); assert.equal(w.held(), false, 'a target that is not a task ref'); const gated = w.raise('coder'); w.resolve(gated, 'yes', 'human'); assert.equal(w.held('coder'), false, 'gated'); const ok = w.raise(); w.resolve(ok); assert.equal(w.held(), true); assert.equal(w.held('pm'), false, 'another role'); assert.equal(w.held('merge-gate', 'thread-1'), false, 'another thread'); assert.throws(() => w.b.landHeld({ business: 'demo', role: 'merge-gate' }), /invalid-request/); assert.throws(() => w.b.landHeld({ business: 'nope', role: 'merge-gate', thread: null })); }); // Row 156 (#1651): landHolders is landHeld's list for the launcher's landing pass. At each step // of the landHeld test above, the two agree. test('landHolders lists a merge decision exactly while landHeld is true for its non-thread instance, in decision order', (t) => { const w = setup(t); const holders = () => w.b.landHolders('demo'); const agree = (msg) => assert.equal(holders().some((h) => h.instance === 'merge-gate'), w.held(), msg); assert.deepEqual(holders(), []); w.push(A); w.approve(A); const d = w.raise(); agree('open'); w.resolve(d, 'no'); agree('declined'); const notTask = w.raise('merge-gate', { target: 'main' }); w.resolve(notTask); agree('a target that is not a task ref'); const gated = w.raise('coder'); w.resolve(gated, 'yes', 'human'); assert.deepEqual(holders(), [], 'gated'); const first = w.raise(); w.resolve(first); const second = w.raise(); w.resolve(second); agree('approved'); const at = (x) => w.store.get("SELECT at FROM decision_events WHERE decision=? AND op='resolved'", x.id).at; const entry = (x) => ({ instance: 'merge-gate', decision: x.id, task_ref: TASK, resolved_at: at(x) }); assert.deepEqual(holders(), [entry(first), entry(second)]); // The second supersedes the first, so only the second lands. The first is never spent, and // landHeld still counts it, so it stays listed (BUILD.md follow-up 5). assert.throws(() => w.check({ task_ref: TASK, decision: first.id }), /superseded/); w.b.recordLand(w.check({ task_ref: TASK, decision: second.id }).ticket, w.land); assert.deepEqual(holders(), [entry(first)], 'spent'); agree('spent'); assert.throws(() => w.b.landHolders('nope'), /unknown-business/); assert.throws(() => w.b.landHolders('no such'), /invalid-request/); }); test("landHolders leaves out a thread instance's decision, even one raised before its role became one", (t) => { const w = setup(t); w.ready(); assert.equal(w.b.landHolders('demo').length, 1); // A restart with merge-gate now a thread instance: landHeld still answers for the role and // thread asked, but the landing pass starts no non-thread run for it. const roles2 = { ...roles, 'merge-gate': { ...roles['merge-gate'], definition: 'assistant' } }; const b2 = new Broker({ store: w.store, businesses: { demo: demo(undefined, roles2) } }); assert.equal(b2.landHeld({ business: 'demo', role: 'merge-gate', thread: null }), true); assert.deepEqual(b2.landHolders('demo'), []); }); // Row 156: the landing pass stops a role run started before its decision was resolved. Unread // delivered messages keep it; waiting ones stay undelivered for the next holder. Round 2 (F1 in // 29033): so does a run that hasn't asked message.receive yet, since it may be in a landing turn. test('landRelease releases the holding run by land-relaunch once it has asked message.receive and nothing delivered to it is unread, and waiting messages go to the next holder', (t) => { const w = setup(t); const { d } = w.ready(); const release = (extra = {}) => w.b.landRelease({ business: 'demo', run: 'merge-gate-run', decision: d.id, ...extra }); assert.deepEqual(release(), { released: false, landing: true }, 'no receive yet'); assert.deepEqual(release(), { released: false, landing: true }, 'asking again changes nothing'); // The resolve in ready() sent merge-gate its DECISION message, so this receive delivers two. const sent = w.call('human', 'message.send', { to: 'merge-gate', body: 'one' }); const got = w.call('merge-gate', 'message.receive'); assert.equal(got.length, 2); assert.equal(got.filter((m) => m.id === sent.id).length, 1); assert.deepEqual(release(), { released: false, unread: 2 }); assert.equal(w.store.get("SELECT op FROM role_claims WHERE role='merge-gate' ORDER BY seq DESC LIMIT 1").op, 'claim', 'kept'); w.call('merge-gate', 'message.read', { id: sent.id }); assert.deepEqual(release(), { released: false, unread: 1 }); for (const m of got) if (m.id !== sent.id) w.call('merge-gate', 'message.read', { id: m.id }); const waiting = w.call('human', 'message.send', { to: 'merge-gate', body: 'two' }); assert.throws(() => release({ decision: 'nope' }), /decision-not-held/); assert.throws(() => release({ extra: 1 }), /invalid-request/); assert.throws(() => w.b.landRelease({ business: 'demo', run: 'no-run', decision: d.id }), /unknown-run/); assert.throws(() => w.b.landRelease({ business: 'demo', run: 'coder-run', decision: d.id }), /decision-not-held/); // A run of the same instance that doesn't hold the claim releases nothing. w.caps.other = w.b.bindLaunch({ business: 'demo', role: 'merge-gate', run: 'merge-gate-0', harness: 'pi' }); assert.throws(() => w.b.landRelease({ business: 'demo', run: 'merge-gate-0', decision: d.id }), /not-holder/); assert.deepEqual(release(), { released: true }); assert.deepEqual( { ...w.store.get("SELECT op, holder_run, \"by\", decision, reason FROM role_claims WHERE role='merge-gate' ORDER BY seq DESC LIMIT 1") }, { op: 'release', holder_run: 'merge-gate-run', by: 'land-relaunch', decision: null, reason: null }, ); assert.throws(() => release(), /not-holder/, 'released once'); assert.throws(() => w.call('merge-gate', 'message.receive'), /launcher-released/); w.b.endLaunch({ business: 'demo', run: 'merge-gate-run', reason: 'exited' }); assert.throws(() => release(), /run-ended/); assert.equal(w.store.get("SELECT count(*) AS n FROM deliveries WHERE message=? AND op='delivered'", waiting.id).n, 0, 'still waiting'); // The decision is still held, and the next run gets the waiting message. assert.equal(w.held(), true); w.caps.next = w.b.bindLaunch({ business: 'demo', role: 'merge-gate', run: 'merge-gate-2', harness: 'pi' }); w.call('next', 'role.claim'); assert.deepEqual(w.call('next', 'message.receive').map((m) => m.id), [waiting.id]); assert.equal(w.store.get("SELECT holder_run FROM deliveries WHERE message=? AND op='delivered'", waiting.id).holder_run, 'merge-gate-2'); }); // Row 156 round 2 (Sage's F3 ruling on 29033): the run the landing pass starts after the // resolution lands in its landing turn, before it has asked message.receive. Its land_work reaches // checkLand through the git socket with the run's capability; here the call is made as that run. test('a landing run started after the resolution passes checkLand for its held decision before any message.receive, and lands it', (t) => { const w = setup(t); const { d, v } = w.ready(); w.b.endLaunch({ business: 'demo', run: 'merge-gate-run', reason: 'exited' }); assert.deepEqual(w.b.landHolders('demo').map(({ instance, decision, task_ref }) => ({ instance, decision, task_ref })), [{ instance: 'merge-gate', decision: d.id, task_ref: TASK }]); const cap = w.b.bindLaunch({ business: 'demo', role: 'merge-gate', run: 'merge-gate-land', harness: 'pi' }); w.b.request(cap, { verb: 'role.claim', args: {} }); assert.deepEqual(w.b.landRelease({ business: 'demo', run: 'merge-gate-land', decision: d.id }), { released: false, landing: true }); const { ticket, resolved_at, ...facts } = w.b.checkLand(cap, { task_ref: TASK, decision: d.id }); assert.match(ticket, /^[0-9a-f-]{36}$/); assert.deepEqual([facts.decision, facts.commit, facts.verdict, facts.choice], [d.id, A, v.verdict, 'yes']); assert.deepEqual(w.b.recordLand(ticket, w.land), { task_ref: TASK, commit: B }); const [e] = w.merges(); assert.deepEqual([e.actor_run, e.body.decision, e.body.commit], ['merge-gate-land', d.id, B]); // Spent: no longer listed, and nothing was delivered to the landing run. The claim routed the // waiting messages to it as their holder; none was read. assert.deepEqual(w.b.landHolders('demo'), []); assert.equal(w.store.get("SELECT count(*) AS n FROM deliveries WHERE holder_run='merge-gate-land' AND op='delivered'").n, 0); }); // Row 156 (sage-0933): the bus process answers both launch ops from its own launchOp line. A // LAUNCH_OPS name without one refuses launch-op-refused; before row 156 it answered as // credentialStatus, whose [] for landHolders read as "no holders" (mutant P02). test('the bus process answers landHolders and landRelease from their own lines, not as credentialStatus', async (t) => { const root = mkdtempSync(join(tmpdir(), 'bus-land-ops-')); t.after(() => rmSync(root, { recursive: true, force: true })); const child = fork(new URL('../src/process.mjs', import.meta.url), [], { stdio: ['ignore', 'ignore', 'ignore', 'ipc'] }); t.after(() => { if (child.exitCode === null) child.kill('SIGKILL'); }); const ask = async (m) => { const reply = once(child, 'message'); child.send(m); return (await reply)[0]; }; const businesses = { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'pm', delivery: 'pm' }, roles } }; assert.equal((await ask({ op: 'boot', config: { dataRoot: root, businesses, launches: [], readers: [] } })).ok, true); // Without its launchOp line each would get launch-op-refused, not the broker's own answer. assert.deepEqual(await ask({ op: 'landHolders', business: 'demo', id: 1 }), { ok: true, result: [], id: 1 }, 'landHolders, not launch-op-refused'); assert.deepEqual( await ask({ op: 'landRelease', record: { business: 'demo', run: 'no-run', decision: 'd1' }, id: 2 }), { ok: false, error: 'unknown-run', id: 2 }, 'landRelease, not launch-op-refused', ); assert.deepEqual(await ask({ op: 'credentialStatus', business: 'demo', role: 'pm', id: 3 }), { ok: true, result: [], id: 3 }); const exit = once(child, 'exit'); child.send({ op: 'close' }); assert.equal((await exit)[0], 0); }); // Row 156 (sage-0808): a role run's landing turn brackets its usage with the decision id. The // begin needs the decision held for the run's instance; the end needs that begin. test('usage.begin with a decision id needs it held for the run\'s instance, usage.end needs the begin, and the limit checks see the landing turn', async (t) => { const asked = []; const w = setup(t, { extra: (store, root, businesses) => { const usage = createUsage({ store, dataRoot: root, businesses, log: () => {} }); return { usage: { collect: (...a) => usage.then((u) => u.collect(...a)), view: () => ({}) }, limits: { mustStop: async (business, subject) => { asked.push({ subject, turns: turnRecords(store, business) }); return { stop: false }; } }, }; }, }); const usage = (who, phase, turn) => w.call(who, `usage.${phase}`, { turn }); const open = w.raise('merge-gate'); await assert.rejects(usage('merge-gate', 'begin', open.id), { code: 'not-recipient' }, 'open'); w.push(A); w.approve(A); w.resolve(open); await assert.rejects(usage('pm', 'begin', open.id), { code: 'not-recipient' }, "another instance's"); await assert.rejects(usage('merge-gate', 'end', open.id), { code: 'not-recipient' }, 'no begin'); assert.deepEqual(await usage('merge-gate', 'begin', open.id), { recorded: true }); // The landing spends the decision inside the turn; the end still records. w.b.recordLand(w.check({ task_ref: TASK, decision: open.id }).ticket, w.land); await assert.rejects(usage('pm', 'end', open.id), { code: 'not-recipient' }, "another run's begin"); assert.deepEqual(await usage('merge-gate', 'end', open.id), { recorded: true }); assert.deepEqual( w.store.all('SELECT run, instance, turn, phase FROM usage_records ORDER BY seq').map((r) => ({ ...r })), ['begin', 'end'].map((phase) => ({ run: 'merge-gate-run', instance: 'merge-gate', turn: open.id, phase })), ); assert.equal(asked.length, 1); assert.deepEqual(asked[0].subject, { instance: 'merge-gate', provider: null, model: null, account: null }); assert.deepEqual(asked[0].turns.map((r) => [r.run, r.turn, r.instance]), [['merge-gate-run', open.id, 'merge-gate']]); // Spent, a second landing turn's begin refuses. await assert.rejects(usage('merge-gate', 'begin', open.id), { code: 'not-recipient' }, 'spent'); }); test('no run may write the action.allowed for git.merge.protected itself', (t) => { const w = setup(t); assert.throws( () => w.b.recordEvent(w.caps.coder, { kind: 'action.allowed', body: { action: MERGE, target: TASK, commit: B }, subject: TASK }), /reserved-event/, ); });