Files
pi-goals/test/goals.test.ts
T
wassnameandPi/OpenAI 6fa518fc49 Fix goal state recovery, approval invalidation and worker tracking
Unify Log boundaries, serialize completion edits, preserve immutable session state and solo recovery, and match stock worker execution events. Remove unused plan views and brittle prose checks; correct installation and context guidance.

Co-Authored-By: Pi/OpenAI <288921227+claudypoo@users.noreply.github.com>
2026-09-12 15:01:43 +08:00

917 lines
52 KiB
TypeScript

import { mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
import { access, readFile, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createEditTool, type ExtensionAPI, SessionManager, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
import { afterEach, expect, it, vi } from "vitest";
import goalsExtension from "../src/index.js";
import { upkeep, upkeepNudges } from "../src/prompts.js";
const roots: string[] = [];
const shutdowns: Array<() => void> = [];
afterEach(() => { for (const shutdown of shutdowns.splice(0)) shutdown(); vi.unstubAllEnvs(); for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); });
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
async function waitFor(predicate: () => boolean, ms = 1500): Promise<void> {
const start = Date.now();
while (!predicate()) {
if (Date.now() - start > ms) throw new Error("timed out waiting for condition");
await delay(10);
}
}
function fixture(child = false) {
vi.stubEnv("PI_SUBAGENT_AGENT", child ? "goals-worker" : "");
const cwd = mkdtempSync(join(tmpdir(), "goals-main-test-")); roots.push(cwd);
const entries: any[] = []; const hooks = new Map<string, any>(); const commands = new Map<string, any>(); const tools = new Map<string, any>();
const messages: any[] = [];
const ctx = { cwd, sessionManager: { getBranch: () => entries, getSessionId: () => "copy-only" }, hasUI: true, ui: {
theme: { fg: (_color: string, text: string) => text }, notify: vi.fn(), setStatus: vi.fn(), setWidget: vi.fn(), select: vi.fn(async (_title: string, _options: string[]) => "Ready"), editor: vi.fn(),
} };
const pi = {
on: (event: string, hook: any) => hooks.set(event, hook),
appendEntry: (customType: string, data: any) => entries.push({ type: "custom", customType, data }),
registerCommand: (name: string, definition: any) => commands.set(name, definition),
registerTool: (definition: any) => tools.set(definition.name, definition),
sendMessage: (message: any, options: any) => messages.push({ message, options }),
sendUserMessage: (content: string, options: any) => messages.push({ message: { content }, options, savedPrompt: true }),
events: { emit: vi.fn() },
getAllTools: vi.fn(() => [
{ name: "subagent", parameters: { properties: { agent: {}, title: {} } } },
{ name: "subagent_resume", parameters: { properties: { sessionFile: {} } } },
{ name: "subagent_kill", parameters: { properties: { id: {} } } },
]),
};
goalsExtension(pi as unknown as ExtensionAPI);
hooks.get("session_start")({}, ctx);
const command = (value: string) => commands.get("goals").handler(value, ctx);
const path = join(cwd, ".pi/plan/copy-only-main.md");
const plan = "# Plan\n- [ ] goal: first output\n- [ ] goal: second output\n\n## Log\n";
const draft = async () => { await command("new two outputs"); writeFileSync(path, plan); };
const shutdown = () => hooks.get("session_shutdown")();
shutdowns.push(shutdown);
const changed = () => messages.filter((m) => m.message?.content?.includes("Plan changed")).length;
const atomicWrite = async (text: string) => {
const tmp = `${path}.tmp`;
writeFileSync(tmp, text);
renameSync(tmp, path);
await delay(25);
};
const start = (toolCallId: string, input: any = { agent: "goals-worker", title: "Implement" }, toolName = "subagent") => hooks.get("tool_call")({ toolCallId, toolName, input }, ctx);
const finish = (toolCallId: string, details: any, toolName = "subagent", isError = false) => hooks.get("tool_execution_end")({ toolCallId, toolName, result: { content: [], details }, isError }, ctx);
const launch = (details: any, agent = "goals-worker", toolName = "subagent") => {
start(details.id, { agent, title: "Work", sessionFile: details.sessionFile }, toolName);
finish(details.id, details, toolName);
};
return { ctx, pi, hooks, tools, commands, messages, command, path, plan, draft, shutdown, changed, atomicWrite, entries, start, finish, launch };
}
it.each([
["chat", ["new", "attach", "help", "quit"]],
["planning", ["edit", "discuss", "ready", "model", "help", "quit"]],
["supervising", ["review", "stop", "model", "help", "quit"]],
["paused", ["resume", "model", "help", "quit"]],
["solo", ["stop", "help", "quit"]],
])("shows only applicable %s actions without starting work", async (mode, expected) => {
const f = fixture();
if (mode !== "chat") await f.draft();
if (mode === "supervising" || mode === "paused") await f.command("ready");
if (mode === "paused") await f.command("stop");
if (mode === "solo") { f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped"); await f.command("solo"); }
const before = f.messages.length;
f.ctx.ui.select.mockResolvedValueOnce(undefined as any);
await f.command("");
const actions = f.ctx.ui.select.mock.calls.at(-1)![1];
expect(actions.map(action => action.split(" — ")[0])).toEqual(expected);
expect(actions.at(-1)).toBe("quit — Exit and clear goals");
expect(f.messages).toHaveLength(before);
expect(f.commands.get("goals").getArgumentCompletions("res")).toEqual([{ value: "resume", label: "resume" }]);
});
it.each(["redy", "start", "two outputs", "status extra", "attach some.md solo extra"])("rejects %s without changing the plan or sending a model prompt", async (text) => {
const f = fixture(); await f.draft();
const before = readFileSync(f.path, "utf8");
const entries = f.entries.length; const messages = f.messages.length;
await f.command(text);
expect(readFileSync(f.path, "utf8")).toBe(before);
expect(f.entries).toHaveLength(entries);
expect(f.messages).toHaveLength(messages);
});
it("requires a model argument without clearing the preference", async () => {
const f = fixture(); await f.draft(); await f.command("model provider/model");
const before = readFileSync(f.path, "utf8");
await f.command("model");
expect(readFileSync(f.path, "utf8")).toBe(before);
});
it.each(["menu", "command"])("enters planning conversation through %s without an objective box or worker launch", async (route) => {
const f = fixture();
f.ctx.ui.select.mockResolvedValueOnce("new — New plan");
await f.command(route === "menu" ? "" : "new");
expect(f.entries.at(-1).data.mode).toBe("planning");
expect(f.ctx.ui.editor).not.toHaveBeenCalled();
expect(f.messages.at(-1).message.content).toContain("Ask what the user wants to achieve");
expect(f.hooks.get("tool_call")({ toolName: "subagent" }).block).toBe(true);
});
it("edits even an empty draft directly without a model call", async () => {
const f = fixture(); await f.command("new"); const before = f.messages.length;
f.ctx.ui.editor.mockResolvedValueOnce(f.plan);
f.ctx.ui.select.mockResolvedValueOnce("edit — Edit plan…"); await f.command("");
expect(readFileSync(f.path, "utf8")).toBe(f.plan);
expect(f.entries.at(-1).data.mode).toBe("planning");
expect(f.messages).toHaveLength(before);
});
it("clear backs up the plan, drops stale bindings and allows a separate new draft", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
f.launch({ id: "stale", sessionFile: "/tmp/old-worker.jsonl" });
const jobs = [
{ id: "owned", name: "goals-copy-only", session: "copy-only", enabled: true },
{ id: "older", name: "older-plan", session: "copy-only", enabled: true },
{ id: "foreign", name: "goals-copy-only", session: "other", enabled: true },
{ id: "unbound", name: "goals-copy-only", enabled: true },
];
const schedule = join(f.ctx.cwd, ".pi/schedule-prompts.json"); writeFileSync(schedule, JSON.stringify({ version: 1, jobs }));
const before = f.messages.length; await f.command("clear");
expect(f.messages).toHaveLength(before);
expect(f.entries.at(-1).data).toEqual({ mode: "chat", helpers: [], signoffs: {} });
expect(JSON.parse(readFileSync(schedule, "utf8")).jobs).toEqual(jobs.slice(1));
expect(f.pi.events.emit).toHaveBeenCalledWith("cron:change", { type: "remove", jobId: "owned" });
const directory = join(f.ctx.cwd, ".pi/plan");
const backup = readdirSync(directory).find(name => name.endsWith(".bak"))!;
expect(readFileSync(join(directory, backup), "utf8")).toBe(f.plan);
await f.command("new a different objective");
const next = f.entries.at(-1).data;
expect(next.mode).toBe("planning"); expect(next.worker).toBeUndefined(); expect(next.plan).not.toBe(f.path);
expect(readFileSync(next.plan, "utf8")).toContain("a different objective");
expect(readFileSync(next.plan, "utf8")).not.toContain("first output");
expect(readFileSync(f.path, "utf8")).toBe(f.plan);
});
it.each(["missing", "empty"])("clear resets a %s plan without a model call", async kind => {
const f = fixture(); await f.draft(); const before = f.messages.length;
if (kind === "missing") rmSync(f.path); else writeFileSync(f.path, "");
await f.command("clear");
expect(f.entries.at(-1).data).toEqual({ mode: "chat", helpers: [], signoffs: {} });
expect(f.messages).toHaveLength(before);
});
it("discusses plan changes only during planning", async () => {
const f = fixture(); await f.command("discuss"); expect(f.messages).toHaveLength(0);
await f.draft();
f.ctx.ui.select.mockResolvedValueOnce("discuss — Discuss changes to the plan"); await f.command("");
expect(f.messages.at(-1).message.content).toContain("Discuss the current draft");
expect(f.entries.at(-1).data.mode).toBe("planning");
await f.command("ready"); const before = f.messages.length;
await f.command("discuss"); expect(f.messages).toHaveLength(before);
});
it("automatically proposes a changed settled draft once and preserves Discuss", async () => {
const f = fixture(); await f.draft();
f.ctx.ui.select.mockResolvedValueOnce("Discuss");
await f.hooks.get("agent_settled")({}, f.ctx);
expect(f.messages.some(m => m.message.customType === "goal-plan-proposal" && m.message.content === f.plan)).toBe(true);
expect(f.entries.at(-1).data.mode).toBe("planning");
const calls = f.ctx.ui.select.mock.calls.length;
await f.hooks.get("agent_settled")({}, f.ctx);
expect(f.ctx.ui.select).toHaveBeenCalledTimes(calls);
writeFileSync(f.path, f.plan.replace("first output", "revised output"));
f.ctx.ui.select.mockResolvedValueOnce("Ready");
await f.hooks.get("agent_settled")({}, f.ctx);
expect(f.entries.at(-1).data.mode).toBe("supervising");
});
it("does not propose an empty draft or a delegated worker's plan", async () => {
const f = fixture(); await f.command("new");
await f.hooks.get("agent_settled")({}, f.ctx);
expect(f.ctx.ui.select).not.toHaveBeenCalled();
const child = fixture(true); await child.hooks.get("agent_settled")({}, child.ctx);
expect(child.ctx.ui.select).not.toHaveBeenCalled();
});
it("keeps Ready in the same chat, sends saved notices and never installs a context hook", async () => {
const f = fixture(); await f.draft(); await f.command("review");
expect(f.entries.at(-1).data.mode).toBe("supervising");
expect(f.messages.at(-1).options).toEqual({ deliverAs: "followUp" });
expect(f.messages.at(-1).savedPrompt).toBe(true);
expect(f.messages.at(-1).message.content).toContain("goals-worker");
expect(f.hooks.has("context")).toBe(false);
const event = { systemPrompt: "original system" };
expect(f.hooks.get("before_agent_start")(event, f.ctx).systemPrompt).toContain("original system");
f.hooks.get("session_compact")();
expect(f.hooks.get("before_agent_start")(event, f.ctx).message.content).toContain("Current goal mode: supervising");
f.shutdown();
});
it("preserves a draft when the wrong subagent package is loaded, and offers explicit solo", async () => {
const f = fixture(); await f.draft(); f.pi.getAllTools.mockReturnValue([]);
await f.command("ready"); expect(f.entries.at(-1).data.mode).toBe("planning");
f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped");
await f.command("solo"); expect(f.entries.at(-1).data.mode).toBe("solo");
});
it("rejects a plan changed while the human was reviewing it", async () => {
const f = fixture(); await f.draft();
f.ctx.ui.select.mockImplementation(async () => { writeFileSync(f.path, "- [ ] goal: substituted\n"); return "Ready"; });
await f.command("review"); expect(f.entries.at(-1).data.mode).toBe("planning");
});
it("reloads a paused plan without launching, and retains the public worker session handle", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
f.launch({ id: "child-1", sessionFile: "/tmp/child.jsonl" });
await f.command("stop");
expect(f.messages.at(-1).message.content).toContain("Remote stop is NOT yet confirmed");
f.hooks.get("session_start")({}, f.ctx);
expect(f.hooks.get("tool_call")({ toolName: "subagent_resume" }).block).toBe(true);
await f.command("resume");
expect(f.messages.at(-1).message.content).toContain("/tmp/child.jsonl");
await f.command("exit"); expect(f.entries.at(-1).data.mode).toBe("chat");
expect(readFileSync(f.path, "utf8")).toContain("first output");
});
it.each(["FIRST OUTPUT", "renamed output", "duplicate", "historical"])("completion uses exact current subjects (%s)", async (subject) => {
const f = fixture(); await f.draft(); await f.command("ready");
const evidence = join(f.ctx.cwd, "verification.txt"); writeFileSync(evidence, "PASS");
const suffix = subject === "duplicate" ? "- [ ] goal: first output\n" : "";
const history = "## Log\n- [ ] goal: first output\n";
writeFileSync(f.path, "- [ ] goal: first output\n - [ ] unrelated task\n" + suffix + history);
const before = readFileSync(f.path, "utf8");
await f.tools.get("CompleteGoal").execute("c", { goal: subject === "duplicate" || subject === "historical" ? "first output" : subject, evidence: [evidence], observation: "Read actual output" }, undefined, undefined, f.ctx);
const after = readFileSync(f.path, "utf8");
if (subject === "renamed output" || subject === "duplicate") expect(after).toBe(before);
else { expect(after).toContain("- [x] goal: first output"); expect(after.split("## Log")[1]).toContain("\n- [ ] goal: first output\n"); expect(after).toContain("- [ ] unrelated task"); }
});
it("rejects an existing zero-byte evidence file", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
const evidence = join(f.ctx.cwd, "empty.log"); writeFileSync(evidence, "");
const before = readFileSync(f.path, "utf8");
const result = await f.tools.get("CompleteGoal").execute("c", { goal: "first output", evidence: [evidence], observation: "claim" }, undefined, undefined, f.ctx);
expect(result.content[0].text).toContain("Empty evidence"); expect(readFileSync(f.path, "utf8")).toBe(before);
});
it("requires actual nonempty evidence, distinguishes manual ticks, and retains signoffs on reload", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
const complete = (goal: string, evidence: string[], signal?: AbortSignal) => f.tools.get("CompleteGoal").execute("t", { goal, evidence, observation: "Inspected exact saved bytes" }, signal, undefined, f.ctx);
expect((await complete("first output", ["missing.log"])).content[0].text).toContain("Evidence unavailable");
mkdirSync(join(f.ctx.cwd, "evidence")); writeFileSync(join(f.ctx.cwd, "evidence/pass.log"), "actual fixture bytes\n");
expect((await complete("first output", ["evidence/pass.log"], AbortSignal.abort())).content[0].text).toContain("Cancelled");
await complete("first output", ["evidence/pass.log"]);
writeFileSync(f.path, readFileSync(f.path, "utf8").replace("[ ] goal: second", "[x] goal: second"));
f.hooks.get("session_start")({}, f.ctx);
expect(f.ctx.ui.setStatus).toHaveBeenLastCalledWith("goals", "goals: supervising | 1/2 reviewed");
expect(f.ctx.ui.setWidget.mock.lastCall?.[1]).toContain("? = completion claim; parent review still required");
writeFileSync(f.path, readFileSync(f.path, "utf8").replace("[x] goal: first", "[ ] goal: first"));
f.hooks.get("agent_end")({}, f.ctx);
expect(f.ctx.ui.setStatus).toHaveBeenLastCalledWith("goals", "goals: supervising | 0/2 reviewed");
f.shutdown();
});
it("reviews a plan replaced atomically, and ignores writes that keep the same content", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
await f.atomicWrite(f.plan.replace("## Log", "- discriminator: changed requirement\n## Log"));
await waitFor(() => f.changed() === 1);
const review = f.messages.find((m) => m.message.content.includes("Plan changed"))?.message.content;
expect(review).toContain("Plan changed: ");
expect(review).toContain(f.path);
await f.atomicWrite(f.plan.replace("## Log", "- discriminator: same requirement again\n## Log"));
await waitFor(() => f.changed() === 2);
// Rewriting identical bytes must not retrigger the review event hook.
const same = f.plan.replace("## Log", "- discriminator: same requirement again\n## Log");
writeFileSync(f.path, same); await delay(300);
expect(f.changed()).toBe(2);
f.shutdown();
});
it("coalesces duplicate plan-change notifications into one review", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
await f.atomicWrite(f.plan.replace("## Log", "- discriminator: first burst edit\n## Log"));
await f.atomicWrite(f.plan.replace("## Log", "- discriminator: second burst edit\n## Log"));
await waitFor(() => f.changed() === 1);
await delay(200);
expect(f.changed()).toBe(1);
f.shutdown();
});
it("stops plan watching on shutdown and re-arms it on reload without duplicating events", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
f.shutdown();
await f.atomicWrite(f.plan.replace("## Log", "- discriminator: ignored while shut down\n## Log"));
await delay(150);
expect(f.changed()).toBe(0);
f.hooks.get("session_start")({}, f.ctx);
await f.atomicWrite(f.plan.replace("## Log", "- discriminator: seen after reload\n## Log"));
await waitFor(() => f.changed() === 1);
expect(f.changed()).toBe(1);
f.shutdown();
});
it("does not retrigger a review for its own CompleteGoal plan write", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
mkdirSync(join(f.ctx.cwd, "evidence")); writeFileSync(join(f.ctx.cwd, "evidence/pass.log"), "bytes\n");
await f.tools.get("CompleteGoal").execute("t", { goal: "first output", evidence: ["evidence/pass.log"], observation: "inspected" }, undefined, undefined, f.ctx);
await delay(200);
expect(f.changed()).toBe(0);
f.shutdown();
});
it("gives pause scheduler guidance but clears on exit without a model prompt", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
await f.command("stop");
const stop = f.messages.at(-1).message.content;
expect(stop).toContain('goals-copy-only"');
expect(stop).toContain("Do not add, enable or recreate any job");
expect(stop).not.toContain("interval '1h'");
expect(stop).toContain("Remote stop is NOT yet confirmed");
await f.command("resume");
const before = f.messages.length;
await f.command("exit");
expect(f.messages).toHaveLength(before);
expect(f.entries.at(-1).data).toEqual({ mode: "chat", helpers: [], signoffs: {} });
});
it("tells the model to remove only its own job after the final review", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
mkdirSync(join(f.ctx.cwd, "evidence")); writeFileSync(join(f.ctx.cwd, "evidence/pass.log"), "bytes\n");
let finalText = "";
for (const goal of ["first output", "second output"]) {
finalText = (await f.tools.get("CompleteGoal").execute("t", { goal, evidence: ["evidence/pass.log"], observation: "inspected" }, undefined, undefined, f.ctx)).content[0].text;
}
expect(finalText).toContain("All non-cancelled goals are reviewed.");
expect(finalText).toContain('job named "goals-copy-only"');
expect(finalText).toContain("leave other jobs untouched");
f.shutdown();
});
it("restores context after compaction without reinstalling or overriding scheduler jobs", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
f.hooks.get("session_compact")();
const result = f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx);
expect(result.systemPrompt).not.toContain("add one session-bound");
expect(result.message.content).toContain(f.path);
});
it("recovers from an unreadable plan after compaction instead of restarting work", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
rmSync(f.path);
f.hooks.get("session_compact")();
const result = f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx);
expect(result.systemPrompt).toContain("ENOENT");
expect(result.systemPrompt).toContain("do not restart completed work");
writeFileSync(f.path, f.plan);
const restored = f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx).message.content;
expect(restored).toContain("- [ ] goal: first output");
expect(restored).toContain(f.path);
f.shutdown();
});
it("requires confirmed worker stop before solo takeover and never lets two writers run together", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
f.launch({ id: "child-1", sessionFile: "/tmp/child.jsonl" });
f.ctx.ui.select.mockResolvedValueOnce("Cancel");
await f.command("solo");
expect(f.entries.at(-1).data.mode).toBe("supervising"); // cancelled
f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped");
await f.command("solo");
expect(f.entries.at(-1).data.mode).toBe("solo");
expect(f.hooks.get("tool_call")({ toolName: "subagent" }).block).toBe(true);
expect(f.hooks.get("tool_call")({ toolName: "subagent_resume" }).block).toBe(true);
expect(f.hooks.get("tool_call")({ toolName: "subagent_kill" })).toBeUndefined();
mkdirSync(join(f.ctx.cwd, "evidence")); writeFileSync(join(f.ctx.cwd, "evidence/pass.log"), "bytes\n");
const text = (await f.tools.get("CompleteGoal").execute("t", { goal: "first output", evidence: ["evidence/pass.log"], observation: "inspected" }, undefined, undefined, f.ctx)).content[0].text;
expect(text).toContain("self-verification");
});
it("attaches an existing plan without restarting completed work, and restores its noted worker session", async () => {
const f = fixture();
const existing = join(f.ctx.cwd, "existing.md");
writeFileSync(existing, "# Plan\n- preferred worker model: deepseek flash\n- worker session: /tmp/attach-child.jsonl\n- [ ] goal: attached goal\n\n## Log\n- previous progress kept\n");
f.ctx.ui.select.mockResolvedValueOnce("Previous supervisor confirmed stopped");
await f.command(`attach ${existing}`);
expect(f.entries.at(-1).data.mode).toBe("planning");
expect(f.entries.at(-1).data.plan).toBe(existing);
expect(f.messages.at(-1).message.content).toContain("without restarting completed work");
expect(f.messages.at(-1).message.content).toContain("/tmp/attach-child.jsonl");
});
it("attaches directly into solo mode and reports the recorded session in status", async () => {
const f = fixture();
const existing = join(f.ctx.cwd, "existing.md");
writeFileSync(existing, "# Plan\n- worker session: /tmp/attach-child.jsonl\n- [ ] goal: attached goal\n\n## Log\n");
f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped");
await f.command(`attach ${existing} solo`);
expect(f.entries.at(-1).data.mode).toBe("solo");
expect(f.entries.at(-1).data.worker?.sessionFile).toBe("/tmp/attach-child.jsonl");
await f.command("status");
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("/tmp/attach-child.jsonl"), "info");
});
it("rejects attaching a missing or goal-less file", async () => {
const f = fixture();
await f.command("attach /no/such/plan.md");
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("Cannot read plan"), "error");
const goalLess = join(f.ctx.cwd, "notes.md");
writeFileSync(goalLess, "# notes\n");
await f.command(`attach ${goalLess}`);
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("has no '- [ ] goal:' lines"), "warning");
expect(f.entries).toEqual([]); // nothing saved: the session was not attached
});
it.each(["exit", "quit", "clear", "menu"])("%s exits planning with the draft preserved and nothing implemented", async command => {
const f = fixture(); await f.draft();
await f.command("stop");
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("A draft cannot pause"), "warning");
const before = f.messages.length;
if (command === "menu") f.ctx.ui.select.mockResolvedValueOnce("quit — Exit and clear goals");
await f.command(command === "menu" ? "" : command);
expect(f.entries.at(-1).data.mode).toBe("chat");
expect(f.ctx.ui.setWidget).toHaveBeenLastCalledWith("goals", undefined);
expect(readFileSync(f.path, "utf8")).toContain("first output");
expect(f.messages.length).toBe(before); // notify only, no model turn started
f.ctx.ui.select.mockResolvedValueOnce("Previous supervisor confirmed stopped");
await f.command(`attach ${f.path}`);
expect(f.entries.at(-1).data.mode).toBe("planning");
});
it("records the preferred worker model as a visible plan preference", async () => {
const f = fixture(); await f.draft();
await f.command("model deepseek flash");
expect(readFileSync(f.path, "utf8")).toContain("- preferred worker model: deepseek flash");
await f.command("status");
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("deepseek flash"), "info");
});
it.each(["solo", "attach"])("%s takeover cannot bypass confirmation or survive a lifecycle change during the menu", async kind => {
const f = fixture(); await f.draft(); await f.command("ready");
f.launch({ id: "child", sessionFile: "/tmp/prior.jsonl" });
let answer!: (choice: string) => void;
f.ctx.ui.select.mockImplementationOnce(() => new Promise(resolve => { answer = resolve; }));
const takeover = f.command(kind === "solo" ? "solo" : `attach ${f.path} solo`);
expect(f.entries.at(-1).data.mode).toBe("supervising");
await f.command("stop");
answer("Worker confirmed stopped"); await takeover;
expect(f.entries.at(-1).data.mode).toBe("paused");
expect(f.entries.at(-1).data.workerStopped).not.toBe(true);
});
it("attach solo requires stop confirmation for a noted worker even in a fresh session", async () => {
const f = fixture(); const path = join(f.ctx.cwd, "saved.md");
writeFileSync(path, `# Plan\n- worker session: /tmp/known.jsonl\n${f.plan}`);
f.ctx.ui.select.mockResolvedValueOnce("Cancel");
await f.command(`attach ${path} solo`);
expect(f.entries).toHaveLength(0);
f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped");
await f.command(`attach ${path} solo`);
expect(f.entries.at(-1).data).toMatchObject({ mode: "solo", workerStopped: true, worker: { sessionFile: "/tmp/known.jsonl" } });
expect(readFileSync(path, "utf8")).toContain("worker session: /tmp/known.jsonl");
});
it("retains the stopped session reference without permanently blocking another plan", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
f.launch({ id: "child", sessionFile: "/tmp/prior.jsonl" });
f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped"); await f.command("solo");
const other = join(f.ctx.cwd, "another.md"); writeFileSync(other, "- [ ] goal: next\n## Log\n");
f.ctx.ui.select.mockResolvedValueOnce("Previous supervisor confirmed stopped");
await f.command(`attach ${other}`);
expect(f.entries.at(-1).data).toMatchObject({ mode: "planning", plan: other, workerStopped: true, worker: { sessionFile: "/tmp/prior.jsonl" } });
await f.command("ready");
f.start("resume", { sessionFile: "/tmp/prior.jsonl" }, "subagent_resume");
expect(f.entries.at(-1).data.workerStopped).toBe(false);
await f.command("solo");
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("still pending"), "warning");
});
it("solo closes a pending plan watcher and sends removal-only scheduler guidance", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
await f.atomicWrite(f.plan.replace("first output", "changed output"));
f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped"); await f.command("solo");
expect(f.messages.at(-1).message.content).toContain('job named "goals-copy-only" bound to session "copy-only"');
expect(f.messages.at(-1).message.content).toContain("Do not add, enable or recreate any job");
await delay(250);
await f.atomicWrite(f.plan.replace("first output", "solo output"));
await delay(250);
expect(f.changed()).toBe(0);
});
it.each(["missing", "empty", "directory"])("%s plan snapshots never erase signoffs and resync retries after repair", async failure => {
const f = fixture(); await f.draft(); await f.command("ready");
writeFileSync(join(f.ctx.cwd, "proof.log"), "PASS\n");
await f.tools.get("CompleteGoal").execute("c", { goal: "first output", evidence: ["proof.log"], observation: "Observed PASS" }, undefined, undefined, f.ctx);
const signed = readFileSync(f.path, "utf8");
if (failure === "empty") writeFileSync(f.path, "");
else { rmSync(f.path); if (failure === "directory") mkdirSync(f.path); }
await delay(250); // also exercise unavailable read after debounce has expired
f.hooks.get("agent_end")({}, f.ctx);
expect(f.entries.at(-1).data.signoffs["first output"]).toBeDefined();
f.hooks.get("session_compact")();
const unavailable = f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx);
expect(unavailable.systemPrompt).toContain("unavailable");
expect(unavailable.message).toBeUndefined();
if (failure === "directory") rmSync(f.path, { recursive: true });
writeFileSync(f.path, signed);
const resync = f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx);
expect(resync.message.content).toContain("- [x] goal: first output");
expect(readFileSync(f.path, "utf8")).toContain("Observed PASS");
await delay(250);
expect(f.entries.at(-1).data.signoffs["first output"]).toBeDefined();
expect(f.changed()).toBe(0);
});
it("ignores post-completion maintenance but reviews evidence, requirement or manual reopening changes", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
writeFileSync(join(f.ctx.cwd, "proof.log"), "PASS\n");
for (const goal of ["first output", "second output"]) await f.tools.get("CompleteGoal").execute("c", { goal, evidence: ["proof.log"], observation: "PASS" }, undefined, undefined, f.ctx);
const signed = readFileSync(f.path, "utf8");
await f.atomicWrite(signed.replace("## Log", "## Log\n- recap: finished"));
await delay(250);
expect(f.changed()).toBe(0); // Log-only edits are history, not requirements
// Worker-authored evidence above the Log must surface: a supervisor caught a worker's
// contradictory evidence block through exactly this event (LUCID3, 2026-09-10).
await f.atomicWrite(signed.replace("## Log", " - evidence: proof.log\n## Log\n- recap: finished"));
await waitFor(() => f.changed() === 1);
await f.atomicWrite(signed.replace("## Log", "- discriminator: exact bytes and trailing newline\n## Log"));
await waitFor(() => f.changed() === 2);
await f.atomicWrite(signed.replace("[x] goal: first", "[ ] goal: first"));
await waitFor(() => f.changed() === 3);
expect(f.entries.at(-1).data.signoffs["first output"]).toBeUndefined();
});
it("cancelled goals do not prevent final cleanup, and solo writes self-verification in Log", async () => {
const f = fixture(); await f.draft();
writeFileSync(f.path, f.plan.replace("[ ] goal: second", "[-] goal: second") + "\n## Appendix\nPreserved context\n");
f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped"); await f.command("solo");
writeFileSync(join(f.ctx.cwd, "proof.log"), "PASS\n");
const done = await f.tools.get("CompleteGoal").execute("c", { goal: "first output", evidence: ["proof.log"], observation: "Exact bytes observed" }, undefined, undefined, f.ctx);
expect(done.content[0].text).toContain("All non-cancelled goals are reviewed");
const text = readFileSync(f.path, "utf8");
expect(text).toContain("Solo self-verification:");
expect(text).not.toContain("Parent review:");
expect(text.indexOf("Solo self-verification:")).toBeLessThan(text.indexOf("## Appendix"));
expect(text).toContain("Preserved context");
});
it("prefixes single and batch launch titles with the project without changing handles or duplicating prefixes", () => {
const f = fixture();
const single = { name: "report-worker", title: "Restore PCA" };
const event = { toolName: "subagent", input: single };
f.hooks.get("tool_call")(event, f.ctx);
const expected = `${f.ctx.cwd.split("/").at(-1)} · Restore PCA`;
expect(single).toEqual({ name: "report-worker", title: expected });
f.hooks.get("tool_call")(event, f.ctx);
expect(single.title).toBe(expected);
const children = [{ name: "test-worker", title: "Check results" }, { ...single }];
f.hooks.get("tool_call")({ toolName: "subagent", input: { children } }, f.ctx);
expect(children[0].title).toBe(`${f.ctx.cwd.split("/").at(-1)} · Check results`);
expect(children[1]).toEqual(single);
});
it("lineage-only child attaches its plan without a widget, retains task context, and cannot complete", async () => {
const f = fixture(true);
const supplied = join(f.ctx.cwd, "supplied.md");
const text = "- [/] goal: exact file\n - [ ] verify bytes\n## Log\n - [ ] archived task\n";
writeFileSync(supplied, text);
const before = f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx);
expect(before.systemPrompt).toContain("AttachGoalPlan");
const attach = f.tools.get("AttachGoalPlan");
await attach.execute("a", { path: "supplied.md" }, undefined, undefined, f.ctx);
expect(f.entries.at(-1).data.plan).toBeUndefined(); // no cwd heuristics
await attach.execute("a", { path: supplied }, undefined, undefined, f.ctx);
expect(f.ctx.ui.setWidget).toHaveBeenLastCalledWith("goals", undefined);
expect(readFileSync(supplied, "utf8")).toBe(text);
f.hooks.get("session_start")({}, f.ctx);
expect(f.ctx.ui.setWidget).toHaveBeenLastCalledWith("goals", undefined);
expect(f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx).message.content).toContain("exact file");
const completion = await f.tools.get("CompleteGoal").execute("c", { goal: "exact file", evidence: [], observation: "claim" }, undefined, undefined, f.ctx);
expect(completion.content[0].text).toContain("only to the active parent");
});
it.each(["solo", "supervising"])("%s widget omits long tasks without altering the plan", async mode => {
const f = fixture(); await f.draft();
const text = "- [/] goal: first output\n - [ ] a long task that should never take widget space\n- [ ] goal: second output\n## Log\n";
writeFileSync(f.path, text);
if (mode === "solo") { f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped"); await f.command("solo"); }
else await f.command("ready");
expect(f.ctx.ui.setWidget.mock.lastCall?.[1]).toEqual(["▸ first output", "○ second output"]);
expect(readFileSync(f.path, "utf8")).toBe(text);
});
it.each(["solo", "supervising"])("%s upkeep is turn-driven, folds Log, and joins the next ordinary prompt once", async mode => {
const f = fixture(); await f.draft();
if (mode === "solo") { f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped"); await f.command("solo"); }
else await f.command("ready");
f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx);
const reminders = () => f.messages.filter(m => m.message.customType === "pi-goals-upkeep");
f.hooks.get("turn_end")({}, f.ctx); // observe initial working set
for (let i = 0; i < 7; i++) {
writeFileSync(f.path, f.plan + `- historical recap ${i}\n`);
f.hooks.get("turn_end")({}, f.ctx);
}
expect(reminders()).toHaveLength(0);
f.hooks.get("turn_end")({}, f.ctx);
for (let i = 0; i < 16; i++) f.hooks.get("turn_end")({}, f.ctx);
expect(reminders()).toHaveLength(0); // No direct send, even after the run would finish.
const reminder = f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx).message;
expect(reminder.customType).toBe("pi-goals-upkeep");
expect(reminder.content).toContain(f.path);
expect(reminder.content).not.toContain("first output");
expect(reminder.content).not.toContain("historical recap");
expect(f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx).message).toBeUndefined();
writeFileSync(f.path, f.plan.replace("first output", "refined output"));
f.hooks.get("turn_end")({}, f.ctx);
for (let i = 0; i < 7; i++) f.hooks.get("turn_end")({}, f.ctx);
expect(f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx).message).toBeUndefined();
await f.command("stop");
for (let i = 0; i < 10; i++) f.hooks.get("turn_end")({}, f.ctx);
expect(reminders()).toHaveLength(0);
});
it.each(["supervising", "solo"])("%s repeats upkeep every eight unchanged turns and rotates only delivered supervisor nudges", async mode => {
const f = fixture(); await f.draft();
if (mode === "solo") { f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped"); await f.command("solo"); }
else await f.command("ready");
const prepare = () => f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx);
prepare();
for (let i = 0; i < 9; i++) f.hooks.get("turn_end")({}, f.ctx);
f.hooks.get("session_compact")();
expect(prepare().message.customType).toBe("pi-goals-plan");
const sent = f.messages.length;
for (let round = 0; round <= upkeepNudges.length; round++) {
for (let turn = 0; turn < 7; turn++) f.hooks.get("turn_end")({}, f.ctx);
expect(prepare().message).toBeUndefined();
f.hooks.get("turn_end")({}, f.ctx);
expect(f.messages).toHaveLength(sent);
expect(prepare().message).toMatchObject({
customType: "pi-goals-upkeep",
content: upkeep(f.path, mode === "supervising" ? round : undefined),
});
expect(prepare().message).toBeUndefined();
}
});
it("extra subagent launches are recorded as helpers and never steal the implementation identity", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
f.launch({ id: "impl", sessionFile: "/tmp/impl.jsonl" });
expect(f.entries.at(-1).data).toMatchObject({ worker: { id: "impl", sessionFile: "/tmp/impl.jsonl" }, helpers: [] });
f.launch({ id: "reviewer", sessionFile: "/tmp/review.jsonl" }, "reviewer");
expect(f.entries.at(-1).data).toMatchObject({ worker: { id: "impl" }, helpers: [{ id: "reviewer", sessionFile: "/tmp/review.jsonl" }] });
// a repeated helper launch updates its record instead of duplicating it
f.launch({ id: "reviewer-2", sessionFile: "/tmp/review.jsonl" }, "reviewer");
expect(f.entries.at(-1).data.helpers).toEqual([{ id: "reviewer-2", sessionFile: "/tmp/review.jsonl" }]);
// resuming the worker keeps the binding and refreshes its id
f.launch({ id: "impl-2", sessionFile: "/tmp/impl.jsonl" }, "goals-worker", "subagent_resume");
expect(f.entries.at(-1).data).toMatchObject({ worker: { id: "impl-2", sessionFile: "/tmp/impl.jsonl" }, helpers: [{ id: "reviewer-2" }] });
});
it("pending call IDs survive concurrent launches until every execution ends", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
f.start("call-a");
f.start("call-b", { agent: "reviewer", title: "Review" });
f.finish("call-a", { id: "a", sessionFile: "/tmp/a.jsonl" });
f.finish("untracked", { id: "noise", sessionFile: "/tmp/noise.jsonl" });
f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped");
await f.command("solo");
expect(f.entries.at(-1).data.mode).toBe("supervising"); // one launch still pending
expect(f.ctx.notify ?? f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("still pending"), "warning");
f.finish("call-b", { id: "b", sessionFile: "/tmp/b.jsonl" });
f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped");
await f.command("solo");
expect(f.entries.at(-1).data.mode).toBe("solo");
expect(f.entries.at(-1).data).toMatchObject({ worker: { id: "a" }, helpers: [{ id: "b" }] });
});
it("a launch started during takeover invalidates the menu without disabling plan watching", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
let answer!: (choice: string) => void;
f.ctx.ui.select.mockImplementationOnce(() => new Promise(resolve => { answer = resolve; }));
const solo = f.command("solo");
f.launch({ id: "late-child", sessionFile: "/tmp/late.jsonl" });
answer("Worker confirmed stopped"); await solo;
expect(f.entries.at(-1).data.mode).toBe("supervising");
expect(f.entries.at(-1).data.workerStopped).toBe(false);
await f.atomicWrite(f.plan.replace("first output", "new requirement"));
await waitFor(() => f.changed() === 1);
});
it("changed plan or shutdown during takeover never grants solo permission", async () => {
const f = fixture(); await f.draft();
f.ctx.ui.select.mockImplementationOnce(async () => { writeFileSync(f.path, f.plan.replace("first", "changed")); return "Worker confirmed stopped"; });
await f.command("solo");
expect(f.entries.at(-1).data.mode).toBe("planning");
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("Plan changed during takeover"), "warning");
f.ctx.ui.select.mockImplementationOnce(async () => { f.shutdown(); return "Worker confirmed stopped"; });
await f.command("solo");
expect(f.entries.at(-1).data.mode).toBe("planning");
});
it("requires explicit supervisor ownership confirmation when attaching an existing plan", async () => {
const f = fixture(); const path = join(f.ctx.cwd, "shared.md");
writeFileSync(path, f.plan);
f.ctx.ui.select.mockResolvedValueOnce("Cancel");
await f.command(`attach ${path}`);
expect(f.entries).toHaveLength(0);
f.ctx.ui.select.mockResolvedValueOnce("Previous supervisor confirmed stopped");
await f.command(`attach ${path}`);
expect(f.entries.at(-1).data).toMatchObject({ mode: "planning", plan: path });
});
it("does not approve cancelled goals or display current completion for an unavailable plan", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
writeFileSync(f.path, "- [-] goal: cancelled output\n## Log\n");
writeFileSync(join(f.ctx.cwd, "evidence.log"), "verified\n");
const reply = await f.tools.get("CompleteGoal").execute("t", { goal: "cancelled output", evidence: ["evidence.log"], observation: "read" }, undefined, undefined, f.ctx);
expect(reply.content[0].text).toContain("no sign-off recorded");
expect(readFileSync(f.path, "utf8")).toContain("[-]");
rmSync(f.path);
f.hooks.get("agent_end")({}, f.ctx);
expect(f.ctx.ui.setWidget).toHaveBeenLastCalledWith("goals", [expect.stringContaining("unavailable")]);
});
it("keeps interactive workers open", () => {
const agent = readFileSync(new URL("../agents/goals-worker.md", import.meta.url), "utf8");
expect(agent).toContain("auto-exit: false");
});
it.each(["stop", "exit", "edit", "session_tree"])("discards pending upkeep after %s instead of reviving stale work", async change => {
const f = fixture(); await f.draft();
f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped"); await f.command("solo");
f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx);
for (let i = 0; i < 9; i++) f.hooks.get("turn_end")({}, f.ctx);
if (change === "edit") writeFileSync(f.path, f.plan.replace("first output", "changed requirement"));
else if (change === "session_tree") f.hooks.get("session_tree")({}, f.ctx);
else await f.command(change);
const prepared = f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx);
expect(prepared?.message?.customType).not.toBe("pi-goals-upkeep");
if (change === "stop") expect(prepared.systemPrompt).toContain("Goal work is paused");
if (change === "exit") expect(prepared).toBeUndefined();
expect(f.messages.filter(m => m.message.customType === "pi-goals-upkeep")).toHaveLength(0);
});
it("coalesces pending upkeep with a repaired post-compaction plan, retaining the user's latest requirements", async () => {
const f = fixture(); await f.draft();
f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped"); await f.command("solo");
f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx);
for (let i = 0; i < 9; i++) f.hooks.get("turn_end")({}, f.ctx);
f.hooks.get("session_compact")();
rmSync(f.path);
const unavailable = f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx);
expect(unavailable.message).toBeUndefined();
expect(unavailable.systemPrompt).toContain("unavailable");
const repaired = f.plan.replace("first output", "the human's latest exact result");
writeFileSync(f.path, repaired);
const ready = f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx);
expect(ready.message).toMatchObject({ customType: "pi-goals-plan" });
expect(ready.message.content).toContain("the human's latest exact result");
expect(readFileSync(f.path, "utf8")).toBe(repaired);
expect(ready.message.content).not.toContain("Plan upkeep:");
expect(f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx).message).toBeUndefined();
});
it("real SessionManager preserves historical state and restores draft authority before Ready", async () => {
const f = fixture();
const session = SessionManager.inMemory(f.ctx.cwd);
f.pi.appendEntry = (type: string, data: unknown) => { session.appendCustomEntry(type, data); return 0; };
f.ctx.sessionManager.getBranch = () => session.getBranch();
await f.draft();
const planned = session.getLeafEntry() as any;
await f.command("ready");
expect(planned.data.mode).toBe("planning");
expect((session.getLeafEntry() as any).data).not.toBe(planned.data);
session.branch(planned.id);
f.hooks.get("session_tree")({ newLeafId: planned.id }, f.ctx);
expect(f.start("after-tree")?.block).toBe(true);
expect(f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx).systemPrompt).toContain("Plan only in");
});
it("serializes CompleteGoal after a real built-in edit without losing either successful update", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
writeFileSync(join(f.ctx.cwd, "proof.log"), "PASS\n");
let reportRead!: () => void; const readStarted = new Promise<void>(resolve => { reportRead = resolve; });
let release!: () => void; const continueRead = new Promise<void>(resolve => { release = resolve; });
const edit = createEditTool(f.ctx.cwd, { operations: {
access: path => access(path),
readFile: async path => { const bytes = await readFile(path); reportRead(); await continueRead; return bytes; },
writeFile: (path, text) => writeFile(path, text, "utf8"),
} });
const editing = edit.execute("edit", { path: f.path, edits: [{ oldText: "# Plan", newText: "# Plan with progress note" }] });
await readStarted;
const completing = f.tools.get("CompleteGoal").execute("complete", { goal: "first output", evidence: ["proof.log"], observation: "Read PASS" }, undefined, undefined, f.ctx);
release();
await editing; await completing;
const text = readFileSync(f.path, "utf8");
expect(text).toContain("# Plan with progress note");
expect(text).toContain("- [x] goal: first output");
expect(text).toContain("Parent review:");
expect(f.entries.at(-1).data.signoffs["first output"]).toBeDefined();
});
it.each(["pause", "replace", "tree", "cancel"])("rejects queued completion after %s while waiting for a file mutation", async change => {
const f = fixture(); await f.draft(); await f.command("ready");
writeFileSync(join(f.ctx.cwd, "proof.log"), "PASS\n");
let entered!: () => void; const held = new Promise<void>(resolve => { entered = resolve; });
let release!: () => void; const wait = new Promise<void>(resolve => { release = resolve; });
const holding = withFileMutationQueue(f.path, async () => { entered(); await wait; });
await held;
const abort = new AbortController();
const completing = f.tools.get("CompleteGoal").execute("complete", { goal: "first output", evidence: ["proof.log"], observation: "Read PASS" }, abort.signal, undefined, f.ctx);
if (change === "pause") await f.command("stop");
if (change === "replace") { await f.command("exit"); await f.command("new different output"); }
if (change === "tree") f.hooks.get("session_tree")({}, f.ctx);
if (change === "cancel") abort.abort();
const current = f.entries.at(-1).data.plan;
const before = readFileSync(current, "utf8");
release(); await holding;
const response = await completing;
expect(response.content[0].text).not.toContain("Recorded parent judgment");
expect(readFileSync(f.path, "utf8")).toBe(f.plan);
expect(readFileSync(current, "utf8")).toBe(before);
expect(f.entries.at(-1).data.signoffs).toEqual({});
});
it.each([true, false])("solo stop/reload/resume preserves ownership with companion tools=%s", async tools => {
const f = fixture(); await f.draft();
if (!tools) f.pi.getAllTools.mockReturnValue([]);
f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped"); await f.command("solo");
await f.command("stop"); await f.command("stop");
f.hooks.get("session_start")({}, f.ctx);
expect(f.entries.at(-1).data.pausedFrom).toBe("solo");
await f.command("resume");
expect(f.entries.at(-1).data.mode).toBe("solo");
expect(f.start("forbidden")?.block).toBe(true);
});
it("rejects blank goal subjects on Ready and CompleteGoal", async () => {
const f = fixture(); await f.draft();
writeFileSync(f.path, "# Plan\n- [ ] goal: \n- [ ] goal: valid\n## Log\n");
await f.command("ready");
expect(f.entries.at(-1).data.mode).toBe("planning");
writeFileSync(f.path, f.plan); await f.command("ready");
writeFileSync(f.path, "# Plan\n- [ ] goal: \n## Log\n");
writeFileSync(join(f.ctx.cwd, "proof.log"), "PASS\n");
const before = readFileSync(f.path, "utf8");
await f.tools.get("CompleteGoal").execute("blank", { goal: " ", evidence: ["proof.log"], observation: "Read PASS" }, undefined, undefined, f.ctx);
expect(readFileSync(f.path, "utf8")).toBe(before);
expect(f.entries.at(-1).data.signoffs).toEqual({});
});
it.each(["denied", "cancelled"])("settles a %s preflight on execution_end without tool_result", async reason => {
const f = fixture(); await f.draft(); await f.command("ready");
f.start("refused");
f.finish("refused", { error: reason }, "subagent", true);
f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped"); await f.command("solo");
expect(f.entries.at(-1).data.mode).toBe("solo");
expect(f.entries.at(-1).data.worker).toBeUndefined();
});
it("batch results and a reviewer arriving first do not take the implementation binding", async () => {
const f = fixture(); await f.draft(); await f.command("ready");
f.launch({ id: "early", sessionFile: "/tmp/early.jsonl", agent: "reviewer" }, "reviewer");
expect(f.entries.at(-1).data.worker).toBeUndefined();
f.start("batch", { children: [{ agent: "reviewer", title: "Review" }, { agent: "goals-worker", title: "Implement" }] });
f.finish("batch", { status: "started", children: [
{ id: "review", sessionFile: "/tmp/review.jsonl", agent: "reviewer" },
{ id: "impl", sessionFile: "/tmp/impl.jsonl", agent: "goals-worker" },
] });
expect(f.entries.at(-1).data.worker).toEqual({ id: "impl", sessionFile: "/tmp/impl.jsonl" });
expect(f.entries.at(-1).data.helpers).toHaveLength(2);
await f.command("stop");
expect(f.messages.at(-1).message.content).toContain("impl");
});
it.each(["replace", "tree", "pause"])("does not attach a launch result after %s changed its originating generation", async change => {
const f = fixture(); await f.draft(); await f.command("ready");
f.start("old");
if (change === "replace") { await f.command("exit"); await f.command("new next output"); }
if (change === "tree") f.hooks.get("session_tree")({}, f.ctx);
if (change === "pause") await f.command("stop");
f.finish("old", { id: "old-worker", sessionFile: "/tmp/old-worker.jsonl", agent: "goals-worker" });
expect(f.entries.at(-1).data.worker).toBeUndefined();
expect(f.entries.at(-1).data.helpers).toEqual([]);
});
it.each([
["criterion", false], ["scope", false], ["other goal", true], ["task", true], ["evidence", true], ["Log", true],
])("%s edits retain signoff=%s according to reviewed acceptance", async (change, retained) => {
const f = fixture(); await f.draft(); await f.command("ready");
writeFileSync(f.path, f.plan.replace("# Plan", "# Plan\nShared scope: exact bytes").replace("goal: first output\n", "goal: first output\n - discriminator: original criterion\n - tasks:\n - [ ] original task\n - evidence: original evidence\n"));
writeFileSync(join(f.ctx.cwd, "proof.log"), "PASS\n");
await f.tools.get("CompleteGoal").execute("complete", { goal: "first output", evidence: ["proof.log"], observation: "Read PASS" }, undefined, undefined, f.ctx);
const signed = readFileSync(f.path, "utf8");
const replacements: Record<string, [string, string]> = {
criterion: ["original criterion", "new criterion"], scope: ["exact bytes", "two files"], "other goal": ["second output", "new second output"],
task: ["[ ] original task", "[x] maintained task"], evidence: ["original evidence", "additional evidence"], Log: ["## Log", "## Log\n- historical note"],
};
writeFileSync(f.path, signed.replace(...replacements[change as string]));
f.hooks.get("agent_end")({}, f.ctx);
expect(Boolean(f.entries.at(-1).data.signoffs["first output"])).toBe(retained);
f.hooks.get("session_start")({}, f.ctx);
expect(Boolean(f.entries.at(-1).data.signoffs["first output"])).toBe(retained);
});
it("passive pause is visible immediately while its model notice waits safely for the next prompt", async () => {
const f = fixture(); await f.draft();
f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped"); await f.command("solo");
await f.command("stop");
expect(f.messages.at(-1).options).toEqual({ deliverAs: "nextTurn" });
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("Remote stop is NOT yet confirmed"), "info");
});