@snoglobe/helios 0.3.6 → 0.4.1

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 (352) hide show
  1. package/dist/__tests__/db-helper.d.ts +8 -0
  2. package/dist/__tests__/db-helper.d.ts.map +1 -0
  3. package/dist/__tests__/db-helper.js +15 -0
  4. package/dist/__tests__/db-helper.js.map +1 -0
  5. package/dist/cli/replay.d.ts.map +1 -1
  6. package/dist/cli/replay.js +2 -3
  7. package/dist/cli/replay.js.map +1 -1
  8. package/dist/cli/sessions.d.ts.map +1 -1
  9. package/dist/cli/sessions.js +2 -1
  10. package/dist/cli/sessions.js.map +1 -1
  11. package/dist/config/project.test.d.ts +2 -0
  12. package/dist/config/project.test.d.ts.map +1 -0
  13. package/dist/config/project.test.js +219 -0
  14. package/dist/config/project.test.js.map +1 -0
  15. package/dist/core/orchestrator-integration.test.d.ts +2 -0
  16. package/dist/core/orchestrator-integration.test.d.ts.map +1 -0
  17. package/dist/core/orchestrator-integration.test.js +674 -0
  18. package/dist/core/orchestrator-integration.test.js.map +1 -0
  19. package/dist/core/orchestrator.d.ts +1 -0
  20. package/dist/core/orchestrator.d.ts.map +1 -1
  21. package/dist/core/orchestrator.js +129 -37
  22. package/dist/core/orchestrator.js.map +1 -1
  23. package/dist/core/orchestrator.test.d.ts +2 -0
  24. package/dist/core/orchestrator.test.d.ts.map +1 -0
  25. package/dist/core/orchestrator.test.js +982 -0
  26. package/dist/core/orchestrator.test.js.map +1 -0
  27. package/dist/core/state-machine.d.ts.map +1 -1
  28. package/dist/core/state-machine.js +3 -0
  29. package/dist/core/state-machine.js.map +1 -1
  30. package/dist/core/state-machine.test.d.ts +2 -0
  31. package/dist/core/state-machine.test.d.ts.map +1 -0
  32. package/dist/core/state-machine.test.js +95 -0
  33. package/dist/core/state-machine.test.js.map +1 -0
  34. package/dist/core/stickies.d.ts.map +1 -1
  35. package/dist/core/stickies.js +3 -0
  36. package/dist/core/stickies.js.map +1 -1
  37. package/dist/core/stickies.test.d.ts +2 -0
  38. package/dist/core/stickies.test.d.ts.map +1 -0
  39. package/dist/core/stickies.test.js +63 -0
  40. package/dist/core/stickies.test.js.map +1 -0
  41. package/dist/core/task-poller.d.ts +2 -1
  42. package/dist/core/task-poller.d.ts.map +1 -1
  43. package/dist/core/task-poller.js +25 -3
  44. package/dist/core/task-poller.js.map +1 -1
  45. package/dist/init.d.ts +2 -0
  46. package/dist/init.d.ts.map +1 -1
  47. package/dist/init.js +42 -7
  48. package/dist/init.js.map +1 -1
  49. package/dist/memory/context-gate.d.ts.map +1 -1
  50. package/dist/memory/context-gate.js +2 -1
  51. package/dist/memory/context-gate.js.map +1 -1
  52. package/dist/memory/context-gate.test.d.ts +2 -0
  53. package/dist/memory/context-gate.test.d.ts.map +1 -0
  54. package/dist/memory/context-gate.test.js +318 -0
  55. package/dist/memory/context-gate.test.js.map +1 -0
  56. package/dist/memory/experiment-tracker.test.d.ts +2 -0
  57. package/dist/memory/experiment-tracker.test.d.ts.map +1 -0
  58. package/dist/memory/experiment-tracker.test.js +325 -0
  59. package/dist/memory/experiment-tracker.test.js.map +1 -0
  60. package/dist/memory/global-memory.d.ts +0 -1
  61. package/dist/memory/global-memory.d.ts.map +1 -1
  62. package/dist/memory/global-memory.js +0 -14
  63. package/dist/memory/global-memory.js.map +1 -1
  64. package/dist/memory/memory-store.d.ts +2 -0
  65. package/dist/memory/memory-store.d.ts.map +1 -1
  66. package/dist/memory/memory-store.js +35 -28
  67. package/dist/memory/memory-store.js.map +1 -1
  68. package/dist/memory/memory-store.test.d.ts +2 -0
  69. package/dist/memory/memory-store.test.d.ts.map +1 -0
  70. package/dist/memory/memory-store.test.js +144 -0
  71. package/dist/memory/memory-store.test.js.map +1 -0
  72. package/dist/memory/token-estimator.test.d.ts +2 -0
  73. package/dist/memory/token-estimator.test.d.ts.map +1 -0
  74. package/dist/memory/token-estimator.test.js +42 -0
  75. package/dist/memory/token-estimator.test.js.map +1 -0
  76. package/dist/metrics/analyzer.test.d.ts +2 -0
  77. package/dist/metrics/analyzer.test.d.ts.map +1 -0
  78. package/dist/metrics/analyzer.test.js +65 -0
  79. package/dist/metrics/analyzer.test.js.map +1 -0
  80. package/dist/metrics/parser.test.d.ts +2 -0
  81. package/dist/metrics/parser.test.d.ts.map +1 -0
  82. package/dist/metrics/parser.test.js +106 -0
  83. package/dist/metrics/parser.test.js.map +1 -0
  84. package/dist/metrics/store.d.ts +2 -0
  85. package/dist/metrics/store.d.ts.map +1 -1
  86. package/dist/metrics/store.js +26 -50
  87. package/dist/metrics/store.js.map +1 -1
  88. package/dist/metrics/store.test.d.ts +2 -0
  89. package/dist/metrics/store.test.d.ts.map +1 -0
  90. package/dist/metrics/store.test.js +148 -0
  91. package/dist/metrics/store.test.js.map +1 -0
  92. package/dist/paths.js +5 -5
  93. package/dist/paths.js.map +1 -1
  94. package/dist/providers/auth/auth-manager.d.ts +1 -0
  95. package/dist/providers/auth/auth-manager.d.ts.map +1 -1
  96. package/dist/providers/auth/auth-manager.js +8 -1
  97. package/dist/providers/auth/auth-manager.js.map +1 -1
  98. package/dist/providers/auth/auth-manager.test.d.ts +2 -0
  99. package/dist/providers/auth/auth-manager.test.d.ts.map +1 -0
  100. package/dist/providers/auth/auth-manager.test.js +364 -0
  101. package/dist/providers/auth/auth-manager.test.js.map +1 -0
  102. package/dist/providers/auth/token-store.js +2 -2
  103. package/dist/providers/auth/token-store.js.map +1 -1
  104. package/dist/providers/claude/history.test.d.ts +2 -0
  105. package/dist/providers/claude/history.test.d.ts.map +1 -0
  106. package/dist/providers/claude/history.test.js +757 -0
  107. package/dist/providers/claude/history.test.js.map +1 -0
  108. package/dist/providers/claude/provider.d.ts +2 -1
  109. package/dist/providers/claude/provider.d.ts.map +1 -1
  110. package/dist/providers/claude/provider.js +87 -25
  111. package/dist/providers/claude/provider.js.map +1 -1
  112. package/dist/providers/claude/provider.test.d.ts +2 -0
  113. package/dist/providers/claude/provider.test.d.ts.map +1 -0
  114. package/dist/providers/claude/provider.test.js +1168 -0
  115. package/dist/providers/claude/provider.test.js.map +1 -0
  116. package/dist/providers/openai/history.test.d.ts +2 -0
  117. package/dist/providers/openai/history.test.d.ts.map +1 -0
  118. package/dist/providers/openai/history.test.js +657 -0
  119. package/dist/providers/openai/history.test.js.map +1 -0
  120. package/dist/providers/openai/provider.d.ts +2 -1
  121. package/dist/providers/openai/provider.d.ts.map +1 -1
  122. package/dist/providers/openai/provider.js +75 -22
  123. package/dist/providers/openai/provider.js.map +1 -1
  124. package/dist/providers/openai/provider.test.d.ts +2 -0
  125. package/dist/providers/openai/provider.test.d.ts.map +1 -0
  126. package/dist/providers/openai/provider.test.js +1093 -0
  127. package/dist/providers/openai/provider.test.js.map +1 -0
  128. package/dist/providers/retry.test.d.ts +2 -0
  129. package/dist/providers/retry.test.d.ts.map +1 -0
  130. package/dist/providers/retry.test.js +194 -0
  131. package/dist/providers/retry.test.js.map +1 -0
  132. package/dist/providers/sse.d.ts +1 -0
  133. package/dist/providers/sse.d.ts.map +1 -1
  134. package/dist/providers/sse.js +29 -20
  135. package/dist/providers/sse.js.map +1 -1
  136. package/dist/providers/sse.test.d.ts +2 -0
  137. package/dist/providers/sse.test.d.ts.map +1 -0
  138. package/dist/providers/sse.test.js +79 -0
  139. package/dist/providers/sse.test.js.map +1 -0
  140. package/dist/providers/types.d.ts +3 -0
  141. package/dist/providers/types.d.ts.map +1 -1
  142. package/dist/providers/types.js.map +1 -1
  143. package/dist/providers/types.test.d.ts +2 -0
  144. package/dist/providers/types.test.d.ts.map +1 -0
  145. package/dist/providers/types.test.js +112 -0
  146. package/dist/providers/types.test.js.map +1 -0
  147. package/dist/remote/connection-pool.d.ts.map +1 -1
  148. package/dist/remote/connection-pool.js +24 -3
  149. package/dist/remote/connection-pool.js.map +1 -1
  150. package/dist/remote/file-sync.d.ts.map +1 -1
  151. package/dist/remote/file-sync.js +4 -3
  152. package/dist/remote/file-sync.js.map +1 -1
  153. package/dist/scheduler/sleep-manager.d.ts.map +1 -1
  154. package/dist/scheduler/sleep-manager.js +9 -2
  155. package/dist/scheduler/sleep-manager.js.map +1 -1
  156. package/dist/scheduler/sleep-manager.test.d.ts +2 -0
  157. package/dist/scheduler/sleep-manager.test.d.ts.map +1 -0
  158. package/dist/scheduler/sleep-manager.test.js +491 -0
  159. package/dist/scheduler/sleep-manager.test.js.map +1 -0
  160. package/dist/scheduler/ssh-batcher.d.ts.map +1 -1
  161. package/dist/scheduler/ssh-batcher.js +6 -4
  162. package/dist/scheduler/ssh-batcher.js.map +1 -1
  163. package/dist/scheduler/ssh-batcher.test.d.ts +2 -0
  164. package/dist/scheduler/ssh-batcher.test.d.ts.map +1 -0
  165. package/dist/scheduler/ssh-batcher.test.js +76 -0
  166. package/dist/scheduler/ssh-batcher.test.js.map +1 -0
  167. package/dist/scheduler/state-store.d.ts.map +1 -1
  168. package/dist/scheduler/state-store.js +1 -2
  169. package/dist/scheduler/state-store.js.map +1 -1
  170. package/dist/scheduler/trigger-scheduler.d.ts.map +1 -1
  171. package/dist/scheduler/trigger-scheduler.js +59 -36
  172. package/dist/scheduler/trigger-scheduler.js.map +1 -1
  173. package/dist/scheduler/trigger-scheduler.test.d.ts +2 -0
  174. package/dist/scheduler/trigger-scheduler.test.d.ts.map +1 -0
  175. package/dist/scheduler/trigger-scheduler.test.js +483 -0
  176. package/dist/scheduler/trigger-scheduler.test.js.map +1 -0
  177. package/dist/scheduler/triggers/file.d.ts.map +1 -1
  178. package/dist/scheduler/triggers/file.js +12 -3
  179. package/dist/scheduler/triggers/file.js.map +1 -1
  180. package/dist/scheduler/triggers/file.test.d.ts +2 -0
  181. package/dist/scheduler/triggers/file.test.d.ts.map +1 -0
  182. package/dist/scheduler/triggers/file.test.js +294 -0
  183. package/dist/scheduler/triggers/file.test.js.map +1 -0
  184. package/dist/scheduler/triggers/metric.d.ts +3 -1
  185. package/dist/scheduler/triggers/metric.d.ts.map +1 -1
  186. package/dist/scheduler/triggers/metric.js +12 -8
  187. package/dist/scheduler/triggers/metric.js.map +1 -1
  188. package/dist/scheduler/triggers/metric.test.d.ts +2 -0
  189. package/dist/scheduler/triggers/metric.test.d.ts.map +1 -0
  190. package/dist/scheduler/triggers/metric.test.js +533 -0
  191. package/dist/scheduler/triggers/metric.test.js.map +1 -0
  192. package/dist/scheduler/triggers/process-exit.d.ts.map +1 -1
  193. package/dist/scheduler/triggers/process-exit.js +2 -1
  194. package/dist/scheduler/triggers/process-exit.js.map +1 -1
  195. package/dist/scheduler/triggers/process-exit.test.d.ts +2 -0
  196. package/dist/scheduler/triggers/process-exit.test.d.ts.map +1 -0
  197. package/dist/scheduler/triggers/process-exit.test.js +118 -0
  198. package/dist/scheduler/triggers/process-exit.test.js.map +1 -0
  199. package/dist/scheduler/triggers/resource.d.ts.map +1 -1
  200. package/dist/scheduler/triggers/resource.js +2 -10
  201. package/dist/scheduler/triggers/resource.js.map +1 -1
  202. package/dist/scheduler/triggers/resource.test.d.ts +2 -0
  203. package/dist/scheduler/triggers/resource.test.d.ts.map +1 -0
  204. package/dist/scheduler/triggers/resource.test.js +225 -0
  205. package/dist/scheduler/triggers/resource.test.js.map +1 -0
  206. package/dist/scheduler/triggers/timer.test.d.ts +2 -0
  207. package/dist/scheduler/triggers/timer.test.d.ts.map +1 -0
  208. package/dist/scheduler/triggers/timer.test.js +56 -0
  209. package/dist/scheduler/triggers/timer.test.js.map +1 -0
  210. package/dist/scheduler/triggers/types.d.ts +4 -2
  211. package/dist/scheduler/triggers/types.d.ts.map +1 -1
  212. package/dist/scheduler/triggers/types.js +0 -1
  213. package/dist/scheduler/triggers/types.js.map +1 -1
  214. package/dist/skills/executor.d.ts.map +1 -1
  215. package/dist/skills/executor.js +13 -15
  216. package/dist/skills/executor.js.map +1 -1
  217. package/dist/store/database.d.ts +5 -0
  218. package/dist/store/database.d.ts.map +1 -1
  219. package/dist/store/database.js +17 -1
  220. package/dist/store/database.js.map +1 -1
  221. package/dist/store/migrations.d.ts.map +1 -1
  222. package/dist/store/migrations.js +7 -0
  223. package/dist/store/migrations.js.map +1 -1
  224. package/dist/store/migrations.test.d.ts +2 -0
  225. package/dist/store/migrations.test.d.ts.map +1 -0
  226. package/dist/store/migrations.test.js +278 -0
  227. package/dist/store/migrations.test.js.map +1 -0
  228. package/dist/store/session-store-edge.test.d.ts +2 -0
  229. package/dist/store/session-store-edge.test.d.ts.map +1 -0
  230. package/dist/store/session-store-edge.test.js +522 -0
  231. package/dist/store/session-store-edge.test.js.map +1 -0
  232. package/dist/store/session-store.d.ts +28 -1
  233. package/dist/store/session-store.d.ts.map +1 -1
  234. package/dist/store/session-store.js +62 -26
  235. package/dist/store/session-store.js.map +1 -1
  236. package/dist/store/session-store.test.d.ts +2 -0
  237. package/dist/store/session-store.test.d.ts.map +1 -0
  238. package/dist/store/session-store.test.js +125 -0
  239. package/dist/store/session-store.test.js.map +1 -0
  240. package/dist/subagent/executor.d.ts +24 -0
  241. package/dist/subagent/executor.d.ts.map +1 -0
  242. package/dist/subagent/executor.js +140 -0
  243. package/dist/subagent/executor.js.map +1 -0
  244. package/dist/subagent/manager.d.ts +20 -0
  245. package/dist/subagent/manager.d.ts.map +1 -0
  246. package/dist/subagent/manager.js +100 -0
  247. package/dist/subagent/manager.js.map +1 -0
  248. package/dist/subagent/scoped-memory.d.ts +28 -0
  249. package/dist/subagent/scoped-memory.d.ts.map +1 -0
  250. package/dist/subagent/scoped-memory.js +122 -0
  251. package/dist/subagent/scoped-memory.js.map +1 -0
  252. package/dist/subagent/types.d.ts +27 -0
  253. package/dist/subagent/types.d.ts.map +1 -0
  254. package/dist/subagent/types.js +2 -0
  255. package/dist/subagent/types.js.map +1 -0
  256. package/dist/tools/file-ops.d.ts.map +1 -1
  257. package/dist/tools/file-ops.js +23 -13
  258. package/dist/tools/file-ops.js.map +1 -1
  259. package/dist/tools/file-ops.test.d.ts +2 -0
  260. package/dist/tools/file-ops.test.d.ts.map +1 -0
  261. package/dist/tools/file-ops.test.js +656 -0
  262. package/dist/tools/file-ops.test.js.map +1 -0
  263. package/dist/tools/memory-tools.test.d.ts +2 -0
  264. package/dist/tools/memory-tools.test.d.ts.map +1 -0
  265. package/dist/tools/memory-tools.test.js +273 -0
  266. package/dist/tools/memory-tools.test.js.map +1 -0
  267. package/dist/tools/sleep.d.ts.map +1 -1
  268. package/dist/tools/sleep.js +19 -7
  269. package/dist/tools/sleep.js.map +1 -1
  270. package/dist/tools/subagent.d.ts +10 -0
  271. package/dist/tools/subagent.d.ts.map +1 -0
  272. package/dist/tools/subagent.js +164 -0
  273. package/dist/tools/subagent.js.map +1 -0
  274. package/dist/tools/task-output.d.ts.map +1 -1
  275. package/dist/tools/task-output.js +13 -2
  276. package/dist/tools/task-output.js.map +1 -1
  277. package/dist/ui/components/input-bar.d.ts +1 -1
  278. package/dist/ui/components/input-bar.d.ts.map +1 -1
  279. package/dist/ui/components/input-bar.js +3 -3
  280. package/dist/ui/components/input-bar.js.map +1 -1
  281. package/dist/ui/components/input-bar.test.d.ts +2 -0
  282. package/dist/ui/components/input-bar.test.d.ts.map +1 -0
  283. package/dist/ui/components/input-bar.test.js +380 -0
  284. package/dist/ui/components/input-bar.test.js.map +1 -0
  285. package/dist/ui/components/key-hint-rule.d.ts +2 -1
  286. package/dist/ui/components/key-hint-rule.d.ts.map +1 -1
  287. package/dist/ui/components/key-hint-rule.js +3 -3
  288. package/dist/ui/components/key-hint-rule.js.map +1 -1
  289. package/dist/ui/components/status-bar.d.ts +1 -1
  290. package/dist/ui/components/status-bar.d.ts.map +1 -1
  291. package/dist/ui/components/status-bar.js +11 -10
  292. package/dist/ui/components/status-bar.js.map +1 -1
  293. package/dist/ui/components/status-bar.test.d.ts +2 -0
  294. package/dist/ui/components/status-bar.test.d.ts.map +1 -0
  295. package/dist/ui/components/status-bar.test.js +206 -0
  296. package/dist/ui/components/status-bar.test.js.map +1 -0
  297. package/dist/ui/format.d.ts +9 -0
  298. package/dist/ui/format.d.ts.map +1 -1
  299. package/dist/ui/format.js +21 -0
  300. package/dist/ui/format.js.map +1 -1
  301. package/dist/ui/format.test.d.ts +2 -0
  302. package/dist/ui/format.test.d.ts.map +1 -0
  303. package/dist/ui/format.test.js +122 -0
  304. package/dist/ui/format.test.js.map +1 -0
  305. package/dist/ui/layout.d.ts.map +1 -1
  306. package/dist/ui/layout.js +123 -16
  307. package/dist/ui/layout.js.map +1 -1
  308. package/dist/ui/markdown.test.d.ts +2 -0
  309. package/dist/ui/markdown.test.d.ts.map +1 -0
  310. package/dist/ui/markdown.test.js +133 -0
  311. package/dist/ui/markdown.test.js.map +1 -0
  312. package/dist/ui/mouse-filter.test.d.ts +2 -0
  313. package/dist/ui/mouse-filter.test.d.ts.map +1 -0
  314. package/dist/ui/mouse-filter.test.js +231 -0
  315. package/dist/ui/mouse-filter.test.js.map +1 -0
  316. package/dist/ui/overlays/metrics-overlay.test.d.ts +2 -0
  317. package/dist/ui/overlays/metrics-overlay.test.d.ts.map +1 -0
  318. package/dist/ui/overlays/metrics-overlay.test.js +248 -0
  319. package/dist/ui/overlays/metrics-overlay.test.js.map +1 -0
  320. package/dist/ui/overlays/task-overlay.test.d.ts +2 -0
  321. package/dist/ui/overlays/task-overlay.test.d.ts.map +1 -0
  322. package/dist/ui/overlays/task-overlay.test.js +238 -0
  323. package/dist/ui/overlays/task-overlay.test.js.map +1 -0
  324. package/dist/ui/panels/conversation.d.ts.map +1 -1
  325. package/dist/ui/panels/conversation.js +60 -5
  326. package/dist/ui/panels/conversation.js.map +1 -1
  327. package/dist/ui/panels/conversation.test.d.ts +2 -0
  328. package/dist/ui/panels/conversation.test.d.ts.map +1 -0
  329. package/dist/ui/panels/conversation.test.js +381 -0
  330. package/dist/ui/panels/conversation.test.js.map +1 -0
  331. package/dist/ui/panels/metrics-dashboard.test.d.ts +2 -0
  332. package/dist/ui/panels/metrics-dashboard.test.d.ts.map +1 -0
  333. package/dist/ui/panels/metrics-dashboard.test.js +191 -0
  334. package/dist/ui/panels/metrics-dashboard.test.js.map +1 -0
  335. package/dist/ui/panels/sleep-panel.test.d.ts +2 -0
  336. package/dist/ui/panels/sleep-panel.test.d.ts.map +1 -0
  337. package/dist/ui/panels/sleep-panel.test.js +376 -0
  338. package/dist/ui/panels/sleep-panel.test.js.map +1 -0
  339. package/dist/ui/panels/task-list.d.ts.map +1 -1
  340. package/dist/ui/panels/task-list.js +5 -6
  341. package/dist/ui/panels/task-list.js.map +1 -1
  342. package/dist/ui/panels/task-list.test.d.ts +2 -0
  343. package/dist/ui/panels/task-list.test.d.ts.map +1 -0
  344. package/dist/ui/panels/task-list.test.js +210 -0
  345. package/dist/ui/panels/task-list.test.js.map +1 -0
  346. package/dist/ui/theme.test.d.ts +2 -0
  347. package/dist/ui/theme.test.d.ts.map +1 -0
  348. package/dist/ui/theme.test.js +159 -0
  349. package/dist/ui/theme.test.js.map +1 -0
  350. package/dist/ui/types.d.ts +3 -0
  351. package/dist/ui/types.d.ts.map +1 -1
  352. package/package.json +6 -2
