@opsee/mcp-server 0.9.34 → 0.9.36
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 +1 -1
- package/gen/api/v1/doc_space_pb.d.ts +11 -0
- package/gen/api/v1/doc_space_pb.js +1 -1
- package/package.json +2 -1
- package/src/tools/docs.ts +28 -27
- package/src/tools/initiatives.ts +2 -1
- package/src/tools/milestones.ts +5 -3
- package/src/utils/format/blocknote.ts +21 -0
- package/src/utils/format/index.ts +1 -1
- package/src/__tests__/blocknote-fixtures.test.ts +0 -58
- package/src/__tests__/blocknote-markdown.test.ts +0 -92
- package/src/__tests__/comments.test.ts +0 -193
- package/src/__tests__/initiatives.test.ts +0 -188
- package/src/__tests__/kv-store.test.ts +0 -53
- package/src/__tests__/task-dependencies.test.ts +0 -173
- package/src/__tests__/tools.test.ts +0 -913
- package/src/__tests__/work-logs.test.ts +0 -140
- package/src/auth/__tests__/manager.test.ts +0 -67
- package/src/auth/__tests__/oauth-provider.test.ts +0 -272
- package/src/utils/__tests__/format.test.ts +0 -53
|
@@ -1,913 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
-
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
4
|
-
import { createServer } from "../server.js";
|
|
5
|
-
import type { ApiClients } from "../client/api.js";
|
|
6
|
-
import { Code, ConnectError } from "@connectrpc/connect";
|
|
7
|
-
|
|
8
|
-
// --- Mock data ---
|
|
9
|
-
|
|
10
|
-
const mockProject = {
|
|
11
|
-
id: 1,
|
|
12
|
-
name: "Test Project",
|
|
13
|
-
shortName: "TP",
|
|
14
|
-
slug: "test-project",
|
|
15
|
-
description: "A test project",
|
|
16
|
-
isActive: true,
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const mockTask = {
|
|
20
|
-
id: 10,
|
|
21
|
-
identifier: "TP-1",
|
|
22
|
-
title: "Fix the bug",
|
|
23
|
-
description: "Something is broken",
|
|
24
|
-
displayOrder: 0,
|
|
25
|
-
storyPoints: 3,
|
|
26
|
-
aiModeEnabled: false,
|
|
27
|
-
boardColumn: { id: 1, name: "To Do" },
|
|
28
|
-
taskPriority: { id: 2, name: "High" },
|
|
29
|
-
taskType: { id: 1, name: "Bug" },
|
|
30
|
-
assignedUser: { id: 5, fullName: "Alice" },
|
|
31
|
-
cycle: { id: 1, name: "Sprint 1" },
|
|
32
|
-
reporterUser: { id: 5 },
|
|
33
|
-
board: { id: 1 },
|
|
34
|
-
project: { id: 1 },
|
|
35
|
-
taskRepositories: [],
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const mockUser = {
|
|
39
|
-
id: 5,
|
|
40
|
-
fullName: "Alice Test",
|
|
41
|
-
email: "alice@test.com",
|
|
42
|
-
companyId: 1,
|
|
43
|
-
company: { name: "TestCo" },
|
|
44
|
-
flattenedRoles: ["User"],
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const mockBoard = { id: 1, name: "Main Board", isActive: true };
|
|
48
|
-
const mockBoardColumn = {
|
|
49
|
-
id: 1,
|
|
50
|
-
name: "To Do",
|
|
51
|
-
displayOrder: 0,
|
|
52
|
-
lifecycleState: "active",
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const mockCycle = {
|
|
56
|
-
id: 1,
|
|
57
|
-
name: "Sprint 1",
|
|
58
|
-
description: "First sprint",
|
|
59
|
-
goal: "Ship v1",
|
|
60
|
-
startDate: "2026-03-01",
|
|
61
|
-
endDate: "2026-03-15",
|
|
62
|
-
isActive: true,
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const mockDocSpace = {
|
|
66
|
-
id: 1,
|
|
67
|
-
title: "Engineering Docs",
|
|
68
|
-
description: "Technical documentation",
|
|
69
|
-
slug: "engineering-docs",
|
|
70
|
-
isDefault: true,
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const mockDocPage = {
|
|
74
|
-
id: 1,
|
|
75
|
-
title: "Getting Started",
|
|
76
|
-
slug: "getting-started",
|
|
77
|
-
currentContent: "Welcome to the docs",
|
|
78
|
-
currentVersionNumber: 1,
|
|
79
|
-
createdByUser: { fullName: "Alice" },
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const mockTaskType = { id: 1, name: "Bug" };
|
|
83
|
-
const mockTaskPriority = { id: 2, name: "High", level: 2 };
|
|
84
|
-
const mockVcsIntegration = {
|
|
85
|
-
id: 1,
|
|
86
|
-
provider: 2,
|
|
87
|
-
repoOwner: "acme",
|
|
88
|
-
repoName: "backend",
|
|
89
|
-
defaultBranch: "main",
|
|
90
|
-
isActive: true,
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
// --- Mock client factory ---
|
|
94
|
-
|
|
95
|
-
function createMockClients(): ApiClients {
|
|
96
|
-
return {
|
|
97
|
-
projects: {
|
|
98
|
-
getProjects: async () => ({ projects: [mockProject] }),
|
|
99
|
-
getProject: async () => ({ project: mockProject }),
|
|
100
|
-
},
|
|
101
|
-
tasks: {
|
|
102
|
-
getTasks: async () => ({ tasks: [mockTask] }),
|
|
103
|
-
getTask: async () => ({ task: mockTask }),
|
|
104
|
-
addTask: async () => ({ task: mockTask }),
|
|
105
|
-
editTask: async () => ({ task: { ...mockTask, title: "Updated bug" } }),
|
|
106
|
-
bulkEditTasks: async (req: { taskIds: number[] }) => ({
|
|
107
|
-
updatedCount: req.taskIds.length,
|
|
108
|
-
}),
|
|
109
|
-
deleteTask: async () => ({}),
|
|
110
|
-
bulkDeleteTasks: async () => ({ deletedCount: 2 }),
|
|
111
|
-
addWorkLog: async (req: { taskId: number; hours: number }) => ({
|
|
112
|
-
workLog: { id: 42, taskId: req.taskId, userId: 5, hours: req.hours },
|
|
113
|
-
}),
|
|
114
|
-
deleteWorkLog: async () => ({}),
|
|
115
|
-
},
|
|
116
|
-
comments: {
|
|
117
|
-
getComment: async () => ({ comment: { id: 11, content: "old text", isInternal: false, taskId: 10, userId: 5 } }),
|
|
118
|
-
getComments: async () => ({ comments: [] }),
|
|
119
|
-
editComment: async () => ({ comment: { id: 11, content: "edited text", isInternal: true, taskId: 10, userId: 5 } }),
|
|
120
|
-
},
|
|
121
|
-
taskLabels: {
|
|
122
|
-
getTaskLabels: async () => ({ taskLabels: [] }),
|
|
123
|
-
},
|
|
124
|
-
taskDependencies: {
|
|
125
|
-
getTaskDependencies: async () => ({ taskDependencies: [] }),
|
|
126
|
-
getDependencyChain: async () => ({
|
|
127
|
-
nodes: [{ id: 10, identifier: "TP-1", title: "Fix the bug" }],
|
|
128
|
-
edges: [],
|
|
129
|
-
}),
|
|
130
|
-
},
|
|
131
|
-
acceptanceCriteria: {
|
|
132
|
-
getAcceptanceCriteriaByTask: async () => ({
|
|
133
|
-
criteria: [{ id: 1, taskId: 10, text: "endpoint returns 200", done: false, displayOrder: 0 }],
|
|
134
|
-
}),
|
|
135
|
-
addAcceptanceCriterion: async (req: { taskId: number; text: string; displayOrder?: number }) => ({
|
|
136
|
-
criterion: { id: 2, taskId: req.taskId, text: req.text, done: false, displayOrder: req.displayOrder ?? 0 },
|
|
137
|
-
}),
|
|
138
|
-
editAcceptanceCriterion: async (req: { id: number; text?: string; done?: boolean }) => ({
|
|
139
|
-
criterion: { id: req.id, taskId: 10, text: req.text ?? "old", done: req.done ?? true, displayOrder: 0 },
|
|
140
|
-
}),
|
|
141
|
-
deleteAcceptanceCriterion: async () => ({}),
|
|
142
|
-
},
|
|
143
|
-
notifications: {
|
|
144
|
-
getNotifications: async () => ({
|
|
145
|
-
notifications: [
|
|
146
|
-
{
|
|
147
|
-
id: 1,
|
|
148
|
-
title: "You were assigned to TP-1",
|
|
149
|
-
message: "Alice assigned you",
|
|
150
|
-
notificationType: "task_assigned",
|
|
151
|
-
entityType: "task",
|
|
152
|
-
entityId: 10,
|
|
153
|
-
isRead: false,
|
|
154
|
-
sentViaEmail: false,
|
|
155
|
-
userId: 5,
|
|
156
|
-
},
|
|
157
|
-
],
|
|
158
|
-
}),
|
|
159
|
-
getNotification: async () => ({
|
|
160
|
-
notification: {
|
|
161
|
-
id: 1,
|
|
162
|
-
title: "You were assigned to TP-1",
|
|
163
|
-
message: "Alice assigned you",
|
|
164
|
-
notificationType: "task_assigned",
|
|
165
|
-
isRead: false,
|
|
166
|
-
sentViaEmail: false,
|
|
167
|
-
userId: 5,
|
|
168
|
-
},
|
|
169
|
-
}),
|
|
170
|
-
editNotification: async () => ({
|
|
171
|
-
notification: { id: 1, title: "x", message: "y", notificationType: "task_assigned", isRead: true, sentViaEmail: false, userId: 5 },
|
|
172
|
-
}),
|
|
173
|
-
},
|
|
174
|
-
boards: {
|
|
175
|
-
getBoards: async () => ({ boards: [mockBoard] }),
|
|
176
|
-
},
|
|
177
|
-
boardColumns: {
|
|
178
|
-
getBoardColumns: async () => ({ boardColumns: [mockBoardColumn] }),
|
|
179
|
-
},
|
|
180
|
-
cycles: {
|
|
181
|
-
getCycles: async () => ({ cycles: [mockCycle] }),
|
|
182
|
-
getCycle: async () => ({ cycle: { ...mockCycle, projectId: 1 } }),
|
|
183
|
-
addCycle: async () => ({ cycle: { ...mockCycle, id: 2, name: "Sprint 2" } }),
|
|
184
|
-
editCycle: async () => ({ cycle: { ...mockCycle, name: "Sprint 1 renamed" } }),
|
|
185
|
-
deleteCycle: async () => ({}),
|
|
186
|
-
},
|
|
187
|
-
docSpaces: {
|
|
188
|
-
getDocSpaces: async () => ({ docSpaces: [mockDocSpace] }),
|
|
189
|
-
},
|
|
190
|
-
docPages: {
|
|
191
|
-
getDocPages: async () => ({ docPages: [mockDocPage] }),
|
|
192
|
-
getDocPage: async () => ({ docPage: mockDocPage }),
|
|
193
|
-
addDocPage: async () => ({ docPage: mockDocPage }),
|
|
194
|
-
},
|
|
195
|
-
taskTypes: {
|
|
196
|
-
getTaskTypes: async () => ({ taskTypes: [mockTaskType] }),
|
|
197
|
-
},
|
|
198
|
-
taskPriorities: {
|
|
199
|
-
getTaskPriorities: async () => ({
|
|
200
|
-
taskPriorities: [mockTaskPriority],
|
|
201
|
-
}),
|
|
202
|
-
},
|
|
203
|
-
users: {
|
|
204
|
-
getMe: async () => ({ user: mockUser }),
|
|
205
|
-
},
|
|
206
|
-
vcsIntegrations: {
|
|
207
|
-
getVCSIntegrations: async () => ({
|
|
208
|
-
vcsIntegrations: [mockVcsIntegration],
|
|
209
|
-
}),
|
|
210
|
-
},
|
|
211
|
-
} as unknown as ApiClients;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// --- Test suite ---
|
|
215
|
-
|
|
216
|
-
describe("MCP Tools", () => {
|
|
217
|
-
let client: Client;
|
|
218
|
-
let cleanup: () => Promise<void>;
|
|
219
|
-
|
|
220
|
-
beforeAll(async () => {
|
|
221
|
-
const [clientTransport, serverTransport] =
|
|
222
|
-
InMemoryTransport.createLinkedPair();
|
|
223
|
-
const server = createServer(() => createMockClients());
|
|
224
|
-
await server.connect(serverTransport);
|
|
225
|
-
|
|
226
|
-
client = new Client({ name: "test-client", version: "1.0.0" });
|
|
227
|
-
await client.connect(clientTransport);
|
|
228
|
-
|
|
229
|
-
cleanup = async () => {
|
|
230
|
-
await client.close();
|
|
231
|
-
await server.close();
|
|
232
|
-
};
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
afterAll(async () => {
|
|
236
|
-
await cleanup();
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
// --- Project tools ---
|
|
240
|
-
|
|
241
|
-
test("opsee_list_projects returns projects", async () => {
|
|
242
|
-
const result = await client.callTool({
|
|
243
|
-
name: "opsee_list_projects",
|
|
244
|
-
arguments: {},
|
|
245
|
-
});
|
|
246
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0]
|
|
247
|
-
.text;
|
|
248
|
-
expect(text).toContain("Test Project");
|
|
249
|
-
expect(text).toContain("TP");
|
|
250
|
-
expect(text).toContain("1 project(s)");
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
test("opsee_get_project returns project details", async () => {
|
|
254
|
-
const result = await client.callTool({
|
|
255
|
-
name: "opsee_get_project",
|
|
256
|
-
arguments: { projectId: 1 },
|
|
257
|
-
});
|
|
258
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0]
|
|
259
|
-
.text;
|
|
260
|
-
expect(text).toContain("Test Project");
|
|
261
|
-
expect(text).toContain("test-project");
|
|
262
|
-
expect(text).toContain("ID: 1");
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
// --- Task tools ---
|
|
266
|
-
|
|
267
|
-
test("opsee_list_tasks returns tasks for a project", async () => {
|
|
268
|
-
const result = await client.callTool({
|
|
269
|
-
name: "opsee_list_tasks",
|
|
270
|
-
arguments: { projectId: 1 },
|
|
271
|
-
});
|
|
272
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0]
|
|
273
|
-
.text;
|
|
274
|
-
expect(text).toContain("TP-1");
|
|
275
|
-
expect(text).toContain("Fix the bug");
|
|
276
|
-
expect(text).toContain("1 task(s)");
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
test("opsee_get_task returns task details", async () => {
|
|
280
|
-
const result = await client.callTool({
|
|
281
|
-
name: "opsee_get_task",
|
|
282
|
-
arguments: { taskId: 10 },
|
|
283
|
-
});
|
|
284
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0]
|
|
285
|
-
.text;
|
|
286
|
-
expect(text).toContain("TP-1");
|
|
287
|
-
expect(text).toContain("Fix the bug");
|
|
288
|
-
expect(text).toContain("High");
|
|
289
|
-
expect(text).toContain("Bug");
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
test("opsee_get_task rejects a non-positive / non-integer taskId", async () => {
|
|
293
|
-
// The ID-validation sweep: an id must be a positive integer. Zero,
|
|
294
|
-
// negatives, and fractionals fail schema validation (isError) before the
|
|
295
|
-
// handler runs. A valid positive integer (taskId: 10) succeeds above.
|
|
296
|
-
for (const taskId of [0, 1.5, -3]) {
|
|
297
|
-
const result = await client.callTool({
|
|
298
|
-
name: "opsee_get_task",
|
|
299
|
-
arguments: { taskId },
|
|
300
|
-
});
|
|
301
|
-
expect(result.isError, `taskId=${taskId} should be rejected`).toBe(true);
|
|
302
|
-
}
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
test("opsee_create_task creates a task with defaults", async () => {
|
|
306
|
-
const result = await client.callTool({
|
|
307
|
-
name: "opsee_create_task",
|
|
308
|
-
arguments: { projectId: 1, title: "New task" },
|
|
309
|
-
});
|
|
310
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0]
|
|
311
|
-
.text;
|
|
312
|
-
expect(text).toContain("Task created");
|
|
313
|
-
expect(text).toContain("TP-1");
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
test("opsee_update_task does read-modify-write", async () => {
|
|
317
|
-
const result = await client.callTool({
|
|
318
|
-
name: "opsee_update_task",
|
|
319
|
-
arguments: { taskId: 10, title: "Updated title" },
|
|
320
|
-
});
|
|
321
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0]
|
|
322
|
-
.text;
|
|
323
|
-
expect(text).toContain("Task updated");
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
// --- User tools ---
|
|
327
|
-
|
|
328
|
-
test("opsee_get_me returns current user", async () => {
|
|
329
|
-
const result = await client.callTool({
|
|
330
|
-
name: "opsee_get_me",
|
|
331
|
-
arguments: {},
|
|
332
|
-
});
|
|
333
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0]
|
|
334
|
-
.text;
|
|
335
|
-
expect(text).toContain("Alice Test");
|
|
336
|
-
expect(text).toContain("alice@test.com");
|
|
337
|
-
expect(text).toContain("TestCo");
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
// --- Board/metadata tools ---
|
|
341
|
-
|
|
342
|
-
test("opsee_list_boards returns boards", async () => {
|
|
343
|
-
const result = await client.callTool({
|
|
344
|
-
name: "opsee_list_boards",
|
|
345
|
-
arguments: { projectId: 1 },
|
|
346
|
-
});
|
|
347
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0]
|
|
348
|
-
.text;
|
|
349
|
-
expect(text).toContain("Main Board");
|
|
350
|
-
expect(text).toContain("ID: 1");
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
test("opsee_list_board_columns returns columns", async () => {
|
|
354
|
-
const result = await client.callTool({
|
|
355
|
-
name: "opsee_list_board_columns",
|
|
356
|
-
arguments: { boardId: 1 },
|
|
357
|
-
});
|
|
358
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0]
|
|
359
|
-
.text;
|
|
360
|
-
expect(text).toContain("To Do");
|
|
361
|
-
expect(text).toContain("ID: 1");
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
test("opsee_list_task_types returns types", async () => {
|
|
365
|
-
const result = await client.callTool({
|
|
366
|
-
name: "opsee_list_task_types",
|
|
367
|
-
arguments: { projectId: 1 },
|
|
368
|
-
});
|
|
369
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0]
|
|
370
|
-
.text;
|
|
371
|
-
expect(text).toContain("Bug");
|
|
372
|
-
expect(text).toContain("ID: 1");
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
test("opsee_list_task_priorities returns priorities", async () => {
|
|
376
|
-
const result = await client.callTool({
|
|
377
|
-
name: "opsee_list_task_priorities",
|
|
378
|
-
arguments: { projectId: 1 },
|
|
379
|
-
});
|
|
380
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0]
|
|
381
|
-
.text;
|
|
382
|
-
expect(text).toContain("High");
|
|
383
|
-
expect(text).toContain("level: 2");
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
// --- Cycle tools ---
|
|
387
|
-
|
|
388
|
-
test("opsee_list_cycles returns cycles", async () => {
|
|
389
|
-
const result = await client.callTool({
|
|
390
|
-
name: "opsee_list_cycles",
|
|
391
|
-
arguments: { projectId: 1 },
|
|
392
|
-
});
|
|
393
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0]
|
|
394
|
-
.text;
|
|
395
|
-
expect(text).toContain("Sprint 1");
|
|
396
|
-
expect(text).toContain("1 cycle(s)");
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
test("opsee_get_cycle returns cycle details", async () => {
|
|
400
|
-
const result = await client.callTool({
|
|
401
|
-
name: "opsee_get_cycle",
|
|
402
|
-
arguments: { cycleId: 1 },
|
|
403
|
-
});
|
|
404
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0]
|
|
405
|
-
.text;
|
|
406
|
-
expect(text).toContain("Sprint 1");
|
|
407
|
-
expect(text).toContain("Ship v1");
|
|
408
|
-
expect(text).toContain("ID: 1");
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
test("opsee_create_cycle creates a cycle", async () => {
|
|
412
|
-
const result = await client.callTool({
|
|
413
|
-
name: "opsee_create_cycle",
|
|
414
|
-
arguments: {
|
|
415
|
-
projectId: 1,
|
|
416
|
-
name: "Sprint 2",
|
|
417
|
-
startDate: "2026-03-25",
|
|
418
|
-
endDate: "2026-04-08",
|
|
419
|
-
},
|
|
420
|
-
});
|
|
421
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0]
|
|
422
|
-
.text;
|
|
423
|
-
expect(text).toContain("Cycle created");
|
|
424
|
-
expect(text).toContain("Sprint 2");
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
// --- Doc tools ---
|
|
428
|
-
|
|
429
|
-
test("opsee_list_doc_spaces returns spaces", async () => {
|
|
430
|
-
const result = await client.callTool({
|
|
431
|
-
name: "opsee_list_doc_spaces",
|
|
432
|
-
arguments: { projectId: 1 },
|
|
433
|
-
});
|
|
434
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0]
|
|
435
|
-
.text;
|
|
436
|
-
expect(text).toContain("Engineering Docs");
|
|
437
|
-
expect(text).toContain("1 doc space(s)");
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
test("opsee_list_doc_pages returns pages", async () => {
|
|
441
|
-
const result = await client.callTool({
|
|
442
|
-
name: "opsee_list_doc_pages",
|
|
443
|
-
arguments: { docSpaceId: 1 },
|
|
444
|
-
});
|
|
445
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0]
|
|
446
|
-
.text;
|
|
447
|
-
expect(text).toContain("Getting Started");
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
test("opsee_get_doc_page returns page content", async () => {
|
|
451
|
-
const result = await client.callTool({
|
|
452
|
-
name: "opsee_get_doc_page",
|
|
453
|
-
arguments: { pageId: 1 },
|
|
454
|
-
});
|
|
455
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0]
|
|
456
|
-
.text;
|
|
457
|
-
expect(text).toContain("Getting Started");
|
|
458
|
-
expect(text).toContain("Welcome to the docs");
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
test("opsee_create_doc_page creates a page", async () => {
|
|
462
|
-
const result = await client.callTool({
|
|
463
|
-
name: "opsee_create_doc_page",
|
|
464
|
-
arguments: {
|
|
465
|
-
projectId: 1,
|
|
466
|
-
title: "New Page",
|
|
467
|
-
content: "Hello world",
|
|
468
|
-
},
|
|
469
|
-
});
|
|
470
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0]
|
|
471
|
-
.text;
|
|
472
|
-
expect(text).toContain("Doc page created");
|
|
473
|
-
expect(text).toContain("Getting Started");
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
// --- Repository tools ---
|
|
477
|
-
|
|
478
|
-
test("opsee_list_repositories returns connected repos", async () => {
|
|
479
|
-
const result = await client.callTool({
|
|
480
|
-
name: "opsee_list_repositories",
|
|
481
|
-
arguments: { projectId: 1 },
|
|
482
|
-
});
|
|
483
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0]
|
|
484
|
-
.text;
|
|
485
|
-
expect(text).toContain("acme/backend");
|
|
486
|
-
expect(text).toContain("GitLab");
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
// --- Error handling ---
|
|
490
|
-
|
|
491
|
-
test("tool returns isError on API failure", async () => {
|
|
492
|
-
const errorClients = createMockClients();
|
|
493
|
-
(errorClients.projects as any).getProjects = async () => {
|
|
494
|
-
throw new Error("connection refused ECONNREFUSED");
|
|
495
|
-
};
|
|
496
|
-
|
|
497
|
-
const [ct, st] = InMemoryTransport.createLinkedPair();
|
|
498
|
-
const errorServer = createServer(() => errorClients);
|
|
499
|
-
await errorServer.connect(st);
|
|
500
|
-
|
|
501
|
-
const errorClient = new Client({
|
|
502
|
-
name: "error-test",
|
|
503
|
-
version: "1.0.0",
|
|
504
|
-
});
|
|
505
|
-
await errorClient.connect(ct);
|
|
506
|
-
|
|
507
|
-
const result = await errorClient.callTool({
|
|
508
|
-
name: "opsee_list_projects",
|
|
509
|
-
arguments: {},
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
expect(result.isError).toBe(true);
|
|
513
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0]
|
|
514
|
-
.text;
|
|
515
|
-
expect(text).toContain("ECONNREFUSED");
|
|
516
|
-
|
|
517
|
-
await errorClient.close();
|
|
518
|
-
await errorServer.close();
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
// --- Track 3 bulk + cycle + label additions ---
|
|
522
|
-
|
|
523
|
-
test("opsee_create_task accepts labelIds and returns the task", async () => {
|
|
524
|
-
const result = await client.callTool({
|
|
525
|
-
name: "opsee_create_task",
|
|
526
|
-
arguments: { projectId: 1, title: "with labels", labelIds: [1, 2] },
|
|
527
|
-
});
|
|
528
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
529
|
-
expect(text).toContain("Task created");
|
|
530
|
-
expect(text).toContain("TP-1");
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
test("opsee_update_task accepts labelIds replace-set", async () => {
|
|
534
|
-
const result = await client.callTool({
|
|
535
|
-
name: "opsee_update_task",
|
|
536
|
-
arguments: { taskId: 10, labelIds: [3] },
|
|
537
|
-
});
|
|
538
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
539
|
-
expect(text).toContain("Task updated");
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
test("opsee_bulk_update_tasks reports per-task count", async () => {
|
|
543
|
-
const result = await client.callTool({
|
|
544
|
-
name: "opsee_bulk_update_tasks",
|
|
545
|
-
arguments: { projectId: 1, taskIds: [10, 11, 12], boardColumnId: 5 },
|
|
546
|
-
});
|
|
547
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
548
|
-
expect(text).toContain("Updated 3 of 3");
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
test("opsee_bulk_update_tasks accepts storyPoints/estimatedHours/parentTaskId/labelIds", async () => {
|
|
552
|
-
const result = await client.callTool({
|
|
553
|
-
name: "opsee_bulk_update_tasks",
|
|
554
|
-
arguments: {
|
|
555
|
-
projectId: 1,
|
|
556
|
-
taskIds: [10, 11],
|
|
557
|
-
storyPoints: 5,
|
|
558
|
-
estimatedHours: 8,
|
|
559
|
-
parentTaskId: 3,
|
|
560
|
-
labelIds: [1, 2],
|
|
561
|
-
},
|
|
562
|
-
});
|
|
563
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
564
|
-
expect(text).toContain("Updated 2 of 2");
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
test("opsee_bulk_move_to_cycle delegates to bulk edit", async () => {
|
|
568
|
-
const result = await client.callTool({
|
|
569
|
-
name: "opsee_bulk_move_to_cycle",
|
|
570
|
-
arguments: { projectId: 1, taskIds: [10, 11], cycleId: 7 },
|
|
571
|
-
});
|
|
572
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
573
|
-
expect(text).toContain("Moved 2 of 2");
|
|
574
|
-
expect(text).toContain("cycle 7");
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
test("opsee_bulk_move_to_column delegates to bulk edit", async () => {
|
|
578
|
-
const result = await client.callTool({
|
|
579
|
-
name: "opsee_bulk_move_to_column",
|
|
580
|
-
arguments: { projectId: 1, taskIds: [10], boardColumnId: 5 },
|
|
581
|
-
});
|
|
582
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
583
|
-
expect(text).toContain("Moved 1 of 1");
|
|
584
|
-
expect(text).toContain("column 5");
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
test("opsee_bulk_assign delegates to bulk edit", async () => {
|
|
588
|
-
const result = await client.callTool({
|
|
589
|
-
name: "opsee_bulk_assign",
|
|
590
|
-
arguments: { projectId: 1, taskIds: [10, 11, 12], assigneeId: 5 },
|
|
591
|
-
});
|
|
592
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
593
|
-
expect(text).toContain("Assigned 3 of 3");
|
|
594
|
-
expect(text).toContain("user 5");
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
test("opsee_bulk_set_priority delegates to bulk edit", async () => {
|
|
598
|
-
const result = await client.callTool({
|
|
599
|
-
name: "opsee_bulk_set_priority",
|
|
600
|
-
arguments: { projectId: 1, taskIds: [10], priorityId: 2 },
|
|
601
|
-
});
|
|
602
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
603
|
-
expect(text).toContain("Set priority 2");
|
|
604
|
-
expect(text).toContain("1 of 1");
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
test("opsee_replace_task_labels with a set returns count", async () => {
|
|
608
|
-
const result = await client.callTool({
|
|
609
|
-
name: "opsee_replace_task_labels",
|
|
610
|
-
arguments: { taskId: 10, labelIds: [1, 2, 3] },
|
|
611
|
-
});
|
|
612
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
613
|
-
expect(text).toContain("Set 3 label(s)");
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
test("opsee_replace_task_labels with empty clears", async () => {
|
|
617
|
-
const result = await client.callTool({
|
|
618
|
-
name: "opsee_replace_task_labels",
|
|
619
|
-
arguments: { taskId: 10, labelIds: [] },
|
|
620
|
-
});
|
|
621
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
622
|
-
expect(text).toContain("Cleared all labels");
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
test("opsee_update_cycle returns the renamed cycle", async () => {
|
|
626
|
-
const result = await client.callTool({
|
|
627
|
-
name: "opsee_update_cycle",
|
|
628
|
-
arguments: { cycleId: 1, name: "Sprint 1 renamed" },
|
|
629
|
-
});
|
|
630
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
631
|
-
expect(text).toContain("Cycle updated");
|
|
632
|
-
expect(text).toContain("Sprint 1 renamed");
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
test("opsee_close_cycle closes and carries forward", async () => {
|
|
636
|
-
const result = await client.callTool({
|
|
637
|
-
name: "opsee_close_cycle",
|
|
638
|
-
arguments: { cycleId: 1, nextCycleId: 2 },
|
|
639
|
-
});
|
|
640
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
641
|
-
expect(text).toContain("closed");
|
|
642
|
-
// mockTask is returned by getTasks, so we should see a carry-forward count.
|
|
643
|
-
expect(text).toContain("Carried 1 task(s) forward to cycle 2");
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
test("opsee_close_cycle rejects mutually exclusive args", async () => {
|
|
647
|
-
const result = await client.callTool({
|
|
648
|
-
name: "opsee_close_cycle",
|
|
649
|
-
arguments: { cycleId: 1, nextCycleId: 2, createNextCycle: true },
|
|
650
|
-
});
|
|
651
|
-
expect(result.isError).toBe(true);
|
|
652
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
653
|
-
expect(text).toContain("mutually exclusive");
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
test("opsee_get_active_cycle returns the active one", async () => {
|
|
657
|
-
const result = await client.callTool({
|
|
658
|
-
name: "opsee_get_active_cycle",
|
|
659
|
-
arguments: { projectId: 1 },
|
|
660
|
-
});
|
|
661
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
662
|
-
expect(text).toContain("Sprint 1");
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
test("opsee_delete_cycle succeeds", async () => {
|
|
666
|
-
const result = await client.callTool({
|
|
667
|
-
name: "opsee_delete_cycle",
|
|
668
|
-
arguments: { cycleId: 1 },
|
|
669
|
-
});
|
|
670
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
671
|
-
expect(text).toContain("Cycle deleted");
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
// --- Track 1 defect fixes ---
|
|
675
|
-
|
|
676
|
-
test("opsee_delete_task succeeds", async () => {
|
|
677
|
-
const result = await client.callTool({
|
|
678
|
-
name: "opsee_delete_task",
|
|
679
|
-
arguments: { taskId: 10 },
|
|
680
|
-
});
|
|
681
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
682
|
-
expect(text).toContain("Task deleted");
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
test("opsee_bulk_delete_tasks reports counts", async () => {
|
|
686
|
-
const result = await client.callTool({
|
|
687
|
-
name: "opsee_bulk_delete_tasks",
|
|
688
|
-
arguments: { projectId: 1, taskIds: [10, 11] },
|
|
689
|
-
});
|
|
690
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
691
|
-
expect(text).toContain("Deleted 2 of 2");
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
test("opsee_edit_comment updates content", async () => {
|
|
695
|
-
const result = await client.callTool({
|
|
696
|
-
name: "opsee_edit_comment",
|
|
697
|
-
arguments: { commentId: 11, content: "edited text", isInternal: true },
|
|
698
|
-
});
|
|
699
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
700
|
-
expect(text).toContain("Comment updated");
|
|
701
|
-
expect(text).toContain("edited text");
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
test("opsee_get_task_with_context composes related data", async () => {
|
|
705
|
-
const result = await client.callTool({
|
|
706
|
-
name: "opsee_get_task_with_context",
|
|
707
|
-
arguments: { taskId: 10 },
|
|
708
|
-
});
|
|
709
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
710
|
-
expect(text).toContain("TP-1");
|
|
711
|
-
expect(text).toContain("--- Labels ---");
|
|
712
|
-
expect(text).toContain("--- Dependencies ---");
|
|
713
|
-
expect(text).toContain("--- Subtasks ---");
|
|
714
|
-
expect(text).toContain("--- Comments ---");
|
|
715
|
-
});
|
|
716
|
-
|
|
717
|
-
// --- Acceptance Criteria (§3.7) ---
|
|
718
|
-
|
|
719
|
-
test("opsee_list_task_acceptance_criteria lists items", async () => {
|
|
720
|
-
const result = await client.callTool({
|
|
721
|
-
name: "opsee_list_task_acceptance_criteria",
|
|
722
|
-
arguments: { taskId: 10 },
|
|
723
|
-
});
|
|
724
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
725
|
-
expect(text).toContain("endpoint returns 200");
|
|
726
|
-
expect(text).toContain("[ ]");
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
test("opsee_add_acceptance_criterion returns the new criterion", async () => {
|
|
730
|
-
const result = await client.callTool({
|
|
731
|
-
name: "opsee_add_acceptance_criterion",
|
|
732
|
-
arguments: { taskId: 10, text: "deploy to staging" },
|
|
733
|
-
});
|
|
734
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
735
|
-
expect(text).toContain("Criterion added");
|
|
736
|
-
expect(text).toContain("deploy to staging");
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
test("opsee_update_acceptance_criterion toggles done", async () => {
|
|
740
|
-
const result = await client.callTool({
|
|
741
|
-
name: "opsee_update_acceptance_criterion",
|
|
742
|
-
arguments: { criterionId: 1, done: true },
|
|
743
|
-
});
|
|
744
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
745
|
-
expect(text).toContain("Criterion updated");
|
|
746
|
-
expect(text).toContain("[x]");
|
|
747
|
-
});
|
|
748
|
-
|
|
749
|
-
test("opsee_delete_acceptance_criterion succeeds", async () => {
|
|
750
|
-
const result = await client.callTool({
|
|
751
|
-
name: "opsee_delete_acceptance_criterion",
|
|
752
|
-
arguments: { criterionId: 1 },
|
|
753
|
-
});
|
|
754
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
755
|
-
expect(text).toContain("Acceptance criterion deleted");
|
|
756
|
-
});
|
|
757
|
-
|
|
758
|
-
// --- Notifications (§15) ---
|
|
759
|
-
|
|
760
|
-
test("opsee_list_my_notifications returns notifications", async () => {
|
|
761
|
-
const result = await client.callTool({
|
|
762
|
-
name: "opsee_list_my_notifications",
|
|
763
|
-
arguments: {},
|
|
764
|
-
});
|
|
765
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
766
|
-
expect(text).toContain("notification(s)");
|
|
767
|
-
expect(text).toContain("task_assigned");
|
|
768
|
-
expect(text).toContain("unread");
|
|
769
|
-
});
|
|
770
|
-
|
|
771
|
-
test("opsee_list_my_notifications unreadOnly returns unread header", async () => {
|
|
772
|
-
const result = await client.callTool({
|
|
773
|
-
name: "opsee_list_my_notifications",
|
|
774
|
-
arguments: { unreadOnly: true },
|
|
775
|
-
});
|
|
776
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
777
|
-
expect(text).toContain("unread notification");
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
test("opsee_mark_notification_read flips is_read", async () => {
|
|
781
|
-
const result = await client.callTool({
|
|
782
|
-
name: "opsee_mark_notification_read",
|
|
783
|
-
arguments: { notificationId: 1 },
|
|
784
|
-
});
|
|
785
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
786
|
-
expect(text).toContain("marked read");
|
|
787
|
-
});
|
|
788
|
-
|
|
789
|
-
test("opsee_mark_all_notifications_read summarizes", async () => {
|
|
790
|
-
const result = await client.callTool({
|
|
791
|
-
name: "opsee_mark_all_notifications_read",
|
|
792
|
-
arguments: {},
|
|
793
|
-
});
|
|
794
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
795
|
-
expect(text).toContain("Marked 1 of 1");
|
|
796
|
-
});
|
|
797
|
-
|
|
798
|
-
test("opsee_get_dependency_chain returns nodes and edges summary", async () => {
|
|
799
|
-
const result = await client.callTool({
|
|
800
|
-
name: "opsee_get_dependency_chain",
|
|
801
|
-
arguments: { taskId: 10, direction: "both", depth: 3 },
|
|
802
|
-
});
|
|
803
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
804
|
-
expect(text).toContain("Dependency chain from task 10");
|
|
805
|
-
expect(text).toContain("1 task(s), 0 edge(s)");
|
|
806
|
-
expect(text).toContain("[TP-1]");
|
|
807
|
-
});
|
|
808
|
-
|
|
809
|
-
// --- WorkLog tools (§12) ---
|
|
810
|
-
|
|
811
|
-
test("opsee_log_work logs hours", async () => {
|
|
812
|
-
const result = await client.callTool({
|
|
813
|
-
name: "opsee_log_work",
|
|
814
|
-
arguments: { taskId: 10, hours: 2.5, description: "implemented foo" },
|
|
815
|
-
});
|
|
816
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
817
|
-
expect(text).toContain("Logged 2.5h on task 10");
|
|
818
|
-
});
|
|
819
|
-
|
|
820
|
-
test("opsee_delete_work_log succeeds", async () => {
|
|
821
|
-
const result = await client.callTool({
|
|
822
|
-
name: "opsee_delete_work_log",
|
|
823
|
-
arguments: { taskId: 10, workLogId: 42 },
|
|
824
|
-
});
|
|
825
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
826
|
-
expect(text).toContain("Work log deleted");
|
|
827
|
-
});
|
|
828
|
-
|
|
829
|
-
test("opsee_list_work_logs reports empty for tasks without logs", async () => {
|
|
830
|
-
const result = await client.callTool({
|
|
831
|
-
name: "opsee_list_work_logs",
|
|
832
|
-
arguments: { taskId: 10 },
|
|
833
|
-
});
|
|
834
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
835
|
-
expect(text).toContain("No work logs");
|
|
836
|
-
});
|
|
837
|
-
|
|
838
|
-
test("opsee_get_task_actual_hours sums to 0 with no logs", async () => {
|
|
839
|
-
const result = await client.callTool({
|
|
840
|
-
name: "opsee_get_task_actual_hours",
|
|
841
|
-
arguments: { taskId: 10 },
|
|
842
|
-
});
|
|
843
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
844
|
-
expect(text).toContain("TP-1");
|
|
845
|
-
expect(text).toContain("0h logged");
|
|
846
|
-
});
|
|
847
|
-
|
|
848
|
-
test("formatTask surfaces storyPoints and estimatedHours", async () => {
|
|
849
|
-
// mockTask has storyPoints: 3 — confirms D4 fix.
|
|
850
|
-
const result = await client.callTool({
|
|
851
|
-
name: "opsee_get_task",
|
|
852
|
-
arguments: { taskId: 10 },
|
|
853
|
-
});
|
|
854
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
855
|
-
expect(text).toContain("Story Points: 3");
|
|
856
|
-
});
|
|
857
|
-
|
|
858
|
-
test("opsee_get_task returns full description while lists truncate", async () => {
|
|
859
|
-
const longDesc = "x".repeat(250);
|
|
860
|
-
const preview = "x".repeat(200) + "...";
|
|
861
|
-
const clients = createMockClients();
|
|
862
|
-
(clients.tasks as any).getTask = async () => ({ task: { ...mockTask, description: longDesc } });
|
|
863
|
-
(clients.tasks as any).getTasks = async () => ({ tasks: [{ ...mockTask, description: longDesc }] });
|
|
864
|
-
|
|
865
|
-
const [ct, st] = InMemoryTransport.createLinkedPair();
|
|
866
|
-
const srv = createServer(() => clients);
|
|
867
|
-
await srv.connect(st);
|
|
868
|
-
const c = new Client({ name: "full-desc-test", version: "1.0.0" });
|
|
869
|
-
await c.connect(ct);
|
|
870
|
-
|
|
871
|
-
const full = await c.callTool({ name: "opsee_get_task", arguments: { taskId: 10 } });
|
|
872
|
-
const fullText = (full.content as Array<{ type: string; text: string }>)[0].text;
|
|
873
|
-
expect(fullText).toContain(longDesc);
|
|
874
|
-
expect(fullText).not.toContain(preview);
|
|
875
|
-
|
|
876
|
-
const list = await c.callTool({ name: "opsee_list_tasks", arguments: { projectId: 1 } });
|
|
877
|
-
const listText = (list.content as Array<{ type: string; text: string }>)[0].text;
|
|
878
|
-
expect(listText).toContain(preview);
|
|
879
|
-
|
|
880
|
-
await c.close();
|
|
881
|
-
await srv.close();
|
|
882
|
-
});
|
|
883
|
-
|
|
884
|
-
test("auth error returns login instruction", async () => {
|
|
885
|
-
const authClients = createMockClients();
|
|
886
|
-
// The connect client surfaces backend auth failures as a ConnectError with
|
|
887
|
-
// Code.Unauthenticated (not a plain Error), which is what formatError keys on.
|
|
888
|
-
(authClients.users as any).getMe = async () => {
|
|
889
|
-
throw new ConnectError("unauthenticated", Code.Unauthenticated);
|
|
890
|
-
};
|
|
891
|
-
|
|
892
|
-
const [ct, st] = InMemoryTransport.createLinkedPair();
|
|
893
|
-
const authServer = createServer(() => authClients);
|
|
894
|
-
await authServer.connect(st);
|
|
895
|
-
|
|
896
|
-
const authClient = new Client({ name: "auth-test", version: "1.0.0" });
|
|
897
|
-
await authClient.connect(ct);
|
|
898
|
-
|
|
899
|
-
const result = await authClient.callTool({
|
|
900
|
-
name: "opsee_get_me",
|
|
901
|
-
arguments: {},
|
|
902
|
-
});
|
|
903
|
-
|
|
904
|
-
expect(result.isError).toBe(true);
|
|
905
|
-
const text = (result.content as Array<{ type: string; text: string }>)[0]
|
|
906
|
-
.text;
|
|
907
|
-
expect(text).toContain("Not authenticated");
|
|
908
|
-
expect(text).toContain("login");
|
|
909
|
-
|
|
910
|
-
await authClient.close();
|
|
911
|
-
await authServer.close();
|
|
912
|
-
});
|
|
913
|
-
});
|