@snoglobe/helios 0.3.5 → 0.4.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 (331) 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/config/project.test.d.ts +2 -0
  6. package/dist/config/project.test.d.ts.map +1 -0
  7. package/dist/config/project.test.js +219 -0
  8. package/dist/config/project.test.js.map +1 -0
  9. package/dist/core/orchestrator-integration.test.d.ts +2 -0
  10. package/dist/core/orchestrator-integration.test.d.ts.map +1 -0
  11. package/dist/core/orchestrator-integration.test.js +674 -0
  12. package/dist/core/orchestrator-integration.test.js.map +1 -0
  13. package/dist/core/orchestrator.d.ts +3 -1
  14. package/dist/core/orchestrator.d.ts.map +1 -1
  15. package/dist/core/orchestrator.js +98 -53
  16. package/dist/core/orchestrator.js.map +1 -1
  17. package/dist/core/orchestrator.test.d.ts +2 -0
  18. package/dist/core/orchestrator.test.d.ts.map +1 -0
  19. package/dist/core/orchestrator.test.js +982 -0
  20. package/dist/core/orchestrator.test.js.map +1 -0
  21. package/dist/core/state-machine.d.ts.map +1 -1
  22. package/dist/core/state-machine.js +3 -0
  23. package/dist/core/state-machine.js.map +1 -1
  24. package/dist/core/state-machine.test.d.ts +2 -0
  25. package/dist/core/state-machine.test.d.ts.map +1 -0
  26. package/dist/core/state-machine.test.js +95 -0
  27. package/dist/core/state-machine.test.js.map +1 -0
  28. package/dist/core/stickies.d.ts.map +1 -1
  29. package/dist/core/stickies.js +3 -0
  30. package/dist/core/stickies.js.map +1 -1
  31. package/dist/core/stickies.test.d.ts +2 -0
  32. package/dist/core/stickies.test.d.ts.map +1 -0
  33. package/dist/core/stickies.test.js +63 -0
  34. package/dist/core/stickies.test.js.map +1 -0
  35. package/dist/core/task-poller.d.ts.map +1 -1
  36. package/dist/core/task-poller.js +2 -1
  37. package/dist/core/task-poller.js.map +1 -1
  38. package/dist/init.d.ts +2 -0
  39. package/dist/init.d.ts.map +1 -1
  40. package/dist/init.js +19 -0
  41. package/dist/init.js.map +1 -1
  42. package/dist/memory/context-gate.test.d.ts +2 -0
  43. package/dist/memory/context-gate.test.d.ts.map +1 -0
  44. package/dist/memory/context-gate.test.js +318 -0
  45. package/dist/memory/context-gate.test.js.map +1 -0
  46. package/dist/memory/experiment-tracker.test.d.ts +2 -0
  47. package/dist/memory/experiment-tracker.test.d.ts.map +1 -0
  48. package/dist/memory/experiment-tracker.test.js +325 -0
  49. package/dist/memory/experiment-tracker.test.js.map +1 -0
  50. package/dist/memory/memory-store.d.ts +2 -0
  51. package/dist/memory/memory-store.d.ts.map +1 -1
  52. package/dist/memory/memory-store.js +35 -28
  53. package/dist/memory/memory-store.js.map +1 -1
  54. package/dist/memory/memory-store.test.d.ts +2 -0
  55. package/dist/memory/memory-store.test.d.ts.map +1 -0
  56. package/dist/memory/memory-store.test.js +144 -0
  57. package/dist/memory/memory-store.test.js.map +1 -0
  58. package/dist/memory/token-estimator.test.d.ts +2 -0
  59. package/dist/memory/token-estimator.test.d.ts.map +1 -0
  60. package/dist/memory/token-estimator.test.js +42 -0
  61. package/dist/memory/token-estimator.test.js.map +1 -0
  62. package/dist/metrics/analyzer.test.d.ts +2 -0
  63. package/dist/metrics/analyzer.test.d.ts.map +1 -0
  64. package/dist/metrics/analyzer.test.js +65 -0
  65. package/dist/metrics/analyzer.test.js.map +1 -0
  66. package/dist/metrics/parser.test.d.ts +2 -0
  67. package/dist/metrics/parser.test.d.ts.map +1 -0
  68. package/dist/metrics/parser.test.js +106 -0
  69. package/dist/metrics/parser.test.js.map +1 -0
  70. package/dist/metrics/store.d.ts +2 -0
  71. package/dist/metrics/store.d.ts.map +1 -1
  72. package/dist/metrics/store.js +26 -50
  73. package/dist/metrics/store.js.map +1 -1
  74. package/dist/metrics/store.test.d.ts +2 -0
  75. package/dist/metrics/store.test.d.ts.map +1 -0
  76. package/dist/metrics/store.test.js +148 -0
  77. package/dist/metrics/store.test.js.map +1 -0
  78. package/dist/paths.js +5 -5
  79. package/dist/paths.js.map +1 -1
  80. package/dist/providers/auth/auth-manager.d.ts +1 -0
  81. package/dist/providers/auth/auth-manager.d.ts.map +1 -1
  82. package/dist/providers/auth/auth-manager.js +8 -1
  83. package/dist/providers/auth/auth-manager.js.map +1 -1
  84. package/dist/providers/auth/auth-manager.test.d.ts +2 -0
  85. package/dist/providers/auth/auth-manager.test.d.ts.map +1 -0
  86. package/dist/providers/auth/auth-manager.test.js +364 -0
  87. package/dist/providers/auth/auth-manager.test.js.map +1 -0
  88. package/dist/providers/auth/token-store.js +2 -2
  89. package/dist/providers/auth/token-store.js.map +1 -1
  90. package/dist/providers/claude/history.test.d.ts +2 -0
  91. package/dist/providers/claude/history.test.d.ts.map +1 -0
  92. package/dist/providers/claude/history.test.js +755 -0
  93. package/dist/providers/claude/history.test.js.map +1 -0
  94. package/dist/providers/claude/provider.d.ts +4 -2
  95. package/dist/providers/claude/provider.d.ts.map +1 -1
  96. package/dist/providers/claude/provider.js +44 -22
  97. package/dist/providers/claude/provider.js.map +1 -1
  98. package/dist/providers/claude/provider.test.d.ts +2 -0
  99. package/dist/providers/claude/provider.test.d.ts.map +1 -0
  100. package/dist/providers/claude/provider.test.js +1167 -0
  101. package/dist/providers/claude/provider.test.js.map +1 -0
  102. package/dist/providers/openai/history.test.d.ts +2 -0
  103. package/dist/providers/openai/history.test.d.ts.map +1 -0
  104. package/dist/providers/openai/history.test.js +655 -0
  105. package/dist/providers/openai/history.test.js.map +1 -0
  106. package/dist/providers/openai/provider.d.ts +3 -2
  107. package/dist/providers/openai/provider.d.ts.map +1 -1
  108. package/dist/providers/openai/provider.js +18 -11
  109. package/dist/providers/openai/provider.js.map +1 -1
  110. package/dist/providers/openai/provider.test.d.ts +2 -0
  111. package/dist/providers/openai/provider.test.d.ts.map +1 -0
  112. package/dist/providers/openai/provider.test.js +1089 -0
  113. package/dist/providers/openai/provider.test.js.map +1 -0
  114. package/dist/providers/retry.test.d.ts +2 -0
  115. package/dist/providers/retry.test.d.ts.map +1 -0
  116. package/dist/providers/retry.test.js +194 -0
  117. package/dist/providers/retry.test.js.map +1 -0
  118. package/dist/providers/sse.d.ts +1 -0
  119. package/dist/providers/sse.d.ts.map +1 -1
  120. package/dist/providers/sse.js +29 -20
  121. package/dist/providers/sse.js.map +1 -1
  122. package/dist/providers/sse.test.d.ts +2 -0
  123. package/dist/providers/sse.test.d.ts.map +1 -0
  124. package/dist/providers/sse.test.js +79 -0
  125. package/dist/providers/sse.test.js.map +1 -0
  126. package/dist/providers/types.d.ts +1 -1
  127. package/dist/providers/types.d.ts.map +1 -1
  128. package/dist/providers/types.test.d.ts +2 -0
  129. package/dist/providers/types.test.d.ts.map +1 -0
  130. package/dist/providers/types.test.js +112 -0
  131. package/dist/providers/types.test.js.map +1 -0
  132. package/dist/remote/connection-pool.d.ts.map +1 -1
  133. package/dist/remote/connection-pool.js +24 -3
  134. package/dist/remote/connection-pool.js.map +1 -1
  135. package/dist/remote/file-sync.d.ts.map +1 -1
  136. package/dist/remote/file-sync.js +4 -3
  137. package/dist/remote/file-sync.js.map +1 -1
  138. package/dist/scheduler/sleep-manager.d.ts.map +1 -1
  139. package/dist/scheduler/sleep-manager.js +8 -1
  140. package/dist/scheduler/sleep-manager.js.map +1 -1
  141. package/dist/scheduler/sleep-manager.test.d.ts +2 -0
  142. package/dist/scheduler/sleep-manager.test.d.ts.map +1 -0
  143. package/dist/scheduler/sleep-manager.test.js +491 -0
  144. package/dist/scheduler/sleep-manager.test.js.map +1 -0
  145. package/dist/scheduler/ssh-batcher.d.ts.map +1 -1
  146. package/dist/scheduler/ssh-batcher.js +6 -4
  147. package/dist/scheduler/ssh-batcher.js.map +1 -1
  148. package/dist/scheduler/ssh-batcher.test.d.ts +2 -0
  149. package/dist/scheduler/ssh-batcher.test.d.ts.map +1 -0
  150. package/dist/scheduler/ssh-batcher.test.js +76 -0
  151. package/dist/scheduler/ssh-batcher.test.js.map +1 -0
  152. package/dist/scheduler/state-store.d.ts.map +1 -1
  153. package/dist/scheduler/state-store.js +1 -2
  154. package/dist/scheduler/state-store.js.map +1 -1
  155. package/dist/scheduler/trigger-scheduler.d.ts.map +1 -1
  156. package/dist/scheduler/trigger-scheduler.js +59 -36
  157. package/dist/scheduler/trigger-scheduler.js.map +1 -1
  158. package/dist/scheduler/trigger-scheduler.test.d.ts +2 -0
  159. package/dist/scheduler/trigger-scheduler.test.d.ts.map +1 -0
  160. package/dist/scheduler/trigger-scheduler.test.js +483 -0
  161. package/dist/scheduler/trigger-scheduler.test.js.map +1 -0
  162. package/dist/scheduler/triggers/file.d.ts.map +1 -1
  163. package/dist/scheduler/triggers/file.js +12 -3
  164. package/dist/scheduler/triggers/file.js.map +1 -1
  165. package/dist/scheduler/triggers/file.test.d.ts +2 -0
  166. package/dist/scheduler/triggers/file.test.d.ts.map +1 -0
  167. package/dist/scheduler/triggers/file.test.js +294 -0
  168. package/dist/scheduler/triggers/file.test.js.map +1 -0
  169. package/dist/scheduler/triggers/metric.d.ts +3 -1
  170. package/dist/scheduler/triggers/metric.d.ts.map +1 -1
  171. package/dist/scheduler/triggers/metric.js +12 -8
  172. package/dist/scheduler/triggers/metric.js.map +1 -1
  173. package/dist/scheduler/triggers/metric.test.d.ts +2 -0
  174. package/dist/scheduler/triggers/metric.test.d.ts.map +1 -0
  175. package/dist/scheduler/triggers/metric.test.js +533 -0
  176. package/dist/scheduler/triggers/metric.test.js.map +1 -0
  177. package/dist/scheduler/triggers/process-exit.d.ts.map +1 -1
  178. package/dist/scheduler/triggers/process-exit.js +2 -1
  179. package/dist/scheduler/triggers/process-exit.js.map +1 -1
  180. package/dist/scheduler/triggers/process-exit.test.d.ts +2 -0
  181. package/dist/scheduler/triggers/process-exit.test.d.ts.map +1 -0
  182. package/dist/scheduler/triggers/process-exit.test.js +118 -0
  183. package/dist/scheduler/triggers/process-exit.test.js.map +1 -0
  184. package/dist/scheduler/triggers/resource.d.ts.map +1 -1
  185. package/dist/scheduler/triggers/resource.js +2 -10
  186. package/dist/scheduler/triggers/resource.js.map +1 -1
  187. package/dist/scheduler/triggers/resource.test.d.ts +2 -0
  188. package/dist/scheduler/triggers/resource.test.d.ts.map +1 -0
  189. package/dist/scheduler/triggers/resource.test.js +225 -0
  190. package/dist/scheduler/triggers/resource.test.js.map +1 -0
  191. package/dist/scheduler/triggers/timer.test.d.ts +2 -0
  192. package/dist/scheduler/triggers/timer.test.d.ts.map +1 -0
  193. package/dist/scheduler/triggers/timer.test.js +56 -0
  194. package/dist/scheduler/triggers/timer.test.js.map +1 -0
  195. package/dist/scheduler/triggers/types.d.ts +4 -2
  196. package/dist/scheduler/triggers/types.d.ts.map +1 -1
  197. package/dist/scheduler/triggers/types.js +0 -1
  198. package/dist/scheduler/triggers/types.js.map +1 -1
  199. package/dist/skills/executor.d.ts.map +1 -1
  200. package/dist/skills/executor.js +13 -15
  201. package/dist/skills/executor.js.map +1 -1
  202. package/dist/store/database.d.ts +5 -0
  203. package/dist/store/database.d.ts.map +1 -1
  204. package/dist/store/database.js +17 -1
  205. package/dist/store/database.js.map +1 -1
  206. package/dist/store/migrations.test.d.ts +2 -0
  207. package/dist/store/migrations.test.d.ts.map +1 -0
  208. package/dist/store/migrations.test.js +278 -0
  209. package/dist/store/migrations.test.js.map +1 -0
  210. package/dist/store/session-store-edge.test.d.ts +2 -0
  211. package/dist/store/session-store-edge.test.d.ts.map +1 -0
  212. package/dist/store/session-store-edge.test.js +522 -0
  213. package/dist/store/session-store-edge.test.js.map +1 -0
  214. package/dist/store/session-store.d.ts +7 -0
  215. package/dist/store/session-store.d.ts.map +1 -1
  216. package/dist/store/session-store.js +27 -22
  217. package/dist/store/session-store.js.map +1 -1
  218. package/dist/store/session-store.test.d.ts +2 -0
  219. package/dist/store/session-store.test.d.ts.map +1 -0
  220. package/dist/store/session-store.test.js +125 -0
  221. package/dist/store/session-store.test.js.map +1 -0
  222. package/dist/subagent/executor.d.ts +21 -0
  223. package/dist/subagent/executor.d.ts.map +1 -0
  224. package/dist/subagent/executor.js +136 -0
  225. package/dist/subagent/executor.js.map +1 -0
  226. package/dist/subagent/manager.d.ts +20 -0
  227. package/dist/subagent/manager.d.ts.map +1 -0
  228. package/dist/subagent/manager.js +98 -0
  229. package/dist/subagent/manager.js.map +1 -0
  230. package/dist/subagent/scoped-memory.d.ts +28 -0
  231. package/dist/subagent/scoped-memory.d.ts.map +1 -0
  232. package/dist/subagent/scoped-memory.js +122 -0
  233. package/dist/subagent/scoped-memory.js.map +1 -0
  234. package/dist/subagent/types.d.ts +27 -0
  235. package/dist/subagent/types.d.ts.map +1 -0
  236. package/dist/subagent/types.js +2 -0
  237. package/dist/subagent/types.js.map +1 -0
  238. package/dist/tools/file-ops.d.ts.map +1 -1
  239. package/dist/tools/file-ops.js +23 -13
  240. package/dist/tools/file-ops.js.map +1 -1
  241. package/dist/tools/file-ops.test.d.ts +2 -0
  242. package/dist/tools/file-ops.test.d.ts.map +1 -0
  243. package/dist/tools/file-ops.test.js +656 -0
  244. package/dist/tools/file-ops.test.js.map +1 -0
  245. package/dist/tools/memory-tools.test.d.ts +2 -0
  246. package/dist/tools/memory-tools.test.d.ts.map +1 -0
  247. package/dist/tools/memory-tools.test.js +273 -0
  248. package/dist/tools/memory-tools.test.js.map +1 -0
  249. package/dist/tools/subagent.d.ts +10 -0
  250. package/dist/tools/subagent.d.ts.map +1 -0
  251. package/dist/tools/subagent.js +164 -0
  252. package/dist/tools/subagent.js.map +1 -0
  253. package/dist/tools/task-output.d.ts.map +1 -1
  254. package/dist/tools/task-output.js +13 -2
  255. package/dist/tools/task-output.js.map +1 -1
  256. package/dist/ui/components/input-bar.d.ts +1 -1
  257. package/dist/ui/components/input-bar.d.ts.map +1 -1
  258. package/dist/ui/components/input-bar.js +3 -3
  259. package/dist/ui/components/input-bar.js.map +1 -1
  260. package/dist/ui/components/input-bar.test.d.ts +2 -0
  261. package/dist/ui/components/input-bar.test.d.ts.map +1 -0
  262. package/dist/ui/components/input-bar.test.js +380 -0
  263. package/dist/ui/components/input-bar.test.js.map +1 -0
  264. package/dist/ui/components/key-hint-rule.d.ts +2 -1
  265. package/dist/ui/components/key-hint-rule.d.ts.map +1 -1
  266. package/dist/ui/components/key-hint-rule.js +3 -3
  267. package/dist/ui/components/key-hint-rule.js.map +1 -1
  268. package/dist/ui/components/status-bar.d.ts +1 -1
  269. package/dist/ui/components/status-bar.d.ts.map +1 -1
  270. package/dist/ui/components/status-bar.js +11 -10
  271. package/dist/ui/components/status-bar.js.map +1 -1
  272. package/dist/ui/components/status-bar.test.d.ts +2 -0
  273. package/dist/ui/components/status-bar.test.d.ts.map +1 -0
  274. package/dist/ui/components/status-bar.test.js +206 -0
  275. package/dist/ui/components/status-bar.test.js.map +1 -0
  276. package/dist/ui/format.d.ts +9 -0
  277. package/dist/ui/format.d.ts.map +1 -1
  278. package/dist/ui/format.js +21 -0
  279. package/dist/ui/format.js.map +1 -1
  280. package/dist/ui/format.test.d.ts +2 -0
  281. package/dist/ui/format.test.d.ts.map +1 -0
  282. package/dist/ui/format.test.js +122 -0
  283. package/dist/ui/format.test.js.map +1 -0
  284. package/dist/ui/layout.d.ts.map +1 -1
  285. package/dist/ui/layout.js +74 -12
  286. package/dist/ui/layout.js.map +1 -1
  287. package/dist/ui/markdown.test.d.ts +2 -0
  288. package/dist/ui/markdown.test.d.ts.map +1 -0
  289. package/dist/ui/markdown.test.js +133 -0
  290. package/dist/ui/markdown.test.js.map +1 -0
  291. package/dist/ui/mouse-filter.test.d.ts +2 -0
  292. package/dist/ui/mouse-filter.test.d.ts.map +1 -0
  293. package/dist/ui/mouse-filter.test.js +231 -0
  294. package/dist/ui/mouse-filter.test.js.map +1 -0
  295. package/dist/ui/overlays/metrics-overlay.test.d.ts +2 -0
  296. package/dist/ui/overlays/metrics-overlay.test.d.ts.map +1 -0
  297. package/dist/ui/overlays/metrics-overlay.test.js +248 -0
  298. package/dist/ui/overlays/metrics-overlay.test.js.map +1 -0
  299. package/dist/ui/overlays/task-overlay.test.d.ts +2 -0
  300. package/dist/ui/overlays/task-overlay.test.d.ts.map +1 -0
  301. package/dist/ui/overlays/task-overlay.test.js +238 -0
  302. package/dist/ui/overlays/task-overlay.test.js.map +1 -0
  303. package/dist/ui/panels/conversation.d.ts.map +1 -1
  304. package/dist/ui/panels/conversation.js +60 -5
  305. package/dist/ui/panels/conversation.js.map +1 -1
  306. package/dist/ui/panels/conversation.test.d.ts +2 -0
  307. package/dist/ui/panels/conversation.test.d.ts.map +1 -0
  308. package/dist/ui/panels/conversation.test.js +381 -0
  309. package/dist/ui/panels/conversation.test.js.map +1 -0
  310. package/dist/ui/panels/metrics-dashboard.test.d.ts +2 -0
  311. package/dist/ui/panels/metrics-dashboard.test.d.ts.map +1 -0
  312. package/dist/ui/panels/metrics-dashboard.test.js +191 -0
  313. package/dist/ui/panels/metrics-dashboard.test.js.map +1 -0
  314. package/dist/ui/panels/sleep-panel.test.d.ts +2 -0
  315. package/dist/ui/panels/sleep-panel.test.d.ts.map +1 -0
  316. package/dist/ui/panels/sleep-panel.test.js +376 -0
  317. package/dist/ui/panels/sleep-panel.test.js.map +1 -0
  318. package/dist/ui/panels/task-list.d.ts.map +1 -1
  319. package/dist/ui/panels/task-list.js +3 -2
  320. package/dist/ui/panels/task-list.js.map +1 -1
  321. package/dist/ui/panels/task-list.test.d.ts +2 -0
  322. package/dist/ui/panels/task-list.test.d.ts.map +1 -0
  323. package/dist/ui/panels/task-list.test.js +210 -0
  324. package/dist/ui/panels/task-list.test.js.map +1 -0
  325. package/dist/ui/theme.test.d.ts +2 -0
  326. package/dist/ui/theme.test.d.ts.map +1 -0
  327. package/dist/ui/theme.test.js +159 -0
  328. package/dist/ui/theme.test.js.map +1 -0
  329. package/dist/ui/types.d.ts +3 -0
  330. package/dist/ui/types.d.ts.map +1 -1
  331. 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