@@ -0,0 +1,656 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { createReadFileTool, createWriteFileTool, createPatchFileTool, } from "./file-ops.js";
3
+ function mockPool(handler) {
4
+ return {
5
+ exec: vi.fn(async (machineId, command, opts) => {
6
+ if (handler)
7
+ return handler(machineId, command, opts);
8
+ return { stdout: "", stderr: "", exitCode: 0 };
9
+ }),
10
+ };
11
+ }
12
+ function parse(json) {
13
+ return JSON.parse(json);
14
+ }
15
+ // ---------------------------------------------------------------------------
16
+ // read_file
17
+ // ---------------------------------------------------------------------------
18
+ describe("read_file", () => {
19
+ it("reads text file with offset and limit", async () => {
20
+ const pool = mockPool((_, cmd) => {
21
+ // Combined command: sed + delimiter + wc -l in one SSH call
22
+ if (cmd.includes("sed") && cmd.includes("HELIOS_LINECOUNT")) {
23
+ return { stdout: "line1\nline2\n---HELIOS_LINECOUNT---\n10\n", stderr: "", exitCode: 0 };
24
+ }
25
+ return { stdout: "", stderr: "", exitCode: 0 };
26
+ });
27
+ const tool = createReadFileTool(pool);
28
+ const result = parse(await tool.execute({ machine_id: "local", path: "/tmp/test.py", offset: 1, limit: 5 }));
29
+ expect(result.content).toBe("line1\nline2\n");
30
+ expect(result.lines.total).toBe(10);
31
+ });
32
+ it("defaults to offset 1, limit 200", async () => {
33
+ const pool = mockPool((_, cmd) => {
34
+ if (cmd.includes("sed")) {
35
+ // Verify the command uses sed -n '1,200p'
36
+ expect(cmd).toContain("1,200p");
37
+ return { stdout: "content\n---HELIOS_LINECOUNT---\n50\n", stderr: "", exitCode: 0 };
38
+ }
39
+ return { stdout: "", stderr: "", exitCode: 0 };
40
+ });
41
+ const tool = createReadFileTool(pool);
42
+ await tool.execute({ machine_id: "local", path: "/tmp/test.py" });
43
+ });
44
+ it("returns total line count", async () => {
45
+ const pool = mockPool((_, cmd) => {
46
+ if (cmd.includes("sed")) {
47
+ return { stdout: "data\n---HELIOS_LINECOUNT---\n42\n", stderr: "", exitCode: 0 };
48
+ }
49
+ return { stdout: "", stderr: "", exitCode: 0 };
50
+ });
51
+ const tool = createReadFileTool(pool);
52
+ const result = parse(await tool.execute({ machine_id: "local", path: "/tmp/test.py" }));
53
+ expect(result.lines.total).toBe(42);
54
+ });
55
+ it("reads image file as base64 multimodal", async () => {
56
+ const pool = mockPool((_, cmd) => {
57
+ if (cmd.includes("wc -c"))
58
+ return { stdout: "1024\n", stderr: "", exitCode: 0 };
59
+ if (cmd.includes("base64"))
60
+ return { stdout: "AQID\n", stderr: "", exitCode: 0 };
61
+ return { stdout: "", stderr: "", exitCode: 0 };
62
+ });
63
+ const tool = createReadFileTool(pool);
64
+ const result = parse(await tool.execute({ machine_id: "local", path: "/tmp/img.png" }));
65
+ expect(result.__multimodal).toBe(true);
66
+ expect(result.attachments).toHaveLength(1);
67
+ expect(result.attachments[0].mediaType).toBe("image/png");
68
+ expect(result.attachments[0].data).toBe("AQID");
69
+ });
70
+ it("reads PDF file as base64 multimodal", async () => {
71
+ const pool = mockPool((_, cmd) => {
72
+ if (cmd.includes("wc -c"))
73
+ return { stdout: "2048\n", stderr: "", exitCode: 0 };
74
+ if (cmd.includes("base64"))
75
+ return { stdout: "PDFDATA\n", stderr: "", exitCode: 0 };
76
+ return { stdout: "", stderr: "", exitCode: 0 };
77
+ });
78
+ const tool = createReadFileTool(pool);
79
+ const result = parse(await tool.execute({ machine_id: "local", path: "/tmp/doc.pdf" }));
80
+ expect(result.__multimodal).toBe(true);
81
+ expect(result.attachments[0].mediaType).toBe("application/pdf");
82
+ expect(result.text).toContain("PDF");
83
+ });
84
+ it("rejects oversized binary files (>10MB)", async () => {
85
+ const pool = mockPool((_, cmd) => {
86
+ if (cmd.includes("wc -c"))
87
+ return { stdout: "15000000\n", stderr: "", exitCode: 0 };
88
+ return { stdout: "", stderr: "", exitCode: 0 };
89
+ });
90
+ const tool = createReadFileTool(pool);
91
+ const result = parse(await tool.execute({ machine_id: "local", path: "/tmp/big.png" }));
92
+ expect(result.error).toBeDefined();
93
+ expect(result.error).toContain("too large");
94
+ });
95
+ it("handles file not found error", async () => {
96
+ const pool = mockPool((_, cmd) => {
97
+ if (cmd.includes("sed"))
98
+ return { stdout: "", stderr: "No such file or directory", exitCode: 1 };
99
+ return { stdout: "", stderr: "", exitCode: 0 };
100
+ });
101
+ const tool = createReadFileTool(pool);
102
+ const result = parse(await tool.execute({ machine_id: "local", path: "/tmp/nope.py" }));
103
+ expect(result.error).toBeDefined();
104
+ expect(result.error).toContain("No such file");
105
+ });
106
+ it("handles empty file", async () => {
107
+ const pool = mockPool((_, cmd) => {
108
+ if (cmd.includes("sed"))
109
+ return { stdout: "", stderr: "", exitCode: 0 };
110
+ if (cmd.includes("wc"))
111
+ return { stdout: "0\n", stderr: "", exitCode: 0 };
112
+ return { stdout: "", stderr: "", exitCode: 0 };
113
+ });
114
+ const tool = createReadFileTool(pool);
115
+ const result = parse(await tool.execute({ machine_id: "local", path: "/tmp/empty.py" }));
116
+ expect(result.content).toBe("");
117
+ expect(result.lines.total).toBe(0);
118
+ });
119
+ it("returns proper MIME types for .png", async () => {
120
+ const pool = mockPool((_, cmd) => {
121
+ if (cmd.includes("wc -c"))
122
+ return { stdout: "100\n", stderr: "", exitCode: 0 };
123
+ if (cmd.includes("base64"))
124
+ return { stdout: "AA==\n", stderr: "", exitCode: 0 };
125
+ return { stdout: "", stderr: "", exitCode: 0 };
126
+ });
127
+ const tool = createReadFileTool(pool);
128
+ const result = parse(await tool.execute({ machine_id: "local", path: "/tmp/test.png" }));
129
+ expect(result.attachments[0].mediaType).toBe("image/png");
130
+ });
131
+ it("returns proper MIME types for .jpg", async () => {
132
+ const pool = mockPool((_, cmd) => {
133
+ if (cmd.includes("wc -c"))
134
+ return { stdout: "100\n", stderr: "", exitCode: 0 };
135
+ if (cmd.includes("base64"))
136
+ return { stdout: "AA==\n", stderr: "", exitCode: 0 };
137
+ return { stdout: "", stderr: "", exitCode: 0 };
138
+ });
139
+ const tool = createReadFileTool(pool);
140
+ const result = parse(await tool.execute({ machine_id: "local", path: "/tmp/photo.jpg" }));
141
+ expect(result.attachments[0].mediaType).toBe("image/jpeg");
142
+ });
143
+ it("returns proper MIME types for .jpeg", async () => {
144
+ const pool = mockPool((_, cmd) => {
145
+ if (cmd.includes("wc -c"))
146
+ return { stdout: "100\n", stderr: "", exitCode: 0 };
147
+ if (cmd.includes("base64"))
148
+ return { stdout: "AA==\n", stderr: "", exitCode: 0 };
149
+ return { stdout: "", stderr: "", exitCode: 0 };
150
+ });
151
+ const tool = createReadFileTool(pool);
152
+ const result = parse(await tool.execute({ machine_id: "local", path: "/tmp/photo.jpeg" }));
153
+ expect(result.attachments[0].mediaType).toBe("image/jpeg");
154
+ });
155
+ it("returns proper MIME types for .gif", async () => {
156
+ const pool = mockPool((_, cmd) => {
157
+ if (cmd.includes("wc -c"))
158
+ return { stdout: "100\n", stderr: "", exitCode: 0 };
159
+ if (cmd.includes("base64"))
160
+ return { stdout: "AA==\n", stderr: "", exitCode: 0 };
161
+ return { stdout: "", stderr: "", exitCode: 0 };
162
+ });
163
+ const tool = createReadFileTool(pool);
164
+ const result = parse(await tool.execute({ machine_id: "local", path: "/tmp/anim.gif" }));
165
+ expect(result.attachments[0].mediaType).toBe("image/gif");
166
+ });
167
+ it("returns proper MIME types for .webp", async () => {
168
+ const pool = mockPool((_, cmd) => {
169
+ if (cmd.includes("wc -c"))
170
+ return { stdout: "100\n", stderr: "", exitCode: 0 };
171
+ if (cmd.includes("base64"))
172
+ return { stdout: "AA==\n", stderr: "", exitCode: 0 };
173
+ return { stdout: "", stderr: "", exitCode: 0 };
174
+ });
175
+ const tool = createReadFileTool(pool);
176
+ const result = parse(await tool.execute({ machine_id: "local", path: "/tmp/pic.webp" }));
177
+ expect(result.attachments[0].mediaType).toBe("image/webp");
178
+ });
179
+ it("validates safe offset (Math.max(1, ...))", async () => {
180
+ const pool = mockPool((_, cmd) => {
181
+ if (cmd.includes("sed")) {
182
+ // With offset=0, safeOffset should be 1
183
+ expect(cmd).toContain("'1,");
184
+ return { stdout: "data\n", stderr: "", exitCode: 0 };
185
+ }
186
+ if (cmd.includes("wc"))
187
+ return { stdout: "10\n", stderr: "", exitCode: 0 };
188
+ return { stdout: "", stderr: "", exitCode: 0 };
189
+ });
190
+ const tool = createReadFileTool(pool);
191
+ await tool.execute({ machine_id: "local", path: "/tmp/test.py", offset: 0, limit: 5 });
192
+ });
193
+ it("returns correct multimodal JSON structure", async () => {
194
+ const pool = mockPool((_, cmd) => {
195
+ if (cmd.includes("wc -c"))
196
+ return { stdout: "5000\n", stderr: "", exitCode: 0 };
197
+ if (cmd.includes("base64"))
198
+ return { stdout: "abc123\n", stderr: "", exitCode: 0 };
199
+ return { stdout: "", stderr: "", exitCode: 0 };
200
+ });
201
+ const tool = createReadFileTool(pool);
202
+ const result = parse(await tool.execute({ machine_id: "local", path: "/tmp/img.png" }));
203
+ expect(result).toHaveProperty("__multimodal", true);
204
+ expect(result).toHaveProperty("text");
205
+ expect(result).toHaveProperty("attachments");
206
+ expect(result.text).toContain("Image");
207
+ expect(result.text).toContain("img.png");
208
+ expect(result.text).toContain("KB");
209
+ });
210
+ it("handles empty binary file", async () => {
211
+ const pool = mockPool((_, cmd) => {
212
+ if (cmd.includes("wc -c"))
213
+ return { stdout: "0\n", stderr: "", exitCode: 0 };
214
+ return { stdout: "", stderr: "", exitCode: 0 };
215
+ });
216
+ const tool = createReadFileTool(pool);
217
+ const result = parse(await tool.execute({ machine_id: "local", path: "/tmp/empty.png" }));
218
+ expect(result.error).toBeDefined();
219
+ });
220
+ it("handles binary file not found", async () => {
221
+ const pool = mockPool((_, cmd) => {
222
+ if (cmd.includes("wc -c"))
223
+ return { stdout: "", stderr: "No such file", exitCode: 1 };
224
+ return { stdout: "", stderr: "", exitCode: 0 };
225
+ });
226
+ const tool = createReadFileTool(pool);
227
+ const result = parse(await tool.execute({ machine_id: "local", path: "/tmp/missing.png" }));
228
+ expect(result.error).toBeDefined();
229
+ });
230
+ });
231
+ // ---------------------------------------------------------------------------
232
+ // write_file
233
+ // ---------------------------------------------------------------------------
234
+ describe("write_file", () => {
235
+ it("writes content to file", async () => {
236
+ const pool = mockPool((_, cmd) => {
237
+ if (cmd.includes("wc"))
238
+ return { stdout: "5\n", stderr: "", exitCode: 0 };
239
+ return { stdout: "", stderr: "", exitCode: 0 };
240
+ });
241
+ const tool = createWriteFileTool(pool);
242
+ const result = parse(await tool.execute({
243
+ machine_id: "local",
244
+ path: "/tmp/out.py",
245
+ content: "print('hello')\n",
246
+ }));
247
+ expect(result.written).toBe("/tmp/out.py");
248
+ });
249
+ it("creates parent directory", async () => {
250
+ const commands = [];
251
+ const pool = mockPool((_, cmd) => {
252
+ commands.push(cmd);
253
+ if (cmd.includes("wc"))
254
+ return { stdout: "1\n", stderr: "", exitCode: 0 };
255
+ return { stdout: "", stderr: "", exitCode: 0 };
256
+ });
257
+ const tool = createWriteFileTool(pool);
258
+ await tool.execute({
259
+ machine_id: "local",
260
+ path: "/tmp/new/dir/file.py",
261
+ content: "x = 1\n",
262
+ });
263
+ expect(commands.some((c) => c.includes("mkdir -p"))).toBe(true);
264
+ });
265
+ it("uses heredoc for safe content", async () => {
266
+ const commands = [];
267
+ const pool = mockPool((_, cmd) => {
268
+ commands.push(cmd);
269
+ if (cmd.includes("wc"))
270
+ return { stdout: "3\n", stderr: "", exitCode: 0 };
271
+ return { stdout: "", stderr: "", exitCode: 0 };
272
+ });
273
+ const tool = createWriteFileTool(pool);
274
+ await tool.execute({
275
+ machine_id: "local",
276
+ path: "/tmp/out.py",
277
+ content: "line1\nline2\n",
278
+ });
279
+ expect(commands.some((c) => c.includes("_HELIOS_EOF_"))).toBe(true);
280
+ });
281
+ it("supports append mode", async () => {
282
+ const commands = [];
283
+ const pool = mockPool((_, cmd) => {
284
+ commands.push(cmd);
285
+ if (cmd.includes("wc"))
286
+ return { stdout: "10\n", stderr: "", exitCode: 0 };
287
+ return { stdout: "", stderr: "", exitCode: 0 };
288
+ });
289
+ const tool = createWriteFileTool(pool);
290
+ await tool.execute({
291
+ machine_id: "local",
292
+ path: "/tmp/log.txt",
293
+ content: "new line\n",
294
+ append: true,
295
+ });
296
+ expect(commands.some((c) => c.includes(">>"))).toBe(true);
297
+ });
298
+ it("returns line count", async () => {
299
+ const pool = mockPool((_, cmd) => {
300
+ if (cmd.includes("wc"))
301
+ return { stdout: "15\n", stderr: "", exitCode: 0 };
302
+ return { stdout: "", stderr: "", exitCode: 0 };
303
+ });
304
+ const tool = createWriteFileTool(pool);
305
+ const result = parse(await tool.execute({
306
+ machine_id: "local",
307
+ path: "/tmp/out.py",
308
+ content: "content\n",
309
+ }));
310
+ expect(result.lines).toBe(15);
311
+ });
312
+ it("handles write error", async () => {
313
+ const pool = mockPool((_, cmd) => {
314
+ if (cmd.includes("cat"))
315
+ return { stdout: "", stderr: "Permission denied", exitCode: 1 };
316
+ return { stdout: "", stderr: "", exitCode: 0 };
317
+ });
318
+ const tool = createWriteFileTool(pool);
319
+ const result = parse(await tool.execute({
320
+ machine_id: "local",
321
+ path: "/root/nope.py",
322
+ content: "x = 1\n",
323
+ }));
324
+ expect(result.error).toBeDefined();
325
+ expect(result.error).toContain("Permission denied");
326
+ });
327
+ it("strips trailing newline to avoid doubling", async () => {
328
+ const commands = [];
329
+ const pool = mockPool((_, cmd) => {
330
+ commands.push(cmd);
331
+ if (cmd.includes("wc"))
332
+ return { stdout: "3\n", stderr: "", exitCode: 0 };
333
+ return { stdout: "", stderr: "", exitCode: 0 };
334
+ });
335
+ const tool = createWriteFileTool(pool);
336
+ await tool.execute({
337
+ machine_id: "local",
338
+ path: "/tmp/out.py",
339
+ content: "line1\nline2\n",
340
+ });
341
+ // The heredoc body should strip the trailing newline
342
+ const catCmd = commands.find((c) => c.includes("_HELIOS_EOF_"));
343
+ expect(catCmd).toBeDefined();
344
+ // Body should be "line1\nline2" not "line1\nline2\n"
345
+ const bodyMatch = catCmd.match(/<<'_HELIOS_EOF_[^']*'\n([\s\S]*)\n_HELIOS_EOF_/);
346
+ expect(bodyMatch).not.toBeNull();
347
+ expect(bodyMatch[1]).toBe("line1\nline2");
348
+ });
349
+ it("uses overwrite mode by default", async () => {
350
+ const commands = [];
351
+ const pool = mockPool((_, cmd) => {
352
+ commands.push(cmd);
353
+ if (cmd.includes("wc"))
354
+ return { stdout: "1\n", stderr: "", exitCode: 0 };
355
+ return { stdout: "", stderr: "", exitCode: 0 };
356
+ });
357
+ const tool = createWriteFileTool(pool);
358
+ await tool.execute({
359
+ machine_id: "local",
360
+ path: "/tmp/out.py",
361
+ content: "x",
362
+ });
363
+ const catCmd = commands.find((c) => c.includes("_HELIOS_EOF_"));
364
+ // Should use ">" not ">>"
365
+ expect(catCmd).toContain("> ");
366
+ expect(catCmd).not.toContain(">>");
367
+ });
368
+ it("handles content without trailing newline", async () => {
369
+ const commands = [];
370
+ const pool = mockPool((_, cmd) => {
371
+ commands.push(cmd);
372
+ if (cmd.includes("wc"))
373
+ return { stdout: "1\n", stderr: "", exitCode: 0 };
374
+ return { stdout: "", stderr: "", exitCode: 0 };
375
+ });
376
+ const tool = createWriteFileTool(pool);
377
+ await tool.execute({
378
+ machine_id: "local",
379
+ path: "/tmp/out.py",
380
+ content: "no newline at end",
381
+ });
382
+ // Should not strip anything since there's no trailing newline
383
+ const catCmd = commands.find((c) => c.includes("_HELIOS_EOF_"));
384
+ expect(catCmd).toContain("no newline at end");
385
+ });
386
+ it("does not mkdir when path has no parent", async () => {
387
+ const commands = [];
388
+ const pool = mockPool((_, cmd) => {
389
+ commands.push(cmd);
390
+ if (cmd.includes("wc"))
391
+ return { stdout: "1\n", stderr: "", exitCode: 0 };
392
+ return { stdout: "", stderr: "", exitCode: 0 };
393
+ });
394
+ const tool = createWriteFileTool(pool);
395
+ await tool.execute({
396
+ machine_id: "local",
397
+ path: "/file.py",
398
+ content: "x",
399
+ });
400
+ // dir would be "" for "/file.py" -> no mkdir
401
+ // The mkdir command should not be called since dir is empty
402
+ expect(commands.filter((c) => c.includes("mkdir")).length).toBe(0);
403
+ });
404
+ it("passes machine_id to pool.exec", async () => {
405
+ const pool = mockPool((_, cmd) => {
406
+ if (cmd.includes("wc"))
407
+ return { stdout: "1\n", stderr: "", exitCode: 0 };
408
+ return { stdout: "", stderr: "", exitCode: 0 };
409
+ });
410
+ const tool = createWriteFileTool(pool);
411
+ await tool.execute({
412
+ machine_id: "gpu-1",
413
+ path: "/tmp/out.py",
414
+ content: "x",
415
+ });
416
+ expect(pool.exec).toHaveBeenCalledWith("gpu-1", expect.any(String));
417
+ });
418
+ });
419
+ // ---------------------------------------------------------------------------
420
+ // patch_file
421
+ // ---------------------------------------------------------------------------
422
+ describe("patch_file", () => {
423
+ it("replaces matching string", async () => {
424
+ let writtenContent = "";
425
+ const pool = mockPool((_, cmd) => {
426
+ if (cmd.includes("cat") && !cmd.includes("<<")) {
427
+ return { stdout: "hello world\n", stderr: "", exitCode: 0 };
428
+ }
429
+ if (cmd.includes("<<")) {
430
+ // Capture the written content from the heredoc
431
+ const match = cmd.match(/<<'[^']+'\n([\s\S]*)\n[^\n]+$/);
432
+ if (match)
433
+ writtenContent = match[1];
434
+ return { stdout: "", stderr: "", exitCode: 0 };
435
+ }
436
+ return { stdout: "", stderr: "", exitCode: 0 };
437
+ });
438
+ const tool = createPatchFileTool(pool);
439
+ const result = parse(await tool.execute({
440
+ machine_id: "local",
441
+ path: "/tmp/test.py",
442
+ old_string: "hello",
443
+ new_string: "goodbye",
444
+ }));
445
+ expect(result.patched).toBe("/tmp/test.py");
446
+ expect(writtenContent).toContain("goodbye");
447
+ expect(writtenContent).not.toContain("hello");
448
+ });
449
+ it("rejects when old_string not found", async () => {
450
+ const pool = mockPool((_, cmd) => {
451
+ if (cmd.includes("cat") && !cmd.includes("<<")) {
452
+ return { stdout: "totally different content\n", stderr: "", exitCode: 0 };
453
+ }
454
+ return { stdout: "", stderr: "", exitCode: 0 };
455
+ });
456
+ const tool = createPatchFileTool(pool);
457
+ const result = parse(await tool.execute({
458
+ machine_id: "local",
459
+ path: "/tmp/test.py",
460
+ old_string: "nonexistent string",
461
+ new_string: "replacement",
462
+ }));
463
+ expect(result.error).toBeDefined();
464
+ expect(result.error).toContain("not found");
465
+ });
466
+ it("rejects when old_string found multiple times", async () => {
467
+ const pool = mockPool((_, cmd) => {
468
+ if (cmd.includes("cat") && !cmd.includes("<<")) {
469
+ return { stdout: "hello hello hello\n", stderr: "", exitCode: 0 };
470
+ }
471
+ return { stdout: "", stderr: "", exitCode: 0 };
472
+ });
473
+ const tool = createPatchFileTool(pool);
474
+ const result = parse(await tool.execute({
475
+ machine_id: "local",
476
+ path: "/tmp/test.py",
477
+ old_string: "hello",
478
+ new_string: "goodbye",
479
+ }));
480
+ expect(result.error).toBeDefined();
481
+ expect(result.error).toContain("3 times");
482
+ expect(result.error).toContain("unique");
483
+ });
484
+ it("reads file first, then writes back", async () => {
485
+ const callOrder = [];
486
+ const pool = mockPool((_, cmd) => {
487
+ if (cmd.includes("cat") && !cmd.includes("<<")) {
488
+ callOrder.push("read");
489
+ return { stdout: "original content\n", stderr: "", exitCode: 0 };
490
+ }
491
+ if (cmd.includes("<<")) {
492
+ callOrder.push("write");
493
+ return { stdout: "", stderr: "", exitCode: 0 };
494
+ }
495
+ return { stdout: "", stderr: "", exitCode: 0 };
496
+ });
497
+ const tool = createPatchFileTool(pool);
498
+ await tool.execute({
499
+ machine_id: "local",
500
+ path: "/tmp/test.py",
501
+ old_string: "original",
502
+ new_string: "updated",
503
+ });
504
+ expect(callOrder).toEqual(["read", "write"]);
505
+ });
506
+ it("handles special characters in old_string", async () => {
507
+ const pool = mockPool((_, cmd) => {
508
+ if (cmd.includes("cat") && !cmd.includes("<<")) {
509
+ return { stdout: "x = {'key': \"value\"}\n", stderr: "", exitCode: 0 };
510
+ }
511
+ return { stdout: "", stderr: "", exitCode: 0 };
512
+ });
513
+ const tool = createPatchFileTool(pool);
514
+ const result = parse(await tool.execute({
515
+ machine_id: "local",
516
+ path: "/tmp/test.py",
517
+ old_string: "{'key': \"value\"}",
518
+ new_string: "{'key': \"new_value\"}",
519
+ }));
520
+ expect(result.patched).toBe("/tmp/test.py");
521
+ });
522
+ it("returns patched filename", async () => {
523
+ const pool = mockPool((_, cmd) => {
524
+ if (cmd.includes("cat") && !cmd.includes("<<")) {
525
+ return { stdout: "abc def\n", stderr: "", exitCode: 0 };
526
+ }
527
+ return { stdout: "", stderr: "", exitCode: 0 };
528
+ });
529
+ const tool = createPatchFileTool(pool);
530
+ const result = parse(await tool.execute({
531
+ machine_id: "local",
532
+ path: "/home/user/script.py",
533
+ old_string: "abc",
534
+ new_string: "xyz",
535
+ }));
536
+ expect(result.patched).toBe("/home/user/script.py");
537
+ });
538
+ it("handles read error", async () => {
539
+ const pool = mockPool((_, cmd) => {
540
+ if (cmd.includes("cat") && !cmd.includes("<<")) {
541
+ return { stdout: "", stderr: "Permission denied", exitCode: 1 };
542
+ }
543
+ return { stdout: "", stderr: "", exitCode: 0 };
544
+ });
545
+ const tool = createPatchFileTool(pool);
546
+ const result = parse(await tool.execute({
547
+ machine_id: "local",
548
+ path: "/tmp/test.py",
549
+ old_string: "x",
550
+ new_string: "y",
551
+ }));
552
+ expect(result.error).toBeDefined();
553
+ expect(result.error).toContain("Permission denied");
554
+ });
555
+ it("handles write error", async () => {
556
+ const pool = mockPool((_, cmd) => {
557
+ if (cmd.includes("cat") && !cmd.includes("<<")) {
558
+ return { stdout: "find me\n", stderr: "", exitCode: 0 };
559
+ }
560
+ if (cmd.includes("<<")) {
561
+ return { stdout: "", stderr: "Disk full", exitCode: 1 };
562
+ }
563
+ return { stdout: "", stderr: "", exitCode: 0 };
564
+ });
565
+ const tool = createPatchFileTool(pool);
566
+ const result = parse(await tool.execute({
567
+ machine_id: "local",
568
+ path: "/tmp/test.py",
569
+ old_string: "find me",
570
+ new_string: "replace me",
571
+ }));
572
+ expect(result.error).toBeDefined();
573
+ expect(result.error).toContain("Disk full");
574
+ });
575
+ it("handles multiline patches", async () => {
576
+ const pool = mockPool((_, cmd) => {
577
+ if (cmd.includes("cat") && !cmd.includes("<<")) {
578
+ return {
579
+ stdout: "def hello():\n return 'world'\n",
580
+ stderr: "",
581
+ exitCode: 0,
582
+ };
583
+ }
584
+ return { stdout: "", stderr: "", exitCode: 0 };
585
+ });
586
+ const tool = createPatchFileTool(pool);
587
+ const result = parse(await tool.execute({
588
+ machine_id: "local",
589
+ path: "/tmp/test.py",
590
+ old_string: "def hello():\n return 'world'",
591
+ new_string: "def hello():\n return 'universe'",
592
+ }));
593
+ expect(result.patched).toBe("/tmp/test.py");
594
+ });
595
+ it("uses heredoc for writing patched content", async () => {
596
+ const commands = [];
597
+ const pool = mockPool((_, cmd) => {
598
+ commands.push(cmd);
599
+ if (cmd.includes("cat") && !cmd.includes("<<")) {
600
+ return { stdout: "old text here\n", stderr: "", exitCode: 0 };
601
+ }
602
+ return { stdout: "", stderr: "", exitCode: 0 };
603
+ });
604
+ const tool = createPatchFileTool(pool);
605
+ await tool.execute({
606
+ machine_id: "local",
607
+ path: "/tmp/test.py",
608
+ old_string: "old text",
609
+ new_string: "new text",
610
+ });
611
+ expect(commands.some((c) => c.includes("_HELIOS_EOF_"))).toBe(true);
612
+ });
613
+ it("passes machine_id correctly", async () => {
614
+ const pool = mockPool((_, cmd) => {
615
+ if (cmd.includes("cat") && !cmd.includes("<<")) {
616
+ return { stdout: "content\n", stderr: "", exitCode: 0 };
617
+ }
618
+ return { stdout: "", stderr: "", exitCode: 0 };
619
+ });
620
+ const tool = createPatchFileTool(pool);
621
+ await tool.execute({
622
+ machine_id: "gpu-2",
623
+ path: "/tmp/test.py",
624
+ old_string: "content",
625
+ new_string: "updated",
626
+ });
627
+ // All calls should use "gpu-2"
628
+ for (const call of pool.exec.mock.calls) {
629
+ expect(call[0]).toBe("gpu-2");
630
+ }
631
+ });
632
+ it("strips trailing newline from patched content before writing", async () => {
633
+ const commands = [];
634
+ const pool = mockPool((_, cmd) => {
635
+ commands.push(cmd);
636
+ if (cmd.includes("cat") && !cmd.includes("<<")) {
637
+ return { stdout: "alpha beta\n", stderr: "", exitCode: 0 };
638
+ }
639
+ return { stdout: "", stderr: "", exitCode: 0 };
640
+ });
641
+ const tool = createPatchFileTool(pool);
642
+ await tool.execute({
643
+ machine_id: "local",
644
+ path: "/tmp/test.py",
645
+ old_string: "alpha",
646
+ new_string: "gamma",
647
+ });
648
+ const writeCmd = commands.find((c) => c.includes("_HELIOS_EOF_"));
649
+ expect(writeCmd).toBeDefined();
650
+ // The body should be "gamma beta" (trailing newline stripped)
651
+ const bodyMatch = writeCmd.match(/<<'_HELIOS_EOF_[^']*'\n([\s\S]*)\n_HELIOS_EOF_/);
652
+ expect(bodyMatch).not.toBeNull();
653
+ expect(bodyMatch[1]).toBe("gamma beta");
654
+ });
655
+ });
656
+ //# sourceMappingURL=file-ops.test.js.map