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.
- package/agent-pane/.prettierignore +5 -0
- package/agent-pane/.prettierrc +8 -0
- package/agent-pane/backend/coverage/base.css +224 -0
- package/agent-pane/backend/coverage/block-navigation.js +87 -0
- package/agent-pane/backend/coverage/clover.xml +887 -0
- package/agent-pane/backend/coverage/coverage-final.json +10 -0
- package/agent-pane/backend/coverage/favicon.png +0 -0
- package/agent-pane/backend/coverage/index.html +176 -0
- package/agent-pane/backend/coverage/prettify.css +1 -0
- package/agent-pane/backend/coverage/prettify.js +2 -0
- package/agent-pane/backend/coverage/sort-arrow-sprite.png +0 -0
- package/agent-pane/backend/coverage/sorter.js +210 -0
- package/agent-pane/backend/coverage/src/codex/codex-provider.ts.html +1402 -0
- package/agent-pane/backend/coverage/src/codex/index.html +131 -0
- package/agent-pane/backend/coverage/src/codex/item-mapper.ts.html +331 -0
- package/agent-pane/backend/coverage/src/db/index.html +131 -0
- package/agent-pane/backend/coverage/src/db/index.ts.html +436 -0
- package/agent-pane/backend/coverage/src/db/schema.ts.html +175 -0
- package/agent-pane/backend/coverage/src/index.html +116 -0
- package/agent-pane/backend/coverage/src/index.ts.html +262 -0
- package/agent-pane/backend/coverage/src/routes/index.html +146 -0
- package/agent-pane/backend/coverage/src/routes/sessions.ts.html +991 -0
- package/agent-pane/backend/coverage/src/routes/turns.ts.html +256 -0
- package/agent-pane/backend/coverage/src/routes/workspaces.ts.html +406 -0
- package/agent-pane/backend/coverage/src/ws/index.html +116 -0
- package/agent-pane/backend/coverage/src/ws/index.ts.html +238 -0
- package/agent-pane/backend/data.db +0 -0
- package/agent-pane/backend/data.db-shm +0 -0
- package/agent-pane/backend/data.db-wal +0 -0
- package/agent-pane/backend/dist/index.js +70869 -0
- package/agent-pane/backend/package-lock.json +4529 -1013
- package/agent-pane/backend/package.json +10 -6
- package/agent-pane/backend/src/claude/claude-message-mapper.test.ts +163 -0
- package/agent-pane/backend/src/claude/claude-message-mapper.ts +154 -0
- package/agent-pane/backend/src/claude/claude-provider.test.ts +65 -0
- package/agent-pane/backend/src/claude/claude-provider.ts +482 -0
- package/agent-pane/backend/src/claude/claude-types.ts +33 -0
- package/agent-pane/backend/src/codex/app-server-client.test.ts +183 -0
- package/agent-pane/backend/src/codex/app-server-client.ts +225 -0
- package/agent-pane/backend/src/codex/app-server-types.ts +259 -0
- package/agent-pane/backend/src/codex/codex-provider-runtime-config.test.ts +56 -0
- package/agent-pane/backend/src/codex/codex-provider.test.ts +60 -0
- package/agent-pane/backend/src/codex/codex-provider.ts +521 -265
- package/agent-pane/backend/src/codex/item-mapper.test.ts +221 -0
- package/agent-pane/backend/src/codex/item-mapper.ts +35 -8
- package/agent-pane/backend/src/codex/shared-app-server.ts +25 -0
- package/agent-pane/backend/src/db/index.test.ts +125 -0
- package/agent-pane/backend/src/db/index.ts +39 -8
- package/agent-pane/backend/src/db/schema.ts +28 -74
- package/agent-pane/backend/src/index.ts +29 -2
- package/agent-pane/backend/src/integration/__fixtures__/cancel-turn.json +29 -0
- package/agent-pane/backend/src/integration/__fixtures__/delta-streaming.json +40 -0
- package/agent-pane/backend/src/integration/__fixtures__/failed-turn.json +16 -0
- package/agent-pane/backend/src/integration/__fixtures__/mcp-tool-call.json +60 -0
- package/agent-pane/backend/src/integration/__fixtures__/missing-item-started.json +34 -0
- package/agent-pane/backend/src/integration/__fixtures__/multi-item-turn.json +85 -0
- package/agent-pane/backend/src/integration/__fixtures__/multi-turn.json +48 -0
- package/agent-pane/backend/src/integration/__fixtures__/simple-turn.json +35 -0
- package/agent-pane/backend/src/integration/delta-streaming.test.ts +130 -0
- package/agent-pane/backend/src/integration/helpers/app-server-mock.ts +272 -0
- package/agent-pane/backend/src/integration/helpers/test-app.ts +108 -0
- package/agent-pane/backend/src/integration/helpers/ws-capture.ts +64 -0
- package/agent-pane/backend/src/integration/item-lifecycle.test.ts +133 -0
- package/agent-pane/backend/src/integration/mcp-tool-call.test.ts +102 -0
- package/agent-pane/backend/src/integration/missing-item-started.test.ts +91 -0
- package/agent-pane/backend/src/integration/multi-turn.test.ts +121 -0
- package/agent-pane/backend/src/integration/session-operations.test.ts +168 -0
- package/agent-pane/backend/src/integration/title-generation.test.ts +109 -0
- package/agent-pane/backend/src/integration/turn-cancel.test.ts +111 -0
- package/agent-pane/backend/src/integration/turn-failure.test.ts +97 -0
- package/agent-pane/backend/src/integration/turn-lifecycle.test.ts +141 -0
- package/agent-pane/backend/src/providers/index.ts +14 -0
- package/agent-pane/backend/src/providers/registry.test.ts +92 -0
- package/agent-pane/backend/src/providers/registry.ts +43 -0
- package/agent-pane/backend/src/providers/runtime-provider.ts +25 -0
- package/agent-pane/backend/src/routes/files.ts +31 -0
- package/agent-pane/backend/src/routes/mcp.ts +33 -0
- package/agent-pane/backend/src/routes/models.test.ts +92 -0
- package/agent-pane/backend/src/routes/models.ts +16 -0
- package/agent-pane/backend/src/routes/plugins.ts +40 -0
- package/agent-pane/backend/src/routes/providers.test.ts +69 -0
- package/agent-pane/backend/src/routes/providers.ts +57 -0
- package/agent-pane/backend/src/routes/sessions.test.ts +424 -0
- package/agent-pane/backend/src/routes/sessions.ts +295 -42
- package/agent-pane/backend/src/routes/skills.test.ts +114 -0
- package/agent-pane/backend/src/routes/skills.ts +70 -0
- package/agent-pane/backend/src/routes/turns.test.ts +152 -0
- package/agent-pane/backend/src/routes/turns.ts +36 -16
- package/agent-pane/backend/src/routes/workspaces.test.ts +197 -0
- package/agent-pane/backend/src/routes/workspaces.ts +73 -55
- package/agent-pane/backend/src/ws/index.test.ts +107 -0
- package/agent-pane/backend/src/ws/index.ts +19 -11
- package/agent-pane/backend/tsconfig.json +4 -2
- package/agent-pane/backend/vitest.config.ts +20 -0
- package/agent-pane/eslint.config.js +33 -0
- package/agent-pane/frontend/coverage/base.css +224 -0
- package/agent-pane/frontend/coverage/block-navigation.js +87 -0
- package/agent-pane/frontend/coverage/clover.xml +2962 -0
- package/agent-pane/frontend/coverage/coverage-final.json +37 -0
- package/agent-pane/frontend/coverage/favicon.png +0 -0
- package/agent-pane/frontend/coverage/index.html +251 -0
- package/agent-pane/frontend/coverage/prettify.css +1 -0
- package/agent-pane/frontend/coverage/prettify.js +2 -0
- package/agent-pane/frontend/coverage/sort-arrow-sprite.png +0 -0
- package/agent-pane/frontend/coverage/sorter.js +210 -0
- package/agent-pane/frontend/coverage/src/App.tsx.html +217 -0
- package/agent-pane/frontend/coverage/src/AppInitializer.tsx.html +223 -0
- package/agent-pane/frontend/coverage/src/api/client.ts.html +187 -0
- package/agent-pane/frontend/coverage/src/api/index.html +161 -0
- package/agent-pane/frontend/coverage/src/api/sessions.ts.html +316 -0
- package/agent-pane/frontend/coverage/src/api/websocket.ts.html +544 -0
- package/agent-pane/frontend/coverage/src/api/workspaces.ts.html +199 -0
- package/agent-pane/frontend/coverage/src/components/CollapsibleToolCalls.tsx.html +226 -0
- package/agent-pane/frontend/coverage/src/components/CommandInputBar.tsx.html +382 -0
- package/agent-pane/frontend/coverage/src/components/CommandPalette.tsx.html +715 -0
- package/agent-pane/frontend/coverage/src/components/IdeWorkspace.tsx.html +160 -0
- package/agent-pane/frontend/coverage/src/components/ModelSelector.tsx.html +478 -0
- package/agent-pane/frontend/coverage/src/components/NewSessionDialog.tsx.html +472 -0
- package/agent-pane/frontend/coverage/src/components/RenderArea.tsx.html +574 -0
- package/agent-pane/frontend/coverage/src/components/RightPanel.tsx.html +205 -0
- package/agent-pane/frontend/coverage/src/components/RuntimeIcon.tsx.html +133 -0
- package/agent-pane/frontend/coverage/src/components/SessionTabs.tsx.html +217 -0
- package/agent-pane/frontend/coverage/src/components/StatusBar.tsx.html +271 -0
- package/agent-pane/frontend/coverage/src/components/StatusDot.tsx.html +109 -0
- package/agent-pane/frontend/coverage/src/components/Toast.tsx.html +313 -0
- package/agent-pane/frontend/coverage/src/components/TurnContent.tsx.html +1216 -0
- package/agent-pane/frontend/coverage/src/components/WorkspaceSidebar.tsx.html +1138 -0
- package/agent-pane/frontend/coverage/src/components/index.html +326 -0
- package/agent-pane/frontend/coverage/src/components/panels/ChangesPanel.tsx.html +247 -0
- package/agent-pane/frontend/coverage/src/components/panels/LogsPanel.tsx.html +160 -0
- package/agent-pane/frontend/coverage/src/components/panels/MetricsPanel.tsx.html +427 -0
- package/agent-pane/frontend/coverage/src/components/panels/TurnsPanel.tsx.html +397 -0
- package/agent-pane/frontend/coverage/src/components/panels/index.html +161 -0
- package/agent-pane/frontend/coverage/src/constants/index.html +116 -0
- package/agent-pane/frontend/coverage/src/constants/models.ts.html +406 -0
- package/agent-pane/frontend/coverage/src/hooks/index.html +146 -0
- package/agent-pane/frontend/coverage/src/hooks/useEscapeKey.ts.html +124 -0
- package/agent-pane/frontend/coverage/src/hooks/useOutsideClick.ts.html +142 -0
- package/agent-pane/frontend/coverage/src/hooks/useSessionStatus.ts.html +187 -0
- package/agent-pane/frontend/coverage/src/index.html +146 -0
- package/agent-pane/frontend/coverage/src/main.tsx.html +115 -0
- package/agent-pane/frontend/coverage/src/store/index.html +116 -0
- package/agent-pane/frontend/coverage/src/store/store.ts.html +2404 -0
- package/agent-pane/frontend/coverage/src/test/component-helpers.ts.html +190 -0
- package/agent-pane/frontend/coverage/src/test/fixtures.ts.html +367 -0
- package/agent-pane/frontend/coverage/src/test/index.html +131 -0
- package/agent-pane/frontend/coverage/src/types/index.html +116 -0
- package/agent-pane/frontend/coverage/src/types/index.ts.html +466 -0
- package/agent-pane/frontend/coverage/src/utils/changes.ts.html +202 -0
- package/agent-pane/frontend/coverage/src/utils/duration.ts.html +127 -0
- package/agent-pane/frontend/coverage/src/utils/index.html +131 -0
- package/agent-pane/frontend/dist/assets/index-Bg0PF2aV.css +1 -0
- package/agent-pane/frontend/dist/assets/index-BlybeX5e.js +281 -0
- package/agent-pane/frontend/dist/index.html +19 -0
- package/agent-pane/frontend/package-lock.json +2434 -213
- package/agent-pane/frontend/package.json +10 -3
- package/agent-pane/frontend/src/App.test.tsx +74 -0
- package/agent-pane/frontend/src/App.tsx +1 -1
- package/agent-pane/frontend/src/AppInitializer.test.tsx +142 -0
- package/agent-pane/frontend/src/AppInitializer.tsx +1 -1
- package/agent-pane/frontend/src/api/client.test.ts +126 -0
- package/agent-pane/frontend/src/api/client.ts +22 -29
- package/agent-pane/frontend/src/api/models.ts +7 -0
- package/agent-pane/frontend/src/api/providers.ts +12 -0
- package/agent-pane/frontend/src/api/sessions.test.ts +241 -0
- package/agent-pane/frontend/src/api/sessions.ts +43 -1
- package/agent-pane/frontend/src/api/skills.ts +12 -0
- package/agent-pane/frontend/src/api/websocket.test.ts +288 -0
- package/agent-pane/frontend/src/api/websocket.ts +27 -2
- package/agent-pane/frontend/src/api/workspaces.test.ts +112 -0
- package/agent-pane/frontend/src/api/workspaces.ts +17 -3
- package/agent-pane/frontend/src/components/CollapsibleToolCalls.test.tsx +49 -0
- package/agent-pane/frontend/src/components/CollapsibleToolCalls.tsx +2 -5
- package/agent-pane/frontend/src/components/CommandInputBar.test.tsx +133 -0
- package/agent-pane/frontend/src/components/CommandInputBar.tsx +296 -17
- package/agent-pane/frontend/src/components/CommandPalette.test.tsx +230 -0
- package/agent-pane/frontend/src/components/CommandPalette.tsx +26 -23
- package/agent-pane/frontend/src/components/IdeWorkspace.tsx +1 -1
- package/agent-pane/frontend/src/components/ModelSelector.test.tsx +129 -0
- package/agent-pane/frontend/src/components/ModelSelector.tsx +191 -0
- package/agent-pane/frontend/src/components/NewSessionDialog.test.tsx +95 -0
- package/agent-pane/frontend/src/components/NewSessionDialog.tsx +58 -13
- package/agent-pane/frontend/src/components/RenderArea.test.tsx +160 -0
- package/agent-pane/frontend/src/components/RenderArea.tsx +18 -25
- package/agent-pane/frontend/src/components/RightPanel.test.tsx +43 -0
- package/agent-pane/frontend/src/components/RightPanel.tsx +1 -1
- package/agent-pane/frontend/src/components/RuntimeIcon.test.tsx +32 -0
- package/agent-pane/frontend/src/components/RuntimeIcon.tsx +1 -1
- package/agent-pane/frontend/src/components/SessionTabs.test.tsx +72 -0
- package/agent-pane/frontend/src/components/SessionTabs.tsx +12 -12
- package/agent-pane/frontend/src/components/StatusBar.test.tsx +56 -0
- package/agent-pane/frontend/src/components/StatusBar.tsx +25 -31
- package/agent-pane/frontend/src/components/StatusDot.test.tsx +25 -0
- package/agent-pane/frontend/src/components/StatusDot.tsx +2 -7
- package/agent-pane/frontend/src/components/Toast.test.tsx +79 -0
- package/agent-pane/frontend/src/components/Toast.tsx +2 -6
- package/agent-pane/frontend/src/components/TurnContent.test.tsx +287 -0
- package/agent-pane/frontend/src/components/TurnContent.tsx +107 -24
- package/agent-pane/frontend/src/components/WorkspaceSidebar.test.tsx +297 -0
- package/agent-pane/frontend/src/components/WorkspaceSidebar.tsx +33 -32
- package/agent-pane/frontend/src/components/input/OptionSubPopup.tsx +32 -0
- package/agent-pane/frontend/src/components/input/SkillMentionPopup.test.tsx +63 -0
- package/agent-pane/frontend/src/components/input/SkillMentionPopup.tsx +78 -0
- package/agent-pane/frontend/src/components/input/SlashPopup.test.tsx +96 -0
- package/agent-pane/frontend/src/components/input/SlashPopup.tsx +63 -0
- package/agent-pane/frontend/src/components/panels/ChangesPanel.test.tsx +61 -0
- package/agent-pane/frontend/src/components/panels/ChangesPanel.tsx +12 -39
- package/agent-pane/frontend/src/components/panels/LogsPanel.test.tsx +58 -0
- package/agent-pane/frontend/src/components/panels/LogsPanel.tsx +1 -1
- package/agent-pane/frontend/src/components/panels/MetricsPanel.test.tsx +71 -0
- package/agent-pane/frontend/src/components/panels/MetricsPanel.tsx +63 -31
- package/agent-pane/frontend/src/components/panels/TurnsPanel.test.tsx +83 -0
- package/agent-pane/frontend/src/components/panels/TurnsPanel.tsx +66 -7
- package/agent-pane/frontend/src/constants/models.test.ts +97 -0
- package/agent-pane/frontend/src/constants/models.ts +129 -0
- package/agent-pane/frontend/src/hooks/useEscapeKey.test.ts +42 -0
- package/agent-pane/frontend/src/hooks/useEscapeKey.ts +13 -0
- package/agent-pane/frontend/src/hooks/useOutsideClick.test.ts +92 -0
- package/agent-pane/frontend/src/hooks/useOutsideClick.ts +19 -0
- package/agent-pane/frontend/src/hooks/useProvider.ts +44 -0
- package/agent-pane/frontend/src/hooks/useSessionStatus.test.ts +114 -0
- package/agent-pane/frontend/src/hooks/useSessionStatus.ts +34 -0
- package/agent-pane/frontend/src/main.test.tsx +37 -0
- package/agent-pane/frontend/src/main.tsx +1 -1
- package/agent-pane/frontend/src/store/sessionSlice.ts +417 -0
- package/agent-pane/frontend/src/store/store.test.ts +1284 -0
- package/agent-pane/frontend/src/store/store.ts +16 -617
- package/agent-pane/frontend/src/store/uiSlice.ts +83 -0
- package/agent-pane/frontend/src/store/workspaceSlice.ts +77 -0
- package/agent-pane/frontend/src/store/wsSlice.ts +292 -0
- package/agent-pane/frontend/src/styles/app.css +113 -7
- package/agent-pane/frontend/src/test/component-helpers.ts +36 -0
- package/agent-pane/frontend/src/test/fixtures.ts +96 -0
- package/agent-pane/frontend/src/test-setup.ts +5 -0
- package/agent-pane/frontend/src/types/index.test.ts +128 -0
- package/agent-pane/frontend/src/types/index.ts +80 -104
- package/agent-pane/frontend/src/utils/changes.test.ts +81 -0
- package/agent-pane/frontend/src/utils/changes.ts +39 -0
- package/agent-pane/frontend/src/utils/duration.test.ts +71 -0
- package/agent-pane/frontend/src/utils/duration.ts +14 -0
- package/agent-pane/frontend/tsconfig.json +4 -1
- package/agent-pane/frontend/vite.config.ts +6 -0
- package/agent-pane/frontend/vitest.config.ts +29 -0
- package/agent-pane/package-lock.json +1527 -0
- package/agent-pane/package.json +19 -0
- package/agent-pane/shared/provider-types.ts +53 -0
- package/agent-pane/shared/types.ts +107 -0
- package/agentpane-branch-session.png +0 -0
- package/agentpane-expanded-all.png +0 -0
- package/agentpane-fixed-rendering.png +0 -0
- package/agentpane-focus-check.png +0 -0
- package/agentpane-initial.png +0 -0
- package/agentpane-mcp-detail.png +0 -0
- package/agentpane-no-focus-ring.png +0 -0
- package/agentpane-soft-focus.png +0 -0
- package/backend/dist/src/llm-provider/ollama-llm.provider.js +1 -0
- package/backend/dist/src/llm-provider/ollama-llm.provider.js.map +1 -1
- package/backend/dist/tsconfig.build.tsbuildinfo +1 -1
- package/data.db +0 -0
- package/empty-state.png +0 -0
- package/frontend-home.png +0 -0
- package/model-selector-open.png +0 -0
- package/package.json +1 -1
- package/slash-popup-working.png +0 -0
- 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
|
+
});
|