mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 08:24:46 +00:00
fix(diff): preserve BOM text and complete file replacements
Decode each string without consuming its leading BOM. File-type replacements emit multiple patch sections for one listed path, so retain all matching hunks. Assert the optional rename metadata in the existing commit-file journey.
This commit is contained in:
@@ -238,3 +238,33 @@ test("roundtrips generated entry lists across varint boundaries", () => {
|
||||
expect(decodeDiffFrame(encodeDiffFrame(begin))).toEqual(begin);
|
||||
}
|
||||
});
|
||||
|
||||
test.each([true, false])(
|
||||
"preserves BOM-prefixed paths and source lines with runs=%s",
|
||||
(highlight) => {
|
||||
const frame = {
|
||||
opcode: 0x41 as const,
|
||||
slot: 0,
|
||||
revision: 1,
|
||||
files: [
|
||||
{
|
||||
path: "\uFEFFbom.ts",
|
||||
hunks: [
|
||||
{
|
||||
oldStart: 1,
|
||||
oldCount: 1,
|
||||
newStart: 1,
|
||||
newCount: 1,
|
||||
lines: (["add", "remove", "context"] as const).map((type) => ({
|
||||
type,
|
||||
content: "\uFEFFvalue",
|
||||
runs: highlight ? [{ style: null, length: 6 }] : undefined,
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(decodeDiffFrame(encodeDiffFrame(frame))).toEqual(frame);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -93,7 +93,7 @@ export type DiffFrame = DiffBegin | DiffBodies | DiffEnd;
|
||||
export class InvalidDiffFrameError extends Error {}
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
const textDecoder = new TextDecoder("utf-8", { fatal: true });
|
||||
const textDecoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
||||
|
||||
class Writer {
|
||||
private bytes: number[] = [];
|
||||
|
||||
@@ -450,3 +450,72 @@ test.each(["worktree", "index", "rev"] as const)(
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test("highlighted BOM source content survives the production frame codec", async () => {
|
||||
const { encodeDiffFrame, decodeDiffFrame } =
|
||||
await import("@getpaseo/protocol/binary-frames/index");
|
||||
const cwd = await mkdtemp(join(tmpdir(), "diff-bom-"));
|
||||
const git = (...args: string[]) => execFileSync("git", args, { cwd, encoding: "utf8" });
|
||||
try {
|
||||
git("init", "-b", "main");
|
||||
await writeFile(join(cwd, "bom.ts"), "\uFEFFexport const value = 1;\n");
|
||||
const comparison = await listComparison({
|
||||
cwd,
|
||||
base: { kind: "empty" },
|
||||
head: { kind: "worktree" },
|
||||
});
|
||||
try {
|
||||
const record = await readBody(comparison);
|
||||
const frame = { opcode: 0x41 as const, slot: 0, revision: 1, files: [record.body] };
|
||||
expect(decodeDiffFrame(encodeDiffFrame(frame))).toEqual(frame);
|
||||
expect(
|
||||
record.body.hunks
|
||||
.flatMap((hunk) => hunk.lines)
|
||||
.some((line) => line.content.startsWith("\uFEFF")),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
await comparison.close();
|
||||
}
|
||||
} finally {
|
||||
await rm(cwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test.skipIf(process.platform === "win32")(
|
||||
"symlink replacement retains both patch sections",
|
||||
async () => {
|
||||
const cwd = await mkdtemp(join(tmpdir(), "diff-type-change-"));
|
||||
const git = (...args: string[]) => execFileSync("git", args, { cwd, encoding: "utf8" });
|
||||
const { symlink, unlink } = await import("node:fs/promises");
|
||||
try {
|
||||
git("init", "-b", "main");
|
||||
git("config", "user.name", "Test");
|
||||
git("config", "user.email", "test@example.com");
|
||||
await symlink("old-target", join(cwd, "file.txt"));
|
||||
git("add", ".");
|
||||
git("-c", "commit.gpgsign=false", "commit", "-m", "base");
|
||||
await unlink(join(cwd, "file.txt"));
|
||||
await writeFile(join(cwd, "file.txt"), "replacement contents\n");
|
||||
const comparison = await listComparison({
|
||||
cwd,
|
||||
base: { kind: "rev", rev: "HEAD" },
|
||||
head: { kind: "worktree" },
|
||||
highlight: false,
|
||||
});
|
||||
try {
|
||||
expect(comparison.files[0].entry).toMatchObject({ additions: 1, deletions: 1 });
|
||||
const record = await readBody(comparison);
|
||||
expect(record.body.hunks.flatMap((hunk) => hunk.lines)).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ type: "remove", content: "old-target" },
|
||||
{ type: "add", content: "replacement contents" },
|
||||
]),
|
||||
);
|
||||
} finally {
|
||||
await comparison.close();
|
||||
}
|
||||
} finally {
|
||||
await rm(cwd, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -268,10 +268,20 @@ async function readComparisonList(
|
||||
const bytes = Buffer.byteLength(result.stdout);
|
||||
if (result.truncated || bytes > remainingBytes || bytes > maxPatchBytes)
|
||||
return { kind: "too_large" };
|
||||
const parsed = parseDiff(result.stdout).find(
|
||||
// File-type changes have a deletion and addition section for the same path.
|
||||
const sections = parseDiff(result.stdout).filter(
|
||||
(candidate) => candidate.path === file.entry.path,
|
||||
);
|
||||
if (!parsed) return { kind: "ready", body: { path: file.entry.path, hunks: [] }, bytes };
|
||||
if (!sections.length)
|
||||
return { kind: "ready", body: { path: file.entry.path, hunks: [] }, bytes };
|
||||
const parsed = {
|
||||
path: file.entry.path,
|
||||
isNew: file.entry.change === "added",
|
||||
isDeleted: file.entry.change === "deleted",
|
||||
additions: file.entry.additions,
|
||||
deletions: file.entry.deletions,
|
||||
hunks: sections.flatMap((section) => section.hunks),
|
||||
};
|
||||
let highlighted = parsed;
|
||||
if (compare.highlight !== false) {
|
||||
metrics.highlightCalls++;
|
||||
|
||||
@@ -29,6 +29,8 @@ test("a rename-only commit returns zero added or deleted lines through the clien
|
||||
expect(await client.getCommitFileDiff(cwd, sha, "new.txt")).toEqual({
|
||||
file: {
|
||||
path: "new.txt",
|
||||
oldPath: "old.txt",
|
||||
status: "ok",
|
||||
isNew: false,
|
||||
isDeleted: false,
|
||||
additions: 0,
|
||||
@@ -98,3 +100,45 @@ test.skipIf(process.platform === "win32")(
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test("streamed Changes preserves BOM-prefixed paths and highlighted content", async () => {
|
||||
const cwd = mkdtempSync(join(tmpdir(), "stream-bom-diff-"));
|
||||
const daemon = await createTestPaseoDaemon({ agentClients: {} });
|
||||
const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` });
|
||||
try {
|
||||
execFileSync("git", ["init", "-b", "main"], { cwd });
|
||||
writeFileSync(join(cwd, "\uFEFFbom.ts"), "\uFEFFexport const value = 1;\n");
|
||||
writeFileSync(join(cwd, "other.txt"), "other edit\n");
|
||||
await client.connect();
|
||||
const states: import("@getpaseo/client").DiffState[] = [];
|
||||
const observation = client.observeDiff({
|
||||
cwd,
|
||||
base: { kind: "empty" },
|
||||
head: { kind: "worktree" },
|
||||
});
|
||||
const unsubscribe = observation.subscribe({
|
||||
snapshot: (state) => states.push(state),
|
||||
update: () => {},
|
||||
});
|
||||
try {
|
||||
await expect.poll(() => states.at(-1)?.phase, { timeout: 15000 }).toBe("ready");
|
||||
const readyState = states.at(-1);
|
||||
if (readyState?.phase !== "ready") throw new Error("Expected ready diff");
|
||||
expect(readyState.files.map((file) => file.path)).toContain("other.txt");
|
||||
const bomFile = readyState.files.find((file) => file.path === "\uFEFFbom.ts");
|
||||
const addedLine = bomFile?.hunks
|
||||
.flatMap((hunk) => hunk.lines)
|
||||
.find((line) => line.type === "add");
|
||||
expect(addedLine?.content).toBe("\uFEFFexport const value = 1;");
|
||||
expect(addedLine?.tokens?.map((token) => token.text).join("")).toBe(addedLine?.content);
|
||||
expect(states.some((state) => state.phase === "error")).toBe(false);
|
||||
} finally {
|
||||
unsubscribe();
|
||||
await observation.release();
|
||||
}
|
||||
} finally {
|
||||
await client.close();
|
||||
await daemon.close();
|
||||
rmSync(cwd, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user