mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
feat: enable permission-aware subagents
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 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:");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user