@shaykec/bridge 0.4.17 → 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 +59 -19
- 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,15 +20,23 @@ 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';
|
|
26
|
+
import { dirname, join } from 'path';
|
|
27
|
+
import { fileURLToPath } from 'url';
|
|
28
|
+
|
|
29
|
+
// Resolve the package tree root (parent of node_modules/@shaykec/bridge)
|
|
30
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
31
|
+
const BRIDGE_ROOT = join(dirname(__filename), '..');
|
|
32
|
+
// Walk up: bridge/ -> @shaykec/ -> node_modules/ -> prefix/
|
|
33
|
+
const NPM_PREFIX = join(BRIDGE_ROOT, '..', '..', '..');
|
|
25
34
|
|
|
26
35
|
// Lazy-load the Agent SDK — auto-installs on first use if missing
|
|
27
36
|
let _sdk = null;
|
|
28
37
|
let _sdkLoadError = null;
|
|
29
38
|
|
|
30
39
|
function tryRequireSDK() {
|
|
31
|
-
// Use createRequire to avoid Node's ESM import cache (which caches failures)
|
|
32
40
|
const require = createRequire(import.meta.url);
|
|
33
41
|
return require('@anthropic-ai/claude-agent-sdk');
|
|
34
42
|
}
|
|
@@ -40,13 +48,13 @@ async function getSDK() {
|
|
|
40
48
|
_sdk = tryRequireSDK();
|
|
41
49
|
return _sdk;
|
|
42
50
|
} catch {
|
|
43
|
-
// SDK not installed —
|
|
51
|
+
// SDK not installed — install it into the same node_modules tree
|
|
44
52
|
console.log('[chat] Agent SDK not found, installing @anthropic-ai/claude-agent-sdk...');
|
|
45
53
|
try {
|
|
46
|
-
execSync(
|
|
47
|
-
|
|
48
|
-
timeout: 60000
|
|
49
|
-
|
|
54
|
+
execSync(
|
|
55
|
+
`npm install @anthropic-ai/claude-agent-sdk --registry https://registry.npmjs.org/ --prefix "${NPM_PREFIX}" --no-save`,
|
|
56
|
+
{ stdio: 'inherit', timeout: 60000 }
|
|
57
|
+
);
|
|
50
58
|
_sdk = tryRequireSDK();
|
|
51
59
|
console.log('[chat] Agent SDK installed successfully.');
|
|
52
60
|
return _sdk;
|
|
@@ -78,9 +86,18 @@ async function getSDK() {
|
|
|
78
86
|
* Manages multiple Claude Code SDK sessions.
|
|
79
87
|
*/
|
|
80
88
|
export class ClaudeSessionManager {
|
|
81
|
-
|
|
89
|
+
/**
|
|
90
|
+
* @param {object} [sdkOverride] - Injected SDK for testing
|
|
91
|
+
*/
|
|
92
|
+
constructor(sdkOverride) {
|
|
82
93
|
/** @type {Map<string, SessionEntry>} */
|
|
83
94
|
this._sessions = new Map();
|
|
95
|
+
this._sdkOverride = sdkOverride || null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** @private */
|
|
99
|
+
async _getSDK() {
|
|
100
|
+
return this._sdkOverride || getSDK();
|
|
84
101
|
}
|
|
85
102
|
|
|
86
103
|
/**
|
|
@@ -89,7 +106,7 @@ export class ClaudeSessionManager {
|
|
|
89
106
|
*/
|
|
90
107
|
async isAvailable() {
|
|
91
108
|
try {
|
|
92
|
-
await
|
|
109
|
+
await this._getSDK();
|
|
93
110
|
return true;
|
|
94
111
|
} catch {
|
|
95
112
|
return false;
|
|
@@ -134,21 +151,24 @@ export class ClaudeSessionManager {
|
|
|
134
151
|
|
|
135
152
|
/**
|
|
136
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
|
+
*
|
|
137
159
|
* @param {ChatSessionOptions} options
|
|
138
|
-
* @returns {Promise<string>} sessionId
|
|
160
|
+
* @returns {Promise<string>} sessionId (our UUID handle)
|
|
139
161
|
*/
|
|
140
162
|
async createSession(options = {}) {
|
|
141
|
-
const sdk = await
|
|
163
|
+
const sdk = await this._getSDK();
|
|
142
164
|
const onMessage = options.onMessage;
|
|
143
165
|
|
|
144
166
|
const sessionOpts = {
|
|
167
|
+
model: options.model || 'claude-sonnet-4-6',
|
|
145
168
|
allowedTools: [
|
|
146
169
|
'Bash(*)', 'Read', 'Write', 'Edit', 'Glob', 'Grep',
|
|
147
170
|
],
|
|
148
171
|
permissionMode: 'bypassPermissions',
|
|
149
|
-
allowDangerouslySkipPermissions: true,
|
|
150
|
-
settingSources: ['user', 'project'],
|
|
151
|
-
includePartialMessages: true,
|
|
152
172
|
};
|
|
153
173
|
|
|
154
174
|
if (options.cwd) sessionOpts.cwd = options.cwd;
|
|
@@ -159,12 +179,15 @@ export class ClaudeSessionManager {
|
|
|
159
179
|
|
|
160
180
|
try {
|
|
161
181
|
const session = sdk.unstable_v2_createSession(sessionOpts);
|
|
162
|
-
|
|
182
|
+
// session.sessionId throws until after first send()+stream()
|
|
183
|
+
// so we use our own UUID as the map key
|
|
184
|
+
const sessionId = randomUUID();
|
|
163
185
|
|
|
164
186
|
this._sessions.set(sessionId, {
|
|
165
187
|
session,
|
|
166
188
|
streaming: false,
|
|
167
189
|
onMessage,
|
|
190
|
+
sdkSessionId: null, // captured after first message
|
|
168
191
|
});
|
|
169
192
|
|
|
170
193
|
this._emit(sessionId, MSG_CHAT_STATUS, { status: 'started', sessionId });
|
|
@@ -186,7 +209,7 @@ export class ClaudeSessionManager {
|
|
|
186
209
|
* @returns {Promise<string>} sessionId
|
|
187
210
|
*/
|
|
188
211
|
async resumeSession(sessionId, options = {}) {
|
|
189
|
-
const sdk = await
|
|
212
|
+
const sdk = await this._getSDK();
|
|
190
213
|
|
|
191
214
|
// Close existing entry for this ID if present
|
|
192
215
|
await this.closeSession(sessionId);
|
|
@@ -194,13 +217,11 @@ export class ClaudeSessionManager {
|
|
|
194
217
|
const onMessage = options.onMessage;
|
|
195
218
|
|
|
196
219
|
const sessionOpts = {
|
|
220
|
+
model: options.model || 'claude-sonnet-4-6',
|
|
197
221
|
allowedTools: [
|
|
198
222
|
'Bash(*)', 'Read', 'Write', 'Edit', 'Glob', 'Grep',
|
|
199
223
|
],
|
|
200
224
|
permissionMode: 'bypassPermissions',
|
|
201
|
-
allowDangerouslySkipPermissions: true,
|
|
202
|
-
settingSources: ['user', 'project'],
|
|
203
|
-
includePartialMessages: true,
|
|
204
225
|
};
|
|
205
226
|
|
|
206
227
|
if (options.cwd) sessionOpts.cwd = options.cwd;
|
|
@@ -210,12 +231,14 @@ export class ClaudeSessionManager {
|
|
|
210
231
|
}
|
|
211
232
|
|
|
212
233
|
try {
|
|
234
|
+
// For resumed sessions, sessionId is available immediately
|
|
213
235
|
const session = sdk.unstable_v2_resumeSession(sessionId, sessionOpts);
|
|
214
236
|
|
|
215
237
|
this._sessions.set(sessionId, {
|
|
216
238
|
session,
|
|
217
239
|
streaming: false,
|
|
218
240
|
onMessage,
|
|
241
|
+
sdkSessionId: sessionId,
|
|
219
242
|
});
|
|
220
243
|
|
|
221
244
|
this._emit(sessionId, MSG_CHAT_STATUS, { status: 'resumed', sessionId });
|
|
@@ -255,6 +278,13 @@ export class ClaudeSessionManager {
|
|
|
255
278
|
let currentText = '';
|
|
256
279
|
|
|
257
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
|
+
|
|
258
288
|
if (msg.type === 'assistant') {
|
|
259
289
|
const textBlocks = (msg.message?.content || []).filter(b => b.type === 'text');
|
|
260
290
|
for (const block of textBlocks) {
|
|
@@ -298,6 +328,16 @@ export class ClaudeSessionManager {
|
|
|
298
328
|
}
|
|
299
329
|
}
|
|
300
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
|
+
|
|
301
341
|
/**
|
|
302
342
|
* Stop the current generation for a specific session.
|
|
303
343
|
* @param {string} sessionId
|
|
@@ -320,7 +360,7 @@ export class ClaudeSessionManager {
|
|
|
320
360
|
*/
|
|
321
361
|
async listSessions(dir) {
|
|
322
362
|
try {
|
|
323
|
-
const sdk = await
|
|
363
|
+
const sdk = await this._getSDK();
|
|
324
364
|
const sessions = await sdk.listSessions({ dir, limit: 20 });
|
|
325
365
|
return sessions.map(s => ({
|
|
326
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 () => {
|