repoburg 1.3.202 → 1.3.209

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 (288) hide show
  1. package/agent-pane/backend/dist/index.js +2103 -32003
  2. package/agent-pane/backend/dist/server.js +45313 -0
  3. package/backend/dist/src/agent-runtime/agent-runtime.types.d.ts +1 -0
  4. package/backend/dist/src/llm-orchestration/hooks/frontend-notification.hook.js +6 -6
  5. package/backend/dist/src/llm-orchestration/hooks/frontend-notification.hook.js.map +1 -1
  6. package/backend/dist/src/llm-orchestration/llm-turn-processor.service.js +1 -0
  7. package/backend/dist/src/llm-orchestration/llm-turn-processor.service.js.map +1 -1
  8. package/backend/dist/tsconfig.build.tsbuildinfo +1 -1
  9. package/daemon/dist/agentPane.js +128 -0
  10. package/daemon/dist/agentPane.spec.js +183 -0
  11. package/daemon/dist/index.js +28 -8
  12. package/daemon/package.json +1 -1
  13. package/model-selector-scroll.png +0 -0
  14. package/package.json +10 -2
  15. package/agent-pane/.prettierignore +0 -5
  16. package/agent-pane/.prettierrc +0 -8
  17. package/agent-pane/backend/coverage/base.css +0 -224
  18. package/agent-pane/backend/coverage/block-navigation.js +0 -87
  19. package/agent-pane/backend/coverage/clover.xml +0 -887
  20. package/agent-pane/backend/coverage/coverage-final.json +0 -10
  21. package/agent-pane/backend/coverage/favicon.png +0 -0
  22. package/agent-pane/backend/coverage/index.html +0 -176
  23. package/agent-pane/backend/coverage/prettify.css +0 -1
  24. package/agent-pane/backend/coverage/prettify.js +0 -2
  25. package/agent-pane/backend/coverage/sort-arrow-sprite.png +0 -0
  26. package/agent-pane/backend/coverage/sorter.js +0 -210
  27. package/agent-pane/backend/coverage/src/codex/codex-provider.ts.html +0 -1402
  28. package/agent-pane/backend/coverage/src/codex/index.html +0 -131
  29. package/agent-pane/backend/coverage/src/codex/item-mapper.ts.html +0 -331
  30. package/agent-pane/backend/coverage/src/db/index.html +0 -131
  31. package/agent-pane/backend/coverage/src/db/index.ts.html +0 -436
  32. package/agent-pane/backend/coverage/src/db/schema.ts.html +0 -175
  33. package/agent-pane/backend/coverage/src/index.html +0 -116
  34. package/agent-pane/backend/coverage/src/index.ts.html +0 -262
  35. package/agent-pane/backend/coverage/src/routes/index.html +0 -146
  36. package/agent-pane/backend/coverage/src/routes/sessions.ts.html +0 -991
  37. package/agent-pane/backend/coverage/src/routes/turns.ts.html +0 -256
  38. package/agent-pane/backend/coverage/src/routes/workspaces.ts.html +0 -406
  39. package/agent-pane/backend/coverage/src/ws/index.html +0 -116
  40. package/agent-pane/backend/coverage/src/ws/index.ts.html +0 -238
  41. package/agent-pane/backend/data.db +0 -0
  42. package/agent-pane/backend/data.db-shm +0 -0
  43. package/agent-pane/backend/data.db-wal +0 -0
  44. package/agent-pane/backend/package-lock.json +0 -5945
  45. package/agent-pane/backend/package.json +0 -33
  46. package/agent-pane/backend/scripts/backfill-from-jsonl.ts +0 -206
  47. package/agent-pane/backend/scripts/import-session-from-jsonl.ts +0 -264
  48. package/agent-pane/backend/src/claude/claude-message-mapper.test.ts +0 -271
  49. package/agent-pane/backend/src/claude/claude-message-mapper.ts +0 -259
  50. package/agent-pane/backend/src/claude/claude-provider.test.ts +0 -455
  51. package/agent-pane/backend/src/claude/claude-provider.ts +0 -1319
  52. package/agent-pane/backend/src/claude/claude-types.ts +0 -45
  53. package/agent-pane/backend/src/codex/app-server-client.test.ts +0 -183
  54. package/agent-pane/backend/src/codex/app-server-client.ts +0 -247
  55. package/agent-pane/backend/src/codex/app-server-types.ts +0 -259
  56. package/agent-pane/backend/src/codex/codex-provider-runtime-config.test.ts +0 -56
  57. package/agent-pane/backend/src/codex/codex-provider.test.ts +0 -60
  58. package/agent-pane/backend/src/codex/codex-provider.ts +0 -619
  59. package/agent-pane/backend/src/codex/item-mapper.test.ts +0 -221
  60. package/agent-pane/backend/src/codex/item-mapper.ts +0 -86
  61. package/agent-pane/backend/src/codex/shared-app-server.ts +0 -25
  62. package/agent-pane/backend/src/db/index.test.ts +0 -125
  63. package/agent-pane/backend/src/db/index.ts +0 -236
  64. package/agent-pane/backend/src/db/schema.ts +0 -34
  65. package/agent-pane/backend/src/index.ts +0 -89
  66. package/agent-pane/backend/src/integration/__fixtures__/cancel-turn.json +0 -29
  67. package/agent-pane/backend/src/integration/__fixtures__/delta-streaming.json +0 -40
  68. package/agent-pane/backend/src/integration/__fixtures__/failed-turn.json +0 -16
  69. package/agent-pane/backend/src/integration/__fixtures__/mcp-tool-call.json +0 -60
  70. package/agent-pane/backend/src/integration/__fixtures__/missing-item-started.json +0 -34
  71. package/agent-pane/backend/src/integration/__fixtures__/multi-item-turn.json +0 -85
  72. package/agent-pane/backend/src/integration/__fixtures__/multi-turn.json +0 -48
  73. package/agent-pane/backend/src/integration/__fixtures__/simple-turn.json +0 -35
  74. package/agent-pane/backend/src/integration/delta-streaming.test.ts +0 -130
  75. package/agent-pane/backend/src/integration/helpers/app-server-mock.ts +0 -272
  76. package/agent-pane/backend/src/integration/helpers/test-app.ts +0 -110
  77. package/agent-pane/backend/src/integration/helpers/ws-capture.ts +0 -64
  78. package/agent-pane/backend/src/integration/item-lifecycle.test.ts +0 -133
  79. package/agent-pane/backend/src/integration/mcp-tool-call.test.ts +0 -102
  80. package/agent-pane/backend/src/integration/missing-item-started.test.ts +0 -91
  81. package/agent-pane/backend/src/integration/multi-turn.test.ts +0 -121
  82. package/agent-pane/backend/src/integration/session-operations.test.ts +0 -175
  83. package/agent-pane/backend/src/integration/threads-claude-fork.test.ts +0 -241
  84. package/agent-pane/backend/src/integration/threads.test.ts +0 -309
  85. package/agent-pane/backend/src/integration/title-generation.test.ts +0 -109
  86. package/agent-pane/backend/src/integration/turn-cancel.test.ts +0 -111
  87. package/agent-pane/backend/src/integration/turn-failure.test.ts +0 -97
  88. package/agent-pane/backend/src/integration/turn-lifecycle.test.ts +0 -141
  89. package/agent-pane/backend/src/providers/index.ts +0 -16
  90. package/agent-pane/backend/src/providers/registry.test.ts +0 -92
  91. package/agent-pane/backend/src/providers/registry.ts +0 -43
  92. package/agent-pane/backend/src/providers/runtime-provider.ts +0 -49
  93. package/agent-pane/backend/src/repoburg/action-mapping.test.ts +0 -122
  94. package/agent-pane/backend/src/repoburg/action-mapping.ts +0 -294
  95. package/agent-pane/backend/src/repoburg/agent-runtime-client.test.ts +0 -271
  96. package/agent-pane/backend/src/repoburg/agent-runtime-client.ts +0 -285
  97. package/agent-pane/backend/src/repoburg/backend-resolver.test.ts +0 -356
  98. package/agent-pane/backend/src/repoburg/backend-resolver.ts +0 -261
  99. package/agent-pane/backend/src/repoburg/repoburg-client.test.ts +0 -180
  100. package/agent-pane/backend/src/repoburg/repoburg-client.ts +0 -194
  101. package/agent-pane/backend/src/repoburg/repoburg-provider.test.ts +0 -780
  102. package/agent-pane/backend/src/repoburg/repoburg-provider.ts +0 -471
  103. package/agent-pane/backend/src/repoburg/turn-projector.test.ts +0 -621
  104. package/agent-pane/backend/src/repoburg/turn-projector.ts +0 -637
  105. package/agent-pane/backend/src/routes/files.ts +0 -31
  106. package/agent-pane/backend/src/routes/mcp.ts +0 -33
  107. package/agent-pane/backend/src/routes/models.test.ts +0 -92
  108. package/agent-pane/backend/src/routes/models.ts +0 -16
  109. package/agent-pane/backend/src/routes/plugins.ts +0 -40
  110. package/agent-pane/backend/src/routes/providers.test.ts +0 -86
  111. package/agent-pane/backend/src/routes/providers.ts +0 -86
  112. package/agent-pane/backend/src/routes/sessions.test.ts +0 -772
  113. package/agent-pane/backend/src/routes/sessions.ts +0 -565
  114. package/agent-pane/backend/src/routes/skills.test.ts +0 -114
  115. package/agent-pane/backend/src/routes/skills.ts +0 -70
  116. package/agent-pane/backend/src/routes/threads.ts +0 -323
  117. package/agent-pane/backend/src/routes/turns.test.ts +0 -196
  118. package/agent-pane/backend/src/routes/turns.ts +0 -103
  119. package/agent-pane/backend/src/routes/workspaces.test.ts +0 -197
  120. package/agent-pane/backend/src/routes/workspaces.ts +0 -128
  121. package/agent-pane/backend/src/ws/index.test.ts +0 -107
  122. package/agent-pane/backend/src/ws/index.ts +0 -51
  123. package/agent-pane/backend/tsconfig.json +0 -25
  124. package/agent-pane/backend/vitest.config.ts +0 -20
  125. package/agent-pane/eslint.config.js +0 -33
  126. package/agent-pane/frontend/coverage/base.css +0 -224
  127. package/agent-pane/frontend/coverage/block-navigation.js +0 -87
  128. package/agent-pane/frontend/coverage/clover.xml +0 -2962
  129. package/agent-pane/frontend/coverage/coverage-final.json +0 -37
  130. package/agent-pane/frontend/coverage/favicon.png +0 -0
  131. package/agent-pane/frontend/coverage/index.html +0 -251
  132. package/agent-pane/frontend/coverage/prettify.css +0 -1
  133. package/agent-pane/frontend/coverage/prettify.js +0 -2
  134. package/agent-pane/frontend/coverage/sort-arrow-sprite.png +0 -0
  135. package/agent-pane/frontend/coverage/sorter.js +0 -210
  136. package/agent-pane/frontend/coverage/src/App.tsx.html +0 -217
  137. package/agent-pane/frontend/coverage/src/AppInitializer.tsx.html +0 -223
  138. package/agent-pane/frontend/coverage/src/api/client.ts.html +0 -187
  139. package/agent-pane/frontend/coverage/src/api/index.html +0 -161
  140. package/agent-pane/frontend/coverage/src/api/sessions.ts.html +0 -316
  141. package/agent-pane/frontend/coverage/src/api/websocket.ts.html +0 -544
  142. package/agent-pane/frontend/coverage/src/api/workspaces.ts.html +0 -199
  143. package/agent-pane/frontend/coverage/src/components/CollapsibleToolCalls.tsx.html +0 -226
  144. package/agent-pane/frontend/coverage/src/components/CommandInputBar.tsx.html +0 -382
  145. package/agent-pane/frontend/coverage/src/components/CommandPalette.tsx.html +0 -715
  146. package/agent-pane/frontend/coverage/src/components/IdeWorkspace.tsx.html +0 -160
  147. package/agent-pane/frontend/coverage/src/components/ModelSelector.tsx.html +0 -478
  148. package/agent-pane/frontend/coverage/src/components/NewSessionDialog.tsx.html +0 -472
  149. package/agent-pane/frontend/coverage/src/components/RenderArea.tsx.html +0 -574
  150. package/agent-pane/frontend/coverage/src/components/RightPanel.tsx.html +0 -205
  151. package/agent-pane/frontend/coverage/src/components/RuntimeIcon.tsx.html +0 -133
  152. package/agent-pane/frontend/coverage/src/components/SessionTabs.tsx.html +0 -217
  153. package/agent-pane/frontend/coverage/src/components/StatusBar.tsx.html +0 -271
  154. package/agent-pane/frontend/coverage/src/components/StatusDot.tsx.html +0 -109
  155. package/agent-pane/frontend/coverage/src/components/Toast.tsx.html +0 -313
  156. package/agent-pane/frontend/coverage/src/components/TurnContent.tsx.html +0 -1216
  157. package/agent-pane/frontend/coverage/src/components/WorkspaceSidebar.tsx.html +0 -1138
  158. package/agent-pane/frontend/coverage/src/components/index.html +0 -326
  159. package/agent-pane/frontend/coverage/src/components/panels/ChangesPanel.tsx.html +0 -247
  160. package/agent-pane/frontend/coverage/src/components/panels/LogsPanel.tsx.html +0 -160
  161. package/agent-pane/frontend/coverage/src/components/panels/MetricsPanel.tsx.html +0 -427
  162. package/agent-pane/frontend/coverage/src/components/panels/TurnsPanel.tsx.html +0 -397
  163. package/agent-pane/frontend/coverage/src/components/panels/index.html +0 -161
  164. package/agent-pane/frontend/coverage/src/constants/index.html +0 -116
  165. package/agent-pane/frontend/coverage/src/constants/models.ts.html +0 -406
  166. package/agent-pane/frontend/coverage/src/hooks/index.html +0 -146
  167. package/agent-pane/frontend/coverage/src/hooks/useEscapeKey.ts.html +0 -124
  168. package/agent-pane/frontend/coverage/src/hooks/useOutsideClick.ts.html +0 -142
  169. package/agent-pane/frontend/coverage/src/hooks/useSessionStatus.ts.html +0 -187
  170. package/agent-pane/frontend/coverage/src/index.html +0 -146
  171. package/agent-pane/frontend/coverage/src/main.tsx.html +0 -115
  172. package/agent-pane/frontend/coverage/src/store/index.html +0 -116
  173. package/agent-pane/frontend/coverage/src/store/store.ts.html +0 -2404
  174. package/agent-pane/frontend/coverage/src/test/component-helpers.ts.html +0 -190
  175. package/agent-pane/frontend/coverage/src/test/fixtures.ts.html +0 -367
  176. package/agent-pane/frontend/coverage/src/test/index.html +0 -131
  177. package/agent-pane/frontend/coverage/src/types/index.html +0 -116
  178. package/agent-pane/frontend/coverage/src/types/index.ts.html +0 -466
  179. package/agent-pane/frontend/coverage/src/utils/changes.ts.html +0 -202
  180. package/agent-pane/frontend/coverage/src/utils/duration.ts.html +0 -127
  181. package/agent-pane/frontend/coverage/src/utils/index.html +0 -131
  182. package/agent-pane/frontend/dist/assets/index-CJxD4YmJ.js +0 -370
  183. package/agent-pane/frontend/dist/assets/index-r-xwfOKQ.css +0 -1
  184. package/agent-pane/frontend/dist/index.html +0 -19
  185. package/agent-pane/frontend/index.html +0 -18
  186. package/agent-pane/frontend/package-lock.json +0 -4211
  187. package/agent-pane/frontend/package.json +0 -35
  188. package/agent-pane/frontend/src/App.test.tsx +0 -74
  189. package/agent-pane/frontend/src/App.tsx +0 -44
  190. package/agent-pane/frontend/src/AppInitializer.test.tsx +0 -142
  191. package/agent-pane/frontend/src/AppInitializer.tsx +0 -46
  192. package/agent-pane/frontend/src/__tests__/ThreadsPanel.test.tsx +0 -192
  193. package/agent-pane/frontend/src/api/client.test.ts +0 -126
  194. package/agent-pane/frontend/src/api/client.ts +0 -34
  195. package/agent-pane/frontend/src/api/models.ts +0 -7
  196. package/agent-pane/frontend/src/api/providers.ts +0 -17
  197. package/agent-pane/frontend/src/api/sessions.test.ts +0 -246
  198. package/agent-pane/frontend/src/api/sessions.ts +0 -97
  199. package/agent-pane/frontend/src/api/skills.ts +0 -12
  200. package/agent-pane/frontend/src/api/websocket.test.ts +0 -288
  201. package/agent-pane/frontend/src/api/websocket.ts +0 -240
  202. package/agent-pane/frontend/src/api/workspaces.test.ts +0 -112
  203. package/agent-pane/frontend/src/api/workspaces.ts +0 -38
  204. package/agent-pane/frontend/src/components/AskUserItem.test.tsx +0 -143
  205. package/agent-pane/frontend/src/components/AskUserItem.tsx +0 -207
  206. package/agent-pane/frontend/src/components/CollapsibleToolCalls.test.tsx +0 -49
  207. package/agent-pane/frontend/src/components/CollapsibleToolCalls.tsx +0 -56
  208. package/agent-pane/frontend/src/components/CommandInputBar.test.tsx +0 -133
  209. package/agent-pane/frontend/src/components/CommandInputBar.tsx +0 -584
  210. package/agent-pane/frontend/src/components/CommandPalette.test.tsx +0 -226
  211. package/agent-pane/frontend/src/components/CommandPalette.tsx +0 -194
  212. package/agent-pane/frontend/src/components/CommentableText.test.tsx +0 -196
  213. package/agent-pane/frontend/src/components/CommentableText.tsx +0 -379
  214. package/agent-pane/frontend/src/components/IdeWorkspace.test.tsx +0 -39
  215. package/agent-pane/frontend/src/components/IdeWorkspace.tsx +0 -51
  216. package/agent-pane/frontend/src/components/ModelSelector.test.tsx +0 -129
  217. package/agent-pane/frontend/src/components/ModelSelector.tsx +0 -191
  218. package/agent-pane/frontend/src/components/NewSessionDialog.test.tsx +0 -95
  219. package/agent-pane/frontend/src/components/NewSessionDialog.tsx +0 -129
  220. package/agent-pane/frontend/src/components/RenderArea.test.tsx +0 -160
  221. package/agent-pane/frontend/src/components/RenderArea.tsx +0 -170
  222. package/agent-pane/frontend/src/components/ResizeHandle.tsx +0 -29
  223. package/agent-pane/frontend/src/components/RightPanel.test.tsx +0 -43
  224. package/agent-pane/frontend/src/components/RightPanel.tsx +0 -46
  225. package/agent-pane/frontend/src/components/RuntimeIcon.test.tsx +0 -32
  226. package/agent-pane/frontend/src/components/RuntimeIcon.tsx +0 -16
  227. package/agent-pane/frontend/src/components/SessionTabs.test.tsx +0 -72
  228. package/agent-pane/frontend/src/components/SessionTabs.tsx +0 -49
  229. package/agent-pane/frontend/src/components/StatusBar.test.tsx +0 -56
  230. package/agent-pane/frontend/src/components/StatusBar.tsx +0 -129
  231. package/agent-pane/frontend/src/components/StatusDot.test.tsx +0 -25
  232. package/agent-pane/frontend/src/components/StatusDot.tsx +0 -8
  233. package/agent-pane/frontend/src/components/Toast.test.tsx +0 -79
  234. package/agent-pane/frontend/src/components/Toast.tsx +0 -76
  235. package/agent-pane/frontend/src/components/TurnContent.test.tsx +0 -377
  236. package/agent-pane/frontend/src/components/TurnContent.tsx +0 -594
  237. package/agent-pane/frontend/src/components/WorkspaceSidebar.test.tsx +0 -297
  238. package/agent-pane/frontend/src/components/WorkspaceSidebar.tsx +0 -351
  239. package/agent-pane/frontend/src/components/input/OptionSubPopup.tsx +0 -32
  240. package/agent-pane/frontend/src/components/input/SkillMentionPopup.test.tsx +0 -63
  241. package/agent-pane/frontend/src/components/input/SkillMentionPopup.tsx +0 -78
  242. package/agent-pane/frontend/src/components/input/SlashPopup.test.tsx +0 -96
  243. package/agent-pane/frontend/src/components/input/SlashPopup.tsx +0 -82
  244. package/agent-pane/frontend/src/components/panels/ChangesPanel.test.tsx +0 -61
  245. package/agent-pane/frontend/src/components/panels/ChangesPanel.tsx +0 -54
  246. package/agent-pane/frontend/src/components/panels/TasksPanel.test.tsx +0 -97
  247. package/agent-pane/frontend/src/components/panels/TasksPanel.tsx +0 -208
  248. package/agent-pane/frontend/src/components/panels/ThreadsPanel.tsx +0 -382
  249. package/agent-pane/frontend/src/components/panels/TurnsPanel.test.tsx +0 -131
  250. package/agent-pane/frontend/src/components/panels/TurnsPanel.tsx +0 -131
  251. package/agent-pane/frontend/src/constants/models.test.ts +0 -97
  252. package/agent-pane/frontend/src/constants/models.ts +0 -166
  253. package/agent-pane/frontend/src/hooks/useEscapeKey.test.ts +0 -42
  254. package/agent-pane/frontend/src/hooks/useEscapeKey.ts +0 -13
  255. package/agent-pane/frontend/src/hooks/useOutsideClick.test.ts +0 -92
  256. package/agent-pane/frontend/src/hooks/useOutsideClick.ts +0 -19
  257. package/agent-pane/frontend/src/hooks/useProvider.ts +0 -44
  258. package/agent-pane/frontend/src/hooks/useResizable.test.ts +0 -97
  259. package/agent-pane/frontend/src/hooks/useResizable.ts +0 -75
  260. package/agent-pane/frontend/src/hooks/useSessionStatus.test.ts +0 -116
  261. package/agent-pane/frontend/src/hooks/useSessionStatus.ts +0 -34
  262. package/agent-pane/frontend/src/main.test.tsx +0 -37
  263. package/agent-pane/frontend/src/main.tsx +0 -10
  264. package/agent-pane/frontend/src/store/sessionSlice.ts +0 -440
  265. package/agent-pane/frontend/src/store/store.test.ts +0 -1338
  266. package/agent-pane/frontend/src/store/store.ts +0 -37
  267. package/agent-pane/frontend/src/store/threadSlice.ts +0 -136
  268. package/agent-pane/frontend/src/store/uiSlice.ts +0 -133
  269. package/agent-pane/frontend/src/store/workspaceSlice.ts +0 -77
  270. package/agent-pane/frontend/src/store/wsSlice.ts +0 -368
  271. package/agent-pane/frontend/src/styles/app.css +0 -627
  272. package/agent-pane/frontend/src/styles/tokens.css +0 -62
  273. package/agent-pane/frontend/src/test/component-helpers.ts +0 -38
  274. package/agent-pane/frontend/src/test/fixtures.ts +0 -101
  275. package/agent-pane/frontend/src/test-setup.ts +0 -5
  276. package/agent-pane/frontend/src/types/index.test.ts +0 -128
  277. package/agent-pane/frontend/src/types/index.ts +0 -134
  278. package/agent-pane/frontend/src/utils/changes.test.ts +0 -81
  279. package/agent-pane/frontend/src/utils/changes.ts +0 -39
  280. package/agent-pane/frontend/src/utils/duration.test.ts +0 -76
  281. package/agent-pane/frontend/src/utils/duration.ts +0 -14
  282. package/agent-pane/frontend/tsconfig.json +0 -25
  283. package/agent-pane/frontend/vite.config.ts +0 -22
  284. package/agent-pane/frontend/vitest.config.ts +0 -29
  285. package/agent-pane/package-lock.json +0 -1527
  286. package/agent-pane/package.json +0 -19
  287. package/agent-pane/shared/provider-types.ts +0 -67
  288. package/agent-pane/shared/types.ts +0 -184
