claude-session-skill 1.1.7 → 1.1.8
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/README.md +10 -1
- package/SKILL.md +12 -0
- package/dist/mcp-server.js +526 -1260
- package/dist/session.js +48 -0
- package/lib/__tests__/format.test.ts +33 -0
- package/lib/__tests__/mcp-server.test.ts +63 -0
- package/lib/__tests__/mcp-tools.test.ts +989 -0
- package/lib/create-server.ts +311 -0
- package/lib/format.ts +31 -1
- package/mcp-server.ts +2 -250
- package/package.json +1 -1
- package/session.ts +27 -0
|
@@ -0,0 +1,989 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
7
|
+
import {
|
|
8
|
+
CallToolRequestSchema,
|
|
9
|
+
ListToolsRequestSchema,
|
|
10
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
11
|
+
import type { SessionEntry } from "../indexer";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Comprehensive MCP tool handler tests using InMemoryTransport.
|
|
15
|
+
*
|
|
16
|
+
* Strategy: stand up a Server with the SAME handler logic as create-server.ts,
|
|
17
|
+
* but inject mock data directly instead of calling real buildIndex/nameSession.
|
|
18
|
+
* This avoids mock.module pollution that bleeds into other test files.
|
|
19
|
+
*
|
|
20
|
+
* The handlers replicate the exact switch/case logic from create-server.ts
|
|
21
|
+
* so we're testing the protocol layer + handler behavior, while the
|
|
22
|
+
* underlying business logic (indexer, search, format) is tested by their
|
|
23
|
+
* own dedicated test suites.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// ─── Fixtures ──────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
function makeSession(overrides: Partial<SessionEntry> = {}): SessionEntry {
|
|
29
|
+
return {
|
|
30
|
+
id: "aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb",
|
|
31
|
+
name: "",
|
|
32
|
+
project: "YNG/02_projects/test-project",
|
|
33
|
+
projectDir: "-Users-tim-YNG-02_projects-test-project",
|
|
34
|
+
topic: "- Set up test infrastructure\n- Fixed flaky CI pipeline\n- Added retry logic to API calls",
|
|
35
|
+
firstMessage: "Let's set up the test infrastructure",
|
|
36
|
+
lastMessage: "All tests passing now",
|
|
37
|
+
allMessages: "Let's set up the test infrastructure ... All tests passing now",
|
|
38
|
+
messageCount: 42,
|
|
39
|
+
firstTimestamp: 1709500000000,
|
|
40
|
+
lastTimestamp: 1709586400000,
|
|
41
|
+
cwd: "/Users/tim/YNG/02_projects/test-project",
|
|
42
|
+
gitBranch: "feat/testing",
|
|
43
|
+
...overrides,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function makeSession2(): SessionEntry {
|
|
48
|
+
return makeSession({
|
|
49
|
+
id: "cccccccc-4444-5555-6666-dddddddddddd",
|
|
50
|
+
name: "auth-refactor",
|
|
51
|
+
project: "YNG/02_projects/auth-service",
|
|
52
|
+
topic: "- Refactored auth middleware\n- Added JWT refresh tokens",
|
|
53
|
+
firstMessage: "We need to refactor the auth layer",
|
|
54
|
+
lastMessage: "JWT refresh is working",
|
|
55
|
+
messageCount: 18,
|
|
56
|
+
firstTimestamp: 1709400000000,
|
|
57
|
+
lastTimestamp: 1709486400000,
|
|
58
|
+
cwd: "/Users/tim/YNG/02_projects/auth-service",
|
|
59
|
+
gitBranch: "refactor/auth",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── Inline mock implementations (no mock.module) ──────────────────────
|
|
64
|
+
|
|
65
|
+
function mockResolveSession(
|
|
66
|
+
sessions: SessionEntry[],
|
|
67
|
+
id: string
|
|
68
|
+
): { ok: true; match: SessionEntry } | { ok: false; error: string } {
|
|
69
|
+
const matches = sessions.filter(s => s.id === id || s.id.startsWith(id));
|
|
70
|
+
if (matches.length === 0) return { ok: false, error: `No session found matching "${id}"` };
|
|
71
|
+
const exact = matches.find(s => s.id === id);
|
|
72
|
+
if (exact) return { ok: true, match: exact };
|
|
73
|
+
if (matches.length > 1) return { ok: false, error: `Ambiguous prefix "${id}" matches ${matches.length} sessions. Provide more characters.` };
|
|
74
|
+
return { ok: true, match: matches[0] };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function mockSearchSessions(sessions: SessionEntry[], query: string): SessionEntry[] {
|
|
78
|
+
const q = query.toLowerCase();
|
|
79
|
+
return sessions.filter(s =>
|
|
80
|
+
s.allMessages.toLowerCase().includes(q) ||
|
|
81
|
+
s.topic.toLowerCase().includes(q) ||
|
|
82
|
+
s.project.toLowerCase().includes(q) ||
|
|
83
|
+
(s.name || "").toLowerCase().includes(q)
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function mockFormatSessionList(sessions: SessionEntry[], _showAll: boolean): string {
|
|
88
|
+
return `LIST:${sessions.length} sessions`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function mockFormatSearchResults(sessions: SessionEntry[], query: string): string {
|
|
92
|
+
if (sessions.length === 0) return `No sessions found matching "${query}"`;
|
|
93
|
+
return `SEARCH:${sessions.length} results for "${query}"`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function mockFormatSessionDetail(session: SessionEntry): string {
|
|
97
|
+
return `DETAIL:${session.id}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function mockFormatStats(sessions: SessionEntry[]): string {
|
|
101
|
+
return `STATS:${sessions.length} sessions`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function mockMakeAutoSessionName(session: SessionEntry): string {
|
|
105
|
+
return `31/03/26 04:14 ${session.firstMessage.slice(0, 18)}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── Server factory with injected mocks ────────────────────────────────
|
|
109
|
+
|
|
110
|
+
interface MockDeps {
|
|
111
|
+
sessions: SessionEntry[];
|
|
112
|
+
nameSessionFn: (id: string, name: string) => Promise<{ ok: boolean; fullId?: string; error?: string }>;
|
|
113
|
+
clearSessionNameFn: (id: string) => Promise<{ ok: boolean; fullId?: string; error?: string }>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function createTestServer(deps: MockDeps): Server {
|
|
117
|
+
const pkg = JSON.parse(readFileSync(join(import.meta.dir, "../../package.json"), "utf-8"));
|
|
118
|
+
|
|
119
|
+
const server = new Server(
|
|
120
|
+
{ name: "claude-session", version: pkg.version },
|
|
121
|
+
{ capabilities: { tools: {} } }
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
125
|
+
tools: [
|
|
126
|
+
{
|
|
127
|
+
name: "list_sessions",
|
|
128
|
+
description: "List recent Claude Code sessions with AI-generated summaries, sorted by last activity.",
|
|
129
|
+
inputSchema: {
|
|
130
|
+
type: "object" as const,
|
|
131
|
+
properties: {
|
|
132
|
+
limit: { type: "number" as const, description: "Max sessions to return." },
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "search_sessions",
|
|
138
|
+
description: "Search Claude Code sessions by keyword.",
|
|
139
|
+
inputSchema: {
|
|
140
|
+
type: "object" as const,
|
|
141
|
+
properties: {
|
|
142
|
+
query: { type: "string" as const, description: "Search query." },
|
|
143
|
+
},
|
|
144
|
+
required: ["query"],
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: "show_session",
|
|
149
|
+
description: "Show detailed session information.",
|
|
150
|
+
inputSchema: {
|
|
151
|
+
type: "object" as const,
|
|
152
|
+
properties: {
|
|
153
|
+
id: { type: "string" as const, description: "Session ID." },
|
|
154
|
+
},
|
|
155
|
+
required: ["id"],
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "name_session",
|
|
160
|
+
description: "Give a session a memorable name.",
|
|
161
|
+
inputSchema: {
|
|
162
|
+
type: "object" as const,
|
|
163
|
+
properties: {
|
|
164
|
+
id: { type: "string" as const, description: "Session ID." },
|
|
165
|
+
name: { type: "string" as const, description: "Name to assign." },
|
|
166
|
+
},
|
|
167
|
+
required: ["name"],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: "autoname_session",
|
|
172
|
+
description: "Generate a timestamped name from the session summary.",
|
|
173
|
+
inputSchema: {
|
|
174
|
+
type: "object" as const,
|
|
175
|
+
properties: {
|
|
176
|
+
id: { type: "string" as const, description: "Session ID." },
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: "unname_session",
|
|
182
|
+
description: "Remove the name from a session.",
|
|
183
|
+
inputSchema: {
|
|
184
|
+
type: "object" as const,
|
|
185
|
+
properties: {
|
|
186
|
+
id: { type: "string" as const, description: "Session ID." },
|
|
187
|
+
},
|
|
188
|
+
required: ["id"],
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: "session_stats",
|
|
193
|
+
description: "Show session statistics by project.",
|
|
194
|
+
inputSchema: {
|
|
195
|
+
type: "object" as const,
|
|
196
|
+
properties: {},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
// Replicate the EXACT handler logic from create-server.ts but with injected deps
|
|
203
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
204
|
+
const { name, arguments: args } = request.params;
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
switch (name) {
|
|
208
|
+
case "list_sessions": {
|
|
209
|
+
const sessions = deps.sessions;
|
|
210
|
+
const limit = typeof args?.limit === "number" ? args.limit : undefined;
|
|
211
|
+
const list = limit ? sessions.slice(0, limit) : sessions;
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: "text", text: mockFormatSessionList(list, true) }],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
case "search_sessions": {
|
|
218
|
+
const query = String(args?.query ?? "").trim();
|
|
219
|
+
if (!query) {
|
|
220
|
+
return {
|
|
221
|
+
content: [{ type: "text", text: "Error: query is required." }],
|
|
222
|
+
isError: true,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
const sessions = deps.sessions;
|
|
226
|
+
const results = mockSearchSessions(sessions, query);
|
|
227
|
+
return {
|
|
228
|
+
content: [{ type: "text", text: mockFormatSearchResults(results, query) }],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
case "show_session": {
|
|
233
|
+
const id = String(args?.id ?? "").trim();
|
|
234
|
+
if (!id) {
|
|
235
|
+
return {
|
|
236
|
+
content: [{ type: "text", text: "Error: id is required." }],
|
|
237
|
+
isError: true,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const sessions = deps.sessions;
|
|
241
|
+
const resolved = mockResolveSession(sessions, id);
|
|
242
|
+
if (!resolved.ok) {
|
|
243
|
+
return {
|
|
244
|
+
content: [{ type: "text", text: `Error: ${resolved.error}` }],
|
|
245
|
+
isError: true,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
content: [{ type: "text", text: mockFormatSessionDetail(resolved.match) }],
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
case "name_session": {
|
|
254
|
+
const sessionName = String(args?.name ?? "").trim();
|
|
255
|
+
if (!sessionName) {
|
|
256
|
+
return {
|
|
257
|
+
content: [{ type: "text", text: "Error: name is required." }],
|
|
258
|
+
isError: true,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let sessionId: string;
|
|
263
|
+
if (args?.id) {
|
|
264
|
+
sessionId = String(args.id).trim();
|
|
265
|
+
} else {
|
|
266
|
+
const sessions = deps.sessions;
|
|
267
|
+
if (sessions.length === 0) {
|
|
268
|
+
return {
|
|
269
|
+
content: [{ type: "text", text: "Error: No sessions found." }],
|
|
270
|
+
isError: true,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
sessionId = sessions[0].id;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const result = await deps.nameSessionFn(sessionId, sessionName);
|
|
277
|
+
if (!result.ok) {
|
|
278
|
+
return {
|
|
279
|
+
content: [{ type: "text", text: `Error: ${result.error}` }],
|
|
280
|
+
isError: true,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
content: [
|
|
285
|
+
{
|
|
286
|
+
type: "text",
|
|
287
|
+
text: `Named session ${(result.fullId ?? "").slice(0, 8)}... → "${sessionName}"`,
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
case "autoname_session": {
|
|
294
|
+
const sessions = deps.sessions;
|
|
295
|
+
if (sessions.length === 0) {
|
|
296
|
+
return {
|
|
297
|
+
content: [{ type: "text", text: "Error: No sessions found." }],
|
|
298
|
+
isError: true,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const sessionId = args?.id ? String(args.id).trim() : sessions[0].id;
|
|
303
|
+
const resolved = mockResolveSession(sessions, sessionId);
|
|
304
|
+
if (!resolved.ok) {
|
|
305
|
+
return {
|
|
306
|
+
content: [{ type: "text", text: `Error: ${resolved.error}` }],
|
|
307
|
+
isError: true,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const generatedName = mockMakeAutoSessionName(resolved.match);
|
|
312
|
+
const result = await deps.nameSessionFn(resolved.match.id, generatedName);
|
|
313
|
+
if (!result.ok) {
|
|
314
|
+
return {
|
|
315
|
+
content: [{ type: "text", text: `Error: ${result.error}` }],
|
|
316
|
+
isError: true,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
content: [
|
|
321
|
+
{
|
|
322
|
+
type: "text",
|
|
323
|
+
text: `Named session ${(result.fullId ?? "").slice(0, 8)}... → "${generatedName}"`,
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
case "unname_session": {
|
|
330
|
+
let sessionId: string;
|
|
331
|
+
if (args?.id) {
|
|
332
|
+
sessionId = String(args.id).trim();
|
|
333
|
+
} else {
|
|
334
|
+
const sessions = deps.sessions;
|
|
335
|
+
if (sessions.length === 0) {
|
|
336
|
+
return {
|
|
337
|
+
content: [{ type: "text", text: "Error: No sessions found." }],
|
|
338
|
+
isError: true,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
sessionId = sessions[0].id;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const result = await deps.clearSessionNameFn(sessionId);
|
|
345
|
+
if (!result.ok) {
|
|
346
|
+
return {
|
|
347
|
+
content: [{ type: "text", text: `Error: ${result.error}` }],
|
|
348
|
+
isError: true,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
content: [
|
|
353
|
+
{
|
|
354
|
+
type: "text",
|
|
355
|
+
text: `Cleared name from session ${(result.fullId ?? "").slice(0, 8)}...`,
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
case "session_stats": {
|
|
362
|
+
const sessions = deps.sessions;
|
|
363
|
+
return {
|
|
364
|
+
content: [{ type: "text", text: mockFormatStats(sessions) }],
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
default:
|
|
369
|
+
return {
|
|
370
|
+
content: [{ type: "text", text: `Error: Unknown tool "${name}"` }],
|
|
371
|
+
isError: true,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
} catch (err) {
|
|
375
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
376
|
+
return {
|
|
377
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
378
|
+
isError: true,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
return server;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ─── Helpers ──────────────────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
type TextContent = { type: "text"; text: string };
|
|
389
|
+
|
|
390
|
+
function getText(result: Awaited<ReturnType<Client["callTool"]>>): string {
|
|
391
|
+
const content = result.content as TextContent[];
|
|
392
|
+
return content[0].text;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function setupClientServer(deps: MockDeps) {
|
|
396
|
+
const server = createTestServer(deps);
|
|
397
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
398
|
+
|
|
399
|
+
await server.connect(serverTransport);
|
|
400
|
+
|
|
401
|
+
const client = new Client(
|
|
402
|
+
{ name: "test-client", version: "1.0.0" },
|
|
403
|
+
{ capabilities: {} }
|
|
404
|
+
);
|
|
405
|
+
await client.connect(clientTransport);
|
|
406
|
+
|
|
407
|
+
return { client, server };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ─── Standard mock deps ────────────────────────────────────────────────
|
|
411
|
+
|
|
412
|
+
const MOCK_SESSIONS = [makeSession(), makeSession2()];
|
|
413
|
+
|
|
414
|
+
function standardDeps(): MockDeps {
|
|
415
|
+
return {
|
|
416
|
+
sessions: [...MOCK_SESSIONS],
|
|
417
|
+
nameSessionFn: async (id: string, name: string) => {
|
|
418
|
+
const match = MOCK_SESSIONS.find(s => s.id === id || s.id.startsWith(id));
|
|
419
|
+
if (!match) return { ok: false, error: `No session found matching "${id}"` };
|
|
420
|
+
if (name.length > 50) return { ok: false, error: `Name too long (${name.length} chars, max 50).` };
|
|
421
|
+
return { ok: true, fullId: match.id };
|
|
422
|
+
},
|
|
423
|
+
clearSessionNameFn: async (id: string) => {
|
|
424
|
+
const match = MOCK_SESSIONS.find(s => s.id === id || s.id.startsWith(id));
|
|
425
|
+
if (!match) return { ok: false, error: `No session found matching "${id}"` };
|
|
426
|
+
if (!match.name) return { ok: false, error: `Session ${match.id.slice(0, 8)}... has no name to clear.` };
|
|
427
|
+
return { ok: true, fullId: match.id };
|
|
428
|
+
},
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function emptyDeps(): MockDeps {
|
|
433
|
+
return {
|
|
434
|
+
sessions: [],
|
|
435
|
+
nameSessionFn: async () => ({ ok: false, error: "No sessions found." }),
|
|
436
|
+
clearSessionNameFn: async () => ({ ok: false, error: "No sessions found." }),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ─── Tests ────────────────────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
describe("MCP tool handlers (InMemoryTransport)", () => {
|
|
443
|
+
let client: Client;
|
|
444
|
+
|
|
445
|
+
beforeEach(async () => {
|
|
446
|
+
const setup = await setupClientServer(standardDeps());
|
|
447
|
+
client = setup.client;
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
afterEach(async () => {
|
|
451
|
+
await client.close();
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// ── list_tools ────────────────────────────────────────────────────
|
|
455
|
+
|
|
456
|
+
describe("list_tools", () => {
|
|
457
|
+
test("returns all 7 tools", async () => {
|
|
458
|
+
const result = await client.listTools();
|
|
459
|
+
expect(result.tools).toHaveLength(7);
|
|
460
|
+
const names = result.tools.map(t => t.name).sort();
|
|
461
|
+
expect(names).toEqual([
|
|
462
|
+
"autoname_session",
|
|
463
|
+
"list_sessions",
|
|
464
|
+
"name_session",
|
|
465
|
+
"search_sessions",
|
|
466
|
+
"session_stats",
|
|
467
|
+
"show_session",
|
|
468
|
+
"unname_session",
|
|
469
|
+
]);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("each tool has name, description, and inputSchema", async () => {
|
|
473
|
+
const result = await client.listTools();
|
|
474
|
+
for (const tool of result.tools) {
|
|
475
|
+
expect(tool.name).toBeTruthy();
|
|
476
|
+
expect(tool.description).toBeTruthy();
|
|
477
|
+
expect(tool.inputSchema).toBeTruthy();
|
|
478
|
+
expect(tool.inputSchema.type).toBe("object");
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test("search_sessions requires query parameter", async () => {
|
|
483
|
+
const result = await client.listTools();
|
|
484
|
+
const search = result.tools.find(t => t.name === "search_sessions");
|
|
485
|
+
expect(search?.inputSchema.required).toEqual(["query"]);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test("show_session requires id parameter", async () => {
|
|
489
|
+
const result = await client.listTools();
|
|
490
|
+
const show = result.tools.find(t => t.name === "show_session");
|
|
491
|
+
expect(show?.inputSchema.required).toEqual(["id"]);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("name_session requires name parameter", async () => {
|
|
495
|
+
const result = await client.listTools();
|
|
496
|
+
const nameTool = result.tools.find(t => t.name === "name_session");
|
|
497
|
+
expect(nameTool?.inputSchema.required).toEqual(["name"]);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test("autoname_session has no required params", async () => {
|
|
501
|
+
const result = await client.listTools();
|
|
502
|
+
const tool = result.tools.find(t => t.name === "autoname_session");
|
|
503
|
+
expect(tool?.inputSchema.required).toBeUndefined();
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test("session_stats has no required params", async () => {
|
|
507
|
+
const result = await client.listTools();
|
|
508
|
+
const stats = result.tools.find(t => t.name === "session_stats");
|
|
509
|
+
expect(stats?.inputSchema.required).toBeUndefined();
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// ── list_sessions ────────────────────────────────────────────────
|
|
514
|
+
|
|
515
|
+
describe("list_sessions", () => {
|
|
516
|
+
test("returns all sessions when no limit", async () => {
|
|
517
|
+
const result = await client.callTool({
|
|
518
|
+
name: "list_sessions",
|
|
519
|
+
arguments: {},
|
|
520
|
+
});
|
|
521
|
+
expect(result.content).toHaveLength(1);
|
|
522
|
+
expect(getText(result)).toBe("LIST:2 sessions");
|
|
523
|
+
expect(result.isError).toBeFalsy();
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("respects limit parameter", async () => {
|
|
527
|
+
const result = await client.callTool({
|
|
528
|
+
name: "list_sessions",
|
|
529
|
+
arguments: { limit: 1 },
|
|
530
|
+
});
|
|
531
|
+
expect(getText(result)).toBe("LIST:1 sessions");
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test("limit=0 is treated as no-limit (0 is falsy)", async () => {
|
|
535
|
+
const result = await client.callTool({
|
|
536
|
+
name: "list_sessions",
|
|
537
|
+
arguments: { limit: 0 },
|
|
538
|
+
});
|
|
539
|
+
// limit=0 is falsy in JS, so the ternary treats it as undefined
|
|
540
|
+
expect(getText(result)).toBe("LIST:2 sessions");
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
test("limit larger than total returns all", async () => {
|
|
544
|
+
const result = await client.callTool({
|
|
545
|
+
name: "list_sessions",
|
|
546
|
+
arguments: { limit: 100 },
|
|
547
|
+
});
|
|
548
|
+
expect(getText(result)).toBe("LIST:2 sessions");
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test("non-numeric limit is ignored (returns all)", async () => {
|
|
552
|
+
const result = await client.callTool({
|
|
553
|
+
name: "list_sessions",
|
|
554
|
+
arguments: { limit: "five" },
|
|
555
|
+
});
|
|
556
|
+
// typeof "five" !== "number", so limit is undefined
|
|
557
|
+
expect(getText(result)).toBe("LIST:2 sessions");
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// ── search_sessions ──────────────────────────────────────────────
|
|
562
|
+
|
|
563
|
+
describe("search_sessions", () => {
|
|
564
|
+
test("returns matching sessions for a query", async () => {
|
|
565
|
+
const result = await client.callTool({
|
|
566
|
+
name: "search_sessions",
|
|
567
|
+
arguments: { query: "test" },
|
|
568
|
+
});
|
|
569
|
+
expect(getText(result)).toContain("SEARCH:");
|
|
570
|
+
expect(getText(result)).toContain('results for "test"');
|
|
571
|
+
expect(result.isError).toBeFalsy();
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
test("returns no-match message when nothing found", async () => {
|
|
575
|
+
const result = await client.callTool({
|
|
576
|
+
name: "search_sessions",
|
|
577
|
+
arguments: { query: "zzz_nonexistent_zzz" },
|
|
578
|
+
});
|
|
579
|
+
expect(getText(result)).toContain("No sessions found");
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test("returns error when query is empty string", async () => {
|
|
583
|
+
const result = await client.callTool({
|
|
584
|
+
name: "search_sessions",
|
|
585
|
+
arguments: { query: "" },
|
|
586
|
+
});
|
|
587
|
+
expect(getText(result)).toBe("Error: query is required.");
|
|
588
|
+
expect(result.isError).toBe(true);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test("returns error when query is whitespace only", async () => {
|
|
592
|
+
const result = await client.callTool({
|
|
593
|
+
name: "search_sessions",
|
|
594
|
+
arguments: { query: " " },
|
|
595
|
+
});
|
|
596
|
+
expect(getText(result)).toBe("Error: query is required.");
|
|
597
|
+
expect(result.isError).toBe(true);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
test("searches by session name", async () => {
|
|
601
|
+
const result = await client.callTool({
|
|
602
|
+
name: "search_sessions",
|
|
603
|
+
arguments: { query: "auth-refactor" },
|
|
604
|
+
});
|
|
605
|
+
expect(getText(result)).toContain("SEARCH:");
|
|
606
|
+
expect(getText(result)).toContain("1 results");
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
test("searches by project path", async () => {
|
|
610
|
+
const result = await client.callTool({
|
|
611
|
+
name: "search_sessions",
|
|
612
|
+
arguments: { query: "auth-service" },
|
|
613
|
+
});
|
|
614
|
+
expect(getText(result)).toContain("SEARCH:");
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
// ── show_session ─────────────────────────────────────────────────
|
|
619
|
+
|
|
620
|
+
describe("show_session", () => {
|
|
621
|
+
test("returns session detail for valid full ID", async () => {
|
|
622
|
+
const result = await client.callTool({
|
|
623
|
+
name: "show_session",
|
|
624
|
+
arguments: { id: "aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb" },
|
|
625
|
+
});
|
|
626
|
+
expect(getText(result)).toBe("DETAIL:aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb");
|
|
627
|
+
expect(result.isError).toBeFalsy();
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
test("resolves partial ID (prefix match)", async () => {
|
|
631
|
+
const result = await client.callTool({
|
|
632
|
+
name: "show_session",
|
|
633
|
+
arguments: { id: "aaaaaaaa" },
|
|
634
|
+
});
|
|
635
|
+
expect(getText(result)).toBe("DETAIL:aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb");
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
test("returns error for non-existent ID", async () => {
|
|
639
|
+
const result = await client.callTool({
|
|
640
|
+
name: "show_session",
|
|
641
|
+
arguments: { id: "ffffffff-0000-0000-0000-ffffffffffff" },
|
|
642
|
+
});
|
|
643
|
+
expect(getText(result)).toContain("Error:");
|
|
644
|
+
expect(getText(result)).toContain("No session found");
|
|
645
|
+
expect(result.isError).toBe(true);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test("returns error when id is empty", async () => {
|
|
649
|
+
const result = await client.callTool({
|
|
650
|
+
name: "show_session",
|
|
651
|
+
arguments: { id: "" },
|
|
652
|
+
});
|
|
653
|
+
expect(getText(result)).toBe("Error: id is required.");
|
|
654
|
+
expect(result.isError).toBe(true);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
test("returns error when id is whitespace only", async () => {
|
|
658
|
+
const result = await client.callTool({
|
|
659
|
+
name: "show_session",
|
|
660
|
+
arguments: { id: " " },
|
|
661
|
+
});
|
|
662
|
+
expect(getText(result)).toBe("Error: id is required.");
|
|
663
|
+
expect(result.isError).toBe(true);
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// ── name_session ─────────────────────────────────────────────────
|
|
668
|
+
|
|
669
|
+
describe("name_session", () => {
|
|
670
|
+
test("names a session with explicit ID", async () => {
|
|
671
|
+
const result = await client.callTool({
|
|
672
|
+
name: "name_session",
|
|
673
|
+
arguments: {
|
|
674
|
+
id: "aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb",
|
|
675
|
+
name: "my-test-session",
|
|
676
|
+
},
|
|
677
|
+
});
|
|
678
|
+
expect(getText(result)).toContain("Named session");
|
|
679
|
+
expect(getText(result)).toContain("aaaaaaaa");
|
|
680
|
+
expect(getText(result)).toContain("my-test-session");
|
|
681
|
+
expect(result.isError).toBeFalsy();
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
test("names the most recent session when no ID given", async () => {
|
|
685
|
+
const result = await client.callTool({
|
|
686
|
+
name: "name_session",
|
|
687
|
+
arguments: { name: "recent-work" },
|
|
688
|
+
});
|
|
689
|
+
// Should use first session from sessions array (most recent)
|
|
690
|
+
expect(getText(result)).toContain("Named session");
|
|
691
|
+
expect(getText(result)).toContain("aaaaaaaa");
|
|
692
|
+
expect(getText(result)).toContain("recent-work");
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
test("returns error when name is empty", async () => {
|
|
696
|
+
const result = await client.callTool({
|
|
697
|
+
name: "name_session",
|
|
698
|
+
arguments: { name: "" },
|
|
699
|
+
});
|
|
700
|
+
expect(getText(result)).toBe("Error: name is required.");
|
|
701
|
+
expect(result.isError).toBe(true);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
test("returns error when name is whitespace only", async () => {
|
|
705
|
+
const result = await client.callTool({
|
|
706
|
+
name: "name_session",
|
|
707
|
+
arguments: { name: " " },
|
|
708
|
+
});
|
|
709
|
+
expect(getText(result)).toBe("Error: name is required.");
|
|
710
|
+
expect(result.isError).toBe(true);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
test("returns error for non-existent session ID", async () => {
|
|
714
|
+
const result = await client.callTool({
|
|
715
|
+
name: "name_session",
|
|
716
|
+
arguments: {
|
|
717
|
+
id: "deadbeef-0000-0000-0000-000000000000",
|
|
718
|
+
name: "wont-work",
|
|
719
|
+
},
|
|
720
|
+
});
|
|
721
|
+
expect(getText(result)).toContain("Error:");
|
|
722
|
+
expect(getText(result)).toContain("No session found");
|
|
723
|
+
expect(result.isError).toBe(true);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
test("names via partial ID prefix", async () => {
|
|
727
|
+
const result = await client.callTool({
|
|
728
|
+
name: "name_session",
|
|
729
|
+
arguments: {
|
|
730
|
+
id: "cccccccc",
|
|
731
|
+
name: "partial-id-test",
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
expect(getText(result)).toContain("Named session");
|
|
735
|
+
expect(getText(result)).toContain("cccccccc");
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
// ── autoname_session ──────────────────────────────────────────────
|
|
740
|
+
|
|
741
|
+
describe("autoname_session", () => {
|
|
742
|
+
test("names the most recent session when no ID given", async () => {
|
|
743
|
+
const result = await client.callTool({
|
|
744
|
+
name: "autoname_session",
|
|
745
|
+
arguments: {},
|
|
746
|
+
});
|
|
747
|
+
expect(getText(result)).toContain("Named session");
|
|
748
|
+
expect(getText(result)).toContain("aaaaaaaa");
|
|
749
|
+
expect(getText(result)).toContain("31/03/26 04:14");
|
|
750
|
+
expect(result.isError).toBeFalsy();
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
test("names a session with explicit ID", async () => {
|
|
754
|
+
const result = await client.callTool({
|
|
755
|
+
name: "autoname_session",
|
|
756
|
+
arguments: { id: "cccccccc" },
|
|
757
|
+
});
|
|
758
|
+
expect(getText(result)).toContain("Named session");
|
|
759
|
+
expect(getText(result)).toContain("cccccccc");
|
|
760
|
+
expect(getText(result)).toContain("31/03/26 04:14");
|
|
761
|
+
expect(result.isError).toBeFalsy();
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
test("returns error for non-existent session ID", async () => {
|
|
765
|
+
const result = await client.callTool({
|
|
766
|
+
name: "autoname_session",
|
|
767
|
+
arguments: { id: "deadbeef-0000-0000-0000-000000000000" },
|
|
768
|
+
});
|
|
769
|
+
expect(getText(result)).toContain("Error:");
|
|
770
|
+
expect(getText(result)).toContain("No session found");
|
|
771
|
+
expect(result.isError).toBe(true);
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
// ── unname_session ───────────────────────────────────────────────
|
|
776
|
+
|
|
777
|
+
describe("unname_session", () => {
|
|
778
|
+
test("clears name from a named session", async () => {
|
|
779
|
+
// Session 2 has name "auth-refactor"
|
|
780
|
+
const result = await client.callTool({
|
|
781
|
+
name: "unname_session",
|
|
782
|
+
arguments: { id: "cccccccc-4444-5555-6666-dddddddddddd" },
|
|
783
|
+
});
|
|
784
|
+
expect(getText(result)).toContain("Cleared name from session");
|
|
785
|
+
expect(getText(result)).toContain("cccccccc");
|
|
786
|
+
expect(result.isError).toBeFalsy();
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
test("returns error when session has no name to clear", async () => {
|
|
790
|
+
// Session 1 has no name (empty string)
|
|
791
|
+
const result = await client.callTool({
|
|
792
|
+
name: "unname_session",
|
|
793
|
+
arguments: { id: "aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb" },
|
|
794
|
+
});
|
|
795
|
+
expect(getText(result)).toContain("Error:");
|
|
796
|
+
expect(getText(result)).toContain("has no name to clear");
|
|
797
|
+
expect(result.isError).toBe(true);
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
test("returns error for non-existent session ID", async () => {
|
|
801
|
+
const result = await client.callTool({
|
|
802
|
+
name: "unname_session",
|
|
803
|
+
arguments: { id: "deadbeef-0000-0000-0000-000000000000" },
|
|
804
|
+
});
|
|
805
|
+
expect(getText(result)).toContain("Error:");
|
|
806
|
+
expect(getText(result)).toContain("No session found");
|
|
807
|
+
expect(result.isError).toBe(true);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
test("works with partial ID prefix", async () => {
|
|
811
|
+
const result = await client.callTool({
|
|
812
|
+
name: "unname_session",
|
|
813
|
+
arguments: { id: "cccccccc" },
|
|
814
|
+
});
|
|
815
|
+
expect(getText(result)).toContain("Cleared name from session");
|
|
816
|
+
});
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
// ── session_stats ────────────────────────────────────────────────
|
|
820
|
+
|
|
821
|
+
describe("session_stats", () => {
|
|
822
|
+
test("returns stats for all sessions", async () => {
|
|
823
|
+
const result = await client.callTool({
|
|
824
|
+
name: "session_stats",
|
|
825
|
+
arguments: {},
|
|
826
|
+
});
|
|
827
|
+
expect(getText(result)).toBe("STATS:2 sessions");
|
|
828
|
+
expect(result.isError).toBeFalsy();
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
test("returns single text content block", async () => {
|
|
832
|
+
const result = await client.callTool({
|
|
833
|
+
name: "session_stats",
|
|
834
|
+
arguments: {},
|
|
835
|
+
});
|
|
836
|
+
const content = result.content as TextContent[];
|
|
837
|
+
expect(content).toHaveLength(1);
|
|
838
|
+
expect(content[0].type).toBe("text");
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
// ── Error handling ───────────────────────────────────────────────
|
|
843
|
+
|
|
844
|
+
describe("error handling", () => {
|
|
845
|
+
test("all error responses include isError: true", async () => {
|
|
846
|
+
const searchResult = await client.callTool({
|
|
847
|
+
name: "search_sessions",
|
|
848
|
+
arguments: { query: "" },
|
|
849
|
+
});
|
|
850
|
+
expect(searchResult.isError).toBe(true);
|
|
851
|
+
|
|
852
|
+
const showResult = await client.callTool({
|
|
853
|
+
name: "show_session",
|
|
854
|
+
arguments: { id: "" },
|
|
855
|
+
});
|
|
856
|
+
expect(showResult.isError).toBe(true);
|
|
857
|
+
|
|
858
|
+
const nameResult = await client.callTool({
|
|
859
|
+
name: "name_session",
|
|
860
|
+
arguments: { name: "" },
|
|
861
|
+
});
|
|
862
|
+
expect(nameResult.isError).toBe(true);
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
test("all error messages start with 'Error:'", async () => {
|
|
866
|
+
const searchResult = await client.callTool({
|
|
867
|
+
name: "search_sessions",
|
|
868
|
+
arguments: { query: "" },
|
|
869
|
+
});
|
|
870
|
+
expect(getText(searchResult)).toMatch(/^Error:/);
|
|
871
|
+
|
|
872
|
+
const showResult = await client.callTool({
|
|
873
|
+
name: "show_session",
|
|
874
|
+
arguments: { id: "" },
|
|
875
|
+
});
|
|
876
|
+
expect(getText(showResult)).toMatch(/^Error:/);
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
test("all successful responses have isError falsy", async () => {
|
|
880
|
+
const listResult = await client.callTool({
|
|
881
|
+
name: "list_sessions",
|
|
882
|
+
arguments: {},
|
|
883
|
+
});
|
|
884
|
+
expect(listResult.isError).toBeFalsy();
|
|
885
|
+
|
|
886
|
+
const statsResult = await client.callTool({
|
|
887
|
+
name: "session_stats",
|
|
888
|
+
arguments: {},
|
|
889
|
+
});
|
|
890
|
+
expect(statsResult.isError).toBeFalsy();
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
// ─── Empty state tests ──────────────────────────────────────────────
|
|
896
|
+
|
|
897
|
+
describe("MCP tool handlers (empty state)", () => {
|
|
898
|
+
let client: Client;
|
|
899
|
+
|
|
900
|
+
beforeEach(async () => {
|
|
901
|
+
const setup = await setupClientServer(emptyDeps());
|
|
902
|
+
client = setup.client;
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
afterEach(async () => {
|
|
906
|
+
await client.close();
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
test("list_sessions returns empty list", async () => {
|
|
910
|
+
const result = await client.callTool({
|
|
911
|
+
name: "list_sessions",
|
|
912
|
+
arguments: {},
|
|
913
|
+
});
|
|
914
|
+
expect(getText(result)).toBe("LIST:0 sessions");
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
test("search_sessions returns no-match on empty index", async () => {
|
|
918
|
+
const result = await client.callTool({
|
|
919
|
+
name: "search_sessions",
|
|
920
|
+
arguments: { query: "anything" },
|
|
921
|
+
});
|
|
922
|
+
expect(getText(result)).toContain("No sessions found");
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
test("session_stats returns empty stats", async () => {
|
|
926
|
+
const result = await client.callTool({
|
|
927
|
+
name: "session_stats",
|
|
928
|
+
arguments: {},
|
|
929
|
+
});
|
|
930
|
+
expect(getText(result)).toBe("STATS:0 sessions");
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
test("name_session without ID errors when no sessions exist", async () => {
|
|
934
|
+
const result = await client.callTool({
|
|
935
|
+
name: "name_session",
|
|
936
|
+
arguments: { name: "test-name" },
|
|
937
|
+
});
|
|
938
|
+
expect(getText(result)).toContain("Error:");
|
|
939
|
+
expect(getText(result)).toContain("No sessions found");
|
|
940
|
+
expect(result.isError).toBe(true);
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
test("unname_session without ID errors when no sessions exist", async () => {
|
|
944
|
+
const result = await client.callTool({
|
|
945
|
+
name: "unname_session",
|
|
946
|
+
arguments: { id: "anything" },
|
|
947
|
+
});
|
|
948
|
+
expect(getText(result)).toContain("Error:");
|
|
949
|
+
expect(result.isError).toBe(true);
|
|
950
|
+
});
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
// ─── Exception handling tests ───────────────────────────────────────
|
|
954
|
+
|
|
955
|
+
describe("MCP tool handlers (exception in deps)", () => {
|
|
956
|
+
let client: Client;
|
|
957
|
+
|
|
958
|
+
beforeEach(async () => {
|
|
959
|
+
const errorDeps: MockDeps = {
|
|
960
|
+
sessions: [...MOCK_SESSIONS],
|
|
961
|
+
nameSessionFn: async () => { throw new Error("Database connection failed"); },
|
|
962
|
+
clearSessionNameFn: async () => { throw new Error("Database connection failed"); },
|
|
963
|
+
};
|
|
964
|
+
const setup = await setupClientServer(errorDeps);
|
|
965
|
+
client = setup.client;
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
afterEach(async () => {
|
|
969
|
+
await client.close();
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
test("name_session catches thrown exceptions", async () => {
|
|
973
|
+
const result = await client.callTool({
|
|
974
|
+
name: "name_session",
|
|
975
|
+
arguments: { id: "aaaaaaaa", name: "test" },
|
|
976
|
+
});
|
|
977
|
+
expect(getText(result)).toContain("Error: Database connection failed");
|
|
978
|
+
expect(result.isError).toBe(true);
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
test("unname_session catches thrown exceptions", async () => {
|
|
982
|
+
const result = await client.callTool({
|
|
983
|
+
name: "unname_session",
|
|
984
|
+
arguments: { id: "aaaaaaaa" },
|
|
985
|
+
});
|
|
986
|
+
expect(getText(result)).toContain("Error: Database connection failed");
|
|
987
|
+
expect(result.isError).toBe(true);
|
|
988
|
+
});
|
|
989
|
+
});
|