@ebowwa/claude-code-mcp 1.0.0 → 1.0.2
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/dist/__tests__/advanced.test.d.ts +6 -0
- package/dist/__tests__/advanced.test.d.ts.map +1 -0
- package/dist/__tests__/advanced.test.js +354 -0
- package/dist/__tests__/advanced.test.js.map +1 -0
- package/dist/advanced.d.ts +109 -0
- package/dist/advanced.d.ts.map +1 -0
- package/dist/advanced.js +427 -0
- package/dist/advanced.js.map +1 -0
- package/dist/cli-wrapper.d.ts +202 -0
- package/dist/cli-wrapper.d.ts.map +1 -0
- package/dist/cli-wrapper.js +347 -0
- package/dist/cli-wrapper.js.map +1 -0
- package/dist/cli-wrapper.test.d.ts +12 -0
- package/dist/cli-wrapper.test.d.ts.map +1 -0
- package/dist/cli-wrapper.test.js +354 -0
- package/dist/cli-wrapper.test.js.map +1 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +354 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +561 -0
- package/dist/index.js.map +1 -0
- package/dist/integration.test.d.ts +12 -0
- package/dist/integration.test.d.ts.map +1 -0
- package/dist/integration.test.js +716 -0
- package/dist/integration.test.js.map +1 -0
- package/dist/queue.d.ts +87 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +273 -0
- package/dist/queue.js.map +1 -0
- package/dist/teammates-integration.d.ts +128 -0
- package/dist/teammates-integration.d.ts.map +1 -0
- package/dist/teammates-integration.js +353 -0
- package/dist/teammates-integration.js.map +1 -0
- package/dist/test-config.d.ts +104 -0
- package/dist/test-config.d.ts.map +1 -0
- package/dist/test-config.js +439 -0
- package/dist/test-config.js.map +1 -0
- package/dist/tools.d.ts +97 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +627 -0
- package/dist/tools.js.map +1 -0
- package/package.json +7 -1
- package/ARCHITECTURE.md +0 -1802
- package/DOCUMENTATION.md +0 -747
- package/TESTING.md +0 -318
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Integration Tests with Mock Claude Binary
|
|
4
|
+
*
|
|
5
|
+
* Tests the full MCP server integration with:
|
|
6
|
+
* - Mock claude binary for predictable testing
|
|
7
|
+
* - Full tool call lifecycle
|
|
8
|
+
* - Session state management
|
|
9
|
+
* - Doppler integration simulation
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
|
12
|
+
// Mock Claude binary script - creates a temporary executable
|
|
13
|
+
async function createMockClaude() {
|
|
14
|
+
const mockPath = `/tmp/mock-claude-${Date.now()}.sh`;
|
|
15
|
+
await Bun.write(mockPath, `#!/bin/bash
|
|
16
|
+
# Mock Claude binary for testing
|
|
17
|
+
|
|
18
|
+
echo "Mock Claude CLI v1.0.0" >&2
|
|
19
|
+
|
|
20
|
+
# Parse arguments
|
|
21
|
+
case "$1" in
|
|
22
|
+
--version)
|
|
23
|
+
echo "claude version 1.0.0"
|
|
24
|
+
exit 0
|
|
25
|
+
;;
|
|
26
|
+
--help)
|
|
27
|
+
echo "Usage: claude [options]"
|
|
28
|
+
exit 0
|
|
29
|
+
;;
|
|
30
|
+
--resume)
|
|
31
|
+
# Mock resume behavior
|
|
32
|
+
UUID="$2"
|
|
33
|
+
echo "Resuming session: $UUID" >&2
|
|
34
|
+
# Simulate session work
|
|
35
|
+
sleep 0.1
|
|
36
|
+
echo '{"status":"resumed","sessionId":"'"$UUID"'"}'
|
|
37
|
+
exit 0
|
|
38
|
+
;;
|
|
39
|
+
*)
|
|
40
|
+
# Default: simulate a new session
|
|
41
|
+
echo "Starting new session..." >&2
|
|
42
|
+
sleep 0.1
|
|
43
|
+
echo '{"status":"started","sessionId":"mock-uuid-'"$(date +%s)"'"}'
|
|
44
|
+
exit 0
|
|
45
|
+
;;
|
|
46
|
+
esac
|
|
47
|
+
`);
|
|
48
|
+
// Make executable
|
|
49
|
+
await Bun.spawn(['chmod', '+x', mockPath]).exited;
|
|
50
|
+
return mockPath;
|
|
51
|
+
}
|
|
52
|
+
describe('MCP Server Integration', () => {
|
|
53
|
+
let mockClaudePath;
|
|
54
|
+
let server;
|
|
55
|
+
beforeEach(async () => {
|
|
56
|
+
mockClaudePath = await createMockClaude();
|
|
57
|
+
// Mock MCP server
|
|
58
|
+
server = {
|
|
59
|
+
tools: new Map(),
|
|
60
|
+
sessions: new Map(),
|
|
61
|
+
registerTool(name, handler) {
|
|
62
|
+
this.tools.set(name, handler);
|
|
63
|
+
},
|
|
64
|
+
async callTool(name, args) {
|
|
65
|
+
const handler = this.tools.get(name);
|
|
66
|
+
if (!handler) {
|
|
67
|
+
throw new Error(`Tool not found: ${name}`);
|
|
68
|
+
}
|
|
69
|
+
return await handler.call(this, args);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
afterEach(async () => {
|
|
74
|
+
// Cleanup mock binary
|
|
75
|
+
try {
|
|
76
|
+
await Bun.spawn(['rm', '-f', mockClaudePath]).exited;
|
|
77
|
+
}
|
|
78
|
+
catch { }
|
|
79
|
+
});
|
|
80
|
+
describe('start_session', () => {
|
|
81
|
+
it('should start a new session', async () => {
|
|
82
|
+
server.registerTool('start_session', async function (args) {
|
|
83
|
+
const { projectPath, message } = args;
|
|
84
|
+
// Mock spawning Claude
|
|
85
|
+
const sessionId = `test-session-${Date.now()}`;
|
|
86
|
+
const process = await Bun.spawn([
|
|
87
|
+
mockClaudePath,
|
|
88
|
+
projectPath || '/tmp/test'
|
|
89
|
+
]);
|
|
90
|
+
this.sessions.set(sessionId, {
|
|
91
|
+
id: sessionId,
|
|
92
|
+
projectPath,
|
|
93
|
+
status: 'running',
|
|
94
|
+
pid: process.pid,
|
|
95
|
+
startTime: Date.now()
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
content: [{
|
|
99
|
+
type: 'text',
|
|
100
|
+
text: JSON.stringify({
|
|
101
|
+
sessionId,
|
|
102
|
+
status: 'started',
|
|
103
|
+
pid: process.pid
|
|
104
|
+
})
|
|
105
|
+
}]
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
const result = await server.callTool('start_session', {
|
|
109
|
+
projectPath: '/tmp/test-project',
|
|
110
|
+
message: 'Hello Claude'
|
|
111
|
+
});
|
|
112
|
+
const data = JSON.parse(result.content[0].text);
|
|
113
|
+
expect(data.status).toBe('started');
|
|
114
|
+
expect(data.sessionId).toBeDefined();
|
|
115
|
+
expect(server.sessions.size).toBe(1);
|
|
116
|
+
});
|
|
117
|
+
it('should handle missing projectPath', async () => {
|
|
118
|
+
server.registerTool('start_session', async function (args) {
|
|
119
|
+
if (!args.projectPath) {
|
|
120
|
+
return {
|
|
121
|
+
content: [{
|
|
122
|
+
type: 'text',
|
|
123
|
+
text: JSON.stringify({
|
|
124
|
+
error: 'projectPath is required'
|
|
125
|
+
})
|
|
126
|
+
}],
|
|
127
|
+
isError: true
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// ... rest of implementation
|
|
131
|
+
});
|
|
132
|
+
const result = await server.callTool('start_session', {
|
|
133
|
+
message: 'Hello'
|
|
134
|
+
});
|
|
135
|
+
expect(result.isError).toBe(true);
|
|
136
|
+
const data = JSON.parse(result.content[0].text);
|
|
137
|
+
expect(data.error).toContain('projectPath');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
describe('resume_session', () => {
|
|
141
|
+
it('should resume an existing session', async () => {
|
|
142
|
+
server.registerTool('resume_session', async function (args) {
|
|
143
|
+
const { sessionId, message, options } = args;
|
|
144
|
+
// Check if useDoppler is enabled
|
|
145
|
+
const useDoppler = options?.useDoppler ?? true;
|
|
146
|
+
if (useDoppler) {
|
|
147
|
+
// Mock doppler-wrapped resume
|
|
148
|
+
const process = await Bun.spawn([
|
|
149
|
+
'bash',
|
|
150
|
+
'-c',
|
|
151
|
+
`${mockClaudePath} --resume ${sessionId}`
|
|
152
|
+
]);
|
|
153
|
+
return {
|
|
154
|
+
content: [{
|
|
155
|
+
type: 'text',
|
|
156
|
+
text: JSON.stringify({
|
|
157
|
+
sessionId,
|
|
158
|
+
status: 'resumed',
|
|
159
|
+
dopplerWrapped: true,
|
|
160
|
+
pid: process.pid
|
|
161
|
+
})
|
|
162
|
+
}]
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
// Direct resume without doppler
|
|
166
|
+
return {
|
|
167
|
+
content: [{
|
|
168
|
+
type: 'text',
|
|
169
|
+
text: JSON.stringify({
|
|
170
|
+
sessionId,
|
|
171
|
+
status: 'resumed',
|
|
172
|
+
dopplerWrapped: false
|
|
173
|
+
})
|
|
174
|
+
}]
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
const result = await server.callTool('resume_session', {
|
|
178
|
+
sessionId: 'test-uuid-123',
|
|
179
|
+
message: 'Continue working',
|
|
180
|
+
options: { useDoppler: true }
|
|
181
|
+
});
|
|
182
|
+
const data = JSON.parse(result.content[0].text);
|
|
183
|
+
expect(data.status).toBe('resumed');
|
|
184
|
+
expect(data.dopplerWrapped).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
it('should handle invalid sessionId', async () => {
|
|
187
|
+
server.registerTool('resume_session', async function (args) {
|
|
188
|
+
const { sessionId } = args;
|
|
189
|
+
if (!sessionId || sessionId.length < 10) {
|
|
190
|
+
return {
|
|
191
|
+
content: [{
|
|
192
|
+
type: 'text',
|
|
193
|
+
text: JSON.stringify({
|
|
194
|
+
error: 'Invalid sessionId format'
|
|
195
|
+
})
|
|
196
|
+
}],
|
|
197
|
+
isError: true
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
// ... resume logic
|
|
201
|
+
});
|
|
202
|
+
const result = await server.callTool('resume_session', {
|
|
203
|
+
sessionId: 'short'
|
|
204
|
+
});
|
|
205
|
+
expect(result.isError).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
describe('list_sessions', () => {
|
|
209
|
+
beforeEach(() => {
|
|
210
|
+
// Add some mock sessions
|
|
211
|
+
server.sessions.set('session-1', {
|
|
212
|
+
id: 'session-1',
|
|
213
|
+
projectPath: '/path/to/project1',
|
|
214
|
+
status: 'running',
|
|
215
|
+
startTime: Date.now() - 3600000
|
|
216
|
+
});
|
|
217
|
+
server.sessions.set('session-2', {
|
|
218
|
+
id: 'session-2',
|
|
219
|
+
projectPath: '/path/to/project2',
|
|
220
|
+
status: 'completed',
|
|
221
|
+
startTime: Date.now() - 7200000
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
it('should list all sessions', async () => {
|
|
225
|
+
server.registerTool('list_sessions', async function (args) {
|
|
226
|
+
const { status, limit } = args;
|
|
227
|
+
let sessions = Array.from(this.sessions.values());
|
|
228
|
+
if (status && status !== 'all') {
|
|
229
|
+
sessions = sessions.filter((s) => s.status === status);
|
|
230
|
+
}
|
|
231
|
+
if (limit) {
|
|
232
|
+
sessions = sessions.slice(0, limit);
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
content: [{
|
|
236
|
+
type: 'text',
|
|
237
|
+
text: JSON.stringify({
|
|
238
|
+
sessions,
|
|
239
|
+
count: sessions.length
|
|
240
|
+
})
|
|
241
|
+
}]
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
const result = await server.callTool('list_sessions', {
|
|
245
|
+
status: 'all'
|
|
246
|
+
});
|
|
247
|
+
const data = JSON.parse(result.content[0].text);
|
|
248
|
+
expect(data.count).toBe(2);
|
|
249
|
+
expect(data.sessions).toHaveLength(2);
|
|
250
|
+
});
|
|
251
|
+
it('should filter by status', async () => {
|
|
252
|
+
server.registerTool('list_sessions', async function (args) {
|
|
253
|
+
const { status } = args;
|
|
254
|
+
let sessions = Array.from(this.sessions.values());
|
|
255
|
+
if (status && status !== 'all') {
|
|
256
|
+
sessions = sessions.filter((s) => s.status === status);
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
content: [{
|
|
260
|
+
type: 'text',
|
|
261
|
+
text: JSON.stringify({ sessions })
|
|
262
|
+
}]
|
|
263
|
+
};
|
|
264
|
+
});
|
|
265
|
+
const result = await server.callTool('list_sessions', {
|
|
266
|
+
status: 'running'
|
|
267
|
+
});
|
|
268
|
+
const data = JSON.parse(result.content[0].text);
|
|
269
|
+
expect(data.sessions).toHaveLength(1);
|
|
270
|
+
expect(data.sessions[0].id).toBe('session-1');
|
|
271
|
+
});
|
|
272
|
+
it('should apply limit', async () => {
|
|
273
|
+
server.registerTool('list_sessions', async function (args) {
|
|
274
|
+
const { limit } = args;
|
|
275
|
+
let sessions = Array.from(this.sessions.values());
|
|
276
|
+
if (limit) {
|
|
277
|
+
sessions = sessions.slice(0, limit);
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
content: [{
|
|
281
|
+
type: 'text',
|
|
282
|
+
text: JSON.stringify({ sessions })
|
|
283
|
+
}]
|
|
284
|
+
};
|
|
285
|
+
});
|
|
286
|
+
const result = await server.callTool('list_sessions', {
|
|
287
|
+
limit: 1
|
|
288
|
+
});
|
|
289
|
+
const data = JSON.parse(result.content[0].text);
|
|
290
|
+
expect(data.sessions).toHaveLength(1);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
describe('kill_session', () => {
|
|
294
|
+
beforeEach(() => {
|
|
295
|
+
server.sessions.set('session-to-kill', {
|
|
296
|
+
id: 'session-to-kill',
|
|
297
|
+
status: 'running',
|
|
298
|
+
pid: 12345
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
it('should kill a running session', async () => {
|
|
302
|
+
server.registerTool('kill_session', async function (args) {
|
|
303
|
+
const { sessionId, force } = args;
|
|
304
|
+
const session = this.sessions.get(sessionId);
|
|
305
|
+
if (!session) {
|
|
306
|
+
return {
|
|
307
|
+
content: [{
|
|
308
|
+
type: 'text',
|
|
309
|
+
text: JSON.stringify({ error: 'Session not found' })
|
|
310
|
+
}],
|
|
311
|
+
isError: true
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
// Mock kill
|
|
315
|
+
session.status = 'killed';
|
|
316
|
+
session.killedAt = Date.now();
|
|
317
|
+
return {
|
|
318
|
+
content: [{
|
|
319
|
+
type: 'text',
|
|
320
|
+
text: JSON.stringify({
|
|
321
|
+
sessionId,
|
|
322
|
+
status: 'killed',
|
|
323
|
+
force: force || false
|
|
324
|
+
})
|
|
325
|
+
}]
|
|
326
|
+
};
|
|
327
|
+
});
|
|
328
|
+
const result = await server.callTool('kill_session', {
|
|
329
|
+
sessionId: 'session-to-kill'
|
|
330
|
+
});
|
|
331
|
+
const data = JSON.parse(result.content[0].text);
|
|
332
|
+
expect(data.status).toBe('killed');
|
|
333
|
+
});
|
|
334
|
+
it('should return error for non-existent session', async () => {
|
|
335
|
+
server.registerTool('kill_session', async function (args) {
|
|
336
|
+
const { sessionId } = args;
|
|
337
|
+
if (!this.sessions.has(sessionId)) {
|
|
338
|
+
return {
|
|
339
|
+
content: [{
|
|
340
|
+
type: 'text',
|
|
341
|
+
text: JSON.stringify({ error: 'Session not found' })
|
|
342
|
+
}],
|
|
343
|
+
isError: true
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
const result = await server.callTool('kill_session', {
|
|
348
|
+
sessionId: 'non-existent'
|
|
349
|
+
});
|
|
350
|
+
expect(result.isError).toBe(true);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
describe('send_message', () => {
|
|
354
|
+
beforeEach(() => {
|
|
355
|
+
server.sessions.set('active-session', {
|
|
356
|
+
id: 'active-session',
|
|
357
|
+
status: 'running',
|
|
358
|
+
messages: []
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
it('should send message to active session', async () => {
|
|
362
|
+
server.registerTool('send_message', async function (args) {
|
|
363
|
+
const { sessionId, message } = args;
|
|
364
|
+
const session = this.sessions.get(sessionId);
|
|
365
|
+
if (!session) {
|
|
366
|
+
return {
|
|
367
|
+
content: [{
|
|
368
|
+
type: 'text',
|
|
369
|
+
text: JSON.stringify({ error: 'Session not found' })
|
|
370
|
+
}],
|
|
371
|
+
isError: true
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
if (session.status !== 'running') {
|
|
375
|
+
return {
|
|
376
|
+
content: [{
|
|
377
|
+
type: 'text',
|
|
378
|
+
text: JSON.stringify({ error: 'Session is not running' })
|
|
379
|
+
}],
|
|
380
|
+
isError: true
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
session.messages.push({
|
|
384
|
+
content: message,
|
|
385
|
+
timestamp: Date.now()
|
|
386
|
+
});
|
|
387
|
+
return {
|
|
388
|
+
content: [{
|
|
389
|
+
type: 'text',
|
|
390
|
+
text: JSON.stringify({
|
|
391
|
+
sessionId,
|
|
392
|
+
messageSent: true,
|
|
393
|
+
messageCount: session.messages.length
|
|
394
|
+
})
|
|
395
|
+
}]
|
|
396
|
+
};
|
|
397
|
+
});
|
|
398
|
+
const result = await server.callTool('send_message', {
|
|
399
|
+
sessionId: 'active-session',
|
|
400
|
+
message: 'What is the status?'
|
|
401
|
+
});
|
|
402
|
+
const data = JSON.parse(result.content[0].text);
|
|
403
|
+
expect(data.messageSent).toBe(true);
|
|
404
|
+
expect(data.messageCount).toBe(1);
|
|
405
|
+
});
|
|
406
|
+
it('should handle waitForResponse option', async () => {
|
|
407
|
+
server.registerTool('send_message', async function (args) {
|
|
408
|
+
const { sessionId, message, waitForResponse } = args;
|
|
409
|
+
const session = this.sessions.get(sessionId);
|
|
410
|
+
session.messages.push({ content: message, timestamp: Date.now() });
|
|
411
|
+
const response = {
|
|
412
|
+
sessionId,
|
|
413
|
+
messageSent: true
|
|
414
|
+
};
|
|
415
|
+
if (waitForResponse) {
|
|
416
|
+
// Mock waiting for response
|
|
417
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
418
|
+
response.response = {
|
|
419
|
+
content: 'Task completed successfully',
|
|
420
|
+
timestamp: Date.now()
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
content: [{
|
|
425
|
+
type: 'text',
|
|
426
|
+
text: JSON.stringify(response)
|
|
427
|
+
}]
|
|
428
|
+
};
|
|
429
|
+
});
|
|
430
|
+
const result = await server.callTool('send_message', {
|
|
431
|
+
sessionId: 'active-session',
|
|
432
|
+
message: 'Check status',
|
|
433
|
+
waitForResponse: true
|
|
434
|
+
});
|
|
435
|
+
const data = JSON.parse(result.content[0].text);
|
|
436
|
+
expect(data.response).toBeDefined();
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
describe('sync_context', () => {
|
|
440
|
+
beforeEach(() => {
|
|
441
|
+
server.sessions.set('source-session', {
|
|
442
|
+
id: 'source-session',
|
|
443
|
+
context: {
|
|
444
|
+
files: ['/path/to/file1.ts'],
|
|
445
|
+
history: ['message1', 'message2']
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
server.sessions.set('target-session', {
|
|
449
|
+
id: 'target-session',
|
|
450
|
+
context: {}
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
it('should sync context between sessions', async () => {
|
|
454
|
+
server.registerTool('sync_context', async function (args) {
|
|
455
|
+
const { sourceSessionId, targetSessionId, contextType } = args;
|
|
456
|
+
const source = this.sessions.get(sourceSessionId);
|
|
457
|
+
const target = this.sessions.get(targetSessionId);
|
|
458
|
+
if (!source || !target) {
|
|
459
|
+
return {
|
|
460
|
+
content: [{
|
|
461
|
+
type: 'text',
|
|
462
|
+
text: JSON.stringify({ error: 'Session not found' })
|
|
463
|
+
}],
|
|
464
|
+
isError: true
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
// Sync based on contextType
|
|
468
|
+
if (contextType === 'all' || contextType === 'history') {
|
|
469
|
+
target.context.history = source.context.history;
|
|
470
|
+
}
|
|
471
|
+
if (contextType === 'all' || contextType === 'files') {
|
|
472
|
+
target.context.files = source.context.files;
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
content: [{
|
|
476
|
+
type: 'text',
|
|
477
|
+
text: JSON.stringify({
|
|
478
|
+
synced: true,
|
|
479
|
+
targetSessionId,
|
|
480
|
+
contextTypes: [contextType]
|
|
481
|
+
})
|
|
482
|
+
}]
|
|
483
|
+
};
|
|
484
|
+
});
|
|
485
|
+
const result = await server.callTool('sync_context', {
|
|
486
|
+
sourceSessionId: 'source-session',
|
|
487
|
+
targetSessionId: 'target-session',
|
|
488
|
+
contextType: 'all'
|
|
489
|
+
});
|
|
490
|
+
const data = JSON.parse(result.content[0].text);
|
|
491
|
+
expect(data.synced).toBe(true);
|
|
492
|
+
const target = server.sessions.get('target-session');
|
|
493
|
+
expect(target.context).toEqual({
|
|
494
|
+
files: ['/path/to/file1.ts'],
|
|
495
|
+
history: ['message1', 'message2']
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
it('should apply direct context data', async () => {
|
|
499
|
+
server.registerTool('sync_context', async function (args) {
|
|
500
|
+
const { targetSessionId, contextData } = args;
|
|
501
|
+
const target = this.sessions.get(targetSessionId);
|
|
502
|
+
if (!target) {
|
|
503
|
+
return {
|
|
504
|
+
content: [{
|
|
505
|
+
type: 'text',
|
|
506
|
+
text: JSON.stringify({ error: 'Session not found' })
|
|
507
|
+
}],
|
|
508
|
+
isError: true
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
target.context = { ...target.context, ...contextData };
|
|
512
|
+
return {
|
|
513
|
+
content: [{
|
|
514
|
+
type: 'text',
|
|
515
|
+
text: JSON.stringify({
|
|
516
|
+
applied: true,
|
|
517
|
+
targetSessionId
|
|
518
|
+
})
|
|
519
|
+
}]
|
|
520
|
+
};
|
|
521
|
+
});
|
|
522
|
+
const result = await server.callTool('sync_context', {
|
|
523
|
+
targetSessionId: 'target-session',
|
|
524
|
+
contextData: {
|
|
525
|
+
files: ['/new/file.ts'],
|
|
526
|
+
customData: 'test'
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
const data = JSON.parse(result.content[0].text);
|
|
530
|
+
expect(data.applied).toBe(true);
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
describe('stream_output', () => {
|
|
534
|
+
beforeEach(() => {
|
|
535
|
+
server.sessions.set('streaming-session', {
|
|
536
|
+
id: 'streaming-session',
|
|
537
|
+
output: [
|
|
538
|
+
'Line 1: Starting task...',
|
|
539
|
+
'Line 2: Processing data...',
|
|
540
|
+
'Line 3: Complete!'
|
|
541
|
+
]
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
it('should stream session output', async () => {
|
|
545
|
+
server.registerTool('stream_output', async function (args) {
|
|
546
|
+
const { sessionId, lines } = args;
|
|
547
|
+
const session = this.sessions.get(sessionId);
|
|
548
|
+
if (!session) {
|
|
549
|
+
return {
|
|
550
|
+
content: [{
|
|
551
|
+
type: 'text',
|
|
552
|
+
text: JSON.stringify({ error: 'Session not found' })
|
|
553
|
+
}],
|
|
554
|
+
isError: true
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
let output = session.output || [];
|
|
558
|
+
if (lines && lines > 0) {
|
|
559
|
+
output = output.slice(-lines);
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
content: [{
|
|
563
|
+
type: 'text',
|
|
564
|
+
text: JSON.stringify({
|
|
565
|
+
sessionId,
|
|
566
|
+
output,
|
|
567
|
+
lineCount: output.length
|
|
568
|
+
})
|
|
569
|
+
}]
|
|
570
|
+
};
|
|
571
|
+
});
|
|
572
|
+
const result = await server.callTool('stream_output', {
|
|
573
|
+
sessionId: 'streaming-session',
|
|
574
|
+
lines: 2
|
|
575
|
+
});
|
|
576
|
+
const data = JSON.parse(result.content[0].text);
|
|
577
|
+
expect(data.lineCount).toBe(2);
|
|
578
|
+
expect(data.output).toEqual([
|
|
579
|
+
'Line 2: Processing data...',
|
|
580
|
+
'Line 3: Complete!'
|
|
581
|
+
]);
|
|
582
|
+
});
|
|
583
|
+
it('should return all output when lines is 0', async () => {
|
|
584
|
+
server.registerTool('stream_output', async function (args) {
|
|
585
|
+
const { sessionId, lines } = args;
|
|
586
|
+
const session = this.sessions.get(sessionId);
|
|
587
|
+
let output = session.output || [];
|
|
588
|
+
if (lines > 0) {
|
|
589
|
+
output = output.slice(-lines);
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
content: [{
|
|
593
|
+
type: 'text',
|
|
594
|
+
text: JSON.stringify({ output })
|
|
595
|
+
}]
|
|
596
|
+
};
|
|
597
|
+
});
|
|
598
|
+
const result = await server.callTool('stream_output', {
|
|
599
|
+
sessionId: 'streaming-session',
|
|
600
|
+
lines: 0
|
|
601
|
+
});
|
|
602
|
+
const data = JSON.parse(result.content[0].text);
|
|
603
|
+
expect(data.output).toHaveLength(3);
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
describe('wait_for_completion', () => {
|
|
607
|
+
it('should wait for session completion', async () => {
|
|
608
|
+
let sessionCompleted = false;
|
|
609
|
+
server.registerTool('wait_for_completion', async function (args) {
|
|
610
|
+
const { sessionId, timeout, pollInterval } = args;
|
|
611
|
+
const startTime = Date.now();
|
|
612
|
+
// Simulate polling
|
|
613
|
+
while (Date.now() - startTime < (timeout || 300000)) {
|
|
614
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval || 1000));
|
|
615
|
+
// Check if session completed (mock)
|
|
616
|
+
if (sessionCompleted) {
|
|
617
|
+
return {
|
|
618
|
+
content: [{
|
|
619
|
+
type: 'text',
|
|
620
|
+
text: JSON.stringify({
|
|
621
|
+
sessionId,
|
|
622
|
+
status: 'completed',
|
|
623
|
+
duration: Date.now() - startTime
|
|
624
|
+
})
|
|
625
|
+
}]
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
content: [{
|
|
631
|
+
type: 'text',
|
|
632
|
+
text: JSON.stringify({
|
|
633
|
+
sessionId,
|
|
634
|
+
status: 'timeout'
|
|
635
|
+
})
|
|
636
|
+
}]
|
|
637
|
+
};
|
|
638
|
+
});
|
|
639
|
+
// Complete session after 50ms
|
|
640
|
+
setTimeout(() => { sessionCompleted = true; }, 50);
|
|
641
|
+
const result = await server.callTool('wait_for_completion', {
|
|
642
|
+
sessionId: 'waiting-session',
|
|
643
|
+
timeout: 5000,
|
|
644
|
+
pollInterval: 10
|
|
645
|
+
});
|
|
646
|
+
const data = JSON.parse(result.content[0].text);
|
|
647
|
+
expect(data.status).toBe('completed');
|
|
648
|
+
expect(data.duration).toBeGreaterThan(0);
|
|
649
|
+
});
|
|
650
|
+
it('should timeout after specified duration', async () => {
|
|
651
|
+
server.registerTool('wait_for_completion', async function (args) {
|
|
652
|
+
const { timeout } = args;
|
|
653
|
+
const startTime = Date.now();
|
|
654
|
+
// Simulate timeout
|
|
655
|
+
await new Promise(resolve => setTimeout(resolve, timeout || 100));
|
|
656
|
+
return {
|
|
657
|
+
content: [{
|
|
658
|
+
type: 'text',
|
|
659
|
+
text: JSON.stringify({
|
|
660
|
+
status: 'timeout',
|
|
661
|
+
duration: Date.now() - startTime
|
|
662
|
+
})
|
|
663
|
+
}]
|
|
664
|
+
};
|
|
665
|
+
});
|
|
666
|
+
const result = await server.callTool('wait_for_completion', {
|
|
667
|
+
sessionId: 'slow-session',
|
|
668
|
+
timeout: 50
|
|
669
|
+
});
|
|
670
|
+
const data = JSON.parse(result.content[0].text);
|
|
671
|
+
expect(data.status).toBe('timeout');
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
describe('Doppler Integration Tests', () => {
|
|
676
|
+
describe('Doppler Environment Setup', () => {
|
|
677
|
+
it('should verify DOPPLER_TOKEN is available', async () => {
|
|
678
|
+
// Mock environment check
|
|
679
|
+
const hasToken = process.env.DOPPLER_TOKEN !== undefined;
|
|
680
|
+
expect(typeof hasToken).toBe('boolean');
|
|
681
|
+
});
|
|
682
|
+
it('should construct doppler run command correctly', () => {
|
|
683
|
+
const buildDopplerCommand = (project, config, command) => {
|
|
684
|
+
return ['doppler', 'run', '--project', project, '--config', config, '--command', command];
|
|
685
|
+
};
|
|
686
|
+
const cmd = buildDopplerCommand('test-project', 'dev', 'claude --resume test-uuid');
|
|
687
|
+
expect(cmd).toContain('doppler');
|
|
688
|
+
expect(cmd).toContain('--project');
|
|
689
|
+
expect(cmd).toContain('test-project');
|
|
690
|
+
expect(cmd).toContain('--config');
|
|
691
|
+
expect(cmd).toContain('dev');
|
|
692
|
+
expect(cmd).toContain('--command');
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
describe('Rolling Keys with Doppler', () => {
|
|
696
|
+
it('should handle ANTHROPIC_API_KEYS array format', () => {
|
|
697
|
+
const mockKeys = '["key1","key2","key3"]';
|
|
698
|
+
const parsed = JSON.parse(mockKeys);
|
|
699
|
+
expect(Array.isArray(parsed)).toBe(true);
|
|
700
|
+
expect(parsed).toHaveLength(3);
|
|
701
|
+
});
|
|
702
|
+
it('should select first available key', () => {
|
|
703
|
+
const keys = ['key1', 'key2', 'key3'];
|
|
704
|
+
const selectedKey = keys[0];
|
|
705
|
+
expect(selectedKey).toBe('key1');
|
|
706
|
+
});
|
|
707
|
+
it('should fallback to ANTHROPIC_AUTH_TOKEN if API_KEY missing', () => {
|
|
708
|
+
const fallback = (apiKey, authToken) => {
|
|
709
|
+
return apiKey || authToken;
|
|
710
|
+
};
|
|
711
|
+
expect(fallback('', 'auth-token-value')).toBe('auth-token-value');
|
|
712
|
+
expect(fallback('api-key-value', 'auth-token-value')).toBe('api-key-value');
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
//# sourceMappingURL=integration.test.js.map
|