Files
stack/packages/cli/tests/lifecycle.test.mjs
jason.woltje f3f33a6171 fix(organization,cli,discord,seat,control-board): refusals name the field or file by label, never a host path (#1873)
redact6. A refusal names the field, the file or the instance. It never quotes a
config value, a host path, or a raw e.message or stderr that carries one.

- organization: checkCredentialRef names the field and each forbidden root by
  label; OrgError keeps .path only for a document field ($...); the registry,
  fence and lock refusals use the quiet helpers (call and code).
- cli: loadSystem never relays mosaic-config stderr: instance refusals keep
  their code and names, everything else reads as a field and its constraint
  against a labelled config. Project-key, access warning, halt gate, sessions
  file, launch socket, harness relays and the workspace rename name labels.
  Launch-log reasons read a Node system error as its call and code.
- scripts/mosaic-config: configuration not found names where the config came from.
- discord, seat, control-board: config, token, context, tool-root, journal,
  binding, registration and seen-marks refusals name labels.

Exit codes, refusal codes and check order are unchanged. Each new or changed
test fails on BASE be698e602.
2026-11-11 08:53:31 -06:00

888 lines
50 KiB
JavaScript

import {ownerConsole} from '../../bus/tests/hire-fixtures.mjs';
import {readTemplate} from '../../organization/src/definitions.mjs';
import { requestHostOps } from './request-host-ops.mjs';
// Row 194 (#1692, D1.1, T2-T5, V5): the launcher half of the lifecycle verbs. The broker records
// the human's launch, end, regenerate and delete as requests (bus lifecycle-verbs.test.mjs), and
// the launcher's lifecycle pass takes each one, acts on it and closes it. Real runners in the bwrap
// sandbox on the fake adapter.
//
// The host is in process, as in thread-launch.test.mjs: the real host proves a human CLI process
// for every human verb, which a test run under an agent can't. The tick interval is long and each
// test calls tick() itself, so a request waits until the test says.
import { test } from "node:test";
import assert from "node:assert/strict";
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 { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { launchDoc } from "../../organization/tests/helpers.mjs";
import { BusError } from "../../bus/src/broker.mjs";
import { startBroker } from "../../bus/src/runtime.mjs";
import { Store } from "../../bus/src/store.mjs";
import { hostGrantRecord, hostRecord } from "../../bus/tests/host-fixtures.mjs";
import { gitEnv } from "../../seat/src/checkout.mjs";
import { sandboxProblem } from "../../seat/src/sandbox.mjs";
import { processIdentity, registerRuntime, sweepRuntimes, unregisterRuntime, updateRuntime } from "../../processing-control/src/runtimes.mjs";
import { controlDirectory, transition } from "../../processing-control/src/state.mjs";
import { launchLogFile, readSessions, workspaceDir } from "../../seat/src/session.mjs";
import { run } from "../src/cli.mjs";
import { bootConfig, loadSystem } from "../src/config.mjs";
import { startTimeOf } from "../src/host.mjs";
import { createLauncher } from "../src/launcher.mjs";
import { fakeHarnesses, fixture, io, tmp, withoutHostCredentials } from "./helpers.mjs";
const FAKE = fileURLToPath(new URL("../../harness/tests/fixtures/fake-adapter.mjs", import.meta.url));
const skip = sandboxProblem() ?? false;
async function until(fn, ms = 15000) {
const end = Date.now() + ms;
while (Date.now() < end) {
const v = await fn();
if (v) return v;
await new Promise((r) => setTimeout(r, 50));
}
throw new Error("timed out waiting");
}
function gitIn(cwd, ...args) {
const r = spawnSync("git", ["-c", "maintenance.auto=false", "-c", "gc.auto=0", "-c", "user.name=test", "-c", "[email protected]", "-C", cwd, ...args], { env: gitEnv(), encoding: "utf8" });
assert.equal(r.status, 0, r.stderr);
}
// chief opus, coder and reviewer sonnet, at most one sonnet run. Only coder is listed, so reviewer
// is an instance the chief may not launch and the human may. sec-1 holds no credential, so the
// organization writer lets the human delete it. assistant is the thread instance.
function prepare(t, edit = (doc) => doc) {
const root = tmp(t, "mosaic-life-");
const f = fixture(root, "acme", (doc) => {
doc.roles.chief.vars = { harness: "pi", model: "claude-opus-5-5", thinking: "high" };
doc.roles.coder.vars.model = "claude-sonnet-5-5";
doc.roles.reviewer.vars = { harness: "pi", model: "claude-sonnet-5-5", thinking: "high" };
doc.roles["sec-1"] = { definition: "security-reviewer", vars: { harness: "pi", model: "claude-sonnet-5-5", thinking: "high" } };
doc.roles.assistant = { definition: "assistant", vars: { harness: "pi", model: "claude-sonnet-5-5", thinking: "high" } };
doc.launch = launchDoc("chief", ["coder"], { opus: 1, sonnet: 1 });
return edit(doc);
});
withoutHostCredentials(f.env, { piKey: true });
const project = f.doc.projects.stack.root;
gitIn(project, "init", "-q", "-b", "main");
writeFileSync(join(project, "file.txt"), "one\n");
gitIn(project, "add", "file.txt");
gitIn(project, "commit", "-q", "-m", "one");
const adapter = join(root, "adapter.sh");
writeFileSync(adapter, `exec '${process.execPath}' '${FAKE}'\n`, { mode: 0o644 });
return { root, f, adapter };
}
async function start(t, { f, adapter }, options = {}) {
const system = loadSystem({ env: f.env });
const { trackers, ...boot } = bootConfig({ system, businessId: "acme", env: f.env });
assert.equal(trackers, undefined);
const runtime = await startBroker(boot);
// The real host makes bus-host/ (0700). A --no-launch launcher's read-only registry doesn't.
mkdirSync(join(f.dataRoot, "bus-host"), { recursive: true, mode: 0o700 });
const ops = {
...requestHostOps(runtime),
identity: (m) => runtime.identity(m.cap),
authorizeLaunch: (m) => runtime.authorizeLaunch(m.cap, m.instance),
refuse: (m) => runtime.refuse(m.cap, m.code, m.subject),
endLaunch: (m) => runtime.endLaunch(m.record),
credentialStatus: (m) => runtime.credentialStatus(m.business, m.role),
// org-defscan (#1873): an org: definition's launch asks the bus's held-secret check, as process.mjs answers it.
definitionScan: (m) => runtime.definitionScan({ business: m.business, definition: m.definition, entry: m.entry }),
threadsState: (m) => runtime.threadsState(m.business),
threadIdle: (m) => runtime.threadIdle(m.record),
mayLaunch: (m) => runtime.mayLaunch(m),
landHeld: (m) => runtime.landHeld(m.record),
landHolders: (m) => runtime.landHolders(m.business),
landRelease: (m) => runtime.landRelease(m.record),
lifecycleRequests: (m) => runtime.lifecycleRequests(m.business),
lifecycleTake: (m) => runtime.lifecycleTake(m.record),
lifecycleDelete: (m) => runtime.lifecycleDelete(m.record),
lifecycleDone: (m) => runtime.lifecycleDone(m.record),
};
const refusing = (what, fn) => async (m) => {
try {
return fn(m);
} catch (e) {
const code = e instanceof BusError ? e.code : `${what}-refused`;
throw Object.assign(new Error(`${what} refused: ${code}`), { code });
}
};
const hooks = [];
let closing = null;
// A host op named here throws once, as a bus that refused it would.
const faults = new Set();
const host = {
business: "acme",
path: runtime.path,
dataRoot: f.dataRoot,
op: refusing("launch-op", (m) => {
if (!Object.hasOwn(ops, m.op)) throw new BusError("invalid-host-request");
if (faults.delete(m.op)) throw new BusError("storage-refused");
return ops[m.op](m);
}),
bindLaunch: refusing("launch bind", (r) => runtime.bindLaunch(r)),
beforeClose: (fn) => void hooks.push(fn),
};
const logs = [];
const launcher = createLauncher({
host,
system,
env: f.env,
makeLoader: await fakeHarnesses(t, { pi: adapter, "claude-code": adapter }),
sessionDefaults: { pollInterval: 100 },
harnessVersion: "0.85.1",
readOnly: [FAKE],
threadTickMs: 3600000,
stopTimeoutMs: 1000,
log: (l) => logs.push(l),
...options,
});
const close = () =>
(closing ??= (async () => {
for (const fn of hooks) await fn(0);
await runtime.close();
})());
t.after(close);
await launcher.listen();
const human = runtime.broker.bindHuman({ business: "acme", human: f.doc.human, via: "cli", outsideAgent: true });
const call = (verb, args = {}) => runtime.broker.request(human, { verb, args });
const state = (request) => call("instance.request", { request });
// One pass, then the request's state.
const carry = async (request) => {
await launcher.tick();
return state(request);
};
const running = (instance) => launcher.registry.sessions.filter((s) => s.instance === instance);
const claimed = (run) => until(() => {
const file = join(f.dataRoot, "launches", "acme", run, "runner.log");
return existsSync(file) && readFileSync(file, "utf8").includes("claimed by run");
});
// The CLI through the in-process human. Each poll of instance.request runs one pass first, so a
// --wait sees the request closed as the launcher's ticker would close it.
const cli = async (args) => {
const x = io(f.env);
const deps = {
system: { dataRoot: f.dataRoot },
transport: () => async (verb, a = {}) => {
if (verb === "instance.request") await launcher.tick();
try {
return structuredClone(call(verb, a));
} catch (e) {
if (!(e instanceof BusError)) throw e;
throw Object.assign(new Error(e.code), { code: e.code });
}
},
};
const code = await run(["business", ...args], x, deps);
return { code, out: x.out.text, err: x.err.text };
};
return { runtime, launcher, call, state, carry, running, claimed, logs, close, faults, cli };
}
const launchLog = (f) =>
existsSync(launchLogFile(f.dataRoot, "acme"))
? readFileSync(launchLogFile(f.dataRoot, "acme"), "utf8").trim().split("\n").map((l) => JSON.parse(l))
: [];
// Stops a runner from outside its namespace, so it can't act on SIGTERM, and kills it at the end.
function stop(t, s) {
process.kill(s.runnerPid, "SIGSTOP");
t.after(() => {
if (startTimeOf(s.runnerPid) === s.runnerStartTime) process.kill(s.runnerPid, "SIGKILL");
});
}
// S6: coder and reviewer listed, so a fire of coder isn't of the last listed name.
const listed = (doc) => {
doc.launch = launchDoc("chief", ["coder", "reviewer"], { opus: 1, sonnet: 1 });
return doc;
};
// S6: a stand-in runner holding a stand-in model variable, with a registry entry and a row 191
// runtime record as start() makes them. One with `ignoreTerm` stays through a SIGTERM. Each SIGTERM
// it gets is a line it prints, so terms() counts what reached it from any sender. One without
// `managed` has no runtime record, so row 191's halt sweep doesn't see it.
async function standIn(t, p, s, { instance, run, ignoreTerm = false, managed = true }) {
const code = `process.on("SIGTERM", () => { process.stdout.write("term\\n"); ${ignoreTerm ? "" : "process.exit(143);"} }); process.stdout.write("ready\\n"); setInterval(() => {}, 1000);`;
const child = spawn(process.execPath, ["-e", code], { stdio: ["ignore", "pipe", "ignore"], env: { PATH: process.env.PATH, MOSAIC_STAND_IN_MODEL_TOKEN: "stand-in" } });
t.after(() => {
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
});
let out = "";
child.stdout.on("data", (d) => (out += d));
const exited = new Promise((resolve) => child.on("exit", (code, signal) => resolve(signal ?? code)));
await until(() => out.includes("ready"));
const startTime = startTimeOf(child.pid);
s.launcher.registry.add({ business: "acme", instance, run, pid: child.pid, startTime, runnerPid: child.pid, runnerStartTime: startTime, startedAt: new Date().toISOString() });
t.after(() => s.launcher.registry.remove("acme", run));
if (managed) {
const record = registerRuntime(p.f.dataRoot, { kind: "session", business: "acme", run });
updateRuntime(p.f.dataRoot, record.id, { ...processIdentity(child.pid), group: false, phase: "running" });
t.after(() => {
try { unregisterRuntime(p.f.dataRoot, record.id); } catch {}
});
}
return {
run,
pid: child.pid,
exited,
gone: () => child.exitCode !== null || child.signalCode !== null,
terms: () => out.split("\n").filter((l) => l === "term").length,
};
}
// A bus session of the role with its claim, as a launched run's bind and claim leave it.
function bind(s, role, run) {
const cap = s.runtime.broker.bindLaunch({ business: "acme", role, run, harness: "pi" });
s.runtime.broker.request(cap, { verb: "role.claim" });
return cap;
}
const fire = (s, chief, name) => s.runtime.broker.request(chief, {verb:"hire.raise",args:{op:"fire",level:"org",name,reason:"the review is done"}}).then(d => s.runtime.broker.request(ownerConsole(s.runtime.broker,"acme"),{verb:"hire.resolve.console",args:{id:d.id,choice:"yes",digest:d.hire.digest}}));
const allowed = (p, action) => {
const db = new DatabaseSync(join(p.f.dataRoot, "bus", "bus.sqlite"), { readOnly: true });
try {
return db
.prepare("SELECT subject, body FROM events WHERE kind='action.allowed' ORDER BY seq")
.all()
.map((e) => ({ subject: e.subject, body: JSON.parse(e.body) }))
.filter((e) => e.body.action === action);
} finally {
db.close();
}
};
test("row 194 T2: the human's launch runs an unlisted instance and launch.by through the pm branch, and closes with the run", { skip, timeout: 60000 }, async (t) => {
const p = prepare(t);
const s = await start(t, p);
const { request } = s.call("instance.launch", { instance: "reviewer" });
assert.equal(s.state(request).state, "open");
const done = await s.carry(request);
assert.equal(done.state, "closed");
assert.equal(done.done.refusal, null);
assert.match(done.done.run, /^r[0-9a-f]{12}$/);
const [r] = s.running("reviewer");
assert.equal(r.run, done.done.run);
const session = readSessions(p.f.dataRoot).find((x) => x.run === r.run);
assert.equal(session.launchedBy, "jason");
await s.claimed(r.run);
// launch.by is not on launch.instances. The human's request takes the pm branch, as --pm does.
const chief = s.call("instance.launch", { instance: "chief" }).request;
const c = await s.carry(chief);
assert.equal(c.done.refusal, null);
assert.equal(s.running("chief")[0].run, c.done.run);
// A request is taken once: the next pass starts nothing more.
await s.launcher.tick();
assert.equal(s.running("reviewer").length, 1);
assert.equal(launchLog(p.f).filter((e) => e.event === "launch").length, 2);
});
test("row 194 T2: a request past its 60 s never launches, and a refused check closes the request with its code", { skip, timeout: 60000 }, async (t) => {
const p = prepare(t);
const s = await start(t, p);
const old = s.call("instance.launch", { instance: "coder" }).request;
const now = Date.now();
t.mock.method(Date, "now", () => now + 61000);
const expired = await s.carry(old);
assert.deepEqual([expired.state, expired.done.refusal, expired.done.run], ["closed", "request-expired", null]);
t.mock.restoreAll();
assert.equal(s.running("coder").length, 0);
// A check refusal: the organization file now names a harness that isn't switched on.
const file = join(p.root, "config", "businesses", "acme.json");
const doc = JSON.parse(readFileSync(file, "utf8"));
doc.roles.coder.vars.harness = "claude-code";
doc.plugins.harness = ["pi"];
writeFileSync(file, JSON.stringify(doc, null, 2));
const off = s.call("instance.launch", { instance: "coder" }).request;
const refused = await s.carry(off);
assert.deepEqual([refused.state, refused.done.refusal, refused.done.run], ["closed", "harness-off", null]);
assert.equal(s.running("coder").length, 0);
const entry = launchLog(p.f).find((e) => e.event === "refused" && e.request === off);
assert.deepEqual([entry.instance, entry.code, entry.by], ["coder", "harness-off", "jason"]);
});
test("row 194 T3: a --no-launch host closes each request launches-disabled and starts nothing", { skip, timeout: 60000 }, async (t) => {
const p = prepare(t);
const s = await start(t, p, { launches: false });
const launch = s.call("instance.launch", { instance: "coder" }).request;
const regen = s.call("instance.regenerate", { instance: "reviewer" }).request;
await s.launcher.tick();
for (const request of [launch, regen]) assert.deepEqual([s.state(request).state, s.state(request).done.refusal], ["closed", "launches-disabled"]);
assert.equal(s.launcher.registry.sessions.length, 0);
assert.equal(existsSync(join(p.f.dataRoot, "launches", "acme")), false);
assert.deepEqual(launchLog(p.f).filter((e) => e.event === "launch"), []);
});
test("row 194 T3: a limit refusal closes the request with its code and the limit in the launch log", { skip, timeout: 60000 }, async (t) => {
const p = prepare(t);
const s = await start(t, p);
const first = await s.carry(s.call("instance.launch", { instance: "coder" }).request);
assert.equal(first.done.refusal, null);
// One sonnet run at a time: reviewer runs sonnet too.
const full = s.call("instance.launch", { instance: "reviewer" }).request;
const r = await s.carry(full);
assert.deepEqual([r.state, r.done.refusal, r.done.run], ["closed", "capacity-full", null]);
assert.equal(s.running("reviewer").length, 0);
const entry = launchLog(p.f).find((e) => e.event === "refused" && e.request === full);
assert.equal(entry.code, "capacity-full");
assert.ok(entry.limit, JSON.stringify(entry));
// The instance already running refuses a second launch of itself.
const again = await s.carry(s.call("instance.launch", { instance: "coder" }).request);
assert.equal(again.done.refusal, "instance-running");
});
test("row 194 T4: terminate revokes and cancels at the verb, then the pass signals the run, which ends once", { skip, timeout: 60000 }, async (t) => {
const p = prepare(t);
const s = await start(t, p);
const launched = await s.carry(s.call("instance.launch", { instance: "coder" }).request);
const run = launched.done.run;
await s.claimed(run);
const open = s.call("instance.launch", { instance: "coder" }).request;
const out = s.call("instance.end", { instance: "coder", reason: "stuck" });
assert.deepEqual([out.runs, out.cancelled], [[run], [open]]);
assert.equal(s.state(open).state, "closed");
const end = await s.carry(out.request);
assert.deepEqual([end.state, end.done], ["closed", { run: null, transaction: null, refusal: null }]);
await until(() => s.running("coder").length === 0);
const log = launchLog(p.f);
assert.equal(log.filter((e) => e.event === "lifecycle-end" && e.run === run).length, 1);
await until(() => launchLog(p.f).some((e) => e.event === "end" && e.run === run));
assert.equal(launchLog(p.f).filter((e) => e.event === "end" && e.run === run).length, 1);
// The cancelled request never ran.
await s.launcher.tick();
assert.equal(s.running("coder").length, 0);
});
test("row 194 T5: regenerate ends the run in the registry and starts a new run on the same workspace", { skip, timeout: 60000 }, async (t) => {
const p = prepare(t);
const s = await start(t, p);
const first = (await s.carry(s.call("instance.launch", { instance: "coder" }).request)).done.run;
await s.claimed(first);
const marker = join(workspaceDir(p.f.dataRoot, "acme", "coder"), "kept.txt");
writeFileSync(marker, "kept\n");
const regen = await s.carry(s.call("instance.regenerate", { instance: "coder" }).request);
assert.equal(regen.done.refusal, null);
const second = regen.done.run;
assert.notEqual(second, first);
assert.deepEqual(s.running("coder").map((x) => x.run), [second]);
assert.ok(existsSync(join(p.f.dataRoot, "launches", "acme", second)));
assert.equal(readFileSync(marker, "utf8"), "kept\n");
const log = launchLog(p.f);
assert.equal(log.filter((e) => e.event === "lifecycle-end" && e.run === first).length, 1);
assert.equal(log.filter((e) => e.event === "end" && e.run === first).length, 1);
// A thread instance is never regenerated: the verb refuses before any request.
assert.throws(() => s.call("instance.regenerate", { instance: "assistant" }), (e) => e.code === "regenerate-thread");
});
test("row 194 T5: a run that outlasts the request's mark closes the regenerate regenerate-timeout and starts nothing", { skip, timeout: 60000 }, async (t) => {
const p = prepare(t);
const s = await start(t, p, { lifecycleRequestMs: 1500 });
const first = (await s.carry(s.call("instance.launch", { instance: "coder" }).request)).done.run;
await s.claimed(first);
stop(t, s.running("coder")[0]);
const regen = await s.carry(s.call("instance.regenerate", { instance: "coder" }).request);
assert.deepEqual([regen.state, regen.done.refusal, regen.done.run], ["closed", "regenerate-timeout", null]);
assert.deepEqual(s.running("coder").map((x) => x.run), [first]);
assert.equal(launchLog(p.f).filter((e) => e.event === "launch").length, 1);
});
test("row 194 V5: delete retires the name, renames the workspace to .retired/I-<transaction> at 0700, and a retry renames after workspace-rename-failed", { skip, timeout: 60000 }, async (t) => {
const p = prepare(t);
const s = await start(t, p);
const ws = workspaceDir(p.f.dataRoot, "acme", "sec-1");
mkdirSync(ws, { recursive: true, mode: 0o700 });
writeFileSync(join(ws, "notes.txt"), "mine\n");
// A file where .retired goes makes the rename fail after the commit.
const retired = join(workspaceDir(p.f.dataRoot, "acme", ".retired"));
writeFileSync(retired, "in the way\n");
const revision = () => s.call("instances").revision;
const del = s.call("instances.set", { revision: revision(), instance: "sec-1", op: "delete" }).request;
const failed = await s.carry(del);
assert.equal(failed.done.refusal, "workspace-rename-failed");
// #1873: the refused event's reason names the workspace by label and the error by code, never a path.
const refused = launchLog(p.f).filter((e) => e.event === "refused" && e.code === "workspace-rename-failed");
assert.deepEqual(refused.map((e) => e.why), ["cannot mkdir the instance workspace (EEXIST); left in place"]);
assert.ok(!JSON.stringify(refused).includes(p.f.dataRoot));
const transaction = failed.done.transaction;
assert.ok(transaction);
const file = JSON.parse(readFileSync(join(p.root, "config", "businesses", "acme.json"), "utf8"));
assert.equal(Object.hasOwn(file.roles, "sec-1"), false, "the commit stands");
assert.ok(file.retired.includes("sec-1"));
assert.equal(readFileSync(join(ws, "notes.txt"), "utf8"), "mine\n", "the workspace stays where it was");
// A retired name launches no more, so nothing opens the old workspace before the retry.
assert.throws(() => s.call("instance.launch", { instance: "sec-1" }), (e) => e.code === "instance-retired");
rmSync(retired);
const retry = s.call("instances.set", { revision: revision(), instance: "sec-1", op: "delete" }).request;
assert.equal(s.state(retry).retry, true);
const ok = await s.carry(retry);
assert.deepEqual(ok.done, { run: null, transaction, refusal: null });
const to = join(retired, `sec-1-${transaction}`);
assert.equal(existsSync(ws), false);
assert.equal(readFileSync(join(to, "notes.txt"), "utf8"), "mine\n");
assert.equal(statSync(retired).mode & 0o777, 0o700);
assert.equal(statSync(to).mode & 0o777, 0o700);
const entry = launchLog(p.f).find((e) => e.event === "workspace-retired");
assert.deepEqual([entry.instance, entry.request, entry.transaction, entry.path], ["sec-1", retry, transaction, to]);
});
test("row 194 V5: a delete with no workspace commits and closes with the transaction", { skip, timeout: 60000 }, async (t) => {
const p = prepare(t);
const s = await start(t, p);
const del = s.call("instances.set", { revision: s.call("instances").revision, instance: "sec-1", op: "delete" }).request;
const r = await s.carry(del);
assert.equal(r.done.refusal, null);
assert.ok(r.done.transaction);
assert.equal(existsSync(join(workspaceDir(p.f.dataRoot, "acme", ".retired"))), false);
});
// Row 281 (#1786, d6 section 2, T10 B2): a delete of an instance with a grant answers at once. The pass
// closes the first request grants-revoking while step G runs as a job, and the host channel stays up:
// the next op answers. The grant is a seeded record on a TEST-NET host, so the job's remove can only
// 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 });
hostGrantRecord(seeded, { business: "acme", agent: "sec-1", host: "web1", commands: [["uptime"]] });
seeded.close();
const s = await start(t, p);
const del = s.call("instances.set", { revision: s.call("instances").revision, instance: "sec-1", op: "delete" }).request;
const r = await s.carry(del);
assert.equal(r.done.refusal, "grants-revoking");
assert.equal(r.done.transaction ?? null, null);
// The channel answers the next op and the next pass.
assert.ok(s.call("instances").revision);
await s.launcher.tick();
assert.ok(Object.hasOwn(JSON.parse(readFileSync(join(p.root, "config", "businesses", "acme.json"), "utf8")).roles, "sec-1"));
await s.runtime.broker.grantsIdle();
});
// Every regular file under root, by path, as a sha256. Sockets and other special files are left out.
function snapshot(root) {
const out = {};
const walk = (dir) => {
for (const name of readdirSync(dir).sort()) {
const path = join(dir, name);
const st = lstatSync(path);
if (st.isDirectory()) walk(path);
else if (st.isFile()) out[path.slice(root.length)] = createHash("sha256").update(readFileSync(path)).digest("hex");
}
};
walk(root);
return out;
}
test("row 194 D1.1 item 5: the CLI's launch, regenerate, terminate and delete end to end through the pass, and a closed refusal exits 3 with the request id", { skip, timeout: 90000 }, async (t) => {
const p = prepare(t);
const s = await start(t, p);
const launched = await s.cli(["instance", "launch", "acme", "reviewer"]);
assert.equal(launched.code, 0, launched.err);
const [r] = s.running("reviewer");
assert.match(launched.out, new RegExp(`^business acme: launched reviewer, run ${r.run} \\(request [^)]+\\)\n$`));
await s.claimed(r.run);
const regen = await s.cli(["instance", "regenerate", "acme", "reviewer"]);
assert.equal(regen.code, 0, regen.err);
const [r2] = s.running("reviewer");
assert.notEqual(r2.run, r.run);
assert.match(regen.out, new RegExp(`^business acme: regenerated reviewer, run ${r2.run} \\(request [^)]+\\)\n$`));
await s.claimed(r2.run);
// One sonnet run at a time: the launcher closes coder's request capacity-full.
const full = await s.cli(["instance", "launch", "acme", "coder"]);
assert.equal(full.code, 3);
const refused = launchLog(p.f).find((e) => e.event === "refused" && e.instance === "coder");
assert.match(full.err, new RegExp(`^mosaic: capacity-full: .* \\(request ${refused.request}\\)$`, "m"));
assert.equal(s.running("coder").length, 0);
const ended = await s.cli(["instance", "terminate", "acme", "reviewer", "--reason", "done for today"]);
assert.equal(ended.code, 0, ended.err);
assert.match(ended.out, new RegExp(`^runs ${r2.run}: claims revoked, the launcher signals each$`, "m"));
await s.launcher.tick();
await until(() => s.running("reviewer").length === 0);
const revision = JSON.parse((await s.cli(["instances", "acme", "--json"])).out).revision;
const del = await s.cli(["instance", "delete", "acme", "sec-1", "--revision", revision]);
assert.equal(del.code, 0, del.err);
assert.match(del.out, /^business acme: deleted sec-1, now retired\ntransaction [^\n]+\n$/);
assert.match((await s.cli(["instances", "acme"])).out, /^retired: sec-1$/m);
});
// Row 194 (brief item 8, sage-1610 b): an instance whose every credential is a store reference
// deletes with its tracker bot. The launcher carries the bot to the close, and the CLI names it for
// removal in Vikunja, since the bus makes and removes no Vikunja account.
test("row 194 T20: a store-only delete with a tracker bot closes naming the bot, and the CLI says to remove it in Vikunja", { skip, timeout: 60000 }, async (t) => {
const p = prepare(t, (doc) => {
doc.roles["rev-2"] = { definition: "reviewer", vars: { harness: "pi", model: "claude-sonnet-5-5", thinking: "high" }, tracker: { bot: "bot-acme-rev-2", botId: 40 },
credentials: { gitea: { store: true, rotateBy: "2099-01-01" }, vikunja: { store: true, expires: "2099-01-01" } } };
return doc;
});
// 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$/);
// The line reads done.bot, which only the launcher's close writes.
assert.equal(Object.hasOwn(JSON.parse((await s.cli(["instances", "acme", "--json"])).out).instances, "rev-2"), false);
});
test("row 194 T2a: a launch.revoke between the request and the pass closes it launch-revoked with no process; a done that fails once leaves the request taken and the next pass starts nothing", { skip, timeout: 60000 }, async (t) => {
const p = prepare(t);
const s = await start(t, p);
const request = s.call("instance.launch", { instance: "coder" }).request;
s.call("launch.revoke");
const revoked = await s.carry(request);
assert.deepEqual([revoked.state, revoked.done.refusal, revoked.done.run], ["closed", "launch-revoked", null]);
assert.equal(s.launcher.registry.sessions.length, 0);
assert.equal(existsSync(join(p.f.dataRoot, "launches", "acme")), false);
s.call("launch.restore");
const held = s.call("instance.launch", { instance: "coder" }).request;
s.faults.add("lifecycleDone");
const taken = await s.carry(held);
assert.deepEqual([taken.state, taken.done], ["taken", null]);
const [r] = s.running("coder");
assert.ok(s.logs.some((l) => l === `request ${held} (coder): lifecycleDone refused: storage-refused; run ${r.run} started`), s.logs.join("\n"));
await s.launcher.tick();
assert.equal(s.state(held).state, "taken");
assert.equal(s.running("coder").length, 1);
assert.equal(launchLog(p.f).filter((e) => e.event === "launch").length, 1);
});
test("row 194 T4: terminate signals a spawned run in the registry that the bus never bound, and not a run whose start time differs", { timeout: 30000 }, async (t) => {
const p = prepare(t);
const s = await start(t, p);
const spawned = (instance) => {
const child = spawn("sleep", ["60"], { stdio: "ignore" });
t.after(() => child.kill("SIGKILL"));
const exited = new Promise((resolve) => child.on("exit", (code, signal) => resolve(signal)));
return { child, exited, instance };
};
const a = spawned("coder");
const b = spawned("reviewer");
await until(() => startTimeOf(a.child.pid) !== null && startTimeOf(b.child.pid) !== null);
// Neither holds a claim. An open request each lets the end through, and the end cancels it.
for (const [x, start] of [[a, startTimeOf(a.child.pid)], [b, startTimeOf(b.child.pid) + 1]])
s.launcher.registry.add({ business: "acme", instance: x.instance, run: `run-${x.instance}`, pid: x.child.pid, startTime: start, runnerPid: x.child.pid, runnerStartTime: start, startedAt: new Date().toISOString() });
t.after(() => ["coder", "reviewer"].forEach((i) => s.launcher.registry.remove("acme", `run-${i}`)));
for (const x of [a, b]) {
s.call("instance.launch", { instance: x.instance });
const out = s.call("instance.end", { instance: x.instance });
assert.deepEqual([out.runs, out.cancelled.length], [[], 1]);
await s.carry(out.request);
}
assert.equal(await a.exited, "SIGTERM");
await new Promise((r) => setTimeout(r, 300));
assert.equal(b.child.exitCode, null);
assert.equal(b.child.signalCode, null);
const ends = launchLog(p.f).filter((e) => e.event === "lifecycle-end").map((e) => e.run);
assert.deepEqual(ends, ["run-coder", "run-reviewer"]);
});
// S6 (D2.2): fire's end goes through row 191's stopRun, which escalates to SIGKILL after the grace.
// The stuck stand-in's SIGKILL is stubbed to report delivery and do nothing, as a failed kill would.
test("row 194 T9a: fire signals its bound run and a spawned but unbound registry run; a SIGTERM-ignoring runner gets SIGKILL and lifecycle.end.killed, and one whose kill fails gets lifecycle.end.alive", { skip, timeout: 60000 }, async (t) => {
const p = prepare(t, listed);
const signals = [];
const signal = (r, sig) => {
signals.push([r.run, sig]);
if (r.run !== "coder-stuck" || sig === "SIGTERM") process.kill(r.pid, sig);
return true;
};
const s = await start(t, p, { stopOptions: { graceMs: 400, killWaitMs: 400, signal } });
const bound = await standIn(t, p, s, { instance: "coder", run: "coder-run", ignoreTerm: true });
const spare = await standIn(t, p, s, { instance: "coder", run: "coder-spare" });
const stuck = await standIn(t, p, s, { instance: "coder", run: "coder-stuck", ignoreTerm: true });
bind(s, "coder", "coder-run");
const fired = await fire(s, bind(s, "chief", "chief-run"), "coder");
// The bus knows only the bound run. The pass takes the spawned ones from the registry.
assert.deepEqual(fired.runs, ["coder-run"]);
await s.launcher.tick();
assert.equal(await bound.exited, "SIGKILL");
assert.equal(await spare.exited, 143);
assert.equal(stuck.gone(), false);
assert.deepEqual([bound.terms(), stuck.terms()], [1, 1]);
assert.deepEqual(signals.filter(([r]) => r === "coder-spare"), [["coder-spare", "SIGTERM"]]);
const events = (action) => allowed(p, action).map((e) => [e.subject, e.body.request, e.body.run, e.body.pid]);
assert.deepEqual(events("lifecycle.end.killed"), [["coder", fired.request, "coder-run", bound.pid]]);
assert.deepEqual(events("lifecycle.end.alive"), [["coder", fired.request, "coder-stuck", stuck.pid]]);
assert.equal(allowed(p, "lifecycle.done").filter((e) => e.body.request === fired.request).length, 1);
// D7: the Console's listing flags the run fire left alive.
assert.deepEqual(s.call("instances").instances.coder.alive, ["coder-stuck"]);
const stops = launchLog(p.f).filter((e) => e.event === "fire-stop" && e.request === fired.request);
assert.deepEqual(stops.map((e) => [e.run, e.outcome]).sort(), [["coder-run", "killed"], ["coder-spare", "stopped"], ["coder-stuck", "alive"]]);
assert.deepEqual(stops.find((e) => e.run === "coder-run").signals, ["SIGTERM", "SIGKILL"]);
});
// sage-2289: a registry run with no row 191 runtime record makes stopRun answer unknown and send no
// signal. Unknown can't confirm a kill, so it reads as alive with reason unknown, and the Console flags it.
test("row 194 T9a: fire of a registry run with no runtime record reads lifecycle.end.alive with reason unknown, never killed", { skip, timeout: 60000 }, async (t) => {
const p = prepare(t, listed);
const s = await start(t, p, { stopOptions: { graceMs: 400, killWaitMs: 400 } });
const bound = await standIn(t, p, s, { instance: "coder", run: "coder-run" });
const ghost = await standIn(t, p, s, { instance: "coder", run: "coder-ghost", managed: false });
bind(s, "coder", "coder-run");
const fired = await fire(s, bind(s, "chief", "chief-run"), "coder");
assert.deepEqual(fired.runs, ["coder-run"]);
await s.launcher.tick();
assert.equal(await bound.exited, 143);
await new Promise((r) => setTimeout(r, 300));
assert.deepEqual([ghost.gone(), ghost.terms()], [false, 0]);
const events = (action) => allowed(p, action).map((e) => [e.subject, e.body.request, e.body.run, e.body.pid, e.body.reason]);
assert.deepEqual(events("lifecycle.end.killed"), []);
assert.deepEqual(events("lifecycle.end.alive"), [["coder", fired.request, "coder-ghost", null, "unknown"]]);
assert.deepEqual(s.call("instances").instances.coder.alive, ["coder-ghost"]);
const stops = launchLog(p.f).filter((e) => e.event === "fire-stop" && e.request === fired.request);
assert.deepEqual(stops.map((e) => [e.run, e.outcome]).sort(), [["coder-ghost", "unknown"], ["coder-run", "stopped"]]);
});
// The fake clock makes the stop's steps exact: each identity look is counted at its instant, and
// the second look at the first instant past the grace is stopRun's re-check just before the SIGKILL.
test("row 194 T9a: a runner whose identity changed before the SIGTERM gets no signal, and one whose identity changed between the SIGTERM and the SIGKILL gets no SIGKILL", { skip, timeout: 60000 }, async (t) => {
const p = prepare(t, listed);
const GRACE = 200;
let clock = 0;
const sent = [];
const looks = new Map();
const changed = new Set();
const pids = {};
const identity = (pid) => {
const real = processIdentity(pid);
if (!real) return real;
if (pid === pids.between && !changed.has(pid)) {
const c = looks.get(pid) ?? { first: clock, at: clock, n: 0 };
if (c.at !== clock) Object.assign(c, { at: clock, n: 0 });
c.n += 1;
looks.set(pid, c);
if (clock >= c.first + GRACE && c.n === 2) changed.add(pid);
}
return pid === pids.before || changed.has(pid) ? { ...real, startTime: String(Number(real.startTime) + 1) } : real;
};
const stopOptions = {
graceMs: GRACE,
killWaitMs: GRACE,
now: () => clock,
sleep: async (ms) => {
clock += ms;
await new Promise((r) => setImmediate(r));
},
identity,
signal: (r, sig) => {
sent.push([r.run, sig]);
process.kill(r.pid, sig);
return true;
},
};
const s = await start(t, p, { stopOptions });
const before = await standIn(t, p, s, { instance: "coder", run: "coder-before", ignoreTerm: true });
const between = await standIn(t, p, s, { instance: "coder", run: "coder-between", ignoreTerm: true });
Object.assign(pids, { before: before.pid, between: between.pid });
bind(s, "coder", "coder-between");
const fired = await fire(s, bind(s, "chief", "chief-run"), "coder");
await s.launcher.tick();
assert.deepEqual(sent, [["coder-between", "SIGTERM"]]);
assert.deepEqual([before.gone(), between.gone(), before.terms(), between.terms()], [false, false, 0, 1]);
assert.ok(changed.has(between.pid), "the identity changed at the SIGKILL's re-check");
const stops = launchLog(p.f).filter((e) => e.event === "fire-stop" && e.request === fired.request);
assert.deepEqual(stops.map((e) => [e.run, e.outcome]).sort(), [["coder-before", "identity-changed"], ["coder-between", "identity-changed"]]);
assert.deepEqual([allowed(p, "lifecycle.end.killed"), allowed(p, "lifecycle.end.alive")], [[], []]);
});
// V3: terminate sends the SIGTERM only, whatever the grace.
test("row 194 T9a: V3 terminate of a SIGTERM-ignoring runner sends no SIGKILL", { skip, timeout: 60000 }, async (t) => {
const p = prepare(t, listed);
const s = await start(t, p, { stopOptions: { graceMs: 200, killWaitMs: 200 } });
const x = await standIn(t, p, s, { instance: "reviewer", run: "reviewer-run", ignoreTerm: true });
bind(s, "reviewer", "reviewer-run");
const out = s.call("instance.end", { instance: "reviewer", reason: "stuck" });
assert.deepEqual(out.runs, ["reviewer-run"]);
await s.carry(out.request);
await until(() => x.terms() === 1);
await new Promise((r) => setTimeout(r, 800));
assert.deepEqual([x.gone(), x.terms()], [false, 1]);
assert.equal(launchLog(p.f).some((e) => e.event === "fire-stop"), false);
});
// Row 191's halt sweep joins a fire's stop of the same run in flight: one owner, one deadline, one
// receipt. The sweep's own grace is its default 5 s, so a restarted deadline shows in the time.
test("row 194 T9b: a halt during a fire's stop sends no second SIGTERM, keeps the fire's deadline, writes one stop receipt with reason fire and the request, and counts the run; a fire while halted refuses", { skip, timeout: 60000 }, async (t) => {
const p = prepare(t, listed);
const GRACE = 1500, OBSERVE = 500;
const s = await start(t, p, { stopOptions: { graceMs: GRACE, killWaitMs: OBSERVE } });
const x = await standIn(t, p, s, { instance: "coder", run: "coder-run", ignoreTerm: true });
const chief = bind(s, "chief", "chief-run");
bind(s, "coder", "coder-run");
const fired = await fire(s, chief, "coder");
const began = Date.now();
const pass = s.launcher.tick();
await until(() => x.terms() === 1);
const halted = transition(p.f.dataRoot, { business: "acme", human: p.f.doc.human, halted: true });
const swept = await sweepRuntimes(p.f.dataRoot, halted.generation);
const took = Date.now() - began;
await pass;
assert.equal(await x.exited, "SIGKILL");
assert.equal(x.terms(), 1);
assert.ok(took < GRACE + OBSERVE + 1000, `the halt kept the fire's deadline (${took} ms)`);
const dir = join(controlDirectory(p.f.dataRoot), "run-stops");
const receipts = readdirSync(dir).filter((n) => n.endsWith(".result.json")).map((n) => JSON.parse(readFileSync(join(dir, n), "utf8")));
assert.deepEqual(receipts.map((r) => [r.run, r.reason, r.owner, r.outcome]), [["coder-run", "fire", { decision: fired.request }, "killed"]]);
const counted = swept.results.find((r) => r.run === "coder-run");
assert.deepEqual([counted.outcome, counted.reason, counted.owner, counted.stop], ["killed", "fire", { decision: fired.request }, receipts[0].id]);
assert.equal(swept.remaining, 0);
assert.ok(swept.stopped >= 1);
assert.deepEqual(allowed(p, "lifecycle.end.killed").map((e) => e.body.run), ["coder-run"]);
// D6: the chief's fire refuses while halted, before step A.
assert.throws(() => fire(s, chief, "reviewer"), (e) => e.code === "processing-halted");
});
// D6 with row 191's switch: while halted a tick runs only the lifecycle pass. Launch and
// regenerate requests opened before the halt close processing-halted, and terminate still signals.
test("row 194 T15: while halted the pass closes a launch and a regenerate processing-halted and runs no thread pass, terminate signals, and after a resume a launch starts", { skip, timeout: 60000 }, async (t) => {
const p = prepare(t);
const s = await start(t, p);
let asked = 0;
const threadsState = s.runtime.threadsState;
s.runtime.threadsState = (business) => {
asked += 1;
return threadsState(business);
};
const x = await standIn(t, p, s, { instance: "reviewer", run: "reviewer-run", managed: false });
bind(s, "reviewer", "reviewer-run");
const launch = s.call("instance.launch", { instance: "coder" }).request;
const regen = s.call("instance.regenerate", { instance: "sec-1" }).request;
const halted = transition(p.f.dataRoot, { business: "acme", human: p.f.doc.human, halted: true });
assert.throws(() => s.call("instance.launch", { instance: "coder" }), (e) => e.code === "processing-halted");
const end = s.call("instance.end", { instance: "reviewer", reason: "stuck" });
assert.deepEqual(end.runs, ["reviewer-run"]);
await s.launcher.tick();
for (const request of [launch, regen]) assert.deepEqual([s.state(request).state, s.state(request).done.refusal], ["closed", "processing-halted"], request);
assert.equal(s.state(end.request).state, "closed");
assert.equal(await x.exited, 143);
// The launcher's exit handler drops its own runs. A stand-in's entry goes by hand.
s.launcher.registry.remove("acme", "reviewer-run");
assert.deepEqual(launchLog(p.f).filter((e) => e.event === "lifecycle-end").map((e) => e.run), ["reviewer-run"]);
assert.equal(asked, 0, "a halted tick runs no thread pass");
assert.equal(s.running("coder").length, 0);
transition(p.f.dataRoot, { business: "acme", human: p.f.doc.human, halted: false, generation: halted.generation });
const after = await s.carry(s.call("instance.launch", { instance: "coder" }).request);
assert.equal(after.done.refusal, null);
assert.ok(asked > 0);
assert.deepEqual(s.running("coder").map((r) => r.run), [after.done.run]);
});
// N6 (sage-2246, DESIGN.md:118-120): an end signals the runs the verb named and the instance's runs
// spawned by the verb's time. A run spawned after the verb survives that pass, and the next end has it.
test("row 194 N6: a run spawned after a terminate's verb survives its pass, and the next terminate ends it", { skip, timeout: 60000 }, async (t) => {
const p = prepare(t);
const s = await start(t, p);
const early = await standIn(t, p, s, { instance: "coder", run: "coder-early" });
// Neither holds a claim. An open request each lets the end through, and the end cancels it.
s.call("instance.launch", { instance: "coder" });
const first = s.call("instance.end", { instance: "coder" });
assert.deepEqual([first.runs, first.cancelled.length], [[], 1]);
await new Promise((r) => setTimeout(r, 20));
const late = await standIn(t, p, s, { instance: "coder", run: "coder-late" });
await s.carry(first.request);
assert.equal(await early.exited, 143);
// The launcher's exit handler drops its own runs. A stand-in's entry goes by hand.
s.launcher.registry.remove("acme", "coder-early");
await new Promise((r) => setTimeout(r, 300));
assert.deepEqual([late.gone(), late.terms()], [false, 0]);
s.call("instance.launch", { instance: "coder" });
const second = s.call("instance.end", { instance: "coder" });
await s.carry(second.request);
assert.equal(await late.exited, 143);
assert.deepEqual(launchLog(p.f).filter((e) => e.event === "lifecycle-end").map((e) => e.run), ["coder-early", "coder-late"]);
});
test("row 194 T6: a delete leaves the run evidence bytes unchanged, and the retired name refuses a create and a launch", { skip, timeout: 60000 }, async (t) => {
const p = prepare(t);
const s = await start(t, p);
const run = (await s.carry(s.call("instance.launch", { instance: "sec-1" }).request)).done.run;
await s.claimed(run);
await s.carry(s.call("instance.end", { instance: "sec-1" }).request);
await until(() => s.running("sec-1").length === 0 && launchLog(p.f).some((e) => e.event === "end" && e.run === run));
const runDir = join(p.f.dataRoot, "launches", "acme", run);
const before = snapshot(runDir);
const log = readFileSync(launchLogFile(p.f.dataRoot, "acme"), "utf8");
assert.ok(Object.keys(before).length > 0);
const del = await s.carry(s.call("instances.set", { revision: s.call("instances").revision, instance: "sec-1", op: "delete" }).request);
assert.equal(del.done.refusal, null);
assert.deepEqual(snapshot(runDir), before);
assert.ok(readFileSync(launchLogFile(p.f.dataRoot, "acme"), "utf8").startsWith(log), "the launch log only grows");
assert.throws(
() => s.call("instances.set", { revision: s.call("instances").revision, instance: "sec-1", op: "add", definition: "security-reviewer" }),
(e) => e.code === "organization-instance-retired",
);
assert.throws(() => s.call("instance.launch", { instance: "sec-1" }), (e) => e.code === "instance-retired");
});
test("row 194 T11: two stack roots with the same business id; a launch, a terminate and a delete in one leave the other byte-identical", { skip, timeout: 90000 }, async (t) => {
const pa = prepare(t);
const pb = prepare(t);
const a = await start(t, pa);
const b = await start(t, pb);
b.call("instances");
const before = snapshot(pb.root);
const run = (await a.carry(a.call("instance.launch", { instance: "coder" }).request)).done.run;
await a.claimed(run);
await a.carry(a.call("instance.end", { instance: "coder" }).request);
await until(() => a.running("coder").length === 0);
const del = await a.carry(a.call("instances.set", { revision: a.call("instances").revision, instance: "sec-1", op: "delete" }).request);
assert.equal(del.done.refusal, null);
await b.launcher.tick();
assert.deepEqual(snapshot(pb.root), before);
assert.equal(b.launcher.registry.sessions.length, 0);
assert.equal(Object.hasOwn(b.call("instances").instances, "sec-1"), true);
});
// D3 item 5 and T8: the chief's session hires through its hire tool, and the same session launches
// the new name with no restart. Its launch tool takes a name, so a name absent from the start-time
// listing gets through to the host, which checks the file.
test("row 194 D3 item 5: the chief's session hires with its tool and launches its hire in the same session, then fires it", { skip, timeout: 90000 }, async (t) => {
const p = prepare(t);
const s = await start(t, p);
const chief = (await s.carry(s.call("instance.launch", { instance: "chief" }).request)).done.run;
await s.claimed(chief);
const ask = async (name, args) => {
const sent = s.call("message.send", { to: "chief", class: "REQUEST", body: `TOOL ${name} ${JSON.stringify(args)}` }).id;
return (await until(() => s.call("replies", { id: sent })[0], 30000)).body;
};
const role = readTemplate(p.f.env.MOSAIC_ROLES_DIR,"chief-information-security-officer");
const hired = JSON.parse(await ask("hire", {op:"hire",level:"org",name:"sec-2",role:{template:role.name,sha256:role.sha256},route:{harness:"pi",model:"claude-sonnet-5-5",thinking:"high"},reason:"a security lead for the review queue"}));
assert.ok(hired.id);
assert.equal(s.call("instances").instances["sec-2"],undefined,"raise writes nothing");
await s.runtime.broker.request(ownerConsole(s.runtime.broker,"acme"),{verb:"hire.resolve.console",args:{id:hired.id,choice:"yes",digest:hired.hire.digest}});
assert.equal(s.call("instances").instances["sec-2"].launched,true);
const launched = JSON.parse(await ask("launch", { instance: "sec-2" }));
assert.match(launched.run, /^r[0-9a-f]{12}$/);
assert.deepEqual(s.running("sec-2").map((x) => x.run), [launched.run]);
assert.equal(readSessions(p.f.dataRoot).find((x) => x.run === launched.run).launchedBy, "chief");
// The schema's pattern guides the model. The host refuses any name its file doesn't list.
assert.equal((await ask("launch", { instance: "NOT A NAME" })).split("\n")[0], "refused: instance-not-listed");
assert.equal((await ask("launch", { instance: "reviewer" })).split("\n")[0], "refused: instance-not-listed");
await s.claimed(launched.run);
const fired = JSON.parse(await ask("fire", {op:"fire",level:"org",name:"sec-2",reason:"the review is done"}));
assert.ok(fired.id);
await s.runtime.broker.request(ownerConsole(s.runtime.broker,"acme"),{verb:"hire.resolve.console",args:{id:fired.id,choice:"yes",digest:fired.hire.digest}});
await until(() => s.running("sec-2").length === 0, 30000);
// A fired instance stays in roles, off the listing, so the chief can't launch it again.
assert.equal(s.call("instances").instances["sec-2"].launched, false);
assert.equal((await ask("launch", { instance: "sec-2" })).split("\n")[0], "refused: instance-not-listed");
});