248 lines
7.0 KiB
TypeScript
248 lines
7.0 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import type {
|
|
AdvancedDiffFallback,
|
|
AdvancedDiffSuccess,
|
|
AdvancedDiffUnpreviewable,
|
|
} from "../../cli/helpers/diff";
|
|
import { computeDiffPreviews, toDiffPreview } from "../../helpers/diffPreview";
|
|
|
|
describe("toDiffPreview", () => {
|
|
it("converts an AdvancedDiffSuccess to an advanced DiffPreview", () => {
|
|
const input: AdvancedDiffSuccess = {
|
|
mode: "advanced",
|
|
fileName: "foo.ts",
|
|
oldStr: "const a = 1;\n",
|
|
newStr: "const a = 2;\n",
|
|
hunks: [
|
|
{
|
|
oldStart: 1,
|
|
newStart: 1,
|
|
lines: [{ raw: "-const a = 1;" }, { raw: "+const a = 2;" }],
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = toDiffPreview(input);
|
|
|
|
expect(result.mode).toBe("advanced");
|
|
if (result.mode !== "advanced") throw new Error("unreachable");
|
|
|
|
expect(result.fileName).toBe("foo.ts");
|
|
// oldStr/newStr must NOT appear on the wire type
|
|
expect("oldStr" in result).toBe(false);
|
|
expect("newStr" in result).toBe(false);
|
|
|
|
expect(result.hunks).toHaveLength(1);
|
|
const hunk = result.hunks[0];
|
|
expect(hunk?.oldStart).toBe(1);
|
|
expect(hunk?.newStart).toBe(1);
|
|
expect(hunk?.oldLines).toBe(1); // one remove
|
|
expect(hunk?.newLines).toBe(1); // one add
|
|
expect(hunk?.lines).toEqual([
|
|
{ type: "remove", content: "const a = 1;" },
|
|
{ type: "add", content: "const a = 2;" },
|
|
]);
|
|
});
|
|
|
|
it("computes oldLines/newLines correctly for mixed hunks", () => {
|
|
const input: AdvancedDiffSuccess = {
|
|
mode: "advanced",
|
|
fileName: "test.ts",
|
|
oldStr: "",
|
|
newStr: "",
|
|
hunks: [
|
|
{
|
|
oldStart: 5,
|
|
newStart: 5,
|
|
lines: [
|
|
{ raw: " context line" },
|
|
{ raw: "-removed line 1" },
|
|
{ raw: "-removed line 2" },
|
|
{ raw: "+added line" },
|
|
{ raw: " more context" },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = toDiffPreview(input);
|
|
if (result.mode !== "advanced") throw new Error("unreachable");
|
|
|
|
const hunk = result.hunks[0];
|
|
// context: 2 (contributes to both), remove: 2, add: 1
|
|
expect(hunk?.oldLines).toBe(4); // 2 context + 2 remove
|
|
expect(hunk?.newLines).toBe(3); // 2 context + 1 add
|
|
});
|
|
|
|
it("parses empty raw lines as context", () => {
|
|
const input: AdvancedDiffSuccess = {
|
|
mode: "advanced",
|
|
fileName: "empty.ts",
|
|
oldStr: "",
|
|
newStr: "",
|
|
hunks: [
|
|
{
|
|
oldStart: 1,
|
|
newStart: 1,
|
|
lines: [{ raw: "" }],
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = toDiffPreview(input);
|
|
if (result.mode !== "advanced") throw new Error("unreachable");
|
|
|
|
expect(result.hunks[0]?.lines[0]).toEqual({
|
|
type: "context",
|
|
content: "",
|
|
});
|
|
});
|
|
|
|
it("converts fallback results with fileName", () => {
|
|
const input: AdvancedDiffFallback = {
|
|
mode: "fallback",
|
|
reason: "File not readable",
|
|
};
|
|
|
|
const result = toDiffPreview(input, "myfile.ts");
|
|
expect(result).toEqual({
|
|
mode: "fallback",
|
|
fileName: "myfile.ts",
|
|
reason: "File not readable",
|
|
});
|
|
});
|
|
|
|
it("converts unpreviewable results with fileName", () => {
|
|
const input: AdvancedDiffUnpreviewable = {
|
|
mode: "unpreviewable",
|
|
reason: "Edit not found in file",
|
|
};
|
|
|
|
const result = toDiffPreview(input, "target.ts");
|
|
expect(result).toEqual({
|
|
mode: "unpreviewable",
|
|
fileName: "target.ts",
|
|
reason: "Edit not found in file",
|
|
});
|
|
});
|
|
|
|
it("uses 'unknown' fileName when no override provided for fallback/unpreviewable", () => {
|
|
const fallback: AdvancedDiffFallback = {
|
|
mode: "fallback",
|
|
reason: "File not readable",
|
|
};
|
|
expect(toDiffPreview(fallback).fileName).toBe("unknown");
|
|
|
|
const unpreviewable: AdvancedDiffUnpreviewable = {
|
|
mode: "unpreviewable",
|
|
reason: "reason",
|
|
};
|
|
expect(toDiffPreview(unpreviewable).fileName).toBe("unknown");
|
|
});
|
|
|
|
it("allows fileName override on advanced results", () => {
|
|
const input: AdvancedDiffSuccess = {
|
|
mode: "advanced",
|
|
fileName: "original.ts",
|
|
oldStr: "",
|
|
newStr: "",
|
|
hunks: [],
|
|
};
|
|
|
|
const result = toDiffPreview(input, "overridden.ts");
|
|
expect(result.fileName).toBe("overridden.ts");
|
|
});
|
|
|
|
it("ignores no-newline metadata lines in advanced hunks", () => {
|
|
const input: AdvancedDiffSuccess = {
|
|
mode: "advanced",
|
|
fileName: "x.txt",
|
|
oldStr: "old-no-newline",
|
|
newStr: "new-no-newline",
|
|
hunks: [
|
|
{
|
|
oldStart: 1,
|
|
newStart: 1,
|
|
lines: [
|
|
{ raw: "-old-no-newline" },
|
|
{ raw: "\\ No newline at end of file" },
|
|
{ raw: "+new-no-newline" },
|
|
{ raw: "\\ No newline at end of file" },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = toDiffPreview(input);
|
|
if (result.mode !== "advanced") throw new Error("unreachable");
|
|
|
|
const hunk = result.hunks[0];
|
|
expect(hunk?.oldLines).toBe(1);
|
|
expect(hunk?.newLines).toBe(1);
|
|
expect(hunk?.lines).toEqual([
|
|
{ type: "remove", content: "old-no-newline" },
|
|
{ type: "add", content: "new-no-newline" },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("computeDiffPreviews", () => {
|
|
it("returns one preview for write tool", async () => {
|
|
const previews = await computeDiffPreviews("write", {
|
|
file_path: "sample.txt",
|
|
content: "hello",
|
|
});
|
|
expect(previews).toHaveLength(1);
|
|
expect(previews[0]?.mode).toBe("advanced");
|
|
expect(previews[0]?.fileName).toBe("sample.txt");
|
|
});
|
|
|
|
it("returns one preview per file for apply_patch", async () => {
|
|
const patch = [
|
|
"*** Begin Patch",
|
|
"*** Update File: a.txt",
|
|
"@@ -1 +1 @@",
|
|
"-old",
|
|
"+new",
|
|
"*** Add File: b.txt",
|
|
"+hello",
|
|
"*** End Patch",
|
|
].join("\n");
|
|
|
|
const previews = await computeDiffPreviews("apply_patch", { input: patch });
|
|
expect(previews).toHaveLength(2);
|
|
expect(previews.map((p) => p.fileName).sort()).toEqual(["a.txt", "b.txt"]);
|
|
});
|
|
|
|
it("resolves relative file paths against the provided working directory", async () => {
|
|
const tempRoot = await mkdtemp(
|
|
path.join(os.tmpdir(), "letta-diff-preview-"),
|
|
);
|
|
const workspaceDir = path.join(tempRoot, "workspace");
|
|
const nestedDir = path.join(workspaceDir, "nested");
|
|
const targetFile = path.join(nestedDir, "sample.txt");
|
|
await mkdir(nestedDir, { recursive: true });
|
|
await writeFile(targetFile, "old content", "utf8");
|
|
|
|
try {
|
|
const previews = await computeDiffPreviews(
|
|
"edit",
|
|
{
|
|
file_path: "nested/sample.txt",
|
|
old_string: "old content",
|
|
new_string: "new content",
|
|
},
|
|
workspaceDir,
|
|
);
|
|
expect(previews).toHaveLength(1);
|
|
expect(previews[0]?.mode).toBe("advanced");
|
|
expect(previews[0]?.fileName).toBe("sample.txt");
|
|
} finally {
|
|
await rm(tempRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|