213 lines
6.4 KiB
TypeScript
213 lines
6.4 KiB
TypeScript
// Tests the kilocode-specific patch module guarantees:
|
|
// - Files retain their original encoding after an update (UTF-8 BOM, UTF-16,
|
|
// legacy single-byte, CJK).
|
|
// - Plain UTF-8 files do not gain a spurious BOM.
|
|
// - Moved files keep the original encoding at the new path.
|
|
// These round-trip through the apply_patch tool so the Kilo encoding layer is
|
|
// exercised with the upstream patch parser.
|
|
|
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
import fs from "fs/promises"
|
|
import path from "path"
|
|
import { tmpdir } from "os"
|
|
import iconv from "iconv-lite"
|
|
import { Effect, Layer } from "effect"
|
|
import { Agent } from "../../src/agent/agent"
|
|
import { Bus } from "../../src/bus"
|
|
import { Format } from "../../src/format"
|
|
import { LSP } from "../../src/lsp/lsp"
|
|
import { MessageID, SessionID } from "../../src/session/schema"
|
|
import { ApplyPatchTool } from "../../src/tool/apply_patch"
|
|
import { Tool } from "../../src/tool/tool"
|
|
import { Truncate } from "../../src/tool/truncate"
|
|
import { provideInstance } from "../fixture/fixture"
|
|
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
|
|
|
const layer = Layer.mergeAll(
|
|
Agent.defaultLayer,
|
|
AppFileSystem.defaultLayer,
|
|
Bus.layer,
|
|
Format.defaultLayer,
|
|
LSP.defaultLayer,
|
|
Truncate.defaultLayer,
|
|
)
|
|
|
|
const apply = (dir: string, patchText: string) =>
|
|
Effect.runPromise(
|
|
Effect.gen(function* () {
|
|
const info = yield* ApplyPatchTool
|
|
const tool = yield* Tool.init(info)
|
|
yield* tool.execute(
|
|
{ patchText },
|
|
{
|
|
sessionID: SessionID.make("ses_patch"),
|
|
messageID: MessageID.make("msg_patch"),
|
|
callID: "call_patch",
|
|
agent: "code",
|
|
abort: AbortSignal.any([]),
|
|
messages: [],
|
|
metadata: () => Effect.void,
|
|
ask: () => Effect.void,
|
|
},
|
|
)
|
|
}).pipe(provideInstance(dir), Effect.scoped, Effect.provide(layer)),
|
|
)
|
|
|
|
const UTF8_BOM = Buffer.from([0xef, 0xbb, 0xbf])
|
|
const UTF16_LE_BOM = Buffer.from([0xff, 0xfe])
|
|
|
|
describe("Patch encoding preservation", () => {
|
|
let dir: string
|
|
|
|
beforeEach(async () => {
|
|
dir = await fs.mkdtemp(path.join(tmpdir(), "kilo-patch-"))
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await fs.rm(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test("preserves UTF-8 BOM through update", async () => {
|
|
const file = path.join(dir, "doc.txt")
|
|
await fs.writeFile(file, Buffer.concat([UTF8_BOM, Buffer.from("line 1\nline 2\n", "utf-8")]))
|
|
|
|
const patch = `*** Begin Patch
|
|
*** Update File: ${file}
|
|
@@
|
|
line 1
|
|
-line 2
|
|
+line 2 updated
|
|
*** End Patch`
|
|
|
|
await apply(dir, patch)
|
|
|
|
const bytes = await fs.readFile(file)
|
|
expect(bytes.subarray(0, 3).equals(UTF8_BOM)).toBe(true)
|
|
expect(bytes.subarray(3).toString("utf-8")).toBe("line 1\nline 2 updated\n")
|
|
})
|
|
|
|
test("does not introduce BOM for plain UTF-8 files", async () => {
|
|
const file = path.join(dir, "plain.txt")
|
|
await fs.writeFile(file, "line 1\nline 2\n", "utf-8")
|
|
|
|
const patch = `*** Begin Patch
|
|
*** Update File: ${file}
|
|
@@
|
|
line 1
|
|
-line 2
|
|
+line 2 updated
|
|
*** End Patch`
|
|
|
|
await apply(dir, patch)
|
|
|
|
const bytes = await fs.readFile(file)
|
|
expect(bytes[0]).not.toBe(0xef)
|
|
expect(bytes.toString("utf-8")).toBe("line 1\nline 2 updated\n")
|
|
})
|
|
|
|
test("preserves UTF-16 LE encoding through update", async () => {
|
|
const file = path.join(dir, "utf16.txt")
|
|
await fs.writeFile(file, Buffer.concat([UTF16_LE_BOM, iconv.encode("line 1\nline 2\n", "utf-16le")]))
|
|
|
|
const patch = `*** Begin Patch
|
|
*** Update File: ${file}
|
|
@@
|
|
line 1
|
|
-line 2
|
|
+line 2 updated
|
|
*** End Patch`
|
|
|
|
await apply(dir, patch)
|
|
|
|
const bytes = await fs.readFile(file)
|
|
expect(bytes.subarray(0, 2).equals(UTF16_LE_BOM)).toBe(true)
|
|
expect(iconv.decode(bytes.subarray(2), "utf-16le")).toBe("line 1\nline 2 updated\n")
|
|
})
|
|
|
|
test("preserves iso-8859-1 encoding through update", async () => {
|
|
const file = path.join(dir, "latin1.txt")
|
|
await fs.writeFile(file, iconv.encode("café\nñandú\n", "iso-8859-1"))
|
|
|
|
const patch = `*** Begin Patch
|
|
*** Update File: ${file}
|
|
@@
|
|
café
|
|
-ñandú
|
|
+águila
|
|
*** End Patch`
|
|
|
|
await apply(dir, patch)
|
|
|
|
const bytes = await fs.readFile(file)
|
|
expect(iconv.decode(bytes, "iso-8859-1")).toBe("café\náguila\n")
|
|
// á and ñ are two bytes in UTF-8, one byte in ISO-8859-1. If the file had
|
|
// been silently re-encoded as UTF-8 the byte length would differ.
|
|
expect(bytes.length).toBe("café\náguila\n".length)
|
|
})
|
|
|
|
test("preserves Shift_JIS encoding through update", async () => {
|
|
const file = path.join(dir, "jp.txt")
|
|
// chardet needs enough characteristic bytes to identify Shift_JIS. A
|
|
// single 19-byte phrase looks like windows-1252, so the sample is padded
|
|
// to match the body of Japanese text the tool tests already rely on.
|
|
const sample = "こんにちは、世界!日本語のテストです。"
|
|
await fs.writeFile(file, iconv.encode(`line1\n${sample}\nline3\n`, "Shift_JIS"))
|
|
|
|
const patch = `*** Begin Patch
|
|
*** Update File: ${file}
|
|
@@
|
|
line1
|
|
-${sample}
|
|
+さようなら、世界!
|
|
line3
|
|
*** End Patch`
|
|
|
|
await apply(dir, patch)
|
|
|
|
const bytes = await fs.readFile(file)
|
|
expect(iconv.decode(bytes, "Shift_JIS")).toBe("line1\nさようなら、世界!\nline3\n")
|
|
const utf8Rendered = Buffer.from("line1\nさようなら、世界!\nline3\n", "utf-8")
|
|
expect(bytes.equals(utf8Rendered)).toBe(false)
|
|
})
|
|
|
|
test("preserves UTF-8 BOM when file is moved", async () => {
|
|
const from = path.join(dir, "old.txt")
|
|
const to = path.join(dir, "new.txt")
|
|
await fs.writeFile(from, Buffer.concat([UTF8_BOM, Buffer.from("original\n", "utf-8")]))
|
|
|
|
const patch = `*** Begin Patch
|
|
*** Update File: ${from}
|
|
*** Move to: ${to}
|
|
@@
|
|
-original
|
|
+updated
|
|
*** End Patch`
|
|
|
|
await apply(dir, patch)
|
|
|
|
const moved = await fs.readFile(to)
|
|
expect(moved.subarray(0, 3).equals(UTF8_BOM)).toBe(true)
|
|
expect(moved.subarray(3).toString("utf-8")).toBe("updated\n")
|
|
|
|
const oldExists = await fs
|
|
.access(from)
|
|
.then(() => true)
|
|
.catch(() => false)
|
|
expect(oldExists).toBe(false)
|
|
})
|
|
|
|
test("new files added via patch are written as plain UTF-8", async () => {
|
|
const file = path.join(dir, "new.txt")
|
|
const patch = `*** Begin Patch
|
|
*** Add File: ${file}
|
|
+hello world
|
|
*** End Patch`
|
|
|
|
await apply(dir, patch)
|
|
|
|
const bytes = await fs.readFile(file)
|
|
expect(bytes[0]).not.toBe(0xef)
|
|
expect(bytes.toString("utf-8")).toBe("hello world\n")
|
|
})
|
|
})
|