a15c39ae7d
- Exported `finalizeSubprocessOutput()` function and `SubmitResultItem` interface for subprocess output finalization with submit_result validation. - Added automatic reminders (up to 3) when subagent stops without calling submit_result tool, aborting with exit code 1 after final reminder. - Extracted subprocess output finalization logic into dedicated `finalizeSubprocessOutput()` function for improved testability and reusability. - Added comprehensive test coverage for subagent warning injection, reminder behavior, and abort handling in executor module.
137 lines
4.3 KiB
TypeScript
137 lines
4.3 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import {
|
|
finalizeSubprocessOutput,
|
|
SUBAGENT_WARNING_MISSING_SUBMIT_RESULT,
|
|
SUBAGENT_WARNING_NULL_SUBMIT_RESULT,
|
|
} from "../../src/task/executor";
|
|
|
|
describe("subagent warning injection", () => {
|
|
it("injects null-data warning when submit_result is success without data", () => {
|
|
const result = finalizeSubprocessOutput({
|
|
rawOutput: "partial output",
|
|
exitCode: 0,
|
|
stderr: "",
|
|
doneAborted: false,
|
|
signalAborted: false,
|
|
submitResultItems: [{ status: "success" }],
|
|
outputSchema: undefined,
|
|
});
|
|
|
|
expect(result.rawOutput).toBe(`${SUBAGENT_WARNING_NULL_SUBMIT_RESULT}\n\npartial output`);
|
|
expect(result.hasSubmitResult).toBe(true);
|
|
});
|
|
|
|
it("injects missing-submit warning when subagent exits cleanly without submit_result", () => {
|
|
const result = finalizeSubprocessOutput({
|
|
rawOutput: "",
|
|
exitCode: 0,
|
|
stderr: "",
|
|
doneAborted: false,
|
|
signalAborted: false,
|
|
submitResultItems: undefined,
|
|
outputSchema: { properties: { ok: { type: "boolean" } } },
|
|
});
|
|
|
|
expect(result.rawOutput).toBe(SUBAGENT_WARNING_MISSING_SUBMIT_RESULT);
|
|
expect(result.hasSubmitResult).toBe(false);
|
|
});
|
|
|
|
it("does not inject missing-submit warning when fallback completion is recoverable", () => {
|
|
const result = finalizeSubprocessOutput({
|
|
rawOutput: '{"data":{"ok":true}}',
|
|
exitCode: 0,
|
|
stderr: "",
|
|
doneAborted: false,
|
|
signalAborted: false,
|
|
submitResultItems: undefined,
|
|
outputSchema: { type: "object", properties: { ok: { type: "boolean" } }, required: ["ok"] },
|
|
});
|
|
|
|
expect(result.rawOutput).toBe('{\n "ok": true\n}');
|
|
expect(result.rawOutput.includes("SYSTEM WARNING")).toBe(false);
|
|
});
|
|
|
|
it("prefixes missing-submit warning on stop outputs", () => {
|
|
const result = finalizeSubprocessOutput({
|
|
rawOutput: "agent stopped after writing analysis",
|
|
exitCode: 0,
|
|
stderr: "",
|
|
doneAborted: false,
|
|
signalAborted: false,
|
|
submitResultItems: undefined,
|
|
outputSchema: { type: "object", properties: { ok: { type: "boolean" } }, required: ["ok"] },
|
|
});
|
|
|
|
expect(result.rawOutput).toBe(
|
|
`${SUBAGENT_WARNING_MISSING_SUBMIT_RESULT}\n\nagent stopped after writing analysis`,
|
|
);
|
|
});
|
|
|
|
it("does not inject missing-submit warning when execution exits non-zero", () => {
|
|
const result = finalizeSubprocessOutput({
|
|
rawOutput: "",
|
|
exitCode: 1,
|
|
stderr: "subagent terminated",
|
|
doneAborted: true,
|
|
signalAborted: false,
|
|
submitResultItems: undefined,
|
|
outputSchema: { type: "object", properties: { ok: { type: "boolean" } }, required: ["ok"] },
|
|
});
|
|
|
|
expect(result.rawOutput).toBe("");
|
|
expect(result.stderr).toBe("subagent terminated");
|
|
expect(result.exitCode).toBe(1);
|
|
});
|
|
|
|
it("normalizes explicit aborted submit_result into aborted payload", () => {
|
|
const result = finalizeSubprocessOutput({
|
|
rawOutput: "partial output",
|
|
exitCode: 1,
|
|
stderr: "old error",
|
|
doneAborted: false,
|
|
signalAborted: false,
|
|
submitResultItems: [{ status: "aborted", error: "blocked by permissions" }],
|
|
outputSchema: undefined,
|
|
});
|
|
|
|
expect(result.abortedViaSubmitResult).toBe(true);
|
|
expect(result.exitCode).toBe(0);
|
|
expect(result.stderr).toBe("blocked by permissions");
|
|
expect(result.rawOutput).toContain('"aborted": true');
|
|
expect(result.rawOutput).toContain('"blocked by permissions"');
|
|
});
|
|
|
|
it("accepts successful submit_result data without warning", () => {
|
|
const result = finalizeSubprocessOutput({
|
|
rawOutput: "should be replaced",
|
|
exitCode: 1,
|
|
stderr: "should clear",
|
|
doneAborted: false,
|
|
signalAborted: false,
|
|
submitResultItems: [{ status: "success", data: { ok: true } }],
|
|
outputSchema: undefined,
|
|
});
|
|
|
|
expect(result.rawOutput).toBe('{\n "ok": true\n}');
|
|
expect(result.exitCode).toBe(0);
|
|
expect(result.stderr).toBe("");
|
|
expect(result.rawOutput.includes("SYSTEM WARNING")).toBe(false);
|
|
});
|
|
|
|
it("does not inject missing-submit warning when no schema and raw text exists", () => {
|
|
const result = finalizeSubprocessOutput({
|
|
rawOutput: "plain text notes",
|
|
exitCode: 0,
|
|
stderr: "",
|
|
doneAborted: false,
|
|
signalAborted: false,
|
|
submitResultItems: undefined,
|
|
outputSchema: undefined,
|
|
});
|
|
|
|
expect(result.rawOutput).toBe("plain text notes");
|
|
expect(result.rawOutput.includes("SYSTEM WARNING")).toBe(false);
|
|
expect(result.exitCode).toBe(0);
|
|
});
|
|
});
|