// 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") }) })