opencode-dux 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +452 -0
  3. package/dist/agents/descriptions.d.ts +6 -0
  4. package/dist/agents/designer.d.ts +2 -0
  5. package/dist/agents/explorer.d.ts +2 -0
  6. package/dist/agents/fixer.d.ts +2 -0
  7. package/dist/agents/index.d.ts +22 -0
  8. package/dist/agents/interpreter.d.ts +2 -0
  9. package/dist/agents/librarian.d.ts +2 -0
  10. package/dist/agents/oracle.d.ts +2 -0
  11. package/dist/agents/orchestrator.d.ts +27 -0
  12. package/dist/agents/overrides.d.ts +18 -0
  13. package/dist/agents/prompt-blocks.d.ts +97 -0
  14. package/dist/agents/steward.d.ts +3 -0
  15. package/dist/cli/config-io.d.ts +24 -0
  16. package/dist/cli/config-manager.d.ts +4 -0
  17. package/dist/cli/index.d.ts +2 -0
  18. package/dist/cli/index.js +1006 -0
  19. package/dist/cli/install.d.ts +2 -0
  20. package/dist/cli/mcps.d.ts +13 -0
  21. package/dist/cli/model-key-normalization.d.ts +1 -0
  22. package/dist/cli/paths.d.ts +35 -0
  23. package/dist/cli/providers.d.ts +137 -0
  24. package/dist/cli/skills.d.ts +22 -0
  25. package/dist/cli/system.d.ts +5 -0
  26. package/dist/cli/types.d.ts +38 -0
  27. package/dist/config/constants.d.ts +12 -0
  28. package/dist/config/index.d.ts +4 -0
  29. package/dist/config/loader.d.ts +40 -0
  30. package/dist/config/runtime-preset.d.ts +12 -0
  31. package/dist/config/schema.d.ts +281 -0
  32. package/dist/config/utils.d.ts +10 -0
  33. package/dist/discovery/local/types.d.ts +79 -0
  34. package/dist/discovery/local.d.ts +73 -0
  35. package/dist/discovery/mcp-servers.d.ts +88 -0
  36. package/dist/discovery/skills.d.ts +94 -0
  37. package/dist/hooks/apply-patch/codec.d.ts +7 -0
  38. package/dist/hooks/apply-patch/errors.d.ts +25 -0
  39. package/dist/hooks/apply-patch/execution-context.d.ts +27 -0
  40. package/dist/hooks/apply-patch/index.d.ts +15 -0
  41. package/dist/hooks/apply-patch/matching.d.ts +26 -0
  42. package/dist/hooks/apply-patch/operations.d.ts +3 -0
  43. package/dist/hooks/apply-patch/patch.d.ts +2 -0
  44. package/dist/hooks/apply-patch/prepared-changes.d.ts +17 -0
  45. package/dist/hooks/apply-patch/resolution.d.ts +19 -0
  46. package/dist/hooks/apply-patch/rewrite.d.ts +7 -0
  47. package/dist/hooks/apply-patch/test-helpers.d.ts +6 -0
  48. package/dist/hooks/apply-patch/types.d.ts +80 -0
  49. package/dist/hooks/auto-update-checker/cache.d.ts +11 -0
  50. package/dist/hooks/auto-update-checker/checker.d.ts +32 -0
  51. package/dist/hooks/auto-update-checker/constants.d.ts +11 -0
  52. package/dist/hooks/auto-update-checker/index.d.ts +18 -0
  53. package/dist/hooks/auto-update-checker/types.d.ts +22 -0
  54. package/dist/hooks/chat-headers.d.ts +16 -0
  55. package/dist/hooks/context-pressure-reminder/index.d.ts +33 -0
  56. package/dist/hooks/delegate-task-retry/guidance.d.ts +2 -0
  57. package/dist/hooks/delegate-task-retry/hook.d.ts +8 -0
  58. package/dist/hooks/delegate-task-retry/index.d.ts +4 -0
  59. package/dist/hooks/delegate-task-retry/patterns.d.ts +11 -0
  60. package/dist/hooks/filter-available-skills/index.d.ts +32 -0
  61. package/dist/hooks/foreground-fallback/index.d.ts +72 -0
  62. package/dist/hooks/image-hook.d.ts +5 -0
  63. package/dist/hooks/index.d.ts +14 -0
  64. package/dist/hooks/json-error-recovery/hook.d.ts +18 -0
  65. package/dist/hooks/json-error-recovery/index.d.ts +1 -0
  66. package/dist/hooks/phase-reminder/index.d.ts +26 -0
  67. package/dist/hooks/post-file-tool-nudge/index.d.ts +19 -0
  68. package/dist/hooks/task-session-manager/index.d.ts +52 -0
  69. package/dist/hooks/todo-continuation/index.d.ts +53 -0
  70. package/dist/hooks/todo-continuation/todo-hygiene.d.ts +35 -0
  71. package/dist/index.d.ts +5 -0
  72. package/dist/index.js +31782 -0
  73. package/dist/mcp/context7.d.ts +6 -0
  74. package/dist/mcp/grep-app.d.ts +6 -0
  75. package/dist/mcp/index.d.ts +13 -0
  76. package/dist/mcp/types.d.ts +12 -0
  77. package/dist/mcp/websearch.d.ts +9 -0
  78. package/dist/skills/registry.d.ts +29 -0
  79. package/dist/subscriptions/accounts-store.d.ts +57 -0
  80. package/dist/subscriptions/index.d.ts +13 -0
  81. package/dist/subscriptions/neuralwatt-scraper.d.ts +14 -0
  82. package/dist/subscriptions/opencode-go-scraper.d.ts +27 -0
  83. package/dist/subscriptions/types.d.ts +115 -0
  84. package/dist/subscriptions/usage-service.d.ts +74 -0
  85. package/dist/tools/ast-grep/cli.d.ts +15 -0
  86. package/dist/tools/ast-grep/constants.d.ts +25 -0
  87. package/dist/tools/ast-grep/downloader.d.ts +5 -0
  88. package/dist/tools/ast-grep/index.d.ts +10 -0
  89. package/dist/tools/ast-grep/tools.d.ts +3 -0
  90. package/dist/tools/ast-grep/types.d.ts +30 -0
  91. package/dist/tools/ast-grep/utils.d.ts +4 -0
  92. package/dist/tools/delegate.d.ts +14 -0
  93. package/dist/tools/index.d.ts +5 -0
  94. package/dist/tools/preset-manager.d.ts +27 -0
  95. package/dist/tools/smartfetch/binary.d.ts +3 -0
  96. package/dist/tools/smartfetch/cache.d.ts +6 -0
  97. package/dist/tools/smartfetch/constants.d.ts +12 -0
  98. package/dist/tools/smartfetch/index.d.ts +3 -0
  99. package/dist/tools/smartfetch/network.d.ts +38 -0
  100. package/dist/tools/smartfetch/secondary-model.d.ts +28 -0
  101. package/dist/tools/smartfetch/tool.d.ts +3 -0
  102. package/dist/tools/smartfetch/types.d.ts +122 -0
  103. package/dist/tools/smartfetch/utils.d.ts +18 -0
  104. package/dist/tui-state.d.ts +168 -0
  105. package/dist/tui.d.ts +37 -0
  106. package/dist/tui.js +1896 -0
  107. package/dist/utils/agent-variant.d.ts +63 -0
  108. package/dist/utils/compat.d.ts +30 -0
  109. package/dist/utils/env.d.ts +1 -0
  110. package/dist/utils/index.d.ts +9 -0
  111. package/dist/utils/internal-initiator.d.ts +6 -0
  112. package/dist/utils/logger.d.ts +8 -0
  113. package/dist/utils/polling.d.ts +21 -0
  114. package/dist/utils/session-manager.d.ts +55 -0
  115. package/dist/utils/session.d.ts +90 -0
  116. package/dist/utils/subagent-depth.d.ts +35 -0
  117. package/dist/utils/system-collapse.d.ts +6 -0
  118. package/dist/utils/task.d.ts +4 -0
  119. package/dist/utils/zip-extractor.d.ts +1 -0
  120. package/index.ts +1 -0
  121. package/opencode-dux.schema.json +634 -0
  122. package/package.json +103 -0
  123. package/src/agents/descriptions.ts +55 -0
  124. package/src/agents/designer.test.ts +86 -0
  125. package/src/agents/designer.ts +154 -0
  126. package/src/agents/display-name.test.ts +186 -0
  127. package/src/agents/explorer.test.ts +79 -0
  128. package/src/agents/explorer.ts +144 -0
  129. package/src/agents/fixer.test.ts +79 -0
  130. package/src/agents/fixer.ts +145 -0
  131. package/src/agents/index.test.ts +472 -0
  132. package/src/agents/index.ts +248 -0
  133. package/src/agents/interpreter.ts +136 -0
  134. package/src/agents/librarian.test.ts +80 -0
  135. package/src/agents/librarian.ts +145 -0
  136. package/src/agents/oracle.test.ts +89 -0
  137. package/src/agents/oracle.ts +184 -0
  138. package/src/agents/orchestrator.test.ts +116 -0
  139. package/src/agents/orchestrator.ts +574 -0
  140. package/src/agents/overrides.ts +95 -0
  141. package/src/agents/prompt-blocks.test.ts +114 -0
  142. package/src/agents/prompt-blocks.ts +640 -0
  143. package/src/agents/steward.ts +146 -0
  144. package/src/cli/config-io.test.ts +536 -0
  145. package/src/cli/config-io.ts +473 -0
  146. package/src/cli/config-manager.test.ts +141 -0
  147. package/src/cli/config-manager.ts +4 -0
  148. package/src/cli/index.ts +88 -0
  149. package/src/cli/install.ts +282 -0
  150. package/src/cli/mcps.test.ts +62 -0
  151. package/src/cli/mcps.ts +39 -0
  152. package/src/cli/model-key-normalization.test.ts +21 -0
  153. package/src/cli/model-key-normalization.ts +60 -0
  154. package/src/cli/paths.test.ts +167 -0
  155. package/src/cli/paths.ts +144 -0
  156. package/src/cli/providers.test.ts +118 -0
  157. package/src/cli/providers.ts +141 -0
  158. package/src/cli/skills.test.ts +111 -0
  159. package/src/cli/skills.ts +103 -0
  160. package/src/cli/system.test.ts +91 -0
  161. package/src/cli/system.ts +180 -0
  162. package/src/cli/types.ts +43 -0
  163. package/src/config/constants.ts +58 -0
  164. package/src/config/index.ts +4 -0
  165. package/src/config/loader.test.ts +1194 -0
  166. package/src/config/loader.ts +269 -0
  167. package/src/config/model-resolution.test.ts +176 -0
  168. package/src/config/runtime-preset.test.ts +61 -0
  169. package/src/config/runtime-preset.ts +37 -0
  170. package/src/config/schema.ts +248 -0
  171. package/src/config/utils.test.ts +41 -0
  172. package/src/config/utils.ts +23 -0
  173. package/src/discovery/local/types.ts +85 -0
  174. package/src/discovery/local.ts +322 -0
  175. package/src/discovery/mcp-servers.ts +804 -0
  176. package/src/discovery/skills.ts +959 -0
  177. package/src/hooks/apply-patch/codec.test.ts +184 -0
  178. package/src/hooks/apply-patch/codec.ts +352 -0
  179. package/src/hooks/apply-patch/errors.ts +117 -0
  180. package/src/hooks/apply-patch/execution-context.ts +432 -0
  181. package/src/hooks/apply-patch/hook.test.ts +768 -0
  182. package/src/hooks/apply-patch/index.ts +126 -0
  183. package/src/hooks/apply-patch/matching.test.ts +215 -0
  184. package/src/hooks/apply-patch/matching.ts +586 -0
  185. package/src/hooks/apply-patch/operations.test.ts +1535 -0
  186. package/src/hooks/apply-patch/operations.ts +3 -0
  187. package/src/hooks/apply-patch/patch.ts +9 -0
  188. package/src/hooks/apply-patch/prepared-changes.ts +400 -0
  189. package/src/hooks/apply-patch/resolution.test.ts +420 -0
  190. package/src/hooks/apply-patch/resolution.ts +437 -0
  191. package/src/hooks/apply-patch/rewrite.ts +496 -0
  192. package/src/hooks/apply-patch/test-helpers.ts +52 -0
  193. package/src/hooks/apply-patch/types.ts +111 -0
  194. package/src/hooks/auto-update-checker/cache.test.ts +179 -0
  195. package/src/hooks/auto-update-checker/cache.ts +188 -0
  196. package/src/hooks/auto-update-checker/checker.test.ts +159 -0
  197. package/src/hooks/auto-update-checker/checker.ts +308 -0
  198. package/src/hooks/auto-update-checker/constants.ts +33 -0
  199. package/src/hooks/auto-update-checker/index.test.ts +282 -0
  200. package/src/hooks/auto-update-checker/index.ts +225 -0
  201. package/src/hooks/auto-update-checker/types.ts +26 -0
  202. package/src/hooks/chat-headers.test.ts +236 -0
  203. package/src/hooks/chat-headers.ts +97 -0
  204. package/src/hooks/context-pressure-reminder/index.test.ts +179 -0
  205. package/src/hooks/context-pressure-reminder/index.ts +137 -0
  206. package/src/hooks/delegate-task-retry/guidance.ts +41 -0
  207. package/src/hooks/delegate-task-retry/hook.ts +23 -0
  208. package/src/hooks/delegate-task-retry/index.test.ts +38 -0
  209. package/src/hooks/delegate-task-retry/index.ts +7 -0
  210. package/src/hooks/delegate-task-retry/patterns.ts +79 -0
  211. package/src/hooks/filter-available-skills/index.test.ts +297 -0
  212. package/src/hooks/filter-available-skills/index.ts +160 -0
  213. package/src/hooks/foreground-fallback/index.test.ts +624 -0
  214. package/src/hooks/foreground-fallback/index.ts +374 -0
  215. package/src/hooks/image-hook.ts +6 -0
  216. package/src/hooks/index.ts +17 -0
  217. package/src/hooks/json-error-recovery/hook.ts +73 -0
  218. package/src/hooks/json-error-recovery/index.test.ts +111 -0
  219. package/src/hooks/json-error-recovery/index.ts +6 -0
  220. package/src/hooks/phase-reminder/index.test.ts +74 -0
  221. package/src/hooks/phase-reminder/index.ts +85 -0
  222. package/src/hooks/post-file-tool-nudge/index.test.ts +94 -0
  223. package/src/hooks/post-file-tool-nudge/index.ts +63 -0
  224. package/src/hooks/task-session-manager/index.test.ts +833 -0
  225. package/src/hooks/task-session-manager/index.ts +434 -0
  226. package/src/hooks/todo-continuation/index.test.ts +3026 -0
  227. package/src/hooks/todo-continuation/index.ts +878 -0
  228. package/src/hooks/todo-continuation/todo-hygiene.test.ts +204 -0
  229. package/src/hooks/todo-continuation/todo-hygiene.ts +207 -0
  230. package/src/index.ts +1672 -0
  231. package/src/mcp/context7.ts +14 -0
  232. package/src/mcp/grep-app.ts +11 -0
  233. package/src/mcp/index.test.ts +96 -0
  234. package/src/mcp/index.ts +66 -0
  235. package/src/mcp/types.ts +16 -0
  236. package/src/mcp/websearch.ts +47 -0
  237. package/src/skills/codemap/README.md +60 -0
  238. package/src/skills/codemap/SKILL.md +174 -0
  239. package/src/skills/codemap/scripts/codemap.mjs +483 -0
  240. package/src/skills/codemap/scripts/codemap.test.ts +129 -0
  241. package/src/skills/registry.ts +218 -0
  242. package/src/skills/simplify/README.md +19 -0
  243. package/src/skills/simplify/SKILL.md +138 -0
  244. package/src/subscriptions/accounts-store.test.ts +236 -0
  245. package/src/subscriptions/accounts-store.ts +184 -0
  246. package/src/subscriptions/index.ts +30 -0
  247. package/src/subscriptions/neuralwatt-scraper.ts +108 -0
  248. package/src/subscriptions/opencode-go-scraper.ts +301 -0
  249. package/src/subscriptions/types.ts +145 -0
  250. package/src/subscriptions/usage-service.test.ts +202 -0
  251. package/src/subscriptions/usage-service.ts +651 -0
  252. package/src/tools/ast-grep/cli.ts +257 -0
  253. package/src/tools/ast-grep/constants.ts +214 -0
  254. package/src/tools/ast-grep/downloader.ts +131 -0
  255. package/src/tools/ast-grep/index.ts +24 -0
  256. package/src/tools/ast-grep/tools.ts +117 -0
  257. package/src/tools/ast-grep/types.ts +51 -0
  258. package/src/tools/ast-grep/utils.ts +126 -0
  259. package/src/tools/delegate-handoff.test.ts +18 -0
  260. package/src/tools/delegate.ts +508 -0
  261. package/src/tools/index.ts +8 -0
  262. package/src/tools/preset-manager.test.ts +795 -0
  263. package/src/tools/preset-manager.ts +332 -0
  264. package/src/tools/smartfetch/binary.ts +58 -0
  265. package/src/tools/smartfetch/cache.test.ts +34 -0
  266. package/src/tools/smartfetch/cache.ts +112 -0
  267. package/src/tools/smartfetch/constants.ts +29 -0
  268. package/src/tools/smartfetch/index.ts +8 -0
  269. package/src/tools/smartfetch/network.test.ts +178 -0
  270. package/src/tools/smartfetch/network.ts +614 -0
  271. package/src/tools/smartfetch/secondary-model.test.ts +85 -0
  272. package/src/tools/smartfetch/secondary-model.ts +276 -0
  273. package/src/tools/smartfetch/tool.test.ts +60 -0
  274. package/src/tools/smartfetch/tool.ts +832 -0
  275. package/src/tools/smartfetch/types.ts +135 -0
  276. package/src/tools/smartfetch/utils.test.ts +24 -0
  277. package/src/tools/smartfetch/utils.ts +456 -0
  278. package/src/tui-state.test.ts +867 -0
  279. package/src/tui-state.ts +1255 -0
  280. package/src/tui.test.ts +336 -0
  281. package/src/tui.ts +1539 -0
  282. package/src/utils/agent-variant.test.ts +244 -0
  283. package/src/utils/agent-variant.ts +187 -0
  284. package/src/utils/compat.ts +91 -0
  285. package/src/utils/env.ts +12 -0
  286. package/src/utils/index.ts +9 -0
  287. package/src/utils/internal-initiator.ts +28 -0
  288. package/src/utils/logger.test.ts +220 -0
  289. package/src/utils/logger.ts +136 -0
  290. package/src/utils/polling.test.ts +191 -0
  291. package/src/utils/polling.ts +67 -0
  292. package/src/utils/session-manager.test.ts +173 -0
  293. package/src/utils/session-manager.ts +356 -0
  294. package/src/utils/session.test.ts +110 -0
  295. package/src/utils/session.ts +389 -0
  296. package/src/utils/subagent-depth.test.ts +170 -0
  297. package/src/utils/subagent-depth.ts +75 -0
  298. package/src/utils/system-collapse.test.ts +86 -0
  299. package/src/utils/system-collapse.ts +24 -0
  300. package/src/utils/task.test.ts +24 -0
  301. package/src/utils/task.ts +20 -0
  302. package/src/utils/zip-extractor.ts +102 -0
