Files
pi-goals/test/supervisor-session.test.ts
T
wassname 47cc054582 Send incremental worker views with direction and tracked job state
Borrow the provider tracker queries from cecb1e9. Keep unavailable state unknown, bind incremental views to acknowledged source entries, reset after compaction, bound serialized payloads, and recheck tracked work at sign-off.
2026-09-08 16:41:47 +08:00

227 lines
12 KiB
TypeScript

import { execFileSync } from "node:child_process";
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { stripVTControlCharacters } from "node:util";
import { AssistantMessageComponent, type ExtensionAPI, initTheme, ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
import { afterEach, describe, expect, it, vi } from "vitest";
import { approvalPath } from "../src/approval.js";
import { registerVisibleSupervisor } from "../src/supervisor-session.js";
import { intercomFixture } from "./intercom-fixture.js";
const shutdowns: Array<() => Promise<void>> = [];
function setup(cwd: string, planPath: string, tokens: number | null = 10, onCompact: (options: any) => void = (options) => options.onComplete()) {
const transport = intercomFixture();
vi.stubEnv("PI_GOALS_WORKER_ID", "worker-session");
vi.stubEnv("PI_GOALS_OWNER_SESSION_ID", "worker-session");
vi.stubEnv("PI_GOALS_PLAN_PATH", planPath);
vi.stubEnv("PI_GOALS_APPROVAL_ID", "approval-1");
const hooks = new Map<string, any>();
const tools = new Map<string, any>();
const entries: any[] = [];
const messages: string[] = [];
let branch: any[] = [];
let activeTools = ["read", "grep", "bash", "write", "edit"];
const ctx = {
cwd,
getSystemPrompt: () => "base",
getContextUsage: () => tokens === null ? undefined : ({ tokens }),
compact: vi.fn(onCompact),
sessionManager: { getEntries: () => entries, getBranch: () => branch, getSessionId: () => "supervisor-session" },
ui: { notify: vi.fn() },
};
const pi = {
events: transport.events,
on: (name: string, handler: any) => {
const prior = hooks.get(name);
hooks.set(name, async (...args: any[]) => { await prior?.(...args); return handler(...args); });
},
registerTool: (tool: any) => tools.set(tool.name, tool),
appendEntry: (customType: string, data: unknown) => entries.push({ type: "custom", customType, data }),
sendUserMessage: (message: string) => messages.push(message),
getActiveTools: () => activeTools,
setActiveTools: (next: string[]) => { activeTools = next; },
};
registerVisibleSupervisor(pi as unknown as ExtensionAPI);
shutdowns.push(() => hooks.get("session_shutdown")());
return {
activeTools: () => activeTools, branch: (value: any[]) => { branch = value; }, ctx, entries, hooks, transport, messages, tools,
ready: () => transport.sent.some(message => message.kind === "hello" && message.role === "supervisor" && message.ready),
start: async () => { await hooks.get("session_start")({}, ctx); await new Promise(resolve => setImmediate(resolve)); },
view: (id: string, text: string, reason = "settled", backgroundQuiet = true) => {
transport.receive({ binding: "approval-1", role: "worker", kind: "view", id, text, reason, backgroundQuiet });
return { text };
},
};
}
afterEach(async () => {
for (const shutdown of shutdowns.splice(0)) await shutdown();
vi.useRealTimers();
vi.unstubAllEnvs();
});
describe("visible supervisor session", () => {
it("restores monitoring and read-only tools without replaying persisted views", async () => {
const cwd = mkdtempSync(join(tmpdir(), "pi-goals-resume-"));
try {
const first = setup(cwd, join(cwd, "plan.md"));
await first.start();
const view = first.view("first", "The worker stopped.");
expect(first.messages).toEqual([view.text]);
await first.hooks.get("session_shutdown")();
const resumed = setup(cwd, join(cwd, "plan.md"), 30_000);
resumed.entries.push(...first.entries);
await resumed.start();
expect(resumed.activeTools()).toEqual(["read", "grep"]);
expect(resumed.ctx.compact).not.toHaveBeenCalled();
resumed.view("first", view.text);
expect(resumed.messages).toEqual([]);
const latest = resumed.view("second", "The worker stopped.\nCurrent view.");
expect(resumed.messages).toEqual([latest.text]);
resumed.view("third", "The worker is still working.", "started");
expect(resumed.messages).toEqual([latest.text]);
} finally { rmSync(cwd, { recursive: true, force: true }); }
});
it("renders all advice in real Pi tool rows, including collapsed and restored rows", () => {
initTheme("dark");
const cwd = mkdtempSync(join(tmpdir(), "pi-goals-render-"));
try {
const runtime = setup(cwd, join(cwd, "plan.md"));
const tool = runtime.tools.get("SteerWorker");
const lines = Array.from({ length: 18 }, (_, i) => `Advice ${i + 1}: inspect evidence.`);
const instruction = lines.join("\n");
for (const restored of [false, true]) {
const row = new ToolExecutionComponent("SteerWorker", "call", restored ? { instruction } : {}, {}, tool, { requestRender() {} } as any, cwd);
expect(stripVTControlCharacters(row.render(40).join("\n"))).not.toContain("undefined");
row.updateArgs({ instruction });
row.setArgsComplete();
row.updateResult({ content: [{ type: "text", text: "Receipt unconfirmed." }], isError: false });
for (const expanded of [false, true]) {
row.setExpanded(expanded);
for (const width of [40, 100]) {
const output = stripVTControlCharacters(row.render(width).join("\n"));
for (const line of lines) expect(output).toContain(line);
expect(output).toContain("Receipt unconfirmed.");
}
}
}
const assistant = new AssistantMessageComponent(undefined, false);
for (const streaming of [true, false]) {
assistant.updateContent({ role: "assistant", content: [
{ type: "thinking", thinking: "The signs disagree. Inspect the outputs." },
{ type: "text", text: "Progress is mixed; the second check still fails." },
{ type: "toolCall", id: "call", name: "SteerWorker", arguments: { instruction } },
] } as any, streaming);
const output = stripVTControlCharacters(assistant.render(100).join("\n"));
expect(output).toContain("The signs disagree.");
expect(output).toContain("Progress is mixed;");
}
} finally { rmSync(cwd, { recursive: true, force: true }); }
});
it("asks for judgment and useful recaps without inventing instructions", async () => {
const cwd = mkdtempSync(join(tmpdir(), "pi-goals-prompt-"));
try {
const runtime = setup(cwd, join(cwd, "plan.md"));
const { systemPrompt } = await runtime.hooks.get("before_agent_start")({}, runtime.ctx);
expect(systemPrompt).toContain("brief visible recap");
expect(systemPrompt).toContain("your judgment");
expect(systemPrompt).toContain("justified confidence, not certainty at any cost");
expect(systemPrompt).toContain('Treat "blocked", "waiting", "impossible", and "already done" as claims to verify');
expect(systemPrompt).toContain("check whether it applies to this task");
expect(systemPrompt).toContain("Modal remote-GPU job");
expect(systemPrompt).toContain("without unpausing the shared queue, duplicating a paid job, or exceeding the approved budget");
expect(systemPrompt).toContain("what event will resume progress and how it will be observed");
expect(systemPrompt).toContain("after checking what is already authorized");
expect(systemPrompt).toContain("Challenge success claims as carefully as blocker claims");
expect(systemPrompt).toContain("verbatim evidence with a source path or link");
expect(systemPrompt).toContain("not independent evidence");
expect(systemPrompt).toContain("outcomes distinguish them");
expect(systemPrompt).toContain("One failed implementation does not refute the idea");
expect(systemPrompt).toContain("do not invent work");
expect(systemPrompt).toContain("stop issuing instructions");
} finally { rmSync(cwd, { recursive: true, force: true }); }
});
it("writes readiness only after removing writing tools", async () => {
const cwd = mkdtempSync(join(tmpdir(), "pi-goals-supervisor-"));
try {
const runtime = setup(cwd, join(cwd, ".pi/plan/worker-v1.md"));
await runtime.hooks.get("session_start")({}, runtime.ctx);
await new Promise((resolve) => setImmediate(resolve));
expect(runtime.ctx.compact).not.toHaveBeenCalled();
expect(runtime.ready()).toBe(true);
expect(runtime.activeTools()).toEqual(["read", "grep"]);
expect(runtime.entries.at(-1)).toMatchObject({ customType: "pi-goals-visible-supervisor-v2" });
} finally { rmSync(cwd, { recursive: true, force: true }); }
});
it("compacts a large planning fork before writing readiness", async () => {
const cwd = mkdtempSync(join(tmpdir(), "pi-goals-supervisor-"));
try {
let complete: (() => void) | undefined;
const runtime = setup(cwd, join(cwd, ".pi/plan/worker-v1.md"), 20_001, (options) => { complete = options.onComplete; });
await runtime.hooks.get("session_start")({}, runtime.ctx);
await new Promise((resolve) => setImmediate(resolve));
expect(runtime.ctx.compact).toHaveBeenCalledOnce();
expect(runtime.ready()).toBe(false);
complete!();
expect(runtime.ready()).toBe(true);
} finally { rmSync(cwd, { recursive: true, force: true }); }
});
it("does not become ready when initial compaction fails", async () => {
const cwd = mkdtempSync(join(tmpdir(), "pi-goals-supervisor-"));
try {
const runtime = setup(cwd, join(cwd, ".pi/plan/worker-v1.md"), null, (options) => options.onError(new Error("offline")));
await runtime.hooks.get("session_start")({}, runtime.ctx);
await new Promise((resolve) => setImmediate(resolve));
expect(runtime.ready()).toBe(false);
expect(runtime.ctx.ui.notify).toHaveBeenCalledWith("Supervisor startup compaction failed: offline", "error");
} finally { rmSync(cwd, { recursive: true, force: true }); }
});
it("writes a durable worker instruction", async () => {
const cwd = mkdtempSync(join(tmpdir(), "pi-goals-supervisor-"));
try {
const runtime = setup(cwd, join(cwd, "plan.md"));
await runtime.start();
const steered = await runtime.tools.get("SteerWorker").execute("id", { instruction: "Run the saved verification." });
expect(steered.isError).toBe(false);
expect(runtime.transport.sent.filter(message => message.kind === "steer")).toMatchObject([{ text: "Run the saved verification." }]);
} finally { rmSync(cwd, { recursive: true, force: true }); }
});
it("records approval only from a stopped view with evidence and no active work", async () => {
const cwd = mkdtempSync(join(tmpdir(), "pi-goals-supervisor-"));
try {
writeFileSync(join(cwd, ".gitignore"), ".pi/\n");
writeFileSync(join(cwd, "verify.txt"), "PASS\n");
execFileSync("git", ["init", "-q"], { cwd });
execFileSync("git", ["add", ".gitignore", "verify.txt"], { cwd });
execFileSync("git", ["-c", "user.name=test", "-c", "user.email=test@example.com", "commit", "-qm", "initial"], { cwd });
const planPath = join(cwd, ".pi/plan/worker-v1.md");
execFileSync("mkdir", ["-p", join(cwd, ".pi/plan")]);
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [ ] goal: make the file\n - discriminator: output exists\n - evidence:\n - `result.txt`: contains ok\n\n## Log\n");
const runtime = setup(cwd, planPath);
await runtime.start();
const view = runtime.view("first", "The worker stopped.\n\ntool calls with no result: none");
runtime.branch([{ type: "message", message: { role: "user", content: [{ type: "text", text: view.text }] } }]);
const approved = await runtime.tools.get("ApproveGoal").execute("id", { goal: "make the file", verifyOutputPath: "verify.txt" }, undefined, undefined, runtime.ctx);
expect(approved.isError).toBe(false);
expect(existsSync(approvalPath(cwd, "worker-session", "make the file"))).toBe(true);
runtime.view("second", "The worker is still working.", "started");
const stale = await runtime.tools.get("ApproveGoal").execute("id", { goal: "make the file", verifyOutputPath: "verify.txt" }, undefined, undefined, runtime.ctx);
expect(stale.isError).toBe(true);
expect(stale.content[0].text).toContain("latest worker view");
const unknown = runtime.view("third", "The worker stopped.\ntracked background work: unknown", "settled", false);
runtime.branch([{ type: "message", message: { role: "user", content: [{ type: "text", text: unknown.text }] } }]);
const blocked = await runtime.tools.get("ApproveGoal").execute("id", { goal: "make the file", verifyOutputPath: "verify.txt" }, undefined, undefined, runtime.ctx);
expect(blocked.isError).toBe(true);
expect(blocked.content[0].text).toContain("background work is active or unknown");
} finally { rmSync(cwd, { recursive: true, force: true }); }
});
});