@l10nmonster/mcp 3.0.0-alpha.17 → 3.1.1

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,947 +0,0 @@
1
- import { describe, it, beforeEach } from 'node:test';
2
- import assert from 'node:assert';
3
- import { z } from 'zod';
4
- import {
5
- McpTool,
6
- McpToolError,
7
- McpInputError,
8
- McpNotFoundError,
9
- McpProviderError
10
- } from '../tools/mcpTool.js';
11
- import { SourceQueryTool } from '../tools/sourceQuery.js';
12
- import { TranslateTool } from '../tools/translate.js';
13
- import { StatusTool } from '../tools/status.js';
14
-
15
- describe('MCP Tool Input Validation and Output', () => {
16
- let mockMM;
17
-
18
- function createMockMM() {
19
- return {
20
- rm: {
21
- channelIds: ['channel1', 'channel2'],
22
- getActiveContentStats: async () => [],
23
- getDesiredLangPairs: async () => [['en', 'fr'], ['en', 'es']],
24
- autoSnap: true
25
- },
26
- tmm: {
27
- getTM: (sourceLang, targetLang) => {
28
- if (sourceLang === 'invalid' || targetLang === 'invalid') {
29
- throw new Error('Language pair not available');
30
- }
31
- return {
32
- querySource: async () => [
33
- {
34
- guid: 'guid1',
35
- rid: 'test.js',
36
- sid: 'hello',
37
- nsrc: ['Hello'],
38
- ntgt: ['Bonjour'],
39
- q: 0.9
40
- }
41
- ],
42
- queryByGuids: async () => [
43
- {
44
- guid: 'guid1',
45
- rid: 'test.js',
46
- sid: 'hello',
47
- nsrc: ['Hello']
48
- }
49
- ],
50
- getStats: async () => [
51
- {
52
- translationProvider: 'test-provider',
53
- status: 'done',
54
- jobCount: 1,
55
- tuCount: 10,
56
- distinctGuids: 10
57
- }
58
- ]
59
- };
60
- },
61
- getAvailableLangPairs: async () => [['en', 'fr'], ['en', 'es']],
62
- getJobTOCByLangPair: async () => [],
63
- getJob: async (jobGuid) => ({
64
- jobGuid,
65
- sourceLang: 'en-US',
66
- targetLang: 'fr-FR',
67
- translationProvider: 'test-provider',
68
- status: 'done',
69
- tus: [
70
- {
71
- guid: 'guid1',
72
- rid: 'test.js',
73
- sid: 'hello',
74
- nsrc: ['Hello'],
75
- ntgt: ['Bonjour'],
76
- q: 0.9,
77
- ts: Date.now()
78
- }
79
- ],
80
- inflight: []
81
- })
82
- },
83
- dispatcher: {
84
- providers: [
85
- {
86
- id: 'test-provider',
87
- info: async () => ({
88
- id: 'test-provider',
89
- type: 'mt',
90
- quality: 'high',
91
- supportedPairs: 'any'
92
- })
93
- }
94
- ],
95
- createJobs: async () => [
96
- {
97
- translationProvider: 'test-provider',
98
- jobGuid: 'job-123',
99
- tus: [{ guid: 'guid1' }]
100
- }
101
- ],
102
- startJobs: async () => [
103
- {
104
- jobGuid: 'job-123',
105
- status: 'done'
106
- }
107
- ]
108
- },
109
- getTranslationStatus: async () => ({
110
- channel1: {
111
- en: {
112
- fr: {
113
- prj1: {
114
- pairSummary: { segs: 100 },
115
- pairSummaryByStatus: {
116
- translated: 80,
117
- untranslated: 20,
118
- 'in flight': 0,
119
- 'low quality': 0
120
- }
121
- }
122
- }
123
- }
124
- }
125
- })
126
- };
127
- }
128
-
129
- beforeEach(() => {
130
- mockMM = createMockMM();
131
- });
132
-
133
- describe('McpTool base class', () => {
134
- describe('formatResult', () => {
135
- it('should format object results as JSON content', () => {
136
- const result = { data: 'test', count: 42 };
137
- const formatted = McpTool.formatResult(result);
138
-
139
- assert.ok(formatted.content);
140
- assert.strictEqual(formatted.content.length, 1);
141
- assert.strictEqual(formatted.content[0].type, 'text');
142
- assert.strictEqual(formatted.content[0].text, JSON.stringify(result, null, 2));
143
- });
144
-
145
- it('should format string results as text content', () => {
146
- const result = 'Simple text result';
147
- const formatted = McpTool.formatResult(result);
148
-
149
- assert.ok(formatted.content);
150
- assert.strictEqual(formatted.content.length, 1);
151
- assert.strictEqual(formatted.content[0].type, 'text');
152
- assert.strictEqual(formatted.content[0].text, result);
153
- });
154
-
155
- it('should pass through MCP-formatted responses', () => {
156
- const mcpFormatted = {
157
- content: [
158
- { type: 'text', text: 'Hello' },
159
- { type: 'json', json: { data: 'test' } }
160
- ]
161
- };
162
- const formatted = McpTool.formatResult(mcpFormatted);
163
-
164
- assert.deepStrictEqual(formatted, mcpFormatted);
165
- });
166
-
167
- it('should format number results as text', () => {
168
- const result = 42;
169
- const formatted = McpTool.formatResult(result);
170
-
171
- assert.strictEqual(formatted.content[0].type, 'text');
172
- assert.strictEqual(formatted.content[0].text, '42');
173
- });
174
-
175
- it('should format array results as JSON', () => {
176
- const result = [1, 2, 3];
177
- const formatted = McpTool.formatResult(result);
178
-
179
- assert.strictEqual(formatted.content[0].type, 'text');
180
- assert.strictEqual(formatted.content[0].text, JSON.stringify(result, null, 2));
181
- });
182
- });
183
-
184
- describe('formatError', () => {
185
- it('should format McpToolError with all properties', () => {
186
- const error = new McpToolError('Test error', {
187
- code: 'TEST_ERROR',
188
- retryable: true,
189
- hints: ['Hint 1', 'Hint 2'],
190
- details: { key: 'value' }
191
- });
192
-
193
- const formatted = McpTool.formatError(error);
194
-
195
- assert.ok(formatted.isError);
196
- assert.ok(formatted.content);
197
- assert.strictEqual(formatted.content.length, 2);
198
- assert.strictEqual(formatted.content[0].type, 'text');
199
- assert.strictEqual(formatted.content[1].type, 'text');
200
-
201
- const payload = JSON.parse(formatted.content[1].text);
202
- assert.strictEqual(payload.name, 'McpToolError');
203
- assert.strictEqual(payload.message, 'Test error');
204
- assert.strictEqual(payload.code, 'TEST_ERROR');
205
- assert.strictEqual(payload.retryable, true);
206
- assert.deepStrictEqual(payload.hints, ['Hint 1', 'Hint 2']);
207
- assert.deepStrictEqual(payload.details, { key: 'value' });
208
- });
209
-
210
- it('should format ZodError with validation details', () => {
211
- const schema = z.object({
212
- required: z.string(),
213
- optional: z.number().optional()
214
- });
215
-
216
- let zodError;
217
- try {
218
- schema.parse({ optional: 'not-a-number' });
219
- } catch (error) {
220
- zodError = error;
221
- }
222
-
223
- const formatted = McpTool.formatError(zodError);
224
-
225
- assert.ok(formatted.isError);
226
- const payload = JSON.parse(formatted.content[1].text);
227
- assert.strictEqual(payload.code, 'INVALID_INPUT');
228
- assert.ok(payload.details);
229
- assert.ok(payload.details.issues);
230
- assert.ok(Array.isArray(payload.details.issues));
231
- assert.ok(payload.hints);
232
- });
233
-
234
- it('should format errors with cause chain', () => {
235
- const cause = new Error('Root cause');
236
- const error = new McpToolError('Wrapper error', { cause });
237
-
238
- const formatted = McpTool.formatError(error);
239
- const payload = JSON.parse(formatted.content[1].text);
240
-
241
- assert.ok(payload.cause);
242
- assert.strictEqual(payload.cause.name, 'Error');
243
- assert.strictEqual(payload.cause.message, 'Root cause');
244
- });
245
-
246
- it('should wrap unknown errors', () => {
247
- const error = new Error('Unknown error');
248
- const formatted = McpTool.formatError(error);
249
- const payload = JSON.parse(formatted.content[1].text);
250
-
251
- assert.strictEqual(payload.code, 'UNKNOWN_ERROR');
252
- assert.strictEqual(payload.message, 'Unknown error');
253
- });
254
-
255
- it('should include stack trace when available', () => {
256
- const error = new Error('Test');
257
- error.stack = 'Error: Test\n at test.js:1:1';
258
-
259
- const formatted = McpTool.formatError(error);
260
- const payload = JSON.parse(formatted.content[1].text);
261
-
262
- assert.ok(payload.stack);
263
- // The wrapped error will have its own stack trace, not the original
264
- assert.ok(payload.stack.includes('McpToolError'));
265
- });
266
- });
267
-
268
- describe('handler', () => {
269
- class TestTool extends McpTool {
270
- static metadata = {
271
- name: 'test_tool',
272
- description: 'Test tool',
273
- inputSchema: z.object({
274
- required: z.string(),
275
- optional: z.number().optional()
276
- })
277
- };
278
-
279
- static async execute(mm, args) {
280
- return { result: args.required, optional: args.optional };
281
- }
282
- }
283
-
284
- it('should validate input before execution', async () => {
285
- const handler = TestTool.handler(mockMM);
286
-
287
- // Valid input
288
- const validResult = await handler({ required: 'test', optional: 42 });
289
- assert.ok(validResult.content);
290
- assert.strictEqual(validResult.content[0].type, 'text');
291
- const validPayload = JSON.parse(validResult.content[0].text);
292
- assert.strictEqual(validPayload.result, 'test');
293
-
294
- // Invalid input - missing required
295
- const invalidResult = await handler({ optional: 42 });
296
- assert.ok(invalidResult.isError);
297
- const invalidPayload = JSON.parse(invalidResult.content[1].text);
298
- assert.strictEqual(invalidPayload.code, 'INVALID_INPUT');
299
- });
300
-
301
- it('should format execution errors', async () => {
302
- class ErrorTool extends McpTool {
303
- static metadata = {
304
- name: 'error_tool',
305
- description: 'Tool that errors',
306
- inputSchema: z.object({})
307
- };
308
-
309
- static async execute() {
310
- throw new Error('Execution failed');
311
- }
312
- }
313
-
314
- const handler = ErrorTool.handler(mockMM);
315
- const result = await handler({});
316
-
317
- assert.ok(result.isError);
318
- assert.ok(result.content[0].text.includes('Error executing error_tool'));
319
- });
320
- });
321
- });
322
-
323
- describe('SourceQueryTool', () => {
324
- describe('input validation', () => {
325
- it('should accept valid language pair format', async () => {
326
- const handler = SourceQueryTool.handler(mockMM);
327
- const result = await handler({
328
- sourceLang: 'en',
329
- targetLang: 'fr',
330
- channel: 'channel1'
331
- });
332
-
333
- assert.ok(!result.isError);
334
- assert.ok(result.content);
335
- });
336
-
337
- it('should reject invalid language pair format', async () => {
338
- const handler = SourceQueryTool.handler(mockMM);
339
-
340
- // Missing sourceLang
341
- const result1 = await handler({
342
- targetLang: 'fr',
343
- channel: 'channel1'
344
- });
345
- assert.ok(result1.isError);
346
-
347
- // Missing targetLang
348
- const result2 = await handler({
349
- sourceLang: 'en',
350
- channel: 'channel1'
351
- });
352
- assert.ok(result2.isError);
353
- });
354
-
355
- it('should require channel parameter', async () => {
356
- const handler = SourceQueryTool.handler(mockMM);
357
- const result = await handler({
358
- sourceLang: 'en',
359
- targetLang: 'fr'
360
- });
361
-
362
- assert.ok(result.isError);
363
- const payload = JSON.parse(result.content[1].text);
364
- assert.strictEqual(payload.code, 'INVALID_INPUT');
365
- });
366
-
367
- it('should accept optional whereCondition', async () => {
368
- const handler = SourceQueryTool.handler(mockMM);
369
-
370
- // Without whereCondition
371
- const result1 = await handler({
372
- sourceLang: 'en',
373
- targetLang: 'fr',
374
- channel: 'channel1'
375
- });
376
- assert.ok(!result1.isError);
377
-
378
- // With whereCondition
379
- const result2 = await handler({
380
- sourceLang: 'en',
381
- targetLang: 'fr',
382
- channel: 'channel1',
383
- whereCondition: 'rid = "test.js"'
384
- });
385
- assert.ok(!result2.isError);
386
- });
387
-
388
- it('should reject empty channel', async () => {
389
- const handler = SourceQueryTool.handler(mockMM);
390
- const result = await handler({
391
- sourceLang: 'en',
392
- targetLang: 'fr',
393
- channel: ''
394
- });
395
-
396
- // Empty string should fail Zod validation (string().min(1) implicit)
397
- assert.ok(result.isError);
398
- });
399
- });
400
-
401
- describe('error handling', () => {
402
- it('should handle non-existent channel', async () => {
403
- const handler = SourceQueryTool.handler(mockMM);
404
- const result = await handler({
405
- sourceLang: 'en',
406
- targetLang: 'fr',
407
- channel: 'nonexistent'
408
- });
409
-
410
- assert.ok(result.isError);
411
- const payload = JSON.parse(result.content[1].text);
412
- assert.strictEqual(payload.code, 'NOT_FOUND');
413
- assert.ok(payload.hints);
414
- });
415
-
416
- it('should handle invalid language pair', async () => {
417
- const handler = SourceQueryTool.handler(mockMM);
418
- const result = await handler({
419
- sourceLang: 'invalid',
420
- targetLang: 'invalid',
421
- channel: 'channel1'
422
- });
423
-
424
- assert.ok(result.isError);
425
- const payload = JSON.parse(result.content[1].text);
426
- assert.strictEqual(payload.code, 'INVALID_INPUT');
427
- });
428
-
429
- it('should handle query failures', async () => {
430
- const localMM = createMockMM();
431
- localMM.tmm.getTM = () => ({
432
- querySource: async () => {
433
- throw new Error('SQL syntax error');
434
- }
435
- });
436
-
437
- const handler = SourceQueryTool.handler(localMM);
438
- const result = await handler({
439
- sourceLang: 'en',
440
- targetLang: 'fr',
441
- channel: 'channel1',
442
- whereCondition: 'invalid SQL'
443
- });
444
-
445
- assert.ok(result.isError);
446
- const payload = JSON.parse(result.content[1].text);
447
- assert.strictEqual(payload.code, 'QUERY_FAILED');
448
- assert.ok(payload.hints);
449
- });
450
- });
451
-
452
- describe('output format', () => {
453
- it('should return structured result with translation units', async () => {
454
- const handler = SourceQueryTool.handler(mockMM);
455
- const result = await handler({
456
- sourceLang: 'en',
457
- targetLang: 'fr',
458
- channel: 'channel1'
459
- });
460
-
461
- assert.ok(!result.isError);
462
- const json = JSON.parse(result.content[0].text);
463
- assert.ok(json.sourceLang);
464
- assert.ok(json.targetLang);
465
- assert.ok(Array.isArray(json.translationUnits));
466
- assert.ok(typeof json.message === 'string');
467
- });
468
-
469
- it('should return empty array message when no results', async () => {
470
- const localMM = createMockMM();
471
- localMM.tmm.getTM = () => ({
472
- querySource: async () => []
473
- });
474
-
475
- const handler = SourceQueryTool.handler(localMM);
476
- const result = await handler({
477
- sourceLang: 'en',
478
- targetLang: 'fr',
479
- channel: 'channel1'
480
- });
481
-
482
- assert.ok(!result.isError);
483
- const json = JSON.parse(result.content[0].text);
484
- assert.strictEqual(json.translationUnits.length, 0);
485
- assert.ok(json.message.includes('No content'));
486
- });
487
- });
488
- });
489
-
490
- describe('TranslateTool', () => {
491
- describe('input validation', () => {
492
- it('should accept all required parameters', async () => {
493
- const handler = TranslateTool.handler(mockMM);
494
- const result = await handler({
495
- sourceLang: 'en-US',
496
- targetLang: 'fr-FR',
497
- channelId: 'channel1',
498
- provider: 'test-provider',
499
- guids: ['guid1']
500
- });
501
-
502
- assert.ok(!result.isError);
503
- });
504
-
505
- it('should require sourceLang', async () => {
506
- const handler = TranslateTool.handler(mockMM);
507
- const result = await handler({
508
- targetLang: 'fr-FR',
509
- channelId: 'channel1',
510
- provider: 'test-provider',
511
- guids: ['guid1']
512
- });
513
-
514
- assert.ok(result.isError);
515
- const payload = JSON.parse(result.content[1].text);
516
- assert.strictEqual(payload.code, 'INVALID_INPUT');
517
- });
518
-
519
- it('should require targetLang', async () => {
520
- const handler = TranslateTool.handler(mockMM);
521
- const result = await handler({
522
- sourceLang: 'en-US',
523
- channelId: 'channel1',
524
- provider: 'test-provider',
525
- guids: ['guid1']
526
- });
527
-
528
- assert.ok(result.isError);
529
- });
530
-
531
- it('should require channelId', async () => {
532
- const handler = TranslateTool.handler(mockMM);
533
- const result = await handler({
534
- sourceLang: 'en-US',
535
- targetLang: 'fr-FR',
536
- provider: 'test-provider',
537
- guids: ['guid1']
538
- });
539
-
540
- assert.ok(result.isError);
541
- });
542
-
543
- it('should require provider', async () => {
544
- const handler = TranslateTool.handler(mockMM);
545
- const result = await handler({
546
- sourceLang: 'en-US',
547
- targetLang: 'fr-FR',
548
- channelId: 'channel1',
549
- guids: ['guid1']
550
- });
551
-
552
- assert.ok(result.isError);
553
- });
554
-
555
- it('should require at least one guid', async () => {
556
- const handler = TranslateTool.handler(mockMM);
557
- const result = await handler({
558
- sourceLang: 'en-US',
559
- targetLang: 'fr-FR',
560
- channelId: 'channel1',
561
- provider: 'test-provider',
562
- guids: []
563
- });
564
-
565
- assert.ok(result.isError);
566
- });
567
-
568
- it('should accept optional instructions', async () => {
569
- const handler = TranslateTool.handler(mockMM);
570
- const result = await handler({
571
- sourceLang: 'en-US',
572
- targetLang: 'fr-FR',
573
- channelId: 'channel1',
574
- provider: 'test-provider',
575
- guids: ['guid1'],
576
- instructions: 'Translate carefully'
577
- });
578
-
579
- assert.ok(!result.isError);
580
- });
581
-
582
- it('should accept array of guids', async () => {
583
- const handler = TranslateTool.handler(mockMM);
584
- const result = await handler({
585
- sourceLang: 'en-US',
586
- targetLang: 'fr-FR',
587
- channelId: 'channel1',
588
- provider: 'test-provider',
589
- guids: ['guid1', 'guid2', 'guid3']
590
- });
591
-
592
- assert.ok(!result.isError);
593
- });
594
- });
595
-
596
- describe('error handling', () => {
597
- it('should handle unknown provider', async () => {
598
- const handler = TranslateTool.handler(mockMM);
599
- const result = await handler({
600
- sourceLang: 'en-US',
601
- targetLang: 'fr-FR',
602
- channelId: 'channel1',
603
- provider: 'unknown-provider',
604
- guids: ['guid1']
605
- });
606
-
607
- assert.ok(result.isError);
608
- const payload = JSON.parse(result.content[1].text);
609
- assert.strictEqual(payload.code, 'INVALID_INPUT');
610
- assert.ok(payload.hints);
611
- });
612
-
613
- it('should handle invalid language pair', async () => {
614
- const handler = TranslateTool.handler(mockMM);
615
- const result = await handler({
616
- sourceLang: 'invalid',
617
- targetLang: 'invalid',
618
- channelId: 'channel1',
619
- provider: 'test-provider',
620
- guids: ['guid1']
621
- });
622
-
623
- assert.ok(result.isError);
624
- const payload = JSON.parse(result.content[1].text);
625
- assert.strictEqual(payload.code, 'INVALID_INPUT');
626
- });
627
-
628
- it('should handle missing translation units', async () => {
629
- const localMM = createMockMM();
630
- localMM.tmm.getTM = () => ({
631
- queryByGuids: async () => []
632
- });
633
-
634
- const handler = TranslateTool.handler(localMM);
635
- const result = await handler({
636
- sourceLang: 'en-US',
637
- targetLang: 'fr-FR',
638
- channelId: 'channel1',
639
- provider: 'test-provider',
640
- guids: ['nonexistent-guid']
641
- });
642
-
643
- assert.ok(result.isError);
644
- const payload = JSON.parse(result.content[1].text);
645
- assert.strictEqual(payload.code, 'NOT_FOUND');
646
- });
647
-
648
- it('should handle provider rejection', async () => {
649
- // Mock TU conversion to fail by returning invalid TU data (missing required fields)
650
- const localMM = createMockMM();
651
- localMM.tmm.getTM = () => ({
652
- queryByGuids: async () => [
653
- {
654
- guid: 'guid1',
655
- // Missing rid and sid which are required fields
656
- nsrc: ['Hello']
657
- }
658
- ]
659
- });
660
-
661
- const handler = TranslateTool.handler(localMM);
662
- const result = await handler({
663
- sourceLang: 'en-US',
664
- targetLang: 'fr-FR',
665
- channelId: 'channel1',
666
- provider: 'test-provider',
667
- guids: ['guid1']
668
- });
669
-
670
- assert.ok(result.isError);
671
- const payload = JSON.parse(result.content[1].text);
672
- assert.strictEqual(payload.code, 'SOURCE_TU_CONVERSION_FAILED');
673
- });
674
- });
675
-
676
- describe('output format', () => {
677
- it('should return structured translation result', async () => {
678
- const handler = TranslateTool.handler(mockMM);
679
- const result = await handler({
680
- sourceLang: 'en-US',
681
- targetLang: 'fr-FR',
682
- channelId: 'channel1',
683
- provider: 'test-provider',
684
- guids: ['guid1']
685
- });
686
-
687
- assert.ok(!result.isError);
688
- const json = JSON.parse(result.content[0].text);
689
- assert.ok(json.jobGuid);
690
- assert.strictEqual(json.sourceLang, 'en-US');
691
- assert.strictEqual(json.targetLang, 'fr-FR');
692
- assert.ok(typeof json.translatedCount === 'number');
693
- assert.ok(Array.isArray(json.translatedTUs));
694
- });
695
-
696
- it('should include inflight guids when present', async () => {
697
- const localMM = createMockMM();
698
- localMM.tmm.getJob = async () => ({
699
- jobGuid: 'job-123',
700
- sourceLang: 'en-US',
701
- targetLang: 'fr-FR',
702
- translationProvider: 'test-provider',
703
- status: 'done',
704
- tus: [
705
- {
706
- guid: 'guid1',
707
- nsrc: ['Hello'],
708
- ntgt: ['Bonjour'],
709
- q: 0.9,
710
- ts: Date.now()
711
- }
712
- ],
713
- inflight: ['guid2']
714
- });
715
-
716
- const handler = TranslateTool.handler(localMM);
717
- const result = await handler({
718
- sourceLang: 'en-US',
719
- targetLang: 'fr-FR',
720
- channelId: 'channel1',
721
- provider: 'test-provider',
722
- guids: ['guid1', 'guid2']
723
- });
724
-
725
- assert.ok(!result.isError);
726
- const json = JSON.parse(result.content[0].text);
727
- assert.ok(json.inflightGuids);
728
- assert.strictEqual(json.inflightCount, 1);
729
- });
730
- });
731
- });
732
-
733
- describe('TranslationStatusTool', () => {
734
- describe('input validation', () => {
735
- it('should accept valid detailLevel enum', async () => {
736
- const handler = StatusTool.handler(mockMM);
737
-
738
- const result1 = await handler({ detailLevel: 'summary' });
739
- assert.ok(!result1.isError);
740
-
741
- const result2 = await handler({ detailLevel: 'detailed' });
742
- assert.ok(!result2.isError);
743
- });
744
-
745
- it('should reject invalid detailLevel', async () => {
746
- const handler = StatusTool.handler(mockMM);
747
- const result = await handler({ detailLevel: 'invalid' });
748
-
749
- assert.ok(result.isError);
750
- const payload = JSON.parse(result.content[1].text);
751
- assert.strictEqual(payload.code, 'INVALID_INPUT');
752
- });
753
-
754
- it('should default detailLevel to summary', async () => {
755
- const handler = StatusTool.handler(mockMM);
756
- const result = await handler({});
757
-
758
- assert.ok(!result.isError);
759
- });
760
-
761
- it('should accept valid include array', async () => {
762
- const handler = StatusTool.handler(mockMM);
763
-
764
- const result1 = await handler({ include: ['jobs'] });
765
- assert.ok(!result1.isError);
766
-
767
- const result2 = await handler({ include: ['coverage', 'providers'] });
768
- assert.ok(!result2.isError);
769
-
770
- const result3 = await handler({ include: ['jobs', 'coverage', 'providers'] });
771
- assert.ok(!result3.isError);
772
- });
773
-
774
- it('should reject invalid include values', async () => {
775
- const handler = StatusTool.handler(mockMM);
776
- const result = await handler({ include: ['invalid'] });
777
-
778
- assert.ok(result.isError);
779
- });
780
-
781
- it('should default include to channels, providers, and languagePairs', async () => {
782
- const handler = StatusTool.handler(mockMM);
783
- const result = await handler({});
784
-
785
- assert.ok(!result.isError);
786
- const json = JSON.parse(result.content[0].text);
787
- assert.ok(json.channels);
788
- assert.ok(json.providers);
789
- assert.ok(json.languagePairs);
790
- // Coverage and translationMemory are not included by default
791
- assert.ok(!json.coverage);
792
- assert.ok(!json.translationMemory);
793
- });
794
-
795
- it('should accept optional filters', async () => {
796
- const handler = StatusTool.handler(mockMM);
797
-
798
- const result1 = await handler({ channel: 'channel1' });
799
- assert.ok(!result1.isError);
800
-
801
- const result2 = await handler({ provider: 'test-provider' });
802
- assert.ok(!result2.isError);
803
-
804
- const result3 = await handler({ sourceLang: 'en' });
805
- assert.ok(!result3.isError);
806
-
807
- const result4 = await handler({ targetLang: 'fr' });
808
- assert.ok(!result4.isError);
809
- });
810
-
811
- it('should accept all filters together', async () => {
812
- const handler = StatusTool.handler(mockMM);
813
- const result = await handler({
814
- detailLevel: 'detailed',
815
- include: ['jobs', 'coverage'],
816
- channel: 'channel1',
817
- provider: 'test-provider',
818
- sourceLang: 'en',
819
- targetLang: 'fr'
820
- });
821
-
822
- assert.ok(!result.isError);
823
- });
824
- });
825
-
826
- describe('error handling', () => {
827
- it('should handle non-existent channel', async () => {
828
- const handler = StatusTool.handler(mockMM);
829
- const result = await handler({ channel: 'nonexistent' });
830
-
831
- assert.ok(result.isError);
832
- const payload = JSON.parse(result.content[1].text);
833
- assert.strictEqual(payload.code, 'NOT_FOUND');
834
- });
835
-
836
- it('should handle non-existent provider', async () => {
837
- const handler = StatusTool.handler(mockMM);
838
- const result = await handler({
839
- include: ['providers'],
840
- provider: 'nonexistent'
841
- });
842
-
843
- assert.ok(result.isError);
844
- const payload = JSON.parse(result.content[1].text);
845
- assert.strictEqual(payload.code, 'NOT_FOUND');
846
- });
847
- });
848
-
849
- describe('output format', () => {
850
- it('should return structured status with required fields', async () => {
851
- const handler = StatusTool.handler(mockMM);
852
- const result = await handler({
853
- include: ['channels', 'languagePairs', 'translationMemory']
854
- });
855
-
856
- assert.ok(!result.isError);
857
- const json = JSON.parse(result.content[0].text);
858
- assert.ok(json.timestamp);
859
- assert.ok(json.channels);
860
- assert.ok(json.languagePairs);
861
- assert.ok(json.translationMemory);
862
- });
863
-
864
- it('should include coverage when requested', async () => {
865
- const handler = StatusTool.handler(mockMM);
866
- const result = await handler({ include: ['coverage'] });
867
-
868
- assert.ok(!result.isError);
869
- const json = JSON.parse(result.content[0].text);
870
- assert.ok(json.coverage);
871
- });
872
-
873
- it('should include jobs when requested', async () => {
874
- const handler = StatusTool.handler(mockMM);
875
- const result = await handler({ include: ['jobs'] });
876
-
877
- assert.ok(!result.isError);
878
- const json = JSON.parse(result.content[0].text);
879
- assert.ok(json.jobs !== undefined);
880
- });
881
-
882
- it('should include providers when requested', async () => {
883
- const handler = StatusTool.handler(mockMM);
884
- const result = await handler({ include: ['providers'] });
885
-
886
- assert.ok(!result.isError);
887
- const json = JSON.parse(result.content[0].text);
888
- assert.ok(json.providers);
889
- });
890
-
891
- it('should include details when detailLevel is detailed', async () => {
892
- const handler = StatusTool.handler(mockMM);
893
- const result = await handler({ detailLevel: 'detailed' });
894
-
895
- assert.ok(!result.isError);
896
- const json = JSON.parse(result.content[0].text);
897
- assert.ok(json.details);
898
- });
899
-
900
- it('should not include details when detailLevel is summary', async () => {
901
- const handler = StatusTool.handler(mockMM);
902
- const result = await handler({ detailLevel: 'summary' });
903
-
904
- assert.ok(!result.isError);
905
- const json = JSON.parse(result.content[0].text);
906
- assert.ok(!json.details);
907
- });
908
- });
909
- });
910
-
911
- describe('Error class hierarchy', () => {
912
- it('should create McpInputError with correct code', () => {
913
- const error = new McpInputError('Invalid input');
914
- assert.strictEqual(error.code, 'INVALID_INPUT');
915
- assert.strictEqual(error.message, 'Invalid input');
916
- });
917
-
918
- it('should create McpNotFoundError with correct code', () => {
919
- const error = new McpNotFoundError('Not found');
920
- assert.strictEqual(error.code, 'NOT_FOUND');
921
- assert.strictEqual(error.message, 'Not found');
922
- });
923
-
924
- it('should create McpProviderError with correct code', () => {
925
- const error = new McpProviderError('Provider error');
926
- assert.strictEqual(error.code, 'PROVIDER_ERROR');
927
- assert.strictEqual(error.message, 'Provider error');
928
- });
929
-
930
- it('should wrap non-McpToolError errors', () => {
931
- const originalError = new Error('Original error');
932
- const wrapped = McpToolError.wrap(originalError, { code: 'CUSTOM' });
933
-
934
- assert.ok(wrapped instanceof McpToolError);
935
- assert.strictEqual(wrapped.code, 'CUSTOM');
936
- assert.strictEqual(wrapped.cause, originalError);
937
- });
938
-
939
- it('should not wrap McpToolError errors', () => {
940
- const mcpError = new McpInputError('Already wrapped');
941
- const wrapped = McpToolError.wrap(mcpError);
942
-
943
- assert.strictEqual(wrapped, mcpError);
944
- });
945
- });
946
- });
947
-