@shaykec/bridge 0.4.18 → 0.4.19
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/package.json +1 -1
- package/src/claude-session.js +46 -13
- package/src/claude-session.test.js +76 -62
- package/src/server.e2e.test.js +8 -3
package/package.json
CHANGED
package/src/claude-session.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
MSG_CHAT_STATUS,
|
|
21
21
|
} from '@shaykec/shared';
|
|
22
22
|
|
|
23
|
+
import { randomUUID } from 'crypto';
|
|
23
24
|
import { execSync } from 'child_process';
|
|
24
25
|
import { createRequire } from 'module';
|
|
25
26
|
import { dirname, join } from 'path';
|
|
@@ -85,9 +86,18 @@ async function getSDK() {
|
|
|
85
86
|
* Manages multiple Claude Code SDK sessions.
|
|
86
87
|
*/
|
|
87
88
|
export class ClaudeSessionManager {
|
|
88
|
-
|
|
89
|
+
/**
|
|
90
|
+
* @param {object} [sdkOverride] - Injected SDK for testing
|
|
91
|
+
*/
|
|
92
|
+
constructor(sdkOverride) {
|
|
89
93
|
/** @type {Map<string, SessionEntry>} */
|
|
90
94
|
this._sessions = new Map();
|
|
95
|
+
this._sdkOverride = sdkOverride || null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** @private */
|
|
99
|
+
async _getSDK() {
|
|
100
|
+
return this._sdkOverride || getSDK();
|
|
91
101
|
}
|
|
92
102
|
|
|
93
103
|
/**
|
|
@@ -96,7 +106,7 @@ export class ClaudeSessionManager {
|
|
|
96
106
|
*/
|
|
97
107
|
async isAvailable() {
|
|
98
108
|
try {
|
|
99
|
-
await
|
|
109
|
+
await this._getSDK();
|
|
100
110
|
return true;
|
|
101
111
|
} catch {
|
|
102
112
|
return false;
|
|
@@ -141,21 +151,24 @@ export class ClaudeSessionManager {
|
|
|
141
151
|
|
|
142
152
|
/**
|
|
143
153
|
* Create a new chat session.
|
|
154
|
+
*
|
|
155
|
+
* The SDK's sessionId isn't available until after the first message
|
|
156
|
+
* exchange, so we generate our own UUID to return immediately and
|
|
157
|
+
* capture the real SDK sessionId lazily during sendMessage().
|
|
158
|
+
*
|
|
144
159
|
* @param {ChatSessionOptions} options
|
|
145
|
-
* @returns {Promise<string>} sessionId
|
|
160
|
+
* @returns {Promise<string>} sessionId (our UUID handle)
|
|
146
161
|
*/
|
|
147
162
|
async createSession(options = {}) {
|
|
148
|
-
const sdk = await
|
|
163
|
+
const sdk = await this._getSDK();
|
|
149
164
|
const onMessage = options.onMessage;
|
|
150
165
|
|
|
151
166
|
const sessionOpts = {
|
|
167
|
+
model: options.model || 'claude-sonnet-4-6',
|
|
152
168
|
allowedTools: [
|
|
153
169
|
'Bash(*)', 'Read', 'Write', 'Edit', 'Glob', 'Grep',
|
|
154
170
|
],
|
|
155
171
|
permissionMode: 'bypassPermissions',
|
|
156
|
-
allowDangerouslySkipPermissions: true,
|
|
157
|
-
settingSources: ['user', 'project'],
|
|
158
|
-
includePartialMessages: true,
|
|
159
172
|
};
|
|
160
173
|
|
|
161
174
|
if (options.cwd) sessionOpts.cwd = options.cwd;
|
|
@@ -166,12 +179,15 @@ export class ClaudeSessionManager {
|
|
|
166
179
|
|
|
167
180
|
try {
|
|
168
181
|
const session = sdk.unstable_v2_createSession(sessionOpts);
|
|
169
|
-
|
|
182
|
+
// session.sessionId throws until after first send()+stream()
|
|
183
|
+
// so we use our own UUID as the map key
|
|
184
|
+
const sessionId = randomUUID();
|
|
170
185
|
|
|
171
186
|
this._sessions.set(sessionId, {
|
|
172
187
|
session,
|
|
173
188
|
streaming: false,
|
|
174
189
|
onMessage,
|
|
190
|
+
sdkSessionId: null, // captured after first message
|
|
175
191
|
});
|
|
176
192
|
|
|
177
193
|
this._emit(sessionId, MSG_CHAT_STATUS, { status: 'started', sessionId });
|
|
@@ -193,7 +209,7 @@ export class ClaudeSessionManager {
|
|
|
193
209
|
* @returns {Promise<string>} sessionId
|
|
194
210
|
*/
|
|
195
211
|
async resumeSession(sessionId, options = {}) {
|
|
196
|
-
const sdk = await
|
|
212
|
+
const sdk = await this._getSDK();
|
|
197
213
|
|
|
198
214
|
// Close existing entry for this ID if present
|
|
199
215
|
await this.closeSession(sessionId);
|
|
@@ -201,13 +217,11 @@ export class ClaudeSessionManager {
|
|
|
201
217
|
const onMessage = options.onMessage;
|
|
202
218
|
|
|
203
219
|
const sessionOpts = {
|
|
220
|
+
model: options.model || 'claude-sonnet-4-6',
|
|
204
221
|
allowedTools: [
|
|
205
222
|
'Bash(*)', 'Read', 'Write', 'Edit', 'Glob', 'Grep',
|
|
206
223
|
],
|
|
207
224
|
permissionMode: 'bypassPermissions',
|
|
208
|
-
allowDangerouslySkipPermissions: true,
|
|
209
|
-
settingSources: ['user', 'project'],
|
|
210
|
-
includePartialMessages: true,
|
|
211
225
|
};
|
|
212
226
|
|
|
213
227
|
if (options.cwd) sessionOpts.cwd = options.cwd;
|
|
@@ -217,12 +231,14 @@ export class ClaudeSessionManager {
|
|
|
217
231
|
}
|
|
218
232
|
|
|
219
233
|
try {
|
|
234
|
+
// For resumed sessions, sessionId is available immediately
|
|
220
235
|
const session = sdk.unstable_v2_resumeSession(sessionId, sessionOpts);
|
|
221
236
|
|
|
222
237
|
this._sessions.set(sessionId, {
|
|
223
238
|
session,
|
|
224
239
|
streaming: false,
|
|
225
240
|
onMessage,
|
|
241
|
+
sdkSessionId: sessionId,
|
|
226
242
|
});
|
|
227
243
|
|
|
228
244
|
this._emit(sessionId, MSG_CHAT_STATUS, { status: 'resumed', sessionId });
|
|
@@ -262,6 +278,13 @@ export class ClaudeSessionManager {
|
|
|
262
278
|
let currentText = '';
|
|
263
279
|
|
|
264
280
|
for await (const msg of entry.session.stream()) {
|
|
281
|
+
// Capture the real SDK sessionId once it becomes available
|
|
282
|
+
if (!entry.sdkSessionId) {
|
|
283
|
+
try {
|
|
284
|
+
entry.sdkSessionId = entry.session.sessionId;
|
|
285
|
+
} catch { /* not ready yet */ }
|
|
286
|
+
}
|
|
287
|
+
|
|
265
288
|
if (msg.type === 'assistant') {
|
|
266
289
|
const textBlocks = (msg.message?.content || []).filter(b => b.type === 'text');
|
|
267
290
|
for (const block of textBlocks) {
|
|
@@ -305,6 +328,16 @@ export class ClaudeSessionManager {
|
|
|
305
328
|
}
|
|
306
329
|
}
|
|
307
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Get the real SDK session ID for a session (available after first message).
|
|
333
|
+
* @param {string} sessionId - Our UUID handle
|
|
334
|
+
* @returns {string|null}
|
|
335
|
+
*/
|
|
336
|
+
getSdkSessionId(sessionId) {
|
|
337
|
+
const entry = this._sessions.get(sessionId);
|
|
338
|
+
return entry?.sdkSessionId || null;
|
|
339
|
+
}
|
|
340
|
+
|
|
308
341
|
/**
|
|
309
342
|
* Stop the current generation for a specific session.
|
|
310
343
|
* @param {string} sessionId
|
|
@@ -327,7 +360,7 @@ export class ClaudeSessionManager {
|
|
|
327
360
|
*/
|
|
328
361
|
async listSessions(dir) {
|
|
329
362
|
try {
|
|
330
|
-
const sdk = await
|
|
363
|
+
const sdk = await this._getSDK();
|
|
331
364
|
const sessions = await sdk.listSessions({ dir, limit: 20 });
|
|
332
365
|
return sessions.map(s => ({
|
|
333
366
|
sessionId: s.sessionId,
|
|
@@ -1,32 +1,10 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { ClaudeSessionManager } from './claude-session.js';
|
|
3
3
|
|
|
4
|
-
// Mock the SDK
|
|
5
|
-
vi.mock('@anthropic-ai/claude-agent-sdk', () => ({
|
|
6
|
-
unstable_v2_createSession: vi.fn(),
|
|
7
|
-
unstable_v2_resumeSession: vi.fn(),
|
|
8
|
-
listSessions: vi.fn(),
|
|
9
|
-
}));
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
unstable_v2_createSession,
|
|
13
|
-
unstable_v2_resumeSession,
|
|
14
|
-
listSessions,
|
|
15
|
-
} from '@anthropic-ai/claude-agent-sdk';
|
|
16
|
-
|
|
17
4
|
describe('ClaudeSessionManager', () => {
|
|
18
5
|
let manager;
|
|
19
6
|
let emittedMessages;
|
|
20
|
-
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
vi.clearAllMocks();
|
|
23
|
-
manager = new ClaudeSessionManager();
|
|
24
|
-
emittedMessages = [];
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const mockOnMessage = (envelope) => {
|
|
28
|
-
emittedMessages.push(envelope);
|
|
29
|
-
};
|
|
7
|
+
let mockSDK;
|
|
30
8
|
|
|
31
9
|
function makeMockSession(id) {
|
|
32
10
|
return {
|
|
@@ -37,23 +15,40 @@ describe('ClaudeSessionManager', () => {
|
|
|
37
15
|
};
|
|
38
16
|
}
|
|
39
17
|
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.clearAllMocks();
|
|
20
|
+
emittedMessages = [];
|
|
21
|
+
mockSDK = {
|
|
22
|
+
unstable_v2_createSession: vi.fn(),
|
|
23
|
+
unstable_v2_resumeSession: vi.fn(),
|
|
24
|
+
listSessions: vi.fn(),
|
|
25
|
+
};
|
|
26
|
+
manager = new ClaudeSessionManager(mockSDK);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const mockOnMessage = (envelope) => {
|
|
30
|
+
emittedMessages.push(envelope);
|
|
31
|
+
};
|
|
32
|
+
|
|
40
33
|
describe('createSession', () => {
|
|
41
34
|
it('creates a session and emits started status', async () => {
|
|
42
|
-
const mockSession = makeMockSession('
|
|
43
|
-
unstable_v2_createSession.mockReturnValue(mockSession);
|
|
35
|
+
const mockSession = makeMockSession('sdk-id-1');
|
|
36
|
+
mockSDK.unstable_v2_createSession.mockReturnValue(mockSession);
|
|
44
37
|
|
|
45
38
|
const sessionId = await manager.createSession({ onMessage: mockOnMessage });
|
|
46
39
|
|
|
47
|
-
|
|
48
|
-
expect(
|
|
40
|
+
// sessionId is a UUID we generate, not the SDK's
|
|
41
|
+
expect(sessionId).toMatch(/^[0-9a-f-]{36}$/);
|
|
42
|
+
expect(manager.isActive(sessionId)).toBe(true);
|
|
49
43
|
expect(emittedMessages).toHaveLength(1);
|
|
50
44
|
expect(emittedMessages[0].type).toBe('chat:status');
|
|
51
45
|
expect(emittedMessages[0].payload.status).toBe('started');
|
|
46
|
+
expect(emittedMessages[0].payload.sessionId).toBe(sessionId);
|
|
52
47
|
});
|
|
53
48
|
|
|
54
|
-
it('passes cwd and pluginDir to the SDK', async () => {
|
|
49
|
+
it('passes model, cwd and pluginDir to the SDK', async () => {
|
|
55
50
|
const mockSession = makeMockSession('s1');
|
|
56
|
-
unstable_v2_createSession.mockReturnValue(mockSession);
|
|
51
|
+
mockSDK.unstable_v2_createSession.mockReturnValue(mockSession);
|
|
57
52
|
|
|
58
53
|
await manager.createSession({
|
|
59
54
|
cwd: '/test/dir',
|
|
@@ -61,13 +56,14 @@ describe('ClaudeSessionManager', () => {
|
|
|
61
56
|
onMessage: mockOnMessage,
|
|
62
57
|
});
|
|
63
58
|
|
|
64
|
-
const opts = unstable_v2_createSession.mock.calls[0][0];
|
|
59
|
+
const opts = mockSDK.unstable_v2_createSession.mock.calls[0][0];
|
|
60
|
+
expect(opts.model).toBe('claude-sonnet-4-6');
|
|
65
61
|
expect(opts.cwd).toBe('/test/dir');
|
|
66
62
|
expect(opts.plugins).toEqual([{ type: 'local', path: '/test/plugin' }]);
|
|
67
63
|
});
|
|
68
64
|
|
|
69
65
|
it('emits error status on SDK failure', async () => {
|
|
70
|
-
unstable_v2_createSession.mockImplementation(() => {
|
|
66
|
+
mockSDK.unstable_v2_createSession.mockImplementation(() => {
|
|
71
67
|
throw new Error('SDK init failed');
|
|
72
68
|
});
|
|
73
69
|
|
|
@@ -83,12 +79,12 @@ describe('ClaudeSessionManager', () => {
|
|
|
83
79
|
describe('resumeSession', () => {
|
|
84
80
|
it('resumes a session by ID', async () => {
|
|
85
81
|
const mockSession = makeMockSession('existing-session');
|
|
86
|
-
unstable_v2_resumeSession.mockReturnValue(mockSession);
|
|
82
|
+
mockSDK.unstable_v2_resumeSession.mockReturnValue(mockSession);
|
|
87
83
|
|
|
88
84
|
const sessionId = await manager.resumeSession('existing-session', { onMessage: mockOnMessage });
|
|
89
85
|
|
|
90
86
|
expect(sessionId).toBe('existing-session');
|
|
91
|
-
expect(unstable_v2_resumeSession).toHaveBeenCalledWith('existing-session', expect.any(Object));
|
|
87
|
+
expect(mockSDK.unstable_v2_resumeSession).toHaveBeenCalledWith('existing-session', expect.any(Object));
|
|
92
88
|
expect(emittedMessages[0].payload.status).toBe('resumed');
|
|
93
89
|
});
|
|
94
90
|
});
|
|
@@ -106,7 +102,7 @@ describe('ClaudeSessionManager', () => {
|
|
|
106
102
|
|
|
107
103
|
const mockSession = makeMockSession('stream-test');
|
|
108
104
|
mockSession.stream.mockReturnValue(fakeStream());
|
|
109
|
-
unstable_v2_createSession.mockReturnValue(mockSession);
|
|
105
|
+
mockSDK.unstable_v2_createSession.mockReturnValue(mockSession);
|
|
110
106
|
const sessionId = await manager.createSession({ onMessage: mockOnMessage });
|
|
111
107
|
|
|
112
108
|
emittedMessages = [];
|
|
@@ -121,6 +117,25 @@ describe('ClaudeSessionManager', () => {
|
|
|
121
117
|
expect(types).toContain('chat:assistant');
|
|
122
118
|
});
|
|
123
119
|
|
|
120
|
+
it('captures SDK sessionId during streaming', async () => {
|
|
121
|
+
async function* fakeStream() {
|
|
122
|
+
yield { type: 'system' };
|
|
123
|
+
yield { type: 'assistant', message: { content: [{ type: 'text', text: 'Hi' }] } };
|
|
124
|
+
yield { type: 'result' };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const mockSession = makeMockSession('real-sdk-id');
|
|
128
|
+
mockSession.stream.mockReturnValue(fakeStream());
|
|
129
|
+
mockSDK.unstable_v2_createSession.mockReturnValue(mockSession);
|
|
130
|
+
const sessionId = await manager.createSession({ onMessage: mockOnMessage });
|
|
131
|
+
|
|
132
|
+
expect(manager.getSdkSessionId(sessionId)).toBeNull();
|
|
133
|
+
|
|
134
|
+
await manager.sendMessage(sessionId, 'test');
|
|
135
|
+
|
|
136
|
+
expect(manager.getSdkSessionId(sessionId)).toBe('real-sdk-id');
|
|
137
|
+
});
|
|
138
|
+
|
|
124
139
|
it('emits tool-use and tool-result events', async () => {
|
|
125
140
|
async function* fakeStream() {
|
|
126
141
|
yield { type: 'tool_use', name: 'Bash', id: 'tool-1', input: { command: 'ls' } };
|
|
@@ -130,7 +145,7 @@ describe('ClaudeSessionManager', () => {
|
|
|
130
145
|
|
|
131
146
|
const mockSession = makeMockSession('tool-test');
|
|
132
147
|
mockSession.stream.mockReturnValue(fakeStream());
|
|
133
|
-
unstable_v2_createSession.mockReturnValue(mockSession);
|
|
148
|
+
mockSDK.unstable_v2_createSession.mockReturnValue(mockSession);
|
|
134
149
|
const sessionId = await manager.createSession({ onMessage: mockOnMessage });
|
|
135
150
|
|
|
136
151
|
emittedMessages = [];
|
|
@@ -148,9 +163,9 @@ describe('ClaudeSessionManager', () => {
|
|
|
148
163
|
|
|
149
164
|
describe('multiple sessions', () => {
|
|
150
165
|
it('supports multiple concurrent sessions', async () => {
|
|
151
|
-
const session1 = makeMockSession('
|
|
152
|
-
const session2 = makeMockSession('
|
|
153
|
-
unstable_v2_createSession
|
|
166
|
+
const session1 = makeMockSession('sdk-1');
|
|
167
|
+
const session2 = makeMockSession('sdk-2');
|
|
168
|
+
mockSDK.unstable_v2_createSession
|
|
154
169
|
.mockReturnValueOnce(session1)
|
|
155
170
|
.mockReturnValueOnce(session2);
|
|
156
171
|
|
|
@@ -160,37 +175,36 @@ describe('ClaudeSessionManager', () => {
|
|
|
160
175
|
const id1 = await manager.createSession({ onMessage: (e) => messages1.push(e) });
|
|
161
176
|
const id2 = await manager.createSession({ onMessage: (e) => messages2.push(e) });
|
|
162
177
|
|
|
163
|
-
expect(id1).toBe(
|
|
164
|
-
expect(
|
|
165
|
-
expect(manager.isActive(
|
|
166
|
-
expect(manager.isActive('session-2')).toBe(true);
|
|
178
|
+
expect(id1).not.toBe(id2);
|
|
179
|
+
expect(manager.isActive(id1)).toBe(true);
|
|
180
|
+
expect(manager.isActive(id2)).toBe(true);
|
|
167
181
|
expect(manager.getActiveSessionIds()).toHaveLength(2);
|
|
168
182
|
|
|
169
183
|
// Status messages go to the right callback
|
|
170
|
-
expect(messages1[0].payload.sessionId).toBe(
|
|
171
|
-
expect(messages2[0].payload.sessionId).toBe(
|
|
184
|
+
expect(messages1[0].payload.sessionId).toBe(id1);
|
|
185
|
+
expect(messages2[0].payload.sessionId).toBe(id2);
|
|
172
186
|
});
|
|
173
187
|
|
|
174
188
|
it('closing one session does not affect others', async () => {
|
|
175
|
-
const session1 = makeMockSession('
|
|
176
|
-
const session2 = makeMockSession('
|
|
177
|
-
unstable_v2_createSession
|
|
189
|
+
const session1 = makeMockSession('sdk-1');
|
|
190
|
+
const session2 = makeMockSession('sdk-2');
|
|
191
|
+
mockSDK.unstable_v2_createSession
|
|
178
192
|
.mockReturnValueOnce(session1)
|
|
179
193
|
.mockReturnValueOnce(session2);
|
|
180
194
|
|
|
181
|
-
await manager.createSession({ onMessage: mockOnMessage });
|
|
182
|
-
await manager.createSession({ onMessage: mockOnMessage });
|
|
195
|
+
const id1 = await manager.createSession({ onMessage: mockOnMessage });
|
|
196
|
+
const id2 = await manager.createSession({ onMessage: mockOnMessage });
|
|
183
197
|
|
|
184
|
-
await manager.closeSession(
|
|
198
|
+
await manager.closeSession(id1);
|
|
185
199
|
|
|
186
|
-
expect(manager.isActive(
|
|
187
|
-
expect(manager.isActive(
|
|
200
|
+
expect(manager.isActive(id1)).toBe(false);
|
|
201
|
+
expect(manager.isActive(id2)).toBe(true);
|
|
188
202
|
});
|
|
189
203
|
|
|
190
204
|
it('closeAll closes all sessions', async () => {
|
|
191
|
-
const session1 = makeMockSession('
|
|
192
|
-
const session2 = makeMockSession('
|
|
193
|
-
unstable_v2_createSession
|
|
205
|
+
const session1 = makeMockSession('sdk-1');
|
|
206
|
+
const session2 = makeMockSession('sdk-2');
|
|
207
|
+
mockSDK.unstable_v2_createSession
|
|
194
208
|
.mockReturnValueOnce(session1)
|
|
195
209
|
.mockReturnValueOnce(session2);
|
|
196
210
|
|
|
@@ -208,7 +222,7 @@ describe('ClaudeSessionManager', () => {
|
|
|
208
222
|
describe('stop', () => {
|
|
209
223
|
it('is a no-op when not streaming', async () => {
|
|
210
224
|
const mockSession = makeMockSession('s1');
|
|
211
|
-
unstable_v2_createSession.mockReturnValue(mockSession);
|
|
225
|
+
mockSDK.unstable_v2_createSession.mockReturnValue(mockSession);
|
|
212
226
|
const sessionId = await manager.createSession({ onMessage: mockOnMessage });
|
|
213
227
|
|
|
214
228
|
emittedMessages = [];
|
|
@@ -222,7 +236,7 @@ describe('ClaudeSessionManager', () => {
|
|
|
222
236
|
|
|
223
237
|
describe('listSessions', () => {
|
|
224
238
|
it('returns mapped session list', async () => {
|
|
225
|
-
listSessions.mockResolvedValue([
|
|
239
|
+
mockSDK.listSessions.mockResolvedValue([
|
|
226
240
|
{ sessionId: 's1', summary: 'Test', lastModified: 12345 },
|
|
227
241
|
{ sessionId: 's2', summary: 'Other', lastModified: 67890 },
|
|
228
242
|
]);
|
|
@@ -233,7 +247,7 @@ describe('ClaudeSessionManager', () => {
|
|
|
233
247
|
});
|
|
234
248
|
|
|
235
249
|
it('returns empty array on error', async () => {
|
|
236
|
-
listSessions.mockRejectedValue(new Error('fail'));
|
|
250
|
+
mockSDK.listSessions.mockRejectedValue(new Error('fail'));
|
|
237
251
|
const sessions = await manager.listSessions('/test');
|
|
238
252
|
expect(sessions).toEqual([]);
|
|
239
253
|
});
|
|
@@ -242,7 +256,7 @@ describe('ClaudeSessionManager', () => {
|
|
|
242
256
|
describe('closeSession', () => {
|
|
243
257
|
it('closes active session and removes from map', async () => {
|
|
244
258
|
const mockSession = makeMockSession('s1');
|
|
245
|
-
unstable_v2_createSession.mockReturnValue(mockSession);
|
|
259
|
+
mockSDK.unstable_v2_createSession.mockReturnValue(mockSession);
|
|
246
260
|
const sessionId = await manager.createSession({ onMessage: mockOnMessage });
|
|
247
261
|
|
|
248
262
|
expect(manager.isActive(sessionId)).toBe(true);
|
|
@@ -252,7 +266,7 @@ describe('ClaudeSessionManager', () => {
|
|
|
252
266
|
});
|
|
253
267
|
|
|
254
268
|
describe('isAvailable', () => {
|
|
255
|
-
it('returns true when SDK is
|
|
269
|
+
it('returns true when SDK is available', async () => {
|
|
256
270
|
const available = await manager.isAvailable();
|
|
257
271
|
expect(available).toBe(true);
|
|
258
272
|
});
|
|
@@ -268,7 +282,7 @@ describe('ClaudeSessionManager', () => {
|
|
|
268
282
|
|
|
269
283
|
const mockSession = makeMockSession('busy');
|
|
270
284
|
mockSession.stream.mockReturnValue(slowStream());
|
|
271
|
-
unstable_v2_createSession.mockReturnValue(mockSession);
|
|
285
|
+
mockSDK.unstable_v2_createSession.mockReturnValue(mockSession);
|
|
272
286
|
const sessionId = await manager.createSession({ onMessage: mockOnMessage });
|
|
273
287
|
|
|
274
288
|
// Start streaming (don't await — it won't finish)
|
|
@@ -290,7 +304,7 @@ describe('ClaudeSessionManager', () => {
|
|
|
290
304
|
|
|
291
305
|
const mockSession = makeMockSession('fail-stream');
|
|
292
306
|
mockSession.stream.mockReturnValue(failStream());
|
|
293
|
-
unstable_v2_createSession.mockReturnValue(mockSession);
|
|
307
|
+
mockSDK.unstable_v2_createSession.mockReturnValue(mockSession);
|
|
294
308
|
const sessionId = await manager.createSession({ onMessage: mockOnMessage });
|
|
295
309
|
|
|
296
310
|
emittedMessages = [];
|
package/src/server.e2e.test.js
CHANGED
|
@@ -787,10 +787,15 @@ describe('Bridge E2E: Chat message routing', () => {
|
|
|
787
787
|
expect(data.ok).toBe(true);
|
|
788
788
|
});
|
|
789
789
|
|
|
790
|
-
it('POST /api/chat/start
|
|
790
|
+
it('POST /api/chat/start returns 200 with sessionId (SDK available) or 500 with error', async () => {
|
|
791
791
|
const { status, data } = await postJson('/api/chat/start', { clientId: 'test-client-123' });
|
|
792
|
-
expect(
|
|
793
|
-
|
|
792
|
+
expect([200, 500]).toContain(status);
|
|
793
|
+
if (status === 200) {
|
|
794
|
+
expect(data.sessionId).toBeDefined();
|
|
795
|
+
expect(data.ok).toBe(true);
|
|
796
|
+
} else {
|
|
797
|
+
expect(data.error).toBeDefined();
|
|
798
|
+
}
|
|
794
799
|
});
|
|
795
800
|
|
|
796
801
|
it('POST /api/chat/message requires sessionId', async () => {
|