honker: row 256 (#1761) b1: the human deletes a creative instance and any other actor is refused organization-creative-human-only; protectedRefusal is the delete check and the listing's per-reader endProtected; actor threaded through planDelete/checkDelete/instances.check; fire unchanged; DEFERRED entry closed for delete; tests T1-T8, unrun

This commit is contained in:
jason.woltje committed 2026-10-22 03:57:05 -05:00
1 parent 8c50481d2f
commit ec82624742
9 files changed
+241 -23

No files matched your search

+67
View File
@@ -0,0 +1,67 @@
# Row 256 (#1761) build notes (append only)
## 2026-10-22 b1 (Honker)
Design d2 8c50481d2 was accepted by Webby (webby-0123). The code is on honker/row256 from be2b008cc.
### Correction to the design: the non-creative END_PROTECTED definition is `assistant`
- DESIGN.md (d1 and d2) says "the chief of staff" for the END_PROTECTED definition that isn't creative,
following the brief's wording. In the code that definition is `assistant`:
- `THREAD_DEFINITION = "assistant"` (vocabulary.mjs:203).
- `END_PROTECTED = THREAD_DEFINITIONS = [assistant, ...CREATIVE_DEFINITIONS]`.
- The `chief-of-staff` definition (the `chief` instance) isn't END_PROTECTED. Its delete refuses
`organization-instance-referenced`, because it is launch.by, and that is unchanged.
- So T3 and M2 use an `assistant` instance. The code is the same either way: protectedRefusal refuses
every END_PROTECTED definition that isn't creative.
- DESIGN.md isn't edited after approval; this note is the record.
### Persona file on delete (brief item 4)
- `<configDir>/personas/<business>/<instance>.md` is left in place. Nothing removes it, so there is no
new open and no O_NOFOLLOW test.
- Nothing reads it once the entry is gone. bundle.mjs reads it only for a launched creative instance,
and the name is retired, so `add` refuses it (instance-settings.mjs, organization-instance-retired)
and no later instance picks up the old persona.
- The human removes the file by hand if they want it gone.
### What changed
- packages/business/src/instance-settings.mjs:
- New `protectedRefusal(document, instance, actor)`.
- `deleteRefusal(..., actor)`, `planDelete(..., actor)` and `checkDelete(..., actor)` take the
actor, and set op delete passes its actor.
- `read(business, actor)`. Its `endProtected` is `protectedRefusal(...) !== null`.
- commit's and fire's returned listings read with the writer's actor.
- The comment at :28-31 is rewritten.
- packages/bus/src/runtime.mjs: `instances` and `instances.check` pass the actor. There is no broker
change.
- packages/webui/src/public/agents.js: comment only.
- docs/plans/DEFERRED.md: a closing line on the creative-delete entry.
- Tests:
- business instance-settings.test: T1-T3 and T5 in one test, T4 in its own test, and the row 213
test's `endProtected` for the human's add is now `false`.
- business lifecycle-settings.test: checkDelete with the actor; fire unchanged.
- bus creative.test: T6 on the real runtime.
- webui agents-browser.test: T7/T8. The comment on the existing assertion is reworded.
### Mutants for the gate (exact-once replacements, runb8 shape)
- M1 (instance-settings.mjs): ` if (actor === document.human) return null;` becomes ` return null;`.
The kill is instance-settings.test "row 256 T1-T3".
- M2 (instance-settings.mjs): move the human check ahead of the creative one. Replace
` if (!CREATIVE_DEFINITIONS.includes(definition)) return { code: "organization-definition-refused" };\n if (actor === document.human) return null;`
with
` if (actor === document.human) return null;\n if (!CREATIVE_DEFINITIONS.includes(definition)) return { code: "organization-definition-refused" };`.
The kill is "row 256 T1-T3" (T3).
- M3 (instance-settings.mjs): ` endProtected: protectedRefusal(document, name, actor) !== null,` becomes
` endProtected: END_PROTECTED.includes(entry.definition),`. The kills are "row 256 T1-T3" (T5) and
bus creative.test T6.
- M4a (runtime.mjs): `instances.checkDelete(id,args.instance,actor)` becomes
`instances.checkDelete(id,args.instance)`. The kill is creative.test "row 256 T6".
- M4b (instance-settings.mjs): `planDelete(old, instance, actor), { revision` becomes
`planDelete(old, instance), { revision`. The kills are "row 256 T1-T3" (T1) and creative.test T6.
### Tests not run
None of these have run. They wait for Sage's CLEAR, after the rebase onto 236.
+6
View File
@@ -396,6 +396,12 @@ at every gate. Started 2026-09-12 during the control board MVP.
(instances.set launch: false) but neither fire nor delete removes a
creative entry (sage-2166). The row covers fire and delete together;
Sage files it. (2026-10-21)
Closed for delete by row 256 (#1761, REQ-COS-3): `deleteRefusal` takes
the actor, so the human deletes a creative instance and any other actor
is refused `organization-creative-human-only`, and the listing's
endProtected is the same check for its reader. Fire still refuses every
END_PROTECTED instance, by the row 256 brief's ruling, not by a code
change. (2026-10-22)
- **A fire stuck between step A and step B has no human escape.** (Webby
N4 on row 194 b5, webby-0094; Sage's ruling sage-2241, #1692.) The
broker's chief fire records `fire.stopped` at step A, and `#firing`
+3 -2
View File
@@ -276,11 +276,12 @@ export async function startBroker({
organization:settings?(id,verb,args,actor,context)=>{
if(verb==='limits')return withTokens(id,settings.read(id));
if(verb==='plugins')return plugins.read(id);
if(verb==='instances')return instances.read(id);
// Row 256 (#1761): the actor is the reader, so endProtected is the delete check for that reader.
if(verb==='instances')return instances.read(id,actor);
// Row 65 (mos-0840): the Console's routing table.
if(verb==='routes')return settings.routes(id);
// Row 194 (#1692, V5): a delete's checks at the verb, with no write and no reload.
if(verb==='instances.check')return instances.checkDelete(id,args.instance);
if(verb==='instances.check')return instances.checkDelete(id,args.instance,actor);
// Row 194 b6 (webby-0094 B1): a fire's checks against the file, before step A.
if(verb==='instance.fireCheck')return instances.checkFire(id,args.name);
let result;
+28 -1
View File
@@ -123,7 +123,7 @@ async function runtime(t) {
const history = join(root, 'businesses/.history/demo/changes.jsonl');
const log = () => (existsSync(history) ? readFileSync(history, 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse) : []);
const bytes = () => readFileSync(file, 'utf8');
return { human, tester, chief, call, log, bytes };
return { bus, human, tester, chief, call, log, bytes };
}
test('M10 through the broker: the chief and the tester are refused on bean, the human\'s limits.set of its models key works', async (t) => {
@@ -151,3 +151,30 @@ test('M10 through the broker: the chief and the tester are refused on bean, the
assert.equal(JSON.parse(bytes()).launch.models['claude-sonnet-5-5'].model, 'claude-sonnet-5-5-next');
assert.deepEqual(log().filter((e) => e.state === 'committed').map((e) => e.actor), ['jason']);
});
// Row 256 (#1761, REQ-COS-3, T6): the human deletes bean through the verb and the launcher's pass, on
// the real writer, so the actor reaches the delete check at both. The chief is refused human-required
// and the tester tester-refused at the verb, so neither reaches the writer. Each reader's instances
// view carries its own endProtected.
test('row 256 T6 the human deletes a creative instance through the broker; the chief and the tester are refused at the verb', async (t) => {
const { bus, human, tester, chief, call, log, bytes } = await runtime(t);
assert.equal(call(human, 'instances').instances.bean.endProtected, false);
assert.equal(call(tester, 'instances').instances.bean.endProtected, true);
const { revision } = call(human, 'instances');
const before = bytes();
assert.throws(() => call(chief, 'instances.set', { revision, instance: 'bean', op: 'delete' }), { code: 'human-required' });
assert.throws(() => call(tester, 'instances.set', { revision, instance: 'bean', op: 'delete' }), { code: 'tester-refused' });
assert.equal(bytes(), before);
// The verb's check runs instances.check with the human as actor, and records the request.
const { request } = call(human, 'instances.set', { revision, instance: 'bean', op: 'delete' });
assert.equal(bytes(), before);
assert.equal(bus.lifecycleTake({ business: 'demo', request }).go, true);
// The launcher's pass commits the delete with the request's by, the human.
const { transaction } = bus.lifecycleDelete({ business: 'demo', request });
const file = JSON.parse(bytes());
assert.equal(Object.hasOwn(file.roles, 'bean'), false);
assert.deepEqual(file.retired, ['bean']);
const committed = log().filter((e) => e.state === 'committed');
assert.deepEqual(committed.map((e) => e.actor), ['jason']);
assert.equal(committed[0].transaction, transaction);
});
+28 -16
View File
@@ -25,15 +25,25 @@ const revisionOk = value => typeof value === "string" && /^[a-f0-9]{64}$/.test(v
// Every limits.* key merges by intersection, so it can only narrow.
const HIRE_VAR = key => key === "model" || key.startsWith("limits.");
// Row 194 (#1692, V5): why an instance can't be deleted, checked in the design's order, or null.
// The last-listed check runs when the proposed file is validated (deletedDocument). A creative
// definition is END_PROTECTED too (row 213), so no one deletes one yet (sage-2164, DEFERRED). The
// listing's endProtected is this same check, so the Agents page offers no Delete it would refuse.
const endProtected = definition => END_PROTECTED.includes(definition);
export function deleteRefusal(document, rolesDir, instance) {
// Row 256 (#1761, REQ-COS-3): the END_PROTECTED part of a delete, for one actor. A creative
// instance is the human's to delete, and any other actor is refused, as row 213 refuses it a
// change. The chief of staff is refused for every actor, the human included. The listing's
// endProtected is this same check for its reader, so the Agents page offers no Delete it would refuse.
function protectedRefusal(document, instance, actor) {
const { definition } = document.roles[instance];
if (!END_PROTECTED.includes(definition)) return null;
if (!CREATIVE_DEFINITIONS.includes(definition)) return { code: "organization-definition-refused" };
if (actor === document.human) return null;
return { code: "organization-creative-human-only", message: `${instance} is a ${definition} instance, which only the human deletes` };
}
// Row 194 (#1692, V5): why an instance can't be deleted by actor, checked in the design's order, or
// null. The last-listed check runs when the proposed file is validated (deletedDocument).
export function deleteRefusal(document, rolesDir, instance, actor) {
if (!Object.hasOwn(document.roles, instance)) return { code: "organization-unknown-instance" };
const entry = document.roles[instance];
if (endProtected(entry.definition)) return { code: "organization-definition-refused" };
const refused = protectedRefusal(document, instance, actor);
if (refused) return refused;
const references = [];
if (document.launch?.by === instance) references.push("launch.by");
for (const kind of ["delivery", "technical"]) if (document.arbiters?.[kind] === instance) references.push(`arbiters.${kind}`);
@@ -157,7 +167,9 @@ export function instanceSettings({ dir, rolesDir, system, levels = () => null, r
return out;
}
function read(business) {
// Row 256: actor is the reader, so endProtected is the delete check for that reader. A read with
// no actor (the bus's reloads) answers as for any non-human.
function read(business, actor) {
const { document, revision } = writer.read(business);
validateBusinessDocument(document, businessFilePath(business, dir), { rolesDir });
const launch = document.launch ?? null;
@@ -168,7 +180,7 @@ export function instanceSettings({ dir, rolesDir, system, levels = () => null, r
launcher: launch ? launch.by === name : false,
protected: closed(entry.definition),
kept: (launch?.keep ?? []).includes(name),
endProtected: endProtected(entry.definition),
endProtected: protectedRefusal(document, name, actor) !== null,
}]));
// Row 194 (D1): the human's forbid lists and the retired names. The bus adds running runs.
return {
@@ -204,7 +216,7 @@ export function instanceSettings({ dir, rolesDir, system, levels = () => null, r
if (Object.keys(args).some(key => !["revision", "instance", "op"].includes(key))) throw new OrganizationError("invalid-request");
const services = Object.keys(old.roles[instance]?.credentials ?? {});
const beforeWrite = revoke ? () => revoke(business, instance, "delete", services) : null;
const result = commit(business, old, planDelete(old, instance), { revision: args.revision, actor, beforeWrite });
const result = commit(business, old, planDelete(old, instance, actor), { revision: args.revision, actor, beforeWrite });
// sage-1610 (b): the bot user and any token it holds live in Vikunja, which this write
// can't reach (row 199 A5).
const bot = old.roles[instance]?.tracker?.bot;
@@ -255,15 +267,15 @@ export function instanceSettings({ dir, rolesDir, system, levels = () => null, r
// Row 194 (V5): the delete's checks against the file as it stands, and its document. The broker
// runs this at the verb with no write, and the launcher's pass runs set op delete after the take.
function planDelete(document, instance) {
const refusal = deleteRefusal(document, rolesDir, instance);
function planDelete(document, instance, actor) {
const refusal = deleteRefusal(document, rolesDir, instance, actor);
if (refusal) throw new OrganizationError(refusal.code, refusal.message);
return deletedDocument(document, instance);
}
function checkDelete(business, instance) {
function checkDelete(business, instance, actor) {
const { document, revision } = writer.read(business);
const next = planDelete(document, instance);
const next = planDelete(document, instance, actor);
validateBusinessDocument(next, businessFilePath(business, dir), { rolesDir });
return { revision };
}
@@ -335,7 +347,7 @@ export function instanceSettings({ dir, rolesDir, system, levels = () => null, r
// its tokens revoked, with no write. A revoke is safe to repeat.
if (!old.launch.instances.includes(name)) {
const revoked = revoke ? revoke(business, name, "fire", services) : null;
return { ...read(business), transaction: null, delisted: false, ...(revoked ? { credentials: revoked } : {}) };
return { ...read(business, actor), transaction: null, delisted: false, ...(revoked ? { credentials: revoked } : {}) };
}
const next = structuredClone(old);
next.launch.instances = old.launch.instances.filter(entry => entry !== name);
@@ -402,7 +414,7 @@ export function instanceSettings({ dir, rolesDir, system, levels = () => null, r
if (writer.read(business).revision !== revision) throw new OrganizationError("revision-conflict", "organization revision changed; read again");
if (beforeWrite) beforeWrite();
const receipt = writer.write(business, next, { revision, actor, context });
return { ...read(business), transaction: receipt.transaction, restart: false };
return { ...read(business, actor), transaction: receipt.transaction, restart: false };
}
return { writer, read, set, checkDelete, checkFire, veto, hire, fire };
@@ -285,7 +285,8 @@ test("row 213 a creative instance is the human's: add, vars and definition chang
e => e.code === "organization-creative-human-only", `${actor} redefine`);
}
let done = f.settings.set("acme", { revision: view.revision, ...add }, "jason");
assert.deepEqual(done.instances.bean, { definition: "companion", vars: persona, launched: false, launcher: false, protected: false, kept: false, endProtected: true });
// Row 256: the listing a write returns is read for the writer, and the human may delete bean.
assert.deepEqual(done.instances.bean, { definition: "companion", vars: persona, launched: false, launcher: false, protected: false, kept: false, endProtected: false });
for (const actor of ["chief-of-staff.chief", "tester"]) {
for (const change of [{ vars: {} }, { launch: true }, { definition: "security-reviewer" }]) {
assert.throws(() => f.settings.set("acme", { revision: done.revision, instance: "bean", op: "change", ...change }, actor),
@@ -315,3 +316,65 @@ test("row 213 a creative instance is the human's: add, vars and definition chang
assert.equal(st.mode & 0o777, st.isDirectory() ? 0o700 : 0o600, name);
}
});
// Row 256 (#1761, REQ-COS-3): the human deletes a creative instance and any other actor is refused
// with the creative code. The assistant, the END_PROTECTED definition that isn't creative, refuses
// every actor, the human included. The listing's endProtected is that same check for its reader.
test("row 256 T1-T3, T5 the human deletes a creative instance, no other actor does, and the assistant stays", () => {
const f = fixture(doc => {
doc.roles.bean = { definition: "companion" };
doc.roles.muse = { definition: "ideas" };
doc.roles.assistant = { definition: "assistant", holder: "mos" };
});
const del = (instance, actor) => () => f.settings.set("acme", { revision: f.settings.read("acme").revision, instance, op: "delete" }, actor);
const refused = (act, code, label) => {
const bytes = readFileSync(f.file, "utf8");
assert.throws(act, e => e.code === code, label);
assert.equal(readFileSync(f.file, "utf8"), bytes, label);
};
// T5: the reader's view. A read with no actor answers as for any non-human.
for (const [actor, bean] of [["jason", false], ["tester", true], ["chief-of-staff.chief", true], [undefined, true]]) {
const view = f.settings.read("acme", actor);
assert.equal(view.instances.bean.endProtected, bean, `bean ${actor}`);
assert.equal(view.instances.assistant.endProtected, true, `assistant ${actor}`);
assert.equal(view.instances.coder.endProtected, false, `coder ${actor}`);
}
assert.equal(f.settings.read("acme").instances.bean.endProtected, true);
// T2: every actor but the human gets the creative code, and the message says why.
for (const actor of ["chief-of-staff.chief", "tester", "mos", undefined]) {
refused(del("bean", actor), "organization-creative-human-only", String(actor));
assert.throws(() => f.settings.checkDelete("acme", "bean", actor), e => e.code === "organization-creative-human-only", `check ${actor}`);
}
assert.throws(del("bean", "tester"), /bean is a companion instance, which only the human deletes/);
// T3: the assistant refuses every actor, the human included.
for (const actor of ["jason", "chief-of-staff.chief", "tester"]) {
refused(del("assistant", actor), "organization-definition-refused", `assistant ${actor}`);
}
// T1: the human's delete.
assert.deepEqual(f.settings.checkDelete("acme", "bean", "jason"), { revision: f.settings.read("acme").revision });
const done = del("bean", "jason")();
assert.equal(Object.hasOwn(done.instances, "bean"), false);
assert.deepEqual(done.retired, ["bean"]);
assert.equal(done.instances.muse.endProtected, false);
const file = JSON.parse(readFileSync(f.file, "utf8"));
assert.equal(Object.hasOwn(file.roles, "bean"), false);
assert.deepEqual(file.retired, ["bean"]);
assert.equal(f.log().at(-1).actor, "jason");
const run = f.validate();
assert.equal(run.status, 0, run.stderr);
});
// Row 256 (R1, webby-0122): a creative delete by the human goes on to the later checks. A creative
// instance can't be kept, launch.by or an arbiter, so the check it reaches is the last-listed one.
test("row 256 T4 the human's delete of the only listed instance, a creative one, is the last-listed refusal", () => {
const f = fixture(doc => {
doc.roles.bean = { definition: "companion" };
doc.launch.instances = ["bean"];
});
const bytes = readFileSync(f.file, "utf8");
const revision = f.settings.read("acme").revision;
assert.throws(() => f.settings.set("acme", { revision, instance: "bean", op: "delete" }, "jason"), e => e.name === "BusinessError");
assert.throws(() => f.settings.checkDelete("acme", "bean", "jason"), e => e.name === "BusinessError");
assert.throws(() => f.settings.set("acme", { revision, instance: "bean", op: "delete" }, "tester"), e => e.code === "organization-creative-human-only");
assert.equal(readFileSync(f.file, "utf8"), bytes);
});
@@ -229,3 +229,18 @@ test("D2.2 fire delists and keeps the entry, a repeat is done with no write, and
const one = fixture(doc => { doc.launch.instances = ["sec-1"]; });
refuses(one, () => fire(one, "sec-1"), "BusinessError");
});
// Row 256 (#1761): checkDelete takes the actor, and fire is unchanged: a creative instance refuses
// every actor's fire, the human's included.
test("row 256 checkDelete passes the human's creative delete and refuses another actor's; fire still refuses", () => {
const f = fixture(doc => {
doc.roles.bean = { definition: "companion" };
doc.launch.instances.push("bean");
});
const bytes = readFileSync(f.file, "utf8");
assert.deepEqual(f.settings.checkDelete("acme", "bean", "jason"), { revision: f.revision() });
for (const actor of ["chief", "tester", undefined]) refuses(f, () => f.settings.checkDelete("acme", "bean", actor), "organization-creative-human-only");
for (const actor of ["chief", "jason"]) refuses(f, () => f.settings.fire("acme", { name: "bean" }, { actor, context: CONTEXT }), "organization-definition-refused");
assert.throws(() => f.settings.checkFire("acme", "bean"), e => e.code === "organization-definition-refused");
assert.equal(readFileSync(f.file, "utf8"), bytes);
});
+2 -1
View File
@@ -42,7 +42,8 @@
if (!c.isTester) {
acts.push(confirmPair('regenerate', name, 'Regenerate', `Regenerate ${name}? Its workspace is renamed under .retired and it starts fresh.`));
acts.push(e.kept ? button('unkeep', name, 'Unkeep') : button('keep', name, 'Keep'));
// endProtected is the server's delete check, creative definitions included (sage-2164).
// endProtected is the server's delete check for this reader (row 256): false on the human's
// page for a creative instance, true for the chief of staff and on the tester's.
if (!e.endProtected) acts.push(confirmPair('delete', name, 'Delete', `Delete ${name}? Its evidence and its workspace under .retired stay.`));
}
return `<tr><th scope="row">${txt(name)}</th><td>${txt(e.definition)}</td><td>${e.launched ? 'yes' : 'no'}</td><td>${e.kept ? 'yes' : 'no'}</td><td>${running.length ? running.map(r => `<code>${txt(r)}</code>`).join(' ') : '<span class="muted">none</span>'}${alive.map(r => ` <strong class="agent-alive">still running after fire: <code>${txt(r)}</code></strong>`).join('')}</td><td class="agent-actions">${acts.join(' ')}</td></tr>`;
+28 -2
View File
@@ -40,8 +40,8 @@ test('row 194 T16 Settings Agents: unavailable read-only, launch follows its req
assert.equal(await b.evaluate('document.querySelectorAll("#agents-section [data-agent-action]").length>0'),true);
// The protected assistant has no Delete; the kept instance offers Unkeep; reviewer is no-hire.
assert.equal(await b.evaluate('!!document.querySelector(\'[data-agent-action="delete"][data-agent-name="assistant"]\')'),false);
// sage-2164: a creative instance isn't protected (the human changes it), yet the server refuses its
// delete, so endProtected hides Delete for it too.
// A creative instance isn't protected (the human changes it). This listing marks bean endProtected,
// as a non-human reader's does, so the page hides its Delete; row 256's test below has the human's.
assert.equal(await b.evaluate('!!document.querySelector(\'[data-agent-action="delete"][data-agent-name="bean"]\')'),false);
assert.equal(await b.evaluate('!!document.querySelector(\'[data-agent-action="launch"][data-agent-name="bean"]\')'),true);
assert.equal(await b.evaluate(`document.querySelector('[data-agent-action="unkeep"]').dataset.agentName`),hostile);
@@ -123,3 +123,29 @@ test('row 194 T16 Settings Agents: unavailable read-only, launch follows its req
assert.equal(await b.evaluate('posts.length'),sent+1);
}finally{await b.close();await close(web);}
});
// Row 256 (#1761, T7, T8): the human's listing carries endProtected false for a creative instance
// (the server's delete check for that reader), so the human's page offers Delete for it. The tester's
// page offers no Delete even on that same listing; the tester's own listing marks bean endProtected
// as well (bus creative.test T6).
test('row 256 T7 T8 the human page offers Delete on a creative instance, the tester page never does',{timeout:90000},async()=>{
const human={...view,instances:{...view.instances,bean:{...view.instances.bean,endProtected:false}}};
const bus={messages:async()=>({messages:[],unread:0}),usage:async()=>{throw Error('no usage');},instances:async()=>human};
const web=await startServer({port:0,board:'http://127.0.0.1:9',bus,reads:{settings:()=>({business:'acme',release:'fixture',notifier:{}})}}),b=await browser();
try{
await b.call('Page.addScriptToEvaluateOnNewDocument',{source:`const q=new URL(location.href).searchParams;const original=window.fetch;window.fetch=async(url,options={})=>{if(url==='/api/config')return new Response(JSON.stringify(q.has('tester')?{tester:true}:{send:true}));return original(url,options);};`});
const port=web.address().port;
const wait=expr=>b.evaluate(`(async()=>{for(let n=0;n<400;n++){if(${expr})return true;await new Promise(r=>setTimeout(r,30));}throw Error('wait failed: '+${JSON.stringify(expr)});})()`);
const del=name=>`document.querySelector('[data-agent-action="delete"][data-agent-name="${name}"]')`;
// T7: the human's page.
await b.navigate(`http://127.0.0.1:${port}/?human=1#/settings`);
await wait(`document.querySelector('[data-agent-action="launch"][data-agent-name="bean"]')?.disabled===false`);
assert.equal(await b.evaluate(`${del('bean')}?.disabled`),false);
assert.equal(await b.evaluate(`!!${del('assistant')}`),false);
assert.equal(await b.evaluate(`!!${del('coder')}`),true);
// T8: the tester's page, on the same listing.
await b.navigate(`http://127.0.0.1:${port}/?tester=1#/settings`);
await wait(`!!document.querySelector('#tester-banner')&&!!document.querySelector('[data-agent-action="launch"][data-agent-name="bean"]')`);
assert.equal(await b.evaluate(`document.querySelectorAll('[data-agent-action="delete"]').length`),0);
}finally{await b.close();await close(web);}
});