mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
109 lines
4.6 KiB
TypeScript
109 lines
4.6 KiB
TypeScript
/**
|
|
* subagent-error-status-e2e.test.ts — regression for issue #144: a subagent
|
|
* whose final assistant turn is a provider error must be reported as a
|
|
* failure, not as "completed" with an empty (or stale) result.
|
|
*
|
|
* Full-stack: real pi loader + real extension + real runAgent + real child
|
|
* sessions on a faux model. Faux is the point, not a shortcut — the scenario is
|
|
* a provider error with zero content, which no live model will produce on
|
|
* request. Each run pins `live: false` so the pre-publish smoke's global
|
|
* `PI_E2E_LIVE=1` can't swap a real model in and turn this suite red.
|
|
*/
|
|
import { fauxAssistantMessage, fauxText, fauxToolCall } from "@earendil-works/pi-ai";
|
|
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
agentCall,
|
|
type PrintModeRun,
|
|
routeBySession,
|
|
runPrintMode,
|
|
} from "./helpers/print-mode-runner.js";
|
|
|
|
/** Text of the parent's Agent tool result — what the orchestrator LLM sees. */
|
|
function agentToolResult(session: AgentSession): string {
|
|
const msg = [...session.messages].reverse().find(
|
|
(m) => m.role === "toolResult" && (m as { toolName?: string }).toolName === "Agent",
|
|
);
|
|
return ((msg?.content ?? []) as Array<{ text?: string }>).map((b) => b.text ?? "").join("");
|
|
}
|
|
|
|
vi.setConfig({ testTimeout: 30_000 });
|
|
|
|
// Not matched by pi's transient-error patterns → no auto-retry, deterministic.
|
|
const FATAL = "invalid request: provider rejected the prompt";
|
|
|
|
describe("issue #144 — empty-error final turns must not be 'completed'", () => {
|
|
let run: PrintModeRun | undefined;
|
|
afterEach(async () => {
|
|
await run?.dispose();
|
|
run = undefined;
|
|
});
|
|
|
|
it("a run whose ONLY turn errors with no output is a failure, not an empty success", async () => {
|
|
run = await runPrintMode({
|
|
prompt: "Delegate.",
|
|
respond: routeBySession({
|
|
parentInitial: agentCall({ run_in_background: false, description: "doomed", prompt: "Do work." }),
|
|
parentFinal: "parent done",
|
|
// The child's one and only turn: provider error, zero content.
|
|
subagent: () => fauxAssistantMessage([], { stopReason: "error", errorMessage: FATAL }),
|
|
}),
|
|
live: false,
|
|
});
|
|
|
|
// DESIRED: the orchestrator sees a failure naming the provider error —
|
|
// not a clean success reading "No output.".
|
|
const toolResult = agentToolResult(run.parentSession);
|
|
expect(toolResult).toContain(FATAL);
|
|
expect(toolResult).not.toContain("No output.");
|
|
});
|
|
|
|
it("an earlier turn's text must not mask a failed final turn as a fresh success", async () => {
|
|
run = await runPrintMode({
|
|
prompt: "Delegate.",
|
|
respond: routeBySession({
|
|
parentInitial: agentCall({ run_in_background: false, description: "masked", prompt: "Do work." }),
|
|
parentFinal: "parent done",
|
|
subagent: (ctx) => {
|
|
const hasToolResult = ctx.messages.some((m) => m.role === "toolResult");
|
|
// Turn 1: real text + a tool call. Turn 2 (after the tool result):
|
|
// provider error with zero content.
|
|
return hasToolResult
|
|
? fauxAssistantMessage([], { stopReason: "error", errorMessage: FATAL })
|
|
: fauxAssistantMessage([
|
|
fauxText("EARLIER-PARTIAL-TEXT"),
|
|
fauxToolCall("bash", { command: "echo hi" }),
|
|
]);
|
|
},
|
|
}),
|
|
live: false,
|
|
});
|
|
|
|
// The orchestrator sees the failure (not the earlier text as a clean
|
|
// answer), AND the partial output is salvaged, clearly labeled as
|
|
// pre-failure so it can't be mistaken for the final answer.
|
|
const toolResult = agentToolResult(run.parentSession);
|
|
expect(toolResult).toContain(FATAL);
|
|
expect(toolResult).toContain("Partial output before the failure:");
|
|
expect(toolResult).toContain("EARLIER-PARTIAL-TEXT");
|
|
// The failure headline comes before the salvaged partial output.
|
|
expect(toolResult.indexOf(FATAL)).toBeLessThan(toolResult.indexOf("EARLIER-PARTIAL-TEXT"));
|
|
});
|
|
|
|
it("a pure empty-error run shows no 'partial output' section", async () => {
|
|
run = await runPrintMode({
|
|
prompt: "Delegate.",
|
|
respond: routeBySession({
|
|
parentInitial: agentCall({ run_in_background: false, description: "empty", prompt: "Do work." }),
|
|
parentFinal: "parent done",
|
|
subagent: () => fauxAssistantMessage([], { stopReason: "error", errorMessage: FATAL }),
|
|
}),
|
|
live: false,
|
|
});
|
|
|
|
const toolResult = agentToolResult(run.parentSession);
|
|
expect(toolResult).toContain(FATAL);
|
|
expect(toolResult).not.toContain("Partial output before the failure:");
|
|
});
|
|
});
|