@@ -1,780 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
-
3
- // Mock the HTTP client class — the provider must only talk to the interface.
4
- vi.mock('./repoburg-client.ts', () => ({
5
- RepoburgHttpClient: vi.fn(),
6
- }));
7
-
8
- // Mock the daemon resolver — lifecycle logic has its own test file.
9
- vi.mock('./backend-resolver.ts', () => ({
10
- resolveBackendUrl: vi.fn(),
11
- invalidateBackend: vi.fn(),
12
- }));
13
-
14
- // Capture SSE stream invocations — deliver agent-runtime messages manually
15
- // and resolve the stream promise to simulate stream end / reconnect.
16
- interface StreamCall {
17
- baseUrl: string;
18
- sessionId: string;
19
- onMessage: (message: AgentRuntimeMessage) => void;
20
- opts: { signal?: AbortSignal; onActivity?: () => void };
21
- resolve: (reason: string) => void;
22
- }
23
- const streamCalls: StreamCall[] = [];
24
- vi.mock('./agent-runtime-client.ts', () => ({
25
- streamAgentRuntimeMessages: vi.fn(
26
- (baseUrl: string, sessionId: string, onMessage: (m: AgentRuntimeMessage) => void, opts: StreamCall['opts']) =>
27
- new Promise((resolve) => {
28
- streamCalls.push({ baseUrl, sessionId, onMessage, opts, resolve });
29
- }),
30
- ),
31
- }));
32
-
33
- import { RepoburgHttpClient } from './repoburg-client.ts';
34
- import {
35
- resolveBackendUrl,
36
- invalidateBackend as invalidateBackendImport,
37
- } from './backend-resolver.ts';
38
- import { streamAgentRuntimeMessages } from './agent-runtime-client.ts';
39
- import type { AgentRuntimeMessage } from './agent-runtime-client.ts';
40
- import type { RepoburgRuntimeClient } from './repoburg-client.ts';
41
-
42
- // Capture broadcasts without touching a real WebSocket server.
43
- const captured: Array<{ event: string; data: unknown }> = [];
44
- vi.mock('../ws/index.ts', async (importOriginal) => {
45
- const actual = (await importOriginal()) as Record<string, unknown>;
46
- return {
47
- ...actual,
48
- broadcast: vi.fn((event: string, data: unknown) => {
49
- captured.push({ event, data });
50
- }),
51
- };
52
- });
53
-
54
- // Import after mocks are set up
55
- import { repoburgProvider, mapReasoningEffort } from './repoburg-provider.ts';
56
- import { initDb } from '../db/index.ts';
57
- import type Database from 'better-sqlite3';
58
- import type { Session, Turn } from '../db/schema.ts';
59
-
60
- /** Build a mock runtime client (HTTP control surface only). */
61
- function makeMockClient(): { client: RepoburgRuntimeClient } {
62
- const client = {
63
- createSession: vi.fn(),
64
- patchSession: vi.fn(),
65
- getSession: vi.fn(),
66
- createInput: vi.fn(),
67
- getInputWithActions: vi.fn(),
68
- getSessionInputs: vi.fn(),
69
- deleteInputsUntilHere: vi.fn().mockResolvedValue(undefined),
70
- cancel: vi.fn().mockResolvedValue({ success: true, cancelled: true }),
71
- getModels: vi.fn(),
72
- } as unknown as RepoburgRuntimeClient;
73
- return { client };
74
- }
75
-
76
- let mock = makeMockClient();
77
-
78
- /** Deliver an agent-runtime message through the active stream. */
79
- function deliver(message: AgentRuntimeMessage): void {
80
- const call = streamCalls[streamCalls.length - 1];
81
- call.onMessage(message);
82
- call.opts.onActivity?.();
83
- }
84
-
85
- describe('repoburgProvider', () => {
86
- let db: Database.Database;
87
-
88
- beforeEach(async () => {
89
- vi.clearAllMocks();
90
- await repoburgProvider.shutdown(); // reset module state + session map
91
- mock = makeMockClient();
92
- streamCalls.length = 0;
93
- vi.mocked(RepoburgHttpClient).mockImplementation(() => mock.client as never);
94
- vi.mocked(resolveBackendUrl)
95
- .mockReset()
96
- .mockResolvedValue({ baseUrl: 'http://repoburg.test', port: 1234, name: 'test' });
97
- captured.length = 0;
98
-
99
- db = initDb(':memory:');
100
- const now = new Date().toISOString();
101
- db.prepare(
102
- `INSERT INTO sessions (id, title, runtime_type, model, model_reasoning_effort, status, is_running, lifecycle, created_at, updated_at)
103
- VALUES ('s1', NULL, 'repoburg', 'ollama/glm-5.3:cloud', 'medium', 'active', 0, 'working', ?, ?)`,
104
- ).run(now, now);
105
- });
106
-
107
- afterEach(() => {
108
- db.close();
109
- });
110
-
111
- /** Insert a pending turn and load session + turn rows for startTurn. */
112
- function seedTurn(turnId: string, prompt = 'hello'): { session: Session; turn: Turn } {
113
- const now = new Date().toISOString();
114
- db.prepare(
115
- `INSERT INTO turns (id, session_id, user_prompt, status, created_at, updated_at)
116
- VALUES (?, 's1', ?, 'pending', ?, ?)`,
117
- ).run(turnId, prompt, now, now);
118
- const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get('s1') as Session;
119
- const turn = db.prepare('SELECT * FROM turns WHERE id = ?').get(turnId) as Turn;
120
- return { session, turn };
121
- }
122
-
123
- /** Start a seeded turn with a successfully created runtime session. */
124
- async function startedTurn(turnId = 't1'): Promise<void> {
125
- mock.client.createSession = vi.fn().mockResolvedValue({ id: 'rs-1' });
126
- mock.client.createInput = vi.fn().mockResolvedValue({ id: 'in-1' });
127
- const { session, turn } = seedTurn(turnId);
128
- await repoburgProvider.startTurn(session, turn);
129
- await vi.waitFor(() => expect(streamCalls.length).toBeGreaterThanOrEqual(1));
130
- }
131
-
132
- it('implements all RuntimeProvider methods', () => {
133
- expect(repoburgProvider.startTurn).toBeTypeOf('function');
134
- expect(repoburgProvider.interruptTurn).toBeTypeOf('function');
135
- expect(repoburgProvider.clearThread).toBeTypeOf('function');
136
- expect(repoburgProvider.clearAllThreads).toBeTypeOf('function');
137
- expect(repoburgProvider.getModels).toBeTypeOf('function');
138
- expect(repoburgProvider.shutdown).toBeTypeOf('function');
139
- });
140
- it('implements all RuntimeProvider methods', () => {
141
- expect(repoburgProvider.startTurn).toBeTypeOf('function');
142
- expect(repoburgProvider.interruptTurn).toBeTypeOf('function');
143
- expect(repoburgProvider.clearThread).toBeTypeOf('function');
144
- expect(repoburgProvider.clearAllThreads).toBeTypeOf('function');
145
- expect(repoburgProvider.getModels).toBeTypeOf('function');
146
- expect(repoburgProvider.rewindRuntime).toBeTypeOf('function');
147
- expect(repoburgProvider.shutdown).toBeTypeOf('function');
148
- });
149
-
150
- it('rewindRuntime truncates via until-here with the LAST runtime input id', async () => {
151
- db.prepare('UPDATE sessions SET external_thread_id = ? WHERE id = ?').run('rs-1', 's1');
152
- const { session } = seedTurn('t1');
153
- db.prepare('UPDATE turns SET runtime_input_ids = ? WHERE id = ?').run(
154
- JSON.stringify(['in-1', 'in-2', 'in-3']),
155
- 't1',
156
- );
157
- // Re-load the turn row AFTER the update (seedTurn fetched a stale row).
158
- const turn = db.prepare('SELECT * FROM turns WHERE id = ?').get('t1') as Turn;
159
-
160
- await repoburgProvider.rewindRuntime!(session, turn);
161
-
162
- expect(mock.client.deleteInputsUntilHere).toHaveBeenCalledWith('rs-1', 'in-3');
163
- });
164
-
165
- it('rewindRuntime throws when the turn has no runtime input ids', async () => {
166
- db.prepare('UPDATE sessions SET external_thread_id = ? WHERE id = ?').run('rs-1', 's1');
167
- const { session, turn } = seedTurn('t1');
168
-
169
- await expect(repoburgProvider.rewindRuntime!(session, turn)).rejects.toThrow(
170
- 'no runtime input ids',
171
- );
172
- expect(mock.client.deleteInputsUntilHere).not.toHaveBeenCalled();
173
- });
174
-
175
- it('rewindRuntime is a no-op when the session has no runtime thread', async () => {
176
- const { session, turn } = seedTurn('t1');
177
-
178
- await repoburgProvider.rewindRuntime!(session, turn);
179
-
180
- expect(mock.client.deleteInputsUntilHere).not.toHaveBeenCalled();
181
- });
182
-
183
- it('rewindRuntime accepts the typed Turn array form of runtime_input_ids', async () => {
184
- db.prepare('UPDATE sessions SET external_thread_id = ? WHERE id = ?').run('rs-1', 's1');
185
- const { session, turn } = seedTurn('t1');
186
- const typedTurn = { ...turn, runtime_input_ids: ['in-a', 'in-b'] } as Turn;
187
-
188
- await repoburgProvider.rewindRuntime!(session, typedTurn);
189
-
190
- expect(mock.client.deleteInputsUntilHere).toHaveBeenCalledWith('rs-1', 'in-b');
191
- });
192
-
193
- describe('mapReasoningEffort', () => {
194
- it('passes valid Ollama levels through and clamps xhigh to high', () => {
195
- expect(mapReasoningEffort('ollama/glm-5.3:cloud', 'low')).toBe('low');
196
- expect(mapReasoningEffort('ollama/glm-5.3:cloud', 'medium')).toBe('medium');
197
- expect(mapReasoningEffort('ollama/glm-5.3:cloud', 'high')).toBe('high');
198
- expect(mapReasoningEffort('ollama/glm-5.3:cloud', 'xhigh')).toBe('high');
199
- });
200
-
201
- it('omits Ollama-inexpressible levels (minimal/none) and unknown values', () => {
202
- expect(mapReasoningEffort('ollama/glm-5.3:cloud', 'minimal')).toBeUndefined();
203
- expect(mapReasoningEffort('ollama/glm-5.3:cloud', 'none')).toBeUndefined();
204
- expect(mapReasoningEffort('ollama/glm-5.3:cloud', 'ultra')).toBeUndefined();
205
- expect(mapReasoningEffort('ollama/glm-5.3:cloud', null)).toBeUndefined();
206
- });
207
-
208
- it('passes the full root enum through for non-Ollama providers', () => {
209
- expect(mapReasoningEffort('zai/glm-5', 'xhigh')).toBe('xhigh');
210
- expect(mapReasoningEffort('zai/glm-5', 'minimal')).toBe('minimal');
211
- expect(mapReasoningEffort(null, 'high')).toBe('high');
212
- });
213
- });
214
-
215
- it('getModels maps runtime models to ModelInfo', async () => {
216
- mock.client.getModels = vi.fn().mockResolvedValue([
217
- { id: 'ollama/glm-5.3:cloud', name: 'glm-5.3:cloud' },
218
- ]);
219
-
220
- const models = await repoburgProvider.getModels();
221
- expect(models).toHaveLength(1);
222
- expect(models[0].slug).toBe('ollama/glm-5.3:cloud');
223
- expect(models[0].displayName).toBe('glm-5.3:cloud');
224
- expect(models[0].reasoningLevels.length).toBeGreaterThan(0);
225
- });
226
-
227
- it('getModels offers 3 levels for Ollama models, 4 for other providers', async () => {
228
- mock.client.getModels = vi.fn().mockResolvedValue([
229
- { id: 'ollama/glm-5.3:cloud', name: 'glm-5.3:cloud' },
230
- { id: 'zai/glm-5', name: 'glm-5' },
231
- ]);
232
-
233
- const models = await repoburgProvider.getModels();
234
- expect(models[0].reasoningLevels.map((l) => l.effort)).toEqual(['low', 'medium', 'high']);
235
- expect(models[1].reasoningLevels.map((l) => l.effort)).toEqual([
236
- 'low',
237
- 'medium',
238
- 'high',
239
- 'xhigh',
240
- ]);
241
- });
242
-
243
- it('getModels never auto-starts a backend (autoStart=false)', async () => {
244
- mock.client.getModels = vi.fn().mockResolvedValue([]);
245
- await repoburgProvider.getModels();
246
- expect(resolveBackendUrl).toHaveBeenCalledWith(process.cwd(), { autoStart: false });
247
- });
248
-
249
- it('getModels returns empty array when no backend is running', async () => {
250
- vi.mocked(resolveBackendUrl).mockResolvedValue(null);
251
- const models = await repoburgProvider.getModels();
252
- expect(models).toEqual([]);
253
- expect(mock.client.getModels).not.toHaveBeenCalled();
254
- });
255
-
256
- it('getModels returns empty array on error', async () => {
257
- mock.client.getModels = vi.fn().mockRejectedValue(new Error('backend down'));
258
-
259
- const models = await repoburgProvider.getModels();
260
- expect(models).toEqual([]);
261
- });
262
-
263
- it('startTurn resolves the workspace backend before creating the session', async () => {
264
- mock.client.createSession = vi.fn().mockResolvedValue({ id: 'rs-1' });
265
- mock.client.createInput = vi.fn().mockResolvedValue({ id: 'in-1' });
266
- db.prepare('UPDATE sessions SET cwd = ? WHERE id = ?').run('/tmp/proj', 's1');
267
-
268
- const { session, turn } = seedTurn('t1');
269
- await repoburgProvider.startTurn(session, turn);
270
-
271
- expect(resolveBackendUrl).toHaveBeenCalledWith('/tmp/proj');
272
- expect(mock.client.createSession).toHaveBeenCalled();
273
- });
274
-
275
- it('startTurn fails the turn when backend resolution fails (daemon down)', async () => {
276
- vi.mocked(resolveBackendUrl).mockRejectedValue(
277
- new Error(
278
- "Repoburg daemon not reachable at http://localhost:9998. Start it with 'repoburg-daemon'.",
279
- ),
280
- );
281
-
282
- const { session, turn } = seedTurn('t1');
283
- await repoburgProvider.startTurn(session, turn);
284
-
285
- const failedTurn = db.prepare('SELECT status, error FROM turns WHERE id = ?').get('t1') as
286
- | { status: string; error: string | null }
287
- | undefined;
288
- expect(failedTurn?.status).toBe('failed');
289
- expect(failedTurn?.error).toMatch(/daemon not reachable/);
290
-
291
- const failed = captured.find((b) => b.event === 'turn:failed');
292
- expect(failed).toBeDefined();
293
- expect(mock.client.createSession).not.toHaveBeenCalled();
294
- });
295
-
296
- it('startTurn invalidates the resolver cache when the backend connection drops', async () => {
297
- mock.client.createSession = vi.fn().mockRejectedValue(new Error('fetch failed ECONNREFUSED'));
298
-
299
- const { session, turn } = seedTurn('t1');
300
- await repoburgProvider.startTurn(session, turn);
301
-
302
- expect(vi.mocked(invalidateBackendImport)).toHaveBeenCalledWith('http://repoburg.test');
303
- const failedTurn = db.prepare('SELECT status FROM turns WHERE id = ?').get('t1') as
304
- | { status: string }
305
- | undefined;
306
- expect(failedTurn?.status).toBe('failed');
307
- });
308
-
309
- it('startTurn invalidates the resolver cache when createInput drops mid-turn', async () => {
310
- mock.client.createSession = vi.fn().mockResolvedValue({ id: 'rs-1' });
311
- mock.client.createInput = vi.fn().mockRejectedValue(new Error('fetch failed ECONNREFUSED'));
312
-
313
- const { session, turn } = seedTurn('t1');
314
- await repoburgProvider.startTurn(session, turn);
315
-
316
- expect(vi.mocked(invalidateBackendImport)).toHaveBeenCalledWith('http://repoburg.test');
317
- const failedTurn = db.prepare('SELECT status FROM turns WHERE id = ?').get('t1') as
318
- | { status: string }
319
- | undefined;
320
- expect(failedTurn?.status).toBe('failed');
321
- expect(captured.find((b) => b.event === 'turn:failed')).toBeDefined();
322
- });
323
-
324
- it('startTurn creates a runtime session, persists external_thread_id, and starts the input + SSE stream', async () => {
325
- mock.client.createSession = vi.fn().mockResolvedValue({ id: 'rs-1' });
326
- mock.client.createInput = vi.fn().mockResolvedValue({ id: 'in-1' });
327
-
328
- const { session, turn } = seedTurn('t1');
329
- await repoburgProvider.startTurn(session, turn);
330
- await vi.waitFor(() => expect(streamCalls.length).toBeGreaterThanOrEqual(1));
331
-
332
- // Runtime session created with the agent-pane model + reasoning config.
333
- expect(mock.client.createSession).toHaveBeenCalledWith({
334
- model_id: 'ollama/glm-5.3:cloud',
335
- reasoning_effort: 'medium',
336
- });
337
- // Repoburg session id persisted as external_thread_id.
338
- const row = db.prepare('SELECT external_thread_id FROM sessions WHERE id = ?').get('s1') as
339
- | { external_thread_id: string | null }
340
- | undefined;
341
- expect(row?.external_thread_id).toBe('rs-1');
342
-
343
- // Turn lifecycle started.
344
- expect(captured.find((b) => b.event === 'turn:started')).toBeDefined();
345
- expect(db.prepare('SELECT status FROM turns WHERE id = ?').get('t1')).toEqual({
346
- status: 'running',
347
- });
348
-
349
- // SSE message stream opened against the runtime session.
350
- expect(streamCalls[0].baseUrl).toBe('http://repoburg.test');
351
- expect(streamCalls[0].sessionId).toBe('rs-1');
352
- expect(streamCalls[0].opts.signal).toBeInstanceOf(AbortSignal);
353
-
354
- // Input started with the autonomous execution strategy.
355
- expect(mock.client.createInput).toHaveBeenCalledWith('rs-1', {
356
- user_prompt: 'hello',
357
- execution_strategy: 'auto_apply',
358
- });
359
- });
360
-
361
- it('startTurn clamps Ollama xhigh to high when creating the session', async () => {
362
- mock.client.createSession = vi.fn().mockResolvedValue({ id: 'rs-1' });
363
- mock.client.createInput = vi.fn().mockResolvedValue({ id: 'in-1' });
364
- db.prepare('UPDATE sessions SET model_reasoning_effort = ? WHERE id = ?').run('xhigh', 's1');
365
-
366
- const { session, turn } = seedTurn('t1');
367
- await repoburgProvider.startTurn(session, turn);
368
-
369
- expect(mock.client.createSession).toHaveBeenCalledWith({
370
- model_id: 'ollama/glm-5.3:cloud',
371
- reasoning_effort: 'high', // xhigh is invalid for Ollama `think`
372
- });
373
- });
374
-
375
- it('startTurn pushes a patch when only the reasoning effort changes', async () => {
376
- mock.client.createSession = vi.fn().mockResolvedValue({ id: 'rs-1' });
377
- mock.client.createInput = vi.fn().mockResolvedValue({ id: 'in-1' });
378
-
379
- // First turn — model ollama/a, effort medium.
380
- db.prepare('UPDATE sessions SET model = ? WHERE id = ?').run('ollama/a', 's1');
381
- const first = seedTurn('t1');
382
- await repoburgProvider.startTurn(first.session, first.turn);
383
- expect(mock.client.patchSession).not.toHaveBeenCalled();
384
-
385
- // Same model, different effort — patch must still fire.
386
- db.prepare('UPDATE sessions SET model_reasoning_effort = ? WHERE id = ?').run('high', 's1');
387
- const secondSession = db.prepare('SELECT * FROM sessions WHERE id = ?').get('s1') as Session;
388
- const second = seedTurn('t2');
389
- await repoburgProvider.startTurn(secondSession, second.turn);
390
-
391
- expect(mock.client.patchSession).toHaveBeenCalledWith('rs-1', {
392
- model_id: 'ollama/a',
393
- reasoning_effort: 'high',
394
- });
395
- });
396
-
397
- it('startTurn re-pushes the clamped effort on resumed sessions (self-heal)', async () => {
398
- // Pre-fix session: runtime session already exists (external_thread_id) with
399
- // an unsafe xhigh effort — the first new turn must PATCH the clamped value.
400
- mock.client.createInput = vi.fn().mockResolvedValue({ id: 'in-1' });
401
- db.prepare(
402
- 'UPDATE sessions SET external_thread_id = ?, model_reasoning_effort = ? WHERE id = ?',
403
- ).run('rs-1', 'xhigh', 's1');
404
-
405
- const { session, turn } = seedTurn('t1');
406
- await repoburgProvider.startTurn(session, turn);
407
-
408
- expect(mock.client.patchSession).toHaveBeenCalledWith('rs-1', {
409
- model_id: 'ollama/glm-5.3:cloud',
410
- reasoning_effort: 'high',
411
- });
412
- });
413
-
414
- it('startTurn pushes mid-session model changes to the runtime session', async () => {
415
- mock.client.createSession = vi.fn().mockResolvedValue({ id: 'rs-1' });
416
- mock.client.createInput = vi.fn().mockResolvedValue({ id: 'in-1' });
417
-
418
- // First turn — session created, model recorded as 'ollama/a'.
419
- db.prepare('UPDATE sessions SET model = ? WHERE id = ?').run('ollama/a', 's1');
420
- const first = seedTurn('t1');
421
- await repoburgProvider.startTurn(first.session, first.turn);
422
- expect(mock.client.patchSession).not.toHaveBeenCalled();
423
-
424
- // Model changed in agent-pane — second turn pushes the change.
425
- db.prepare('UPDATE sessions SET model = ? WHERE id = ?').run('ollama/b', 's1');
426
- const secondSession = db.prepare('SELECT * FROM sessions WHERE id = ?').get('s1') as Session;
427
- const second = seedTurn('t2');
428
- await repoburgProvider.startTurn(secondSession, second.turn);
429
-
430
- expect(mock.client.patchSession).toHaveBeenCalledWith('rs-1', {
431
- model_id: 'ollama/b',
432
- reasoning_effort: 'medium',
433
- });
434
- });
435
-
436
- it('full turn: live deltas, assistant, tool results, result → completed turn with usage and title', async () => {
437
- mock.client.getSession = vi.fn().mockResolvedValue({
438
- id: 'rs-1',
439
- session_title: 'Auto Title',
440
- is_running: false,
441
- });
442
-
443
- await startedTurn('t1');
444
-
445
- // Live text delta (starts the agent_message item)…
446
- deliver({ type: 'stream_event', sessionId: 'rs-1', inputId: 'in-1', delta: { type: 'text', text: 'Hello' } });
447
- const started = captured.find((b) => b.event === 'item:started');
448
- expect(started).toBeDefined();
449
- expect((started!.data as { item: { type: string } }).item.type).toBe('agent_message');
450
-
451
- // …then a delta…
452
- deliver({ type: 'stream_event', sessionId: 'rs-1', inputId: 'in-1', delta: { type: 'text', text: ' world' } });
453
- const delta = captured.find((b) => b.event === 'item:delta');
454
- expect(delta).toBeDefined();
455
- expect((delta!.data as { itemId: string; delta: string }).itemId).toBe('in-1-text');
456
- expect((delta!.data as { itemId: string; delta: string }).delta).toBe(' world');
457
-
458
- // …and a thinking delta (starts the reasoning item).
459
- deliver({ type: 'stream_event', sessionId: 'rs-1', inputId: 'in-1', delta: { type: 'thinking', text: 'thinking about it' } });
460
- expect(
461
- captured
462
- .filter((b) => b.event === 'item:started')
463
- .some((b) => (b.data as { item: { type: string } }).item.type === 'reasoning'),
464
- ).toBe(true);
465
-
466
- // Round ends — assistant completes the streamed items.
467
- deliver({
468
- type: 'assistant',
469
- sessionId: 'rs-1',
470
- inputId: 'in-1',
471
- content: [
472
- { type: 'thinking', thinking: 'thinking about it' },
473
- { type: 'text', text: 'Hello world' },
474
- ],
475
- });
476
-
477
- // Executed actions become items (one tool_result each).
478
- deliver({
479
- type: 'user',
480
- sessionId: 'rs-1',
481
- inputId: 'in-1',
482
- content: [
483
- {
484
- type: 'tool_result',
485
- tool_use_id: 'a1',
486
- content: 'created',
487
- is_error: false,
488
- action: {
489
- id: 'a1',
490
- action_type: 'create_file',
491
- file_path: 'src/a.ts',
492
- command_string: null,
493
- status: 'CONFIRMED_KEPT',
494
- server_name: null,
495
- tool_name: null,
496
- arguments: null,
497
- plain: null,
498
- selections: null,
499
- order_of_execution: 0,
500
- },
501
- },
502
- {
503
- type: 'tool_result',
504
- tool_use_id: 'a2',
505
- content: 'a.ts',
506
- is_error: false,
507
- action: {
508
- id: 'a2',
509
- action_type: 'run_command',
510
- file_path: null,
511
- command_string: 'ls',
512
- status: 'CONFIRMED_KEPT',
513
- server_name: null,
514
- tool_name: null,
515
- arguments: null,
516
- plain: null,
517
- selections: null,
518
- order_of_execution: 1,
519
- },
520
- },
521
- ],
522
- });
523
- expect(
524
- captured.some(
525
- (b) => b.event === 'item:completed' && (b.data as { item: { type: string } }).item.type === 'file_change',
526
- ),
527
- ).toBe(true);
528
-
529
- const items = db.prepare('SELECT type, status FROM items WHERE turn_id = ? ORDER BY rowid').all('t1') as Array<{
530
- type: string;
531
- status: string;
532
- }>;
533
- expect(items.map((i) => i.type)).toEqual([
534
- 'agent_message',
535
- 'reasoning',
536
- 'file_change',
537
- 'command_execution',
538
- ]);
539
- expect(items.every((i) => i.status === 'completed')).toBe(true);
540
-
541
- // Terminal result — turn completes with result text + accumulated usage.
542
- deliver({
543
- type: 'result',
544
- subtype: 'success',
545
- sessionId: 'rs-1',
546
- is_error: false,
547
- result: 'Hello world',
548
- usage: {
549
- input_tokens: 100,
550
- output_tokens: 20,
551
- cache_read_input_tokens: 5,
552
- cache_creation_input_tokens: 0,
553
- },
554
- num_rounds: 1,
555
- });
556
-
557
- const completedTurn = db.prepare(
558
- 'SELECT status, agent_response, token_usage FROM turns WHERE id = ?',
559
- ).get('t1') as { status: string; agent_response: string | null; token_usage: string | null } | undefined;
560
- expect(completedTurn?.status).toBe('completed');
561
- expect(completedTurn?.agent_response).toBe('Hello world');
562
- expect(JSON.parse(completedTurn!.token_usage!)).toMatchObject({
563
- input_tokens: 100,
564
- output_tokens: 20,
565
- cached_input_tokens: 5,
566
- });
567
- expect(captured.find((b) => b.event === 'turn:completed')).toBeDefined();
568
-
569
- // Title synced from the runtime session (async — fire-and-forget).
570
- await vi.waitFor(() => {
571
- const sess = db.prepare('SELECT title, has_unreviewed FROM sessions WHERE id = ?').get('s1') as
572
- | { title: string | null; has_unreviewed: number }
573
- | undefined;
574
- expect(sess?.title).toBe('Auto Title');
575
- expect(sess?.has_unreviewed).toBe(1);
576
- });
577
- expect(captured.find((b) => b.event === 'session:title')).toBeDefined();
578
-
579
- // Stream ends — no reconnect after the terminal result.
580
- streamCalls[0].resolve('result');
581
- await new Promise((r) => setTimeout(r, 20));
582
- expect(streamCalls.length).toBe(1);
583
- });
584
-
585
- it('result error_during_execution fails the turn and closes open items as failed', async () => {
586
- await startedTurn('t1');
587
-
588
- deliver({ type: 'stream_event', sessionId: 'rs-1', inputId: 'in-1', delta: { type: 'text', text: 'partial' } });
589
- deliver({
590
- type: 'result',
591
- subtype: 'error_during_execution',
592
- sessionId: 'rs-1',
593
- is_error: true,
594
- result: '',
595
- num_rounds: 1,
596
- errors: ['boom'],
597
- });
598
-
599
- const failedTurn = db.prepare('SELECT status, error FROM turns WHERE id = ?').get('t1') as
600
- | { status: string; error: string | null }
601
- | undefined;
602
- expect(failedTurn?.status).toBe('failed');
603
- expect(failedTurn?.error).toBe('boom');
604
-
605
- // The in_progress streamed item is closed as failed.
606
- const items = db.prepare('SELECT status FROM items WHERE turn_id = ?').all('t1') as Array<{
607
- status: string;
608
- }>;
609
- expect(items.length).toBe(1);
610
- expect(items[0].status).toBe('failed');
611
-
612
- const failed = captured.find((b) => b.event === 'turn:failed');
613
- expect(failed).toBeDefined();
614
- expect((failed!.data as { error: string }).error).toBe('boom');
615
- });
616
-
617
- it('result aborted aborts the turn', async () => {
618
- await startedTurn('t1');
619
-
620
- deliver({
621
- type: 'result',
622
- subtype: 'aborted',
623
- sessionId: 'rs-1',
624
- is_error: true,
625
- result: '',
626
- num_rounds: 0,
627
- });
628
-
629
- expect(db.prepare('SELECT status FROM turns WHERE id = ?').get('t1')).toEqual({
630
- status: 'aborted',
631
- });
632
- expect(captured.find((b) => b.event === 'turn:cancelled')).toBeDefined();
633
- });
634
-
635
- it('replayed messages after reconnect are projected idempotently (no duplicate items)', async () => {
636
- await startedTurn('t1');
637
-
638
- deliver({
639
- type: 'assistant',
640
- sessionId: 'rs-1',
641
- inputId: 'in-1',
642
- content: [{ type: 'text', text: 'Hello world' }],
643
- });
644
- deliver({
645
- type: 'user',
646
- sessionId: 'rs-1',
647
- inputId: 'in-1',
648
- content: [
649
- {
650
- type: 'tool_result',
651
- tool_use_id: 'a1',
652
- content: 'created',
653
- is_error: false,
654
- action: {
655
- id: 'a1',
656
- action_type: 'create_file',
657
- file_path: 'src/a.ts',
658
- command_string: null,
659
- status: 'CONFIRMED_KEPT',
660
- server_name: null,
661
- tool_name: null,
662
- arguments: null,
663
- plain: null,
664
- selections: null,
665
- order_of_execution: 0,
666
- },
667
- },
668
- ],
669
- });
670
-
671
- // Reconnect: the SAME messages replay on a second stream call.
672
- streamCalls[0].resolve('aborted'); // stall-style disconnect
673
- await vi.waitFor(() => expect(streamCalls.length).toBe(2));
674
- deliver({
675
- type: 'assistant',
676
- sessionId: 'rs-1',
677
- inputId: 'in-1',
678
- content: [{ type: 'text', text: 'Hello world' }],
679
- });
680
- deliver({
681
- type: 'user',
682
- sessionId: 'rs-1',
683
- inputId: 'in-1',
684
- content: [
685
- {
686
- type: 'tool_result',
687
- tool_use_id: 'a1',
688
- content: 'created',
689
- is_error: false,
690
- action: {
691
- id: 'a1',
692
- action_type: 'create_file',
693
- file_path: 'src/a.ts',
694
- command_string: null,
695
- status: 'CONFIRMED_KEPT',
696
- server_name: null,
697
- tool_name: null,
698
- arguments: null,
699
- plain: null,
700
- selections: null,
701
- order_of_execution: 0,
702
- },
703
- },
704
- ],
705
- });
706
-
707
- const items = db.prepare('SELECT external_item_id FROM items WHERE turn_id = ?').all('t1') as Array<{
708
- external_item_id: string;
709
- }>;
710
- expect(items.map((i) => i.external_item_id).sort()).toEqual(['in-1-action-a1', 'in-1-text']);
711
-
712
- deliver({
713
- type: 'result',
714
- subtype: 'success',
715
- sessionId: 'rs-1',
716
- is_error: false,
717
- result: 'done',
718
- num_rounds: 1,
719
- });
720
- expect(db.prepare('SELECT status FROM turns WHERE id = ?').get('t1')).toEqual({
721
- status: 'completed',
722
- });
723
- });
724
-
725
- it('stream session-not-found fails the turn', async () => {
726
- await startedTurn('t1');
727
-
728
- streamCalls[0].resolve('session-not-found');
729
- await vi.waitFor(() => {
730
- const failedTurn = db.prepare('SELECT status, error FROM turns WHERE id = ?').get('t1') as
731
- | { status: string; error: string | null }
732
- | undefined;
733
- expect(failedTurn?.status).toBe('failed');
734
- expect(failedTurn?.error).toMatch(/no longer exists/);
735
- });
736
- });
737
-
738
- it('interruptTurn cancels the runtime round and aborts the turn', async () => {
739
- await startedTurn('t1');
740
-
741
- const cancelled = await repoburgProvider.interruptTurn('s1');
742
- expect(cancelled).toBe(true);
743
- expect(mock.client.cancel).toHaveBeenCalledWith('rs-1');
744
-
745
- expect(db.prepare('SELECT status FROM turns WHERE id = ?').get('t1')).toEqual({
746
- status: 'aborted',
747
- });
748
- expect(captured.find((b) => b.event === 'turn:cancelled')).toBeDefined();
749
-
750
- // Messages after cancellation are ignored — the turn stays aborted.
751
- deliver({ type: 'result', subtype: 'success', sessionId: 'rs-1', is_error: false, result: 'late', num_rounds: 1 });
752
- expect(db.prepare('SELECT status FROM turns WHERE id = ?').get('t1')).toEqual({
753
- status: 'aborted',
754
- });
755
-
756
- // The interrupted stream signal is aborted (fetch closed).
757
- expect(streamCalls[0]!.opts.signal!.aborted).toBe(true);
758
- streamCalls[0]!.resolve('aborted');
759
- });
760
-
761
- it('interruptTurn returns false for unknown session', async () => {
762
- expect(await repoburgProvider.interruptTurn('unknown')).toBe(false);
763
- });
764
-
765
- it('clearThread removes session state', async () => {
766
- await startedTurn('t1');
767
-
768
- repoburgProvider.clearThread('s1');
769
- expect(await repoburgProvider.interruptTurn('s1')).toBe(false);
770
- });
771
-
772
- it('clearAllThreads and shutdown do not throw when empty', async () => {
773
- expect(() => repoburgProvider.clearAllThreads()).not.toThrow();
774
- await expect(repoburgProvider.shutdown()).resolves.not.toThrow();
775
- });
776
-
777
- it('streamAgentRuntimeMessages is imported once and used per turn', () => {
778
- expect(streamAgentRuntimeMessages).toBeDefined();
779
- });
780
- });