chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
156
third_party/pi/packages/coding-agent/test/program-status-reporter.test.ts
vendored
Normal file
156
third_party/pi/packages/coding-agent/test/program-status-reporter.test.ts
vendored
Normal file
@@ -0,0 +1,156 @@
|
||||
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
||||
import type { ProgramStatus, Terminal } from "@earendil-works/pi-tui";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { AgentSessionEvent } from "../src/core/agent-session.ts";
|
||||
import { ProgramStatusReporter } from "../src/modes/interactive/program-status-reporter.ts";
|
||||
|
||||
function setup(sessionName?: string) {
|
||||
const reports: ProgramStatus[] = [];
|
||||
const terminal = { setProgramStatus: (status: ProgramStatus) => reports.push(status) } as unknown as Terminal;
|
||||
const session = { name: sessionName };
|
||||
const reporter = new ProgramStatusReporter(
|
||||
() => terminal,
|
||||
() => session.name,
|
||||
);
|
||||
const send = (...events: AgentSessionEvent[]) => {
|
||||
for (const event of events) reporter.handleEvent(event);
|
||||
};
|
||||
const last = () => {
|
||||
const { app: _app, ...status } = reports.at(-1)!;
|
||||
return Object.fromEntries(Object.entries(status).filter(([, value]) => value !== undefined));
|
||||
};
|
||||
return { reporter, reports, session, send, last };
|
||||
}
|
||||
|
||||
function assistantEnd(stopReason: AssistantMessage["stopReason"], errorMessage?: string): AgentSessionEvent {
|
||||
const message = {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "secret assistant output" }],
|
||||
stopReason,
|
||||
errorMessage,
|
||||
} as AssistantMessage;
|
||||
return { type: "message_end", message };
|
||||
}
|
||||
|
||||
const settled: AgentSessionEvent = { type: "agent_settled", aborted: false };
|
||||
|
||||
const compactionEnd = (
|
||||
reason: "manual" | "threshold" | "overflow",
|
||||
options: { aborted?: boolean; errorMessage?: string } = {},
|
||||
): AgentSessionEvent => ({
|
||||
type: "compaction_end",
|
||||
reason,
|
||||
result: undefined,
|
||||
aborted: options.aborted ?? false,
|
||||
willRetry: false,
|
||||
errorMessage: options.errorMessage,
|
||||
});
|
||||
|
||||
// #10607
|
||||
describe("ProgramStatusReporter", () => {
|
||||
it("reports idle, working during a run, and done once it settles", () => {
|
||||
const { reporter, reports, send, last } = setup("Fix login");
|
||||
reporter.report();
|
||||
expect(reports.at(-1)).toEqual({ state: "idle", app: "pi" });
|
||||
|
||||
send({ type: "agent_start" });
|
||||
expect(last()).toEqual({ state: "working", message: "Fix login" });
|
||||
|
||||
send(assistantEnd("toolUse"), assistantEnd("stop"));
|
||||
expect(last()).toEqual({ state: "working", message: "Fix login" });
|
||||
|
||||
send(settled);
|
||||
expect(last()).toEqual({ state: "done", message: "Fix login" });
|
||||
expect(JSON.stringify(reports)).not.toContain("secret assistant output");
|
||||
});
|
||||
|
||||
it("reports only the outcome of the run: retried errors, final errors, and aborts", () => {
|
||||
const { send, last } = setup();
|
||||
send({ type: "agent_start" }, assistantEnd("error", "overloaded"), assistantEnd("stop"), settled);
|
||||
expect(last()).toEqual({ state: "done" });
|
||||
|
||||
send({ type: "agent_start" }, assistantEnd("error", "Invalid API key\n{details}"), settled);
|
||||
expect(last()).toEqual({ state: "error", message: "Invalid API key" });
|
||||
|
||||
send({ type: "agent_start" }, assistantEnd("aborted"), { type: "agent_settled", aborted: true });
|
||||
expect(last()).toEqual({ state: "idle" });
|
||||
|
||||
// Aborted after a successful response, for example in an agent_before_settle hook or a retry delay.
|
||||
send({ type: "agent_start" }, assistantEnd("stop"), { type: "agent_settled", aborted: true });
|
||||
expect(last()).toEqual({ state: "idle" });
|
||||
});
|
||||
|
||||
it("reports a failed recovery compaction as the run's error unless a later response succeeds", () => {
|
||||
const { send, last } = setup();
|
||||
send(
|
||||
{ type: "agent_start" },
|
||||
assistantEnd("length"),
|
||||
{ type: "compaction_start", reason: "overflow" },
|
||||
compactionEnd("overflow", { errorMessage: "Compaction failed\nstack" }),
|
||||
settled,
|
||||
);
|
||||
expect(last()).toEqual({ state: "error", message: "Compaction failed" });
|
||||
|
||||
send(
|
||||
{ type: "agent_start" },
|
||||
{ type: "compaction_start", reason: "threshold" },
|
||||
compactionEnd("threshold", { errorMessage: "Compaction failed" }),
|
||||
assistantEnd("stop"),
|
||||
settled,
|
||||
);
|
||||
expect(last()).toEqual({ state: "done" });
|
||||
});
|
||||
|
||||
it("reports compaction inside a run and the result of a manual compaction", () => {
|
||||
const { send, last } = setup("Session");
|
||||
send({ type: "agent_start" }, { type: "compaction_start", reason: "threshold" });
|
||||
expect(last()).toEqual({ state: "working", message: "Compacting context" });
|
||||
send(compactionEnd("threshold"));
|
||||
expect(last()).toEqual({ state: "working", message: "Session" });
|
||||
send(assistantEnd("stop"), settled);
|
||||
|
||||
send({ type: "compaction_start", reason: "manual" }, compactionEnd("manual"));
|
||||
expect(last()).toEqual({ state: "done", message: "Session" });
|
||||
send({ type: "compaction_start", reason: "manual" }, compactionEnd("manual", { errorMessage: "No model" }));
|
||||
expect(last()).toEqual({ state: "error", message: "No model" });
|
||||
send({ type: "compaction_start", reason: "manual" }, compactionEnd("manual", { aborted: true }));
|
||||
expect(last()).toEqual({ state: "idle" });
|
||||
});
|
||||
|
||||
it("reports the most recent open dialog and the underlying state once all close", () => {
|
||||
const { reporter, send, last } = setup();
|
||||
send({ type: "agent_start" });
|
||||
reporter.setBlocked("extension-selector", { kind: "permission", message: "Allow bash?" });
|
||||
reporter.setBlocked("login", { kind: "auth", message: "Log in to Anthropic" });
|
||||
expect(last()).toEqual({ state: "blocked", kind: "auth", message: "Log in to Anthropic" });
|
||||
|
||||
// The run settles while the selector is still open.
|
||||
reporter.setBlocked("login", undefined);
|
||||
send(assistantEnd("stop"), settled);
|
||||
expect(last()).toEqual({ state: "blocked", kind: "permission", message: "Allow bash?" });
|
||||
|
||||
// Reopening a source replaces its dialog instead of stacking a second one.
|
||||
reporter.setBlocked("extension-selector", { kind: "question", message: "Pick one" });
|
||||
expect(last()).toEqual({ state: "blocked", kind: "question", message: "Pick one" });
|
||||
reporter.setBlocked("extension-selector", undefined);
|
||||
expect(last()).toEqual({ state: "done" });
|
||||
});
|
||||
|
||||
it("sends each status once and follows session name changes", () => {
|
||||
const { reporter, reports, session, send, last } = setup("Old");
|
||||
send({ type: "agent_start" }, { type: "turn_start" } as AgentSessionEvent, assistantEnd("toolUse"));
|
||||
reporter.report();
|
||||
expect(reports).toHaveLength(1);
|
||||
|
||||
session.name = "New";
|
||||
send({ type: "session_info_changed", name: "New" });
|
||||
expect(last()).toEqual({ state: "working", message: "New" });
|
||||
});
|
||||
|
||||
it("returns to idle when the session is replaced", () => {
|
||||
const { reporter, send, last } = setup();
|
||||
send({ type: "agent_start" }, assistantEnd("stop"), settled);
|
||||
reporter.reset();
|
||||
expect(last()).toEqual({ state: "idle" });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user