@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,982 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { createTestDb } from "../__tests__/db-helper.js";
3
+ // ---------------------------------------------------------------------------
4
+ // Module mocks — must come before dynamic import of Orchestrator
5
+ // ---------------------------------------------------------------------------
6
+ const mockDb = { current: createTestDb() };
7
+ vi.mock("../store/database.js", () => {
8
+ const getDb = () => mockDb.current;
9
+ class StmtCache {
10
+ cache = new Map();
11
+ stmt(sql) {
12
+ let s = this.cache.get(sql);
13
+ if (!s) {
14
+ s = getDb().prepare(sql);
15
+ this.cache.set(sql, s);
16
+ }
17
+ return s;
18
+ }
19
+ }
20
+ return { getDb, StmtCache, getHeliosDir: () => "/tmp/helios-test" };
21
+ });
22
+ vi.mock("../store/preferences.js", () => ({
23
+ savePreferences: vi.fn(),
24
+ }));
25
+ vi.mock("../paths.js", () => ({
26
+ debugLog: vi.fn(),
27
+ }));
28
+ const { Orchestrator } = await import("./orchestrator.js");
29
+ const { SessionStore } = await import("../store/session-store.js");
30
+ const { StickyManager } = await import("./stickies.js");
31
+ // ---------------------------------------------------------------------------
32
+ // Mock provider factory
33
+ // ---------------------------------------------------------------------------
34
+ /**
35
+ * Creates a mock ModelProvider. If a SessionStore is provided, createSession
36
+ * will insert the session into the DB so that foreign-key dependent operations
37
+ * (addMessage, addCost, etc.) succeed.
38
+ */
39
+ function mockProvider(name = "claude", sessionStore) {
40
+ return {
41
+ name,
42
+ displayName: name === "claude" ? "Claude" : "OpenAI",
43
+ currentModel: name === "claude" ? "claude-opus-4-6" : "gpt-5.4",
44
+ reasoningEffort: "medium",
45
+ isAuthenticated: vi.fn().mockResolvedValue(true),
46
+ authenticate: vi.fn().mockResolvedValue(undefined),
47
+ createSession: vi.fn().mockImplementation(async (_config) => {
48
+ if (sessionStore) {
49
+ return sessionStore.createSession(name, name === "claude" ? "claude-opus-4-6" : "gpt-5.4");
50
+ }
51
+ const now = Date.now();
52
+ return {
53
+ id: `sess-${Math.random().toString(36).slice(2, 8)}`,
54
+ providerId: name,
55
+ createdAt: now,
56
+ lastActiveAt: now,
57
+ };
58
+ }),
59
+ resumeSession: vi.fn().mockImplementation(async (id) => {
60
+ return {
61
+ id,
62
+ providerId: name,
63
+ createdAt: Date.now(),
64
+ lastActiveAt: Date.now(),
65
+ };
66
+ }),
67
+ send: vi.fn().mockImplementation(async function* () {
68
+ yield {
69
+ type: "text",
70
+ text: "Hello",
71
+ delta: "Hello",
72
+ };
73
+ yield {
74
+ type: "done",
75
+ usage: { inputTokens: 100, outputTokens: 50, costUsd: 0.01 },
76
+ };
77
+ }),
78
+ interrupt: vi.fn(),
79
+ resetHistory: vi.fn(),
80
+ closeSession: vi.fn().mockResolvedValue(undefined),
81
+ fetchModels: vi
82
+ .fn()
83
+ .mockResolvedValue([{ id: "test-model", name: "Test Model" }]),
84
+ };
85
+ }
86
+ // ---------------------------------------------------------------------------
87
+ // Helpers
88
+ // ---------------------------------------------------------------------------
89
+ function makeOrchestrator(defaultProvider = "claude") {
90
+ return new Orchestrator({
91
+ defaultProvider,
92
+ systemPrompt: "You are a test agent.",
93
+ sessionStore: new SessionStore("test-agent"),
94
+ });
95
+ }
96
+ /**
97
+ * Create an orchestrator wired to a mock provider whose createSession inserts
98
+ * into the DB (so addMessage / addCost don't violate foreign key constraints).
99
+ */
100
+ async function makeWiredOrchestrator(providerName = "claude") {
101
+ const orch = makeOrchestrator(providerName);
102
+ const provider = mockProvider(providerName, orch.sessionStore);
103
+ orch.registerProvider(provider);
104
+ await orch.switchProvider(providerName);
105
+ return { orch, provider };
106
+ }
107
+ function makeTool(name) {
108
+ return {
109
+ name,
110
+ description: `Tool ${name}`,
111
+ parameters: { type: "object", properties: {} },
112
+ execute: vi.fn().mockResolvedValue("ok"),
113
+ };
114
+ }
115
+ /** Collect all events from an async generator. */
116
+ async function collectEvents(gen) {
117
+ const events = [];
118
+ for await (const e of gen) {
119
+ events.push(e);
120
+ }
121
+ return events;
122
+ }
123
+ // ---------------------------------------------------------------------------
124
+ // Tests
125
+ // ---------------------------------------------------------------------------
126
+ describe("Orchestrator", () => {
127
+ beforeEach(() => {
128
+ mockDb.current = createTestDb();
129
+ });
130
+ // =======================================================================
131
+ // Provider Management
132
+ // =======================================================================
133
+ describe("Provider Management", () => {
134
+ it("registerProvider stores provider by name", () => {
135
+ const orch = makeOrchestrator();
136
+ const provider = mockProvider("claude");
137
+ orch.registerProvider(provider);
138
+ expect(orch.getProvider("claude")).toBe(provider);
139
+ });
140
+ it("registerProvider for both claude and openai", () => {
141
+ const orch = makeOrchestrator();
142
+ const claude = mockProvider("claude");
143
+ const openai = mockProvider("openai");
144
+ orch.registerProvider(claude);
145
+ orch.registerProvider(openai);
146
+ expect(orch.getProvider("claude")).toBe(claude);
147
+ expect(orch.getProvider("openai")).toBe(openai);
148
+ });
149
+ it("getProvider returns registered provider by name", () => {
150
+ const orch = makeOrchestrator();
151
+ const provider = mockProvider("openai");
152
+ orch.registerProvider(provider);
153
+ expect(orch.getProvider("openai")).toBe(provider);
154
+ });
155
+ it("getProvider with no args returns active provider", async () => {
156
+ const orch = makeOrchestrator();
157
+ const provider = mockProvider("claude");
158
+ orch.registerProvider(provider);
159
+ // Before switching, no active provider
160
+ expect(orch.getProvider()).toBeNull();
161
+ await orch.switchProvider("claude");
162
+ expect(orch.getProvider()).toBe(provider);
163
+ });
164
+ it("getProvider returns null if not registered", () => {
165
+ const orch = makeOrchestrator();
166
+ expect(orch.getProvider("claude")).toBeNull();
167
+ expect(orch.getProvider("openai")).toBeNull();
168
+ });
169
+ it("switchProvider authenticates and sets active", async () => {
170
+ const orch = makeOrchestrator();
171
+ const provider = mockProvider("claude");
172
+ orch.registerProvider(provider);
173
+ await orch.switchProvider("claude");
174
+ expect(provider.authenticate).toHaveBeenCalledOnce();
175
+ expect(orch.getProvider()).toBe(provider);
176
+ });
177
+ it("switchProvider cleans up previous session", async () => {
178
+ const orch = makeOrchestrator();
179
+ const claude = mockProvider("claude");
180
+ const openai = mockProvider("openai");
181
+ orch.registerProvider(claude);
182
+ orch.registerProvider(openai);
183
+ await orch.switchProvider("claude");
184
+ const session = await orch.startSession();
185
+ await orch.switchProvider("openai");
186
+ expect(claude.closeSession).toHaveBeenCalledWith(session);
187
+ expect(orch.activeSession).toBeNull();
188
+ });
189
+ it("switchProvider throws for unregistered provider", async () => {
190
+ const orch = makeOrchestrator();
191
+ await expect(orch.switchProvider("openai")).rejects.toThrow('Provider "openai" not registered');
192
+ });
193
+ it("switchProvider saves preference", async () => {
194
+ const { savePreferences } = await import("../store/preferences.js");
195
+ const orch = makeOrchestrator();
196
+ orch.registerProvider(mockProvider("claude"));
197
+ await orch.switchProvider("claude");
198
+ expect(savePreferences).toHaveBeenCalledWith({
199
+ lastProvider: "claude",
200
+ });
201
+ });
202
+ it("switchProvider to same provider re-authenticates", async () => {
203
+ const orch = makeOrchestrator();
204
+ const provider = mockProvider("claude");
205
+ orch.registerProvider(provider);
206
+ await orch.switchProvider("claude");
207
+ await orch.switchProvider("claude");
208
+ expect(provider.authenticate).toHaveBeenCalledTimes(2);
209
+ });
210
+ it("currentProvider returns active provider", async () => {
211
+ const orch = makeOrchestrator();
212
+ const provider = mockProvider("claude");
213
+ orch.registerProvider(provider);
214
+ expect(orch.currentProvider).toBeNull();
215
+ await orch.switchProvider("claude");
216
+ expect(orch.currentProvider).toBe(provider);
217
+ });
218
+ it("registerProvider overwrites existing provider with same name", () => {
219
+ const orch = makeOrchestrator();
220
+ const p1 = mockProvider("claude");
221
+ const p2 = mockProvider("claude");
222
+ orch.registerProvider(p1);
223
+ orch.registerProvider(p2);
224
+ expect(orch.getProvider("claude")).toBe(p2);
225
+ });
226
+ it("switchProvider cleans up session even if closeSession throws", async () => {
227
+ const orch = makeOrchestrator();
228
+ const claude = mockProvider("claude");
229
+ const openai = mockProvider("openai");
230
+ claude.closeSession.mockRejectedValue(new Error("cleanup failed"));
231
+ orch.registerProvider(claude);
232
+ orch.registerProvider(openai);
233
+ await orch.switchProvider("claude");
234
+ await orch.startSession();
235
+ // Should not throw despite closeSession failure
236
+ await orch.switchProvider("openai");
237
+ expect(orch.getProvider()).toBe(openai);
238
+ });
239
+ });
240
+ // =======================================================================
241
+ // Session Lifecycle
242
+ // =======================================================================
243
+ describe("Session Lifecycle", () => {
244
+ it("startSession creates session via provider", async () => {
245
+ const orch = makeOrchestrator();
246
+ const provider = mockProvider("claude");
247
+ orch.registerProvider(provider);
248
+ await orch.switchProvider("claude");
249
+ const session = await orch.startSession();
250
+ expect(provider.createSession).toHaveBeenCalledOnce();
251
+ expect(session.id).toBeTruthy();
252
+ expect(session.providerId).toBe("claude");
253
+ });
254
+ it("startSession auto-switches to default provider if none active", async () => {
255
+ const orch = makeOrchestrator("claude");
256
+ const provider = mockProvider("claude");
257
+ orch.registerProvider(provider);
258
+ const session = await orch.startSession();
259
+ expect(provider.authenticate).toHaveBeenCalledOnce();
260
+ expect(session).toBeTruthy();
261
+ });
262
+ it("startSession binds context gate", async () => {
263
+ const orch = makeOrchestrator();
264
+ const provider = mockProvider("claude");
265
+ orch.registerProvider(provider);
266
+ await orch.switchProvider("claude");
267
+ const mockGate = {
268
+ onSessionStart: vi.fn(),
269
+ checkThreshold: vi.fn().mockReturnValue(false),
270
+ performCheckpointWithGist: vi.fn(),
271
+ };
272
+ orch.setContextGate(mockGate);
273
+ const session = await orch.startSession();
274
+ expect(mockGate.onSessionStart).toHaveBeenCalledWith(session.id);
275
+ });
276
+ it("startSession transitions state machine to active", async () => {
277
+ const orch = makeOrchestrator();
278
+ orch.registerProvider(mockProvider("claude"));
279
+ await orch.switchProvider("claude");
280
+ expect(orch.currentState).toBe("idle");
281
+ await orch.startSession();
282
+ expect(orch.currentState).toBe("active");
283
+ });
284
+ it("startSession passes config to provider", async () => {
285
+ const orch = makeOrchestrator();
286
+ const provider = mockProvider("claude");
287
+ orch.registerProvider(provider);
288
+ await orch.switchProvider("claude");
289
+ await orch.startSession({ model: "opus", temperature: 0.5 });
290
+ expect(provider.createSession).toHaveBeenCalledWith(expect.objectContaining({
291
+ systemPrompt: "You are a test agent.",
292
+ model: "opus",
293
+ temperature: 0.5,
294
+ }));
295
+ });
296
+ it("ensureSession returns existing session", async () => {
297
+ const orch = makeOrchestrator();
298
+ orch.registerProvider(mockProvider("claude"));
299
+ await orch.switchProvider("claude");
300
+ const s1 = await orch.startSession();
301
+ const s2 = await orch.ensureSession();
302
+ expect(s2).toBe(s1);
303
+ });
304
+ it("ensureSession creates new if none exists", async () => {
305
+ const orch = makeOrchestrator();
306
+ orch.registerProvider(mockProvider("claude"));
307
+ await orch.switchProvider("claude");
308
+ const session = await orch.ensureSession();
309
+ expect(session).toBeTruthy();
310
+ expect(session.id).toBeTruthy();
311
+ });
312
+ it("resumeSession loads from DB", async () => {
313
+ const orch = makeOrchestrator();
314
+ const provider = mockProvider("claude");
315
+ orch.registerProvider(provider);
316
+ await orch.switchProvider("claude");
317
+ // Create a session in the DB first
318
+ const dbSession = orch.sessionStore.createSession("claude", "opus");
319
+ const session = await orch.resumeSession(dbSession.id);
320
+ expect(provider.resumeSession).toHaveBeenCalledWith(dbSession.id, "You are a test agent.");
321
+ expect(session.id).toBe(dbSession.id);
322
+ });
323
+ it("resumeSession switches provider if needed", async () => {
324
+ const orch = makeOrchestrator();
325
+ const claude = mockProvider("claude");
326
+ const openai = mockProvider("openai");
327
+ orch.registerProvider(claude);
328
+ orch.registerProvider(openai);
329
+ await orch.switchProvider("claude");
330
+ // Create a session stored as openai
331
+ const dbSession = orch.sessionStore.createSession("openai", "gpt-5.4");
332
+ await orch.resumeSession(dbSession.id);
333
+ // Should have switched to openai
334
+ expect(orch.getProvider()).toBe(openai);
335
+ expect(openai.authenticate).toHaveBeenCalled();
336
+ });
337
+ it("resumeSession binds context gate", async () => {
338
+ const orch = makeOrchestrator();
339
+ orch.registerProvider(mockProvider("claude"));
340
+ await orch.switchProvider("claude");
341
+ const mockGate = {
342
+ onSessionStart: vi.fn(),
343
+ checkThreshold: vi.fn().mockReturnValue(false),
344
+ performCheckpointWithGist: vi.fn(),
345
+ };
346
+ orch.setContextGate(mockGate);
347
+ const dbSession = orch.sessionStore.createSession("claude");
348
+ await orch.resumeSession(dbSession.id);
349
+ expect(mockGate.onSessionStart).toHaveBeenCalledWith(dbSession.id);
350
+ });
351
+ it("resumeSession throws for unknown session", async () => {
352
+ const orch = makeOrchestrator();
353
+ orch.registerProvider(mockProvider("claude"));
354
+ await orch.switchProvider("claude");
355
+ await expect(orch.resumeSession("nonexistent-id")).rejects.toThrow('Session "nonexistent-id" not found');
356
+ });
357
+ it("resumeSession transitions state machine to active", async () => {
358
+ const orch = makeOrchestrator();
359
+ orch.registerProvider(mockProvider("claude"));
360
+ await orch.switchProvider("claude");
361
+ const dbSession = orch.sessionStore.createSession("claude");
362
+ await orch.resumeSession(dbSession.id);
363
+ expect(orch.currentState).toBe("active");
364
+ });
365
+ it("activeSession reflects current session", async () => {
366
+ const orch = makeOrchestrator();
367
+ orch.registerProvider(mockProvider("claude"));
368
+ await orch.switchProvider("claude");
369
+ expect(orch.activeSession).toBeNull();
370
+ const session = await orch.startSession();
371
+ expect(orch.activeSession).toBe(session);
372
+ });
373
+ it("currentSession is an alias for activeSession", async () => {
374
+ const orch = makeOrchestrator();
375
+ orch.registerProvider(mockProvider("claude"));
376
+ await orch.switchProvider("claude");
377
+ const session = await orch.startSession();
378
+ expect(orch.currentSession).toBe(session);
379
+ });
380
+ });
381
+ // =======================================================================
382
+ // Message Flow
383
+ // =======================================================================
384
+ describe("Message Flow", () => {
385
+ it("send yields events from provider", async () => {
386
+ const { orch } = await makeWiredOrchestrator();
387
+ await orch.startSession();
388
+ const events = await collectEvents(orch.send("Hello"));
389
+ const textEvents = events.filter((e) => e.type === "text");
390
+ const doneEvents = events.filter((e) => e.type === "done");
391
+ expect(textEvents.length).toBeGreaterThanOrEqual(1);
392
+ expect(doneEvents).toHaveLength(1);
393
+ });
394
+ it("send stores user message in session store", async () => {
395
+ const { orch } = await makeWiredOrchestrator();
396
+ await orch.startSession();
397
+ await collectEvents(orch.send("Test message"));
398
+ const messages = orch.sessionStore.getMessages(orch.activeSession.id);
399
+ expect(messages.some((m) => m.role === "user" && m.content === "Test message")).toBe(true);
400
+ });
401
+ it("send stores assistant response in session store", async () => {
402
+ const { orch } = await makeWiredOrchestrator();
403
+ await orch.startSession();
404
+ await collectEvents(orch.send("Hello"));
405
+ const messages = orch.sessionStore.getMessages(orch.activeSession.id);
406
+ expect(messages.some((m) => m.role === "assistant" && m.content === "Hello")).toBe(true);
407
+ });
408
+ it("send prepends sticky notes to message", async () => {
409
+ const { orch, provider } = await makeWiredOrchestrator();
410
+ await orch.startSession();
411
+ const stickies = new StickyManager();
412
+ stickies.add("Always use GPU");
413
+ orch.setStickyManager(stickies);
414
+ await collectEvents(orch.send("Train a model"));
415
+ // The provider.send should have been called with augmented message
416
+ const sendCall = provider.send.mock.calls[0];
417
+ const sentMessage = sendCall[1];
418
+ expect(sentMessage).toContain("STICKY NOTES");
419
+ expect(sentMessage).toContain("Always use GPU");
420
+ expect(sentMessage).toContain("Train a model");
421
+ });
422
+ it("send without stickies passes message unchanged", async () => {
423
+ const { orch, provider } = await makeWiredOrchestrator();
424
+ await orch.startSession();
425
+ await collectEvents(orch.send("Plain message"));
426
+ const sendCall = provider.send.mock.calls[0];
427
+ const sentMessage = sendCall[1];
428
+ expect(sentMessage).toBe("Plain message");
429
+ });
430
+ it("send tracks cost from done event", async () => {
431
+ const { orch } = await makeWiredOrchestrator();
432
+ await orch.startSession();
433
+ await collectEvents(orch.send("Hello"));
434
+ expect(orch.totalCostUsd).toBeCloseTo(0.01);
435
+ });
436
+ it("send tracks input tokens from done event", async () => {
437
+ const { orch } = await makeWiredOrchestrator();
438
+ await orch.startSession();
439
+ await collectEvents(orch.send("Hello"));
440
+ expect(orch.lastInputTokens).toBe(100);
441
+ });
442
+ it("send lock prevents concurrent sends", async () => {
443
+ const { orch, provider } = await makeWiredOrchestrator();
444
+ // Make send take a while
445
+ let resolveBarrier;
446
+ const barrier = new Promise((r) => (resolveBarrier = r));
447
+ provider.send.mockImplementation(async function* () {
448
+ await barrier;
449
+ yield { type: "text", text: "ok", delta: "ok" };
450
+ yield { type: "done", usage: { inputTokens: 10, outputTokens: 5, costUsd: 0 } };
451
+ });
452
+ await orch.startSession();
453
+ // Start first send (will block on barrier)
454
+ const gen1 = orch.send("First");
455
+ // Start iterating to acquire the lock
456
+ const p1 = gen1.next();
457
+ // Second send should throw
458
+ await expect(collectEvents(orch.send("Second"))).rejects.toThrow("Another message is already being processed");
459
+ // Clean up
460
+ resolveBarrier();
461
+ await p1;
462
+ await collectEvents(gen1);
463
+ });
464
+ it("send lock is released after completion", async () => {
465
+ const { orch } = await makeWiredOrchestrator();
466
+ await orch.startSession();
467
+ await collectEvents(orch.send("First"));
468
+ // Should not throw
469
+ await collectEvents(orch.send("Second"));
470
+ });
471
+ it("send lock is released after error", async () => {
472
+ const { orch, provider } = await makeWiredOrchestrator();
473
+ provider.send.mockImplementation(async function* () {
474
+ throw new Error("provider error");
475
+ });
476
+ await orch.startSession();
477
+ await expect(collectEvents(orch.send("Fail"))).rejects.toThrow("provider error");
478
+ // Restore working provider
479
+ provider.send.mockImplementation(async function* () {
480
+ yield { type: "text", text: "ok", delta: "ok" };
481
+ yield { type: "done", usage: { inputTokens: 10, outputTokens: 5, costUsd: 0 } };
482
+ });
483
+ // Lock should be released — next send should work
484
+ await collectEvents(orch.send("Recover"));
485
+ });
486
+ it("send auto-starts session if none active", async () => {
487
+ const orch = makeOrchestrator();
488
+ const provider = mockProvider("claude", orch.sessionStore);
489
+ orch.registerProvider(provider);
490
+ // No switchProvider, no startSession — send should handle it
491
+ const events = await collectEvents(orch.send("Auto start"));
492
+ expect(provider.authenticate).toHaveBeenCalled();
493
+ expect(provider.createSession).toHaveBeenCalled();
494
+ expect(events.some((e) => e.type === "done")).toBe(true);
495
+ });
496
+ it("send calls maybeCheckpoint after done", async () => {
497
+ const { orch, provider } = await makeWiredOrchestrator();
498
+ const mockGate = {
499
+ onSessionStart: vi.fn(),
500
+ checkThreshold: vi.fn().mockReturnValue(true),
501
+ performCheckpointWithGist: vi.fn().mockReturnValue("checkpoint briefing"),
502
+ };
503
+ orch.setContextGate(mockGate);
504
+ // Provider will be called twice: once for the user message, once for checkpoint gist
505
+ let callCount = 0;
506
+ provider.send.mockImplementation(async function* () {
507
+ callCount++;
508
+ if (callCount === 1) {
509
+ yield { type: "text", text: "response", delta: "response" };
510
+ yield { type: "done", usage: { inputTokens: 100, outputTokens: 50, costUsd: 0.01 } };
511
+ }
512
+ else {
513
+ // checkpoint gist generation
514
+ yield { type: "text", text: "gist content", delta: "gist content" };
515
+ yield { type: "done", usage: { inputTokens: 50, outputTokens: 20, costUsd: 0.005 } };
516
+ }
517
+ });
518
+ await orch.startSession();
519
+ const events = await collectEvents(orch.send("Hello"));
520
+ // Should have a checkpoint text event
521
+ expect(events.some((e) => e.type === "text" && e.text.includes("Context checkpoint"))).toBe(true);
522
+ expect(mockGate.checkThreshold).toHaveBeenCalled();
523
+ });
524
+ it("send passes tools and attachments to provider", async () => {
525
+ const { orch, provider } = await makeWiredOrchestrator();
526
+ await orch.startSession();
527
+ const tool = makeTool("my_tool");
528
+ orch.registerTool(tool);
529
+ const attachment = {
530
+ filename: "test.png",
531
+ mediaType: "image/png",
532
+ data: "base64data",
533
+ };
534
+ await collectEvents(orch.send("Use tool", [attachment]));
535
+ const sendCall = provider.send.mock.calls[0];
536
+ expect(sendCall[2]).toContain(tool); // tools
537
+ expect(sendCall[3]).toEqual([attachment]); // attachments
538
+ });
539
+ it("send accumulates cost from multiple messages", async () => {
540
+ const { orch } = await makeWiredOrchestrator();
541
+ await orch.startSession();
542
+ await collectEvents(orch.send("msg1")); // 0.01
543
+ await collectEvents(orch.send("msg2")); // 0.01
544
+ expect(orch.totalCostUsd).toBeCloseTo(0.02);
545
+ });
546
+ it("send updates lastActive on session store", async () => {
547
+ const { orch } = await makeWiredOrchestrator();
548
+ await orch.startSession();
549
+ const sessionId = orch.activeSession.id;
550
+ // Spy on updateLastActive
551
+ const spy = vi.spyOn(orch.sessionStore, "updateLastActive");
552
+ await collectEvents(orch.send("Hello"));
553
+ expect(spy).toHaveBeenCalledWith(sessionId);
554
+ });
555
+ it("send with empty stickies passes message unchanged", async () => {
556
+ const { orch, provider } = await makeWiredOrchestrator();
557
+ await orch.startSession();
558
+ // Set sticky manager with no notes
559
+ orch.setStickyManager(new StickyManager());
560
+ await collectEvents(orch.send("No stickies"));
561
+ const sendCall = provider.send.mock.calls[0];
562
+ expect(sendCall[1]).toBe("No stickies");
563
+ });
564
+ it("send builds full response from text deltas", async () => {
565
+ const { orch, provider } = await makeWiredOrchestrator();
566
+ provider.send.mockImplementation(async function* () {
567
+ yield { type: "text", text: "He", delta: "He" };
568
+ yield { type: "text", text: "llo", delta: "llo" };
569
+ yield { type: "done", usage: { inputTokens: 10, outputTokens: 5, costUsd: 0 } };
570
+ });
571
+ await orch.startSession();
572
+ await collectEvents(orch.send("Hi"));
573
+ const messages = orch.sessionStore.getMessages(orch.activeSession.id);
574
+ const assistantMsg = messages.find((m) => m.role === "assistant");
575
+ expect(assistantMsg.content).toBe("Hello");
576
+ });
577
+ it("send does not store empty assistant response", async () => {
578
+ const { orch, provider } = await makeWiredOrchestrator();
579
+ provider.send.mockImplementation(async function* () {
580
+ yield { type: "done", usage: { inputTokens: 10, outputTokens: 0, costUsd: 0 } };
581
+ });
582
+ await orch.startSession();
583
+ await collectEvents(orch.send("Hi"));
584
+ const messages = orch.sessionStore.getMessages(orch.activeSession.id);
585
+ expect(messages.filter((m) => m.role === "assistant")).toHaveLength(0);
586
+ });
587
+ });
588
+ // =======================================================================
589
+ // Tool Management
590
+ // =======================================================================
591
+ describe("Tool Management", () => {
592
+ it("registerTool adds tool", () => {
593
+ const orch = makeOrchestrator();
594
+ const tool = makeTool("remote_exec");
595
+ orch.registerTool(tool);
596
+ expect(orch.getTools()).toContain(tool);
597
+ });
598
+ it("registerTool deduplicates by name", () => {
599
+ const orch = makeOrchestrator();
600
+ const tool1 = makeTool("remote_exec");
601
+ const tool2 = makeTool("remote_exec");
602
+ orch.registerTool(tool1);
603
+ orch.registerTool(tool2);
604
+ expect(orch.getTools()).toHaveLength(1);
605
+ // First registration wins
606
+ expect(orch.getTools()[0]).toBe(tool1);
607
+ });
608
+ it("registerTools adds multiple tools", () => {
609
+ const orch = makeOrchestrator();
610
+ const tools = [makeTool("tool_a"), makeTool("tool_b"), makeTool("tool_c")];
611
+ orch.registerTools(tools);
612
+ expect(orch.getTools()).toHaveLength(3);
613
+ });
614
+ it("registerTools deduplicates", () => {
615
+ const orch = makeOrchestrator();
616
+ const tools = [
617
+ makeTool("tool_a"),
618
+ makeTool("tool_b"),
619
+ makeTool("tool_a"),
620
+ ];
621
+ orch.registerTools(tools);
622
+ expect(orch.getTools()).toHaveLength(2);
623
+ });
624
+ it("getTools returns all registered tools", () => {
625
+ const orch = makeOrchestrator();
626
+ orch.registerTool(makeTool("alpha"));
627
+ orch.registerTool(makeTool("beta"));
628
+ const tools = orch.getTools();
629
+ expect(tools).toHaveLength(2);
630
+ expect(tools.map((t) => t.name)).toEqual(["alpha", "beta"]);
631
+ });
632
+ it("getTools returns empty array when no tools registered", () => {
633
+ const orch = makeOrchestrator();
634
+ expect(orch.getTools()).toEqual([]);
635
+ });
636
+ it("registerTool after registerTools appends", () => {
637
+ const orch = makeOrchestrator();
638
+ orch.registerTools([makeTool("a"), makeTool("b")]);
639
+ orch.registerTool(makeTool("c"));
640
+ expect(orch.getTools()).toHaveLength(3);
641
+ });
642
+ it("registerTools preserves order", () => {
643
+ const orch = makeOrchestrator();
644
+ const tools = [makeTool("z"), makeTool("a"), makeTool("m")];
645
+ orch.registerTools(tools);
646
+ expect(orch.getTools().map((t) => t.name)).toEqual(["z", "a", "m"]);
647
+ });
648
+ });
649
+ // =======================================================================
650
+ // Cost Tracking
651
+ // =======================================================================
652
+ describe("Cost Tracking", () => {
653
+ it("addCost accumulates total", async () => {
654
+ const { orch } = await makeWiredOrchestrator();
655
+ await orch.startSession();
656
+ orch.addCost(0.05);
657
+ orch.addCost(0.03);
658
+ expect(orch.totalCostUsd).toBeCloseTo(0.08);
659
+ });
660
+ it("addCost persists to session store", async () => {
661
+ const { orch } = await makeWiredOrchestrator();
662
+ await orch.startSession();
663
+ const spy = vi.spyOn(orch.sessionStore, "addCost");
664
+ orch.addCost(0.01, 100, 50);
665
+ expect(spy).toHaveBeenCalledWith(orch.activeSession.id, 0.01, 100, 50);
666
+ });
667
+ it("addCost skips DB write for ephemeral sessions (eph- prefix)", async () => {
668
+ const orch = makeOrchestrator();
669
+ const provider = mockProvider("claude");
670
+ // Make provider return ephemeral-like session
671
+ provider.createSession.mockResolvedValue({
672
+ id: "eph-abc123",
673
+ providerId: "claude",
674
+ createdAt: Date.now(),
675
+ lastActiveAt: Date.now(),
676
+ });
677
+ orch.registerProvider(provider);
678
+ await orch.switchProvider("claude");
679
+ await orch.startSession();
680
+ const spy = vi.spyOn(orch.sessionStore, "addCost");
681
+ orch.addCost(0.01, 100, 50);
682
+ expect(spy).not.toHaveBeenCalled();
683
+ // But total still accumulates
684
+ expect(orch.totalCostUsd).toBeCloseTo(0.01);
685
+ });
686
+ it("totalCostUsd returns accumulated cost", () => {
687
+ const orch = makeOrchestrator();
688
+ expect(orch.totalCostUsd).toBe(0);
689
+ });
690
+ it("lastInputTokens returns last reported tokens", async () => {
691
+ const { orch } = await makeWiredOrchestrator();
692
+ await orch.startSession();
693
+ expect(orch.lastInputTokens).toBe(0);
694
+ await collectEvents(orch.send("Hello"));
695
+ expect(orch.lastInputTokens).toBe(100);
696
+ });
697
+ it("addCost with no tokens defaults to zero", async () => {
698
+ const { orch } = await makeWiredOrchestrator();
699
+ await orch.startSession();
700
+ const spy = vi.spyOn(orch.sessionStore, "addCost");
701
+ orch.addCost(0.01);
702
+ expect(spy).toHaveBeenCalledWith(orch.activeSession.id, 0.01, 0, 0);
703
+ });
704
+ it("addCost with no active session does not write to DB", () => {
705
+ const orch = makeOrchestrator();
706
+ const spy = vi.spyOn(orch.sessionStore, "addCost");
707
+ orch.addCost(0.01);
708
+ expect(spy).not.toHaveBeenCalled();
709
+ expect(orch.totalCostUsd).toBeCloseTo(0.01);
710
+ });
711
+ it("cost persists across multiple send calls", async () => {
712
+ const { orch } = await makeWiredOrchestrator();
713
+ await orch.startSession();
714
+ await collectEvents(orch.send("a"));
715
+ await collectEvents(orch.send("b"));
716
+ await collectEvents(orch.send("c"));
717
+ expect(orch.totalCostUsd).toBeCloseTo(0.03);
718
+ });
719
+ });
720
+ // =======================================================================
721
+ // Model & Reasoning
722
+ // =======================================================================
723
+ describe("Model & Reasoning", () => {
724
+ it("setModel changes provider model", async () => {
725
+ const orch = makeOrchestrator();
726
+ const provider = mockProvider("claude");
727
+ orch.registerProvider(provider);
728
+ await orch.switchProvider("claude");
729
+ await orch.setModel("claude-sonnet-4-20250514");
730
+ expect(provider.currentModel).toBe("claude-sonnet-4-20250514");
731
+ });
732
+ it("setModel closes current session (force new session)", async () => {
733
+ const orch = makeOrchestrator();
734
+ const provider = mockProvider("claude");
735
+ orch.registerProvider(provider);
736
+ await orch.switchProvider("claude");
737
+ const session = await orch.startSession();
738
+ await orch.setModel("claude-sonnet-4-20250514");
739
+ expect(provider.closeSession).toHaveBeenCalledWith(session);
740
+ expect(orch.activeSession).toBeNull();
741
+ });
742
+ it("setModel auto-switches provider if none active", async () => {
743
+ const orch = makeOrchestrator();
744
+ const provider = mockProvider("claude");
745
+ orch.registerProvider(provider);
746
+ await orch.setModel("new-model");
747
+ expect(provider.authenticate).toHaveBeenCalled();
748
+ expect(provider.currentModel).toBe("new-model");
749
+ });
750
+ it("setReasoningEffort updates provider", async () => {
751
+ const orch = makeOrchestrator();
752
+ const provider = mockProvider("claude");
753
+ orch.registerProvider(provider);
754
+ await orch.switchProvider("claude");
755
+ await orch.setReasoningEffort("high");
756
+ expect(provider.reasoningEffort).toBe("high");
757
+ });
758
+ it("setReasoningEffort auto-switches provider if none active", async () => {
759
+ const orch = makeOrchestrator();
760
+ const provider = mockProvider("claude");
761
+ orch.registerProvider(provider);
762
+ await orch.setReasoningEffort("max");
763
+ expect(provider.authenticate).toHaveBeenCalled();
764
+ expect(provider.reasoningEffort).toBe("max");
765
+ });
766
+ it("fetchModels delegates to provider", async () => {
767
+ const orch = makeOrchestrator();
768
+ const provider = mockProvider("claude");
769
+ orch.registerProvider(provider);
770
+ await orch.switchProvider("claude");
771
+ const models = await orch.fetchModels();
772
+ expect(provider.fetchModels).toHaveBeenCalledOnce();
773
+ expect(models).toEqual([{ id: "test-model", name: "Test Model" }]);
774
+ });
775
+ it("fetchModels returns current model if fetchModels not implemented", async () => {
776
+ const orch = makeOrchestrator();
777
+ const provider = mockProvider("claude");
778
+ delete provider.fetchModels;
779
+ orch.registerProvider(provider);
780
+ await orch.switchProvider("claude");
781
+ const models = await orch.fetchModels();
782
+ expect(models).toEqual([
783
+ { id: "claude-opus-4-6", name: "claude-opus-4-6" },
784
+ ]);
785
+ });
786
+ it("currentModel returns provider's model", async () => {
787
+ const orch = makeOrchestrator();
788
+ const provider = mockProvider("claude");
789
+ orch.registerProvider(provider);
790
+ await orch.switchProvider("claude");
791
+ expect(orch.currentModel).toBe("claude-opus-4-6");
792
+ });
793
+ it("currentModel returns null when no provider active", () => {
794
+ const orch = makeOrchestrator();
795
+ expect(orch.currentModel).toBeNull();
796
+ });
797
+ it("reasoningEffort returns provider's effort", async () => {
798
+ const orch = makeOrchestrator();
799
+ const provider = mockProvider("claude");
800
+ orch.registerProvider(provider);
801
+ await orch.switchProvider("claude");
802
+ expect(orch.reasoningEffort).toBe("medium");
803
+ });
804
+ it("reasoningEffort returns null when no provider active", () => {
805
+ const orch = makeOrchestrator();
806
+ expect(orch.reasoningEffort).toBeNull();
807
+ });
808
+ });
809
+ // =======================================================================
810
+ // Interrupt
811
+ // =======================================================================
812
+ describe("Interrupt", () => {
813
+ it("interrupt calls provider interrupt", async () => {
814
+ const orch = makeOrchestrator();
815
+ const provider = mockProvider("claude");
816
+ orch.registerProvider(provider);
817
+ await orch.switchProvider("claude");
818
+ const session = await orch.startSession();
819
+ orch.interrupt();
820
+ expect(provider.interrupt).toHaveBeenCalledWith(session);
821
+ });
822
+ it("interrupt does nothing without active session", () => {
823
+ const orch = makeOrchestrator();
824
+ const provider = mockProvider("claude");
825
+ orch.registerProvider(provider);
826
+ // Should not throw
827
+ orch.interrupt();
828
+ expect(provider.interrupt).not.toHaveBeenCalled();
829
+ });
830
+ it("interrupt aborts secondary controller", async () => {
831
+ const orch = makeOrchestrator();
832
+ const provider = mockProvider("claude");
833
+ orch.registerProvider(provider);
834
+ await orch.switchProvider("claude");
835
+ await orch.startSession();
836
+ const controller = new AbortController();
837
+ orch.setActiveAbort(controller);
838
+ orch.interrupt();
839
+ expect(controller.signal.aborted).toBe(true);
840
+ });
841
+ it("setActiveAbort registers controller", async () => {
842
+ const orch = makeOrchestrator();
843
+ const provider = mockProvider("claude");
844
+ orch.registerProvider(provider);
845
+ await orch.switchProvider("claude");
846
+ await orch.startSession();
847
+ const controller = new AbortController();
848
+ orch.setActiveAbort(controller);
849
+ orch.interrupt();
850
+ expect(controller.signal.aborted).toBe(true);
851
+ });
852
+ it("interrupt clears the active abort controller", async () => {
853
+ const orch = makeOrchestrator();
854
+ const provider = mockProvider("claude");
855
+ orch.registerProvider(provider);
856
+ await orch.switchProvider("claude");
857
+ await orch.startSession();
858
+ const controller1 = new AbortController();
859
+ orch.setActiveAbort(controller1);
860
+ orch.interrupt();
861
+ // Register a second controller — first interrupt should have cleared
862
+ const controller2 = new AbortController();
863
+ orch.setActiveAbort(controller2);
864
+ // First controller stays aborted, second is not yet aborted
865
+ expect(controller1.signal.aborted).toBe(true);
866
+ expect(controller2.signal.aborted).toBe(false);
867
+ });
868
+ it("setActiveAbort with null clears controller", async () => {
869
+ const orch = makeOrchestrator();
870
+ const provider = mockProvider("claude");
871
+ orch.registerProvider(provider);
872
+ await orch.switchProvider("claude");
873
+ await orch.startSession();
874
+ const controller = new AbortController();
875
+ orch.setActiveAbort(controller);
876
+ orch.setActiveAbort(null);
877
+ orch.interrupt();
878
+ // Controller should NOT be aborted since it was cleared
879
+ expect(controller.signal.aborted).toBe(false);
880
+ });
881
+ });
882
+ // =======================================================================
883
+ // Context Gate integration
884
+ // =======================================================================
885
+ describe("Context Gate", () => {
886
+ it("setContextGate stores the gate", () => {
887
+ const orch = makeOrchestrator();
888
+ const mockGate = { onSessionStart: vi.fn() };
889
+ orch.setContextGate(mockGate);
890
+ expect(orch.contextGate).toBe(mockGate);
891
+ });
892
+ it("contextGate returns null by default", () => {
893
+ const orch = makeOrchestrator();
894
+ expect(orch.contextGate).toBeNull();
895
+ });
896
+ it("maybeCheckpoint skips when no context gate", async () => {
897
+ const { orch } = await makeWiredOrchestrator();
898
+ await orch.startSession();
899
+ // Should not throw or emit checkpoint events
900
+ const events = await collectEvents(orch.send("Hello"));
901
+ expect(events.some((e) => e.type === "text" &&
902
+ e.text.includes("Context checkpoint"))).toBe(false);
903
+ });
904
+ it("maybeCheckpoint skips when threshold not reached", async () => {
905
+ const { orch } = await makeWiredOrchestrator();
906
+ const mockGate = {
907
+ onSessionStart: vi.fn(),
908
+ checkThreshold: vi.fn().mockReturnValue(false),
909
+ performCheckpointWithGist: vi.fn(),
910
+ };
911
+ orch.setContextGate(mockGate);
912
+ await orch.startSession();
913
+ const events = await collectEvents(orch.send("Hello"));
914
+ expect(mockGate.checkThreshold).toHaveBeenCalled();
915
+ expect(mockGate.performCheckpointWithGist).not.toHaveBeenCalled();
916
+ });
917
+ });
918
+ // =======================================================================
919
+ // Sticky Manager
920
+ // =======================================================================
921
+ describe("Sticky Manager", () => {
922
+ it("setStickyManager stores the manager", () => {
923
+ const orch = makeOrchestrator();
924
+ const stickies = new StickyManager();
925
+ orch.setStickyManager(stickies);
926
+ // Verify it's used in send — if setStickyManager didn't store it,
927
+ // the augmented message test above would fail
928
+ });
929
+ });
930
+ // =======================================================================
931
+ // State Machine
932
+ // =======================================================================
933
+ describe("State Machine", () => {
934
+ it("stateMachine is accessible", () => {
935
+ const orch = makeOrchestrator();
936
+ expect(orch.stateMachine).toBeTruthy();
937
+ expect(orch.stateMachine.state).toBe("idle");
938
+ });
939
+ it("currentState reflects state machine", async () => {
940
+ const orch = makeOrchestrator();
941
+ expect(orch.currentState).toBe("idle");
942
+ orch.registerProvider(mockProvider("claude"));
943
+ await orch.switchProvider("claude");
944
+ await orch.startSession();
945
+ expect(orch.currentState).toBe("active");
946
+ });
947
+ it("startSession does not transition if already active", async () => {
948
+ const orch = makeOrchestrator();
949
+ orch.registerProvider(mockProvider("claude"));
950
+ await orch.switchProvider("claude");
951
+ await orch.startSession();
952
+ expect(orch.currentState).toBe("active");
953
+ // Starting another session should not throw (already active)
954
+ await orch.startSession();
955
+ expect(orch.currentState).toBe("active");
956
+ });
957
+ });
958
+ // =======================================================================
959
+ // Config
960
+ // =======================================================================
961
+ describe("Config", () => {
962
+ it("config is accessible", () => {
963
+ const orch = makeOrchestrator();
964
+ expect(orch.config.defaultProvider).toBe("claude");
965
+ expect(orch.config.systemPrompt).toBe("You are a test agent.");
966
+ });
967
+ it("sessionStore is accessible", () => {
968
+ const orch = makeOrchestrator();
969
+ expect(orch.sessionStore).toBeInstanceOf(SessionStore);
970
+ });
971
+ it("uses provided sessionStore", () => {
972
+ const store = new SessionStore("custom");
973
+ const orch = new Orchestrator({
974
+ defaultProvider: "claude",
975
+ systemPrompt: "test",
976
+ sessionStore: store,
977
+ });
978
+ expect(orch.sessionStore).toBe(store);
979
+ });
980
+ });
981
+ });
982
+ //# sourceMappingURL=orchestrator.test.js.map