apply_patch.test.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. import { describe, expect, test } from "bun:test"
  2. import path from "path"
  3. import * as fs from "fs/promises"
  4. import { Effect, ManagedRuntime, Layer } from "effect"
  5. import { ApplyPatchTool } from "../../src/tool/apply_patch"
  6. import { Instance } from "../../src/project/instance"
  7. import { LSP } from "../../src/lsp"
  8. import { AppFileSystem } from "@opencode-ai/shared/filesystem"
  9. import { Format } from "../../src/format"
  10. import { Agent } from "../../src/agent/agent"
  11. import { Bus } from "../../src/bus"
  12. import { Truncate } from "../../src/tool"
  13. import { tmpdir } from "../fixture/fixture"
  14. import { SessionID, MessageID } from "../../src/session/schema"
  15. const runtime = ManagedRuntime.make(
  16. Layer.mergeAll(
  17. LSP.defaultLayer,
  18. AppFileSystem.defaultLayer,
  19. Format.defaultLayer,
  20. Bus.layer,
  21. Truncate.defaultLayer,
  22. Agent.defaultLayer,
  23. ),
  24. )
  25. const baseCtx = {
  26. sessionID: SessionID.make("ses_test"),
  27. messageID: MessageID.make(""),
  28. callID: "",
  29. agent: "build",
  30. abort: AbortSignal.any([]),
  31. messages: [],
  32. metadata: () => Effect.void,
  33. }
  34. type AskInput = {
  35. permission: string
  36. patterns: string[]
  37. always: string[]
  38. metadata: {
  39. diff: string
  40. filepath: string
  41. files: Array<{
  42. filePath: string
  43. relativePath: string
  44. type: "add" | "update" | "delete" | "move"
  45. patch: string
  46. additions: number
  47. deletions: number
  48. movePath?: string
  49. }>
  50. }
  51. }
  52. type ToolCtx = typeof baseCtx & {
  53. ask: (input: AskInput) => Effect.Effect<void>
  54. }
  55. const execute = async (params: { patchText: string }, ctx: ToolCtx) => {
  56. const info = await runtime.runPromise(ApplyPatchTool)
  57. const tool = await runtime.runPromise(info.init())
  58. return Effect.runPromise(tool.execute(params, ctx))
  59. }
  60. const makeCtx = () => {
  61. const calls: AskInput[] = []
  62. const ctx: ToolCtx = {
  63. ...baseCtx,
  64. ask: (input) =>
  65. Effect.sync(() => {
  66. calls.push(input)
  67. }),
  68. }
  69. return { ctx, calls }
  70. }
  71. describe("tool.apply_patch freeform", () => {
  72. test("requires patchText", async () => {
  73. const { ctx } = makeCtx()
  74. await expect(execute({ patchText: "" }, ctx)).rejects.toThrow("patchText is required")
  75. })
  76. test("rejects invalid patch format", async () => {
  77. const { ctx } = makeCtx()
  78. await expect(execute({ patchText: "invalid patch" }, ctx)).rejects.toThrow("apply_patch verification failed")
  79. })
  80. test("rejects empty patch", async () => {
  81. const { ctx } = makeCtx()
  82. const emptyPatch = "*** Begin Patch\n*** End Patch"
  83. await expect(execute({ patchText: emptyPatch }, ctx)).rejects.toThrow("patch rejected: empty patch")
  84. })
  85. test("applies add/update/delete in one patch", async () => {
  86. await using fixture = await tmpdir({ git: true })
  87. const { ctx, calls } = makeCtx()
  88. await Instance.provide({
  89. directory: fixture.path,
  90. fn: async () => {
  91. const modifyPath = path.join(fixture.path, "modify.txt")
  92. const deletePath = path.join(fixture.path, "delete.txt")
  93. await fs.writeFile(modifyPath, "line1\nline2\n", "utf-8")
  94. await fs.writeFile(deletePath, "obsolete\n", "utf-8")
  95. const patchText =
  96. "*** Begin Patch\n*** Add File: nested/new.txt\n+created\n*** Delete File: delete.txt\n*** Update File: modify.txt\n@@\n-line2\n+changed\n*** End Patch"
  97. const result = await execute({ patchText }, ctx)
  98. expect(result.title).toContain("Success. Updated the following files")
  99. expect(result.output).toContain("Success. Updated the following files")
  100. // Strict formatting assertions for slashes
  101. expect(result.output).toMatch(/A nested\/new\.txt/)
  102. expect(result.output).toMatch(/D delete\.txt/)
  103. expect(result.output).toMatch(/M modify\.txt/)
  104. if (process.platform === "win32") {
  105. expect(result.output).not.toContain("\\")
  106. }
  107. expect(result.metadata.diff).toContain("Index:")
  108. expect(calls.length).toBe(1)
  109. // Verify permission metadata includes files array for UI rendering
  110. const permissionCall = calls[0]
  111. expect(permissionCall.metadata.files).toHaveLength(3)
  112. expect(permissionCall.metadata.files.map((f) => f.type).sort()).toEqual(["add", "delete", "update"])
  113. const addFile = permissionCall.metadata.files.find((f) => f.type === "add")
  114. expect(addFile).toBeDefined()
  115. expect(addFile!.relativePath).toBe("nested/new.txt")
  116. expect(addFile!.patch).toContain("+created")
  117. const updateFile = permissionCall.metadata.files.find((f) => f.type === "update")
  118. expect(updateFile).toBeDefined()
  119. expect(updateFile!.patch).toContain("-line2")
  120. expect(updateFile!.patch).toContain("+changed")
  121. const added = await fs.readFile(path.join(fixture.path, "nested", "new.txt"), "utf-8")
  122. expect(added).toBe("created\n")
  123. expect(await fs.readFile(modifyPath, "utf-8")).toBe("line1\nchanged\n")
  124. await expect(fs.readFile(deletePath, "utf-8")).rejects.toThrow()
  125. },
  126. })
  127. })
  128. test("permission metadata includes move file info", async () => {
  129. await using fixture = await tmpdir({ git: true })
  130. const { ctx, calls } = makeCtx()
  131. await Instance.provide({
  132. directory: fixture.path,
  133. fn: async () => {
  134. const original = path.join(fixture.path, "old", "name.txt")
  135. await fs.mkdir(path.dirname(original), { recursive: true })
  136. await fs.writeFile(original, "old content\n", "utf-8")
  137. const patchText =
  138. "*** Begin Patch\n*** Update File: old/name.txt\n*** Move to: renamed/dir/name.txt\n@@\n-old content\n+new content\n*** End Patch"
  139. await execute({ patchText }, ctx)
  140. expect(calls.length).toBe(1)
  141. const permissionCall = calls[0]
  142. expect(permissionCall.metadata.files).toHaveLength(1)
  143. const moveFile = permissionCall.metadata.files[0]
  144. expect(moveFile.type).toBe("move")
  145. expect(moveFile.relativePath).toBe("renamed/dir/name.txt")
  146. expect(moveFile.movePath).toBe(path.join(fixture.path, "renamed/dir/name.txt"))
  147. expect(moveFile.patch).toContain("-old content")
  148. expect(moveFile.patch).toContain("+new content")
  149. },
  150. })
  151. })
  152. test("applies multiple hunks to one file", async () => {
  153. await using fixture = await tmpdir()
  154. const { ctx } = makeCtx()
  155. await Instance.provide({
  156. directory: fixture.path,
  157. fn: async () => {
  158. const target = path.join(fixture.path, "multi.txt")
  159. await fs.writeFile(target, "line1\nline2\nline3\nline4\n", "utf-8")
  160. const patchText =
  161. "*** Begin Patch\n*** Update File: multi.txt\n@@\n-line2\n+changed2\n@@\n-line4\n+changed4\n*** End Patch"
  162. await execute({ patchText }, ctx)
  163. expect(await fs.readFile(target, "utf-8")).toBe("line1\nchanged2\nline3\nchanged4\n")
  164. },
  165. })
  166. })
  167. test("does not invent a first-line diff for BOM files", async () => {
  168. await using fixture = await tmpdir()
  169. const { ctx, calls } = makeCtx()
  170. await Instance.provide({
  171. directory: fixture.path,
  172. fn: async () => {
  173. const bom = String.fromCharCode(0xfeff)
  174. const target = path.join(fixture.path, "example.cs")
  175. await fs.writeFile(target, `${bom}using System;\n\nclass Test {}\n`, "utf-8")
  176. const patchText = "*** Begin Patch\n*** Update File: example.cs\n@@\n class Test {}\n+class Next {}\n*** End Patch"
  177. await execute({ patchText }, ctx)
  178. expect(calls.length).toBe(1)
  179. const shown = calls[0].metadata.files[0]?.patch ?? ""
  180. expect(shown).not.toContain(bom)
  181. expect(shown).not.toContain("-using System;")
  182. expect(shown).not.toContain("+using System;")
  183. const content = await fs.readFile(target, "utf-8")
  184. expect(content.charCodeAt(0)).toBe(0xfeff)
  185. expect(content.slice(1)).toBe("using System;\n\nclass Test {}\nclass Next {}\n")
  186. },
  187. })
  188. })
  189. test("inserts lines with insert-only hunk", async () => {
  190. await using fixture = await tmpdir()
  191. const { ctx } = makeCtx()
  192. await Instance.provide({
  193. directory: fixture.path,
  194. fn: async () => {
  195. const target = path.join(fixture.path, "insert_only.txt")
  196. await fs.writeFile(target, "alpha\nomega\n", "utf-8")
  197. const patchText = "*** Begin Patch\n*** Update File: insert_only.txt\n@@\n alpha\n+beta\n omega\n*** End Patch"
  198. await execute({ patchText }, ctx)
  199. expect(await fs.readFile(target, "utf-8")).toBe("alpha\nbeta\nomega\n")
  200. },
  201. })
  202. })
  203. test("appends trailing newline on update", async () => {
  204. await using fixture = await tmpdir()
  205. const { ctx } = makeCtx()
  206. await Instance.provide({
  207. directory: fixture.path,
  208. fn: async () => {
  209. const target = path.join(fixture.path, "no_newline.txt")
  210. await fs.writeFile(target, "no newline at end", "utf-8")
  211. const patchText =
  212. "*** Begin Patch\n*** Update File: no_newline.txt\n@@\n-no newline at end\n+first line\n+second line\n*** End Patch"
  213. await execute({ patchText }, ctx)
  214. const contents = await fs.readFile(target, "utf-8")
  215. expect(contents.endsWith("\n")).toBe(true)
  216. expect(contents).toBe("first line\nsecond line\n")
  217. },
  218. })
  219. })
  220. test("moves file to a new directory", async () => {
  221. await using fixture = await tmpdir()
  222. const { ctx } = makeCtx()
  223. await Instance.provide({
  224. directory: fixture.path,
  225. fn: async () => {
  226. const original = path.join(fixture.path, "old", "name.txt")
  227. await fs.mkdir(path.dirname(original), { recursive: true })
  228. await fs.writeFile(original, "old content\n", "utf-8")
  229. const patchText =
  230. "*** Begin Patch\n*** Update File: old/name.txt\n*** Move to: renamed/dir/name.txt\n@@\n-old content\n+new content\n*** End Patch"
  231. await execute({ patchText }, ctx)
  232. const moved = path.join(fixture.path, "renamed", "dir", "name.txt")
  233. await expect(fs.readFile(original, "utf-8")).rejects.toThrow()
  234. expect(await fs.readFile(moved, "utf-8")).toBe("new content\n")
  235. },
  236. })
  237. })
  238. test("moves file overwriting existing destination", async () => {
  239. await using fixture = await tmpdir()
  240. const { ctx } = makeCtx()
  241. await Instance.provide({
  242. directory: fixture.path,
  243. fn: async () => {
  244. const original = path.join(fixture.path, "old", "name.txt")
  245. const destination = path.join(fixture.path, "renamed", "dir", "name.txt")
  246. await fs.mkdir(path.dirname(original), { recursive: true })
  247. await fs.mkdir(path.dirname(destination), { recursive: true })
  248. await fs.writeFile(original, "from\n", "utf-8")
  249. await fs.writeFile(destination, "existing\n", "utf-8")
  250. const patchText =
  251. "*** Begin Patch\n*** Update File: old/name.txt\n*** Move to: renamed/dir/name.txt\n@@\n-from\n+new\n*** End Patch"
  252. await execute({ patchText }, ctx)
  253. await expect(fs.readFile(original, "utf-8")).rejects.toThrow()
  254. expect(await fs.readFile(destination, "utf-8")).toBe("new\n")
  255. },
  256. })
  257. })
  258. test("adds file overwriting existing file", async () => {
  259. await using fixture = await tmpdir()
  260. const { ctx } = makeCtx()
  261. await Instance.provide({
  262. directory: fixture.path,
  263. fn: async () => {
  264. const target = path.join(fixture.path, "duplicate.txt")
  265. await fs.writeFile(target, "old content\n", "utf-8")
  266. const patchText = "*** Begin Patch\n*** Add File: duplicate.txt\n+new content\n*** End Patch"
  267. await execute({ patchText }, ctx)
  268. expect(await fs.readFile(target, "utf-8")).toBe("new content\n")
  269. },
  270. })
  271. })
  272. test("rejects update when target file is missing", async () => {
  273. await using fixture = await tmpdir()
  274. const { ctx } = makeCtx()
  275. await Instance.provide({
  276. directory: fixture.path,
  277. fn: async () => {
  278. const patchText = "*** Begin Patch\n*** Update File: missing.txt\n@@\n-nope\n+better\n*** End Patch"
  279. await expect(execute({ patchText }, ctx)).rejects.toThrow(
  280. "apply_patch verification failed: Failed to read file to update",
  281. )
  282. },
  283. })
  284. })
  285. test("rejects delete when file is missing", async () => {
  286. await using fixture = await tmpdir()
  287. const { ctx } = makeCtx()
  288. await Instance.provide({
  289. directory: fixture.path,
  290. fn: async () => {
  291. const patchText = "*** Begin Patch\n*** Delete File: missing.txt\n*** End Patch"
  292. await expect(execute({ patchText }, ctx)).rejects.toThrow()
  293. },
  294. })
  295. })
  296. test("rejects delete when target is a directory", async () => {
  297. await using fixture = await tmpdir()
  298. const { ctx } = makeCtx()
  299. await Instance.provide({
  300. directory: fixture.path,
  301. fn: async () => {
  302. const dirPath = path.join(fixture.path, "dir")
  303. await fs.mkdir(dirPath)
  304. const patchText = "*** Begin Patch\n*** Delete File: dir\n*** End Patch"
  305. await expect(execute({ patchText }, ctx)).rejects.toThrow()
  306. },
  307. })
  308. })
  309. test("rejects invalid hunk header", async () => {
  310. await using fixture = await tmpdir()
  311. const { ctx } = makeCtx()
  312. await Instance.provide({
  313. directory: fixture.path,
  314. fn: async () => {
  315. const patchText = "*** Begin Patch\n*** Frobnicate File: foo\n*** End Patch"
  316. await expect(execute({ patchText }, ctx)).rejects.toThrow("apply_patch verification failed")
  317. },
  318. })
  319. })
  320. test("rejects update with missing context", async () => {
  321. await using fixture = await tmpdir()
  322. const { ctx } = makeCtx()
  323. await Instance.provide({
  324. directory: fixture.path,
  325. fn: async () => {
  326. const target = path.join(fixture.path, "modify.txt")
  327. await fs.writeFile(target, "line1\nline2\n", "utf-8")
  328. const patchText = "*** Begin Patch\n*** Update File: modify.txt\n@@\n-missing\n+changed\n*** End Patch"
  329. await expect(execute({ patchText }, ctx)).rejects.toThrow("apply_patch verification failed")
  330. expect(await fs.readFile(target, "utf-8")).toBe("line1\nline2\n")
  331. },
  332. })
  333. })
  334. test("verification failure leaves no side effects", async () => {
  335. await using fixture = await tmpdir()
  336. const { ctx } = makeCtx()
  337. await Instance.provide({
  338. directory: fixture.path,
  339. fn: async () => {
  340. const patchText =
  341. "*** Begin Patch\n*** Add File: created.txt\n+hello\n*** Update File: missing.txt\n@@\n-old\n+new\n*** End Patch"
  342. await expect(execute({ patchText }, ctx)).rejects.toThrow()
  343. const createdPath = path.join(fixture.path, "created.txt")
  344. await expect(fs.readFile(createdPath, "utf-8")).rejects.toThrow()
  345. },
  346. })
  347. })
  348. test("supports end of file anchor", async () => {
  349. await using fixture = await tmpdir()
  350. const { ctx } = makeCtx()
  351. await Instance.provide({
  352. directory: fixture.path,
  353. fn: async () => {
  354. const target = path.join(fixture.path, "tail.txt")
  355. await fs.writeFile(target, "alpha\nlast\n", "utf-8")
  356. const patchText = "*** Begin Patch\n*** Update File: tail.txt\n@@\n-last\n+end\n*** End of File\n*** End Patch"
  357. await execute({ patchText }, ctx)
  358. expect(await fs.readFile(target, "utf-8")).toBe("alpha\nend\n")
  359. },
  360. })
  361. })
  362. test("rejects missing second chunk context", async () => {
  363. await using fixture = await tmpdir()
  364. const { ctx } = makeCtx()
  365. await Instance.provide({
  366. directory: fixture.path,
  367. fn: async () => {
  368. const target = path.join(fixture.path, "two_chunks.txt")
  369. await fs.writeFile(target, "a\nb\nc\nd\n", "utf-8")
  370. const patchText = "*** Begin Patch\n*** Update File: two_chunks.txt\n@@\n-b\n+B\n\n-d\n+D\n*** End Patch"
  371. await expect(execute({ patchText }, ctx)).rejects.toThrow()
  372. expect(await fs.readFile(target, "utf-8")).toBe("a\nb\nc\nd\n")
  373. },
  374. })
  375. })
  376. test("disambiguates change context with @@ header", async () => {
  377. await using fixture = await tmpdir()
  378. const { ctx } = makeCtx()
  379. await Instance.provide({
  380. directory: fixture.path,
  381. fn: async () => {
  382. const target = path.join(fixture.path, "multi_ctx.txt")
  383. await fs.writeFile(target, "fn a\nx=10\ny=2\nfn b\nx=10\ny=20\n", "utf-8")
  384. const patchText = "*** Begin Patch\n*** Update File: multi_ctx.txt\n@@ fn b\n-x=10\n+x=11\n*** End Patch"
  385. await execute({ patchText }, ctx)
  386. expect(await fs.readFile(target, "utf-8")).toBe("fn a\nx=10\ny=2\nfn b\nx=11\ny=20\n")
  387. },
  388. })
  389. })
  390. test("EOF anchor matches from end of file first", async () => {
  391. await using fixture = await tmpdir()
  392. const { ctx } = makeCtx()
  393. await Instance.provide({
  394. directory: fixture.path,
  395. fn: async () => {
  396. const target = path.join(fixture.path, "eof_anchor.txt")
  397. // File has duplicate "marker" lines - one in middle, one at end
  398. await fs.writeFile(target, "start\nmarker\nmiddle\nmarker\nend\n", "utf-8")
  399. // With EOF anchor, should match the LAST "marker" line, not the first
  400. const patchText =
  401. "*** Begin Patch\n*** Update File: eof_anchor.txt\n@@\n-marker\n-end\n+marker-changed\n+end\n*** End of File\n*** End Patch"
  402. await execute({ patchText }, ctx)
  403. // First marker unchanged, second marker changed
  404. expect(await fs.readFile(target, "utf-8")).toBe("start\nmarker\nmiddle\nmarker-changed\nend\n")
  405. },
  406. })
  407. })
  408. test("parses heredoc-wrapped patch", async () => {
  409. await using fixture = await tmpdir()
  410. const { ctx } = makeCtx()
  411. await Instance.provide({
  412. directory: fixture.path,
  413. fn: async () => {
  414. const patchText = `cat <<'EOF'
  415. *** Begin Patch
  416. *** Add File: heredoc_test.txt
  417. +heredoc content
  418. *** End Patch
  419. EOF`
  420. await execute({ patchText }, ctx)
  421. const content = await fs.readFile(path.join(fixture.path, "heredoc_test.txt"), "utf-8")
  422. expect(content).toBe("heredoc content\n")
  423. },
  424. })
  425. })
  426. test("parses heredoc-wrapped patch without cat", async () => {
  427. await using fixture = await tmpdir()
  428. const { ctx } = makeCtx()
  429. await Instance.provide({
  430. directory: fixture.path,
  431. fn: async () => {
  432. const patchText = `<<EOF
  433. *** Begin Patch
  434. *** Add File: heredoc_no_cat.txt
  435. +no cat prefix
  436. *** End Patch
  437. EOF`
  438. await execute({ patchText }, ctx)
  439. const content = await fs.readFile(path.join(fixture.path, "heredoc_no_cat.txt"), "utf-8")
  440. expect(content).toBe("no cat prefix\n")
  441. },
  442. })
  443. })
  444. test("matches with trailing whitespace differences", async () => {
  445. await using fixture = await tmpdir()
  446. const { ctx } = makeCtx()
  447. await Instance.provide({
  448. directory: fixture.path,
  449. fn: async () => {
  450. const target = path.join(fixture.path, "trailing_ws.txt")
  451. // File has trailing spaces on some lines
  452. await fs.writeFile(target, "line1 \nline2\nline3 \n", "utf-8")
  453. // Patch doesn't have trailing spaces - should still match via rstrip pass
  454. const patchText = "*** Begin Patch\n*** Update File: trailing_ws.txt\n@@\n-line2\n+changed\n*** End Patch"
  455. await execute({ patchText }, ctx)
  456. expect(await fs.readFile(target, "utf-8")).toBe("line1 \nchanged\nline3 \n")
  457. },
  458. })
  459. })
  460. test("matches with leading whitespace differences", async () => {
  461. await using fixture = await tmpdir()
  462. const { ctx } = makeCtx()
  463. await Instance.provide({
  464. directory: fixture.path,
  465. fn: async () => {
  466. const target = path.join(fixture.path, "leading_ws.txt")
  467. // File has leading spaces
  468. await fs.writeFile(target, " line1\nline2\n line3\n", "utf-8")
  469. // Patch without leading spaces - should match via trim pass
  470. const patchText = "*** Begin Patch\n*** Update File: leading_ws.txt\n@@\n-line2\n+changed\n*** End Patch"
  471. await execute({ patchText }, ctx)
  472. expect(await fs.readFile(target, "utf-8")).toBe(" line1\nchanged\n line3\n")
  473. },
  474. })
  475. })
  476. test("matches with Unicode punctuation differences", async () => {
  477. await using fixture = await tmpdir()
  478. const { ctx } = makeCtx()
  479. await Instance.provide({
  480. directory: fixture.path,
  481. fn: async () => {
  482. const target = path.join(fixture.path, "unicode.txt")
  483. // File has fancy Unicode quotes (U+201C, U+201D) and em-dash (U+2014)
  484. const leftQuote = "\u201C"
  485. const rightQuote = "\u201D"
  486. const emDash = "\u2014"
  487. await fs.writeFile(target, `He said ${leftQuote}hello${rightQuote}\nsome${emDash}dash\nend\n`, "utf-8")
  488. // Patch uses ASCII equivalents - should match via normalized pass
  489. // The replacement uses ASCII quotes from the patch (not preserving Unicode)
  490. const patchText =
  491. '*** Begin Patch\n*** Update File: unicode.txt\n@@\n-He said "hello"\n+He said "hi"\n*** End Patch'
  492. await execute({ patchText }, ctx)
  493. // Result has ASCII quotes because that's what the patch specifies
  494. expect(await fs.readFile(target, "utf-8")).toBe(`He said "hi"\nsome${emDash}dash\nend\n`)
  495. },
  496. })
  497. })
  498. })