@@ -0,0 +1,1535 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { chmod, mkdir, stat, symlink } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { platform } from 'node:os';
5
+
6
+ function maybeMode(mode: number): number | undefined {
7
+ return platform() === 'win32' ? undefined : mode;
8
+ }
9
+
10
+ async function expectMode(p: string, mode: number): Promise<void> {
11
+ if (platform() === 'win32') return;
12
+ expect((await stat(p)).mode & 0o777).toBe(mode);
13
+ }
14
+
15
+ import { parsePatch } from './codec';
16
+ import {
17
+ isApplyPatchBlockedError,
18
+ isApplyPatchValidationError,
19
+ isApplyPatchVerificationError,
20
+ } from './errors';
21
+ import {
22
+ applyPreparedChanges,
23
+ preparePatchChanges,
24
+ rewritePatch,
25
+ rewritePatchText,
26
+ } from './operations';
27
+ import {
28
+ applyPatch,
29
+ createTempDir,
30
+ DEFAULT_OPTIONS,
31
+ readText,
32
+ writeFixture,
33
+ } from './test-helpers';
34
+
35
+ describe('apply-patch/operations', () => {
36
+ test('preparePatchChanges and applyPreparedChanges apply an exact match', async () => {
37
+ const root = await createTempDir();
38
+ await writeFixture(root, 'sample.txt', 'alpha\nbeta\ngamma\n');
39
+ await chmod(path.join(root, 'sample.txt'), 0o750);
40
+
41
+ await applyPatch(
42
+ root,
43
+ `*** Begin Patch
44
+ *** Update File: sample.txt
45
+ @@
46
+ alpha
47
+ -beta
48
+ +BETA
49
+ gamma
50
+ *** End Patch`,
51
+ );
52
+
53
+ expect(await readText(root, 'sample.txt')).toBe('alpha\nBETA\ngamma\n');
54
+ await expectMode(path.join(root, 'sample.txt'), 0o750);
55
+ });
56
+
57
+ test('rewritePatchText leaves a healthy patch intact', async () => {
58
+ const root = await createTempDir();
59
+ const patchText = `*** Begin Patch
60
+ *** Update File: sample.txt
61
+ @@ exact-top
62
+ -exact-old
63
+ +exact-new
64
+ exact-bottom
65
+ *** End Patch`;
66
+ await writeFixture(
67
+ root,
68
+ 'sample.txt',
69
+ 'line-01\nexact-top\nexact-old\nexact-bottom\nline-05\n',
70
+ );
71
+
72
+ expect(await rewritePatchText(root, patchText, DEFAULT_OPTIONS)).toBe(
73
+ patchText,
74
+ );
75
+ expect(await rewritePatch(root, patchText, DEFAULT_OPTIONS)).toMatchObject({
76
+ patchText,
77
+ changed: false,
78
+ });
79
+ });
80
+
81
+ test('rewritePatchText unwraps an exact patch wrapped in a heredoc', async () => {
82
+ const root = await createTempDir();
83
+ const cleanPatchText = `*** Begin Patch
84
+ *** Update File: sample.txt
85
+ @@ exact-top
86
+ -exact-old
87
+ +exact-new
88
+ exact-bottom
89
+ *** End Patch`;
90
+ const patchText = `cat <<'PATCH'
91
+ ${cleanPatchText}
92
+ PATCH`;
93
+ await writeFixture(
94
+ root,
95
+ 'sample.txt',
96
+ 'line-01\nexact-top\nexact-old\nexact-bottom\nline-05\n',
97
+ );
98
+
99
+ expect(await rewritePatchText(root, patchText, DEFAULT_OPTIONS)).toBe(
100
+ cleanPatchText,
101
+ );
102
+ expect(await rewritePatch(root, patchText, DEFAULT_OPTIONS)).toMatchObject({
103
+ patchText: cleanPatchText,
104
+ changed: true,
105
+ });
106
+ });
107
+
108
+ test('rewritePatchText normalizes exact CRLF + heredoc input and the patch still works', async () => {
109
+ const root = await createTempDir();
110
+ const cleanPatchText = `*** Begin Patch
111
+ *** Update File: sample.txt
112
+ @@ exact-top
113
+ -exact-old
114
+ +exact-new
115
+ exact-bottom
116
+ *** End Patch`;
117
+ const patchText = [
118
+ "cat <<'PATCH'",
119
+ '*** Begin Patch',
120
+ '*** Update File: sample.txt',
121
+ '@@ exact-top',
122
+ '-exact-old',
123
+ '+exact-new',
124
+ ' exact-bottom',
125
+ '*** End Patch',
126
+ 'PATCH',
127
+ ].join('\r\n');
128
+ await writeFixture(
129
+ root,
130
+ 'sample.txt',
131
+ 'line-01\nexact-top\nexact-old\nexact-bottom\nline-05\n',
132
+ );
133
+
134
+ const rewritten = await rewritePatchText(root, patchText, DEFAULT_OPTIONS);
135
+
136
+ expect(rewritten).toBe(cleanPatchText);
137
+ await applyPatch(root, rewritten);
138
+ expect(await readText(root, 'sample.txt')).toBe(
139
+ 'line-01\nexact-top\nexact-new\nexact-bottom\nline-05\n',
140
+ );
141
+ });
142
+
143
+ test('rewritePatchText rewrites a stale patch and preserves new_lines byte-for-byte', async () => {
144
+ const root = await createTempDir();
145
+ await writeFixture(
146
+ root,
147
+ 'sample.txt',
148
+ 'top\nprefix\nstale-value\nsuffix\nbottom\n',
149
+ );
150
+ const patchText = `*** Begin Patch
151
+ *** Update File: sample.txt
152
+ @@ top
153
+ prefix
154
+ -old-value
155
+ + \tverbatim "" Ω
156
+ suffix
157
+ *** End Patch`;
158
+
159
+ const rewritten = parsePatch(
160
+ await rewritePatchText(root, patchText, DEFAULT_OPTIONS),
161
+ ).hunks[0];
162
+
163
+ expect(rewritten.type).toBe('update');
164
+ expect(
165
+ rewritten.type === 'update' && rewritten.chunks[0]?.old_lines,
166
+ ).toEqual(['prefix', 'stale-value', 'suffix']);
167
+ expect(
168
+ rewritten.type === 'update' && rewritten.chunks[0]?.new_lines,
169
+ ).toEqual(['prefix', ' \tverbatim "" Ω ', 'suffix']);
170
+ });
171
+
172
+ test('rewritePatchText removes EOF when a rescue moves the chunk away from the real end', async () => {
173
+ const root = await createTempDir();
174
+ await writeFixture(
175
+ root,
176
+ 'sample.txt',
177
+ 'top\nprefix\nstale\nsuffix\nbottom\n',
178
+ );
179
+ const patchText = `*** Begin Patch
180
+ *** Update File: sample.txt
181
+ @@ top
182
+ prefix
183
+ -old
184
+ +new
185
+ suffix
186
+ *** End of File
187
+ *** End Patch`;
188
+
189
+ const rewrittenText = await rewritePatchText(
190
+ root,
191
+ patchText,
192
+ DEFAULT_OPTIONS,
193
+ );
194
+ const rewritten = parsePatch(rewrittenText).hunks[0];
195
+
196
+ expect(rewrittenText.includes('*** End of File')).toBeFalse();
197
+ expect(rewritten.type).toBe('update');
198
+ expect(
199
+ rewritten.type === 'update'
200
+ ? rewritten.chunks[0]?.is_end_of_file
201
+ : undefined,
202
+ ).toBeUndefined();
203
+ });
204
+
205
+ test('rewritePatchText keeps EOF when the resolved chunk still ends at the real end', async () => {
206
+ const root = await createTempDir();
207
+ await writeFixture(root, 'sample.txt', 'alpha\nstale\nomega');
208
+ const patchText = `*** Begin Patch
209
+ *** Update File: sample.txt
210
+ @@
211
+ -alpha
212
+ -old
213
+ -omega
214
+ +alpha
215
+ +new
216
+ +omega
217
+ *** End of File
218
+ *** End Patch`;
219
+
220
+ const rewrittenText = await rewritePatchText(
221
+ root,
222
+ patchText,
223
+ DEFAULT_OPTIONS,
224
+ );
225
+ const rewritten = parsePatch(rewrittenText).hunks[0];
226
+
227
+ expect(rewrittenText.includes('*** End of File')).toBeTrue();
228
+ expect(rewritten.type).toBe('update');
229
+ expect(
230
+ rewritten.type === 'update'
231
+ ? rewritten.chunks[0]?.is_end_of_file
232
+ : undefined,
233
+ ).toBeTrue();
234
+ });
235
+
236
+ test('rewritePatchText canonicalizes a unicode-only stale patch', async () => {
237
+ const root = await createTempDir();
238
+ await writeFixture(root, 'sample.txt', 'const title = “Hola”;\n');
239
+ const patchText = `*** Begin Patch
240
+ *** Update File: sample.txt
241
+ @@
242
+ -const title = "Hola";
243
+ +const title = "Hola mundo";
244
+ *** End Patch`;
245
+
246
+ const rewritten = parsePatch(
247
+ await rewritePatchText(root, patchText, DEFAULT_OPTIONS),
248
+ ).hunks[0];
249
+
250
+ expect(rewritten.type).toBe('update');
251
+ expect(
252
+ rewritten.type === 'update' ? rewritten.chunks[0]?.old_lines : undefined,
253
+ ).toEqual(['const title = “Hola”;']);
254
+ expect(
255
+ rewritten.type === 'update' ? rewritten.chunks[0]?.new_lines : undefined,
256
+ ).toEqual(['const title = "Hola mundo";']);
257
+ });
258
+
259
+ test('rewritePatchText canonicalizes a trim-end stale patch', async () => {
260
+ const root = await createTempDir();
261
+ await writeFixture(root, 'sample.txt', 'alpha \n');
262
+ const patchText = `*** Begin Patch
263
+ *** Update File: sample.txt
264
+ @@
265
+ -alpha
266
+ +omega
267
+ *** End Patch`;
268
+
269
+ const rewritten = parsePatch(
270
+ await rewritePatchText(root, patchText, DEFAULT_OPTIONS),
271
+ ).hunks[0];
272
+
273
+ expect(rewritten.type).toBe('update');
274
+ expect(
275
+ rewritten.type === 'update' ? rewritten.chunks[0]?.old_lines : undefined,
276
+ ).toEqual(['alpha ']);
277
+ expect(
278
+ rewritten.type === 'update' ? rewritten.chunks[0]?.new_lines : undefined,
279
+ ).toEqual(['omega']);
280
+ });
281
+
282
+ test('rewritePatchText no longer rescues a trim-only stale patch', async () => {
283
+ const root = await createTempDir();
284
+ await writeFixture(root, 'sample.txt', ' alpha \n');
285
+ const patchText = `*** Begin Patch
286
+ *** Update File: sample.txt
287
+ @@
288
+ -alpha
289
+ +omega
290
+ *** End Patch`;
291
+
292
+ await expect(
293
+ rewritePatchText(root, patchText, DEFAULT_OPTIONS),
294
+ ).rejects.toThrow(
295
+ 'apply_patch verification failed: Failed to find expected lines',
296
+ );
297
+ });
298
+
299
+ test('rewritePatchText no longer canonicalizes a dangerous indented case', async () => {
300
+ const root = await createTempDir();
301
+ await writeFixture(
302
+ root,
303
+ 'sample.yml',
304
+ 'root:\n child:\n enabled: false\nnext: true\n',
305
+ );
306
+ const patchText = `*** Begin Patch
307
+ *** Update File: sample.yml
308
+ @@
309
+ -enabled: false
310
+ +enabled: true
311
+ *** End Patch`;
312
+
313
+ await expect(
314
+ rewritePatchText(root, patchText, DEFAULT_OPTIONS),
315
+ ).rejects.toThrow(
316
+ 'apply_patch verification failed: Failed to find expected lines',
317
+ );
318
+ });
319
+
320
+ test('rewritePatchText rejects malformed @@ instead of silently sanitizing it', async () => {
321
+ const root = await createTempDir();
322
+ await writeFixture(root, 'sample.txt', 'alpha\nbeta\n');
323
+
324
+ await expect(
325
+ rewritePatchText(
326
+ root,
327
+ `*** Begin Patch
328
+ *** Update File: sample.txt
329
+ @@
330
+ alpha
331
+ garbage
332
+ -beta
333
+ +BETA
334
+ *** End Patch`,
335
+ DEFAULT_OPTIONS,
336
+ ),
337
+ ).rejects.toThrow(
338
+ 'apply_patch validation failed: Invalid patch format: unexpected line in patch chunk: garbage',
339
+ );
340
+ });
341
+
342
+ test('preparePatchChanges rejects a malformed Add File', async () => {
343
+ const root = await createTempDir();
344
+
345
+ await expect(
346
+ preparePatchChanges(
347
+ root,
348
+ `*** Begin Patch
349
+ *** Add File: added.txt
350
+ +fresh
351
+ garbage
352
+ *** End Patch`,
353
+ DEFAULT_OPTIONS,
354
+ ),
355
+ ).rejects.toThrow(
356
+ 'apply_patch validation failed: Invalid patch format: unexpected line in Add File body: garbage',
357
+ );
358
+ });
359
+
360
+ test('preparePatchChanges normalizes an Update File with an absolute path inside root', async () => {
361
+ const root = await createTempDir();
362
+ const absolutePath = path.join(root, 'sample.txt');
363
+ await writeFixture(root, 'sample.txt', 'alpha\nbeta\n');
364
+
365
+ const rewritten = await rewritePatch(
366
+ root,
367
+ `*** Begin Patch
368
+ *** Update File: ${absolutePath}
369
+ @@
370
+ -alpha
371
+ +omega
372
+ *** End Patch`,
373
+ DEFAULT_OPTIONS,
374
+ );
375
+
376
+ expect(rewritten.changed).toBeTrue();
377
+ const [rewrittenHunk] = parsePatch(rewritten.patchText).hunks;
378
+ expect(rewrittenHunk.type).toBe('update');
379
+ expect(rewrittenHunk.path).toBe('sample.txt');
380
+
381
+ await expect(
382
+ preparePatchChanges(
383
+ root,
384
+ `*** Begin Patch
385
+ *** Update File: ${absolutePath}
386
+ @@
387
+ -alpha
388
+ +omega
389
+ *** End Patch`,
390
+ DEFAULT_OPTIONS,
391
+ ),
392
+ ).resolves.toEqual([
393
+ {
394
+ type: 'update',
395
+ file: absolutePath,
396
+ move: undefined,
397
+ text: 'omega\nbeta\n',
398
+ },
399
+ ]);
400
+ });
401
+
402
+ test('preparePatchChanges normalizes an Add File with an absolute path inside root', async () => {
403
+ const root = await createTempDir();
404
+ const absolutePath = path.join(root, 'added.txt');
405
+
406
+ const rewritten = await rewritePatch(
407
+ root,
408
+ `*** Begin Patch
409
+ *** Add File: ${absolutePath}
410
+ +fresh
411
+ *** End Patch`,
412
+ DEFAULT_OPTIONS,
413
+ );
414
+
415
+ expect(rewritten.changed).toBeTrue();
416
+ expect(parsePatch(rewritten.patchText).hunks[0]).toMatchObject({
417
+ type: 'add',
418
+ path: 'added.txt',
419
+ contents: 'fresh',
420
+ });
421
+
422
+ await expect(
423
+ preparePatchChanges(
424
+ root,
425
+ `*** Begin Patch
426
+ *** Add File: ${absolutePath}
427
+ +fresh
428
+ *** End Patch`,
429
+ DEFAULT_OPTIONS,
430
+ ),
431
+ ).resolves.toEqual([
432
+ {
433
+ type: 'add',
434
+ file: absolutePath,
435
+ text: 'fresh\n',
436
+ },
437
+ ]);
438
+ });
439
+
440
+ test('preparePatchChanges normalizes a Move to with an absolute path inside root', async () => {
441
+ const root = await createTempDir();
442
+ const absoluteMovePath = path.join(root, 'nested/after.txt');
443
+
444
+ await writeFixture(root, 'before.txt', 'alpha\nbeta\n');
445
+
446
+ const rewritten = await rewritePatch(
447
+ root,
448
+ `*** Begin Patch
449
+ *** Update File: before.txt
450
+ *** Move to: ${absoluteMovePath}
451
+ @@
452
+ alpha
453
+ -beta
454
+ +BETA
455
+ *** End Patch`,
456
+ DEFAULT_OPTIONS,
457
+ );
458
+
459
+ expect(rewritten.changed).toBeTrue();
460
+ expect(parsePatch(rewritten.patchText).hunks[0]).toMatchObject({
461
+ type: 'update',
462
+ path: 'before.txt',
463
+ move_path: 'nested/after.txt',
464
+ });
465
+
466
+ await expect(
467
+ preparePatchChanges(
468
+ root,
469
+ `*** Begin Patch
470
+ *** Update File: before.txt
471
+ *** Move to: ${absoluteMovePath}
472
+ @@
473
+ alpha
474
+ -beta
475
+ +BETA
476
+ *** End Patch`,
477
+ DEFAULT_OPTIONS,
478
+ ),
479
+ ).resolves.toEqual([
480
+ {
481
+ type: 'update',
482
+ file: path.join(root, 'before.txt'),
483
+ move: absoluteMovePath,
484
+ text: 'alpha\nBETA\n',
485
+ },
486
+ ]);
487
+ });
488
+
489
+ test('preparePatchChanges blocks an absolute path outside root/worktree', async () => {
490
+ const root = await createTempDir();
491
+ const outsidePath = path.join(path.dirname(root), 'outside.txt');
492
+
493
+ const error = await preparePatchChanges(
494
+ root,
495
+ `*** Begin Patch
496
+ *** Add File: ${outsidePath}
497
+ +fresh
498
+ *** End Patch`,
499
+ DEFAULT_OPTIONS,
500
+ ).catch((caughtError) => caughtError);
501
+
502
+ expect(isApplyPatchBlockedError(error)).toBeTrue();
503
+ expect(error).toBeInstanceOf(Error);
504
+ expect((error as Error).message).toBe(
505
+ `apply_patch blocked: patch contains path outside workspace root: ${outsidePath}`,
506
+ );
507
+ });
508
+
509
+ test('preparePatchChanges allows an absolute path inside worktree even when it is outside root', async () => {
510
+ const worktree = await createTempDir();
511
+ const root = path.join(worktree, 'subdir');
512
+ await mkdir(root, { recursive: true });
513
+ const siblingPath = path.join(worktree, 'shared.txt');
514
+
515
+ const rewritten = await rewritePatch(
516
+ root,
517
+ `*** Begin Patch
518
+ *** Add File: ${siblingPath}
519
+ +fresh
520
+ *** End Patch`,
521
+ DEFAULT_OPTIONS,
522
+ worktree,
523
+ );
524
+
525
+ expect(rewritten.changed).toBeTrue();
526
+ expect(parsePatch(rewritten.patchText).hunks[0]).toMatchObject({
527
+ type: 'add',
528
+ path: '../shared.txt',
529
+ contents: 'fresh',
530
+ });
531
+
532
+ await expect(
533
+ preparePatchChanges(
534
+ root,
535
+ `*** Begin Patch
536
+ *** Add File: ${siblingPath}
537
+ +fresh
538
+ *** End Patch`,
539
+ DEFAULT_OPTIONS,
540
+ worktree,
541
+ ),
542
+ ).resolves.toEqual([
543
+ {
544
+ type: 'add',
545
+ file: siblingPath,
546
+ text: 'fresh\n',
547
+ },
548
+ ]);
549
+ });
550
+
551
+ test('preparePatchChanges does not redirect an absolute root target to its basename', async () => {
552
+ const root = await createTempDir();
553
+
554
+ await expect(
555
+ preparePatchChanges(
556
+ root,
557
+ `*** Begin Patch
558
+ *** Add File: ${root}
559
+ +fresh
560
+ *** End Patch`,
561
+ DEFAULT_OPTIONS,
562
+ ),
563
+ ).rejects.toThrow(
564
+ `apply_patch verification failed: Add File target already exists: ${root}`,
565
+ );
566
+ });
567
+
568
+ test('preparePatchChanges rejects Add File on an existing path', async () => {
569
+ const root = await createTempDir();
570
+ await writeFixture(root, 'added.txt', 'legacy\n');
571
+
572
+ await expect(
573
+ preparePatchChanges(
574
+ root,
575
+ `*** Begin Patch
576
+ *** Add File: added.txt
577
+ +fresh
578
+ *** End Patch`,
579
+ DEFAULT_OPTIONS,
580
+ ),
581
+ ).rejects.toThrow(
582
+ `apply_patch verification failed: Add File target already exists: ${path.join(root, 'added.txt')}`,
583
+ );
584
+ });
585
+
586
+ test('preparePatchChanges rejects Move to on a different existing destination', async () => {
587
+ const root = await createTempDir();
588
+ await writeFixture(root, 'before.txt', 'alpha\nbeta\n');
589
+ await writeFixture(root, 'nested/after.txt', 'legacy\n');
590
+
591
+ await expect(
592
+ preparePatchChanges(
593
+ root,
594
+ `*** Begin Patch
595
+ *** Update File: before.txt
596
+ *** Move to: nested/after.txt
597
+ @@
598
+ alpha
599
+ -beta
600
+ +BETA
601
+ *** End Patch`,
602
+ DEFAULT_OPTIONS,
603
+ ),
604
+ ).rejects.toThrow(
605
+ `apply_patch verification failed: Move destination already exists: ${path.join(root, 'nested/after.txt')}`,
606
+ );
607
+ });
608
+
609
+ test('rewritePatchText rejects a missing Delete File like preparePatchChanges', async () => {
610
+ const root = await createTempDir();
611
+ const patchText = `*** Begin Patch
612
+ *** Delete File: missing.txt
613
+ *** End Patch`;
614
+ const expectedMessage = `apply_patch verification failed: Failed to read file to delete: ${path.join(root, 'missing.txt')}`;
615
+
616
+ await expect(
617
+ rewritePatchText(root, patchText, DEFAULT_OPTIONS),
618
+ ).rejects.toThrow(expectedMessage);
619
+ await expect(
620
+ preparePatchChanges(root, patchText, DEFAULT_OPTIONS),
621
+ ).rejects.toThrow(expectedMessage);
622
+ });
623
+
624
+ test('rewritePatchText rejects duplicate Delete File on the same path', async () => {
625
+ const root = await createTempDir();
626
+ await writeFixture(root, 'obsolete.txt', 'legacy\n');
627
+
628
+ await expect(
629
+ rewritePatchText(
630
+ root,
631
+ `*** Begin Patch
632
+ *** Delete File: obsolete.txt
633
+ *** Delete File: obsolete.txt
634
+ *** End Patch`,
635
+ DEFAULT_OPTIONS,
636
+ ),
637
+ ).rejects.toThrow(
638
+ `apply_patch verification failed: Failed to read file to delete: ${path.join(root, 'obsolete.txt')}`,
639
+ );
640
+ });
641
+
642
+ test('rewritePatchText rejects Delete File on the source after a previous move', async () => {
643
+ const root = await createTempDir();
644
+ await writeFixture(root, 'before.txt', 'alpha\nbeta\n');
645
+
646
+ await expect(
647
+ rewritePatchText(
648
+ root,
649
+ `*** Begin Patch
650
+ *** Update File: before.txt
651
+ *** Move to: nested/after.txt
652
+ @@
653
+ alpha
654
+ -beta
655
+ +BETA
656
+ *** Delete File: before.txt
657
+ *** End Patch`,
658
+ DEFAULT_OPTIONS,
659
+ ),
660
+ ).rejects.toThrow(
661
+ `apply_patch verification failed: Failed to read file to delete: ${path.join(root, 'before.txt')}`,
662
+ );
663
+ });
664
+
665
+ test('rewritePatchText keeps a valid Delete File and apply still works', async () => {
666
+ const root = await createTempDir();
667
+ await writeFixture(root, 'obsolete.txt', 'legacy\n');
668
+ const patchText = `*** Begin Patch
669
+ *** Delete File: obsolete.txt
670
+ *** End Patch`;
671
+
672
+ expect(await rewritePatchText(root, patchText, DEFAULT_OPTIONS)).toBe(
673
+ patchText,
674
+ );
675
+
676
+ await applyPatch(root, patchText);
677
+ await expect(readText(root, 'obsolete.txt')).rejects.toThrow();
678
+ });
679
+
680
+ test('applyPreparedChanges rejects direct add on an existing path', async () => {
681
+ const root = await createTempDir();
682
+ const target = path.join(root, 'added.txt');
683
+ await writeFixture(root, 'added.txt', 'legacy\n');
684
+
685
+ await expect(
686
+ applyPreparedChanges([
687
+ {
688
+ type: 'add',
689
+ file: target,
690
+ text: 'fresh\n',
691
+ },
692
+ ]),
693
+ ).rejects.toThrow(
694
+ `apply_patch verification failed: Prepared add target already exists: ${target}`,
695
+ );
696
+
697
+ expect(await readText(root, 'added.txt')).toBe('legacy\n');
698
+ });
699
+
700
+ test('applyPreparedChanges rejects direct move on an existing destination', async () => {
701
+ const root = await createTempDir();
702
+ const source = path.join(root, 'before.txt');
703
+ const target = path.join(root, 'nested/after.txt');
704
+ await writeFixture(root, 'before.txt', 'alpha\nbeta\n');
705
+ await writeFixture(root, 'nested/after.txt', 'legacy\n');
706
+
707
+ await expect(
708
+ applyPreparedChanges([
709
+ {
710
+ type: 'update',
711
+ file: source,
712
+ move: target,
713
+ text: 'alpha\nBETA\n',
714
+ },
715
+ ]),
716
+ ).rejects.toThrow(
717
+ `apply_patch verification failed: Prepared move destination already exists: ${target}`,
718
+ );
719
+
720
+ expect(await readText(root, 'before.txt')).toBe('alpha\nbeta\n');
721
+ expect(await readText(root, 'nested/after.txt')).toBe('legacy\n');
722
+ });
723
+
724
+ test('applyPreparedChanges rejects legacy arrays with relative paths', async () => {
725
+ const error = await applyPreparedChanges([
726
+ {
727
+ type: 'add',
728
+ file: 'relative.txt' as unknown as string,
729
+ text: 'fresh\n',
730
+ },
731
+ ]).catch((caughtError) => caughtError);
732
+
733
+ expect(isApplyPatchValidationError(error)).toBeTrue();
734
+ expect(error).toBeInstanceOf(Error);
735
+ expect((error as Error).message).toBe(
736
+ 'apply_patch validation failed: Prepared changes require absolute normalized file paths at index 0: relative.txt',
737
+ );
738
+ });
739
+
740
+ test('rewritePatchText and preparePatchChanges share the validation/verification taxonomy', async () => {
741
+ const root = await createTempDir();
742
+ await writeFixture(root, 'sample.txt', 'alpha\nbeta\n');
743
+
744
+ const verificationError = await rewritePatchText(
745
+ root,
746
+ `*** Begin Patch
747
+ *** Update File: sample.txt
748
+ @@
749
+ -missing
750
+ +omega
751
+ *** End Patch`,
752
+ DEFAULT_OPTIONS,
753
+ ).catch((error) => error);
754
+
755
+ const validationError = await preparePatchChanges(
756
+ root,
757
+ `*** Begin Patch
758
+ *** Add File: added.txt
759
+ +fresh
760
+ garbage
761
+ *** End Patch`,
762
+ DEFAULT_OPTIONS,
763
+ ).catch((error) => error);
764
+
765
+ expect(isApplyPatchVerificationError(verificationError)).toBeTrue();
766
+ expect(isApplyPatchValidationError(validationError)).toBeTrue();
767
+ });
768
+
769
+ test('rewritePatchText canonicalizes EOF insertion with a tolerant anchor', async () => {
770
+ const root = await createTempDir();
771
+ await writeFixture(root, 'sample.txt', 'top\n“anchor”\n');
772
+
773
+ const rewritten = parsePatch(
774
+ await rewritePatchText(
775
+ root,
776
+ `*** Begin Patch
777
+ *** Update File: sample.txt
778
+ @@ "anchor"
779
+ +middle
780
+ *** End Patch`,
781
+ DEFAULT_OPTIONS,
782
+ ),
783
+ ).hunks[0];
784
+
785
+ expect(rewritten.type).toBe('update');
786
+ expect(
787
+ rewritten.type === 'update'
788
+ ? rewritten.chunks[0]?.change_context
789
+ : undefined,
790
+ ).toBe('“anchor”');
791
+ expect(
792
+ rewritten.type === 'update' ? rewritten.chunks[0]?.new_lines : undefined,
793
+ ).toEqual(['middle']);
794
+ });
795
+
796
+ test('rewritePatch groups two exact Update File hunks on the same path', async () => {
797
+ const root = await createTempDir();
798
+ await writeFixture(root, 'sample.txt', 'alpha\nbeta\ngamma\ndelta\n');
799
+
800
+ const result = await rewritePatch(
801
+ root,
802
+ `*** Begin Patch
803
+ *** Update File: sample.txt
804
+ @@
805
+ alpha
806
+ -beta
807
+ +BETA
808
+ gamma
809
+ *** Update File: sample.txt
810
+ @@
811
+ gamma
812
+ -delta
813
+ +DELTA
814
+ *** End Patch`,
815
+ DEFAULT_OPTIONS,
816
+ );
817
+
818
+ const rewritten = parsePatch(result.patchText);
819
+ expect(result.changed).toBeTrue();
820
+ expect(rewritten.hunks).toHaveLength(1);
821
+ expect(rewritten.hunks[0]).toEqual({
822
+ type: 'update',
823
+ path: 'sample.txt',
824
+ move_path: undefined,
825
+ chunks: [
826
+ {
827
+ old_lines: ['beta'],
828
+ new_lines: ['BETA'],
829
+ change_context: 'alpha',
830
+ is_end_of_file: undefined,
831
+ },
832
+ {
833
+ old_lines: ['delta'],
834
+ new_lines: ['DELTA'],
835
+ change_context: 'gamma',
836
+ is_end_of_file: undefined,
837
+ },
838
+ ],
839
+ });
840
+ });
841
+
842
+ test('rewritePatch groups a second update that depends on the first', async () => {
843
+ const root = await createTempDir();
844
+ await writeFixture(root, 'sample.txt', 'alpha\nbeta\ngamma\n');
845
+
846
+ const rewrittenText = await rewritePatchText(
847
+ root,
848
+ `*** Begin Patch
849
+ *** Update File: sample.txt
850
+ @@
851
+ alpha
852
+ -beta
853
+ +BETA
854
+ gamma
855
+ *** Update File: sample.txt
856
+ @@
857
+ alpha
858
+ -BETA
859
+ +BETA!
860
+ gamma
861
+ *** End Patch`,
862
+ DEFAULT_OPTIONS,
863
+ );
864
+
865
+ expect(parsePatch(rewrittenText).hunks).toEqual([
866
+ {
867
+ type: 'update',
868
+ path: 'sample.txt',
869
+ move_path: undefined,
870
+ chunks: [
871
+ {
872
+ old_lines: ['beta'],
873
+ new_lines: ['BETA!'],
874
+ change_context: 'alpha',
875
+ is_end_of_file: undefined,
876
+ },
877
+ ],
878
+ },
879
+ ]);
880
+
881
+ const changes = await preparePatchChanges(
882
+ root,
883
+ rewrittenText,
884
+ DEFAULT_OPTIONS,
885
+ );
886
+ await applyPreparedChanges(changes);
887
+ expect(await readText(root, 'sample.txt')).toBe('alpha\nBETA!\ngamma\n');
888
+ });
889
+
890
+ test('rewritePatch collapses Add File + exact Update File into a self-contained add', async () => {
891
+ const root = await createTempDir();
892
+
893
+ const result = await rewritePatch(
894
+ root,
895
+ `*** Begin Patch
896
+ *** Add File: added.txt
897
+ +alpha
898
+ +beta
899
+ *** Update File: added.txt
900
+ @@
901
+ alpha
902
+ -beta
903
+ +BETA
904
+ *** End Patch`,
905
+ DEFAULT_OPTIONS,
906
+ );
907
+
908
+ expect(result.changed).toBeTrue();
909
+ expect(parsePatch(result.patchText).hunks).toEqual([
910
+ {
911
+ type: 'add',
912
+ path: 'added.txt',
913
+ contents: 'alpha\nBETA',
914
+ },
915
+ ]);
916
+ });
917
+
918
+ test('rewritePatch collapses Add File + Update File + Move to into a self-contained final add', async () => {
919
+ const root = await createTempDir();
920
+
921
+ const result = await rewritePatch(
922
+ root,
923
+ `*** Begin Patch
924
+ *** Add File: before.txt
925
+ +alpha
926
+ +beta
927
+ *** Update File: before.txt
928
+ *** Move to: nested/after.txt
929
+ @@
930
+ alpha
931
+ -beta
932
+ +BETA
933
+ *** End Patch`,
934
+ DEFAULT_OPTIONS,
935
+ );
936
+
937
+ expect(result.changed).toBeTrue();
938
+ expect(parsePatch(result.patchText).hunks).toEqual([
939
+ {
940
+ type: 'add',
941
+ path: 'nested/after.txt',
942
+ contents: 'alpha\nBETA',
943
+ },
944
+ ]);
945
+ });
946
+
947
+ test('rewritePatch collapses an exact move followed by an update on the destination', async () => {
948
+ const root = await createTempDir();
949
+ await writeFixture(root, 'before.txt', 'alpha\nbeta\ngamma\n');
950
+
951
+ const result = await rewritePatch(
952
+ root,
953
+ `*** Begin Patch
954
+ *** Update File: before.txt
955
+ *** Move to: nested/after.txt
956
+ @@
957
+ alpha
958
+ -beta
959
+ +BETA
960
+ gamma
961
+ *** Update File: nested/after.txt
962
+ @@
963
+ alpha
964
+ BETA
965
+ -gamma
966
+ +GAMMA
967
+ *** End Patch`,
968
+ DEFAULT_OPTIONS,
969
+ );
970
+
971
+ expect(result.changed).toBeTrue();
972
+ expect(parsePatch(result.patchText).hunks).toEqual([
973
+ {
974
+ type: 'update',
975
+ path: 'before.txt',
976
+ move_path: 'nested/after.txt',
977
+ chunks: [
978
+ {
979
+ old_lines: ['beta', 'gamma'],
980
+ new_lines: ['BETA', 'GAMMA'],
981
+ change_context: 'alpha',
982
+ is_end_of_file: true,
983
+ },
984
+ ],
985
+ },
986
+ ]);
987
+ });
988
+
989
+ test('rewritePatch minimizes whole-file collapse when the fallback remains verifiable', async () => {
990
+ const root = await createTempDir();
991
+ await writeFixture(root, 'before.txt', 'alpha\nbeta\ngamma\n');
992
+
993
+ const result = await rewritePatch(
994
+ root,
995
+ `*** Begin Patch
996
+ *** Update File: before.txt
997
+ *** Move to: nested/after.txt
998
+ @@
999
+ alpha
1000
+ beta
1001
+ gamma
1002
+ *** Update File: nested/after.txt
1003
+ @@
1004
+ alpha
1005
+ -beta
1006
+ +BETA
1007
+ gamma
1008
+ *** End Patch`,
1009
+ DEFAULT_OPTIONS,
1010
+ );
1011
+
1012
+ expect(result.changed).toBeTrue();
1013
+ expect(parsePatch(result.patchText).hunks).toEqual([
1014
+ {
1015
+ type: 'update',
1016
+ path: 'before.txt',
1017
+ move_path: 'nested/after.txt',
1018
+ chunks: [
1019
+ {
1020
+ old_lines: ['beta'],
1021
+ new_lines: ['BETA'],
1022
+ change_context: 'alpha',
1023
+ is_end_of_file: undefined,
1024
+ },
1025
+ ],
1026
+ },
1027
+ ]);
1028
+
1029
+ await applyPatch(root, result.patchText);
1030
+ expect(await readText(root, 'nested/after.txt')).toBe(
1031
+ 'alpha\nBETA\ngamma\n',
1032
+ );
1033
+ });
1034
+
1035
+ test('rewritePatch keeps the correct change order when grouping same-file updates', async () => {
1036
+ const root = await createTempDir();
1037
+ await writeFixture(root, 'sample.txt', 'one\ntwo\nthree\nfour\nfive\n');
1038
+
1039
+ const rewrittenText = await rewritePatchText(
1040
+ root,
1041
+ `*** Begin Patch
1042
+ *** Update File: sample.txt
1043
+ @@
1044
+ one
1045
+ -two
1046
+ +TWO
1047
+ three
1048
+ *** Update File: sample.txt
1049
+ @@
1050
+ three
1051
+ -four
1052
+ +FOUR
1053
+ five
1054
+ *** End Patch`,
1055
+ DEFAULT_OPTIONS,
1056
+ );
1057
+
1058
+ expect(parsePatch(rewrittenText).hunks[0]).toEqual({
1059
+ type: 'update',
1060
+ path: 'sample.txt',
1061
+ move_path: undefined,
1062
+ chunks: [
1063
+ {
1064
+ old_lines: ['two'],
1065
+ new_lines: ['TWO'],
1066
+ change_context: 'one',
1067
+ is_end_of_file: undefined,
1068
+ },
1069
+ {
1070
+ old_lines: ['four'],
1071
+ new_lines: ['FOUR'],
1072
+ change_context: 'three',
1073
+ is_end_of_file: undefined,
1074
+ },
1075
+ ],
1076
+ });
1077
+
1078
+ await applyPatch(root, rewrittenText);
1079
+ expect(await readText(root, 'sample.txt')).toBe(
1080
+ 'one\nTWO\nthree\nFOUR\nfive\n',
1081
+ );
1082
+ });
1083
+
1084
+ test('preparePatchChanges fails when rescue is ambiguous', async () => {
1085
+ const root = await createTempDir();
1086
+ await writeFixture(
1087
+ root,
1088
+ 'sample.txt',
1089
+ 'left\nstale-one\nright\nseparator\nleft\nstale-two\nright\n',
1090
+ );
1091
+
1092
+ await expect(
1093
+ preparePatchChanges(
1094
+ root,
1095
+ `*** Begin Patch
1096
+ *** Update File: sample.txt
1097
+ @@
1098
+ left
1099
+ -old
1100
+ +new
1101
+ right
1102
+ *** End Patch`,
1103
+ DEFAULT_OPTIONS,
1104
+ ),
1105
+ ).rejects.toThrow('apply_patch verification failed:');
1106
+ });
1107
+
1108
+ test('applyPreparedChanges reverts previous changes when a later apply fails', async () => {
1109
+ const root = await createTempDir();
1110
+ await writeFixture(root, 'first.txt', 'one\n');
1111
+ await writeFixture(root, 'blocker', 'not-a-dir\n');
1112
+ await chmod(path.join(root, 'first.txt'), 0o755);
1113
+
1114
+ await expect(
1115
+ applyPreparedChanges([
1116
+ {
1117
+ type: 'update',
1118
+ file: path.join(root, 'first.txt'),
1119
+ text: 'ONE\n',
1120
+ },
1121
+ {
1122
+ type: 'add',
1123
+ file: path.join(root, 'blocker', 'second.txt'),
1124
+ text: 'two\n',
1125
+ },
1126
+ ]),
1127
+ ).rejects.toThrow(
1128
+ 'apply_patch internal error: Failed to apply prepared changes',
1129
+ );
1130
+
1131
+ expect(await readText(root, 'first.txt')).toBe('one\n');
1132
+ await expectMode(path.join(root, 'first.txt'), 0o755);
1133
+ expect(await readText(root, 'blocker')).toBe('not-a-dir\n');
1134
+ await expect(readText(root, 'blocker/second.txt')).rejects.toThrow();
1135
+ });
1136
+
1137
+ test('applyPreparedChanges supports update with move_path and preserves the source mode', async () => {
1138
+ const root = await createTempDir();
1139
+ await writeFixture(root, 'before.txt', 'alpha\nbeta\ngamma\n');
1140
+ await chmod(path.join(root, 'before.txt'), 0o755);
1141
+
1142
+ const changes = await preparePatchChanges(
1143
+ root,
1144
+ `*** Begin Patch
1145
+ *** Update File: before.txt
1146
+ *** Move to: nested/after.txt
1147
+ @@
1148
+ alpha
1149
+ -beta
1150
+ +BETA
1151
+ gamma
1152
+ *** End Patch`,
1153
+ DEFAULT_OPTIONS,
1154
+ );
1155
+ await applyPreparedChanges(changes);
1156
+
1157
+ expect(await readText(root, 'nested/after.txt')).toBe(
1158
+ 'alpha\nBETA\ngamma\n',
1159
+ );
1160
+ await expectMode(path.join(root, 'nested/after.txt'), 0o755);
1161
+ await expect(readText(root, 'before.txt')).rejects.toThrow();
1162
+ expect(changes[0]).toMatchObject({
1163
+ type: 'update',
1164
+ file: path.join(root, 'before.txt'),
1165
+ move: path.join(root, 'nested/after.txt'),
1166
+ });
1167
+ });
1168
+
1169
+ test('applyPreparedChanges rejects direct update on a missing source', async () => {
1170
+ const root = await createTempDir();
1171
+ const target = path.join(root, 'missing.txt');
1172
+
1173
+ await expect(
1174
+ applyPreparedChanges([
1175
+ {
1176
+ type: 'update',
1177
+ file: target,
1178
+ text: 'fresh\n',
1179
+ },
1180
+ ]),
1181
+ ).rejects.toThrow(
1182
+ `apply_patch verification failed: Prepared update source does not exist: ${target}`,
1183
+ );
1184
+ });
1185
+
1186
+ test('applyPreparedChanges rejects direct delete on a missing source', async () => {
1187
+ const root = await createTempDir();
1188
+ const target = path.join(root, 'missing.txt');
1189
+
1190
+ await expect(
1191
+ applyPreparedChanges([
1192
+ {
1193
+ type: 'delete',
1194
+ file: target,
1195
+ },
1196
+ ]),
1197
+ ).rejects.toThrow(
1198
+ `apply_patch verification failed: Prepared delete source does not exist: ${target}`,
1199
+ );
1200
+ });
1201
+
1202
+ test('applyPreparedChanges rejects direct move with a missing source', async () => {
1203
+ const root = await createTempDir();
1204
+ const source = path.join(root, 'missing.txt');
1205
+ const target = path.join(root, 'nested/after.txt');
1206
+
1207
+ await expect(
1208
+ applyPreparedChanges([
1209
+ {
1210
+ type: 'update',
1211
+ file: source,
1212
+ move: target,
1213
+ text: 'fresh\n',
1214
+ },
1215
+ ]),
1216
+ ).rejects.toThrow(
1217
+ `apply_patch verification failed: Prepared move source does not exist: ${source}`,
1218
+ );
1219
+ });
1220
+
1221
+ test('applyPreparedChanges rejects an invalid transition after a previous delete', async () => {
1222
+ const root = await createTempDir();
1223
+ const target = path.join(root, 'sample.txt');
1224
+ await writeFixture(root, 'sample.txt', 'alpha\n');
1225
+
1226
+ await expect(
1227
+ applyPreparedChanges([
1228
+ {
1229
+ type: 'delete',
1230
+ file: target,
1231
+ },
1232
+ {
1233
+ type: 'update',
1234
+ file: target,
1235
+ text: 'omega\n',
1236
+ },
1237
+ ]),
1238
+ ).rejects.toThrow(
1239
+ `apply_patch verification failed: Prepared update source does not exist: ${target}`,
1240
+ );
1241
+
1242
+ expect(await readText(root, 'sample.txt')).toBe('alpha\n');
1243
+ });
1244
+
1245
+ test('applyPatch supports move + update when the block is stale', async () => {
1246
+ const root = await createTempDir();
1247
+ await writeFixture(
1248
+ root,
1249
+ 'before.txt',
1250
+ 'top\nprefix\nstale-value\nsuffix\nbottom\n',
1251
+ );
1252
+
1253
+ await applyPatch(
1254
+ root,
1255
+ `*** Begin Patch
1256
+ *** Update File: before.txt
1257
+ *** Move to: nested/after.txt
1258
+ @@ top
1259
+ prefix
1260
+ -old-value
1261
+ +new-value
1262
+ suffix
1263
+ *** End Patch`,
1264
+ );
1265
+
1266
+ expect(await readText(root, 'nested/after.txt')).toBe(
1267
+ 'top\nprefix\nnew-value\nsuffix\nbottom\n',
1268
+ );
1269
+ await expect(readText(root, 'before.txt')).rejects.toThrow();
1270
+ });
1271
+
1272
+ test('preparePatchChanges and applyPreparedChanges preserve CRLF with stale rescue + exact chunk', async () => {
1273
+ const root = await createTempDir();
1274
+ await writeFixture(
1275
+ root,
1276
+ 'sample.txt',
1277
+ 'top\r\nprefix\r\nstale-value\r\nsuffix\r\nkeep\r\ntail-old\r\n',
1278
+ );
1279
+
1280
+ const changes = await preparePatchChanges(
1281
+ root,
1282
+ `*** Begin Patch
1283
+ *** Update File: sample.txt
1284
+ @@ top
1285
+ prefix
1286
+ -old-value
1287
+ +new-value
1288
+ suffix
1289
+ @@ suffix
1290
+ keep
1291
+ -tail-old
1292
+ +tail-new
1293
+ *** End Patch`,
1294
+ DEFAULT_OPTIONS,
1295
+ );
1296
+ await applyPreparedChanges(changes);
1297
+
1298
+ expect(await readText(root, 'sample.txt')).toBe(
1299
+ 'top\r\nprefix\r\nnew-value\r\nsuffix\r\nkeep\r\ntail-new\r\n',
1300
+ );
1301
+ });
1302
+
1303
+ test('preparePatchChanges and applyPreparedChanges support pure insertion at EOF', async () => {
1304
+ const root = await createTempDir();
1305
+ await writeFixture(root, 'sample.txt', 'top\nanchor\n');
1306
+
1307
+ const changes = await preparePatchChanges(
1308
+ root,
1309
+ `*** Begin Patch
1310
+ *** Update File: sample.txt
1311
+ @@ anchor
1312
+ +middle
1313
+ *** End Patch`,
1314
+ DEFAULT_OPTIONS,
1315
+ );
1316
+ await applyPreparedChanges(changes);
1317
+
1318
+ expect(await readText(root, 'sample.txt')).toBe('top\nanchor\nmiddle\n');
1319
+ });
1320
+
1321
+ test('preparePatchChanges and applyPreparedChanges accumulate two Update File hunks on the same path', async () => {
1322
+ const root = await createTempDir();
1323
+ await writeFixture(root, 'sample.txt', 'alpha\nbeta\ngamma\n');
1324
+
1325
+ const changes = await preparePatchChanges(
1326
+ root,
1327
+ `*** Begin Patch
1328
+ *** Update File: sample.txt
1329
+ @@
1330
+ alpha
1331
+ -beta
1332
+ +BETA
1333
+ gamma
1334
+ *** Update File: sample.txt
1335
+ @@
1336
+ alpha
1337
+ BETA
1338
+ -gamma
1339
+ +GAMMA
1340
+ *** End Patch`,
1341
+ DEFAULT_OPTIONS,
1342
+ );
1343
+
1344
+ expect(changes).toHaveLength(2);
1345
+ expect(changes[0]).toMatchObject({
1346
+ type: 'update',
1347
+ file: path.join(root, 'sample.txt'),
1348
+ text: 'alpha\nBETA\ngamma\n',
1349
+ });
1350
+ expect(changes[1]).toMatchObject({
1351
+ type: 'update',
1352
+ file: path.join(root, 'sample.txt'),
1353
+ text: 'alpha\nBETA\nGAMMA\n',
1354
+ });
1355
+
1356
+ await applyPreparedChanges(changes);
1357
+
1358
+ expect(await readText(root, 'sample.txt')).toBe('alpha\nBETA\nGAMMA\n');
1359
+ });
1360
+
1361
+ test('preparePatchChanges and applyPreparedChanges preserve a file without a final newline', async () => {
1362
+ const root = await createTempDir();
1363
+ await writeFixture(root, 'sample.txt', 'alpha\nbeta');
1364
+
1365
+ const changes = await preparePatchChanges(
1366
+ root,
1367
+ `*** Begin Patch
1368
+ *** Update File: sample.txt
1369
+ @@
1370
+ alpha
1371
+ -beta
1372
+ +omega
1373
+ *** End Patch`,
1374
+ DEFAULT_OPTIONS,
1375
+ );
1376
+
1377
+ await applyPreparedChanges(changes);
1378
+
1379
+ expect(await readText(root, 'sample.txt')).toBe('alpha\nomega');
1380
+ });
1381
+
1382
+ test('applyPatch applies add + update in the same patch', async () => {
1383
+ const root = await createTempDir();
1384
+ await writeFixture(root, 'sample.txt', 'alpha\nbeta\n');
1385
+
1386
+ await applyPatch(
1387
+ root,
1388
+ `*** Begin Patch
1389
+ *** Add File: added.txt
1390
+ +fresh
1391
+ *** Update File: sample.txt
1392
+ @@
1393
+ alpha
1394
+ -beta
1395
+ +BETA
1396
+ *** End Patch`,
1397
+ );
1398
+
1399
+ expect(await readText(root, 'added.txt')).toBe('fresh\n');
1400
+ expect(await readText(root, 'sample.txt')).toBe('alpha\nBETA\n');
1401
+ });
1402
+
1403
+ test('applyPatch applies update + delete in the same patch', async () => {
1404
+ const root = await createTempDir();
1405
+ await writeFixture(root, 'sample.txt', 'alpha\nbeta\n');
1406
+ await writeFixture(root, 'obsolete.txt', 'legacy\n');
1407
+
1408
+ await applyPatch(
1409
+ root,
1410
+ `*** Begin Patch
1411
+ *** Update File: sample.txt
1412
+ @@
1413
+ alpha
1414
+ -beta
1415
+ +BETA
1416
+ *** Delete File: obsolete.txt
1417
+ *** End Patch`,
1418
+ );
1419
+
1420
+ expect(await readText(root, 'sample.txt')).toBe('alpha\nBETA\n');
1421
+ await expect(readText(root, 'obsolete.txt')).rejects.toThrow();
1422
+ });
1423
+
1424
+ test('applyPatch applies move + add in the same patch', async () => {
1425
+ const root = await createTempDir();
1426
+ await writeFixture(root, 'before.txt', 'alpha\nbeta\n');
1427
+
1428
+ await applyPatch(
1429
+ root,
1430
+ `*** Begin Patch
1431
+ *** Update File: before.txt
1432
+ *** Move to: nested/after.txt
1433
+ @@
1434
+ alpha
1435
+ -beta
1436
+ +BETA
1437
+ *** Add File: before.txt
1438
+ +replacement
1439
+ *** End Patch`,
1440
+ );
1441
+
1442
+ expect(await readText(root, 'nested/after.txt')).toBe('alpha\nBETA\n');
1443
+ expect(await readText(root, 'before.txt')).toBe('replacement\n');
1444
+ });
1445
+
1446
+ const testSymlink = platform() === 'win32' ? test.skip : test;
1447
+ testSymlink('rewritePatchText blocks a patch when the path escapes through a symlink with a missing ancestor', async () => {
1448
+ const root = await createTempDir();
1449
+ const outside = await createTempDir();
1450
+ await writeFixture(root, 'before.txt', 'alpha\nbeta\n');
1451
+ await symlink(outside, path.join(root, 'linked-outside'));
1452
+
1453
+ const patchText = `*** Begin Patch
1454
+ *** Update File: before.txt
1455
+ *** Move to: linked-outside/missing/child.txt
1456
+ @@
1457
+ alpha
1458
+ -beta
1459
+ +BETA
1460
+ *** End Patch`;
1461
+
1462
+ await expect(
1463
+ rewritePatchText(root, patchText, DEFAULT_OPTIONS, root),
1464
+ ).rejects.toThrow(
1465
+ 'apply_patch blocked: patch contains path outside workspace root:',
1466
+ );
1467
+ });
1468
+
1469
+ test('rewritePatchText blocks the whole patch if any add/delete escapes root even when an update is rewritable', async () => {
1470
+ const root = await createTempDir();
1471
+ const outsideDir = await createTempDir();
1472
+ await writeFixture(root, 'sample.txt', 'prefix\nstale-value\nsuffix\n');
1473
+ await writeFixture(outsideDir, 'outside.txt', 'legacy\n');
1474
+
1475
+ const patchText = `*** Begin Patch
1476
+ *** Add File: ../outside-added.txt
1477
+ +fresh
1478
+ *** Update File: sample.txt
1479
+ @@
1480
+ prefix
1481
+ -old-value
1482
+ +new-value
1483
+ suffix
1484
+ *** Delete File: ../${path.basename(outsideDir)}/outside.txt
1485
+ *** End Patch`;
1486
+
1487
+ await expect(
1488
+ rewritePatchText(root, patchText, DEFAULT_OPTIONS, root),
1489
+ ).rejects.toThrow(
1490
+ 'apply_patch blocked: patch contains path outside workspace root:',
1491
+ );
1492
+ });
1493
+
1494
+ test('preparePatchChanges keeps an escaping relative path as blocked', async () => {
1495
+ const root = await createTempDir();
1496
+
1497
+ const error = await preparePatchChanges(
1498
+ root,
1499
+ `*** Begin Patch
1500
+ *** Add File: ../outside-added.txt
1501
+ +fresh
1502
+ *** End Patch`,
1503
+ DEFAULT_OPTIONS,
1504
+ root,
1505
+ ).catch((caughtError) => caughtError);
1506
+
1507
+ expect(isApplyPatchBlockedError(error)).toBeTrue();
1508
+ expect(isApplyPatchValidationError(error)).toBeFalse();
1509
+ expect(error).toBeInstanceOf(Error);
1510
+ expect((error as Error).message).toContain(
1511
+ 'apply_patch blocked: patch contains path outside workspace root:',
1512
+ );
1513
+ });
1514
+
1515
+ testSymlink('preparePatchChanges rejects a path that escapes through a symlink with a missing ancestor', async () => {
1516
+ const root = await createTempDir();
1517
+ const outside = await createTempDir();
1518
+ await mkdir(path.join(outside, 'real-target'), { recursive: true });
1519
+ await symlink(outside, path.join(root, 'linked-outside'));
1520
+
1521
+ await expect(
1522
+ preparePatchChanges(
1523
+ root,
1524
+ `*** Begin Patch
1525
+ *** Add File: linked-outside/missing/child.txt
1526
+ +fresh
1527
+ *** End Patch`,
1528
+ DEFAULT_OPTIONS,
1529
+ root,
1530
+ ),
1531
+ ).rejects.toThrow(
1532
+ 'apply_patch blocked: patch contains path outside workspace root:',
1533
+ );
1534
+ });
1535
+ });