Files
oh-my-pi/packages/ai/test/cursor-mcp-tool-catalog.test.ts
T
Diogo Soares Rodrigues 7b62fef366 fix(cursor): honor Pi frame arguments and preserve open-block args
Review of the modern exec wire protocol surfaced defects the committed
suite did not pin.

The Pi bridge dropped frame arguments: `pi_read`'s offset/limit (ranged
reads returned whole files), `pi_grep`'s literal (fixed strings ran as
regexes), and the path/glob join emitted `./`-prefixed specs. These are
`optional int32`, so a present `0` is a value, not "unset" — `limit: 0`
now answers empty rather than reading everything, and `pi_find` clamps
to 1 like the reference client.

The provider synthesized its transcript block from a second, divergent
translation of the same frame, so the displayed operation differed from
the executed one. Both sides now share one mapper in `exec-modern.ts`.

End-of-transport cleanup reparsed every open block's streamed argument
buffer; blocks whose args arrive whole never set that buffer, and
`parseStreamingJson(undefined)` is `{}`, so a truncated turn erased
their arguments.

All fixes are mutation-verified: reverting each one fails a test.

(cherry picked from commit bb7bcfebce4200d436e17d6e39320da13fc85ca8)
2026-07-30 01:41:55 +02:00

60 lines
2.4 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import { buildMcpToolDefinitions } from "@oh-my-pi/pi-ai/providers/cursor";
import type { Tool } from "@oh-my-pi/pi-ai/types";
const tool = (name: string): Tool => ({
name,
description: `${name} tool`,
parameters: { type: "object", properties: {} },
});
describe("cursor buildMcpToolDefinitions", () => {
it("forwards the write transport alongside preview-staging devices so xd:// resolution stays reachable", () => {
// A Cursor session with xdev on: ast_edit is a mounted device, write is the
// xd:// transport carried top-level. ast_edit always stages a preview whose
// resolution rides `write xd://resolve` / `write xd://reject`.
const defs = buildMcpToolDefinitions([
tool("read"),
tool("write"),
tool("bash"),
tool("todo"),
tool("ast_edit"),
tool("task"),
]);
const names = defs.map(def => def.name);
expect(names).toContain("ast_edit");
expect(names).toContain("task");
// `write` is the sole native-filtered tool re-included: without it the
// staged preview can never be resolved and the SoftToolRequirement('write')
// escalation aborts the turn.
expect(names).toContain("write");
expect(names).not.toContain("read");
expect(names).not.toContain("bash");
expect(names).not.toContain("todo");
// The forwarded write must be a routable pi-agent MCP tool, so Cursor
// dispatches it back through the coding-agent write tool's xd:// handler.
const writeDef = defs.find(def => def.name === "write");
expect(writeDef?.providerIdentifier).toBe("pi-agent");
expect(writeDef?.toolName).toBe("write");
});
it("keeps write out when only native tools are advertised (no pi-agent device needs resolution)", () => {
const names = buildMcpToolDefinitions([tool("read"), tool("write"), tool("bash")]).map(def => def.name);
expect(names).toEqual([]);
});
it("advertises lsp, which Cursor has no native equivalent for", () => {
// `lsp` is deliberately absent from the native-filtered set: Cursor's own
// tools cover none of definition/references/rename, so filtering it out
// leaves the model with no way to reach them at all.
const defs = buildMcpToolDefinitions([tool("read"), tool("bash"), tool("lsp")]);
const lspDef = defs.find(def => def.name === "lsp");
expect(lspDef).toBeDefined();
expect(lspDef?.providerIdentifier).toBe("pi-agent");
expect(lspDef?.toolName).toBe("lsp");
});
});