repoburg 1.3.188 → 1.3.196

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 (265) hide show
  1. package/agent-pane/.prettierignore +5 -0
  2. package/agent-pane/.prettierrc +8 -0
  3. package/agent-pane/backend/coverage/base.css +224 -0
  4. package/agent-pane/backend/coverage/block-navigation.js +87 -0
  5. package/agent-pane/backend/coverage/clover.xml +887 -0
  6. package/agent-pane/backend/coverage/coverage-final.json +10 -0
  7. package/agent-pane/backend/coverage/favicon.png +0 -0
  8. package/agent-pane/backend/coverage/index.html +176 -0
  9. package/agent-pane/backend/coverage/prettify.css +1 -0
  10. package/agent-pane/backend/coverage/prettify.js +2 -0
  11. package/agent-pane/backend/coverage/sort-arrow-sprite.png +0 -0
  12. package/agent-pane/backend/coverage/sorter.js +210 -0
  13. package/agent-pane/backend/coverage/src/codex/codex-provider.ts.html +1402 -0
  14. package/agent-pane/backend/coverage/src/codex/index.html +131 -0
  15. package/agent-pane/backend/coverage/src/codex/item-mapper.ts.html +331 -0
  16. package/agent-pane/backend/coverage/src/db/index.html +131 -0
  17. package/agent-pane/backend/coverage/src/db/index.ts.html +436 -0
  18. package/agent-pane/backend/coverage/src/db/schema.ts.html +175 -0
  19. package/agent-pane/backend/coverage/src/index.html +116 -0
  20. package/agent-pane/backend/coverage/src/index.ts.html +262 -0
  21. package/agent-pane/backend/coverage/src/routes/index.html +146 -0
  22. package/agent-pane/backend/coverage/src/routes/sessions.ts.html +991 -0
  23. package/agent-pane/backend/coverage/src/routes/turns.ts.html +256 -0
  24. package/agent-pane/backend/coverage/src/routes/workspaces.ts.html +406 -0
  25. package/agent-pane/backend/coverage/src/ws/index.html +116 -0
  26. package/agent-pane/backend/coverage/src/ws/index.ts.html +238 -0
  27. package/agent-pane/backend/data.db +0 -0
  28. package/agent-pane/backend/data.db-shm +0 -0
  29. package/agent-pane/backend/data.db-wal +0 -0
  30. package/agent-pane/backend/dist/index.js +70869 -0
  31. package/agent-pane/backend/package-lock.json +4529 -1013
  32. package/agent-pane/backend/package.json +10 -6
  33. package/agent-pane/backend/src/claude/claude-message-mapper.test.ts +163 -0
  34. package/agent-pane/backend/src/claude/claude-message-mapper.ts +154 -0
  35. package/agent-pane/backend/src/claude/claude-provider.test.ts +65 -0
  36. package/agent-pane/backend/src/claude/claude-provider.ts +482 -0
  37. package/agent-pane/backend/src/claude/claude-types.ts +33 -0
  38. package/agent-pane/backend/src/codex/app-server-client.test.ts +183 -0
  39. package/agent-pane/backend/src/codex/app-server-client.ts +225 -0
  40. package/agent-pane/backend/src/codex/app-server-types.ts +259 -0
  41. package/agent-pane/backend/src/codex/codex-provider-runtime-config.test.ts +56 -0
  42. package/agent-pane/backend/src/codex/codex-provider.test.ts +60 -0
  43. package/agent-pane/backend/src/codex/codex-provider.ts +521 -265
  44. package/agent-pane/backend/src/codex/item-mapper.test.ts +221 -0
  45. package/agent-pane/backend/src/codex/item-mapper.ts +35 -8
  46. package/agent-pane/backend/src/codex/shared-app-server.ts +25 -0
  47. package/agent-pane/backend/src/db/index.test.ts +125 -0
  48. package/agent-pane/backend/src/db/index.ts +39 -8
  49. package/agent-pane/backend/src/db/schema.ts +28 -74
  50. package/agent-pane/backend/src/index.ts +29 -2
  51. package/agent-pane/backend/src/integration/__fixtures__/cancel-turn.json +29 -0
  52. package/agent-pane/backend/src/integration/__fixtures__/delta-streaming.json +40 -0
  53. package/agent-pane/backend/src/integration/__fixtures__/failed-turn.json +16 -0
  54. package/agent-pane/backend/src/integration/__fixtures__/mcp-tool-call.json +60 -0
  55. package/agent-pane/backend/src/integration/__fixtures__/missing-item-started.json +34 -0
  56. package/agent-pane/backend/src/integration/__fixtures__/multi-item-turn.json +85 -0
  57. package/agent-pane/backend/src/integration/__fixtures__/multi-turn.json +48 -0
  58. package/agent-pane/backend/src/integration/__fixtures__/simple-turn.json +35 -0
  59. package/agent-pane/backend/src/integration/delta-streaming.test.ts +130 -0
  60. package/agent-pane/backend/src/integration/helpers/app-server-mock.ts +272 -0
  61. package/agent-pane/backend/src/integration/helpers/test-app.ts +108 -0
  62. package/agent-pane/backend/src/integration/helpers/ws-capture.ts +64 -0
  63. package/agent-pane/backend/src/integration/item-lifecycle.test.ts +133 -0
  64. package/agent-pane/backend/src/integration/mcp-tool-call.test.ts +102 -0
  65. package/agent-pane/backend/src/integration/missing-item-started.test.ts +91 -0
  66. package/agent-pane/backend/src/integration/multi-turn.test.ts +121 -0
  67. package/agent-pane/backend/src/integration/session-operations.test.ts +168 -0
  68. package/agent-pane/backend/src/integration/title-generation.test.ts +109 -0
  69. package/agent-pane/backend/src/integration/turn-cancel.test.ts +111 -0
  70. package/agent-pane/backend/src/integration/turn-failure.test.ts +97 -0
  71. package/agent-pane/backend/src/integration/turn-lifecycle.test.ts +141 -0
  72. package/agent-pane/backend/src/providers/index.ts +14 -0
  73. package/agent-pane/backend/src/providers/registry.test.ts +92 -0
  74. package/agent-pane/backend/src/providers/registry.ts +43 -0
  75. package/agent-pane/backend/src/providers/runtime-provider.ts +25 -0
  76. package/agent-pane/backend/src/routes/files.ts +31 -0
  77. package/agent-pane/backend/src/routes/mcp.ts +33 -0
  78. package/agent-pane/backend/src/routes/models.test.ts +92 -0
  79. package/agent-pane/backend/src/routes/models.ts +16 -0
  80. package/agent-pane/backend/src/routes/plugins.ts +40 -0
  81. package/agent-pane/backend/src/routes/providers.test.ts +69 -0
  82. package/agent-pane/backend/src/routes/providers.ts +57 -0
  83. package/agent-pane/backend/src/routes/sessions.test.ts +424 -0
  84. package/agent-pane/backend/src/routes/sessions.ts +295 -42
  85. package/agent-pane/backend/src/routes/skills.test.ts +114 -0
  86. package/agent-pane/backend/src/routes/skills.ts +70 -0
  87. package/agent-pane/backend/src/routes/turns.test.ts +152 -0
  88. package/agent-pane/backend/src/routes/turns.ts +36 -16
  89. package/agent-pane/backend/src/routes/workspaces.test.ts +197 -0
  90. package/agent-pane/backend/src/routes/workspaces.ts +73 -55
  91. package/agent-pane/backend/src/ws/index.test.ts +107 -0
  92. package/agent-pane/backend/src/ws/index.ts +19 -11
  93. package/agent-pane/backend/tsconfig.json +4 -2
  94. package/agent-pane/backend/vitest.config.ts +20 -0
  95. package/agent-pane/eslint.config.js +33 -0
  96. package/agent-pane/frontend/coverage/base.css +224 -0
  97. package/agent-pane/frontend/coverage/block-navigation.js +87 -0
  98. package/agent-pane/frontend/coverage/clover.xml +2962 -0
  99. package/agent-pane/frontend/coverage/coverage-final.json +37 -0
  100. package/agent-pane/frontend/coverage/favicon.png +0 -0
  101. package/agent-pane/frontend/coverage/index.html +251 -0
  102. package/agent-pane/frontend/coverage/prettify.css +1 -0
  103. package/agent-pane/frontend/coverage/prettify.js +2 -0
  104. package/agent-pane/frontend/coverage/sort-arrow-sprite.png +0 -0
  105. package/agent-pane/frontend/coverage/sorter.js +210 -0
  106. package/agent-pane/frontend/coverage/src/App.tsx.html +217 -0
  107. package/agent-pane/frontend/coverage/src/AppInitializer.tsx.html +223 -0
  108. package/agent-pane/frontend/coverage/src/api/client.ts.html +187 -0
  109. package/agent-pane/frontend/coverage/src/api/index.html +161 -0
  110. package/agent-pane/frontend/coverage/src/api/sessions.ts.html +316 -0
  111. package/agent-pane/frontend/coverage/src/api/websocket.ts.html +544 -0
  112. package/agent-pane/frontend/coverage/src/api/workspaces.ts.html +199 -0
  113. package/agent-pane/frontend/coverage/src/components/CollapsibleToolCalls.tsx.html +226 -0
  114. package/agent-pane/frontend/coverage/src/components/CommandInputBar.tsx.html +382 -0
  115. package/agent-pane/frontend/coverage/src/components/CommandPalette.tsx.html +715 -0
  116. package/agent-pane/frontend/coverage/src/components/IdeWorkspace.tsx.html +160 -0
  117. package/agent-pane/frontend/coverage/src/components/ModelSelector.tsx.html +478 -0
  118. package/agent-pane/frontend/coverage/src/components/NewSessionDialog.tsx.html +472 -0
  119. package/agent-pane/frontend/coverage/src/components/RenderArea.tsx.html +574 -0
  120. package/agent-pane/frontend/coverage/src/components/RightPanel.tsx.html +205 -0
  121. package/agent-pane/frontend/coverage/src/components/RuntimeIcon.tsx.html +133 -0
  122. package/agent-pane/frontend/coverage/src/components/SessionTabs.tsx.html +217 -0
  123. package/agent-pane/frontend/coverage/src/components/StatusBar.tsx.html +271 -0
  124. package/agent-pane/frontend/coverage/src/components/StatusDot.tsx.html +109 -0
  125. package/agent-pane/frontend/coverage/src/components/Toast.tsx.html +313 -0
  126. package/agent-pane/frontend/coverage/src/components/TurnContent.tsx.html +1216 -0
  127. package/agent-pane/frontend/coverage/src/components/WorkspaceSidebar.tsx.html +1138 -0
  128. package/agent-pane/frontend/coverage/src/components/index.html +326 -0
  129. package/agent-pane/frontend/coverage/src/components/panels/ChangesPanel.tsx.html +247 -0
  130. package/agent-pane/frontend/coverage/src/components/panels/LogsPanel.tsx.html +160 -0
  131. package/agent-pane/frontend/coverage/src/components/panels/MetricsPanel.tsx.html +427 -0
  132. package/agent-pane/frontend/coverage/src/components/panels/TurnsPanel.tsx.html +397 -0
  133. package/agent-pane/frontend/coverage/src/components/panels/index.html +161 -0
  134. package/agent-pane/frontend/coverage/src/constants/index.html +116 -0
  135. package/agent-pane/frontend/coverage/src/constants/models.ts.html +406 -0
  136. package/agent-pane/frontend/coverage/src/hooks/index.html +146 -0
  137. package/agent-pane/frontend/coverage/src/hooks/useEscapeKey.ts.html +124 -0
  138. package/agent-pane/frontend/coverage/src/hooks/useOutsideClick.ts.html +142 -0
  139. package/agent-pane/frontend/coverage/src/hooks/useSessionStatus.ts.html +187 -0
  140. package/agent-pane/frontend/coverage/src/index.html +146 -0
  141. package/agent-pane/frontend/coverage/src/main.tsx.html +115 -0
  142. package/agent-pane/frontend/coverage/src/store/index.html +116 -0
  143. package/agent-pane/frontend/coverage/src/store/store.ts.html +2404 -0
  144. package/agent-pane/frontend/coverage/src/test/component-helpers.ts.html +190 -0
  145. package/agent-pane/frontend/coverage/src/test/fixtures.ts.html +367 -0
  146. package/agent-pane/frontend/coverage/src/test/index.html +131 -0
  147. package/agent-pane/frontend/coverage/src/types/index.html +116 -0
  148. package/agent-pane/frontend/coverage/src/types/index.ts.html +466 -0
  149. package/agent-pane/frontend/coverage/src/utils/changes.ts.html +202 -0
  150. package/agent-pane/frontend/coverage/src/utils/duration.ts.html +127 -0
  151. package/agent-pane/frontend/coverage/src/utils/index.html +131 -0
  152. package/agent-pane/frontend/dist/assets/index-Bg0PF2aV.css +1 -0
  153. package/agent-pane/frontend/dist/assets/index-BlybeX5e.js +281 -0
  154. package/agent-pane/frontend/dist/index.html +19 -0
  155. package/agent-pane/frontend/package-lock.json +2434 -213
  156. package/agent-pane/frontend/package.json +10 -3
  157. package/agent-pane/frontend/src/App.test.tsx +74 -0
  158. package/agent-pane/frontend/src/App.tsx +1 -1
  159. package/agent-pane/frontend/src/AppInitializer.test.tsx +142 -0
  160. package/agent-pane/frontend/src/AppInitializer.tsx +1 -1
  161. package/agent-pane/frontend/src/api/client.test.ts +126 -0
  162. package/agent-pane/frontend/src/api/client.ts +22 -29
  163. package/agent-pane/frontend/src/api/models.ts +7 -0
  164. package/agent-pane/frontend/src/api/providers.ts +12 -0
  165. package/agent-pane/frontend/src/api/sessions.test.ts +241 -0
  166. package/agent-pane/frontend/src/api/sessions.ts +43 -1
  167. package/agent-pane/frontend/src/api/skills.ts +12 -0
  168. package/agent-pane/frontend/src/api/websocket.test.ts +288 -0
  169. package/agent-pane/frontend/src/api/websocket.ts +27 -2
  170. package/agent-pane/frontend/src/api/workspaces.test.ts +112 -0
  171. package/agent-pane/frontend/src/api/workspaces.ts +17 -3
  172. package/agent-pane/frontend/src/components/CollapsibleToolCalls.test.tsx +49 -0
  173. package/agent-pane/frontend/src/components/CollapsibleToolCalls.tsx +2 -5
  174. package/agent-pane/frontend/src/components/CommandInputBar.test.tsx +133 -0
  175. package/agent-pane/frontend/src/components/CommandInputBar.tsx +296 -17
  176. package/agent-pane/frontend/src/components/CommandPalette.test.tsx +230 -0
  177. package/agent-pane/frontend/src/components/CommandPalette.tsx +26 -23
  178. package/agent-pane/frontend/src/components/IdeWorkspace.tsx +1 -1
  179. package/agent-pane/frontend/src/components/ModelSelector.test.tsx +129 -0
  180. package/agent-pane/frontend/src/components/ModelSelector.tsx +191 -0
  181. package/agent-pane/frontend/src/components/NewSessionDialog.test.tsx +95 -0
  182. package/agent-pane/frontend/src/components/NewSessionDialog.tsx +58 -13
  183. package/agent-pane/frontend/src/components/RenderArea.test.tsx +160 -0
  184. package/agent-pane/frontend/src/components/RenderArea.tsx +18 -25
  185. package/agent-pane/frontend/src/components/RightPanel.test.tsx +43 -0
  186. package/agent-pane/frontend/src/components/RightPanel.tsx +1 -1
  187. package/agent-pane/frontend/src/components/RuntimeIcon.test.tsx +32 -0
  188. package/agent-pane/frontend/src/components/RuntimeIcon.tsx +1 -1
  189. package/agent-pane/frontend/src/components/SessionTabs.test.tsx +72 -0
  190. package/agent-pane/frontend/src/components/SessionTabs.tsx +12 -12
  191. package/agent-pane/frontend/src/components/StatusBar.test.tsx +56 -0
  192. package/agent-pane/frontend/src/components/StatusBar.tsx +25 -31
  193. package/agent-pane/frontend/src/components/StatusDot.test.tsx +25 -0
  194. package/agent-pane/frontend/src/components/StatusDot.tsx +2 -7
  195. package/agent-pane/frontend/src/components/Toast.test.tsx +79 -0
  196. package/agent-pane/frontend/src/components/Toast.tsx +2 -6
  197. package/agent-pane/frontend/src/components/TurnContent.test.tsx +287 -0
  198. package/agent-pane/frontend/src/components/TurnContent.tsx +107 -24
  199. package/agent-pane/frontend/src/components/WorkspaceSidebar.test.tsx +297 -0
  200. package/agent-pane/frontend/src/components/WorkspaceSidebar.tsx +33 -32
  201. package/agent-pane/frontend/src/components/input/OptionSubPopup.tsx +32 -0
  202. package/agent-pane/frontend/src/components/input/SkillMentionPopup.test.tsx +63 -0
  203. package/agent-pane/frontend/src/components/input/SkillMentionPopup.tsx +78 -0
  204. package/agent-pane/frontend/src/components/input/SlashPopup.test.tsx +96 -0
  205. package/agent-pane/frontend/src/components/input/SlashPopup.tsx +63 -0
  206. package/agent-pane/frontend/src/components/panels/ChangesPanel.test.tsx +61 -0
  207. package/agent-pane/frontend/src/components/panels/ChangesPanel.tsx +12 -39
  208. package/agent-pane/frontend/src/components/panels/LogsPanel.test.tsx +58 -0
  209. package/agent-pane/frontend/src/components/panels/LogsPanel.tsx +1 -1
  210. package/agent-pane/frontend/src/components/panels/MetricsPanel.test.tsx +71 -0
  211. package/agent-pane/frontend/src/components/panels/MetricsPanel.tsx +63 -31
  212. package/agent-pane/frontend/src/components/panels/TurnsPanel.test.tsx +83 -0
  213. package/agent-pane/frontend/src/components/panels/TurnsPanel.tsx +66 -7
  214. package/agent-pane/frontend/src/constants/models.test.ts +97 -0
  215. package/agent-pane/frontend/src/constants/models.ts +129 -0
  216. package/agent-pane/frontend/src/hooks/useEscapeKey.test.ts +42 -0
  217. package/agent-pane/frontend/src/hooks/useEscapeKey.ts +13 -0
  218. package/agent-pane/frontend/src/hooks/useOutsideClick.test.ts +92 -0
  219. package/agent-pane/frontend/src/hooks/useOutsideClick.ts +19 -0
  220. package/agent-pane/frontend/src/hooks/useProvider.ts +44 -0
  221. package/agent-pane/frontend/src/hooks/useSessionStatus.test.ts +114 -0
  222. package/agent-pane/frontend/src/hooks/useSessionStatus.ts +34 -0
  223. package/agent-pane/frontend/src/main.test.tsx +37 -0
  224. package/agent-pane/frontend/src/main.tsx +1 -1
  225. package/agent-pane/frontend/src/store/sessionSlice.ts +417 -0
  226. package/agent-pane/frontend/src/store/store.test.ts +1284 -0
  227. package/agent-pane/frontend/src/store/store.ts +16 -617
  228. package/agent-pane/frontend/src/store/uiSlice.ts +83 -0
  229. package/agent-pane/frontend/src/store/workspaceSlice.ts +77 -0
  230. package/agent-pane/frontend/src/store/wsSlice.ts +292 -0
  231. package/agent-pane/frontend/src/styles/app.css +113 -7
  232. package/agent-pane/frontend/src/test/component-helpers.ts +36 -0
  233. package/agent-pane/frontend/src/test/fixtures.ts +96 -0
  234. package/agent-pane/frontend/src/test-setup.ts +5 -0
  235. package/agent-pane/frontend/src/types/index.test.ts +128 -0
  236. package/agent-pane/frontend/src/types/index.ts +80 -104
  237. package/agent-pane/frontend/src/utils/changes.test.ts +81 -0
  238. package/agent-pane/frontend/src/utils/changes.ts +39 -0
  239. package/agent-pane/frontend/src/utils/duration.test.ts +71 -0
  240. package/agent-pane/frontend/src/utils/duration.ts +14 -0
  241. package/agent-pane/frontend/tsconfig.json +4 -1
  242. package/agent-pane/frontend/vite.config.ts +6 -0
  243. package/agent-pane/frontend/vitest.config.ts +29 -0
  244. package/agent-pane/package-lock.json +1527 -0
  245. package/agent-pane/package.json +19 -0
  246. package/agent-pane/shared/provider-types.ts +53 -0
  247. package/agent-pane/shared/types.ts +107 -0
  248. package/agentpane-branch-session.png +0 -0
  249. package/agentpane-expanded-all.png +0 -0
  250. package/agentpane-fixed-rendering.png +0 -0
  251. package/agentpane-focus-check.png +0 -0
  252. package/agentpane-initial.png +0 -0
  253. package/agentpane-mcp-detail.png +0 -0
  254. package/agentpane-no-focus-ring.png +0 -0
  255. package/agentpane-soft-focus.png +0 -0
  256. package/backend/dist/src/llm-provider/ollama-llm.provider.js +1 -0
  257. package/backend/dist/src/llm-provider/ollama-llm.provider.js.map +1 -1
  258. package/backend/dist/tsconfig.build.tsbuildinfo +1 -1
  259. package/data.db +0 -0
  260. package/empty-state.png +0 -0
  261. package/frontend-home.png +0 -0
  262. package/model-selector-open.png +0 -0
  263. package/package.json +1 -1
  264. package/slash-popup-working.png +0 -0
  265. package/agent-pane/frontend/src/components/DiffViewer.tsx +0 -27
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Integration test: missing item.started auto-insert fallback.
3
+ *
4
+ * Verifies that when item.updated or item.completed arrives without
5
+ * a prior item.started, the handleEvent auto-inserts the item into DB.
6
+ *
7
+ * Uses HTTP POST to trigger the turn (fire-and-forget startTurn).
8
+ */
9
+
10
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
11
+ import { readFileSync } from 'node:fs';
12
+ import { resolve } from 'node:path';
13
+ import type Database from 'better-sqlite3';
14
+ import { createAppServerMock, type Fixture } from './helpers/app-server-mock.ts';
15
+ import { clearBroadcasts, capturedBroadcasts } from './helpers/ws-capture.ts';
16
+ import type { TestApp } from './helpers/test-app.ts';
17
+
18
+ const fixture = JSON.parse(
19
+ readFileSync(resolve(__dirname, '__fixtures__/missing-item-started.json'), 'utf-8'),
20
+ ) as Fixture;
21
+
22
+ vi.mock('../codex/app-server-client.ts', () => ({
23
+ AppServerClient: createAppServerMock(fixture),
24
+ }));
25
+
26
+ vi.mock('../ws/index.ts', async (importOriginal) => {
27
+ const actual = (await importOriginal()) as Record<string, unknown>;
28
+ return {
29
+ ...actual,
30
+ broadcast: vi.fn((event: string, data: unknown) => {
31
+ capturedBroadcasts.push({ event, data });
32
+ }),
33
+ };
34
+ });
35
+
36
+ const { getDb } = await import('../db/index.ts');
37
+ const { createTestApp, waitForTurn } = await import('./helpers/test-app.ts');
38
+
39
+ describe('Integration: missing item.started auto-insert', () => {
40
+ let testApp: TestApp;
41
+
42
+ beforeEach(async () => {
43
+ vi.clearAllMocks();
44
+ clearBroadcasts();
45
+ testApp = await createTestApp();
46
+ });
47
+
48
+ afterEach(async () => {
49
+ await testApp.cleanup();
50
+ });
51
+
52
+ it('auto-inserts item when item.updated arrives without item.started', async () => {
53
+ const db: Database.Database = getDb();
54
+
55
+ // Create session
56
+ const createRes = await testApp.app.inject({
57
+ method: 'POST',
58
+ url: '/api/sessions',
59
+ payload: {},
60
+ });
61
+ const session = createRes.json();
62
+
63
+ // Submit turn via HTTP
64
+ const turnRes = await testApp.app.inject({
65
+ method: 'POST',
66
+ url: `/api/sessions/${session.id}/turns`,
67
+ payload: { user_prompt: 'Hello' },
68
+ });
69
+ const turn = turnRes.json();
70
+
71
+ await waitForTurn(db, turn.id);
72
+
73
+ // Only 1 item in DB (no duplicate from missing item.started)
74
+ const items = db.prepare('SELECT * FROM items WHERE turn_id = ? ORDER BY created_at ASC').all(turn.id) as {
75
+ type: string;
76
+ data: string;
77
+ status: string;
78
+ }[];
79
+ expect(items).toHaveLength(1);
80
+ expect(items[0].type).toBe('agent_message');
81
+ expect(items[0].status).toBe('completed');
82
+ expect(JSON.parse(items[0].data)).toEqual({ text: 'Hello! How can I help?' });
83
+
84
+ // Verify WS: item:updated broadcast fired (auto-insert path)
85
+ const updatedBroadcasts = capturedBroadcasts.filter((b) => b.event === 'item:updated');
86
+ expect(updatedBroadcasts).toHaveLength(1);
87
+
88
+ const completedBroadcasts = capturedBroadcasts.filter((b) => b.event === 'item:completed');
89
+ expect(completedBroadcasts).toHaveLength(1);
90
+ });
91
+ });
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Integration test: multi-turn session — thread reuse and title only once.
3
+ *
4
+ * Verifies that a second turn in the same session reuses the existing
5
+ * thread (external_thread_id set after first turn) and that the title
6
+ * is only generated once (for the first turn).
7
+ *
8
+ * Uses HTTP POST to trigger each turn (fire-and-forget startTurn).
9
+ */
10
+
11
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
12
+ import { readFileSync } from 'node:fs';
13
+ import { resolve } from 'node:path';
14
+ import type Database from 'better-sqlite3';
15
+ import { createAppServerMock, type Fixture } from './helpers/app-server-mock.ts';
16
+ import { clearBroadcasts, capturedBroadcasts } from './helpers/ws-capture.ts';
17
+ import type { TestApp } from './helpers/test-app.ts';
18
+
19
+ const fixture = JSON.parse(readFileSync(resolve(__dirname, '__fixtures__/multi-turn.json'), 'utf-8')) as Fixture;
20
+
21
+ vi.mock('../codex/app-server-client.ts', () => ({
22
+ AppServerClient: createAppServerMock(fixture),
23
+ }));
24
+
25
+ vi.mock('../ws/index.ts', async (importOriginal) => {
26
+ const actual = (await importOriginal()) as Record<string, unknown>;
27
+ return {
28
+ ...actual,
29
+ broadcast: vi.fn((event: string, data: unknown) => {
30
+ capturedBroadcasts.push({ event, data });
31
+ }),
32
+ };
33
+ });
34
+
35
+ const { getDb } = await import('../db/index.ts');
36
+ const { createTestApp, waitForTurn } = await import('./helpers/test-app.ts');
37
+
38
+ describe('Integration: multi-turn', () => {
39
+ let testApp: TestApp;
40
+
41
+ beforeEach(async () => {
42
+ vi.clearAllMocks();
43
+ clearBroadcasts();
44
+ testApp = await createTestApp();
45
+ });
46
+
47
+ afterEach(async () => {
48
+ await testApp.cleanup();
49
+ });
50
+
51
+ it('handles two turns with thread reuse and title only once', async () => {
52
+ const db: Database.Database = getDb();
53
+
54
+ // Create session
55
+ const createRes = await testApp.app.inject({
56
+ method: 'POST',
57
+ url: '/api/sessions',
58
+ payload: {},
59
+ });
60
+ const session = createRes.json();
61
+
62
+ // Turn 1 via HTTP
63
+ const turn1Res = await testApp.app.inject({
64
+ method: 'POST',
65
+ url: `/api/sessions/${session.id}/turns`,
66
+ payload: { user_prompt: 'What is 2+2?' },
67
+ });
68
+ const turn1 = turn1Res.json();
69
+ await waitForTurn(db, turn1.id);
70
+
71
+ // After turn 1: external_thread_id should be set
72
+ const sessionAfterT1 = db
73
+ .prepare('SELECT external_thread_id, title FROM sessions WHERE id = ?')
74
+ .get(session.id) as { external_thread_id: string | null; title: string | null };
75
+ expect(sessionAfterT1.external_thread_id).toBe('thread_fix_005');
76
+
77
+ // Wait for title generation (fire-and-forget)
78
+ await new Promise((r) => setTimeout(r, 500));
79
+ const sessionTitled = db.prepare('SELECT title FROM sessions WHERE id = ?').get(session.id) as {
80
+ title: string | null;
81
+ };
82
+ expect(sessionTitled.title).toBe('Simple Math Questions');
83
+
84
+ // Turn 2 via HTTP
85
+ const turn2Res = await testApp.app.inject({
86
+ method: 'POST',
87
+ url: `/api/sessions/${session.id}/turns`,
88
+ payload: { user_prompt: 'What about 3+3?' },
89
+ });
90
+ const turn2 = turn2Res.json();
91
+ await waitForTurn(db, turn2.id);
92
+
93
+ // Both turns in DB
94
+ const turns = db.prepare('SELECT * FROM turns WHERE session_id = ? ORDER BY created_at ASC').all(session.id) as {
95
+ user_prompt: string;
96
+ agent_response: string | null;
97
+ status: string;
98
+ }[];
99
+ expect(turns).toHaveLength(2);
100
+ expect(turns[0].user_prompt).toBe('What is 2+2?');
101
+ expect(turns[0].agent_response).toBe('2+2 equals 4.');
102
+ expect(turns[0].status).toBe('completed');
103
+ expect(turns[1].user_prompt).toBe('What about 3+3?');
104
+ expect(turns[1].agent_response).toBe('3+3 equals 6.');
105
+ expect(turns[1].status).toBe('completed');
106
+
107
+ // Title should still be from first turn (not regenerated)
108
+ const finalTitle = db.prepare('SELECT title FROM sessions WHERE id = ?').get(session.id) as {
109
+ title: string | null;
110
+ };
111
+ expect(finalTitle.title).toBe('Simple Math Questions');
112
+
113
+ // Only one session:title broadcast (from first turn's title generation)
114
+ const titleBroadcasts = capturedBroadcasts.filter((b) => b.event === 'session:title');
115
+ expect(titleBroadcasts).toHaveLength(1);
116
+
117
+ // Two turn:completed broadcasts
118
+ const completedBroadcasts = capturedBroadcasts.filter((b) => b.event === 'turn:completed');
119
+ expect(completedBroadcasts).toHaveLength(2);
120
+ });
121
+ });
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Integration test: session operations — rewind, branch, model change.
3
+ *
4
+ * These operations interact with codex-provider's clearThread() function.
5
+ * Uses the real codex-provider.ts (not mocked) with a simple-turn fixture
6
+ * to populate DB state, then tests the operations via HTTP.
7
+ */
8
+
9
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
10
+ import { readFileSync } from 'node:fs';
11
+ import { resolve } from 'node:path';
12
+ import type Database from 'better-sqlite3';
13
+ import { createAppServerMock, type Fixture } from './helpers/app-server-mock.ts';
14
+ import { clearBroadcasts, capturedBroadcasts } from './helpers/ws-capture.ts';
15
+ import type { TestApp } from './helpers/test-app.ts';
16
+
17
+ const fixture = JSON.parse(readFileSync(resolve(__dirname, '__fixtures__/simple-turn.json'), 'utf-8')) as Fixture;
18
+
19
+ vi.mock('../codex/app-server-client.ts', () => ({
20
+ AppServerClient: createAppServerMock(fixture),
21
+ }));
22
+
23
+ vi.mock('../ws/index.ts', async (importOriginal) => {
24
+ const actual = (await importOriginal()) as Record<string, unknown>;
25
+ return {
26
+ ...actual,
27
+ broadcast: vi.fn((event: string, data: unknown) => {
28
+ capturedBroadcasts.push({ event, data });
29
+ }),
30
+ };
31
+ });
32
+
33
+ const { getDb } = await import('../db/index.ts');
34
+ const { createTestApp, insertTestSession } = await import('./helpers/test-app.ts');
35
+
36
+ describe('Integration: session operations', () => {
37
+ let testApp: TestApp;
38
+
39
+ beforeEach(async () => {
40
+ vi.clearAllMocks();
41
+ clearBroadcasts();
42
+ testApp = await createTestApp();
43
+ });
44
+
45
+ afterEach(async () => {
46
+ await testApp.cleanup();
47
+ });
48
+
49
+ it('rewind deletes turns after target and nulls external_thread_id', async () => {
50
+ const db: Database.Database = getDb();
51
+
52
+ insertTestSession(db, 's1', { external_thread_id: 'thread_old' });
53
+
54
+ // Insert two turns manually
55
+ const now1 = '2025-01-01T00:00:00Z';
56
+ const now2 = '2025-01-01T01:00:00Z';
57
+ db.prepare(
58
+ `INSERT INTO turns (id, session_id, user_prompt, agent_response, thoughts, token_usage, external_turn_id, status, error, created_at, updated_at)
59
+ VALUES (?, ?, ?, NULL, NULL, NULL, NULL, 'completed', NULL, ?, ?)`,
60
+ ).run('t1', 's1', 'first', now1, now1);
61
+ db.prepare(
62
+ `INSERT INTO turns (id, session_id, user_prompt, agent_response, thoughts, token_usage, external_turn_id, status, error, created_at, updated_at)
63
+ VALUES (?, ?, ?, NULL, NULL, NULL, NULL, 'completed', NULL, ?, ?)`,
64
+ ).run('t2', 's1', 'second', now2, now2);
65
+
66
+ // Rewind to t1 — deletes t2 and after
67
+ const res = await testApp.app.inject({
68
+ method: 'POST',
69
+ url: '/api/sessions/s1/rewind',
70
+ payload: { turn_id: 't1' },
71
+ });
72
+ expect(res.statusCode).toBe(200);
73
+
74
+ // Only t1 remains
75
+ const turns = db.prepare('SELECT id FROM turns WHERE session_id = ?').all('s1') as {
76
+ id: string;
77
+ }[];
78
+ expect(turns).toHaveLength(1);
79
+ expect(turns[0].id).toBe('t1');
80
+
81
+ // external_thread_id nulled
82
+ const session = db.prepare('SELECT external_thread_id FROM sessions WHERE id = ?').get('s1') as {
83
+ external_thread_id: string | null;
84
+ };
85
+ expect(session.external_thread_id).toBeNull();
86
+ });
87
+
88
+ it('branch creates new session with copied turns and items', async () => {
89
+ const db: Database.Database = getDb();
90
+
91
+ insertTestSession(db, 's1', { title: 'Original', model: 'gpt-4' });
92
+
93
+ const now = '2025-01-01T00:00:00Z';
94
+ db.prepare(
95
+ `INSERT INTO turns (id, session_id, user_prompt, agent_response, thoughts, token_usage, external_turn_id, status, error, created_at, updated_at)
96
+ VALUES (?, ?, ?, 'answer', NULL, NULL, NULL, 'completed', NULL, ?, ?)`,
97
+ ).run('t1', 's1', 'hello', now, now);
98
+ db.prepare(
99
+ `INSERT INTO items (id, turn_id, external_item_id, type, data, status, created_at, updated_at)
100
+ VALUES (?, ?, ?, 'agent_message', ?, 'completed', ?, ?)`,
101
+ ).run('i1', 't1', 'ext1', JSON.stringify({ text: 'answer' }), now, now);
102
+
103
+ const res = await testApp.app.inject({
104
+ method: 'POST',
105
+ url: '/api/sessions/s1/branch',
106
+ payload: { turn_id: 't1' },
107
+ });
108
+ expect(res.statusCode).toBe(200);
109
+ const newSession = res.json();
110
+ expect(newSession.id).not.toBe('s1');
111
+ expect(newSession.title).toBe('Branch of Original');
112
+ expect(newSession.model).toBe('gpt-4');
113
+ expect(newSession.external_thread_id).toBeNull();
114
+
115
+ // New session has copied turn + item
116
+ const newTurns = db.prepare('SELECT * FROM turns WHERE session_id = ?').all(newSession.id) as {
117
+ user_prompt: string;
118
+ agent_response: string;
119
+ }[];
120
+ expect(newTurns).toHaveLength(1);
121
+ expect(newTurns[0].user_prompt).toBe('hello');
122
+ expect(newTurns[0].agent_response).toBe('answer');
123
+
124
+ const newItems = db
125
+ .prepare('SELECT * FROM items WHERE turn_id IN (SELECT id FROM turns WHERE session_id = ?)')
126
+ .all(newSession.id) as { type: string }[];
127
+ expect(newItems).toHaveLength(1);
128
+ expect(newItems[0].type).toBe('agent_message');
129
+ });
130
+
131
+ it('model change clears thread when external_thread_id exists', async () => {
132
+ const db: Database.Database = getDb();
133
+
134
+ insertTestSession(db, 's1', { model: 'gpt-4', external_thread_id: 'thread_123' });
135
+
136
+ const res = await testApp.app.inject({
137
+ method: 'PATCH',
138
+ url: '/api/sessions/s1',
139
+ payload: { model: 'o3' },
140
+ });
141
+ expect(res.statusCode).toBe(200);
142
+
143
+ const session = db.prepare('SELECT external_thread_id, model FROM sessions WHERE id = ?').get('s1') as {
144
+ external_thread_id: string | null;
145
+ model: string | null;
146
+ };
147
+ expect(session.external_thread_id).toBeNull();
148
+ expect(session.model).toBe('o3');
149
+ });
150
+
151
+ it('model change does NOT clear thread when external_thread_id is null', async () => {
152
+ const db: Database.Database = getDb();
153
+
154
+ insertTestSession(db, 's1', { model: 'gpt-4' });
155
+
156
+ const res = await testApp.app.inject({
157
+ method: 'PATCH',
158
+ url: '/api/sessions/s1',
159
+ payload: { model: 'o3' },
160
+ });
161
+ expect(res.statusCode).toBe(200);
162
+
163
+ const session = db.prepare('SELECT external_thread_id FROM sessions WHERE id = ?').get('s1') as {
164
+ external_thread_id: string | null;
165
+ };
166
+ expect(session.external_thread_id).toBeNull();
167
+ });
168
+ });
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Integration test: title auto-generation.
3
+ *
4
+ * Verifies that after a turn completes, a throwaway Codex thread is
5
+ * used to generate a title, which is saved to the DB and broadcast
6
+ * via session:title.
7
+ *
8
+ * Uses HTTP POST to trigger the turn (fire-and-forget startTurn).
9
+ */
10
+
11
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
12
+ import { readFileSync } from 'node:fs';
13
+ import { resolve } from 'node:path';
14
+ import type Database from 'better-sqlite3';
15
+ import { createAppServerMock, type Fixture } from './helpers/app-server-mock.ts';
16
+ import { clearBroadcasts, findBroadcast, capturedBroadcasts } from './helpers/ws-capture.ts';
17
+ import type { TestApp } from './helpers/test-app.ts';
18
+
19
+ const fixture = JSON.parse(readFileSync(resolve(__dirname, '__fixtures__/simple-turn.json'), 'utf-8')) as Fixture;
20
+
21
+ vi.mock('../codex/app-server-client.ts', () => ({
22
+ AppServerClient: createAppServerMock(fixture),
23
+ }));
24
+
25
+ vi.mock('../ws/index.ts', async (importOriginal) => {
26
+ const actual = (await importOriginal()) as Record<string, unknown>;
27
+ return {
28
+ ...actual,
29
+ broadcast: vi.fn((event: string, data: unknown) => {
30
+ capturedBroadcasts.push({ event, data });
31
+ }),
32
+ };
33
+ });
34
+
35
+ const { getDb } = await import('../db/index.ts');
36
+ const { createTestApp, waitForTurn, insertTestSession } = await import('./helpers/test-app.ts');
37
+
38
+ describe('Integration: title generation', () => {
39
+ let testApp: TestApp;
40
+
41
+ beforeEach(async () => {
42
+ vi.clearAllMocks();
43
+ clearBroadcasts();
44
+ testApp = await createTestApp();
45
+ });
46
+
47
+ afterEach(async () => {
48
+ await testApp.cleanup();
49
+ });
50
+
51
+ it('generates title after first turn via throwaway thread', async () => {
52
+ const db: Database.Database = getDb();
53
+
54
+ insertTestSession(db, 's1');
55
+
56
+ // Submit turn via HTTP
57
+ const turnRes = await testApp.app.inject({
58
+ method: 'POST',
59
+ url: '/api/sessions/s1/turns',
60
+ payload: { user_prompt: 'Hello, how are you?' },
61
+ });
62
+ const turn = turnRes.json();
63
+
64
+ await waitForTurn(db, turn.id);
65
+
66
+ // Wait for fire-and-forget title generation
67
+ await new Promise((r) => setTimeout(r, 500));
68
+
69
+ // Title in DB
70
+ const dbSession = db.prepare('SELECT title FROM sessions WHERE id = ?').get('s1') as {
71
+ title: string | null;
72
+ };
73
+ expect(dbSession.title).toBe('Simple Hello Chat');
74
+
75
+ // session:title broadcast
76
+ const titleData = findBroadcast('session:title') as { sessionId: string; title: string };
77
+ expect(titleData.sessionId).toBe('s1');
78
+ expect(titleData.title).toBe('Simple Hello Chat');
79
+ });
80
+
81
+ it('does not regenerate title when session already has one', async () => {
82
+ const db: Database.Database = getDb();
83
+
84
+ insertTestSession(db, 's1', { title: 'Existing Title' });
85
+
86
+ // Submit turn via HTTP
87
+ const turnRes = await testApp.app.inject({
88
+ method: 'POST',
89
+ url: '/api/sessions/s1/turns',
90
+ payload: { user_prompt: 'Hello, how are you?' },
91
+ });
92
+ const turn = turnRes.json();
93
+
94
+ await waitForTurn(db, turn.id);
95
+
96
+ // Wait for any potential title generation
97
+ await new Promise((r) => setTimeout(r, 500));
98
+
99
+ // Title should NOT have changed
100
+ const dbSession = db.prepare('SELECT title FROM sessions WHERE id = ?').get('s1') as {
101
+ title: string | null;
102
+ };
103
+ expect(dbSession.title).toBe('Existing Title');
104
+
105
+ // No session:title broadcast
106
+ const titleBroadcasts = capturedBroadcasts.filter((b) => b.event === 'session:title');
107
+ expect(titleBroadcasts).toHaveLength(0);
108
+ });
109
+ });
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Integration test: turn cancellation.
3
+ *
4
+ * Verifies that aborting a running turn sets the correct DB state
5
+ * and broadcasts turn:cancelled.
6
+ *
7
+ * Uses manual turn insertion + direct startTurn call to control cancel timing.
8
+ */
9
+
10
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
11
+ import { readFileSync } from 'node:fs';
12
+ import { resolve } from 'node:path';
13
+ import type Database from 'better-sqlite3';
14
+ import { createAppServerMock, type Fixture } from './helpers/app-server-mock.ts';
15
+ import { clearBroadcasts, findBroadcast, capturedBroadcasts } from './helpers/ws-capture.ts';
16
+ import type { TestApp } from './helpers/test-app.ts';
17
+
18
+ const fixture = JSON.parse(readFileSync(resolve(__dirname, '__fixtures__/cancel-turn.json'), 'utf-8')) as Fixture;
19
+
20
+ vi.mock('../codex/app-server-client.ts', () => ({
21
+ AppServerClient: createAppServerMock(fixture),
22
+ }));
23
+
24
+ vi.mock('../ws/index.ts', async (importOriginal) => {
25
+ const actual = (await importOriginal()) as Record<string, unknown>;
26
+ return {
27
+ ...actual,
28
+ broadcast: vi.fn((event: string, data: unknown) => {
29
+ capturedBroadcasts.push({ event, data });
30
+ }),
31
+ };
32
+ });
33
+
34
+ const { startTurn, interruptTurn } = await import('../codex/codex-provider.ts');
35
+ const { getDb } = await import('../db/index.ts');
36
+ const { createTestApp, waitForTurn, insertTestSession } = await import('./helpers/test-app.ts');
37
+
38
+ describe('Integration: turn cancellation', () => {
39
+ let testApp: TestApp;
40
+
41
+ beforeEach(async () => {
42
+ vi.clearAllMocks();
43
+ clearBroadcasts();
44
+ testApp = await createTestApp();
45
+ });
46
+
47
+ afterEach(async () => {
48
+ await testApp.cleanup();
49
+ });
50
+
51
+ it('aborts a running turn and sets correct DB + WS state', async () => {
52
+ const db: Database.Database = getDb();
53
+
54
+ // Create session via HTTP
55
+ insertTestSession(db, 's1');
56
+
57
+ // Insert turn manually (not via HTTP — avoids fire-and-forget startTurn)
58
+ const now = new Date().toISOString();
59
+ db.prepare(
60
+ `INSERT INTO turns (id, session_id, user_prompt, agent_response, thoughts, token_usage, external_turn_id, status, error, created_at, updated_at)
61
+ VALUES (?, ?, ?, NULL, NULL, NULL, NULL, 'pending', NULL, ?, ?)`,
62
+ ).run('t1', 's1', 'Write a long essay about history', now, now);
63
+
64
+ const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get('s1') as Record<string, unknown>;
65
+ const turn = db.prepare('SELECT * FROM turns WHERE id = ?').get('t1') as Record<string, unknown>;
66
+
67
+ // Start turn directly — do NOT await (need to cancel while running)
68
+ const turnPromise = startTurn(session as never, turn as never);
69
+
70
+ // Give it a moment to start streaming and set up abortController
71
+ await new Promise((r) => setTimeout(r, 50));
72
+
73
+ // Check that abortController is registered
74
+ const isRunning = db.prepare('SELECT is_running FROM sessions WHERE id = ?').get('s1') as { is_running: number };
75
+ expect(isRunning.is_running).toBe(1);
76
+
77
+ // Cancel the turn
78
+ const cancelled = await interruptTurn('s1');
79
+ expect(cancelled).toBe(true);
80
+
81
+ // Wait for the turn promise to resolve
82
+ await turnPromise;
83
+ await waitForTurn(db, 't1');
84
+
85
+ // Assert DB state
86
+ const dbTurn = db.prepare('SELECT status FROM turns WHERE id = ?').get('t1') as {
87
+ status: string;
88
+ };
89
+ expect(dbTurn.status).toBe('aborted');
90
+
91
+ const dbSession = db.prepare('SELECT is_running, status FROM sessions WHERE id = ?').get('s1') as {
92
+ is_running: number;
93
+ status: string;
94
+ };
95
+ expect(dbSession.is_running).toBe(0);
96
+ expect(dbSession.status).toBe('active');
97
+
98
+ // Assert WS broadcast
99
+ const cancelledData = findBroadcast('turn:cancelled') as {
100
+ sessionId: string;
101
+ turnId: string;
102
+ };
103
+ expect(cancelledData.sessionId).toBe('s1');
104
+ expect(cancelledData.turnId).toBe('t1');
105
+ });
106
+
107
+ it('returns false when interrupting a non-running session', async () => {
108
+ const result = await interruptTurn('nonexistent-session');
109
+ expect(result).toBe(false);
110
+ });
111
+ });