queue: pin the add retry order and drop row history from the gate-owner refusal (row 107, #1601)
A retried privileged add recorded with no gate owner answers with its receipt before the --gate-owner check runs; a test pins that order and a hoisting mutant fails it. The refusal no longer cites row 100. SEMANTICS stays 2. Packet: agents/researcher/work/queue-107/ (manifest b70dfe4a). Filbert approved round 1 in comment 28286. Sage's canonical-root gate: queue 156/0, test-queue 30/0 with queue verify. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -528,11 +528,12 @@ function mutateAs(ctx, { verb, op, args, by, yes = false, derived = false }) {
|
||||
if (view.state === "unknown") throw refuse(unknownMessage(view));
|
||||
if (view.state === "stale" && verb !== "accept-history") throw refuse(staleMessage(view, cur.doc.log));
|
||||
// A privileged add names its gate owner. There is no default for it: the
|
||||
// Jason default put a gate nobody meant on row 100. This check lives in the
|
||||
// Jason default put a gate nobody meant on a row. This check lives in the
|
||||
// write path, not in applyEntry, so replay of old entries is unchanged and an
|
||||
// older reader still loads the log.
|
||||
// older reader still loads the log. It stays below step 3: a retry of an add
|
||||
// recorded before this check answers with its receipt.
|
||||
if (verb === "add" && PRIVILEGED.has(actor) && cargs.gateOwner === null) {
|
||||
throw refuse(`${actor} must pass --gate-owner SEAT on add: a privileged add has no default gate owner, so a row never gets a gate nobody meant (row 100 got jason by default)`);
|
||||
throw refuse(`${actor} must pass --gate-owner SEAT on add: a privileged add has no default gate owner, so a row never gets a gate nobody meant`);
|
||||
}
|
||||
// Step 5.
|
||||
const resolved = resolveFor(ctx, loc, cur, verb, cargs, cmp);
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
// scratch repository. Replay is exercised through loadDoc, which runs the real
|
||||
// replay path over a log that holds an old privileged add with no gate owner.
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { test } from "node:test";
|
||||
import {
|
||||
applyEntry, buildDoc, canonArgs, genesisReceipt, genesisRows, loadDoc, parseMigrationMap, render, rowsArray, serialize, sha256,
|
||||
applyEntry, buildDoc, canonArgs, genesisReceipt, genesisRows, loadDoc, logDigest, parseMigrationMap, render, rowsArray, serialize, sha256, splitView,
|
||||
} from "../src/queue.mjs";
|
||||
import { QueueError } from "../src/errors.mjs";
|
||||
import { MAP_ROWS, cli, genesisCommitted, mapText, scratchRepo } from "./helpers.mjs";
|
||||
@@ -82,6 +83,7 @@ test("the CLI refuses a privileged add without --gate-owner, for jason and sage,
|
||||
assert.equal(r.code, 2, r.err);
|
||||
assert.match(r.err, /--gate-owner/);
|
||||
assert.match(r.err, /no default gate owner/);
|
||||
assert.doesNotMatch(r.err, /row \d+/, "the refusal states the rule, not a row's history");
|
||||
assert.deepEqual(readFileSync(repo.queuePath), queueBefore);
|
||||
assert.deepEqual(readFileSync(repo.viewPath), viewBefore);
|
||||
assert.equal(JSON.parse(readFileSync(repo.queuePath, "utf8")).revision, revBefore);
|
||||
@@ -113,3 +115,39 @@ test("the CLI add by a non-privileged seat gets jason and can't name a gate owne
|
||||
assert.match(refusedRun.err, /only a privileged actor may set gateOwner on add/);
|
||||
assert.deepEqual(readFileSync(repo.queuePath), before);
|
||||
});
|
||||
|
||||
// Step 3 of the write path answers a recorded op before the gate-owner check
|
||||
// runs. An add recorded before row 101 has a null gate owner, so a retry of it
|
||||
// carries none either; hoisting the check above step 3 would refuse that retry.
|
||||
// Every other retry test passes --gate-owner, so this one pins the order.
|
||||
test("a retried sage add recorded with no gate owner answers (already recorded at rev N) and writes nothing", (t) => {
|
||||
const repo = scratchRepo(t);
|
||||
genesisCommitted(repo);
|
||||
const before = JSON.parse(readFileSync(repo.queuePath, "utf8"));
|
||||
const doc = addUnder(before, "sage", addArgs(), 2);
|
||||
const entry = doc.log.at(-1);
|
||||
assert.equal(entry.args.gateOwner, null);
|
||||
assert.equal(entry.by, "sage");
|
||||
// The state the old write path left: queue.json, the view and the witness all at the add's rev.
|
||||
const bytes = Buffer.from(serialize(doc));
|
||||
const parts = splitView(readFileSync(repo.viewPath, "utf8"));
|
||||
writeFileSync(repo.queuePath, bytes);
|
||||
writeFileSync(repo.viewPath, parts.head + render(doc.rows, entry.rev) + parts.tail);
|
||||
writeFileSync(join(repo.gitDir, "mosaic-queue.head"), `${JSON.stringify({ revision: entry.rev, logDigest: logDigest(doc.log, entry.rev), fileSha: sha256(bytes), at: entry.at })}\n`);
|
||||
const queueBefore = readFileSync(repo.queuePath);
|
||||
const viewBefore = readFileSync(repo.viewPath);
|
||||
|
||||
const retry = ["add", "--op", entry.op, "--piece", "Gate row", "--gate", "g", "--brief", "docs/plans/brief-b.md#Queue"];
|
||||
const r = cli(repo, retry, { by: "sage" });
|
||||
assert.equal(r.code, 0, r.err);
|
||||
assert.match(r.out, new RegExp(`\\(already recorded at rev ${entry.rev}\\)`));
|
||||
assert.deepEqual(readFileSync(repo.queuePath), queueBefore);
|
||||
assert.deepEqual(readFileSync(repo.viewPath), viewBefore);
|
||||
assert.equal(JSON.parse(readFileSync(repo.queuePath, "utf8")).revision, entry.rev);
|
||||
|
||||
// The check still bites a new op with the same arguments.
|
||||
const fresh = cli(repo, ["add", "--op", "add-gate-fresh-1", ...retry.slice(3)], { by: "sage" });
|
||||
assert.equal(fresh.code, 2, fresh.err);
|
||||
assert.match(fresh.err, /--gate-owner/);
|
||||
assert.deepEqual(readFileSync(repo.queuePath), queueBefore);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user