mirror of
https://github.com/wassname/pi-plan.git
synced 2026-09-26 14:10:23 +08:00
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.
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { backgroundState } from "../src/background.js";
|
||||
|
||||
function api(tools: string[], processes?: unknown, subagents?: number) {
|
||||
const bus = new EventEmitter();
|
||||
if (processes !== undefined) bus.on("processes:request:list", request => request.reply(processes));
|
||||
if (subagents !== undefined) bus.on("subagents:rpc:v1:request", request => bus.emit(`subagents:rpc:v1:reply:${request.requestId}`, { requestId: request.requestId, success: true, data: { fleet: { version: 1, totalActive: subagents } } }));
|
||||
return { getAllTools: () => tools.map(name => ({ name })), events: { emit: (name: string, value: unknown) => bus.emit(name, value), on: (name: string, fn: (...args: any[]) => void) => { bus.on(name, fn); return () => { bus.off(name, fn); }; } } } as unknown as ExtensionAPI;
|
||||
}
|
||||
|
||||
it("reports tracked running work, rather than equating idle agent with finished jobs", async () => {
|
||||
const active = await backgroundState(api(["process", "subagent"], [{ name: "generation", status: "running" }], 1));
|
||||
expect(active.quiet).toBe(false);
|
||||
expect(active.description).toContain("processes: 1 (generation)");
|
||||
expect(active.description).toContain("subagents: 1");
|
||||
const finished = await backgroundState(api(["process", "subagent"], [{ status: "exited" }], 0));
|
||||
expect(finished.quiet).toBe(true);
|
||||
});
|
||||
|
||||
it("distinguishes missing providers from an unavailable installed tracker", async () => {
|
||||
expect((await backgroundState(api([]))).quiet).toBe(true);
|
||||
expect(await backgroundState(api(["process"]))).toMatchObject({ quiet: false, description: expect.stringContaining("processes: unknown") });
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const unavailable = backgroundState(api(["subagent"]));
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
expect(await unavailable).toMatchObject({ quiet: false, description: expect.stringContaining("subagents: unknown") });
|
||||
} finally { vi.useRealTimers(); }
|
||||
});
|
||||
@@ -52,6 +52,17 @@ describe("pi-intercom transport", () => {
|
||||
expect(resumed.fixture.sent.filter(message => message.kind === "steer")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("advances the incremental overview only after acknowledgment", async () => {
|
||||
const runtime = setup("worker");
|
||||
await runtime.link.waitReady();
|
||||
const view = runtime.link.view("The worker stopped.", "settled", "entry-1", true);
|
||||
expect(runtime.link.acknowledgedEntry).toBeUndefined();
|
||||
runtime.fixture.receive({ binding: "binding", role: "supervisor", kind: "received", id: view.id });
|
||||
expect(runtime.link.acknowledgedEntry).toBe("entry-1");
|
||||
const resumed = setup("worker", [...runtime.entries]);
|
||||
expect(resumed.link.acknowledgedEntry).toBe("entry-1");
|
||||
});
|
||||
|
||||
it("cancels a readiness wait on shutdown", async () => {
|
||||
const runtime = setup("worker");
|
||||
await runtime.link.waitReady();
|
||||
|
||||
@@ -49,8 +49,8 @@ function setup(cwd: string, planPath: string, tokens: number | null = 10, onComp
|
||||
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") => {
|
||||
transport.receive({ binding: "approval-1", role: "worker", kind: "view", id, text, reason });
|
||||
view: (id: string, text: string, reason = "settled", backgroundQuiet = true) => {
|
||||
transport.receive({ binding: "approval-1", role: "worker", kind: "view", id, text, reason, backgroundQuiet });
|
||||
return { text };
|
||||
},
|
||||
};
|
||||
@@ -216,6 +216,11 @@ describe("visible supervisor session", () => {
|
||||
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 }); }
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { workerView } from "../src/worker-view.js";
|
||||
|
||||
const context = { sourceSession: "/sessions/worker.jsonl", latestDirection: "Modal uses a remote GPU.", model: "provider/worker", background: "processes: 0; subagents: 0" };
|
||||
const entry = (id: string, text: string) => ({ id, type: "message", message: { role: "assistant", content: text } });
|
||||
|
||||
it("keeps human direction and source location while sending only new messages", () => {
|
||||
const view = workerView([entry("old", "old detail"), entry("new", "new result")], "interval", true, { ...context, since: "old" });
|
||||
expect(view).toContain(context.latestDirection);
|
||||
expect(view).toContain(context.sourceSession);
|
||||
expect(view).toContain("new result");
|
||||
expect(view).not.toContain("old detail");
|
||||
});
|
||||
|
||||
it("restarts after compaction and does not report historical tool calls as active", () => {
|
||||
const entries = [
|
||||
{ id: "old", type: "message", message: { role: "assistant", content: [{ type: "toolCall", id: "call", name: "edit" }] } },
|
||||
{ id: "compact", type: "compaction", summary: "Saved worker account." },
|
||||
entry("new", "new result"),
|
||||
];
|
||||
const initial = workerView(entries, "interval", true, { ...context, since: "old" });
|
||||
expect(initial).toContain("Saved worker account.");
|
||||
expect(initial).toContain("tool calls with no result: none");
|
||||
const next = workerView(entries, "interval", true, { ...context, since: "new" });
|
||||
expect(next).not.toContain("Saved worker account.");
|
||||
expect(next).toContain("No new messages.");
|
||||
});
|
||||
|
||||
it("bounds serialized Unicode and quoted logs while marking omissions", () => {
|
||||
const view = workerView([
|
||||
{ id: "compact", type: "compaction", summary: '"\\🧪'.repeat(20_000) },
|
||||
entry("new", '"\\🧪'.repeat(20_000)),
|
||||
], "interval", true, { ...context, latestDirection: "Remote only. ".repeat(3000) });
|
||||
expect(Buffer.byteLength(JSON.stringify({ binding: "binding", role: "worker", kind: "view", id: "id", text: view }))).toBeLessThan(16_000);
|
||||
expect(view).toContain("[truncated; inspect source session]");
|
||||
expect(view).toContain(context.sourceSession);
|
||||
});
|
||||
Reference in New Issue
Block a user