fix: preserve BOM in text tool round-trips (#23797)

This commit is contained in:
Luke Parker
2026-04-22 18:03:34 +10:00
committed by Aiden Cline
parent c819804638
commit 8113a4360e
10 changed files with 276 additions and 39 deletions

View File

@@ -126,6 +126,24 @@ describe("Format", () => {
it.live("service initializes without error", () => provideTmpdirInstance(() => Format.Service.use(() => Effect.void)))
it.live("file() returns false when no formatter runs", () =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
const file = `${dir}/test.txt`
yield* Effect.promise(() => Bun.write(file, "x"))
const formatted = yield* Format.Service.use((fmt) => fmt.file(file))
expect(formatted).toBe(false)
}),
{
config: {
formatter: false,
},
},
),
)
it.live("status() initializes formatter state per directory", () =>
Effect.gen(function* () {
const a = yield* provideTmpdirInstance(() => Format.Service.use((fmt) => fmt.status()), {
@@ -219,7 +237,7 @@ describe("Format", () => {
yield* Format.Service.use((fmt) =>
Effect.gen(function* () {
yield* fmt.init()
yield* fmt.file(file)
expect(yield* fmt.file(file)).toBe(true)
}),
)
@@ -229,11 +247,21 @@ describe("Format", () => {
config: {
formatter: {
first: {
command: ["sh", "-c", 'sleep 0.05; v=$(cat "$1"); printf \'%sA\' "$v" > "$1"', "sh", "$FILE"],
command: [
"node",
"-e",
"const fs = require('fs'); const file = process.argv[1]; fs.writeFileSync(file, fs.readFileSync(file, 'utf8') + 'A')",
"$FILE",
],
extensions: [".seq"],
},
second: {
command: ["sh", "-c", 'v=$(cat "$1"); printf \'%sB\' "$v" > "$1"', "sh", "$FILE"],
command: [
"node",
"-e",
"const fs = require('fs'); const file = process.argv[1]; fs.writeFileSync(file, fs.readFileSync(file, 'utf8') + 'B')",
"$FILE",
],
extensions: [".seq"],
},
},

View File

@@ -195,6 +195,34 @@ describe("tool.apply_patch freeform", () => {
})
})
test("does not invent a first-line diff for BOM files", async () => {
await using fixture = await tmpdir()
const { ctx, calls } = makeCtx()
await Instance.provide({
directory: fixture.path,
fn: async () => {
const bom = String.fromCharCode(0xfeff)
const target = path.join(fixture.path, "example.cs")
await fs.writeFile(target, `${bom}using System;\n\nclass Test {}\n`, "utf-8")
const patchText = "*** Begin Patch\n*** Update File: example.cs\n@@\n class Test {}\n+class Next {}\n*** End Patch"
await execute({ patchText }, ctx)
expect(calls.length).toBe(1)
const shown = calls[0].metadata.files[0]?.patch ?? ""
expect(shown).not.toContain(bom)
expect(shown).not.toContain("-using System;")
expect(shown).not.toContain("+using System;")
const content = await fs.readFile(target, "utf-8")
expect(content.charCodeAt(0)).toBe(0xfeff)
expect(content.slice(1)).toBe("using System;\n\nclass Test {}\nclass Next {}\n")
},
})
})
test("inserts lines with insert-only hunk", async () => {
await using fixture = await tmpdir()
const { ctx } = makeCtx()

View File

@@ -96,6 +96,37 @@ describe("tool.edit", () => {
})
})
test("preserves BOM when oldString is empty on existing files", async () => {
await using tmp = await tmpdir()
const filepath = path.join(tmp.path, "existing.cs")
const bom = String.fromCharCode(0xfeff)
await fs.writeFile(filepath, `${bom}using System;\n`, "utf-8")
await Instance.provide({
directory: tmp.path,
fn: async () => {
const edit = await resolve()
const result = await Effect.runPromise(
edit.execute(
{
filePath: filepath,
oldString: "",
newString: "using Up;\n",
},
ctx,
),
)
expect(result.metadata.diff).toContain("-using System;")
expect(result.metadata.diff).toContain("+using Up;")
const content = await fs.readFile(filepath, "utf-8")
expect(content.charCodeAt(0)).toBe(0xfeff)
expect(content.slice(1)).toBe("using Up;\n")
},
})
})
test("creates new file with nested directories", async () => {
await using tmp = await tmpdir()
const filepath = path.join(tmp.path, "nested", "dir", "file.txt")
@@ -183,6 +214,38 @@ describe("tool.edit", () => {
})
})
test("replaces the first visible line in BOM files", async () => {
await using tmp = await tmpdir()
const filepath = path.join(tmp.path, "existing.cs")
const bom = String.fromCharCode(0xfeff)
await fs.writeFile(filepath, `${bom}using System;\nclass Test {}\n`, "utf-8")
await Instance.provide({
directory: tmp.path,
fn: async () => {
const edit = await resolve()
const result = await Effect.runPromise(
edit.execute(
{
filePath: filepath,
oldString: "using System;",
newString: "using Up;",
},
ctx,
),
)
expect(result.metadata.diff).toContain("-using System;")
expect(result.metadata.diff).toContain("+using Up;")
expect(result.metadata.diff).not.toContain(bom)
const content = await fs.readFile(filepath, "utf-8")
expect(content.charCodeAt(0)).toBe(0xfeff)
expect(content.slice(1)).toBe("using Up;\nclass Test {}\n")
},
})
})
test("throws error when file does not exist", async () => {
await using tmp = await tmpdir()
const filepath = path.join(tmp.path, "nonexistent.txt")

View File

@@ -114,6 +114,54 @@ describe("tool.write", () => {
),
)
it.live("preserves BOM when overwriting existing files", () =>
provideTmpdirInstance((dir) =>
Effect.gen(function* () {
const filepath = path.join(dir, "existing.cs")
const bom = String.fromCharCode(0xfeff)
yield* Effect.promise(() => fs.writeFile(filepath, `${bom}using System;\n`, "utf-8"))
yield* run({ filePath: filepath, content: "using Up;\n" })
const content = yield* Effect.promise(() => fs.readFile(filepath, "utf-8"))
expect(content.charCodeAt(0)).toBe(0xfeff)
expect(content.slice(1)).toBe("using Up;\n")
}),
),
)
it.live("restores BOM after formatter strips it", () =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
const filepath = path.join(dir, "formatted.cs")
const bom = String.fromCharCode(0xfeff)
yield* Effect.promise(() => fs.writeFile(filepath, `${bom}using System;\n`, "utf-8"))
yield* run({ filePath: filepath, content: "using Up;\n" })
const content = yield* Effect.promise(() => fs.readFile(filepath, "utf-8"))
expect(content.charCodeAt(0)).toBe(0xfeff)
expect(content.slice(1)).toBe("using Up;\n")
}),
{
config: {
formatter: {
stripbom: {
extensions: [".cs"],
command: [
"node",
"-e",
"const fs = require('fs'); const file = process.argv[1]; let text = fs.readFileSync(file, 'utf8'); if (text.charCodeAt(0) === 0xfeff) text = text.slice(1); fs.writeFileSync(file, text, 'utf8')",
"$FILE",
],
},
},
},
},
),
)
it.live("returns diff in metadata for existing files", () =>
provideTmpdirInstance((dir) =>
Effect.gen(function* () {