repoburg 1.3.202 → 1.3.204

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 (282) hide show
  1. package/agent-pane/backend/dist/index.js +2092 -32001
  2. package/agent-pane/backend/dist/server.js +45304 -0
  3. package/backend/dist/tsconfig.build.tsbuildinfo +1 -1
  4. package/daemon/dist/agentPane.js +128 -0
  5. package/daemon/dist/agentPane.spec.js +183 -0
  6. package/daemon/dist/index.js +28 -8
  7. package/daemon/package.json +1 -1
  8. package/package.json +15 -2
  9. package/agent-pane/.prettierignore +0 -5
  10. package/agent-pane/.prettierrc +0 -8
  11. package/agent-pane/backend/coverage/base.css +0 -224
  12. package/agent-pane/backend/coverage/block-navigation.js +0 -87
  13. package/agent-pane/backend/coverage/clover.xml +0 -887
  14. package/agent-pane/backend/coverage/coverage-final.json +0 -10
  15. package/agent-pane/backend/coverage/favicon.png +0 -0
  16. package/agent-pane/backend/coverage/index.html +0 -176
  17. package/agent-pane/backend/coverage/prettify.css +0 -1
  18. package/agent-pane/backend/coverage/prettify.js +0 -2
  19. package/agent-pane/backend/coverage/sort-arrow-sprite.png +0 -0
  20. package/agent-pane/backend/coverage/sorter.js +0 -210
  21. package/agent-pane/backend/coverage/src/codex/codex-provider.ts.html +0 -1402
  22. package/agent-pane/backend/coverage/src/codex/index.html +0 -131
  23. package/agent-pane/backend/coverage/src/codex/item-mapper.ts.html +0 -331
  24. package/agent-pane/backend/coverage/src/db/index.html +0 -131
  25. package/agent-pane/backend/coverage/src/db/index.ts.html +0 -436
  26. package/agent-pane/backend/coverage/src/db/schema.ts.html +0 -175
  27. package/agent-pane/backend/coverage/src/index.html +0 -116
  28. package/agent-pane/backend/coverage/src/index.ts.html +0 -262
  29. package/agent-pane/backend/coverage/src/routes/index.html +0 -146
  30. package/agent-pane/backend/coverage/src/routes/sessions.ts.html +0 -991
  31. package/agent-pane/backend/coverage/src/routes/turns.ts.html +0 -256
  32. package/agent-pane/backend/coverage/src/routes/workspaces.ts.html +0 -406
  33. package/agent-pane/backend/coverage/src/ws/index.html +0 -116
  34. package/agent-pane/backend/coverage/src/ws/index.ts.html +0 -238
  35. package/agent-pane/backend/data.db +0 -0
  36. package/agent-pane/backend/data.db-shm +0 -0
  37. package/agent-pane/backend/data.db-wal +0 -0
  38. package/agent-pane/backend/package-lock.json +0 -5945
  39. package/agent-pane/backend/package.json +0 -33
  40. package/agent-pane/backend/scripts/backfill-from-jsonl.ts +0 -206
  41. package/agent-pane/backend/scripts/import-session-from-jsonl.ts +0 -264
  42. package/agent-pane/backend/src/claude/claude-message-mapper.test.ts +0 -271
  43. package/agent-pane/backend/src/claude/claude-message-mapper.ts +0 -259
  44. package/agent-pane/backend/src/claude/claude-provider.test.ts +0 -455
  45. package/agent-pane/backend/src/claude/claude-provider.ts +0 -1319
  46. package/agent-pane/backend/src/claude/claude-types.ts +0 -45
  47. package/agent-pane/backend/src/codex/app-server-client.test.ts +0 -183
  48. package/agent-pane/backend/src/codex/app-server-client.ts +0 -247
  49. package/agent-pane/backend/src/codex/app-server-types.ts +0 -259
  50. package/agent-pane/backend/src/codex/codex-provider-runtime-config.test.ts +0 -56
  51. package/agent-pane/backend/src/codex/codex-provider.test.ts +0 -60
  52. package/agent-pane/backend/src/codex/codex-provider.ts +0 -619
  53. package/agent-pane/backend/src/codex/item-mapper.test.ts +0 -221
  54. package/agent-pane/backend/src/codex/item-mapper.ts +0 -86
  55. package/agent-pane/backend/src/codex/shared-app-server.ts +0 -25
  56. package/agent-pane/backend/src/db/index.test.ts +0 -125
  57. package/agent-pane/backend/src/db/index.ts +0 -236
  58. package/agent-pane/backend/src/db/schema.ts +0 -34
  59. package/agent-pane/backend/src/index.ts +0 -89
  60. package/agent-pane/backend/src/integration/__fixtures__/cancel-turn.json +0 -29
  61. package/agent-pane/backend/src/integration/__fixtures__/delta-streaming.json +0 -40
  62. package/agent-pane/backend/src/integration/__fixtures__/failed-turn.json +0 -16
  63. package/agent-pane/backend/src/integration/__fixtures__/mcp-tool-call.json +0 -60
  64. package/agent-pane/backend/src/integration/__fixtures__/missing-item-started.json +0 -34
  65. package/agent-pane/backend/src/integration/__fixtures__/multi-item-turn.json +0 -85
  66. package/agent-pane/backend/src/integration/__fixtures__/multi-turn.json +0 -48
  67. package/agent-pane/backend/src/integration/__fixtures__/simple-turn.json +0 -35
  68. package/agent-pane/backend/src/integration/delta-streaming.test.ts +0 -130
  69. package/agent-pane/backend/src/integration/helpers/app-server-mock.ts +0 -272
  70. package/agent-pane/backend/src/integration/helpers/test-app.ts +0 -110
  71. package/agent-pane/backend/src/integration/helpers/ws-capture.ts +0 -64
  72. package/agent-pane/backend/src/integration/item-lifecycle.test.ts +0 -133
  73. package/agent-pane/backend/src/integration/mcp-tool-call.test.ts +0 -102
  74. package/agent-pane/backend/src/integration/missing-item-started.test.ts +0 -91
  75. package/agent-pane/backend/src/integration/multi-turn.test.ts +0 -121
  76. package/agent-pane/backend/src/integration/session-operations.test.ts +0 -175
  77. package/agent-pane/backend/src/integration/threads-claude-fork.test.ts +0 -241
  78. package/agent-pane/backend/src/integration/threads.test.ts +0 -309
  79. package/agent-pane/backend/src/integration/title-generation.test.ts +0 -109
  80. package/agent-pane/backend/src/integration/turn-cancel.test.ts +0 -111
  81. package/agent-pane/backend/src/integration/turn-failure.test.ts +0 -97
  82. package/agent-pane/backend/src/integration/turn-lifecycle.test.ts +0 -141
  83. package/agent-pane/backend/src/providers/index.ts +0 -16
  84. package/agent-pane/backend/src/providers/registry.test.ts +0 -92
  85. package/agent-pane/backend/src/providers/registry.ts +0 -43
  86. package/agent-pane/backend/src/providers/runtime-provider.ts +0 -49
  87. package/agent-pane/backend/src/repoburg/action-mapping.test.ts +0 -122
  88. package/agent-pane/backend/src/repoburg/action-mapping.ts +0 -294
  89. package/agent-pane/backend/src/repoburg/agent-runtime-client.test.ts +0 -271
  90. package/agent-pane/backend/src/repoburg/agent-runtime-client.ts +0 -285
  91. package/agent-pane/backend/src/repoburg/backend-resolver.test.ts +0 -356
  92. package/agent-pane/backend/src/repoburg/backend-resolver.ts +0 -261
  93. package/agent-pane/backend/src/repoburg/repoburg-client.test.ts +0 -180
  94. package/agent-pane/backend/src/repoburg/repoburg-client.ts +0 -194
  95. package/agent-pane/backend/src/repoburg/repoburg-provider.test.ts +0 -780
  96. package/agent-pane/backend/src/repoburg/repoburg-provider.ts +0 -471
  97. package/agent-pane/backend/src/repoburg/turn-projector.test.ts +0 -621
  98. package/agent-pane/backend/src/repoburg/turn-projector.ts +0 -637
  99. package/agent-pane/backend/src/routes/files.ts +0 -31
  100. package/agent-pane/backend/src/routes/mcp.ts +0 -33
  101. package/agent-pane/backend/src/routes/models.test.ts +0 -92
  102. package/agent-pane/backend/src/routes/models.ts +0 -16
  103. package/agent-pane/backend/src/routes/plugins.ts +0 -40
  104. package/agent-pane/backend/src/routes/providers.test.ts +0 -86
  105. package/agent-pane/backend/src/routes/providers.ts +0 -86
  106. package/agent-pane/backend/src/routes/sessions.test.ts +0 -772
  107. package/agent-pane/backend/src/routes/sessions.ts +0 -565
  108. package/agent-pane/backend/src/routes/skills.test.ts +0 -114
  109. package/agent-pane/backend/src/routes/skills.ts +0 -70
  110. package/agent-pane/backend/src/routes/threads.ts +0 -323
  111. package/agent-pane/backend/src/routes/turns.test.ts +0 -196
  112. package/agent-pane/backend/src/routes/turns.ts +0 -103
  113. package/agent-pane/backend/src/routes/workspaces.test.ts +0 -197
  114. package/agent-pane/backend/src/routes/workspaces.ts +0 -128
  115. package/agent-pane/backend/src/ws/index.test.ts +0 -107
  116. package/agent-pane/backend/src/ws/index.ts +0 -51
  117. package/agent-pane/backend/tsconfig.json +0 -25
  118. package/agent-pane/backend/vitest.config.ts +0 -20
  119. package/agent-pane/eslint.config.js +0 -33
  120. package/agent-pane/frontend/coverage/base.css +0 -224
  121. package/agent-pane/frontend/coverage/block-navigation.js +0 -87
  122. package/agent-pane/frontend/coverage/clover.xml +0 -2962
  123. package/agent-pane/frontend/coverage/coverage-final.json +0 -37
  124. package/agent-pane/frontend/coverage/favicon.png +0 -0
  125. package/agent-pane/frontend/coverage/index.html +0 -251
  126. package/agent-pane/frontend/coverage/prettify.css +0 -1
  127. package/agent-pane/frontend/coverage/prettify.js +0 -2
  128. package/agent-pane/frontend/coverage/sort-arrow-sprite.png +0 -0
  129. package/agent-pane/frontend/coverage/sorter.js +0 -210
  130. package/agent-pane/frontend/coverage/src/App.tsx.html +0 -217
  131. package/agent-pane/frontend/coverage/src/AppInitializer.tsx.html +0 -223
  132. package/agent-pane/frontend/coverage/src/api/client.ts.html +0 -187
  133. package/agent-pane/frontend/coverage/src/api/index.html +0 -161
  134. package/agent-pane/frontend/coverage/src/api/sessions.ts.html +0 -316
  135. package/agent-pane/frontend/coverage/src/api/websocket.ts.html +0 -544
  136. package/agent-pane/frontend/coverage/src/api/workspaces.ts.html +0 -199
  137. package/agent-pane/frontend/coverage/src/components/CollapsibleToolCalls.tsx.html +0 -226
  138. package/agent-pane/frontend/coverage/src/components/CommandInputBar.tsx.html +0 -382
  139. package/agent-pane/frontend/coverage/src/components/CommandPalette.tsx.html +0 -715
  140. package/agent-pane/frontend/coverage/src/components/IdeWorkspace.tsx.html +0 -160
  141. package/agent-pane/frontend/coverage/src/components/ModelSelector.tsx.html +0 -478
  142. package/agent-pane/frontend/coverage/src/components/NewSessionDialog.tsx.html +0 -472
  143. package/agent-pane/frontend/coverage/src/components/RenderArea.tsx.html +0 -574
  144. package/agent-pane/frontend/coverage/src/components/RightPanel.tsx.html +0 -205
  145. package/agent-pane/frontend/coverage/src/components/RuntimeIcon.tsx.html +0 -133
  146. package/agent-pane/frontend/coverage/src/components/SessionTabs.tsx.html +0 -217
  147. package/agent-pane/frontend/coverage/src/components/StatusBar.tsx.html +0 -271
  148. package/agent-pane/frontend/coverage/src/components/StatusDot.tsx.html +0 -109
  149. package/agent-pane/frontend/coverage/src/components/Toast.tsx.html +0 -313
  150. package/agent-pane/frontend/coverage/src/components/TurnContent.tsx.html +0 -1216
  151. package/agent-pane/frontend/coverage/src/components/WorkspaceSidebar.tsx.html +0 -1138
  152. package/agent-pane/frontend/coverage/src/components/index.html +0 -326
  153. package/agent-pane/frontend/coverage/src/components/panels/ChangesPanel.tsx.html +0 -247
  154. package/agent-pane/frontend/coverage/src/components/panels/LogsPanel.tsx.html +0 -160
  155. package/agent-pane/frontend/coverage/src/components/panels/MetricsPanel.tsx.html +0 -427
  156. package/agent-pane/frontend/coverage/src/components/panels/TurnsPanel.tsx.html +0 -397
  157. package/agent-pane/frontend/coverage/src/components/panels/index.html +0 -161
  158. package/agent-pane/frontend/coverage/src/constants/index.html +0 -116
  159. package/agent-pane/frontend/coverage/src/constants/models.ts.html +0 -406
  160. package/agent-pane/frontend/coverage/src/hooks/index.html +0 -146
  161. package/agent-pane/frontend/coverage/src/hooks/useEscapeKey.ts.html +0 -124
  162. package/agent-pane/frontend/coverage/src/hooks/useOutsideClick.ts.html +0 -142
  163. package/agent-pane/frontend/coverage/src/hooks/useSessionStatus.ts.html +0 -187
  164. package/agent-pane/frontend/coverage/src/index.html +0 -146
  165. package/agent-pane/frontend/coverage/src/main.tsx.html +0 -115
  166. package/agent-pane/frontend/coverage/src/store/index.html +0 -116
  167. package/agent-pane/frontend/coverage/src/store/store.ts.html +0 -2404
  168. package/agent-pane/frontend/coverage/src/test/component-helpers.ts.html +0 -190
  169. package/agent-pane/frontend/coverage/src/test/fixtures.ts.html +0 -367
  170. package/agent-pane/frontend/coverage/src/test/index.html +0 -131
  171. package/agent-pane/frontend/coverage/src/types/index.html +0 -116
  172. package/agent-pane/frontend/coverage/src/types/index.ts.html +0 -466
  173. package/agent-pane/frontend/coverage/src/utils/changes.ts.html +0 -202
  174. package/agent-pane/frontend/coverage/src/utils/duration.ts.html +0 -127
  175. package/agent-pane/frontend/coverage/src/utils/index.html +0 -131
  176. package/agent-pane/frontend/dist/assets/index-CJxD4YmJ.js +0 -370
  177. package/agent-pane/frontend/dist/assets/index-r-xwfOKQ.css +0 -1
  178. package/agent-pane/frontend/dist/index.html +0 -19
  179. package/agent-pane/frontend/index.html +0 -18
  180. package/agent-pane/frontend/package-lock.json +0 -4211
  181. package/agent-pane/frontend/package.json +0 -35
  182. package/agent-pane/frontend/src/App.test.tsx +0 -74
  183. package/agent-pane/frontend/src/App.tsx +0 -44
  184. package/agent-pane/frontend/src/AppInitializer.test.tsx +0 -142
  185. package/agent-pane/frontend/src/AppInitializer.tsx +0 -46
  186. package/agent-pane/frontend/src/__tests__/ThreadsPanel.test.tsx +0 -192
  187. package/agent-pane/frontend/src/api/client.test.ts +0 -126
  188. package/agent-pane/frontend/src/api/client.ts +0 -34
  189. package/agent-pane/frontend/src/api/models.ts +0 -7
  190. package/agent-pane/frontend/src/api/providers.ts +0 -17
  191. package/agent-pane/frontend/src/api/sessions.test.ts +0 -246
  192. package/agent-pane/frontend/src/api/sessions.ts +0 -97
  193. package/agent-pane/frontend/src/api/skills.ts +0 -12
  194. package/agent-pane/frontend/src/api/websocket.test.ts +0 -288
  195. package/agent-pane/frontend/src/api/websocket.ts +0 -240
  196. package/agent-pane/frontend/src/api/workspaces.test.ts +0 -112
  197. package/agent-pane/frontend/src/api/workspaces.ts +0 -38
  198. package/agent-pane/frontend/src/components/AskUserItem.test.tsx +0 -143
  199. package/agent-pane/frontend/src/components/AskUserItem.tsx +0 -207
  200. package/agent-pane/frontend/src/components/CollapsibleToolCalls.test.tsx +0 -49
  201. package/agent-pane/frontend/src/components/CollapsibleToolCalls.tsx +0 -56
  202. package/agent-pane/frontend/src/components/CommandInputBar.test.tsx +0 -133
  203. package/agent-pane/frontend/src/components/CommandInputBar.tsx +0 -584
  204. package/agent-pane/frontend/src/components/CommandPalette.test.tsx +0 -226
  205. package/agent-pane/frontend/src/components/CommandPalette.tsx +0 -194
  206. package/agent-pane/frontend/src/components/CommentableText.test.tsx +0 -196
  207. package/agent-pane/frontend/src/components/CommentableText.tsx +0 -379
  208. package/agent-pane/frontend/src/components/IdeWorkspace.test.tsx +0 -39
  209. package/agent-pane/frontend/src/components/IdeWorkspace.tsx +0 -51
  210. package/agent-pane/frontend/src/components/ModelSelector.test.tsx +0 -129
  211. package/agent-pane/frontend/src/components/ModelSelector.tsx +0 -191
  212. package/agent-pane/frontend/src/components/NewSessionDialog.test.tsx +0 -95
  213. package/agent-pane/frontend/src/components/NewSessionDialog.tsx +0 -129
  214. package/agent-pane/frontend/src/components/RenderArea.test.tsx +0 -160
  215. package/agent-pane/frontend/src/components/RenderArea.tsx +0 -170
  216. package/agent-pane/frontend/src/components/ResizeHandle.tsx +0 -29
  217. package/agent-pane/frontend/src/components/RightPanel.test.tsx +0 -43
  218. package/agent-pane/frontend/src/components/RightPanel.tsx +0 -46
  219. package/agent-pane/frontend/src/components/RuntimeIcon.test.tsx +0 -32
  220. package/agent-pane/frontend/src/components/RuntimeIcon.tsx +0 -16
  221. package/agent-pane/frontend/src/components/SessionTabs.test.tsx +0 -72
  222. package/agent-pane/frontend/src/components/SessionTabs.tsx +0 -49
  223. package/agent-pane/frontend/src/components/StatusBar.test.tsx +0 -56
  224. package/agent-pane/frontend/src/components/StatusBar.tsx +0 -129
  225. package/agent-pane/frontend/src/components/StatusDot.test.tsx +0 -25
  226. package/agent-pane/frontend/src/components/StatusDot.tsx +0 -8
  227. package/agent-pane/frontend/src/components/Toast.test.tsx +0 -79
  228. package/agent-pane/frontend/src/components/Toast.tsx +0 -76
  229. package/agent-pane/frontend/src/components/TurnContent.test.tsx +0 -377
  230. package/agent-pane/frontend/src/components/TurnContent.tsx +0 -594
  231. package/agent-pane/frontend/src/components/WorkspaceSidebar.test.tsx +0 -297
  232. package/agent-pane/frontend/src/components/WorkspaceSidebar.tsx +0 -351
  233. package/agent-pane/frontend/src/components/input/OptionSubPopup.tsx +0 -32
  234. package/agent-pane/frontend/src/components/input/SkillMentionPopup.test.tsx +0 -63
  235. package/agent-pane/frontend/src/components/input/SkillMentionPopup.tsx +0 -78
  236. package/agent-pane/frontend/src/components/input/SlashPopup.test.tsx +0 -96
  237. package/agent-pane/frontend/src/components/input/SlashPopup.tsx +0 -82
  238. package/agent-pane/frontend/src/components/panels/ChangesPanel.test.tsx +0 -61
  239. package/agent-pane/frontend/src/components/panels/ChangesPanel.tsx +0 -54
  240. package/agent-pane/frontend/src/components/panels/TasksPanel.test.tsx +0 -97
  241. package/agent-pane/frontend/src/components/panels/TasksPanel.tsx +0 -208
  242. package/agent-pane/frontend/src/components/panels/ThreadsPanel.tsx +0 -382
  243. package/agent-pane/frontend/src/components/panels/TurnsPanel.test.tsx +0 -131
  244. package/agent-pane/frontend/src/components/panels/TurnsPanel.tsx +0 -131
  245. package/agent-pane/frontend/src/constants/models.test.ts +0 -97
  246. package/agent-pane/frontend/src/constants/models.ts +0 -166
  247. package/agent-pane/frontend/src/hooks/useEscapeKey.test.ts +0 -42
  248. package/agent-pane/frontend/src/hooks/useEscapeKey.ts +0 -13
  249. package/agent-pane/frontend/src/hooks/useOutsideClick.test.ts +0 -92
  250. package/agent-pane/frontend/src/hooks/useOutsideClick.ts +0 -19
  251. package/agent-pane/frontend/src/hooks/useProvider.ts +0 -44
  252. package/agent-pane/frontend/src/hooks/useResizable.test.ts +0 -97
  253. package/agent-pane/frontend/src/hooks/useResizable.ts +0 -75
  254. package/agent-pane/frontend/src/hooks/useSessionStatus.test.ts +0 -116
  255. package/agent-pane/frontend/src/hooks/useSessionStatus.ts +0 -34
  256. package/agent-pane/frontend/src/main.test.tsx +0 -37
  257. package/agent-pane/frontend/src/main.tsx +0 -10
  258. package/agent-pane/frontend/src/store/sessionSlice.ts +0 -440
  259. package/agent-pane/frontend/src/store/store.test.ts +0 -1338
  260. package/agent-pane/frontend/src/store/store.ts +0 -37
  261. package/agent-pane/frontend/src/store/threadSlice.ts +0 -136
  262. package/agent-pane/frontend/src/store/uiSlice.ts +0 -133
  263. package/agent-pane/frontend/src/store/workspaceSlice.ts +0 -77
  264. package/agent-pane/frontend/src/store/wsSlice.ts +0 -368
  265. package/agent-pane/frontend/src/styles/app.css +0 -627
  266. package/agent-pane/frontend/src/styles/tokens.css +0 -62
  267. package/agent-pane/frontend/src/test/component-helpers.ts +0 -38
  268. package/agent-pane/frontend/src/test/fixtures.ts +0 -101
  269. package/agent-pane/frontend/src/test-setup.ts +0 -5
  270. package/agent-pane/frontend/src/types/index.test.ts +0 -128
  271. package/agent-pane/frontend/src/types/index.ts +0 -134
  272. package/agent-pane/frontend/src/utils/changes.test.ts +0 -81
  273. package/agent-pane/frontend/src/utils/changes.ts +0 -39
  274. package/agent-pane/frontend/src/utils/duration.test.ts +0 -76
  275. package/agent-pane/frontend/src/utils/duration.ts +0 -14
  276. package/agent-pane/frontend/tsconfig.json +0 -25
  277. package/agent-pane/frontend/vite.config.ts +0 -22
  278. package/agent-pane/frontend/vitest.config.ts +0 -29
  279. package/agent-pane/package-lock.json +0 -1527
  280. package/agent-pane/package.json +0 -19
  281. package/agent-pane/shared/provider-types.ts +0 -67
  282. 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
- });