Files
oh-my-pi/packages/agent/test/compaction-file-ops.test.ts
T

86 lines
3.4 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import {
computeFileLists,
createFileOps,
extractFileOpsFromMessage,
formatFileOperations,
stripReadSelector,
} from "../src/compaction/utils";
import { createAssistantMessage } from "./helpers";
function readCall(id: string, path: string) {
return { type: "toolCall" as const, id, name: "read", arguments: { path } };
}
describe("stripReadSelector", () => {
it("strips line-range and raw selectors in every supported shape", () => {
expect(stripReadSelector("src/foo.ts:50")).toBe("src/foo.ts");
expect(stripReadSelector("src/foo.ts:50-")).toBe("src/foo.ts");
expect(stripReadSelector("src/foo.ts:50-200")).toBe("src/foo.ts");
expect(stripReadSelector("src/foo.ts:50+150")).toBe("src/foo.ts");
expect(stripReadSelector("src/foo.ts:5-16,960-973")).toBe("src/foo.ts");
expect(stripReadSelector("src/foo.ts:2724..2727")).toBe("src/foo.ts");
expect(stripReadSelector("src/foo.ts:raw")).toBe("src/foo.ts");
expect(stripReadSelector("src/foo.ts:conflicts")).toBe("src/foo.ts");
// Compound raw+range, either order.
expect(stripReadSelector("src/foo.ts:100-170:raw")).toBe("src/foo.ts");
expect(stripReadSelector("src/foo.ts:raw:2-4")).toBe("src/foo.ts");
});
it("keeps archive member paths, stripping only the trailing selector", () => {
expect(stripReadSelector("archive.zip:dir/file.ts:50-60")).toBe("archive.zip:dir/file.ts");
expect(stripReadSelector("archive.zip:dir/file.ts")).toBe("archive.zip:dir/file.ts");
});
it("leaves non-selector colons untouched", () => {
expect(stripReadSelector("db.sqlite:users")).toBe("db.sqlite:users");
expect(stripReadSelector("local://ctx.md")).toBe("local://ctx.md");
expect(stripReadSelector("https://example.com/page")).toBe("https://example.com/page");
expect(stripReadSelector("src/foo.ts")).toBe("src/foo.ts");
});
});
describe("extractFileOpsFromMessage", () => {
it("dedupes the same file read through different selectors to one entry", () => {
const fileOps = createFileOps();
const message = createAssistantMessage([
readCall("r1", "docs/compaction.md:100-170:raw"),
readCall("r2", "docs/compaction.md:8-16,128-139,384-388"),
readCall("r3", "docs/compaction.md:raw"),
readCall("r4", "docs/compaction.md"),
]);
extractFileOpsFromMessage(message, fileOps);
expect([...fileOps.read]).toEqual(["docs/compaction.md"]);
});
it("matches selector-suffixed reads against modified paths", () => {
const fileOps = createFileOps();
const message = createAssistantMessage([
readCall("r1", "src/login.ts:30-80"),
{ type: "toolCall" as const, id: "w1", name: "write", arguments: { path: "src/login.ts" } },
]);
extractFileOpsFromMessage(message, fileOps);
const { readFiles, modifiedFiles } = computeFileLists(fileOps);
expect(readFiles).toEqual([]);
expect(modifiedFiles).toEqual(["src/login.ts"]);
});
});
describe("formatFileOperations", () => {
it("renders one grouped <files> tree with Read/Write/RW markers", () => {
const rendered = formatFileOperations(
["src/a.ts", "src/b.ts"],
["src/c.ts", "src/d.ts"],
new Set(["src/a.ts", "src/b.ts", "src/c.ts"]),
);
expect(rendered).toBe(
["<files>", "# src/", "a.ts (Read)", "b.ts (Read)", "c.ts (RW)", "d.ts (Write)", "</files>"].join("\n"),
);
});
it("marks modified files Write when no read set is provided", () => {
const rendered = formatFileOperations([], ["c.ts"]);
expect(rendered).toBe(["<files>", "c.ts (Write)", "</files>"].join("\n"));
});
});