chore(vendor): import pi 1.3.0

Imported as a linear vendor commit so forge rebases do not replay the upstream root tree.

git-subtree-dir: third_party/pi

git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
2026-10-15 17:58:48 -04:00
parent 719370c8f7
commit 6081e3e325
1743 changed files with 205460 additions and 131705 deletions

View File

@@ -4,7 +4,7 @@ import { tmpdir } from "os";
import { join } from "path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { executeBashWithOperations } from "../src/core/bash-executor.ts";
import type { ExtensionContext } from "../src/core/extensions/types.ts";
import type { ExtensionToolContext } from "../src/core/extensions/types.ts";
import {
type BashOperations,
createBashTool,
@@ -89,6 +89,7 @@ describe("Coding Agent Tools", () => {
// No truncation message since file fits within limits
expect(getTextOutput(result)).not.toContain("Use offset=");
expect(result.details).toBeUndefined();
expect(result.structuredContent).toBe(content);
});
it("should handle non-existent files", async () => {
@@ -217,6 +218,13 @@ describe("Coding Agent Tools", () => {
expect(imageBlock?.mimeType).toBe("image/png");
expect(typeof imageBlock?.data).toBe("string");
expect((imageBlock?.data ?? "").length).toBeGreaterThan(0);
// Programmatic callers get the same image as a block. https://github.com/earendil-works/pi/issues/10251
expect(result.structuredContent).toEqual({
type: "image",
data: imageBlock?.data,
mimeType: "image/png",
note: getTextOutput(result),
});
});
it("should read BMP files from disk as PNG image attachments", async () => {
@@ -490,9 +498,95 @@ describe("Coding Agent Tools", () => {
expect(result.details).toBeUndefined();
});
it("should handle command errors", async () => {
await expect(bashTool.execute("test-call-9", { command: "exit 1" })).rejects.toThrow(
/(Command failed|code 1)/,
it("should report non-zero exit codes as error results with structured content", async () => {
const result = await bashTool.execute("test-call-9", { command: "echo out; exit 3" });
expect(result.isError).toBe(true);
expect(getTextOutput(result)).toBe("out\n\n\nCommand exited with code 3");
expect(result.structuredContent).toEqual({
output: "out\n",
truncated: false,
exit_code: 3,
wall_time_seconds: expect.any(Number),
});
const ok = await bashTool.execute("test-call-9b", { command: "echo fine" });
expect(ok.isError).toBeUndefined();
expect(ok.structuredContent).toMatchObject({ output: "fine\n", exit_code: 0 });
const empty = await bashTool.execute("test-call-9c", { command: "true" });
expect(getTextOutput(empty)).toBe("(no output)");
expect(empty.structuredContent).toMatchObject({ output: "", truncated: false });
});
it("should return up to 1 MiB of output in structured content", async () => {
// 3000 lines exceed the model-facing 2000 line limit but not 1 MiB.
const medium = await bashTool.execute("test-call-9d", { command: "seq 1 3000" });
expect(getTextOutput(medium)).not.toContain("\n1\n2\n");
expect(medium.details?.truncation?.truncated).toBe(true);
const mediumOutput = medium.structuredContent as { output: string; truncated: boolean };
expect(mediumOutput.truncated).toBe(false);
expect(mediumOutput.output).toBe(`${Array.from({ length: 3000 }, (_, i) => i + 1).join("\n")}\n`);
// About 2 MB: keeps the first and last 512 KiB around an omission marker.
const large = await bashTool.execute("test-call-9e", { command: "seq 1 300000" });
const largeOutput = large.structuredContent as {
output: string;
truncated: boolean;
full_output_path?: string;
};
expect(largeOutput.truncated).toBe(true);
expect(largeOutput.output.startsWith("1\n2\n3\n")).toBe(true);
expect(largeOutput.output.endsWith("299999\n300000\n")).toBe(true);
expect(largeOutput.output).toMatch(/\n\n\[\.\.\. \d+ bytes omitted \.\.\.\]\n\n/);
expect(Buffer.byteLength(largeOutput.output)).toBeLessThan(1024 * 1024 + 100);
expect(largeOutput.full_output_path).toBe(large.details?.fullOutputPath);
expect(readFileSync(largeOutput.full_output_path!, "utf-8").endsWith("300000\n")).toBe(true);
});
// Regression tests for https://github.com/earendil-works/pi/issues/9577
it.skipIf(process.platform === "win32")(
"should map signal-killed commands to 128 plus the signal number",
async () => {
const operations = createLocalBashOperations();
for (const { signal, exitCode } of [
{ signal: "KILL", exitCode: 137 },
{ signal: "TERM", exitCode: 143 },
]) {
const result = await operations.exec(`kill -${signal} $$`, testDir, { onData: () => {} });
expect(result.exitCode).toBe(exitCode);
}
},
);
it.skipIf(process.platform === "win32")(
"should report signal-killed commands as errors while preserving partial output",
async () => {
for (const { signal, exitCode } of [
{ signal: "KILL", exitCode: 137 },
{ signal: "TERM", exitCode: 143 },
]) {
const result = await bashTool.execute(`test-call-signal-${signal}`, {
command: `printf 'before-kill\\n'; kill -${signal} $$`,
});
expect(result.isError).toBe(true);
expect(getTextOutput(result)).toMatch(
new RegExp(`before-kill\\s+Command exited with code ${exitCode}$`),
);
}
},
);
it("should reject a null exit code from custom operations", async () => {
const operations: BashOperations = {
exec: async (_command, _cwd, { onData }) => {
onData(Buffer.from("partial\n", "utf-8"));
return { exitCode: null };
},
};
const bash = createBashTool(testDir, { operations });
await expect(bash.execute("test-call-null-exit", { command: "remote" })).rejects.toThrow(
/partial\s+Command terminated without an exit code$/,
);
});
@@ -901,8 +995,8 @@ describe("Coding Agent Tools", () => {
});
});
function fakeCtx(cwd: string): ExtensionContext {
return { cwd } as ExtensionContext;
function fakeCtx(cwd: string): ExtensionToolContext {
return { cwd } as ExtensionToolContext;
}
describe("tool cwd resolution", () => {