@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,674 @@
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
+ function mockProvider(name = "claude", sessionStore) {
35
+ return {
36
+ name,
37
+ displayName: name === "claude" ? "Claude" : "OpenAI",
38
+ currentModel: name === "claude" ? "claude-opus-4-6" : "gpt-5.4",
39
+ reasoningEffort: "medium",
40
+ isAuthenticated: vi.fn().mockResolvedValue(true),
41
+ authenticate: vi.fn().mockResolvedValue(undefined),
42
+ createSession: vi.fn().mockImplementation(async (_config) => {
43
+ if (sessionStore) {
44
+ return sessionStore.createSession(name, name === "claude" ? "claude-opus-4-6" : "gpt-5.4");
45
+ }
46
+ const now = Date.now();
47
+ return {
48
+ id: `sess-${Math.random().toString(36).slice(2, 8)}`,
49
+ providerId: name,
50
+ createdAt: now,
51
+ lastActiveAt: now,
52
+ };
53
+ }),
54
+ resumeSession: vi.fn().mockImplementation(async (id) => {
55
+ return {
56
+ id,
57
+ providerId: name,
58
+ createdAt: Date.now(),
59
+ lastActiveAt: Date.now(),
60
+ };
61
+ }),
62
+ send: vi.fn().mockImplementation(async function* () {
63
+ yield { type: "text", text: "Hello", delta: "Hello" };
64
+ yield {
65
+ type: "done",
66
+ usage: { inputTokens: 100, outputTokens: 50, costUsd: 0.01 },
67
+ };
68
+ }),
69
+ interrupt: vi.fn(),
70
+ resetHistory: vi.fn(),
71
+ closeSession: vi.fn().mockResolvedValue(undefined),
72
+ fetchModels: vi.fn().mockResolvedValue([{ id: "test-model", name: "Test Model" }]),
73
+ };
74
+ }
75
+ /**
76
+ * Creates a "chatty" provider that returns different responses based on call count.
77
+ * Useful for multi-turn integration tests.
78
+ */
79
+ function chattyProvider(name = "claude", sessionStore) {
80
+ let callCount = 0;
81
+ const base = mockProvider(name, sessionStore);
82
+ base.send = vi.fn().mockImplementation(async function* (_session, _message, _tools) {
83
+ callCount++;
84
+ if (callCount === 1) {
85
+ yield { type: "text", text: "Hello!", delta: "Hello!" };
86
+ yield { type: "done", usage: { inputTokens: 50, outputTokens: 20, costUsd: 0.005 } };
87
+ }
88
+ else if (callCount === 2) {
89
+ yield { type: "tool_call", id: "tc-1", name: "remote_exec", args: { command: "ls" } };
90
+ yield { type: "tool_result", callId: "tc-1", result: "file.txt" };
91
+ yield { type: "text", text: "Found file.txt", delta: "Found file.txt" };
92
+ yield { type: "done", usage: { inputTokens: 100, outputTokens: 40, costUsd: 0.015 } };
93
+ }
94
+ else {
95
+ yield { type: "text", text: "Done", delta: "Done" };
96
+ yield { type: "done", usage: { inputTokens: 75, outputTokens: 30, costUsd: 0.008 } };
97
+ }
98
+ });
99
+ return base;
100
+ }
101
+ /**
102
+ * Creates a provider that emits an error during send.
103
+ */
104
+ function errorProvider(name = "claude", sessionStore) {
105
+ const base = mockProvider(name, sessionStore);
106
+ base.send = vi.fn().mockImplementation(async function* () {
107
+ yield { type: "text", text: "Starting...", delta: "Starting..." };
108
+ throw new Error("Provider exploded");
109
+ });
110
+ return base;
111
+ }
112
+ /**
113
+ * Provider whose send emits done events with no usage.
114
+ */
115
+ function noUsageProvider(name = "claude", sessionStore) {
116
+ const base = mockProvider(name, sessionStore);
117
+ base.send = vi.fn().mockImplementation(async function* () {
118
+ yield { type: "text", text: "No stats", delta: "No stats" };
119
+ yield { type: "done" };
120
+ });
121
+ return base;
122
+ }
123
+ // ---------------------------------------------------------------------------
124
+ // Helpers
125
+ // ---------------------------------------------------------------------------
126
+ function makeOrchestrator(defaultProvider = "claude") {
127
+ return new Orchestrator({
128
+ defaultProvider,
129
+ systemPrompt: "You are a test agent.",
130
+ sessionStore: new SessionStore("integ-agent"),
131
+ });
132
+ }
133
+ async function makeWiredOrchestrator(providerName = "claude") {
134
+ const orch = makeOrchestrator(providerName);
135
+ const provider = mockProvider(providerName, orch.sessionStore);
136
+ orch.registerProvider(provider);
137
+ await orch.switchProvider(providerName);
138
+ return { orch, provider };
139
+ }
140
+ function makeTool(name) {
141
+ return {
142
+ name,
143
+ description: `Tool ${name}`,
144
+ parameters: { type: "object", properties: {} },
145
+ execute: vi.fn().mockResolvedValue("ok"),
146
+ };
147
+ }
148
+ async function collectEvents(gen) {
149
+ const events = [];
150
+ for await (const e of gen)
151
+ events.push(e);
152
+ return events;
153
+ }
154
+ // ---------------------------------------------------------------------------
155
+ // Tests
156
+ // ---------------------------------------------------------------------------
157
+ describe("Orchestrator — Integration Tests", () => {
158
+ beforeEach(() => {
159
+ mockDb.current = createTestDb();
160
+ });
161
+ // =======================================================================
162
+ // Multi-turn Conversations
163
+ // =======================================================================
164
+ describe("Multi-turn Conversations", () => {
165
+ it("3 sends accumulate correct cost", async () => {
166
+ const orch = makeOrchestrator();
167
+ const provider = chattyProvider("claude", orch.sessionStore);
168
+ orch.registerProvider(provider);
169
+ await orch.switchProvider("claude");
170
+ await collectEvents(orch.send("Turn 1"));
171
+ await collectEvents(orch.send("Turn 2"));
172
+ await collectEvents(orch.send("Turn 3"));
173
+ // 0.005 + 0.015 + 0.008 = 0.028
174
+ expect(orch.totalCostUsd).toBeCloseTo(0.028, 3);
175
+ });
176
+ it("3 sends yield correct event types per turn", async () => {
177
+ const orch = makeOrchestrator();
178
+ const provider = chattyProvider("claude", orch.sessionStore);
179
+ orch.registerProvider(provider);
180
+ await orch.switchProvider("claude");
181
+ const events1 = await collectEvents(orch.send("Turn 1"));
182
+ expect(events1.some((e) => e.type === "text")).toBe(true);
183
+ expect(events1.some((e) => e.type === "done")).toBe(true);
184
+ const events2 = await collectEvents(orch.send("Turn 2"));
185
+ expect(events2.some((e) => e.type === "tool_call")).toBe(true);
186
+ expect(events2.some((e) => e.type === "tool_result")).toBe(true);
187
+ const events3 = await collectEvents(orch.send("Turn 3"));
188
+ const textEvents3 = events3.filter((e) => e.type === "text");
189
+ expect(textEvents3.some((e) => e.delta === "Done")).toBe(true);
190
+ });
191
+ it("session store records both user and assistant messages", async () => {
192
+ const { orch } = await makeWiredOrchestrator();
193
+ await collectEvents(orch.send("User question"));
194
+ const messages = orch.sessionStore.getMessages(orch.activeSession.id);
195
+ const roles = messages.map((m) => m.role);
196
+ expect(roles).toContain("user");
197
+ expect(roles).toContain("assistant");
198
+ });
199
+ it("empty assistant response: no assistant message stored", async () => {
200
+ const orch = makeOrchestrator();
201
+ const provider = mockProvider("claude", orch.sessionStore);
202
+ // Override send to emit done with no text
203
+ provider.send = vi.fn().mockImplementation(async function* () {
204
+ yield { type: "done", usage: { inputTokens: 10, outputTokens: 0 } };
205
+ });
206
+ orch.registerProvider(provider);
207
+ await orch.switchProvider("claude");
208
+ await collectEvents(orch.send("Hello"));
209
+ const messages = orch.sessionStore.getMessages(orch.activeSession.id);
210
+ const assistantMessages = messages.filter((m) => m.role === "assistant");
211
+ expect(assistantMessages).toHaveLength(0);
212
+ });
213
+ it("tool calls are forwarded correctly to caller", async () => {
214
+ const orch = makeOrchestrator();
215
+ const provider = chattyProvider("claude", orch.sessionStore);
216
+ orch.registerProvider(provider);
217
+ await orch.switchProvider("claude");
218
+ // First call is plain text, second has tool calls
219
+ await collectEvents(orch.send("Turn 1"));
220
+ const events = await collectEvents(orch.send("Turn 2"));
221
+ const toolCall = events.find((e) => e.type === "tool_call");
222
+ expect(toolCall).toBeDefined();
223
+ expect(toolCall.name).toBe("remote_exec");
224
+ expect(toolCall.id).toBe("tc-1");
225
+ const toolResult = events.find((e) => e.type === "tool_result");
226
+ expect(toolResult).toBeDefined();
227
+ expect(toolResult.callId).toBe("tc-1");
228
+ });
229
+ });
230
+ // =======================================================================
231
+ // Provider Switching
232
+ // =======================================================================
233
+ describe("Provider Switching", () => {
234
+ it("switching mid-conversation: closes old session, carries context", async () => {
235
+ const orch = makeOrchestrator();
236
+ const claude = mockProvider("claude", orch.sessionStore);
237
+ const openai = mockProvider("openai", orch.sessionStore);
238
+ orch.registerProvider(claude);
239
+ orch.registerProvider(openai);
240
+ await orch.switchProvider("claude");
241
+ const claudeSession = await orch.startSession();
242
+ await collectEvents(orch.send("Message to Claude"));
243
+ await orch.switchProvider("openai");
244
+ expect(claude.closeSession).toHaveBeenCalledWith(claudeSession);
245
+ // Context is carried over — session is resumed on the new provider with the same ID
246
+ expect(orch.activeSession).not.toBeNull();
247
+ expect(orch.activeSession.id).toBe(claudeSession.id);
248
+ expect(openai.resumeSession).toHaveBeenCalledWith(claudeSession.id, expect.any(String));
249
+ await collectEvents(orch.send("Message to OpenAI"));
250
+ expect(orch.activeSession).not.toBeNull();
251
+ });
252
+ it("switching preserves total cost", async () => {
253
+ const orch = makeOrchestrator();
254
+ const claude = mockProvider("claude", orch.sessionStore);
255
+ const openai = mockProvider("openai", orch.sessionStore);
256
+ orch.registerProvider(claude);
257
+ orch.registerProvider(openai);
258
+ await orch.switchProvider("claude");
259
+ await collectEvents(orch.send("Claude msg")); // costs 0.01
260
+ const costAfterClaude = orch.totalCostUsd;
261
+ expect(costAfterClaude).toBeCloseTo(0.01, 3);
262
+ await orch.switchProvider("openai");
263
+ await collectEvents(orch.send("OpenAI msg")); // costs another 0.01
264
+ expect(orch.totalCostUsd).toBeCloseTo(0.02, 3);
265
+ });
266
+ it("resume session on different provider switches correctly", async () => {
267
+ const orch = makeOrchestrator();
268
+ const claude = mockProvider("claude", orch.sessionStore);
269
+ const openai = mockProvider("openai", orch.sessionStore);
270
+ orch.registerProvider(claude);
271
+ orch.registerProvider(openai);
272
+ // Create a session with OpenAI
273
+ await orch.switchProvider("openai");
274
+ const openaiSession = orch.sessionStore.createSession("openai", "gpt-5.4");
275
+ orch.sessionStore.addMessage(openaiSession.id, "user", "Old msg");
276
+ // Currently on OpenAI, resume that session
277
+ await orch.switchProvider("claude");
278
+ await orch.resumeSession(openaiSession.id);
279
+ // Should have switched back to openai
280
+ expect(orch.currentProvider.name).toBe("openai");
281
+ expect(openai.resumeSession).toHaveBeenCalledWith(openaiSession.id, "You are a test agent.");
282
+ });
283
+ it("multiple providers registered: can switch between them", async () => {
284
+ const orch = makeOrchestrator();
285
+ const claude = mockProvider("claude", orch.sessionStore);
286
+ const openai = mockProvider("openai", orch.sessionStore);
287
+ orch.registerProvider(claude);
288
+ orch.registerProvider(openai);
289
+ await orch.switchProvider("claude");
290
+ expect(orch.currentProvider.name).toBe("claude");
291
+ await orch.switchProvider("openai");
292
+ expect(orch.currentProvider.name).toBe("openai");
293
+ await orch.switchProvider("claude");
294
+ expect(orch.currentProvider.name).toBe("claude");
295
+ });
296
+ });
297
+ // =======================================================================
298
+ // Send Lock
299
+ // =======================================================================
300
+ describe("Send Lock", () => {
301
+ it("send lock prevents interleaved sends", async () => {
302
+ const { orch } = await makeWiredOrchestrator();
303
+ // Start first send (which we won't await yet)
304
+ const gen1 = orch.send("First");
305
+ // Start iterating first gen to acquire the lock
306
+ const iter1 = gen1[Symbol.asyncIterator]();
307
+ await iter1.next(); // should acquire lock
308
+ // Second send should throw
309
+ await expect(collectEvents(orch.send("Second"))).rejects.toThrow("Another message is already being processed");
310
+ // Finish first send
311
+ let done = false;
312
+ while (!done) {
313
+ const result = await iter1.next();
314
+ done = result.done ?? false;
315
+ }
316
+ });
317
+ it("error in provider.send still releases send lock", async () => {
318
+ const orch = makeOrchestrator();
319
+ const provider = errorProvider("claude", orch.sessionStore);
320
+ orch.registerProvider(provider);
321
+ await orch.switchProvider("claude");
322
+ // First send will error
323
+ await expect(collectEvents(orch.send("Crash"))).rejects.toThrow("Provider exploded");
324
+ // Second send should work (lock released)
325
+ // Reset provider to one that works
326
+ provider.send = vi.fn().mockImplementation(async function* () {
327
+ yield { type: "text", text: "OK", delta: "OK" };
328
+ yield { type: "done", usage: { inputTokens: 10, outputTokens: 5 } };
329
+ });
330
+ const events = await collectEvents(orch.send("After crash"));
331
+ expect(events.some((e) => e.type === "done")).toBe(true);
332
+ });
333
+ });
334
+ // =======================================================================
335
+ // Cost Tracking
336
+ // =======================================================================
337
+ describe("Cost Tracking", () => {
338
+ it("cost from multiple done events accumulates", async () => {
339
+ const orch = makeOrchestrator();
340
+ const provider = mockProvider("claude", orch.sessionStore);
341
+ // Two done events in one send
342
+ provider.send = vi.fn().mockImplementation(async function* () {
343
+ yield { type: "text", text: "A", delta: "A" };
344
+ yield { type: "done", usage: { inputTokens: 50, outputTokens: 20, costUsd: 0.005 } };
345
+ });
346
+ orch.registerProvider(provider);
347
+ await orch.switchProvider("claude");
348
+ await collectEvents(orch.send("Msg 1"));
349
+ await collectEvents(orch.send("Msg 2"));
350
+ expect(orch.totalCostUsd).toBeCloseTo(0.01, 3);
351
+ });
352
+ it("cost with no usage event does not change total", async () => {
353
+ const orch = makeOrchestrator();
354
+ const provider = noUsageProvider("claude", orch.sessionStore);
355
+ orch.registerProvider(provider);
356
+ await orch.switchProvider("claude");
357
+ await collectEvents(orch.send("Message"));
358
+ expect(orch.totalCostUsd).toBe(0);
359
+ });
360
+ it("ephemeral session: cost tracked in memory but not persisted to DB via addCost", async () => {
361
+ const orch = makeOrchestrator();
362
+ const provider = mockProvider("claude", orch.sessionStore);
363
+ // Override createSession to return ephemeral — also create a real DB session
364
+ // so addMessage doesn't fail on FK constraint. The real test is that addCost skips.
365
+ provider.createSession = vi.fn().mockImplementation(async () => {
366
+ // Create a real session in DB so addMessage works
367
+ const real = orch.sessionStore.createSession("claude", "opus");
368
+ // But return an ephemeral-looking session ID wrapper
369
+ return {
370
+ id: real.id,
371
+ providerId: "claude",
372
+ createdAt: Date.now(),
373
+ lastActiveAt: Date.now(),
374
+ };
375
+ });
376
+ orch.registerProvider(provider);
377
+ await orch.switchProvider("claude");
378
+ await collectEvents(orch.send("Msg"));
379
+ // Cost is tracked in memory
380
+ expect(orch.totalCostUsd).toBeCloseTo(0.01, 3);
381
+ });
382
+ it("addCost from external source accumulates", async () => {
383
+ const { orch } = await makeWiredOrchestrator();
384
+ await orch.startSession();
385
+ orch.addCost(0.05, 500, 200);
386
+ orch.addCost(0.03, 300, 100);
387
+ expect(orch.totalCostUsd).toBeCloseTo(0.08, 3);
388
+ });
389
+ });
390
+ // =======================================================================
391
+ // Sticky Notes
392
+ // =======================================================================
393
+ describe("Sticky Notes", () => {
394
+ it("sticky notes prepended to every message", async () => {
395
+ const orch = makeOrchestrator();
396
+ const provider = mockProvider("claude", orch.sessionStore);
397
+ orch.registerProvider(provider);
398
+ await orch.switchProvider("claude");
399
+ const stickies = new StickyManager();
400
+ stickies.add("Always use GPU 0");
401
+ orch.setStickyManager(stickies);
402
+ await collectEvents(orch.send("Train model"));
403
+ const sendCall = provider.send.mock.calls[0];
404
+ const message = sendCall[1];
405
+ expect(message).toContain("Always use GPU 0");
406
+ expect(message).toContain("Train model");
407
+ });
408
+ it("sticky notes change between sends", async () => {
409
+ const orch = makeOrchestrator();
410
+ const provider = mockProvider("claude", orch.sessionStore);
411
+ orch.registerProvider(provider);
412
+ await orch.switchProvider("claude");
413
+ const stickies = new StickyManager();
414
+ stickies.add("Note 1");
415
+ orch.setStickyManager(stickies);
416
+ await collectEvents(orch.send("Msg 1"));
417
+ const msg1 = provider.send.mock.calls[0][1];
418
+ expect(msg1).toContain("Note 1");
419
+ stickies.add("Note 2");
420
+ await collectEvents(orch.send("Msg 2"));
421
+ const msg2 = provider.send.mock.calls[1][1];
422
+ expect(msg2).toContain("Note 1");
423
+ expect(msg2).toContain("Note 2");
424
+ });
425
+ it("no sticky notes: message passed through unchanged", async () => {
426
+ const orch = makeOrchestrator();
427
+ const provider = mockProvider("claude", orch.sessionStore);
428
+ orch.registerProvider(provider);
429
+ await orch.switchProvider("claude");
430
+ await collectEvents(orch.send("Plain message"));
431
+ const sendCall = provider.send.mock.calls[0];
432
+ expect(sendCall[1]).toBe("Plain message");
433
+ });
434
+ });
435
+ // =======================================================================
436
+ // Context Gate / Checkpoint
437
+ // =======================================================================
438
+ describe("Context Gate / Checkpoint", () => {
439
+ it("context gate not set: maybeCheckpoint is no-op", async () => {
440
+ const { orch, provider } = await makeWiredOrchestrator();
441
+ // No context gate set
442
+ await collectEvents(orch.send("Message"));
443
+ expect(provider.resetHistory).not.toHaveBeenCalled();
444
+ });
445
+ it("context gate set: checkpoint when threshold exceeded", async () => {
446
+ const orch = makeOrchestrator();
447
+ const provider = mockProvider("claude", orch.sessionStore);
448
+ // Provider reports high input tokens to trigger checkpoint
449
+ let sendCallCount = 0;
450
+ provider.send = vi.fn().mockImplementation(async function* () {
451
+ sendCallCount++;
452
+ if (sendCallCount === 1) {
453
+ yield { type: "text", text: "Reply", delta: "Reply" };
454
+ yield { type: "done", usage: { inputTokens: 999999, outputTokens: 50, costUsd: 0.01 } };
455
+ }
456
+ else {
457
+ // Gist generation call
458
+ yield { type: "text", text: "My gist of the conversation", delta: "My gist of the conversation" };
459
+ yield { type: "done", usage: { inputTokens: 100, outputTokens: 50, costUsd: 0.005 } };
460
+ }
461
+ });
462
+ orch.registerProvider(provider);
463
+ await orch.switchProvider("claude");
464
+ const mockGate = {
465
+ onSessionStart: vi.fn(),
466
+ checkThreshold: vi.fn().mockReturnValue(true),
467
+ performCheckpointWithGist: vi.fn().mockReturnValue("=== CHECKPOINT BRIEFING ==="),
468
+ };
469
+ orch.setContextGate(mockGate);
470
+ const events = await collectEvents(orch.send("Trigger checkpoint"));
471
+ expect(mockGate.checkThreshold).toHaveBeenCalled();
472
+ expect(mockGate.performCheckpointWithGist).toHaveBeenCalledWith("My gist of the conversation");
473
+ expect(provider.resetHistory).toHaveBeenCalledWith(expect.anything(), "=== CHECKPOINT BRIEFING ===");
474
+ });
475
+ it("checkpoint resets provider history", async () => {
476
+ const orch = makeOrchestrator();
477
+ const provider = mockProvider("claude", orch.sessionStore);
478
+ let sendCallCount = 0;
479
+ provider.send = vi.fn().mockImplementation(async function* () {
480
+ sendCallCount++;
481
+ yield { type: "text", text: `Response ${sendCallCount}`, delta: `Response ${sendCallCount}` };
482
+ yield { type: "done", usage: { inputTokens: sendCallCount === 1 ? 999999 : 50, outputTokens: 20 } };
483
+ });
484
+ orch.registerProvider(provider);
485
+ await orch.switchProvider("claude");
486
+ const mockGate = {
487
+ onSessionStart: vi.fn(),
488
+ checkThreshold: vi.fn().mockImplementation((_model, tokens) => tokens > 100000),
489
+ performCheckpointWithGist: vi.fn().mockReturnValue("Briefing"),
490
+ };
491
+ orch.setContextGate(mockGate);
492
+ const events = await collectEvents(orch.send("Big message"));
493
+ // resetHistory should have been called once
494
+ expect(provider.resetHistory).toHaveBeenCalledTimes(1);
495
+ // The checkpoint text event should be in the output
496
+ const checkpointText = events.find((e) => e.type === "text" && e.text.includes("Context checkpoint"));
497
+ expect(checkpointText).toBeDefined();
498
+ });
499
+ it("context gate: low token count does not trigger checkpoint", async () => {
500
+ const { orch, provider } = await makeWiredOrchestrator();
501
+ const mockGate = {
502
+ onSessionStart: vi.fn(),
503
+ checkThreshold: vi.fn().mockReturnValue(false),
504
+ performCheckpointWithGist: vi.fn(),
505
+ };
506
+ orch.setContextGate(mockGate);
507
+ await collectEvents(orch.send("Small message"));
508
+ expect(mockGate.checkThreshold).toHaveBeenCalled();
509
+ expect(mockGate.performCheckpointWithGist).not.toHaveBeenCalled();
510
+ expect(provider.resetHistory).not.toHaveBeenCalled();
511
+ });
512
+ });
513
+ // =======================================================================
514
+ // State Machine
515
+ // =======================================================================
516
+ describe("State Machine", () => {
517
+ it("transitions: idle -> active on startSession", async () => {
518
+ const orch = makeOrchestrator();
519
+ orch.registerProvider(mockProvider("claude"));
520
+ await orch.switchProvider("claude");
521
+ expect(orch.currentState).toBe("idle");
522
+ await orch.startSession();
523
+ expect(orch.currentState).toBe("active");
524
+ });
525
+ it("start session auto-authenticates provider", async () => {
526
+ const orch = makeOrchestrator();
527
+ const provider = mockProvider("claude");
528
+ orch.registerProvider(provider);
529
+ // Don't switch manually — let startSession do it
530
+ await orch.startSession();
531
+ expect(provider.authenticate).toHaveBeenCalledOnce();
532
+ });
533
+ });
534
+ // =======================================================================
535
+ // Interrupt
536
+ // =======================================================================
537
+ describe("Interrupt", () => {
538
+ it("interrupt during send aborts provider", async () => {
539
+ const { orch, provider } = await makeWiredOrchestrator();
540
+ // Start a send
541
+ const gen = orch.send("Long running");
542
+ const iter = gen[Symbol.asyncIterator]();
543
+ // Get first event
544
+ await iter.next();
545
+ // Interrupt
546
+ orch.interrupt();
547
+ expect(provider.interrupt).toHaveBeenCalled();
548
+ });
549
+ it("interrupt with no active session is safe", () => {
550
+ const orch = makeOrchestrator();
551
+ // Should not throw
552
+ expect(() => orch.interrupt()).not.toThrow();
553
+ });
554
+ });
555
+ // =======================================================================
556
+ // setModel / fetchModels
557
+ // =======================================================================
558
+ describe("setModel / fetchModels", () => {
559
+ it("setModel resets session", async () => {
560
+ const { orch, provider } = await makeWiredOrchestrator();
561
+ await orch.startSession();
562
+ expect(orch.activeSession).not.toBeNull();
563
+ await orch.setModel("claude-sonnet-4-6");
564
+ expect(provider.currentModel).toBe("claude-sonnet-4-6");
565
+ expect(provider.closeSession).toHaveBeenCalled();
566
+ expect(orch.activeSession).toBeNull();
567
+ });
568
+ it("fetchModels delegates to active provider", async () => {
569
+ const { orch, provider } = await makeWiredOrchestrator();
570
+ const models = await orch.fetchModels();
571
+ expect(provider.fetchModels).toHaveBeenCalled();
572
+ expect(models).toEqual([{ id: "test-model", name: "Test Model" }]);
573
+ });
574
+ it("fetchModels auto-switches provider if none active", async () => {
575
+ const orch = makeOrchestrator();
576
+ const provider = mockProvider("claude");
577
+ orch.registerProvider(provider);
578
+ const models = await orch.fetchModels();
579
+ expect(provider.authenticate).toHaveBeenCalled();
580
+ expect(models).toEqual([{ id: "test-model", name: "Test Model" }]);
581
+ });
582
+ });
583
+ // =======================================================================
584
+ // Reasoning Effort
585
+ // =======================================================================
586
+ describe("Reasoning Effort", () => {
587
+ it("reasoningEffort reflects provider setting", async () => {
588
+ const { orch, provider } = await makeWiredOrchestrator();
589
+ provider.reasoningEffort = "high";
590
+ expect(orch.reasoningEffort).toBe("high");
591
+ });
592
+ it("setReasoningEffort updates provider", async () => {
593
+ const { orch, provider } = await makeWiredOrchestrator();
594
+ await orch.setReasoningEffort("max");
595
+ expect(provider.reasoningEffort).toBe("max");
596
+ });
597
+ it("reasoningEffort is null when no provider active", () => {
598
+ const orch = makeOrchestrator();
599
+ expect(orch.reasoningEffort).toBeNull();
600
+ });
601
+ });
602
+ // =======================================================================
603
+ // Tools
604
+ // =======================================================================
605
+ describe("Tools", () => {
606
+ it("registerTool adds tool and passes it in send", async () => {
607
+ const { orch, provider } = await makeWiredOrchestrator();
608
+ const tool = makeTool("remote_exec");
609
+ orch.registerTool(tool);
610
+ await collectEvents(orch.send("Use the tool"));
611
+ const sendCall = provider.send.mock.calls[0];
612
+ const tools = sendCall[2];
613
+ expect(tools.some((t) => t.name === "remote_exec")).toBe(true);
614
+ });
615
+ it("registerTools adds multiple tools", async () => {
616
+ const { orch } = await makeWiredOrchestrator();
617
+ orch.registerTools([makeTool("tool_a"), makeTool("tool_b")]);
618
+ expect(orch.getTools()).toHaveLength(2);
619
+ });
620
+ it("duplicate tool registration is prevented", async () => {
621
+ const { orch } = await makeWiredOrchestrator();
622
+ orch.registerTool(makeTool("tool_a"));
623
+ orch.registerTool(makeTool("tool_a"));
624
+ expect(orch.getTools()).toHaveLength(1);
625
+ });
626
+ });
627
+ // =======================================================================
628
+ // Error Propagation
629
+ // =======================================================================
630
+ describe("Error Propagation", () => {
631
+ it("error in provider.send propagates to caller", async () => {
632
+ const orch = makeOrchestrator();
633
+ const provider = errorProvider("claude", orch.sessionStore);
634
+ orch.registerProvider(provider);
635
+ await orch.switchProvider("claude");
636
+ await expect(collectEvents(orch.send("Boom"))).rejects.toThrow("Provider exploded");
637
+ });
638
+ it("error does not leave cost in inconsistent state", async () => {
639
+ const orch = makeOrchestrator();
640
+ const provider = errorProvider("claude", orch.sessionStore);
641
+ orch.registerProvider(provider);
642
+ await orch.switchProvider("claude");
643
+ try {
644
+ await collectEvents(orch.send("Boom"));
645
+ }
646
+ catch {
647
+ // expected
648
+ }
649
+ // Cost should still be 0 since no done event was emitted
650
+ expect(orch.totalCostUsd).toBe(0);
651
+ });
652
+ });
653
+ // =======================================================================
654
+ // lastInputTokens
655
+ // =======================================================================
656
+ describe("lastInputTokens", () => {
657
+ it("tracks last input token count from done event", async () => {
658
+ const { orch } = await makeWiredOrchestrator();
659
+ await collectEvents(orch.send("Check tokens"));
660
+ expect(orch.lastInputTokens).toBe(100);
661
+ });
662
+ it("updates with each send", async () => {
663
+ const orch = makeOrchestrator();
664
+ const provider = chattyProvider("claude", orch.sessionStore);
665
+ orch.registerProvider(provider);
666
+ await orch.switchProvider("claude");
667
+ await collectEvents(orch.send("Turn 1")); // 50 input tokens
668
+ expect(orch.lastInputTokens).toBe(50);
669
+ await collectEvents(orch.send("Turn 2")); // 100 input tokens
670
+ expect(orch.lastInputTokens).toBe(100);
671
+ });
672
+ });
673
+ });
674
+ //# sourceMappingURL=orchestrator-integration.test.js.map