1be025bb72
Cursor exec bridges derived failure state only from thrown exceptions, so structured AgentToolResult.isError failures were emitted as successes. Propagate the returned flag through standard and streaming shell execution, with regression coverage for both paths.
124 lines
4.3 KiB
TypeScript
124 lines
4.3 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
import * as fs from "node:fs/promises";
|
|
import * as os from "node:os";
|
|
import * as path from "node:path";
|
|
import { create } from "@bufbuild/protobuf";
|
|
import type { AgentEvent, AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
import { ReadArgsSchema, ShellArgsSchema } from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb";
|
|
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
|
import { CursorExecHandlers } from "@oh-my-pi/pi-coding-agent/cursor";
|
|
import { GrepTool, type ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
|
|
import { removeWithRetries } from "@oh-my-pi/pi-utils";
|
|
import { type } from "arktype";
|
|
|
|
function createTestSession(cwd: string, overrides: Partial<ToolSession> = {}): ToolSession {
|
|
return {
|
|
cwd,
|
|
hasUI: false,
|
|
getSessionFile: () => null,
|
|
getSessionSpawns: () => "*",
|
|
settings: Settings.isolated(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("CursorExecHandlers.grep bridge", () => {
|
|
let cwd: string;
|
|
let searchTool: GrepTool;
|
|
let handlers: CursorExecHandlers;
|
|
|
|
beforeEach(async () => {
|
|
cwd = await fs.mkdtemp(path.join(os.tmpdir(), "cursor-exec-test-"));
|
|
await Bun.write(path.join(cwd, "sample.txt"), "Hello World\nhello world\n");
|
|
searchTool = new GrepTool(createTestSession(cwd));
|
|
handlers = new CursorExecHandlers({
|
|
cwd,
|
|
tools: new Map([["grep", searchTool as any]]),
|
|
});
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await removeWithRetries(cwd);
|
|
});
|
|
|
|
it("maps caseInsensitive parameter correctly through the grep bridge", async () => {
|
|
// 1. By default/omitted caseInsensitive, should be case-sensitive (match count 1 for "hello")
|
|
const defaultResult = await handlers.grep({
|
|
toolCallId: "call-1",
|
|
path: cwd,
|
|
pattern: "hello",
|
|
} as any);
|
|
expect((defaultResult.details as { matchCount?: number } | undefined)?.matchCount).toBe(1);
|
|
|
|
// 2. If caseInsensitive: true, should be case-insensitive (match count 2 for "hello")
|
|
const insensitiveResult = await handlers.grep({
|
|
toolCallId: "call-2",
|
|
path: cwd,
|
|
pattern: "hello",
|
|
caseInsensitive: true,
|
|
} as any);
|
|
expect((insensitiveResult.details as { matchCount?: number } | undefined)?.matchCount).toBe(2);
|
|
|
|
// 3. If caseInsensitive: false, should be case-sensitive (match count 1 for "hello")
|
|
const sensitiveResult = await handlers.grep({
|
|
toolCallId: "call-3",
|
|
path: cwd,
|
|
pattern: "hello",
|
|
caseInsensitive: false,
|
|
} as any);
|
|
expect((sensitiveResult.details as { matchCount?: number } | undefined)?.matchCount).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("CursorExecHandlers error results", () => {
|
|
const rewrittenErrorTool = (name: string): AgentTool => ({
|
|
name,
|
|
label: name,
|
|
description: "returns a rewritten tool failure",
|
|
parameters: type({}),
|
|
execute: async () => ({
|
|
content: [{ type: "text", text: "Enriched recovery guidance" }],
|
|
details: { enriched: true },
|
|
isError: true,
|
|
}),
|
|
});
|
|
|
|
it("propagates returned isError through the standard exec bridge", async () => {
|
|
const events: AgentEvent[] = [];
|
|
const handlers = new CursorExecHandlers({
|
|
cwd: ".",
|
|
tools: new Map([["read", rewrittenErrorTool("read")]]),
|
|
emitEvent: event => events.push(event),
|
|
});
|
|
|
|
const result = await handlers.read(create(ReadArgsSchema, { toolCallId: "call-read", path: "ignored" }));
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content).toEqual([{ type: "text", text: "Enriched recovery guidance" }]);
|
|
const end = events.find(event => event.type === "tool_execution_end");
|
|
expect(end?.isError).toBe(true);
|
|
});
|
|
|
|
it("propagates returned isError through the shell stream bridge", async () => {
|
|
const events: AgentEvent[] = [];
|
|
const stdout: string[] = [];
|
|
const handlers = new CursorExecHandlers({
|
|
cwd: ".",
|
|
tools: new Map([["bash", rewrittenErrorTool("bash")]]),
|
|
emitEvent: event => events.push(event),
|
|
});
|
|
|
|
const result = await handlers.shellStream(
|
|
create(ShellArgsSchema, { toolCallId: "call-shell", command: "ignored" }),
|
|
{
|
|
onStdout: data => stdout.push(data),
|
|
onStderr: () => {},
|
|
},
|
|
);
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content).toEqual([{ type: "text", text: "Enriched recovery guidance" }]);
|
|
expect(stdout).toEqual(["Enriched recovery guidance"]);
|
|
const end = events.find(event => event.type === "tool_execution_end");
|
|
expect(end?.isError).toBe(true);
|
|
});
|
|
});
|