pi-mono/packages/coding-agent/test/input-transform-streaming-example.test.ts
Christian Klotz c466a35957
fix(coding-agent): cap pi.exec output at 256 MiB (#11546)
Closes #11360.

## Summary
- Cap combined stdout/stderr with `ExecOptions.maxBuffer`, defaulting to
the smaller of 256 MiB and the runtime string limit. Invalid explicit
limits reject before spawning.
- Terminate oversized commands and return `truncated: true`, `killed:
true`, and a nonzero exit code. Close output pipes so writing
descendants cannot keep the call pending, and discard incomplete UTF-8
suffixes rather than adding replacement characters.
- Fix SIGKILL escalation for children that ignore SIGTERM, and simplify
completion to `await` with one cleanup block and result.
- Document the API and add regressions for byte limits, shell
descendants, UTF-8 chunk boundaries, and 32-bit runtime limits.

`ExecResult.truncated` is required. Existing extensions reading
execution results continue working; typed mocks or wrappers constructing
results must add the field.

## Verification
- `npm run build` and the post-rebase `npm run build:offline`
- `npm run check`
- 39 focused execution and extension tests
- `./test.sh` passes after building the env daemon. Earlier runs hit
remote-cleanup and reftable-watcher timeouts; both passed in focused
reruns and the final full run.
2026-10-29 00:13:41 +02:00

84 lines
2.9 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import inputTransformStreaming from "../examples/extensions/input-transform-streaming.ts";
import type {
ExecResult,
ExtensionAPI,
ExtensionContext,
InputEvent,
InputEventResult,
} from "../src/core/extensions/index.ts";
type InputHandler = (event: InputEvent, ctx: ExtensionContext) => Promise<InputEventResult | undefined>;
function setup(execResult: ExecResult) {
let handler: InputHandler | undefined;
const exec = vi.fn<ExtensionAPI["exec"]>().mockResolvedValue(execResult);
const api = {
on: (event: string, h: InputHandler) => {
if (event === "input") handler = h;
},
exec,
} as unknown as ExtensionAPI;
inputTransformStreaming(api);
const ctx = {} as ExtensionContext;
function emit(text: string, streamingBehavior?: "steer" | "followUp") {
return handler!({ type: "input", text, source: "interactive", streamingBehavior }, ctx);
}
return { emit, exec };
}
describe("input-transform-streaming example", () => {
const diffOutput = " src/index.ts | 5 ++---\n 1 file changed, 2 insertions(+), 3 deletions(-)";
const gitSuccess: ExecResult = { stdout: diffOutput, stderr: "", code: 0, killed: false, truncated: false };
const gitEmpty: ExecResult = { stdout: "", stderr: "", code: 0, killed: false, truncated: false };
const gitFail: ExecResult = { stdout: "", stderr: "not a git repo", code: 128, killed: false, truncated: false };
it("skips exec during steering", async () => {
const { emit, exec } = setup(gitSuccess);
const result = await emit("what changes did I make?", "steer");
expect(result).toEqual({ action: "continue" });
expect(exec).not.toHaveBeenCalled();
});
it("transforms when idle and text matches trigger", async () => {
const { emit, exec } = setup(gitSuccess);
const result = await emit("review my changes");
expect(exec).toHaveBeenCalledWith("git", ["diff", "--stat"]);
expect(result).toMatchObject({ action: "transform" });
const text = (result as { text: string }).text;
expect(text).toContain("review my changes");
expect(text).toContain("src/index.ts");
});
it("transforms when queued as follow-up", async () => {
const { emit, exec } = setup(gitSuccess);
const result = await emit("show me the diff", "followUp");
expect(exec).toHaveBeenCalled();
expect(result).toMatchObject({ action: "transform" });
});
it("continues when text does not match trigger", async () => {
const { emit, exec } = setup(gitSuccess);
const result = await emit("explain this function");
expect(result).toEqual({ action: "continue" });
expect(exec).not.toHaveBeenCalled();
});
it("continues when git diff is empty", async () => {
const { emit } = setup(gitEmpty);
const result = await emit("any changes?");
expect(result).toEqual({ action: "continue" });
});
it("continues when git fails", async () => {
const { emit } = setup(gitFail);
const result = await emit("show modified files");
expect(result).toEqual({ action: "continue" });
});
});