@access-mcp/announcements 0.1.0 → 0.3.0

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.
@@ -1,9 +1,35 @@
1
- import { describe, it, expect, beforeEach, vi } from "vitest";
1
+ import { describe, it, expect, beforeEach, afterEach, vi, Mock } from "vitest";
2
2
  import { AnnouncementsServer } from "./server.js";
3
+ import { DrupalAuthProvider } from "@access-mcp/shared";
4
+
5
+ // Mock the DrupalAuthProvider
6
+ vi.mock("@access-mcp/shared", async () => {
7
+ const actual = await vi.importActual("@access-mcp/shared");
8
+ return {
9
+ ...actual,
10
+ DrupalAuthProvider: vi.fn().mockImplementation(() => ({
11
+ ensureAuthenticated: vi.fn().mockResolvedValue(undefined),
12
+ getUserUuid: vi.fn().mockReturnValue("user-uuid-123"),
13
+ get: vi.fn(),
14
+ post: vi.fn(),
15
+ patch: vi.fn(),
16
+ delete: vi.fn(),
17
+ })),
18
+ };
19
+ });
20
+
21
+ interface MockHttpClient {
22
+ get: Mock<(url: string) => Promise<{ status: number; data?: unknown; statusText?: string }>>;
23
+ }
24
+
25
+ interface TextContent {
26
+ type: "text";
27
+ text: string;
28
+ }
3
29
 
