From 06ee5ed6b603b81caf6bce7f60da09c74ed3b0d6 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Thu, 22 Oct 2026 06:30:00 -0500 Subject: [PATCH] row 281 b3: the first base run's five red tests were test faults, no code change; T4b reads the manual's revoke id from its event, T5b expects gitea-left:,tokens (N10), T6b allows step 0's GET checks, gitea T8 leaves the call log out, cli T10 makes the 0700 store root; rebase onto 980cec88c noted (sage-2605, sage-2615, #1786) --- agents/megavolt/work/queue-281/BUILD.md | 42 +++++++++++++++++++ .../bus/tests/fire-revokes-grants.test.mjs | 10 +++-- packages/bus/tests/gitea-grant.test.mjs | 4 +- packages/cli/tests/lifecycle.test.mjs | 4 +- 4 files changed, 55 insertions(+), 5 deletions(-) diff --git a/agents/megavolt/work/queue-281/BUILD.md b/agents/megavolt/work/queue-281/BUILD.md index 5830e9e4d..6e362e8fa 100644 --- a/agents/megavolt/work/queue-281/BUILD.md +++ b/agents/megavolt/work/queue-281/BUILD.md @@ -692,6 +692,48 @@ this row: - `read(business, actor)` changes only the listing's `endProtected`. The broker and these tests don't read it. The T10 cli case deletes `sec-1` (security-reviewer), which isn't END_PROTECTED. +Rebase onto 980cec88c, after row 277 (sage-2605). It's on branch `megavolt/row281-c`. +`git range-diff 46dedf925..7454be493 980cec88c..c174f37ac` gives `=` for all ten commits. Row 277's +`thread.claims` change to `broker.mjs` doesn't touch step G or any line the mutants change: +`scratch/megavolt-r281/anchors.sh` counts each M1-M21 sed anchor in the rebased `broker.mjs`, and all 28 +counts are 1. The mutants script runs it after a green base and stops if a count isn't 1. + +b3, after the first base run on c174f37ac (11:24:47Z) came back red. These tests had never run before: b2 +was written under git and docs only. bus: 120 tests, 116 pass, 4 fail. cli lifecycle: 24 tests, 23 pass, +1 fail. Every older test in those files passed. No mutant ran. Each failure is a test fault, and b3 changes +no code: +- T4b, `fire-revokes-grants`: **test changed**. The test read `manual.revoke`, but the `hosts.revoke` + answer has never carried `revoke`: `#revokeHost` drops it, so the verb's answer keeps its pre-281 shape. + The fire's close names the manual revoke, as section 3a says. The test now takes that id from the + manual's `grant.revoked` event. +- T5b, `fire-revokes-grants`: **test changed**. The job refused `gitea-left:9,tokens`, and the test + expected `gitea-left:`. Section 4 sets the reason as `gitea-left:`, the ids left at Gitea. + For a fire, a close's `serviceLeft` is the N10 list from before 281: each token id whose DELETE failed, + then `tokens` or `login` for a fire step that failed (`#giteaCloseRevoke`). With the DELETE failing, + `#giteaFire`'s token list never clears, so `tokens` is a real entry left at Gitea. It belongs in the + reason. The test now expects `gitea-left:,tokens`. +- T6b, `fire-revokes-grants`: **test changed**. After the fire, the executor made 4 Gitea calls where the + test expected none. They are step 0's admin checks C1 to C4: version, self, token scopes and the repo + check, all GET. Step 1's `#giteaRevokedSince` then ends the decision `revoked` before any write. Section + 3c of d6 names that step 1 check as the only thing that stops the decision. T6b's line in section 8 says + "before any Gitea call", but the evidence it lists is no row, no token file and no token create. That + phrase was wrong, because step 0 always runs first. The approved text stays as it is, and this note is + the correction. The test now checks that every call after the fire is a GET and that no token create + happened. Moving the revoked check ahead of step 0 would change the executor for every revoke, not just + step G's, and the design doesn't ask for that. +- T8, `gitea-grant`: **test changed**. The human revoke's events between `grant.revoked` and + `grant.revoke.closed` include its 5 `grant.call`/`grant.result` pairs (`#giteaLog` logs both around every + Gitea call). T8 checks that a human revoke writes no job records, not that it makes no calls. It now + leaves the call log out before comparing. +- T10, cli `lifecycle`: **test changed** (sage-2615 asked about this one). The `credential-file` came from + `startBroker`, not from the delete and not from Gitea. The seeded host grant gives `sec-1` a `host.web1` + store reference. `Credentials` checks the store root (`/secrets/agents`) with `privateDir` at + start, and the fixture had no store root. That check is row 199's, and it is right to fail closed. The + test now makes the store root at 0700 under `dirname(p.f.config)`, as the bus host fixtures do, never + under the real config. sage-2575's condition still holds. This business has no Gitea grant, so + `#grantSet` adds no `gitea` entry (`#giteaPending` and `#giteaUnfired` read only the store), and step G + never reaches `#stepGGitea`. A delete with no Gitea grant reads no Gitea credential file. + ## Runs (after the gate) diff --git a/packages/bus/tests/fire-revokes-grants.test.mjs b/packages/bus/tests/fire-revokes-grants.test.mjs index a1ed31783..beb02cd28 100644 --- a/packages/bus/tests/fire-revokes-grants.test.mjs +++ b/packages/bus/tests/fire-revokes-grants.test.mjs @@ -532,7 +532,9 @@ test('row 281 T4b (Q1): a line a manual hosts.revoke left on C is removed by the assert.equal(close.length, 1); assert.equal(close[0].hostLine, 'removed'); assert.equal(close[0].tag, tag3); - assert.equal(close[0].revoke, manual.revoke); + // hosts.revoke's answer has no revoke id; the manual's grant.revoked has it. + const [manualRevoked] = f.revoked('coder', (e) => e.host === 'web3' && e.fire !== true); + assert.equal(close[0].revoke, manualRevoked.revoke); const [done] = f.allowed('fire.done'); assert.deepEqual(done.body.grants.map((x) => x.grant), ['host:web1', 'line:web3']); assert.deepEqual(done.body.grantsLeft, []); @@ -594,7 +596,8 @@ test("row 281 T5b (N1): a failed token DELETE refuses gitea-left; the repeat ret await f.broker.grantsIdle(); const [refused] = f.grantEvents().filter((e) => e.action === 'grant.revoke.refused'); assert.equal(refused.grant, 'gitea'); - assert.match(refused.reason, /^gitea-left:\d+$/); + // The close's serviceLeft (N10): the token id, and "tokens" for the fire's list that didn't clear. + assert.match(refused.reason, /^gitea-left:\d+,tokens$/); assert.equal(refused.job, out.job); assert.equal(f.userTokens('demo-coder').length, 1); assert.equal(f.allowed('fire.done').length, 0); @@ -648,7 +651,8 @@ test('row 281 T6b (sage-2574): a Gitea decision approved before the fire and run assert.equal(f.ended(id).code, 'revoked'); assert.equal(f.giteaRow('coder'), undefined); assert.equal(existsSync(f.tokenFile('coder')), false); - assert.equal(f.gitea.requests.length, calls); + // Only step 0's C1 to C4 reach Gitea, all reads; step 1's recheck ends it before any write. + assert.ok(f.gitea.requests.slice(calls).every((e) => e.method === 'GET')); assert.ok(!f.gitea.requests.some((e) => e.method === 'POST' && e.path.endsWith('/tokens'))); }); diff --git a/packages/bus/tests/gitea-grant.test.mjs b/packages/bus/tests/gitea-grant.test.mjs index 7b08d6ee4..f7c49986f 100644 --- a/packages/bus/tests/gitea-grant.test.mjs +++ b/packages/bus/tests/gitea-grant.test.mjs @@ -1227,7 +1227,9 @@ test('row 281 T8: a tester grants.revoke refuses tester-dev-only once bus.tester assert.equal(f.grantEvents().length, before); assert.ok(f.record()); await f.call('human', 'grants.revoke', { service: 'gitea', agent: 'coder', reason: 'r' }); - const after = f.grantEvents().slice(before).map((e) => [e.action, e.fire ?? false]); + const after = f.grantEvents().slice(before) + .filter((e) => e.action !== 'grant.call' && e.action !== 'grant.result') + .map((e) => [e.action, e.fire ?? false]); assert.deepEqual(after, [['grant.revoked', false], ['grant.revoke.closed', false]]); assert.deepEqual(f.userTokens(), []); const jobs = f.store.all("SELECT body FROM events WHERE business='demo' AND kind='action.allowed'").map((e) => JSON.parse(e.body).action) diff --git a/packages/cli/tests/lifecycle.test.mjs b/packages/cli/tests/lifecycle.test.mjs index a5bd07b78..a69f991c1 100644 --- a/packages/cli/tests/lifecycle.test.mjs +++ b/packages/cli/tests/lifecycle.test.mjs @@ -13,7 +13,7 @@ import { spawn, spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import { DatabaseSync } from "node:sqlite"; import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { launchDoc } from "../../business/tests/helpers.mjs"; import { BusError } from "../../bus/src/broker.mjs"; @@ -436,6 +436,8 @@ test("row 194 V5: a delete with no workspace commits and closes with the transac // fail or wait; either way the request has already closed. test("row 281 T10: the delete pass closes a granted instance's first request grants-revoking and the host channel stays up", { skip, timeout: 90000 }, async (t) => { const p = prepare(t); + // A host grant gives the agent a host.web1 store reference, so Credentials checks the store root at start. + mkdirSync(join(dirname(p.f.config), "secrets", "agents"), { recursive: true, mode: 0o700 }); const seeded = new Store(p.f.dataRoot); const entry = { address: "192.0.2.10", port: 22, user: "deploy", hostKeys: ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHostKeyForTests"] }; hostRecord(seeded, { business: "acme", host: "web1", entry });