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:<id>,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)

This commit is contained in:
jason.woltje committed 2026-10-22 06:31:01 -05:00
1 parent eed5a65b6d
commit 06ee5ed6b6
4 files changed
+55 -5

No files matched your search

+42
View File
@@ -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:<digits>`. Section 4 sets the reason as `gitea-left:<ids>`, 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:<id>,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 (`<config dir>/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)
@@ -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')));
});
+3 -1
View File
@@ -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)
+3 -1
View File
@@ -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 });