4
30
  describe("AnnouncementsServer", () => {
5
31
  let server: AnnouncementsServer;
6
- let mockHttpClient: any;
32
+ let mockHttpClient: MockHttpClient;
7
33
 
8
34
  beforeEach(() => {
9
35
  server = new AnnouncementsServer();
@@ -21,7 +47,7 @@ describe("AnnouncementsServer", () => {
21
47
  });
22
48
 
23
49
  describe("Tool Methods", () => {
24
- describe("get_announcements", () => {
50
+ describe("search_announcements", () => {
25
51
  it("should fetch announcements with filters", async () => {
26
52
  const mockResponse = {
27
53
  status: 200,
@@ -29,18 +55,18 @@ describe("AnnouncementsServer", () => {
29
55
  {
30
56
  title: "Scheduled Maintenance",
31
57
  body: "System will be down for maintenance",
32
- field_published_date: "2024-03-15",
58
+ published_date: "2024-03-15",
33
59
  author: "ACCESS Support",
34
- custom_announcement_tags: "maintenance, scheduled",
35
- custom_announcement_ag: "123,456",
60
+ tags: ["maintenance", "scheduled"],
61
+ affinity_group: ["123", "456"],
36
62
  },
37
63
  {
38
64
  title: "New GPU Nodes Available",
39
65
  body: "Additional GPU resources added",
40
- field_published_date: "2024-03-10",
66
+ published_date: "2024-03-10",
41
67
  author: "Resource Team",
42
- custom_announcement_tags: "gpu, hardware",
43
- custom_announcement_ag: "789",
68
+ tags: ["gpu", "hardware"],
69
+ affinity_group: ["789"],
44
70
  },
45
71
  ],
46
72
  };
@@ -48,24 +74,25 @@ describe("AnnouncementsServer", () => {
48
74
  mockHttpClient.get.mockResolvedValue(mockResponse);
49
75
 
50
76
  const result = await server["handleToolCall"]({
77
+ method: "tools/call",
51
78
  params: {
52
- name: "get_announcements",
79
+ name: "search_announcements",
53
80
  arguments: {
54
81
  tags: "maintenance",
55
82
  limit: 10,
56
83
  },
57
84
  },
58
- } as any);
85
+ });
59
86
 
60
87
  expect(mockHttpClient.get).toHaveBeenCalled();
61
88
  const url = mockHttpClient.get.mock.calls[0][0];
62
- expect(url).toContain("/api/2.1/announcements");
89
+ expect(url).toContain("/api/2.2/announcements");
63
90
  expect(url).toContain("tags=maintenance");
64
91
 
65
- const responseData = JSON.parse(result.content[0].text);
66
- expect(responseData.total_announcements).toBe(2);
67
- expect(responseData.announcements).toHaveLength(2);
68
- expect(responseData.announcements[0].tags).toEqual(["maintenance", "scheduled"]);
92
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
93
+ expect(responseData.total).toBe(2);
94
+ expect(responseData.items).toHaveLength(2);
95
+ expect(responseData.items[0].tags).toEqual(["maintenance", "scheduled"]);
69
96
  });
70
97
 
71
98
  it("should handle empty results", async () => {
@@ -75,17 +102,18 @@ describe("AnnouncementsServer", () => {
75
102
  });
76
103
 
77
104
  const result = await server["handleToolCall"]({
105
+ method: "tools/call",
78
106
  params: {
79
- name: "get_announcements",
107
+ name: "search_announcements",
80
108
  arguments: {
81
109
  tags: "nonexistent",
82
110
  },
83
111
  },
84
- } as any);
112
+ });
85
113
 
86
- const responseData = JSON.parse(result.content[0].text);
87
- expect(responseData.total_announcements).toBe(0);
88
- expect(responseData.announcements).toEqual([]);
114
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
115
+ expect(responseData.total).toBe(0);
116
+ expect(responseData.items).toEqual([]);
89
117
  });
90
118
 
91
119
  it("should handle API errors", async () => {
@@ -95,18 +123,65 @@ describe("AnnouncementsServer", () => {
95
123
  });
96
124
 
97
125
  const result = await server["handleToolCall"]({
126
+ method: "tools/call",
98
127
  params: {
99
- name: "get_announcements",
128
+ name: "search_announcements",
100
129
  arguments: {},
101
130
  },
102
- } as any);
131
+ });
103
132
 
104
133
  // Server handles errors and returns them in content, not as isError
105
- expect(result.content[0].text).toContain("500");
134
+ expect((result.content[0] as TextContent).text).toContain("500");
135
+ });
136
+ });
137
+
138
+ describe("search with query parameter", () => {
139
+ it("should include search_api_fulltext in URL", async () => {
140
+ mockHttpClient.get.mockResolvedValue({
141
+ status: 200,
142
+ data: [],
143
+ });
144
+
145
+ await server["handleToolCall"]({
146
+ method: "tools/call",
147
+ params: {
148
+ name: "search_announcements",
149
+ arguments: {
150
+ query: "GPU computing",
151
+ },
152
+ },
153
+ });
154
+
155
+ const url = mockHttpClient.get.mock.calls[0][0];
156
+ expect(url).toContain("search_api_fulltext=GPU+computing");
157
+ });
158
+
159
+ it("should combine query with other filters", async () => {
160
+ mockHttpClient.get.mockResolvedValue({
161
+ status: 200,
162
+ data: [],
163
+ });
164
+
165
+ await server["handleToolCall"]({
166
+ method: "tools/call",
167
+ params: {
168
+ name: "search_announcements",
169
+ arguments: {
170
+ query: "workshop",
171
+ tags: "training",
172
+ date: "this_month",
173
+ },
174
+ },
175
+ });
176
+
177
+ const url = mockHttpClient.get.mock.calls[0][0];
178
+ expect(url).toContain("search_api_fulltext=workshop");
179
+ expect(url).toContain("tags=training");
180
+ expect(url).toContain("relative_start_date=-1+month");
106
181
  });
107
182
  });
108
183
 
109
- describe("get_announcements_by_tags", () => {
184
+ describe("search by tags", () => {
110
185
  it("should fetch announcements by specific tags", async () => {
111
186
  const mockResponse = {
112
187
  status: 200,
@@ -114,10 +189,10 @@ describe("AnnouncementsServer", () => {
114
189
  {
115
190
  title: "GPU Maintenance",
116
191
  body: "GPU nodes maintenance",
117
- field_published_date: "2024-03-15",
192
+ published_date: "2024-03-15",
118
193
  author: "Support",
119
- custom_announcement_tags: "gpu, maintenance",
120
- custom_announcement_ag: "",
194
+ tags: ["gpu", "maintenance"],
195
+ affinity_group: [],
121
196
  },
122
197
  ],
123
198
  };
@@ -125,37 +200,36 @@ describe("AnnouncementsServer", () => {
125
200
  mockHttpClient.get.mockResolvedValue(mockResponse);
126
201
 
127
202
  const result = await server["handleToolCall"]({
203
+ method: "tools/call",
128
204
  params: {
129
- name: "get_announcements_by_tags",
205
+ name: "search_announcements",
130
206
  arguments: {
131
207
  tags: "gpu,maintenance",
132
208
  limit: 20,
133
209
  },
134
210
  },
135
- } as any);
211
+ });
136
212
 
137
213
  const url = mockHttpClient.get.mock.calls[0][0];
138
214
  expect(url).toContain("tags=gpu%2Cmaintenance");
139
- // Note: exact_match is not implemented in the server
140
215
 
141
- const responseData = JSON.parse(result.content[0].text);
142
- expect(responseData.announcements[0].tags).toContain("gpu");
143
- expect(responseData.announcements[0].tags).toContain("maintenance");
216
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
217
+ expect(responseData.items[0].tags).toContain("gpu");
218
+ expect(responseData.items[0].tags).toContain("maintenance");
144
219
  });
145
220
  });
146
221
 
147
- describe("get_announcements_by_affinity_group", () => {
148
- it("should fetch announcements for affinity group", async () => {
222
+ describe("search with limit", () => {
223
+ it("should respect limit parameter", async () => {
149
224
  const mockResponse = {
150
225
  status: 200,
151
226
  data: [
152
227
  {
153
- title: "AI/ML Community Update",
154
- body: "New resources for AI/ML",
155
- field_published_date: "2024-03-14",
156
- author: "Community Team",
157
- custom_announcement_tags: "ai, ml, community",
158
- custom_announcement_ag: "ai-ml-123",
228
+ title: "Update 1",
229
+ body: "Body 1",
230
+ published_date: "2024-03-14",
231
+ tags: ["ai"],
232
+ affinity_group: [],
159
233
  },
160
234
  ],
161
235
  };
@@ -163,37 +237,35 @@ describe("AnnouncementsServer", () => {
163
237
  mockHttpClient.get.mockResolvedValue(mockResponse);
164
238
 
165
239
  const result = await server["handleToolCall"]({
240
+ method: "tools/call",
166
241
  params: {
167
- name: "get_announcements_by_affinity_group",
242
+ name: "search_announcements",
168
243
  arguments: {
169
- ag: "ai-ml-123",
170
244
  limit: 5,
171
245
  },
172
246
  },
173
- } as any);
247
+ });
174
248
 
175
249
  const url = mockHttpClient.get.mock.calls[0][0];
176
- expect(url).toContain("ag=ai-ml-123");
177
- // Note: limit is handled in JavaScript, not in URL
250
+ expect(url).toContain("items_per_page=5");
178
251
 
179
- const responseData = JSON.parse(result.content[0].text);
180
- expect(responseData.announcements).toHaveLength(1);
181
- expect(responseData.announcements[0].affinity_groups).toContain("ai-ml-123");
252
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
253
+ expect(responseData.items).toHaveLength(1);
182
254
  });
183
255
  });
184
256
 
185
- describe("get_recent_announcements", () => {
186
- it("should fetch recent announcements", async () => {
257
+ describe("recent announcements with date filters", () => {
258
+ it("should fetch announcements with date filter", async () => {
187
259
  const mockResponse = {
188
260
  status: 200,
189
261
  data: [
190
262
  {
191
263
  title: "Today's Update",
192
264
  body: "Important update",
193
- date: new Date().toISOString(),
265
+ published_date: new Date().toISOString(),
194
266
  author: "Admin",
195
- custom_support_tags: "urgent",
196
- custom_affinity_groups: "",
267
+ tags: ["urgent"],
268
+ affinity_group: [],
197
269
  },
198
270
  ],
199
271
  };
@@ -201,36 +273,39 @@ describe("AnnouncementsServer", () => {
201
273
  mockHttpClient.get.mockResolvedValue(mockResponse);
202
274
 
203
275
  const result = await server["handleToolCall"]({
276
+ method: "tools/call",
204
277
  params: {
205
- name: "get_recent_announcements",
278
+ name: "search_announcements",
206
279
  arguments: {
207
- period: "1 week",
280
+ date: "this_week",
208
281
  },
209
282
  },
210
- } as any);
283
+ });
211
284
 
212
285
  const url = mockHttpClient.get.mock.calls[0][0];
213
286
  expect(url).toContain("relative_start_date=-1+week");
214
287
 
215
- const responseData = JSON.parse(result.content[0].text);
216
- expect(responseData.announcements).toHaveLength(1);
288
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
289
+ expect(responseData.items).toHaveLength(1);
217
290
  });
218
291
 
219
- it("should default to past week if no time period specified", async () => {
292
+ it("should handle search with no date filter", async () => {
220
293
  mockHttpClient.get.mockResolvedValue({
221
294
  status: 200,
222
295
  data: [],
223
296
  });
224
297
 
225
298
  await server["handleToolCall"]({
299
+ method: "tools/call",
226
300
  params: {
227
- name: "get_recent_announcements",
301
+ name: "search_announcements",
228
302
  arguments: {},
229
303
  },
230
- } as any);
304
+ });
231
305
 
306
+ expect(mockHttpClient.get).toHaveBeenCalled();
232
307
  const url = mockHttpClient.get.mock.calls[0][0];
233
- expect(url).toContain("relative_start_date=-1+month");
308
+ expect(url).toContain("/api/2.2/announcements");
234
309
  });
235
310
  });
236
311
  });
@@ -243,58 +318,53 @@ describe("AnnouncementsServer", () => {
243
318
  });
244
319
 
245
320
  await server["handleToolCall"]({
321
+ method: "tools/call",
246
322
  params: {
247
- name: "get_announcements",
323
+ name: "search_announcements",
248
324
  arguments: {
249
325
  tags: "gpu,maintenance",
250
- ag: "123,456",
251
- start_date: "2024-01-01",
252
- end_date: "2024-12-31",
326
+ date: "this_month",
253
327
  limit: 20,
254
328
  },
255
329
  },
256
- } as any);
330
+ });
257
331
 
258
332
  const url = mockHttpClient.get.mock.calls[0][0];
259
333
  expect(url).toContain("tags=gpu%2Cmaintenance");
260
- expect(url).toContain("ag=123%2C456");
261
- expect(url).toContain("start_date=2024-01-01");
262
- expect(url).toContain("end_date=2024-12-31");
263
- // Note: exact_match and limit are not URL parameters
334
+ expect(url).toContain("relative_start_date=-1+month");
264
335
  });
265
336
 
266
- it("should handle relative date filters", async () => {
337
+ it("should handle date filters", async () => {
267
338
  mockHttpClient.get.mockResolvedValue({
268
339
  status: 200,
269
340
  data: [],
270
341
  });
271
342
 
272
343
  await server["handleToolCall"]({
344
+ method: "tools/call",
273
345
  params: {
274
- name: "get_announcements",
346
+ name: "search_announcements",
275
347
  arguments: {
276
- relative_start_date: "today",
277
- relative_end_date: "+1month",
348
+ date: "today",
278
349
  },
279
350
  },
280
- } as any);
351
+ });
281
352
 
282
353
  const url = mockHttpClient.get.mock.calls[0][0];
283
354
  expect(url).toContain("relative_start_date=today");
284
- expect(url).toContain("relative_end_date=%2B1month");
285
355
  });
286
356
  });
287
357
 
288
358
  describe("Data Enhancement", () => {
289
- it("should parse tags correctly", async () => {
359
+ it("should parse tags array correctly", async () => {
290
360
  const mockResponse = {
291
361
  status: 200,
292
362
  data: [
293
363
  {
294
364
  title: "Test",
295
- field_published_date: "2024-03-15",
296
- custom_announcement_tags: "tag1, tag2, tag3 ",
297
- custom_announcement_ag: "",
365
+ published_date: "2024-03-15",
366
+ tags: ["tag1", "tag2", "tag3"],
367
+ affinity_group: [],
298
368
  },
299
369
  ],
300
370
  };
@@ -302,50 +372,106 @@ describe("AnnouncementsServer", () => {
302
372
  mockHttpClient.get.mockResolvedValue(mockResponse);
303
373
 
304
374
  const result = await server["handleToolCall"]({
375
+ method: "tools/call",
305
376
  params: {
306
- name: "get_announcements",
377
+ name: "search_announcements",
307
378
  arguments: {},
308
379
  },
309
- } as any);
380
+ });
310
381
 
311
- const responseData = JSON.parse(result.content[0].text);
312
- expect(responseData.announcements[0].tags).toEqual(["tag1", "tag2", "tag3"]);
382
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
383
+ expect(responseData.items[0].tags).toEqual(["tag1", "tag2", "tag3"]);
384
+ });
385
+
386
+ it("should parse tags from comma-separated string", async () => {
387
+ const mockResponse = {
388
+ status: 200,
389
+ data: [
390
+ {
391
+ title: "Test",
392
+ published_date: "2024-03-15",
393
+ tags: "gpu, machine-learning, hpc",
394
+ affinity_group: [],
395
+ },
396
+ ],
397
+ };
398
+
399
+ mockHttpClient.get.mockResolvedValue(mockResponse);
400
+
401
+ const result = await server["handleToolCall"]({
402
+ method: "tools/call",
403
+ params: {
404
+ name: "search_announcements",
405
+ arguments: {},
406
+ },
407
+ });
408
+
409
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
410
+ expect(responseData.items[0].tags).toEqual(["gpu", "machine-learning", "hpc"]);
411
+ });
412
+
413
+ it("should handle empty tags string", async () => {
414
+ const mockResponse = {
415
+ status: 200,
416
+ data: [
417
+ {
418
+ title: "Test",
419
+ published_date: "2024-03-15",
420
+ tags: "",
421
+ affinity_group: [],
422
+ },
423
+ ],
424
+ };
425
+
426
+ mockHttpClient.get.mockResolvedValue(mockResponse);
427
+
428
+ const result = await server["handleToolCall"]({
429
+ method: "tools/call",
430
+ params: {
431
+ name: "search_announcements",
432
+ arguments: {},
433
+ },
434
+ });
435
+
436
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
437
+ expect(responseData.items[0].tags).toEqual([]);
313
438
  });
314
439
 
315
440
  it("should extract popular tags", async () => {
316
441
  const mockResponse = {
317
442
  status: 200,
318
443
  data: [
319
- { title: "1", field_published_date: "2024-03-15", custom_announcement_tags: "gpu, maintenance", custom_announcement_ag: "" },
320
- { title: "2", field_published_date: "2024-03-14", custom_announcement_tags: "gpu, network", custom_announcement_ag: "" },
321
- { title: "3", field_published_date: "2024-03-13", custom_announcement_tags: "gpu, storage", custom_announcement_ag: "" },
322
- { title: "4", field_published_date: "2024-03-12", custom_announcement_tags: "maintenance", custom_announcement_ag: "" },
444
+ { title: "1", published_date: "2024-03-15", tags: ["gpu", "maintenance"], affinity_group: [] },
445
+ { title: "2", published_date: "2024-03-14", tags: ["gpu", "network"], affinity_group: [] },
446
+ { title: "3", published_date: "2024-03-13", tags: ["gpu", "storage"], affinity_group: [] },
447
+ { title: "4", published_date: "2024-03-12", tags: ["maintenance"], affinity_group: [] },
323
448
  ],
324
449
  };
325
450
 
326
451
  mockHttpClient.get.mockResolvedValue(mockResponse);
327
452
 
328
453
  const result = await server["handleToolCall"]({
454
+ method: "tools/call",
329
455
  params: {
330
- name: "get_announcements",
456
+ name: "search_announcements",
331
457
  arguments: {},
332
458
  },
333
- } as any);
459
+ });
334
460
 
335
- const responseData = JSON.parse(result.content[0].text);
336
- expect(responseData.popular_tags).toContain("gpu");
337
- expect(responseData.popular_tags).toContain("maintenance");
461
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
462
+ // Popular tags are in metadata, not in the universal {total, items} format
463
+ expect(responseData.items).toHaveLength(4);
338
464
  });
339
465
 
340
- it("should format dates correctly", async () => {
466
+ it("should include published_date in items", async () => {
341
467
  const mockResponse = {
342
468
  status: 200,
343
469
  data: [
344
470
  {
345
471
  title: "Test",
346
- field_published_date: "2024-03-15",
347
- custom_announcement_tags: "",
348
- custom_announcement_ag: "",
472
+ published_date: "2024-03-15",
473
+ tags: [],
474
+ affinity_group: [],
349
475
  },
350
476
  ],
351
477
  };
@@ -353,15 +479,816 @@ describe("AnnouncementsServer", () => {
353
479
  mockHttpClient.get.mockResolvedValue(mockResponse);
354
480
 
355
481
  const result = await server["handleToolCall"]({
482
+ method: "tools/call",
483
+ params: {
484
+ name: "search_announcements",
485
+ arguments: {},
486
+ },
487
+ });
488
+
489
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
490
+ expect(responseData.items[0].published_date).toBe("2024-03-15");
491
+ });
492
+ });
493
+
494
+ describe("CRUD Operations", () => {
495
+ let mockDrupalAuth: {
496
+ ensureAuthenticated: Mock;
497
+ getUserUuid: Mock;
498
+ get: Mock;
499
+ post: Mock;
500
+ patch: Mock;
501
+ delete: Mock;
502
+ };
503
+
504
+ beforeEach(() => {
505
+ // Set up environment variables for CRUD operations
506
+ process.env.DRUPAL_API_URL = "https://test.drupal.site";
507
+ process.env.DRUPAL_USERNAME = "test_user";
508
+ process.env.DRUPAL_PASSWORD = "test_password";
509
+ process.env.ACTING_USER_UID = "1985";
510
+
511
+ // Create a fresh mock for each test
512
+ mockDrupalAuth = {
513
+ ensureAuthenticated: vi.fn().mockResolvedValue(undefined),
514
+ getUserUuid: vi.fn().mockReturnValue("user-uuid-123"),
515
+ get: vi.fn(),
516
+ post: vi.fn(),
517
+ patch: vi.fn(),
518
+ delete: vi.fn(),
519
+ };
520
+
521
+ // Mock the DrupalAuthProvider constructor to return our mock
522
+ (DrupalAuthProvider as unknown as Mock).mockImplementation(() => mockDrupalAuth);
523
+ });
524
+
525
+ afterEach(() => {
526
+ delete process.env.DRUPAL_API_URL;
527
+ delete process.env.DRUPAL_USERNAME;
528
+ delete process.env.DRUPAL_PASSWORD;
529
+ delete process.env.ACTING_USER_UID;
530
+ });
531
+
532
+ describe("create_announcement", () => {
533
+ it("should create an announcement with required fields", async () => {
534
+ // Mock user UUID lookup for ACTING_USER_UID
535
+ mockDrupalAuth.get.mockResolvedValueOnce({
536
+ data: [{ id: "acting-user-uuid-123" }],
537
+ });
538
+
539
+ mockDrupalAuth.post.mockResolvedValue({
540
+ data: {
541
+ id: "new-announcement-uuid",
542
+ attributes: {
543
+ title: "Test Announcement",
544
+ drupal_internal__nid: 12345,
545
+ },
546
+ },
547
+ });
548
+
549
+ const result = await server["handleToolCall"]({
550
+ method: "tools/call",
551
+ params: {
552
+ name: "create_announcement",
553
+ arguments: {
554
+ title: "Test Announcement",
555
+ body: "<p>This is a test</p>",
556
+ },
557
+ },
558
+ });
559
+
560
+ // Should have looked up the acting user UUID
561
+ expect(mockDrupalAuth.get).toHaveBeenCalledWith(
562
+ "/jsonapi/user/user?filter[drupal_internal__uid]=1985"
563
+ );
564
+
565
+ expect(mockDrupalAuth.post).toHaveBeenCalledWith(
566
+ "/jsonapi/node/access_news",
567
+ expect.objectContaining({
568
+ data: expect.objectContaining({
569
+ type: "node--access_news",
570
+ attributes: expect.objectContaining({
571
+ title: "Test Announcement",
572
+ moderation_state: "draft",
573
+ body: expect.objectContaining({
574
+ value: "<p>This is a test</p>",
575
+ format: "basic_html",
576
+ }),
577
+ }),
578
+ relationships: expect.objectContaining({
579
+ uid: {
580
+ data: {
581
+ type: "user--user",
582
+ id: "acting-user-uuid-123",
583
+ },
584
+ },
585
+ }),
586
+ }),
587
+ })
588
+ );
589
+
590
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
591
+ expect(responseData.success).toBe(true);
592
+ expect(responseData.uuid).toBe("new-announcement-uuid");
593
+ });
594
+
595
+ it("should look up tags by name when provided (with caching)", async () => {
596
+ // Mock user lookup first, then bulk tag fetch for cache
597
+ mockDrupalAuth.get
598
+ .mockResolvedValueOnce({ data: [{ id: "acting-user-uuid" }] }) // user lookup
599
+ .mockResolvedValueOnce({
600
+ data: [
601
+ { id: "tag-uuid-1", attributes: { name: "gpu" } },
602
+ { id: "tag-uuid-2", attributes: { name: "maintenance" } },
603
+ { id: "tag-uuid-3", attributes: { name: "hpc" } },
604
+ ]
605
+ }); // bulk tag cache fetch
606
+
607
+ mockDrupalAuth.post.mockResolvedValue({
608
+ data: {
609
+ id: "new-announcement-uuid",
610
+ attributes: { title: "Test" },
611
+ },
612
+ });
613
+
614
+ await server["handleToolCall"]({
615
+ method: "tools/call",
616
+ params: {
617
+ name: "create_announcement",
618
+ arguments: {
619
+ title: "Test",
620
+ body: "Body",
621
+ tags: ["gpu", "maintenance"],
622
+ },
623
+ },
624
+ });
625
+
626
+ // Should have looked up user, then fetched all tags for cache
627
+ expect(mockDrupalAuth.get).toHaveBeenCalledWith(
628
+ "/jsonapi/user/user?filter[drupal_internal__uid]=1985"
629
+ );
630
+ expect(mockDrupalAuth.get).toHaveBeenCalledWith(
631
+ "/jsonapi/taxonomy_term/tags?page[limit]=500"
632
+ );
633
+
634
+ // Should have created with the correct tag UUIDs
635
+ expect(mockDrupalAuth.post).toHaveBeenCalledWith(
636
+ "/jsonapi/node/access_news",
637
+ expect.objectContaining({
638
+ data: expect.objectContaining({
639
+ relationships: expect.objectContaining({
640
+ field_tags: {
641
+ data: [
642
+ { type: "taxonomy_term--tags", id: "tag-uuid-1" },
643
+ { type: "taxonomy_term--tags", id: "tag-uuid-2" },
644
+ ],
645
+ },
646
+ }),
647
+ }),
648
+ })
649
+ );
650
+ });
651
+
652
+ it("should fail without credentials", async () => {
653
+ delete process.env.DRUPAL_API_URL;
654
+
655
+ const result = await server["handleToolCall"]({
656
+ method: "tools/call",
657
+ params: {
658
+ name: "create_announcement",
659
+ arguments: {
660
+ title: "Test",
661
+ body: "Body",
662
+ },
663
+ },
664
+ });
665
+
666
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
667
+ expect(responseData.error).toContain("DRUPAL_API_URL");
668
+ });
669
+
670
+ it("should fail without ACTING_USER_UID", async () => {
671
+ delete process.env.ACTING_USER_UID;
672
+
673
+ const result = await server["handleToolCall"]({
674
+ method: "tools/call",
675
+ params: {
676
+ name: "create_announcement",
677
+ arguments: {
678
+ title: "Test",
679
+ body: "Body",
680
+ },
681
+ },
682
+ });
683
+
684
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
685
+ expect(responseData.error).toContain("ACTING_USER_UID");
686
+ expect(responseData.error).toContain("No acting user specified");
687
+ });
688
+
689
+ it("should create announcement with external link", async () => {
690
+ mockDrupalAuth.get.mockResolvedValueOnce({
691
+ data: [{ id: "acting-user-uuid-123" }],
692
+ });
693
+
694
+ mockDrupalAuth.post.mockResolvedValue({
695
+ data: {
696
+ id: "new-announcement-uuid",
697
+ attributes: {
698
+ title: "Test with Link",
699
+ drupal_internal__nid: 12345,
700
+ },
701
+ },
702
+ });
703
+
704
+ await server["handleToolCall"]({
705
+ method: "tools/call",
706
+ params: {
707
+ name: "create_announcement",
708
+ arguments: {
709
+ title: "Test with Link",
710
+ body: "<p>Body content</p>",
711
+ summary: "Test summary",
712
+ external_link: {
713
+ uri: "https://example.com/resource",
714
+ title: "Learn more",
715
+ },
716
+ },
717
+ },
718
+ });
719
+
720
+ expect(mockDrupalAuth.post).toHaveBeenCalledWith(
721
+ "/jsonapi/node/access_news",
722
+ expect.objectContaining({
723
+ data: expect.objectContaining({
724
+ attributes: expect.objectContaining({
725
+ field_news_external_link: {
726
+ uri: "https://example.com/resource",
727
+ title: "Learn more",
728
+ },
729
+ }),
730
+ }),
731
+ })
732
+ );
733
+ });
734
+
735
+ it("should create announcement with where_to_share", async () => {
736
+ mockDrupalAuth.get.mockResolvedValueOnce({
737
+ data: [{ id: "acting-user-uuid-123" }],
738
+ });
739
+
740
+ mockDrupalAuth.post.mockResolvedValue({
741
+ data: {
742
+ id: "new-announcement-uuid",
743
+ attributes: {
744
+ title: "Test with sharing",
745
+ drupal_internal__nid: 12345,
746
+ },
747
+ },
748
+ });
749
+
750
+ await server["handleToolCall"]({
751
+ method: "tools/call",
752
+ params: {
753
+ name: "create_announcement",
754
+ arguments: {
755
+ title: "Test with sharing",
756
+ body: "<p>Body content</p>",
757
+ summary: "Test summary",
758
+ where_to_share: ["Announcements page", "Bi-Weekly Digest"],
759
+ },
760
+ },
761
+ });
762
+
763
+ expect(mockDrupalAuth.post).toHaveBeenCalledWith(
764
+ "/jsonapi/node/access_news",
765
+ expect.objectContaining({
766
+ data: expect.objectContaining({
767
+ attributes: expect.objectContaining({
768
+ field_choose_where_to_share_this: [
769
+ "on_the_announcements_page",
770
+ "in_the_access_support_bi_weekly_digest",
771
+ ],
772
+ }),
773
+ }),
774
+ })
775
+ );
776
+ });
777
+
778
+ it("should fail with invalid where_to_share value", async () => {
779
+ mockDrupalAuth.get.mockResolvedValueOnce({
780
+ data: [{ id: "acting-user-uuid-123" }],
781
+ });
782
+
783
+ const result = await server["handleToolCall"]({
784
+ method: "tools/call",
785
+ params: {
786
+ name: "create_announcement",
787
+ arguments: {
788
+ title: "Test",
789
+ body: "Body",
790
+ summary: "Summary",
791
+ where_to_share: ["Invalid Option"],
792
+ },
793
+ },
794
+ });
795
+
796
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
797
+ expect(responseData.error).toContain("Invalid where_to_share value");
798
+ });
799
+
800
+ it("should create announcement with affinity group", async () => {
801
+ // User lookup
802
+ mockDrupalAuth.get.mockResolvedValueOnce({
803
+ data: [{ id: "acting-user-uuid-123" }],
804
+ });
805
+ // Affinity group lookup by title
806
+ mockDrupalAuth.get.mockResolvedValueOnce({
807
+ data: [],
808
+ });
809
+ mockDrupalAuth.get.mockResolvedValueOnce({
810
+ data: [{ id: "group-uuid-456", attributes: { title: "Test Group" } }],
811
+ });
812
+
813
+ mockDrupalAuth.post.mockResolvedValue({
814
+ data: {
815
+ id: "new-announcement-uuid",
816
+ attributes: {
817
+ title: "Test with group",
818
+ drupal_internal__nid: 12345,
819
+ },
820
+ },
821
+ });
822
+
823
+ await server["handleToolCall"]({
824
+ method: "tools/call",
825
+ params: {
826
+ name: "create_announcement",
827
+ arguments: {
828
+ title: "Test with group",
829
+ body: "<p>Body content</p>",
830
+ summary: "Test summary",
831
+ affinity_group: "Test Group",
832
+ },
833
+ },
834
+ });
835
+
836
+ expect(mockDrupalAuth.post).toHaveBeenCalledWith(
837
+ "/jsonapi/node/access_news",
838
+ expect.objectContaining({
839
+ data: expect.objectContaining({
840
+ relationships: expect.objectContaining({
841
+ field_affinity_group_node: {
842
+ data: {
843
+ type: "node--affinity_group",
844
+ id: "group-uuid-456",
845
+ },
846
+ },
847
+ }),
848
+ }),
849
+ })
850
+ );
851
+ });
852
+
853
+ it("should fail when affinity group not found", async () => {
854
+ mockDrupalAuth.get.mockResolvedValueOnce({
855
+ data: [{ id: "acting-user-uuid-123" }],
856
+ });
857
+ // Both lookups return empty
858
+ mockDrupalAuth.get.mockResolvedValueOnce({ data: [] });
859
+ mockDrupalAuth.get.mockResolvedValueOnce({ data: [] });
860
+
861
+ const result = await server["handleToolCall"]({
862
+ method: "tools/call",
863
+ params: {
864
+ name: "create_announcement",
865
+ arguments: {
866
+ title: "Test",
867
+ body: "Body",
868
+ summary: "Summary",
869
+ affinity_group: "Nonexistent Group",
870
+ },
871
+ },
872
+ });
873
+
874
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
875
+ expect(responseData.error).toContain("Affinity group not found");
876
+ });
877
+ });
878
+
879
+ describe("update_announcement", () => {
880
+ it("should update an announcement", async () => {
881
+ mockDrupalAuth.patch.mockResolvedValue({
882
+ data: {
883
+ id: "announcement-uuid",
884
+ attributes: { title: "Updated Title" },
885
+ },
886
+ });
887
+
888
+ const result = await server["handleToolCall"]({
889
+ method: "tools/call",
890
+ params: {
891
+ name: "update_announcement",
892
+ arguments: {
893
+ uuid: "announcement-uuid",
894
+ title: "Updated Title",
895
+ },
896
+ },
897
+ });
898
+
899
+ expect(mockDrupalAuth.patch).toHaveBeenCalledWith(
900
+ "/jsonapi/node/access_news/announcement-uuid",
901
+ expect.objectContaining({
902
+ data: expect.objectContaining({
903
+ id: "announcement-uuid",
904
+ attributes: expect.objectContaining({
905
+ title: "Updated Title",
906
+ }),
907
+ }),
908
+ })
909
+ );
910
+
911
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
912
+ expect(responseData.success).toBe(true);
913
+ });
914
+
915
+ it("should preserve existing body when updating summary only", async () => {
916
+ // First call: fetch existing announcement
917
+ mockDrupalAuth.get.mockResolvedValueOnce({
918
+ data: {
919
+ attributes: {
920
+ body: {
921
+ value: "<p>Existing body content</p>",
922
+ summary: "Old summary",
923
+ },
924
+ },
925
+ },
926
+ });
927
+
928
+ mockDrupalAuth.patch.mockResolvedValue({
929
+ data: {
930
+ id: "announcement-uuid",
931
+ attributes: { title: "Test" },
932
+ },
933
+ });
934
+
935
+ await server["handleToolCall"]({
936
+ method: "tools/call",
937
+ params: {
938
+ name: "update_announcement",
939
+ arguments: {
940
+ uuid: "announcement-uuid",
941
+ summary: "New summary only",
942
+ },
943
+ },
944
+ });
945
+
946
+ expect(mockDrupalAuth.patch).toHaveBeenCalledWith(
947
+ "/jsonapi/node/access_news/announcement-uuid",
948
+ expect.objectContaining({
949
+ data: expect.objectContaining({
950
+ attributes: expect.objectContaining({
951
+ body: {
952
+ value: "<p>Existing body content</p>",
953
+ format: "basic_html",
954
+ summary: "New summary only",
955
+ },
956
+ }),
957
+ }),
958
+ })
959
+ );
960
+ });
961
+
962
+ it("should update with tags", async () => {
963
+ // Tag cache fetch
964
+ mockDrupalAuth.get.mockResolvedValueOnce({
965
+ data: [
966
+ { id: "tag-uuid-1", attributes: { name: "gpu" } },
967
+ { id: "tag-uuid-2", attributes: { name: "hpc" } },
968
+ ],
969
+ });
970
+
971
+ mockDrupalAuth.patch.mockResolvedValue({
972
+ data: {
973
+ id: "announcement-uuid",
974
+ attributes: { title: "Test" },
975
+ },
976
+ });
977
+
978
+ await server["handleToolCall"]({
979
+ method: "tools/call",
980
+ params: {
981
+ name: "update_announcement",
982
+ arguments: {
983
+ uuid: "announcement-uuid",
984
+ tags: ["gpu", "hpc"],
985
+ },
986
+ },
987
+ });
988
+
989
+ expect(mockDrupalAuth.patch).toHaveBeenCalledWith(
990
+ "/jsonapi/node/access_news/announcement-uuid",
991
+ expect.objectContaining({
992
+ data: expect.objectContaining({
993
+ relationships: expect.objectContaining({
994
+ field_tags: {
995
+ data: [
996
+ { type: "taxonomy_term--tags", id: "tag-uuid-1" },
997
+ { type: "taxonomy_term--tags", id: "tag-uuid-2" },
998
+ ],
999
+ },
1000
+ }),
1001
+ }),
1002
+ })
1003
+ );
1004
+ });
1005
+ });
1006
+
1007
+ describe("delete_announcement", () => {
1008
+ it("should delete an announcement when confirmed", async () => {
1009
+ mockDrupalAuth.delete.mockResolvedValue({});
1010
+
1011
+ const result = await server["handleToolCall"]({
1012
+ method: "tools/call",
1013
+ params: {
1014
+ name: "delete_announcement",
1015
+ arguments: {
1016
+ uuid: "announcement-to-delete",
1017
+ confirmed: true,
1018
+ },
1019
+ },
1020
+ });
1021
+
1022
+ expect(mockDrupalAuth.delete).toHaveBeenCalledWith(
1023
+ "/jsonapi/node/access_news/announcement-to-delete"
1024
+ );
1025
+
1026
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
1027
+ expect(responseData.success).toBe(true);
1028
+ expect(responseData.uuid).toBe("announcement-to-delete");
1029
+ });
1030
+
1031
+ it("should reject deletion without confirmation", async () => {
1032
+ const result = await server["handleToolCall"]({
1033
+ method: "tools/call",
1034
+ params: {
1035
+ name: "delete_announcement",
1036
+ arguments: {
1037
+ uuid: "announcement-to-delete",
1038
+ confirmed: false,
1039
+ },
1040
+ },
1041
+ });
1042
+
1043
+ // Should not call delete
1044
+ expect(mockDrupalAuth.delete).not.toHaveBeenCalled();
1045
+
1046
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
1047
+ expect(responseData.error).toContain("explicit confirmation");
1048
+ });
1049
+ });
1050
+
1051
+ describe("get_my_announcements", () => {
1052
+ it("should fetch announcements for acting user", async () => {
1053
+ // First call: user UUID lookup, Second call: announcements
1054
+ mockDrupalAuth.get
1055
+ .mockResolvedValueOnce({ data: [{ id: "acting-user-uuid-456" }] })
1056
+ .mockResolvedValueOnce({
1057
+ data: [
1058
+ {
1059
+ id: "announcement-1",
1060
+ attributes: {
1061
+ title: "My First Announcement",
1062
+ status: false,
1063
+ created: "2024-03-15T10:00:00Z",
1064
+ body: { value: "<p>Content</p>", summary: "Summary" },
1065
+ },
1066
+ },
1067
+ ],
1068
+ });
1069
+
1070
+ const result = await server["handleToolCall"]({
1071
+ method: "tools/call",
1072
+ params: {
1073
+ name: "get_my_announcements",
1074
+ arguments: { limit: 10 },
1075
+ },
1076
+ });
1077
+
1078
+ // Should look up acting user UUID first
1079
+ expect(mockDrupalAuth.get).toHaveBeenCalledWith(
1080
+ "/jsonapi/user/user?filter[drupal_internal__uid]=1985"
1081
+ );
1082
+ // Then fetch announcements for that user
1083
+ expect(mockDrupalAuth.get).toHaveBeenCalledWith(
1084
+ expect.stringContaining("filter[uid.id]=acting-user-uuid-456")
1085
+ );
1086
+
1087
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
1088
+ expect(responseData.items).toHaveLength(1);
1089
+ expect(responseData.items[0].title).toBe("My First Announcement");
1090
+ expect(responseData.items[0].status).toBe("draft");
1091
+ });
1092
+ });
1093
+
1094
+ describe("get_announcement_context", () => {
1095
+ it("should return tags and affinity groups for coordinator", async () => {
1096
+ // First call: get user UUID
1097
+ mockDrupalAuth.get.mockResolvedValueOnce({
1098
+ data: [{ id: "user-uuid-123", attributes: { name: "testuser" } }],
1099
+ });
1100
+ // Second call: get tags (parallel)
1101
+ mockDrupalAuth.get.mockResolvedValueOnce({
1102
+ data: [
1103
+ { id: "tag-1", attributes: { name: "gpu" } },
1104
+ { id: "tag-2", attributes: { name: "hpc" } },
1105
+ ],
1106
+ });
1107
+ // Third call: get affinity groups (parallel)
1108
+ mockDrupalAuth.get.mockResolvedValueOnce({
1109
+ data: [
1110
+ {
1111
+ id: "group-uuid-1",
1112
+ attributes: {
1113
+ title: "Test Group",
1114
+ field_group_id: 123,
1115
+ field_affinity_group_category: "Research"
1116
+ }
1117
+ },
1118
+ ],
1119
+ });
1120
+
1121
+ const result = await server["handleToolCall"]({
1122
+ method: "tools/call",
1123
+ params: {
1124
+ name: "get_announcement_context",
1125
+ arguments: {},
1126
+ },
1127
+ });
1128
+
1129
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
1130
+ expect(responseData.tags).toHaveLength(2);
1131
+ expect(responseData.tags[0].name).toBe("gpu");
1132
+ expect(responseData.affinity_groups).toHaveLength(1);
1133
+ expect(responseData.affinity_groups[0].name).toBe("Test Group");
1134
+ expect(responseData.is_coordinator).toBe(true);
1135
+ expect(responseData.affiliations).toContain("ACCESS Collaboration");
1136
+ expect(responseData.affiliations).toContain("Community");
1137
+ });
1138
+
1139
+ it("should indicate non-coordinator when user has no affinity groups", async () => {
1140
+ // First call: get user UUID
1141
+ mockDrupalAuth.get.mockResolvedValueOnce({
1142
+ data: [{ id: "user-uuid-123", attributes: { name: "testuser" } }],
1143
+ });
1144
+ // Second call: get tags (parallel)
1145
+ mockDrupalAuth.get.mockResolvedValueOnce({
1146
+ data: [{ id: "tag-1", attributes: { name: "gpu" } }],
1147
+ });
1148
+ // Third call: get affinity groups - empty (parallel)
1149
+ mockDrupalAuth.get.mockResolvedValueOnce({
1150
+ data: [],
1151
+ });
1152
+
1153
+ const result = await server["handleToolCall"]({
1154
+ method: "tools/call",
1155
+ params: {
1156
+ name: "get_announcement_context",
1157
+ arguments: {},
1158
+ },
1159
+ });
1160
+
1161
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
1162
+ expect(responseData.is_coordinator).toBe(false);
1163
+ expect(responseData.affinity_groups).toHaveLength(0);
1164
+ });
1165
+ });
1166
+ });
1167
+
1168
+ describe("Resources", () => {
1169
+ it("should read accessci://announcements resource", async () => {
1170
+ mockHttpClient.get.mockResolvedValue({
1171
+ status: 200,
1172
+ data: [
1173
+ {
1174
+ uuid: "test-uuid-1",
1175
+ title: "Test Announcement",
1176
+ body: "<p>Content</p>",
1177
+ published_date: "2024-03-15",
1178
+ tags: "gpu,hpc",
1179
+ affinity_group: [],
1180
+ },
1181
+ ],
1182
+ });
1183
+
1184
+ const result = await server["handleResourceRead"]({
1185
+ method: "resources/read",
1186
+ params: {
1187
+ uri: "accessci://announcements",
1188
+ },
1189
+ });
1190
+
1191
+ expect(result.contents).toHaveLength(1);
1192
+ expect(result.contents[0].uri).toBe("accessci://announcements");
1193
+ expect(result.contents[0].mimeType).toBe("application/json");
1194
+
1195
+ const data = JSON.parse(result.contents[0].text as string);
1196
+ expect(data).toHaveLength(1);
1197
+ expect(data[0].title).toBe("Test Announcement");
1198
+ });
1199
+
1200
+ it("should handle resource read errors", async () => {
1201
+ mockHttpClient.get.mockResolvedValue({
1202
+ status: 500,
1203
+ statusText: "Internal Server Error",
1204
+ });
1205
+
1206
+ const result = await server["handleResourceRead"]({
1207
+ method: "resources/read",
1208
+ params: {
1209
+ uri: "accessci://announcements",
1210
+ },
1211
+ });
1212
+
1213
+ expect(result.contents[0].text).toContain("Error loading announcements");
1214
+ });
1215
+
1216
+ it("should throw for unknown resource", async () => {
1217
+ await expect(
1218
+ server["handleResourceRead"]({
1219
+ method: "resources/read",
1220
+ params: {
1221
+ uri: "accessci://unknown",
1222
+ },
1223
+ })
1224
+ ).rejects.toThrow("Unknown resource");
1225
+ });
1226
+ });
1227
+
1228
+ describe("Prompts", () => {
1229
+ it("should return create_announcement_guide prompt", async () => {
1230
+ const result = await server["handleGetPrompt"]({
1231
+ params: {
1232
+ name: "create_announcement_guide",
1233
+ arguments: {},
1234
+ },
1235
+ });
1236
+
1237
+ expect(result.description).toBe("Guide for creating an ACCESS announcement");
1238
+ expect(result.messages).toHaveLength(2);
1239
+ expect(result.messages[0].role).toBe("user");
1240
+ expect(result.messages[1].role).toBe("assistant");
1241
+ });
1242
+
1243
+ it("should include topic in create_announcement_guide", async () => {
1244
+ const result = await server["handleGetPrompt"]({
1245
+ params: {
1246
+ name: "create_announcement_guide",
1247
+ arguments: { topic: "GPU availability" },
1248
+ },
1249
+ });
1250
+
1251
+ expect((result.messages[0].content as any).text).toContain("GPU availability");
1252
+ expect((result.messages[1].content as any).text).toContain("GPU availability");
1253
+ });
1254
+
1255
+ it("should return manage_announcements_guide prompt", async () => {
1256
+ const result = await server["handleGetPrompt"]({
1257
+ params: {
1258
+ name: "manage_announcements_guide",
1259
+ arguments: {},
1260
+ },
1261
+ });
1262
+
1263
+ expect(result.description).toBe("Guide for managing existing announcements");
1264
+ expect(result.messages).toHaveLength(2);
1265
+ expect((result.messages[1].content as any).text).toContain("get_my_announcements");
1266
+ });
1267
+
1268
+ it("should throw for unknown prompt", async () => {
1269
+ await expect(
1270
+ server["handleGetPrompt"]({
1271
+ params: {
1272
+ name: "unknown_prompt",
1273
+ arguments: {},
1274
+ },
1275
+ })
1276
+ ).rejects.toThrow("Unknown prompt");
1277
+ });
1278
+ });
1279
+
1280
+ describe("Unknown Tool", () => {
1281
+ it("should return error for unknown tool", async () => {
1282
+ const result = await server["handleToolCall"]({
1283
+ method: "tools/call",
356
1284
  params: {
357
- name: "get_announcements",
1285
+ name: "unknown_tool",
358
1286
  arguments: {},
359
1287
  },
360
- } as any);
1288
+ });
361
1289
 
362
- const responseData = JSON.parse(result.content[0].text);
363
- expect(responseData.announcements[0].formatted_date).toBeDefined();
364
- expect(responseData.announcements[0].formatted_date).toContain("March");
1290
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
1291
+ expect(responseData.error).toContain("Unknown tool");
365
1292
  });
366
1293
  });
367
1294
  });