honker: row 194 b3 sage-1620: credential-revoke-failed says the business file is unchanged and a retry is safe (CLI and Console), W2 checks the log per case

This commit is contained in:
jason.woltje committed 2026-10-20 20:05:59 -05:00
1 parent b1db067c59
commit b397661129
6 files changed
+38 -3

No files matched your search

+15
View File
@@ -690,3 +690,18 @@ Every test file under `packages/{business,bus,harness,cli,webui}/tests`, run sin
above). After the fixes and the tester change in `instance-settings.mjs`, 17 files reran singly: 167 pass,
0 fail. The total uses the rerun counts for those 17, summed by awk over the logs. Suites and mutants
M1-M73 wait on Sage's slot.
### sage-1620 (2026-10-21T01:05Z)
- W2 reading confirmed. Each refused W2 case now asserts the log length as well as the bytes and revision
(`bus/tests/tester-writes.test.mjs`, inside the loop). M73 stays in the batch.
- Deviation 11 accepted. The refusal text says a retry is safe: `credential-revoke-failed` has a reason in
the CLI (`cli/src/instances.mjs` REASONS) and in the Console (`webui/src/serve.mjs` AGENT_REASONS, shown on
a closed request's read). CLI T20 now fails a delete with a foreign file in rev-2's store, checks the text
and the unchanged revision, then clears the file and deletes. Console agents.test checks the read's reason.
- Mutants for the slot: M74 drops the CLI reason (CLI T20 fails); M75 drops the Console reason (agents.test
fails).
- Seat: every `packages/seat/tests` file singly (164 pass, 0 fail, 7 files) and the seat suite whole
(`node --test tests/`, 164 pass, 0 fail), for the broker import change (row 210's gate finding).
Touched files singly after this change: tester-writes 8, cli lifecycle 16, cli instances 14, webui agents
8, agents-browser 1, all pass.
+1 -1
View File
@@ -108,8 +108,8 @@ test('row 175: W2 as the tester refuses credentials, protected definitions, huma
assert.equal(r.code(r.tester, 'instances.set', { revision, ...args }), want, JSON.stringify(args));
assert.equal(r.bytes(), before, JSON.stringify(args));
assert.equal(r.call(r.tester, 'instances').revision, revision);
assert.equal(r.log().length, entries, `a refused write logs nothing: ${JSON.stringify(args)}`);
}
assert.equal(r.log().length, entries, 'a refused write logs nothing');
// The control: the same add with an instance var lands, so the refusal above is the var.
r.call(r.tester, 'instances.set', { revision, instance: 'gate-2', op: 'add', definition: 'merge-gate', vars: { model: 'claude-sonnet-5-5' } });
assert.ok(r.log().length > entries);
+5
View File
@@ -50,6 +50,11 @@ export const REASONS = {
"regenerate-timeout": "the old run didn't exit within the request's 60 s, so no new run started; it still runs",
"workspace-rename-failed": "the business file is changed and the name retired, but the workspace stayed where it was; " +
"run the same delete again to retry the rename",
// Row 194 (brief item 8, deviation 11, sage-1620): a delete revokes before it writes, so a failure can
// leave tokens revoked with the instance still listed. It holds nothing it can use, and a retry is safe.
"credential-revoke-failed": "a credential couldn't be revoked, so the business file is unchanged; tokens already revoked " +
"stay revoked and the instance can't use them; clear what else the agent's store directory holds and run the same " +
"command again, which is safe",
"capacity-full": "a launch limit is reached; `mosaic limits --business BUSINESS` shows them",
"limit-reached": "a launch limit is reached; `mosaic limits --business BUSINESS` shows them",
"unknown-role": "the business has no such instance",
+10
View File
@@ -424,7 +424,17 @@ test("row 194 T20: a store-only delete with a tracker bot closes naming the bot,
// The agents' store root beside the business files, private as the boot check needs it.
mkdirSync(join(p.root, "config", "secrets", "agents"), { recursive: true, mode: 0o700 });
const s = await start(t, p);
// Deviation 11 (sage-1620): a file the store doesn't own fails the revoke. The CLI says the business
// file is unchanged and a retry is safe, and rev-2 stays listed.
const store = join(p.root, "config", "secrets", "agents", "acme", "rev-2");
mkdirSync(store, { recursive: true, mode: 0o700 });
writeFileSync(join(store, "notes"), "x");
const revision = JSON.parse((await s.cli(["instances", "acme", "--json"])).out).revision;
const failed = await s.cli(["instance", "delete", "acme", "rev-2", "--revision", revision]);
assert.equal(failed.code, 3, failed.err);
assert.match(failed.err, /^mosaic: credential-revoke-failed: .*business file is unchanged; tokens already revoked stay revoked .*which is safe \(request [^)]+\)$/m);
assert.equal(JSON.parse((await s.cli(["instances", "acme", "--json"])).out).revision, revision);
rmSync(join(store, "notes"));
const del = await s.cli(["instance", "delete", "acme", "rev-2", "--revision", revision]);
assert.equal(del.code, 0, del.err);
assert.match(del.out, /^business acme: deleted rev-2, now retired\ntransaction [^\n]+\ntracker bot bot-acme-rev-2: remove the Vikunja bot user and any token it holds by hand in Vikunja\n$/);
+2
View File
@@ -359,6 +359,8 @@ const AGENT_REASONS = {
'request-cancelled': 'a terminate cancelled the request',
'regenerate-timeout': 'the old run didn\'t exit within the request\'s 60 s, so no new run started; it still runs',
'workspace-rename-failed': 'the business file is changed and the name retired, but the workspace stayed where it was; delete it again to retry the rename',
// Brief item 8, deviation 11 (sage-1620): the revoke runs before the write, so a retry is safe.
'credential-revoke-failed': 'a credential couldn\'t be revoked, so the business file is unchanged. Tokens already revoked stay revoked and the instance can\'t use them. Clear what else the agent\'s store directory holds and delete again, which is safe',
'capacity-full': 'a launch limit is reached',
'limit-reached': 'a launch limit is reached',
'no-route': 'the instance has no harness, model and thinking yet; set its route first',
+5 -2
View File
@@ -146,16 +146,19 @@ test('row 194 T16: the request read takes one id, adds the refusal reason and re
assert.deepEqual([closed.body.state, closed.body.done.run, closed.body.reason], ['closed', 'r-2', undefined]);
answer = args => ({ ...ANSWERS['instance.request'](args), done: { run: null, transaction: null, refusal: 'no-route' } });
assert.match((await get(s, `/api/agent/request?id=${ID}`)).body.reason, /route/);
// Row 194 deviation 11 (sage-1620): a failed revoke says the file is unchanged and a retry is safe.
answer = args => ({ ...ANSWERS['instance.request'](args), done: { run: null, transaction: null, refusal: 'credential-revoke-failed' } });
assert.match((await get(s, `/api/agent/request?id=${ID}`)).body.reason, /business file is unchanged\. Tokens already revoked stay revoked .* delete again, which is safe$/);
answer = args => ({ ...ANSWERS['instance.request'](args), done: null, cancelled: { by: 'jason' } });
assert.ok((await get(s, `/api/agent/request?id=${ID}`)).body.reason);
answer = args => ({ ...ANSWERS['instance.request'](args), state: 'taken', done: null });
assert.equal((await get(s, `/api/agent/request?id=${ID}`)).body.state, 'taken');
assert.deepEqual(s.calls.map(c => c[0]), Array(4).fill('instance.request'));
assert.deepEqual(s.calls.map(c => c[0]), Array(5).fill('instance.request'));
for (const query of ['', '?id=nope', `?id=${ID}&id=${ID}`, `?id=${ID}&x=1`, `?id=${ID.toUpperCase()}`]) {
const r = await get(s, `/api/agent/request${query}`);
assert.equal(r.status, 400, query); assert.equal(r.body.error, 'invalid-request');
}
assert.equal(s.calls.length, 4, 'a bad id makes no call');
assert.equal(s.calls.length, 5, 'a bad id makes no call');
answer = () => { throw new BusReadError('unknown-request'); };
const unknown = await get(s, `/api/agent/request?id=${ID}`);
assert.equal(unknown.body.error, 'unknown-request');