diff --git a/agents/megavolt/work/queue-199/BUILD.md b/agents/megavolt/work/queue-199/BUILD.md index f9f69102a..969f3c202 100644 --- a/agents/megavolt/work/queue-199/BUILD.md +++ b/agents/megavolt/work/queue-199/BUILD.md @@ -107,6 +107,13 @@ this branch, apart from one slip noted under "Runs". refuses `ref-moved` with no request to the fork; a push recorded while the bus process pushes refuses `decision-mismatch`, filed once, and the fork holds only the ticket's oid. +15. **Fix: rotate on a due gitea key (gizmoduck-0050, sage-1494).** The CLI's `ISSUED` set + (`packages/cli/src/credentials.mjs`) listed valid, expiring and expired, but the bus's `status()` + gives a gitea key past its rotate-by date `rotation-due`. So `credentials rotate` refused the one + case it exists for. `ISSUED` now names every state status() gives a held token. The test: + `cli/tests/credentials.test.mjs`, "rotate goes ahead on each held state". Its mutant (the old set) + fails that test and goes in the section 11 list. + ## N9, for the routing commit (sage-1388) Today git.push.remote is a hard wait, so every publish carries a decision, and the decision's @@ -152,6 +159,9 @@ Hash pins: none on a changed file. sage-1440 changes (the poll bound, the mid-flight tests), single files, green before the commit: cli publish-socket (7), git-socket (24), launcher (29). Hash pins: none on a changed file. +The rotation-due fix, single files: cli credentials (4), green; the old-set mutant fails it 3/1. +Hash pins: none on a changed file. + Slip, 2026-10-20 about 19:51Z: I ran `node --test packages/tasks/tests/*.test.mjs` in one command, which is a package suite, without asking Sage for the slot. It passed, 97 pass and 0 fail, in under a minute. Reported to Sage in megavolt-0017. diff --git a/packages/cli/src/credentials.mjs b/packages/cli/src/credentials.mjs index daea2c3b4..b5f3972c1 100644 --- a/packages/cli/src/credentials.mjs +++ b/packages/cli/src/credentials.mjs @@ -34,7 +34,9 @@ async function readToken(stdin) { return text; } -const ISSUED = new Set(["valid", "expiring", "expired"]); +// The states the bus's status() gives a held token: gitea is valid or rotation-due, vikunja valid, +// expiring or expired. not-issued and revoked hold none. +const ISSUED = new Set(["valid", "rotation-due", "expiring", "expired"]); export async function credentialsCommand(args, io, callFor) { const parsed = parse(args); diff --git a/packages/cli/tests/credentials.test.mjs b/packages/cli/tests/credentials.test.mjs index c13e5278e..0604f9b8c 100644 --- a/packages/cli/tests/credentials.test.mjs +++ b/packages/cli/tests/credentials.test.mjs @@ -17,10 +17,10 @@ const ROWS = [ { service: "gitea", account: null, role: "reviewer", dateKind: "rotateBy", date: "2099-01-01", state: "revoked" }, ]; -function deps(calls) { +function deps(calls, rows = ROWS) { const answer = (who) => (business) => async (verb, args) => { calls.push({ who, business, verb, args }); - if (verb === "credentials") return ROWS; + if (verb === "credentials") return rows; if (verb === "credentials.issue") return { agent: args.agent, verb: "issue", service: args.service, date: args.date, state: "valid" }; return { agent: args.agent, verb: verb.slice(12), services: [] }; }; @@ -57,6 +57,23 @@ test("row 199: issue and rotate send the stdin token once to credentials.issue a await assert.rejects(main(["credentials", "issue", "coder", "gitea", "--rotate-by", "2099-01-01", "--business", "acme"], withStdin(io({ CLAUDECODE: "1" }), TOKEN), d), /outside any agent run/); }); +// gizmoduck-0050: a gitea key past its rotate-by date is rotation-due, and rotate is how a human +// clears it. Every state the bus's status() gives a held token rotates; the other two refuse. +test("row 199: rotate goes ahead on each held state, rotation-due included, and refuses not-issued, revoked and an unknown entry", async () => { + const states = { valid: true, "rotation-due": true, expiring: true, expired: true, "not-issued": false, revoked: false }; + for (const [state, held] of Object.entries(states)) { + const calls = []; + const d = deps(calls, [{ service: "gitea", account: null, role: "coder", dateKind: "rotateBy", date: "2020-01-01", state }]); + const run = main(["credentials", "rotate", "coder", "gitea", "--rotate-by", "2099-06-01", "--business", "acme"], withStdin(io(), TOKEN), d); + if (held) await run; + else await assert.rejects(run, /no issued gitea credential to rotate; use issue/); + assert.deepEqual(calls.map((c) => c.verb), held ? ["credentials", "credentials.issue"] : ["credentials"], state); + } + const calls = []; + await assert.rejects(main(["credentials", "rotate", "tester", "gitea", "--rotate-by", "2099-06-01", "--business", "acme"], withStdin(io(), TOKEN), deps(calls)), /no issued gitea credential/); + assert.deepEqual(calls.map((c) => c.verb), ["credentials"]); +}); + test("row 199: status, revoke and delete; the tester runs status and revoke only", async () => { const calls = []; const d = deps(calls);