Squashed 'third_party/pi/' content from commit d981de122
git-subtree-dir: third_party/pi git-subtree-split: d981de1229ef899957bbe968bc8dcda02a21f477
This commit is contained in:
151
packages/agent/test/harness/output-capture.test.ts
Normal file
151
packages/agent/test/harness/output-capture.test.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { BACKGROUND_CONTEXT } from "../../src/harness/context.ts";
|
||||
import type { ShellOutputUpdate, ShellOutputView } from "../../src/harness/types.ts";
|
||||
import { applyShellOutputUpdate, OutputCapture, sanitizeShellOutput } from "../../src/harness/utils/output-capture.ts";
|
||||
|
||||
function createCapture(options?: { maxBytes?: number; maxLines?: number; retain?: "head" | "tail" }) {
|
||||
const updates: ShellOutputUpdate[] = [];
|
||||
const errors: unknown[] = [];
|
||||
const capture = new OutputCapture(
|
||||
{
|
||||
limits: {
|
||||
maxBytes: options?.maxBytes ?? 50,
|
||||
maxLines: options?.maxLines ?? 100,
|
||||
retain: options?.retain ?? "tail",
|
||||
},
|
||||
},
|
||||
BACKGROUND_CONTEXT,
|
||||
{
|
||||
onUpdate: (update) => updates.push(update),
|
||||
onError: (error) => errors.push(error),
|
||||
},
|
||||
);
|
||||
return { capture, updates, errors };
|
||||
}
|
||||
|
||||
function fold(updates: ShellOutputUpdate[]): ShellOutputView | undefined {
|
||||
let output: ShellOutputView | undefined;
|
||||
for (const update of updates) output = applyShellOutputUpdate(output, update);
|
||||
return output;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("OutputCapture", () => {
|
||||
it("removes invalid control characters without changing text or line boundaries", () => {
|
||||
const input = "a\0b\tc\nd\re\u0007f\ufff9g\ufffbh😀";
|
||||
expect(sanitizeShellOutput(input)).toBe("ab\tc\ndefgh😀");
|
||||
const { capture } = createCapture();
|
||||
capture.push(input);
|
||||
expect(capture.snapshot().text).toBe("ab\tc\ndefgh😀");
|
||||
});
|
||||
it("decodes UTF-8 split across raw process chunks", () => {
|
||||
const { capture } = createCapture();
|
||||
const bytes = new TextEncoder().encode("😀");
|
||||
capture.push(bytes.subarray(0, 2));
|
||||
expect(capture.snapshot().text).toBe("");
|
||||
capture.push(bytes.subarray(2));
|
||||
capture.finish();
|
||||
expect(capture.snapshot().text).toBe("😀");
|
||||
});
|
||||
|
||||
it("publishes the first bounded view immediately and trickling appends responsively", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(0);
|
||||
const { capture, updates } = createCapture();
|
||||
|
||||
capture.push("one");
|
||||
expect(updates).toHaveLength(1);
|
||||
expect(updates[0]?.kind).toBe("replace");
|
||||
|
||||
vi.advanceTimersByTime(150);
|
||||
capture.push(" two");
|
||||
expect(updates).toHaveLength(2);
|
||||
expect(updates[1]).toMatchObject({ kind: "append", text: " two" });
|
||||
expect(fold(updates)?.text).toBe("one two");
|
||||
});
|
||||
|
||||
it("collapses a burst into one trailing update", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(0);
|
||||
const { capture, updates } = createCapture();
|
||||
capture.push("a");
|
||||
capture.push("b");
|
||||
capture.push("c");
|
||||
expect(updates).toHaveLength(1);
|
||||
|
||||
vi.advanceTimersByTime(100);
|
||||
expect(updates).toHaveLength(2);
|
||||
expect(updates[1]).toMatchObject({ kind: "append", text: "bc" });
|
||||
expect(fold(updates)?.text).toBe("abc");
|
||||
});
|
||||
|
||||
it("publishes a small slide for post-cap trickle", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(0);
|
||||
const { capture, updates } = createCapture({ maxBytes: 10 });
|
||||
capture.push("abcdefghij");
|
||||
vi.advanceTimersByTime(150);
|
||||
capture.push("k");
|
||||
|
||||
expect(updates[1]).toMatchObject({ kind: "slide", drop: 1, text: "k" });
|
||||
expect(fold(updates)?.text).toBe("bcdefghijk");
|
||||
expect(fold(updates)?.truncation.totalBytes).toBe(11);
|
||||
});
|
||||
|
||||
it("keeps the exact byte count for a single line larger than its working buffer", () => {
|
||||
vi.useFakeTimers();
|
||||
const { capture } = createCapture({ maxBytes: 10 });
|
||||
capture.push("x".repeat(100));
|
||||
expect(capture.snapshot()).toMatchObject({
|
||||
text: "x".repeat(10),
|
||||
lastLineBytes: 100,
|
||||
truncation: { lastLinePartial: true },
|
||||
});
|
||||
});
|
||||
|
||||
it("uses a cap-bounded replacement after complete turnover", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(0);
|
||||
const { capture, updates } = createCapture({ maxBytes: 10 });
|
||||
capture.push("abcdefghij");
|
||||
capture.push("x".repeat(100));
|
||||
vi.advanceTimersByTime(100);
|
||||
|
||||
expect(updates[1]?.kind).toBe("replace");
|
||||
expect(fold(updates)?.text).toHaveLength(10);
|
||||
expect(fold(updates)?.truncation.totalBytes).toBe(110);
|
||||
});
|
||||
|
||||
it("forces held state and cancels its trailing timer on dispose", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(0);
|
||||
const { capture, updates } = createCapture();
|
||||
capture.push("a");
|
||||
capture.push("b");
|
||||
capture.flush();
|
||||
expect(fold(updates)?.text).toBe("ab");
|
||||
capture.dispose();
|
||||
vi.advanceTimersByTime(1_000);
|
||||
expect(updates).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("preserves the original head after its raw guard is crossed", () => {
|
||||
vi.useFakeTimers();
|
||||
const { capture } = createCapture({ maxBytes: 100, maxLines: 2, retain: "head" });
|
||||
capture.push(`first\nsecond\n${"tail".repeat(100)}`);
|
||||
expect(capture.snapshot().text).toBe("first\nsecond");
|
||||
});
|
||||
|
||||
it("publishes spill metadata without resending text", () => {
|
||||
vi.useFakeTimers();
|
||||
const { capture, updates, errors } = createCapture();
|
||||
capture.push("output");
|
||||
capture.setSpillPath("/tmp/output.log");
|
||||
expect(updates.at(-1)).toMatchObject({ kind: "metadata", metadata: { spillPath: "/tmp/output.log" } });
|
||||
expect(fold(updates)?.spillPath).toBe("/tmp/output.log");
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user