mirror of
https://github.com/wassname/pi-plan.git
synced 2026-09-26 14:10:23 +08:00
Append a UI-only saved entry after supervisor or solo final updates; omit short unchanged waits. Remove the prompt instructions to sample fortunes, and cover the non-context renderer and lifecycle. Co-Authored-By: PI/OpenAI <288921227+claudypoo@users.noreply.github.com>
1581 lines
105 KiB
TypeScript
1581 lines
105 KiB
TypeScript
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
||
import { access, readFile, writeFile } from "node:fs/promises";
|
||
import { tmpdir } from "node:os";
|
||
import { basename, join, relative } from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
import { createEditTool, type ExtensionAPI, initTheme, SessionManager, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
||
import { visibleWidth } from "@earendil-works/pi-tui";
|
||
import { openProjectPane } from "pi-subagents/project-panes";
|
||
import { afterEach, expect, it, vi } from "vitest";
|
||
import goalsExtension from "../src/index.js";
|
||
import { buildWorkerView } from "../src/worker-view.js";
|
||
|
||
vi.mock("pi-subagents/project-panes", () => ({ openProjectPane: vi.fn(async () => ({ ok: true, data: { bindingPath: "/project/.pi/subagents/project-pane.json", disposition: "opened", binding: { paneId: "native-pane", projectRoot: "/project", command: "pi" } } })) }));
|
||
|
||
const roots: string[] = [];
|
||
const shutdowns: Array<() => void> = [];
|
||
afterEach(() => { for (const shutdown of shutdowns.splice(0)) shutdown(); vi.unstubAllEnvs(); vi.mocked(openProjectPane).mockClear(); 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) {
|
||
const cwd = mkdtempSync(join(tmpdir(), "goals-main-test-")); roots.push(cwd);
|
||
const entries: any[] = child ? [{ type: "custom", customType: "pi-goals-main-supervisor-v1", data: { mode: "solo", child: true } }] : []; const hooks = new Map<string, any>(); const commands = new Map<string, any>(); const tools = new Map<string, any>();
|
||
const messages: any[] = [];
|
||
const ctx = { cwd, isIdle: vi.fn(() => true), sessionManager: { getBranch: () => entries, getSessionId: () => "copy-only", getSessionFile: () => join(cwd, "session.jsonl"), getLeafId: () => "reviewed-leaf", getHeader: () => ({ id: "copy-only", cwd }) }, hasUI: true, hasPendingMessages: vi.fn(() => false), ui: {
|
||
getEditorText: vi.fn(() => ""), 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(),
|
||
} };
|
||
let registration: any;
|
||
const channel = { snapshot: vi.fn(() => ({ connected: true, supported: true })), listSessions: vi.fn(async () => [{ id: "parent-intercom", pid: process.pid }, { id: "live-parent", pid: process.pid + 1 }]), publish: 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),
|
||
registerMarkdownTransformer: vi.fn(),
|
||
registerEntryRenderer: vi.fn(),
|
||
registerMessageRenderer: vi.fn(),
|
||
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((name, data) => { if (name === "intercom:extension-register") { registration = data; data.onReady(channel); } }) },
|
||
getAllTools: vi.fn((): any[] => []),
|
||
getCommands: vi.fn((): any[] => [...commands.keys()].map(name => ({name}))),
|
||
};
|
||
goalsExtension(pi as unknown as ExtensionAPI);
|
||
hooks.get("session_start")({}, ctx);
|
||
let path = "";
|
||
const command = async (value: string) => {
|
||
await commands.get("goals").handler(value, ctx);
|
||
const planDir = join(cwd, ".pi", "plan");
|
||
if (!path && existsSync(planDir)) {
|
||
const firstPlan = readdirSync(planDir).find(name => name.endsWith(".md"));
|
||
if (firstPlan) path = join(planDir, firstPlan);
|
||
}
|
||
};
|
||
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 = (_id: string) => hooks.get("tool_call")({ toolName: "OpenGoalWorker" }, ctx);
|
||
const launch = async (details: { id: string; sessionFile: string; task?: string; cwd?: string }) => {
|
||
await tools.get("OpenGoalWorker").execute("open", { task: details.task ?? "Implement first output", cwd: details.cwd }, undefined, undefined, ctx);
|
||
const state = entries.at(-1).data;
|
||
registration.onEvent({ type: "message", fromSessionId: details.id, payload: { type: "attached", to: state.worker.parentId, requestId: state.worker.requestId, plan: state.plan, sessionFile: details.sessionFile } });
|
||
};
|
||
return { ctx, pi, hooks, tools, commands, messages, command, get path() { return path; }, plan, draft, shutdown, changed, atomicWrite, get entries() { return entries.filter(entry => entry.customType === "pi-goals-main-supervisor-v1"); }, start, launch, channel, event: (event: any) => registration.onEvent(event) };
|
||
}
|
||
|
||
it("saves an automatic non-context fortune after a substantive supervisor update, not an unchanged wait", async () => {
|
||
const f = fixture(); await f.draft(); await f.command("ready");
|
||
const entries = f.ctx.sessionManager.getBranch();
|
||
const complete = (text: string) => f.hooks.get("agent_end")({ messages: [{ role: "assistant", stopReason: "stop", content: [{ type: "text", text }] }] }, f.ctx);
|
||
complete("The held-out prediction beat the matched shuffled control on the same examples; next I will inspect error cases.");
|
||
const fortune = entries.filter(entry => entry.customType === "pi-goals-fortune");
|
||
expect(fortune).toHaveLength(1);
|
||
expect(fortune[0].type).toBe("custom");
|
||
expect(fortune[0].data.content).toMatch(/ -- /);
|
||
complete("No change: waiting on the followed job.");
|
||
expect(entries.filter(entry => entry.customType === "pi-goals-fortune")).toHaveLength(1);
|
||
});
|
||
|
||
it("shows incremental VCC Markdown without raw tool results or compaction dumps", async () => {
|
||
initTheme("dark");
|
||
const f = fixture(true), history = f.ctx.sessionManager.getBranch(), timestamp = new Date().toISOString();
|
||
const entry = (id: string, message: object) => ({ type: "message", id, timestamp, parentId: null, message });
|
||
history.push(entry("kept", { role: "user", content: "Retained heading requirement" }),
|
||
{ type: "compaction", id: "checkpoint", timestamp, firstKeptEntryId: "kept", summary: "COMPACTION_DUMP_MUST_STAY_HIDDEN" },
|
||
entry("failed-call", { role: "assistant", content: [{ type: "thinking", thinking: "PRIVATE_REASONING_SENTINEL" }, { type: "toolCall", id: "read-1", name: "read", arguments: { path: "missing.txt" } }] }),
|
||
entry("failed-result", { role: "toolResult", toolCallId: "read-1", toolName: "read", isError: true, content: [{ type: "text", text: `Permission denied while reading missing.txt. ${"x".repeat(500)} RESULT_TAIL_MUST_STAY_HIDDEN` }] }),
|
||
entry("pending-call", { role: "assistant", content: [{ type: "toolCall", id: "job-1", name: "process", arguments: { action: "start", command: "long job", notify: { onSuccess: "turn" }, nested: Array(20).fill({ payload: "x".repeat(100_000) }) } }] }));
|
||
const published = f.channel.publish.mock.calls.length;
|
||
const tool = f.tools.get("worker_view"), output = await tool.execute("view", {}, undefined, undefined, f.ctx), text = output.content[0].text;
|
||
expect(Buffer.byteLength(text)).toBeLessThanOrEqual(8_000);
|
||
expect(text).toContain("### VCC summary of new turns");
|
||
expect(text).toContain("* read \"missing.txt\"");
|
||
expect(text).toContain("unanswered tool calls: process");
|
||
expect(text).toContain("read failed: Permission denied while reading missing.txt");
|
||
for (const hidden of ["RESULT_TAIL_MUST_STAY_HIDDEN", "COMPACTION_DUMP_MUST_STAY_HIDDEN", "PRIVATE_REASONING_SENTINEL", "onSuccess", "Recent calls and results", "vcc_recall"]) expect(text).not.toContain(hidden);
|
||
expect(f.channel.publish).toHaveBeenCalledTimes(published);
|
||
for (const width of [40, 80]) {
|
||
const collapsed = tool.renderResult(output, { expanded: false }).render(width), expanded = tool.renderResult(output, { expanded: true }).render(width);
|
||
expect(expanded.length).toBeGreaterThan(collapsed.length);
|
||
for (const line of expanded) expect(visibleWidth(line)).toBeLessThanOrEqual(width);
|
||
}
|
||
history.push(
|
||
entry("late-result", { role: "toolResult", toolCallId: "job-1", toolName: "process", isError: false, content: [{ type: "text", text: "Process 1820 exited successfully with saved output." }] }),
|
||
entry("new-work", { role: "assistant", content: [{ type: "text", text: "Implemented the correction." }, { type: "toolCall", id: "edit-1", name: "edit", arguments: { path: "src/a.ts" } }] }));
|
||
const next = (await tool.execute("view-again", {}, undefined, undefined, f.ctx)).content[0].text;
|
||
expect(next).toContain("Implemented the correction");
|
||
expect(next).toContain("src/a.ts");
|
||
expect(next).toContain("process returned: Process 1820 exited successfully");
|
||
expect(next.split("### VCC summary of new turns")[1]).not.toContain("missing.txt");
|
||
history.push({ type: "compaction", id: "later-checkpoint", firstKeptEntryId: "pending-call", timestamp, summary: "SECOND_COMPACTION_DUMP_MUST_STAY_HIDDEN" });
|
||
for (let i = 0; i < 12; i++) history.push(entry(`post-compaction-${i}`, { role: "assistant", content: [{ type: "text", text: `POST_COMPACTION_TURN_${i}` }] }));
|
||
const afterCompaction = (await tool.execute("after-compaction", {}, undefined, undefined, f.ctx)).content[0].text;
|
||
expect(afterCompaction).toContain("unanswered tool calls: edit"); // retained process call has its result; retained edit is still unanswered
|
||
expect(afterCompaction).toContain("earlier unanswered calls are unknown");
|
||
expect(afterCompaction).toContain("POST_COMPACTION_TURN_0");
|
||
expect(afterCompaction).toContain("POST_COMPACTION_TURN_11");
|
||
expect(afterCompaction).not.toContain("SECOND_COMPACTION_DUMP_MUST_STAY_HIDDEN");
|
||
const diagnostic = (await tool.execute("diagnostic", { detail: "diagnostic" }, undefined, undefined, f.ctx)).content[0].text;
|
||
expect(diagnostic).toContain("### Diagnostics");
|
||
expect(diagnostic).toContain("Saved session:");
|
||
expect(diagnostic).not.toContain("RESULT_TAIL_MUST_STAY_HIDDEN");
|
||
});
|
||
|
||
it("paginates oversized worker history without advancing past omitted turns", () => {
|
||
const timestamp = new Date().toISOString();
|
||
const entries = Array.from({ length: 48 }, (_, index) => ({
|
||
type: "message" as const, id: `large-${index}`, parentId: index ? `large-${index - 1}` : null, timestamp,
|
||
message: { role: "assistant" as const, content: [{ type: "text" as const, text: `TURN_${index} ${String(index).repeat(900)}` }], stopReason: "stop" as const, timestamp: Date.now() },
|
||
}));
|
||
entries.push({ type: "message", id: "provider-failure", parentId: "large-47", timestamp, message: { role: "assistant", content: [], stopReason: "error", errorMessage: "429: provider quota exhausted", timestamp: Date.now() } } as any);
|
||
const runtime = { connected: false, processes: [{ pid: 7, ppid: 1, command: "node", args: "node /opt/pi-coding-agent/dist/cli.js" }] };
|
||
let view = buildWorkerView(entries as any, "/tmp/worker.jsonl", "large history", runtime);
|
||
expect(view.text).toContain("newer saved turns remain");
|
||
expect(view.text).toContain("429: provider quota exhausted");
|
||
expect(view.text).toContain("Status: disconnected");
|
||
expect(view.text).toContain("1 probable child Pi process");
|
||
expect(view.cursor.through).not.toBe("large-47");
|
||
const first = view.cursor.through;
|
||
for (let page = 0; page < 64 && view.cursor.through !== "provider-failure"; page++) view = buildWorkerView(entries as any, "/tmp/worker.jsonl", "large history", runtime, view.cursor);
|
||
expect(view.cursor.through).toBe("provider-failure");
|
||
view = buildWorkerView(entries as any, "/tmp/worker.jsonl", "large history", runtime, view.cursor);
|
||
expect(view.text).toContain("429: provider quota exhausted");
|
||
expect(Buffer.byteLength(view.text)).toBeLessThanOrEqual(8_000);
|
||
expect(view.cursor.through).not.toBe(first);
|
||
});
|
||
|
||
it.each([
|
||
["chat", ["new", "attach", "help", "quit"], undefined],
|
||
["planning", ["edit", "discuss", "ready", "model", "help", "quit"], "📝 planning"],
|
||
["supervising", ["review", "stop", "model", "help", "quit"], "👀 supervising"],
|
||
["paused", ["resume", "model", "help", "quit"], "⏸ paused"],
|
||
["solo", ["stop", "help", "quit"], "🛠 solo"],
|
||
])("shows applicable %s actions and stage without starting work", async (mode, expected, badge) => {
|
||
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"); }
|
||
await f.command("status");
|
||
expect(f.ctx.ui.setStatus).toHaveBeenLastCalledWith("goals", badge ? `${badge} 0/2 goals` : undefined);
|
||
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 a worker launch", async (route) => {
|
||
const f = fixture();
|
||
f.ctx.ui.select.mockResolvedValueOnce("new — New plan…");
|
||
f.ctx.ui.editor.mockResolvedValueOnce("supplied instructions");
|
||
await f.command(route === "menu" ? "" : "new");
|
||
expect(f.entries.at(-1).data.mode).toBe("planning");
|
||
expect(f.ctx.ui.editor).toHaveBeenCalledTimes(route === "menu" ? 1 : 0);
|
||
expect(f.messages).toHaveLength(1);
|
||
expect(f.messages[0].message.content).toContain(route === "menu" ? "Initial idea: supplied instructions" : "Use the existing conversation");
|
||
expect(f.hooks.get("tool_call")({ toolName: "subagent" }).block).toBe(true);
|
||
expect(f.hooks.get("tool_call")({ toolName: "subagent", input: { action: "status" } })).toBeUndefined();
|
||
});
|
||
|
||
it("cancelled menu New creates nothing and sends nothing", async () => {
|
||
const f = fixture();
|
||
f.ctx.ui.select.mockResolvedValueOnce("new — New plan…");
|
||
await f.command(""); // editor returns undefined on Cancel
|
||
expect(f.entries).toHaveLength(0); expect(f.messages).toHaveLength(0);
|
||
expect(existsSync(join(f.ctx.cwd, ".pi/plan"))).toBe(false);
|
||
});
|
||
|
||
it("new names use six session characters, skip deletion holes and suffix collisions, and preserve old files", async () => {
|
||
const f = fixture(); f.ctx.sessionManager.getSessionId = () => "first-abc123";
|
||
const directory = join(f.ctx.cwd, ".pi/plan"); mkdirSync(directory, { recursive: true });
|
||
const old = ["2026-09-14-000000Z-descriptive-plan-v1.md", "abc123-v1.md", "abc123-v2.md", "abc123-v10.md"];
|
||
for (const name of old) writeFileSync(join(directory, name), name);
|
||
rmSync(join(directory, "abc123-v2.md"));
|
||
await f.command("new Preserve the descriptive title");
|
||
const first = f.entries.at(-1).data.plan;
|
||
expect(basename(first)).toBe("abc123-v11.md");
|
||
expect(readFileSync(first, "utf8")).toContain("# Preserve the descriptive title\n");
|
||
f.ctx.sessionManager.getSessionId = () => "another-abc123";
|
||
await f.command("new Different session with same suffix");
|
||
expect(basename(f.entries.at(-1).data.plan)).toBe("abc123-v12.md");
|
||
expect(readFileSync(first, "utf8")).toContain("# Preserve the descriptive title\n");
|
||
for (const name of old.filter(name => name !== "abc123-v2.md")) expect(readFileSync(join(directory, name), "utf8")).toBe(name);
|
||
expect(readdirSync(directory)).toHaveLength(5);
|
||
});
|
||
|
||
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 preserves the plan without a backup, warns for misbound jobs and allows a separate new draft", async () => {
|
||
const f = fixture(); await f.draft(); await f.command("ready");
|
||
await f.launch({ id: "stale", sessionFile: "/tmp/old-worker.jsonl" });
|
||
const jobs = [
|
||
{ id: "owned", name: "goals-copy-only", action: "prompt", scope: "session", sessionFile: f.ctx.sessionManager.getSessionFile() },
|
||
{ id: "older", name: "older-plan", action: "prompt", scope: "session", sessionFile: f.ctx.sessionManager.getSessionFile() },
|
||
{ id: "foreign", name: "goals-copy-only", action: "prompt", scope: "session", sessionFile: "/other.jsonl" },
|
||
{ id: "unbound", name: "goals-copy-only", action: "prompt", scope: "global" },
|
||
];
|
||
const sourceInfo = { path: fileURLToPath(import.meta.resolve("@jl1990/pi-scheduler/extensions/scheduler/index.ts")) };
|
||
f.pi.getCommands.mockReturnValue([...["schedules", "schedule-remove"].map(name => ({ name, source: "extension", sourceInfo: { path: "/foreign/index.ts" } })), ...["schedules:2", "schedule-remove:2"].map(name => ({ name, source: "extension", sourceInfo }))]);
|
||
const branch = f.ctx.sessionManager.getBranch();
|
||
branch.push({ type: "custom_message", id: "reviewed-leaf", customType: "scheduled-task", details: { includeAll: true, tasks: jobs } });
|
||
writeFileSync(f.ctx.sessionManager.getSessionFile(), "persisted fixture\n");
|
||
const before = f.messages.length; await f.command("clear");
|
||
expect(f.entries.at(-1).data).toEqual({ mode: "chat" });
|
||
expect(f.messages.slice(before)).toEqual([{ message: { content: "/schedules:2 all" }, options: { expandPromptTemplates: true, deliverAs: "followUp" }, savedPrompt: true }]);
|
||
// Passive command results append public entries and persist, without message_end.
|
||
branch.push({ type: "custom_message", id: "fresh-list", customType: "scheduled-task", details: { includeAll: true, tasks: jobs } });
|
||
writeFileSync(f.ctx.sessionManager.getSessionFile(), "new persistence signal\n");
|
||
await waitFor(() => f.messages.length === before + 2);
|
||
expect(f.messages.at(-1)).toMatchObject({ message: { content: "/schedule-remove:2 owned" }, options: { expandPromptTemplates: true } });
|
||
expect(f.messages).toHaveLength(before + 2);
|
||
expect(f.ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("missing/different session scope"), "warning");
|
||
const wake = (id: string) => ({ source: "extension", text: `[Scheduled task ${id} fired]\nName: goals-copy-only\nAction: prompt\nType: interval\n` });
|
||
expect(f.hooks.get("input")(wake("owned"), f.ctx)).toEqual({ action: "handled" });
|
||
expect(f.hooks.get("input")(wake("foreign"), f.ctx)).toBeUndefined();
|
||
expect(f.hooks.get("input")({ ...wake("owned"), source: "interactive" }, f.ctx)).toBeUndefined();
|
||
const directory = join(f.ctx.cwd, ".pi/plan");
|
||
expect(readdirSync(directory)).toEqual([basename(f.path)]);
|
||
expect(readFileSync(f.path, "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);
|
||
expect(readdirSync(directory)).toHaveLength(2);
|
||
expect(f.messages).toHaveLength(before + 3); // Two scheduler commands, then New's normal planning turn.
|
||
});
|
||
|
||
it.each(["missing", "timeout", "busy-timeout", "non-agent-busy", "cancelled"])("clear fails closed for %s session observation", async kind => {
|
||
const f = fixture();
|
||
const sourceInfo = { path: fileURLToPath(import.meta.resolve("@jl1990/pi-scheduler/extensions/scheduler/index.ts")) };
|
||
f.pi.getCommands.mockReturnValue(["schedules", "schedule-remove"].map(name => ({ name, source: "extension", sourceInfo })));
|
||
const branch = f.ctx.sessionManager.getBranch();
|
||
branch.push({ type: "custom", id: "reviewed-leaf" });
|
||
if (kind !== "missing") writeFileSync(f.ctx.sessionManager.getSessionFile(), "persisted");
|
||
if (kind === "busy-timeout" || kind === "non-agent-busy") f.ctx.isIdle.mockReturnValue(false);
|
||
vi.useFakeTimers();
|
||
try {
|
||
await f.command("clear");
|
||
if (kind === "busy-timeout") {
|
||
// A run beginning after dispatch must suspend the already-armed deadline.
|
||
f.hooks.get("agent_start")({}, f.ctx);
|
||
await vi.advanceTimersByTimeAsync(6_000);
|
||
expect(f.ctx.ui.notify).not.toHaveBeenCalledWith(expect.stringContaining("removal unconfirmed"), "warning");
|
||
f.ctx.isIdle.mockReturnValue(true); await f.hooks.get("agent_settled")({}, f.ctx);
|
||
}
|
||
if (kind === "cancelled") f.shutdown();
|
||
else await vi.advanceTimersByTimeAsync(5_000);
|
||
} finally { vi.useRealTimers(); }
|
||
branch.push({ type: "custom_message", id: "late-list", customType: "scheduled-task", details: { includeAll: true, tasks: [{ id: "owned", name: "goals-copy-only", action: "prompt", scope: "session", sessionFile: f.ctx.sessionManager.getSessionFile() }] } });
|
||
writeFileSync(f.ctx.sessionManager.getSessionFile(), "late persistence");
|
||
await delay(20);
|
||
expect(f.messages.map(message => message.message.content)).toEqual(kind === "missing" ? [] : ["/schedules all"]);
|
||
if (kind !== "cancelled") expect(f.ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("removal unconfirmed"), "warning");
|
||
});
|
||
|
||
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" });
|
||
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(); const sent = f.messages.length;
|
||
f.ctx.ui.select.mockResolvedValueOnce("discuss — Discuss changes to the plan"); await f.command("");
|
||
expect(f.messages).toHaveLength(sent);
|
||
expect(readFileSync(f.path, "utf8")).toBe(f.plan);
|
||
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("keeps provisional drafts and interview updates separate from intentional acceptance", async () => {
|
||
const f = fixture(); await f.draft();
|
||
const review = async () => {
|
||
await f.tools.get("RequestPlanReview").execute("review", {}, undefined, undefined, f.ctx);
|
||
await f.hooks.get("agent_settled")({}, f.ctx);
|
||
};
|
||
for (const text of [f.plan, f.plan + "## Interview\nTODO: consequential choice unanswered.\n", f.plan.replace("first output", "revised output")]) {
|
||
writeFileSync(f.path, text);
|
||
await f.hooks.get("agent_settled")({}, f.ctx);
|
||
}
|
||
for (const stopReason of ["aborted", "error"]) {
|
||
await f.tools.get("RequestPlanReview").execute("cancelled", {}, undefined, undefined, f.ctx);
|
||
f.hooks.get("agent_end")({ messages: [{ role: "assistant", content: [], stopReason }] }, f.ctx);
|
||
await f.hooks.get("agent_settled")({}, f.ctx);
|
||
}
|
||
const interrupted = new AbortController();
|
||
await f.tools.get("RequestPlanReview").execute("interrupted", {}, interrupted.signal, undefined, f.ctx);
|
||
interrupted.abort(); // can arrive after the tool returned, without a new assistant error
|
||
await f.hooks.get("agent_settled")({}, f.ctx);
|
||
await f.tools.get("RequestPlanReview").execute("superseded", {}, undefined, undefined, f.ctx);
|
||
f.hooks.get("input")({ source: "interactive", text: "Wait, another question" }, f.ctx);
|
||
await f.hooks.get("agent_settled")({}, f.ctx);
|
||
await f.tools.get("RequestPlanReview").execute("discussed", {}, undefined, undefined, f.ctx);
|
||
await f.command("discuss");
|
||
await f.hooks.get("agent_settled")({}, f.ctx);
|
||
await f.tools.get("RequestPlanReview").execute("stale", {}, undefined, undefined, f.ctx);
|
||
writeFileSync(f.path, f.plan + "\n## Interview\nNew unresolved choice.\n");
|
||
await f.hooks.get("agent_settled")({}, f.ctx);
|
||
expect(f.ctx.ui.select).not.toHaveBeenCalled();
|
||
expect(f.messages.some(m => m.message.customType === "goal-plan-proposal")).toBe(false);
|
||
f.ctx.ui.select.mockResolvedValueOnce("Discuss"); await review();
|
||
expect(f.entries.at(-1).data.mode).toBe("planning");
|
||
expect(f.messages.some(m => m.message.customType === "goal-plan-proposal")).toBe(true);
|
||
await f.hooks.get("agent_settled")({}, f.ctx);
|
||
expect(f.ctx.ui.select).toHaveBeenCalledTimes(1);
|
||
f.ctx.ui.select.mockResolvedValueOnce("Ready"); await review();
|
||
expect(f.entries.at(-1).data.mode).toBe("supervising");
|
||
const child = fixture(true);
|
||
await child.tools.get("RequestPlanReview").execute("child", {}, undefined, undefined, 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");
|
||
await f.command("review");
|
||
expect(f.messages.at(-1)).toMatchObject({ message: { customType: "pi-goals-prompt", display: true }, options: { triggerTurn: true, deliverAs: "followUp" } });
|
||
expect(f.messages.at(-1).savedPrompt).toBeUndefined();
|
||
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("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");
|
||
await 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: "OpenGoalWorker" }).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");
|
||
const params = { goal: subject === "duplicate" || subject === "historical" ? "first output" : subject, evidence: [evidence], observation: "Read actual output" };
|
||
const first = await f.tools.get("CompleteGoal").execute("c", params, undefined, undefined, f.ctx);
|
||
if (first.content[0].text.includes("Final review queued")) {
|
||
f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx);
|
||
await f.tools.get("CompleteGoal").execute("c", params, undefined, undefined, f.ctx);
|
||
}
|
||
const after = readFileSync(f.path, "utf8");
|
||
if (subject === "renamed output" || subject === "duplicate") expect(after).toBe(before);
|
||
else { expect(after).toContain("- [✓] 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 reviewed markers through same-context restoration", 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"]);
|
||
await f.command(`attach ${f.path}`);
|
||
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", "👀 supervising 1/2 goals");
|
||
expect(f.ctx.ui.setWidget.mock.lastCall?.[1]).toContain("✓ G1: first output");
|
||
expect(f.ctx.ui.setWidget.mock.lastCall?.[1]).toContain("x G2: second output");
|
||
f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx);
|
||
for (let i = 0; i < 9; i++) f.hooks.get("turn_end")({}, f.ctx);
|
||
const reminder = f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx).message.content;
|
||
expect(reminder).toContain("[x] goal: second output");
|
||
expect(reminder).not.toContain("first output");
|
||
writeFileSync(f.path, readFileSync(f.path, "utf8").replace("[✓] goal: first", "[ ] goal: first"));
|
||
f.hooks.get("agent_end")({ messages: [] }, f.ctx);
|
||
expect(f.ctx.ui.setStatus).toHaveBeenLastCalledWith("goals", "👀 supervising 0/2 goals");
|
||
writeFileSync(f.path, readFileSync(f.path, "utf8").replace("[ ] goal: first", "[x] goal: first"));
|
||
f.hooks.get("agent_end")({ messages: [] }, f.ctx);
|
||
expect(f.ctx.ui.setStatus).toHaveBeenLastCalledWith("goals", "👀 supervising 0/2 goals");
|
||
f.shutdown();
|
||
});
|
||
|
||
it("reviews a plan replaced atomically with a current-file notice, and ignores writes that keep the same content", async () => {
|
||
const f = fixture(); await f.draft();
|
||
const plan = `# Context title
|
||
|
||
A short introduction for ordinary reminders.
|
||
|
||
## User-visible result
|
||
A visible artifact.
|
||
|
||
## User voice
|
||
- > "The full requirement must survive resync."
|
||
|
||
## Goals
|
||
- [ ] goal: produce the artifact
|
||
- tasks:
|
||
- [ ] run the detailed check
|
||
|
||
## Log
|
||
old progress`;
|
||
writeFileSync(f.path, plan); await f.command("ready");
|
||
const revised = plan.replace("A visible artifact.", "A revised visible artifact.");
|
||
await f.atomicWrite(revised);
|
||
await waitFor(() => f.changed() === 1);
|
||
const review = f.messages.find((m) => m.message.content.includes("Plan changed"))?.message.content;
|
||
expect(review).toContain(f.path);
|
||
expect(review).not.toContain("The full requirement must survive resync.");
|
||
expect(review).not.toContain("run the detailed check");
|
||
f.hooks.get("message_end")({ message: { role: "user", content: review } });
|
||
await f.atomicWrite(revised.replace("A revised", "A second revised"));
|
||
await waitFor(() => f.changed() === 2);
|
||
// Rewriting identical bytes must not retrigger the review event hook.
|
||
const same = revised.replace("A revised", "A second revised");
|
||
writeFileSync(f.path, same); await delay(300);
|
||
expect(f.changed()).toBe(2);
|
||
f.shutdown();
|
||
});
|
||
|
||
it("records task and evidence bookkeeping without waking, but wakes for goal status", async () => {
|
||
const f = fixture(); await f.draft();
|
||
const plan = `# Plan
|
||
## Goals
|
||
- [ ] goal: first output
|
||
- discriminator: output exists
|
||
- tasks:
|
||
- [ ] run it
|
||
- evidence:
|
||
- old.log
|
||
- [ ] goal: second output
|
||
|
||
## Log
|
||
`;
|
||
writeFileSync(f.path, plan); await f.command("ready");
|
||
await f.atomicWrite(plan.replace("- [ ] run it", "- [x] run it").replace("old.log", "new.log"));
|
||
await waitFor(() => f.messages.some(m => m.message?.content?.includes("[pi-goals: plan activity]")));
|
||
const activity = f.messages.find(m => m.message?.content?.includes("[pi-goals: plan activity]"));
|
||
expect(activity.options).toEqual({ deliverAs: "nextTurn" });
|
||
expect(activity.message.content).toContain("Recorded without waking the supervisor");
|
||
expect(f.changed()).toBe(0);
|
||
await f.atomicWrite(readFileSync(f.path, "utf8").replace("[ ] goal: first", "[/] goal: first"));
|
||
await waitFor(() => f.changed() === 1);
|
||
expect(f.messages.find(m => m.message?.content?.includes("Plan changed: requirements or goal status changed"))?.options).toMatchObject({ deliverAs: "followUp" });
|
||
f.shutdown();
|
||
});
|
||
|
||
it("delivers changed plans while coalescing only its own pending notice", async () => {
|
||
const f = fixture(); await f.draft(); await f.command("ready");
|
||
f.ctx.hasPendingMessages.mockReturnValue(true); // An unrelated queued prompt must not suppress the notice.
|
||
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);
|
||
f.hooks.get("message_end")({ message: { role: "user", content: "unrelated input" } });
|
||
await f.atomicWrite(f.plan.replace("## Log", "- discriminator: later queued edit\n## Log"));
|
||
await delay(200);
|
||
expect(f.changed()).toBe(1);
|
||
f.hooks.get("message_end")({ message: { role: "user", content: f.messages.at(-1).message.content } });
|
||
await f.atomicWrite(f.plan.replace("## Log", "- discriminator: after same-run delivery\n## Log"));
|
||
await waitFor(() => f.changed() === 2);
|
||
expect(f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx).message.content).toContain("after same-run delivery");
|
||
f.hooks.get("message_end")({ message: { role: "user", content: f.messages.at(-1).message.content } });
|
||
await f.atomicWrite(f.plan.replaceAll("[ ] goal:", "[-] goal:"));
|
||
await waitFor(() => f.changed() === 3); // Cancelling the last goals must still notify an ongoing run.
|
||
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");
|
||
const task = { id: "owned", name: "goals-copy-only", action: "prompt", scope: "session", sessionFile: f.ctx.sessionManager.getSessionFile(), disabledAt: "2026-09-14T01:00:00Z" };
|
||
f.hooks.get("tool_result")({ toolName: "manage_scheduled_task", input: { action: "disable" }, details: { task } }, f.ctx);
|
||
f.shutdown(); f.hooks.get("session_start")({}, f.ctx);
|
||
await f.command("resume");
|
||
expect(f.messages.at(-1).message.content).toContain(task.disabledAt);
|
||
expect(f.messages.at(-1).message.content).toContain("Leave later human edits unchanged");
|
||
expect(f.hooks.get("tool_call")({ toolName: "schedule_task", input: { name: task.name, action: "prompt", type: "interval", scope: "session", prompt: "custom\n indentation" } }, f.ctx).block).toBe(true);
|
||
f.ctx.sessionManager.getBranch().push({ type: "message", message: { role: "toolResult", toolName: "schedule_task", details: { task } } });
|
||
expect(f.hooks.get("tool_call")({ toolName: "manage_scheduled_task", input: { action: "update", id: "own", prompt: "custom\n indentation" } }, f.ctx).block).toBe(true);
|
||
const before = f.messages.length;
|
||
await f.command("exit");
|
||
expect(f.messages).toHaveLength(before);
|
||
expect(f.entries.at(-1).data).toEqual({ mode: "chat" });
|
||
});
|
||
|
||
it("requires a full-plan review turn before recording the final goal", async () => {
|
||
const f = fixture(); await f.draft();
|
||
const plan = `# Final review fixture
|
||
- [ ] goal: first output
|
||
- discriminator: first output has exact saved bytes
|
||
- [ ] goal: second output
|
||
- discriminator: second output has exact saved bytes
|
||
|
||
## Log
|
||
- worker evidence: keep this history in the final review`;
|
||
writeFileSync(f.path, plan); await f.command("ready");
|
||
f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx);
|
||
mkdirSync(join(f.ctx.cwd, "evidence")); writeFileSync(join(f.ctx.cwd, "evidence/pass.log"), "bytes\n");
|
||
const complete = (goal: string) => f.tools.get("CompleteGoal").execute("t", { goal, evidence: ["evidence/pass.log"], observation: "inspected" }, undefined, undefined, f.ctx);
|
||
await complete("first output");
|
||
const queued = await complete("second output");
|
||
expect(queued.content[0].text).toContain("Final review queued");
|
||
expect(readFileSync(f.path, "utf8")).toContain("- [ ] goal: second output");
|
||
const direct = f.messages.at(-1);
|
||
expect(direct.savedPrompt).toBe(true);
|
||
expect(direct.message.content).toContain("Read the complete file at");
|
||
expect(direct.message.content).not.toContain("worker evidence: keep this history");
|
||
// A queued follow-up may be consumed without another before_agent_start.
|
||
f.hooks.get("message_end")({ message: { role: "user", content: direct.message.content } });
|
||
expect(readFileSync(f.path, "utf8")).toContain("second output has exact saved bytes");
|
||
f.hooks.get("turn_end")({}, f.ctx); // Evidence-reading tool round must not invalidate this review.
|
||
const finalText = (await complete("second output")).content[0].text;
|
||
expect(finalText).toContain("All non-cancelled goals are reviewed. ヽ(•‿•)ノ");
|
||
expect(finalText).toContain('name "goals-copy-only"');
|
||
expect(finalText).toContain("Never use cleanup or change foreign tasks");
|
||
for (let i = 0; i < 10; i++) f.hooks.get("turn_end")({}, f.ctx);
|
||
expect(f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx).message).toBeUndefined();
|
||
f.shutdown();
|
||
});
|
||
|
||
it("recovers a queued final review and invalidates it when the plan changes", async () => {
|
||
const f = fixture(); await f.draft(); await f.command("ready");
|
||
writeFileSync(join(f.ctx.cwd, "proof.log"), "PASS\n");
|
||
const complete = (goal: string) => f.tools.get("CompleteGoal").execute("t", { goal, evidence: ["proof.log"], observation: "inspected" }, undefined, undefined, f.ctx);
|
||
await complete("first output");
|
||
await complete("second output");
|
||
const oldPrompt = f.messages.at(-1).message.content;
|
||
f.hooks.get("session_start")({}, f.ctx);
|
||
const recovered = f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx).message;
|
||
expect(recovered).toMatchObject({ customType: "pi-goals-final-review" });
|
||
expect(recovered.content).toContain("- [ ] goal: second output");
|
||
writeFileSync(f.path, readFileSync(f.path, "utf8").replace("## Log", " - discriminator: changed exact bytes\n## Log"));
|
||
const invalidated = await complete("second output");
|
||
expect(invalidated.content[0].text).toContain("plan changed since the final review");
|
||
const changed = await complete("second output");
|
||
expect(changed.content[0].text).toContain("Final review queued");
|
||
expect(readFileSync(f.path, "utf8")).toContain("- [ ] goal: second output");
|
||
expect(f.messages.at(-1).message.content).toContain("second output");
|
||
f.hooks.get("message_end")({ message: { role: "user", content: oldPrompt } });
|
||
expect((await complete("second output")).content[0].text).toContain("Final review queued");
|
||
f.shutdown();
|
||
});
|
||
|
||
it("restores the active plan above Log after session restore", async () => {
|
||
const f = fixture(); await f.draft();
|
||
const plan = `${f.plan.replace("## Log", "## User voice\n- > \"Keep the user voice after restore.\"\n## Log")}old progress`;
|
||
writeFileSync(f.path, plan); await f.command("ready");
|
||
f.hooks.get("session_start")({}, f.ctx);
|
||
const restored = f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx);
|
||
expect(restored.message.content).toContain("Keep the user voice after restore.");
|
||
expect(restored.message.content).not.toContain("old progress");
|
||
});
|
||
|
||
it("restores the active plan above Log after compaction without reinstalling or overriding scheduler jobs", async () => {
|
||
const f = fixture(); await f.draft();
|
||
const plan = `${f.plan.replace("## Log", "## User voice\n- > \"Keep this exact requirement.\"\n - task detail\n## Log")}old progress`;
|
||
writeFileSync(f.path, plan); 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("Keep this exact requirement.");
|
||
expect(result.message.content).toContain("task detail");
|
||
expect(result.message.content).not.toContain("old progress");
|
||
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");
|
||
await 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", input: { agent: "scout", async: true } })).toBeUndefined();
|
||
expect(f.hooks.get("tool_call")({ toolName: "OpenGoalWorker" }).block).toBe(true);
|
||
expect(f.hooks.get("tool_call")({ toolName: "read" })).toBeUndefined();
|
||
expect(f.hooks.get("tool_call")({ toolName: "subagent", input: { action: "status" } })).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("leaves an unverified external plan and its noted worker untouched", 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).toEqual([]);
|
||
expect(f.messages).toEqual([]);
|
||
expect(f.ctx.ui.select).not.toHaveBeenCalled();
|
||
expect(readFileSync(existing, "utf8")).toContain("previous progress kept");
|
||
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("Original supervisor unknown"), "warning");
|
||
});
|
||
|
||
it("retains same-current-plan solo recovery and reports the recorded session in status", async () => {
|
||
const f = fixture(); await f.draft();
|
||
const existing = f.path;
|
||
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
|
||
await f.command(`attach ${f.path}`);
|
||
expect(f.entries.at(-1).data.mode).toBe("chat");
|
||
expect(f.messages.length).toBe(before);
|
||
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("Cannot verify ownership"), "warning");
|
||
});
|
||
|
||
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");
|
||
await 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("external attach solo cannot turn a noted worker or stop checkbox into ownership proof", 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).toHaveLength(0);
|
||
expect(f.ctx.ui.select).not.toHaveBeenCalled();
|
||
expect(f.messages).toEqual([]);
|
||
expect(readFileSync(path, "utf8")).toContain("worker session: /tmp/known.jsonl");
|
||
});
|
||
|
||
it("retains current solo authority and stopped-session reference when external adoption is blocked", async () => {
|
||
const f = fixture(); await f.draft(); await f.command("ready");
|
||
await f.launch({ id: "child", sessionFile: "/tmp/prior.jsonl" });
|
||
const binding = f.entries.at(-1).data.worker;
|
||
f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped"); await f.command(`attach ${f.path} solo`);
|
||
expect(f.entries.at(-1).data.worker).toEqual(binding);
|
||
const other = join(f.ctx.cwd, "another.md"); writeFileSync(other, "- [ ] goal: next\n## Log\n");
|
||
const before = f.entries.at(-1), messageCount = f.messages.length;
|
||
f.ctx.ui.select.mockResolvedValueOnce("Previous supervisor confirmed stopped");
|
||
await f.command(`attach ${other}`);
|
||
expect(f.entries.at(-1).data).toMatchObject({ mode: "solo", plan: f.path, workerStopped: true, worker: { sessionFile: "/tmp/prior.jsonl" } });
|
||
expect(f.entries.at(-1)).toBe(before); expect(f.messages).toHaveLength(messageCount);
|
||
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining('read({path:"/tmp/prior.jsonl"})'), "warning");
|
||
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining('herdr pane process-info --pane "native-pane"'), "warning");
|
||
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("locate exact ID child"), "warning");
|
||
const response = await f.tools.get("OpenGoalWorker").execute("open", { task: "next task" }, undefined, undefined, f.ctx);
|
||
expect(response.content[0].text).toContain("solo");
|
||
expect(f.entries.at(-1).data.workerStopped).toBe(true);
|
||
});
|
||
|
||
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('name "goals-copy-only", action prompt, scope session');
|
||
expect(f.messages.at(-1).message.content).toContain("sessionFile exactly your current saved session");
|
||
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 remain unavailable 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")({ messages: [] }, f.ctx);
|
||
expect(f.ctx.ui.setStatus).toHaveBeenLastCalledWith("goals", expect.stringContaining("unavailable"));
|
||
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("- [✓] goal: first output");
|
||
expect(readFileSync(f.path, "utf8")).toContain("Observed PASS");
|
||
await delay(250);
|
||
expect(f.changed()).toBe(0);
|
||
});
|
||
|
||
it("ignores post-completion history, records evidence, and reviews requirement or 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.
|
||
await f.atomicWrite(signed.replace("## Log", " - evidence: proof.log\n## Log\n- recap: finished"));
|
||
await waitFor(() => f.messages.some(m => m.message?.content?.includes("[pi-goals: plan activity]")));
|
||
expect(f.changed()).toBe(0);
|
||
await f.atomicWrite(signed.replace("## Log", "- discriminator: exact bytes and trailing newline\n## Log"));
|
||
await waitFor(() => f.changed() === 1);
|
||
expect(readFileSync(f.path, "utf8")).toContain("[✓] goal: first output"); // Supervisor decides whether changed requirements require reopening.
|
||
f.hooks.get("message_end")({ message: { role: "user", content: f.messages.at(-1).message.content } });
|
||
await f.atomicWrite(signed.replace("[✓] goal: first", "[ ] goal: first"));
|
||
await waitFor(() => f.changed() === 2);
|
||
expect(readFileSync(f.path, "utf8")).toContain("[ ] goal: first output");
|
||
});
|
||
|
||
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 params = { goal: "first output", evidence: ["proof.log"], observation: "Exact bytes observed" };
|
||
const queued = await f.tools.get("CompleteGoal").execute("c", params, undefined, undefined, f.ctx);
|
||
expect(queued.content[0].text).toContain("Final review queued");
|
||
f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx);
|
||
const done = await f.tools.get("CompleteGoal").execute("c", params, 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("persisted 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("prioritizes unfinished goals and says when the widget list is truncated", async () => {
|
||
const f = fixture(); await f.draft();
|
||
writeFileSync(f.path, "- [✓] goal: completed one\n- [✓] goal: completed two\n- [/] goal: active work\n- [ ] goal: open one\n- [ ] goal: open two\n");
|
||
await f.command("ready");
|
||
expect(f.ctx.ui.setWidget.mock.lastCall?.[1]).toEqual(["◼ G3: active work", "◻ G4: open one", "◻ G5: open two", `… 2 ✓; ${relative(f.ctx.cwd, f.path)}`]);
|
||
f.shutdown();
|
||
});
|
||
|
||
it.each([
|
||
["[✓]", "[ ]", "[ ]", "… 1 ✓, 2 ◻"],
|
||
["[/]", "[✓]", "[-]", "… 1 ✓, 1 ◼, 1 ✗"],
|
||
["[ ]", "[ ]", "[ ]", "… 3 ◻"],
|
||
])("summarizes only hidden goal statuses: %s %s %s", async (first, second, third, summary) => {
|
||
const f = fixture(); await f.draft();
|
||
const marks = ["[/]", "[/]", "[/]", first, second, third];
|
||
writeFileSync(f.path, marks.map((mark, index) => `- ${mark} goal: output ${index + 1}`).join("\n"));
|
||
await f.command("ready");
|
||
expect(f.ctx.ui.setWidget.mock.lastCall?.[1]).toEqual([
|
||
"◼ G1: output 1", "◼ G2: output 2", "◼ G3: output 3", `${summary}; ${relative(f.ctx.cwd, f.path)}`,
|
||
]);
|
||
f.shutdown();
|
||
});
|
||
|
||
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(["◼ G1: first output", "◻ G2: second output", relative(f.ctx.cwd, f.path)]);
|
||
f.ctx.cwd = join(f.ctx.cwd, "another-project", "nested");
|
||
await f.command("status");
|
||
expect(f.ctx.ui.setWidget.mock.lastCall?.[1]).toEqual(["◼ G1: first output", "◻ G2: second output", `${basename(f.path)} (external)`]);
|
||
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining(`Plan: ${f.path}`), "info");
|
||
f.ctx.mode = "tui";
|
||
await f.command("status");
|
||
const widget = f.ctx.ui.setWidget.mock.lastCall?.[1]();
|
||
expect(widget.render(100)).toEqual([" ◼ G1: first output", " ◻ G2: second output", ` ${basename(f.path)} (external)`]);
|
||
expect(widget.render(24)).toHaveLength(3);
|
||
expect(widget.render(24).every((line: string) => visibleWidth(line) <= 24)).toBe(true);
|
||
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).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.content).toContain("refined output");
|
||
await f.command("stop");
|
||
for (let i = 0; i < 10; i++) f.hooks.get("turn_end")({}, f.ctx);
|
||
expect(reminders()).toHaveLength(0);
|
||
});
|
||
|
||
it("keeps the user-visible outcome beside unfinished goals without task or history noise", async () => {
|
||
const f = fixture(); await f.draft();
|
||
const plan = `# Context title
|
||
|
||
A short introduction.
|
||
|
||
## User-visible result
|
||
A visible artifact.
|
||
|
||
## User voice
|
||
- > "Keep this exact user requirement."
|
||
|
||
## Goals
|
||
- [/] goal: produce the artifact
|
||
- tasks:
|
||
- [ ] run the detailed check
|
||
- evidence: proof.log
|
||
- [✓] goal: previously demonstrated output
|
||
- [-] goal: abandoned experiment
|
||
|
||
## Log
|
||
old progress`;
|
||
writeFileSync(f.path, plan); await f.command("ready");
|
||
// Consume active context before observing the routine reminder.
|
||
f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx);
|
||
for (let i = 0; i < 9; i++) f.hooks.get("turn_end")({}, f.ctx);
|
||
const reminder = f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx).message;
|
||
expect(reminder.customType).toBe("pi-goals-upkeep");
|
||
expect(reminder.content).not.toContain("Keep this exact user requirement.");
|
||
expect(reminder.content).toContain("goal: produce the artifact");
|
||
expect(reminder.content).toContain("A visible artifact.");
|
||
for (const hidden of ["run the detailed check", "proof.log", "old progress", "previously demonstrated output", "abandoned experiment"]) expect(reminder.content).not.toContain(hidden);
|
||
await f.command("review");
|
||
const review = f.messages.at(-1).message.content;
|
||
expect(review).toContain("A visible artifact.");
|
||
expect(review).toContain("goal: produce the artifact");
|
||
expect(review).not.toContain("previously demonstrated output");
|
||
});
|
||
|
||
it.each(["supervising", "solo"])("%s repeats concise upkeep every eight unchanged turns", 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;
|
||
let previous: string | undefined;
|
||
for (let round = 0; round < 2; 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);
|
||
const reminder = prepare().message;
|
||
expect(reminder.customType).toBe("pi-goals-upkeep");
|
||
expect(reminder.content).toContain("goal: first output");
|
||
if (previous && mode === "supervising") expect(reminder.content).not.toBe(previous);
|
||
if (previous && mode === "solo") expect(reminder.content).toBe(previous);
|
||
f.ctx.sessionManager.getBranch().push({ type: "custom_message", ...reminder }); // host saves returned messages
|
||
previous = reminder.content;
|
||
expect(prepare().message).toBeUndefined();
|
||
}
|
||
});
|
||
|
||
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");
|
||
await 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("blocks unknown external ownership without offering an attestation or launching work", 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).toHaveLength(0);
|
||
expect(f.ctx.ui.select).not.toHaveBeenCalled();
|
||
expect(f.messages).toEqual([]);
|
||
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("supported Intercom roster does not identify per-plan supervisors"), "warning");
|
||
});
|
||
|
||
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")({ messages: [] }, f.ctx);
|
||
expect(f.ctx.ui.setWidget).toHaveBeenLastCalledWith("goals", [expect.stringContaining("unavailable")]);
|
||
});
|
||
|
||
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 latestState = () => session.getBranch().filter(entry => entry.type === "custom" && entry.customType === "pi-goals-main-supervisor-v1").at(-1) as any;
|
||
const planned = latestState();
|
||
await f.command("ready");
|
||
expect(planned.data.mode).toBe("planning");
|
||
expect(latestState().data).not.toBe(planned.data);
|
||
expect(latestState().data.mode).toBe("supervising");
|
||
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("- [✓] goal: first output");
|
||
expect(text).toContain("Parent review:");
|
||
});
|
||
|
||
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(readFileSync(f.path, "utf8")).not.toContain("[✓]");
|
||
});
|
||
|
||
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(readFileSync(f.path, "utf8")).not.toContain("[✓]");
|
||
});
|
||
|
||
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");
|
||
});
|
||
|
||
// The native surface has one project binding; these replace old launch-schema/helper tests.
|
||
it.each(["default", "absolute", "relative"])("opens no-focus with %s cwd and keeps exact-worker stop correlation", async (mode) => {
|
||
const f = fixture(); await f.draft(); await f.command("ready");
|
||
const supervisorCwd = f.ctx.cwd;
|
||
const workerCwd = mode === "default" ? supervisorCwd : mkdtempSync(join(tmpdir(), "goals-worker-project-"));
|
||
if (mode !== "default") roots.push(workerCwd);
|
||
const cwd = mode === "default" ? undefined : mode === "relative" ? relative(supervisorCwd, workerCwd) : workerCwd;
|
||
const projectRoot = realpathSync(workerCwd);
|
||
for (const event of [
|
||
{ toolName: "intercom", input: { action: "send", cwd: "/tmp/other", openProjectPaneIfMissing: true } },
|
||
{ toolName: "subagent", input: { action: "project.open", cwd: "/tmp/other" } },
|
||
]) expect(f.hooks.get("tool_call")(event, f.ctx)).toMatchObject({ block: true, reason: expect.stringContaining("orphan worker") });
|
||
expect(f.hooks.get("tool_call")({ toolName: "intercom", input: { action: "send", to: "existing" } }, f.ctx)).toBeUndefined();
|
||
f.channel.listSessions.mockRejectedValueOnce(new Error("Intercom is not connected"));
|
||
const waiting = await f.tools.get("OpenGoalWorker").execute("open", { task: "first" }, undefined, undefined, f.ctx);
|
||
expect(waiting.content[0].text).toContain("still connecting"); expect(openProjectPane).not.toHaveBeenCalled();
|
||
vi.mocked(openProjectPane).mockResolvedValueOnce({ ok: true, data: { bindingPath: join(projectRoot, ".pi/subagents/project-panes/herdr.json"), disposition: "opened", binding: { paneId: "native-pane", projectRoot, command: "pi" } } });
|
||
await f.launch({ id: "worker-id", sessionFile: "/tmp/native-worker.jsonl", cwd, task: "Inspect [cached interruption audit](slop/audits/20260916_job1551_a2_cached_interruption_audit.md) before rerun" });
|
||
expect(openProjectPane).toHaveBeenCalledWith(expect.objectContaining({ cwd: workerCwd, focus: false }));
|
||
const startup = vi.mocked(openProjectPane).mock.calls[0][0].message!;
|
||
const worker = f.entries.at(-1).data.worker;
|
||
expect(worker).toMatchObject({ paneId: "native-pane", projectRoot, parentId: "parent-intercom", intercomId: "worker-id", sessionFile: "/tmp/native-worker.jsonl" });
|
||
for (const reference of [f.path, worker.parentId, worker.requestId, workerCwd]) expect(startup).toContain(JSON.stringify(reference));
|
||
expect(f.entries.at(-1).data.plan).toBe(f.path);
|
||
expect(f.ctx.cwd).toBe(supervisorCwd);
|
||
expect(f.channel.publish).not.toHaveBeenCalled(); // attachment alone sends no assignment
|
||
expect(f.messages.at(-1)).toMatchObject({ message: { customType: "pi-goals-supervision", display: true, content: expect.stringContaining("Metadata only; no acknowledgement or review turn requested") }, options: { triggerTurn: false } });
|
||
expect(f.messages.at(-1).savedPrompt).toBeUndefined();
|
||
f.event({ type: "message", fromSessionId: "orphan-worker", payload: { type: "attached", to: worker.parentId, requestId: "invented-request", plan: f.path, sessionFile: "/tmp/orphan.jsonl" } });
|
||
expect(f.channel.publish).toHaveBeenLastCalledWith(expect.objectContaining({ type: "attachment_rejected", to: "orphan-worker", requestId: "invented-request" }), { audience: "capable" });
|
||
expect(f.messages.at(-1)?.message.content).toContain("Rejected uncorrelated worker attachment");
|
||
expect(f.messages.at(-1)?.savedPrompt).toBe(true);
|
||
const notice = { type: "stopped", to: worker.parentId, requestId: worker.requestId, plan: f.path, entryId: "revision-1", kind: "blocker", text: "Blocked: input missing" };
|
||
const count = f.messages.length;
|
||
for (const fromSessionId of [worker.parentId, "foreign-id"]) f.event({ type: "message", fromSessionId, payload: notice });
|
||
f.event({ type: "message", fromSessionId: "worker-id", payload: { ...notice, plan: "/foreign.md" } });
|
||
f.event({ type: "message", fromSessionId: "worker-id", payload: { ...notice, requestId: "stale" } });
|
||
f.event({ type: "message", fromSessionId: "worker-id", payload: { ...notice, entryId: undefined } });
|
||
expect(f.messages).toHaveLength(count);
|
||
f.event({ type: "message", fromSessionId: "worker-id", payload: notice });
|
||
expect(f.messages.at(-1)?.message.content).toContain("## Worker status: blocker");
|
||
expect(f.messages.at(-1)?.message.content).toContain("Blocked: input missing");
|
||
expect(f.messages.at(-1)?.message.content).toContain("No formal review was created");
|
||
expect(f.ctx.sessionManager.getBranch().filter((entry: any) => entry.customType === "pi-goals-report")).toHaveLength(0);
|
||
expect(f.ctx.sessionManager.getBranch().some((entry: any) => entry.customType === "pi-goals-worker-event" && entry.data.id.endsWith(":revision-1"))).toBe(true);
|
||
const afterFirstRevision = f.messages.length;
|
||
for (const text of ["Done: output.txt", "Error: execution failed"]) f.event({ type: "message", fromSessionId: "worker-id", payload: { ...notice, text } });
|
||
expect(f.messages).toHaveLength(afterFirstRevision);
|
||
f.event({ type: "message", fromSessionId: "worker-id", payload: { ...notice, entryId: "waiting-1", kind: "waiting", text: "Pueue 1552 is running." } });
|
||
expect(f.messages.at(-1)?.message.content).toContain("## Worker status: waiting");
|
||
const formalReports = f.ctx.sessionManager.getBranch().filter((entry: any) => entry.customType === "pi-goals-report").length;
|
||
f.event({ type: "message", fromSessionId: "worker-id", payload: { ...notice, entryId: "decision-1", kind: "decision", text: "Choose retry A or B." } });
|
||
expect(f.messages.at(-1)?.message.content).toContain("## Worker status: decision");
|
||
expect(f.messages.at(-1)?.savedPrompt).toBe(true);
|
||
expect(f.ctx.sessionManager.getBranch().filter((entry: any) => entry.customType === "pi-goals-report")).toHaveLength(formalReports);
|
||
f.ctx.isIdle.mockReturnValue(false);
|
||
f.event({ type: "message", fromSessionId: "worker-id", payload: { ...notice, entryId: "automatic-stop", kind: "unclassified", text: "Worker turn ended without an explicit event." } });
|
||
expect(f.messages.at(-1)?.message.content).toContain("## Worker status: unclassified");
|
||
expect(f.messages.at(-1)?.savedPrompt).toBe(true);
|
||
expect(f.ctx.sessionManager.getBranch().filter((entry: any) => entry.customType === "pi-goals-report")).toHaveLength(formalReports);
|
||
f.ctx.isIdle.mockReturnValue(true);
|
||
await f.command("status");
|
||
expect(f.ctx.ui.notify.mock.lastCall?.[0]).toContain("Latest worker status event: unclassified");
|
||
expect(f.ctx.ui.notify.mock.lastCall?.[0]).not.toContain("automatic-stop");
|
||
expect(f.ctx.ui.notify.mock.lastCall?.[0]).toContain(projectRoot);
|
||
f.hooks.get("session_start")({}, f.ctx);
|
||
await f.command("status");
|
||
expect(f.ctx.ui.notify.mock.lastCall?.[0]).toContain(projectRoot);
|
||
expect(f.entries.at(-1).data.worker).toMatchObject({ projectRoot, requestId: worker.requestId, intercomId: "worker-id" });
|
||
expect(readFileSync(f.path, "utf8")).not.toContain("[✓]");
|
||
await f.command("stop");
|
||
f.event({ type: "message", fromSessionId: "worker-id", payload: { ...notice, entryId: "revision-2", text: "New stop request during pause" } });
|
||
expect(f.messages.at(-1)?.message.content).toContain("New stop request during pause");
|
||
await f.command("clear");
|
||
const cleared = f.messages.length; f.event({ type: "message", fromSessionId: "worker-id", payload: { ...notice, entryId: "revision-3" } });
|
||
expect(f.messages).toHaveLength(cleared);
|
||
});
|
||
|
||
it("records separate disconnect episodes when saved worker history is unavailable", async () => {
|
||
const f = fixture(); await f.draft(); await f.command("ready");
|
||
await f.launch({ id: "worker-id", sessionFile: "/tmp/missing-worker-history.jsonl" });
|
||
f.event({ type: "session_left", sessionId: "worker-id" });
|
||
f.event({ type: "session_left", sessionId: "worker-id" });
|
||
const disconnects = f.ctx.sessionManager.getBranch().filter((entry: any) => entry.customType === "pi-goals-worker-event" && entry.data.id.includes("disconnect-"));
|
||
expect(disconnects.map((entry: any) => entry.data.id)).toEqual(["worker-id:disconnect-1", "worker-id:disconnect-2"]);
|
||
});
|
||
|
||
it("records a passive disconnect receipt after an already-visible stop", async () => {
|
||
const f = fixture(); await f.draft(); await f.command("ready");
|
||
const sessionFile = join(f.ctx.cwd, "worker.jsonl");
|
||
const timestamp = new Date().toISOString();
|
||
writeFileSync(sessionFile, [
|
||
{ type: "session", version: 3, id: "worker-id", timestamp, cwd: f.ctx.cwd },
|
||
{ type: "custom", id: "saved-stop", parentId: null, timestamp, customType: "pi-goals-worker-stop", data: { type: "stopped", entryId: "run:completion", to: "parent-intercom", requestId: "placeholder", plan: f.path, text: "Finished output", kind: "completion" } },
|
||
].map(entry => JSON.stringify(entry)).join("\n") + "\n");
|
||
await f.launch({ id: "worker-id", sessionFile });
|
||
const worker = f.entries.at(-1).data.worker;
|
||
const saved = SessionManager.open(sessionFile);
|
||
const stop = saved.getBranch().find((entry: any) => entry.customType === "pi-goals-worker-stop") as any;
|
||
stop.data.requestId = worker.requestId;
|
||
stop.data.to = worker.parentId;
|
||
writeFileSync(sessionFile, [saved.getHeader(), stop].map(entry => JSON.stringify(entry)).join("\n") + "\n");
|
||
const before = f.messages.length;
|
||
f.event({ type: "session_left", sessionId: "worker-id" });
|
||
const receipt = f.ctx.sessionManager.getBranch().find((entry: any) => entry.customType === "pi-goals-worker-event" && entry.data.id.includes("run:completion:disconnect-1"));
|
||
expect(receipt?.data).toMatchObject({ kind: "receipt", text: expect.stringContaining("disconnected after its recorded completion event") });
|
||
expect(f.messages).toHaveLength(before + 1);
|
||
expect(f.messages.at(-1)?.options).toEqual({ deliverAs: "nextTurn" });
|
||
});
|
||
|
||
it("rejects an oversized review before selecting formal review", async () => {
|
||
const f = fixture(); await f.draft(); await f.command("ready");
|
||
await f.launch({ id: "worker-id", sessionFile: join(f.ctx.cwd, "worker.jsonl") });
|
||
const worker = f.entries.at(-1).data.worker;
|
||
const eventId = "worker-id:completion-oversized";
|
||
f.event({ type: "message", fromSessionId: "worker-id", payload: { type: "stopped", to: worker.parentId, requestId: worker.requestId, plan: f.path, entryId: "completion-oversized", kind: "completion", text: "Potential completion" } });
|
||
const quote = "q".repeat(17_000); writeFileSync(join(f.ctx.cwd, "proof.txt"), quote);
|
||
await expect(f.tools.get("review_subagent").execute("review", {
|
||
eventId, goal: { path: f.path, quote: "goal: first output" }, evidence: [{ path: "proof.txt", quote, observation: "Read exact proof" }],
|
||
observation: "Inspected proof", unmet: "none", verdict: "accepted", continuation: "",
|
||
}, undefined, undefined, f.ctx)).rejects.toThrow("16 KiB");
|
||
expect(f.ctx.sessionManager.getBranch().filter((entry: any) => entry.customType === "pi-goals-report")).toHaveLength(0);
|
||
});
|
||
|
||
it("supersedes an inherited worker binding when the supervisor opens a replacement", async () => {
|
||
const f = fixture(); await f.draft(); await f.command("ready");
|
||
await f.launch({ id: "old-worker", sessionFile: "/tmp/old-worker.jsonl", task: "Old task" });
|
||
const oldRequest = f.entries.at(-1).data.worker.requestId;
|
||
const selects = f.ctx.ui.select.mock.calls.length;
|
||
const opened = await f.tools.get("OpenGoalWorker").execute("replacement", { task: "Continue the approved plan" }, undefined, undefined, f.ctx);
|
||
expect(opened.content[0].text).toContain('"disposition":"opened"');
|
||
const replacement = f.entries.at(-1).data.worker;
|
||
expect(replacement).toMatchObject({ paneId: "native-pane", projectRoot: "/project", parentId: "parent-intercom", task: "Continue the approved plan" }); // retain stock's root, not the supervisor cwd
|
||
expect(replacement.requestId).not.toBe(oldRequest);
|
||
const release = f.ctx.sessionManager.getBranch().find((entry: any) => entry.customType === "pi-goals-worker-release");
|
||
expect(release?.data).toMatchObject({ plan: f.path, worker: { intercomId: "old-worker", requestId: oldRequest }, task: "Continue the approved plan" });
|
||
expect(f.entries.some(entry => entry.data.worker?.intercomId === "old-worker")).toBe(true); // saved state history is retained
|
||
expect(f.ctx.ui.select).toHaveBeenCalledTimes(selects);
|
||
expect(openProjectPane).toHaveBeenCalledTimes(2);
|
||
});
|
||
|
||
it.each(["default", "alternate"])("preserves the current worker binding when stock reports an existing %s pane", async (mode) => {
|
||
const f = fixture(); await f.draft(); await f.command("ready");
|
||
await f.launch({ id: "old-worker", sessionFile: "/tmp/old-worker.jsonl", task: "Old task" });
|
||
const previous = structuredClone(f.entries.at(-1).data.worker);
|
||
const cwd = mode === "default" ? undefined : mkdtempSync(join(tmpdir(), "goals-foreign-project-"));
|
||
if (cwd) roots.push(cwd);
|
||
const projectRoot = realpathSync(cwd ?? f.ctx.cwd);
|
||
vi.mocked(openProjectPane).mockResolvedValueOnce({ ok: true, data: { bindingPath: "/existing/binding.json", disposition: "already-open", binding: { paneId: "existing-pane", projectRoot, command: "pi" } } });
|
||
const opened = await f.tools.get("OpenGoalWorker").execute("existing", { task: "Proposed replacement", cwd }, undefined, undefined, f.ctx);
|
||
expect(openProjectPane).toHaveBeenLastCalledWith(expect.objectContaining({ cwd: cwd ?? f.ctx.cwd, focus: false }));
|
||
expect(opened.content[0].text).toContain('"disposition":"already-open"');
|
||
expect(opened.content[0].text).toContain(JSON.stringify(projectRoot));
|
||
expect(f.entries.at(-1).data.worker).toEqual(previous);
|
||
expect(f.channel.publish).not.toHaveBeenCalled();
|
||
expect(f.ctx.sessionManager.getBranch().some((entry: any) => entry.customType === "pi-goals-worker-release")).toBe(false);
|
||
f.event({ type: "message", fromSessionId: "old-worker", payload: { type: "stopped", to: previous.parentId, requestId: previous.requestId, plan: f.path, entryId: "after-foreign-open", kind: "blocker", text: "Original worker still reports" } });
|
||
expect(f.ctx.sessionManager.getBranch().some((entry: any) => entry.customType === "pi-goals-worker-event" && entry.data.id === "old-worker:after-foreign-open")).toBe(true);
|
||
});
|
||
|
||
it("retains worker history and report routing after definite pre-open failure, not ambiguous partial open", async () => {
|
||
const f = fixture(); await f.draft(); await f.command("ready");
|
||
const session = SessionManager.create(f.ctx.cwd, join(f.ctx.cwd, "sessions"));
|
||
session.appendMessage({ role: "assistant", content: [{ type: "text", text: "Existing approved work" }], stopReason: "stop", timestamp: Date.now() } as any);
|
||
await f.launch({ id: "old-worker", sessionFile: session.getSessionFile()! });
|
||
const previous = structuredClone(f.entries.at(-1).data.worker);
|
||
const tool = f.tools.get("OpenGoalWorker");
|
||
// Exercise the actual stock preflight, which returns before calling Herdr for this missing cwd.
|
||
const stock = await vi.importActual<typeof import("pi-subagents/project-panes")>("pi-subagents/project-panes");
|
||
vi.mocked(openProjectPane).mockImplementationOnce(stock.openProjectPane);
|
||
const reply = await tool.execute("failed", { task: "Proposed replacement", cwd: "missing-directory" }, undefined, undefined, f.ctx);
|
||
expect(openProjectPane).toHaveBeenLastCalledWith(expect.objectContaining({ cwd: join(f.ctx.cwd, "missing-directory") }));
|
||
expect(existsSync(join(f.ctx.cwd, "missing-directory"))).toBe(false);
|
||
expect(reply.content[0].text).toContain("INVALID_PROJECT_ROOT");
|
||
expect(f.entries.at(-1).data.worker).toEqual(previous);
|
||
vi.mocked(openProjectPane).mockResolvedValueOnce({ ok: false, error: { code: "HERDR_UNSUPPORTED_VERSION", message: "Stock version preflight rejected" } });
|
||
await tool.execute("preflight", { task: "Proposed replacement" }, undefined, undefined, f.ctx);
|
||
expect(f.entries.at(-1).data.worker).toEqual(previous);
|
||
const view = await f.tools.get("worker_view").execute("view", {}, undefined, undefined, f.ctx);
|
||
expect(view.content[0].text).toContain("Existing approved work");
|
||
f.event({ type: "message", fromSessionId: "old-worker", payload: { type: "stopped", to: previous.parentId, requestId: previous.requestId, plan: f.path, entryId: "still-routed", kind: "blocker", text: "Original worker reports a failure" } });
|
||
expect(f.ctx.sessionManager.getBranch().some((entry: any) => entry.customType === "pi-goals-worker-event" && entry.data.id === "old-worker:still-routed")).toBe(true);
|
||
vi.mocked(openProjectPane).mockResolvedValueOnce({ ok: false, error: { code: "BINDING_WRITE_FAILED", message: "Pane started; cleanup uncertain" } });
|
||
const uncertain = await tool.execute("partial", { task: "Proposed replacement" }, undefined, undefined, f.ctx);
|
||
expect(uncertain.content[0].text).toContain("uncertain");
|
||
expect(f.entries.at(-1).data.worker.requestId).not.toBe(previous.requestId);
|
||
expect(f.entries.some(entry => entry.data.worker?.requestId === previous.requestId)).toBe(true);
|
||
expect(f.ctx.sessionManager.getBranch().some((entry: any) => entry.customType === "pi-goals-worker-release")).toBe(false);
|
||
});
|
||
|
||
it("automatically reports worker turn end and pauses a rejected attachment", async () => {
|
||
const f = fixture(); const path = join(f.ctx.cwd, "supplied.md"); writeFileSync(path, f.plan);
|
||
await f.tools.get("AttachGoalPlan").execute("attach", { path, parent: "live-parent", requestId: "owned-request" }, undefined, undefined, f.ctx);
|
||
f.hooks.get("agent_start")({}, f.ctx);
|
||
const assistant = { role: "assistant", content: [{ type: "text", text: "Awaiting review." }], stopReason: "stop" };
|
||
f.ctx.sessionManager.getBranch().push({ type: "message", id: "automatic-stop-turn", message: assistant });
|
||
await f.tools.get("ReportGoalEvent").execute("waiting", { kind: "waiting", summary: "Awaiting review." }, undefined, undefined, f.ctx);
|
||
expect(f.channel.publish).toHaveBeenLastCalledWith(expect.objectContaining({ type: "stopped", requestId: "owned-request", kind: "waiting" }), { audience: "capable" });
|
||
const afterWaiting = f.channel.publish.mock.calls.length;
|
||
f.hooks.get("agent_end")({ messages: [assistant] }, f.ctx);
|
||
expect(f.channel.publish).toHaveBeenCalledTimes(afterWaiting); // intentional waiting already describes the stopped turn
|
||
f.hooks.get("agent_start")({}, f.ctx);
|
||
const silent = { role: "assistant", content: [{ type: "text", text: "Turn ended without an event." }], stopReason: "stop" };
|
||
f.ctx.sessionManager.getBranch().push({ type: "message", id: "silent-stop-turn", message: silent });
|
||
f.hooks.get("agent_end")({ messages: [silent] }, f.ctx);
|
||
expect(f.channel.publish).toHaveBeenLastCalledWith(expect.objectContaining({ type: "stopped", requestId: "owned-request", kind: "unclassified", text: "Turn ended without an event." }), { audience: "capable" });
|
||
f.hooks.get("agent_start")({}, f.ctx);
|
||
const progress = { role: "assistant", content: [{ type: "text", text: "Implemented the first half." }], stopReason: "stop" };
|
||
f.ctx.sessionManager.getBranch().push({ type: "message", id: "progress-stop-turn", message: progress });
|
||
await f.tools.get("ReportGoalEvent").execute("progress", { kind: "progress", summary: "Implemented the first half." }, undefined, undefined, f.ctx);
|
||
f.hooks.get("agent_end")({ messages: [progress] }, f.ctx);
|
||
expect(f.channel.publish).toHaveBeenLastCalledWith(expect.objectContaining({ type: "stopped", requestId: "owned-request", kind: "unclassified", text: "Implemented the first half." }), { audience: "capable" });
|
||
f.event({ type: "message", fromSessionId: "live-parent", payload: { type: "attachment_rejected", to: "worker", requestId: "owned-request", plan: path, text: "Parent rejected the uncorrelated attachment." } });
|
||
expect(f.entries.at(-1).data).toMatchObject({ child: true, mode: "paused", plan: path });
|
||
expect(f.entries.at(-1).data.parent).toBeUndefined();
|
||
expect(f.messages.at(-1)?.message.content).toContain("Parent rejected the uncorrelated attachment");
|
||
});
|
||
|
||
it("reports graceful shutdown after an intentional waiting turn", async () => {
|
||
const f = fixture(); const path = join(f.ctx.cwd, "supplied.md"); writeFileSync(path, f.plan);
|
||
await f.tools.get("AttachGoalPlan").execute("attach", { path, parent: "live-parent", requestId: "owned-request" }, undefined, undefined, f.ctx);
|
||
f.hooks.get("agent_start")({}, f.ctx);
|
||
const assistant = { role: "assistant", content: [{ type: "text", text: "Task 7 is running with a completion follower." }], stopReason: "stop" };
|
||
f.ctx.sessionManager.getBranch().push({ type: "message", id: "waiting-turn", message: assistant });
|
||
await f.tools.get("ReportGoalEvent").execute("waiting", { kind: "waiting", summary: "Task 7 is running with a completion follower." }, undefined, undefined, f.ctx);
|
||
f.hooks.get("agent_end")({ messages: [assistant] }, f.ctx);
|
||
const afterWaiting = f.channel.publish.mock.calls.length;
|
||
f.shutdown();
|
||
expect(f.channel.publish).toHaveBeenCalledTimes(afterWaiting + 1);
|
||
expect(f.channel.publish).toHaveBeenLastCalledWith(expect.objectContaining({ type: "stopped", kind: "unclassified", text: expect.stringContaining("shutting down") }), { audience: "capable" });
|
||
});
|
||
|
||
it("ordinary project peer explicitly attaches as worker, never gaining approval authority", async () => {
|
||
vi.stubEnv("PI_SUBAGENT_CHILD", "1");
|
||
const helperApi = { on: vi.fn(), registerTool: vi.fn() };
|
||
goalsExtension(helperApi as unknown as ExtensionAPI);
|
||
expect(helperApi.on).not.toHaveBeenCalled(); expect(helperApi.registerTool).not.toHaveBeenCalled();
|
||
vi.unstubAllEnvs();
|
||
const f = fixture(); const path = join(f.ctx.cwd, "supplied.md"); writeFileSync(path, f.plan);
|
||
Object.assign(f.ctx, { model: { provider: "offline", id: "inherited" } });
|
||
const tool = f.tools.get("AttachGoalPlan");
|
||
await tool.execute("attach", { path }, undefined, undefined, f.ctx);
|
||
expect(f.entries).toHaveLength(0);
|
||
f.channel.listSessions.mockRejectedValueOnce(new Error("Intercom is not connected"));
|
||
const waiting = await tool.execute("attach", { path, parent: "live-parent", requestId: "assignment-id" }, undefined, undefined, f.ctx);
|
||
expect(waiting.content[0].text).toContain("still connecting"); expect(f.entries).toHaveLength(0);
|
||
await tool.execute("attach", { path, parent: "live-parent", requestId: "assignment-id" }, undefined, undefined, f.ctx);
|
||
expect(f.entries.at(-1).data).toMatchObject({ child: true, parent: { intercomId: "live-parent", requestId: "assignment-id" }, plan: path });
|
||
expect(f.channel.publish).toHaveBeenCalledWith(expect.objectContaining({ type: "attached", to: "live-parent", sessionFile: f.ctx.sessionManager.getSessionFile(), identity: expect.objectContaining({ model: "offline/inherited" }) }), { audience: "capable" });
|
||
await f.command("ready"); await f.command("solo");
|
||
await f.command("stop"); // inspection remains available while paused; it does not authorize continuation
|
||
expect(f.hooks.get("tool_call")({ toolName: "subagent", input: { action: "status" } })).toBeUndefined();
|
||
for (const action of [undefined, "resume", "schedule.create", "update", "project.open", "unknown.action", ["status"]]) {
|
||
expect(f.hooks.get("tool_call")({ toolName: "subagent", input: { action, agent: "reviewer" } }).block).toBe(true);
|
||
}
|
||
expect(f.hooks.get("tool_call")({ toolName: "OpenGoalWorker", input: { action: "status" } }).block).toBe(true);
|
||
for (const action of ["stop", "interrupt"]) expect(f.hooks.get("tool_call")({ toolName: "subagent", input: { action, id: "owned-helper" } })).toBeUndefined();
|
||
await f.command("resume");
|
||
for (const input of [{ agent: "scout", async: true }, { workflowScript: "return runs.run('helper', {agent:'scout',task:'Inspect'});", async: true }, { action: "resume", id: "owned-helper" }]) expect(f.hooks.get("tool_call")({ toolName: "subagent", input })).toBeUndefined();
|
||
for (const action of ["project.open", "project.close"]) expect(f.hooks.get("tool_call")({ toolName: "subagent", input: { action } }).block).toBe(true);
|
||
const reply = await f.tools.get("CompleteGoal").execute("complete", { goal: "first output", evidence: [path], observation: "claim" }, undefined, undefined, f.ctx);
|
||
expect(reply.content[0].text).toContain("only to the active parent");
|
||
const next = join(f.ctx.cwd, "next.md"); writeFileSync(next, f.plan);
|
||
const before = f.entries.length;
|
||
await tool.execute("missing", { path: next }, undefined, undefined, f.ctx);
|
||
f.channel.listSessions.mockResolvedValue([{ id: "live-parent", pid: process.pid + 1 }, { id: "foreign-parent", pid: process.pid + 2 }]);
|
||
await tool.execute("foreign", { path: next, parent: "foreign-parent", requestId: "next" }, undefined, undefined, f.ctx);
|
||
expect(f.entries).toHaveLength(before); // neither a missing request nor a live stranger can take over
|
||
await tool.execute("next", { path: next, parent: "live-parent", requestId: "next" }, undefined, undefined, f.ctx);
|
||
expect(f.entries.at(-1).data).toMatchObject({ plan: next, parent: { intercomId: "live-parent", requestId: "next" } });
|
||
f.hooks.get("session_start")({}, f.ctx); f.hooks.get("session_compact")();
|
||
expect(f.hooks.get("before_agent_start")({ systemPrompt: "base" }, f.ctx).systemPrompt).toContain("delegated implementation worker");
|
||
const assistant = { role: "assistant", content: [{ type: "text", text: "Result at output.txt" }], stopReason: "stop" };
|
||
f.ctx.sessionManager.getBranch().push({ type: "message", id: "saved-report", message: assistant });
|
||
f.hooks.get("agent_end")({ messages: [assistant] }, f.ctx);
|
||
expect(f.channel.publish).toHaveBeenLastCalledWith(expect.objectContaining({ type: "stopped", text: "Result at output.txt" }), { audience: "capable" });
|
||
});
|
||
|
||
it("blocks concurrent opening and lets stock pane ownership resolve a retry", async () => {
|
||
const f = fixture(); await f.draft(); await f.command("ready");
|
||
const tool = f.tools.get("OpenGoalWorker");
|
||
expect(tool.parameters.properties.task.minLength).toBe(1);
|
||
expect((await tool.execute("empty", { task: "" }, undefined, undefined, f.ctx)).content[0].text).toContain("task");
|
||
const before = structuredClone(f.entries.at(-1).data);
|
||
for (const cwd of ["", " "]) await tool.execute("empty-cwd", { task: "first", cwd }, undefined, undefined, f.ctx);
|
||
expect(openProjectPane).not.toHaveBeenCalled();
|
||
expect(f.entries.at(-1).data).toEqual(before);
|
||
let release!: () => void;
|
||
vi.mocked(openProjectPane).mockImplementationOnce(() => new Promise((_resolve, reject) => { release = () => reject(new Error("connection lost after open")); }));
|
||
const opening = f.tools.get("OpenGoalWorker").execute("open", { task: "first" }, undefined, undefined, f.ctx);
|
||
await waitFor(() => Boolean(release));
|
||
const concurrent = await f.tools.get("OpenGoalWorker").execute("concurrent", { task: "second" }, undefined, undefined, f.ctx);
|
||
expect(concurrent.content[0].text).toContain("already opening");
|
||
expect(openProjectPane).toHaveBeenCalledTimes(1);
|
||
await f.command("solo");
|
||
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("still pending"), "warning");
|
||
release(); expect((await opening).content[0].text).toContain("possible live writer");
|
||
const again = await f.tools.get("OpenGoalWorker").execute("open", { task: "again" }, undefined, undefined, f.ctx);
|
||
expect(again.content[0].text).toContain('"disposition":"opened"');
|
||
expect(openProjectPane).toHaveBeenCalledTimes(2);
|
||
});
|
||
|
||
it("leaves an existing stock pane unbound instead of replacing or retasking it", async () => {
|
||
const f = fixture(); await f.draft(); await f.command("ready");
|
||
vi.mocked(openProjectPane).mockResolvedValueOnce({ ok: true, data: { bindingPath: "/existing/binding.json", disposition: "already-open", binding: { paneId: "existing-pane", projectRoot: f.ctx.cwd, command: "pi" } } });
|
||
const before = f.messages.length;
|
||
const reply = await f.tools.get("OpenGoalWorker").execute("open", { task: "proposed work" }, undefined, undefined, f.ctx);
|
||
expect(reply.content[0].text).toContain("no startup was sent");
|
||
expect(f.entries.at(-1).data.worker).toMatchObject({ paneId: "existing-pane", projectRoot: f.ctx.cwd });
|
||
expect(f.entries.at(-1).data.worker.intercomId).toBeUndefined();
|
||
expect(f.messages).toHaveLength(before);
|
||
expect(f.channel.publish).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it.each(["inherit", "plan", "explicit"])("hands off %s model policy without claiming configuration or invoking an unavailable control", async policy => {
|
||
const f = fixture(); await f.draft();
|
||
if (policy !== "inherit") await f.command("model same model, low");
|
||
await f.command("ready");
|
||
const model = policy === "explicit" ? "missing/unavailable" : undefined;
|
||
const requested = model ?? (policy === "plan" ? "same model, low" : undefined);
|
||
await f.tools.get("OpenGoalWorker").execute("open", { task: "bounded work", model }, undefined, undefined, f.ctx);
|
||
const worker = f.entries.at(-1).data.worker;
|
||
expect(worker.intercomId).toBeUndefined();
|
||
const startup = vi.mocked(openProjectPane).mock.calls[0][0].message!;
|
||
if (requested) expect(startup).toContain(JSON.stringify(requested));
|
||
expect(vi.mocked(openProjectPane).mock.calls[0][0]).not.toHaveProperty("model");
|
||
const identity = { paneId: "observed-pane", sessionId: "11111111-1111-4111-8111-111111111111", sessionFile: join(f.ctx.cwd, "worker.jsonl"), model: "offline/inherited" };
|
||
writeFileSync(identity.sessionFile, [
|
||
{ type: "session", version: 3, id: identity.sessionId, timestamp: new Date().toISOString(), cwd: f.ctx.cwd },
|
||
{ type: "message", id: "inspected", parentId: null, timestamp: new Date().toISOString(), message: { role: "user", content: "Distinct runtime and Intercom identity evidence", timestamp: Date.now() } },
|
||
].map(entry => JSON.stringify(entry)).join("\n") + "\n");
|
||
f.event({ type: "message", fromSessionId: "worker", payload: { type: "attached", to: worker.parentId, requestId: worker.requestId, plan: f.path, sessionFile: identity.sessionFile, identity } });
|
||
await f.command("status");
|
||
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("Last observed worker model: offline/inherited"), "info");
|
||
expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("native pane: observed-pane"), "info");
|
||
const view = await f.tools.get("worker_view").execute("view", {}, undefined, undefined, f.ctx);
|
||
expect(view.content[0].text).toContain("Distinct runtime and Intercom identity evidence");
|
||
f.event({ type: "message", fromSessionId: "worker", payload: { type: "stopped", to: worker.parentId, requestId: worker.requestId, plan: f.path, entryId: "view-inspection", kind: "unclassified", text: "Inspection ended." } });
|
||
expect(f.messages.at(-1)?.message.content).toContain("Inspection ended.");
|
||
expect(f.messages.at(-1)?.message.content).not.toContain("Distinct runtime and Intercom identity evidence");
|
||
if (model) {
|
||
f.event({ type: "message", fromSessionId: "worker", payload: { type: "stopped", to: worker.parentId, requestId: worker.requestId, plan: f.path, entryId: "model-unavailable", kind: "progress", text: "Requested missing/unavailable is unavailable; unrelated work can continue." } });
|
||
expect(f.messages.at(-1)?.message.content).toContain("## Worker status: progress");
|
||
await f.command("status");
|
||
expect(f.ctx.ui.notify.mock.lastCall?.[0]).toContain("Selected worker-stop reviews pending delivery: none");
|
||
}
|
||
|
||
});
|