0a71899499
BREAKING CHANGE: ast_find parameter 'pattern' (string) is replaced by
'patterns' (string[]). ast_replace parameters 'pattern' + 'rewrite' are
replaced by 'ops: Array<{ pat: string; out: string }>'.
Native (pi-natives / crates/pi-natives):
- astFind accepts patterns[] array; all patterns run per file, results
merged and sorted by path/line/column before offset+limit are applied
- astReplace accepts rewrites Record<string,string>; all patterns compiled
once upfront and applied per file in a single pass
- Deterministic result ordering via BTreeSet/BTreeMap
Coding-agent tools:
- ast_find: multi-pattern deduplication, '>>' prefix on match-start lines,
padded line numbers, directory-tree grouping (# dir / ## └─ file headers),
scopePath/files/fileMatches in tool details
- ast_replace: ops[] interface with duplicate-pattern rejection, diff-style
(-before/+after) previews grouped by directory, parse errors shown on
zero-replacement path, fileReplacements in tool details
- Tool prompts updated to document new interfaces with multi-pattern examples
- Task item id maxLength raised from 32 to 48 characters
- Added ast_replace tool test suite
85 lines
3.1 KiB
TypeScript
85 lines
3.1 KiB
TypeScript
import { 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 { adaptSchemaForStrict } from "@oh-my-pi/pi-ai/utils/schema";
|
|
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
|
import { createTools, type ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
|
|
|
|
function createTestSession(cwd = "/tmp/test"): ToolSession {
|
|
return {
|
|
cwd,
|
|
hasUI: true,
|
|
getSessionFile: () => null,
|
|
getSessionSpawns: () => "*",
|
|
settings: Settings.isolated(),
|
|
};
|
|
}
|
|
|
|
function asSchemaObject(value: unknown): Record<string, unknown> {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
throw new Error("Expected object schema");
|
|
}
|
|
return value as Record<string, unknown>;
|
|
}
|
|
|
|
describe("ast_replace tool schema", () => {
|
|
it("uses op entries as [{ pat, out }]", async () => {
|
|
const tools = await createTools(createTestSession());
|
|
const tool = tools.find(entry => entry.name === "ast_replace");
|
|
expect(tool).toBeDefined();
|
|
const schema = asSchemaObject(tool?.parameters);
|
|
const properties = asSchemaObject(schema.properties);
|
|
const ops = asSchemaObject(properties.ops);
|
|
|
|
expect(ops.type).toBe("array");
|
|
const items = asSchemaObject(ops.items);
|
|
expect(items.type).toBe("object");
|
|
expect(items.required).toEqual(["pat", "out"]);
|
|
const itemProperties = asSchemaObject(items.properties);
|
|
expect(asSchemaObject(itemProperties.pat).type).toBe("string");
|
|
expect(asSchemaObject(itemProperties.out).type).toBe("string");
|
|
});
|
|
|
|
it("remains strict-representable after strict adaptation", async () => {
|
|
const tools = await createTools(createTestSession());
|
|
const tool = tools.find(entry => entry.name === "ast_replace");
|
|
expect(tool).toBeDefined();
|
|
const schema = asSchemaObject(tool?.parameters);
|
|
|
|
const strict = adaptSchemaForStrict(schema, true);
|
|
expect(strict.strict).toBe(true);
|
|
});
|
|
|
|
it("renders +/- lines with aligned hashline prefixes", async () => {
|
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ast-replace-render-"));
|
|
try {
|
|
const filePath = path.join(tempDir, "legacy.ts");
|
|
await Bun.write(filePath, "legacyWrap(x, value)\n");
|
|
|
|
const tools = await createTools(createTestSession(tempDir));
|
|
const tool = tools.find(entry => entry.name === "ast_replace");
|
|
expect(tool).toBeDefined();
|
|
|
|
const result = await tool!.execute("ast-replace-test", {
|
|
ops: [{ pat: "legacyWrap($A, $B)", out: "modernWrap($A, $B)" }],
|
|
lang: "typescript",
|
|
path: filePath,
|
|
dry_run: true,
|
|
});
|
|
const text = result.content.find(content => content.type === "text")?.text ?? "";
|
|
const lines = text.split("\n");
|
|
const removedLine = lines.find(line => line.startsWith("-"));
|
|
const addedLine = lines.find(line => line.startsWith("+"));
|
|
|
|
expect(removedLine).toBeDefined();
|
|
expect(addedLine).toBeDefined();
|
|
expect(removedLine).toMatch(/^-\d+#\w+:/);
|
|
expect(addedLine).toMatch(/^\+\d+#\w+:/);
|
|
expect(removedLine?.split(":", 1)[0].length).toBe(addedLine?.split(":", 1)[0].length);
|
|
} finally {
|
|
await fs.rm(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|