@smartbear/mcp 0.3.0 → 0.4.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.
- package/README.md +68 -7
- package/dist/api-hub/client.js +298 -52
- package/dist/common/server.js +145 -0
- package/dist/index.js +31 -22
- package/dist/insight-hub/client.js +383 -385
- package/dist/package.json +3 -2
- package/dist/pactflow/client/ai.js +127 -0
- package/dist/pactflow/client/base.js +1 -0
- package/dist/pactflow/client/tools.js +46 -0
- package/dist/pactflow/client.js +132 -0
- package/dist/reflect/client.js +100 -18
- package/dist/tests/unit/common/server.test.js +319 -0
- package/dist/tests/unit/insight-hub/client.test.js +114 -182
- package/dist/tests/unit/pactflow/ai.test.js +21 -0
- package/dist/tests/unit/pactflow/client.test.js +67 -0
- package/dist/tests/unit/pactflow/tools.test.js +34 -0
- package/package.json +3 -2
- package/dist/common/templates.js +0 -57
- package/dist/tests/unit/common/templates.test.js +0 -149
|
@@ -441,55 +441,50 @@ describe('InsightHubClient', () => {
|
|
|
441
441
|
});
|
|
442
442
|
});
|
|
443
443
|
describe('tool registration', () => {
|
|
444
|
-
let
|
|
444
|
+
let registerToolsSpy;
|
|
445
|
+
let getInputFunctionSpy;
|
|
445
446
|
beforeEach(() => {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
resource: vi.fn()
|
|
449
|
-
};
|
|
447
|
+
registerToolsSpy = vi.fn();
|
|
448
|
+
getInputFunctionSpy = vi.fn();
|
|
450
449
|
});
|
|
451
|
-
it('should register
|
|
452
|
-
client.registerTools(
|
|
453
|
-
expect(
|
|
450
|
+
it('should register list_projects tool when no project API key', () => {
|
|
451
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
452
|
+
expect(registerToolsSpy).toBeCalledWith(expect.any(Object), expect.any(Function));
|
|
454
453
|
});
|
|
455
|
-
it('should not register
|
|
454
|
+
it('should not register list_projects tool when project API key is provided', () => {
|
|
456
455
|
const clientWithApiKey = new InsightHubClient('test-token', 'project-api-key');
|
|
457
|
-
clientWithApiKey.registerTools(
|
|
458
|
-
const registeredTools =
|
|
459
|
-
expect(registeredTools).not.toContain('
|
|
456
|
+
clientWithApiKey.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
457
|
+
const registeredTools = registerToolsSpy.mock.calls.map((call) => call[0].title);
|
|
458
|
+
expect(registeredTools).not.toContain('List Projects');
|
|
460
459
|
});
|
|
461
460
|
it('should register common tools regardless of project API key', () => {
|
|
462
|
-
client.registerTools(
|
|
463
|
-
const registeredTools =
|
|
464
|
-
expect(registeredTools).toContain('
|
|
465
|
-
expect(registeredTools).toContain('
|
|
466
|
-
expect(registeredTools).toContain('
|
|
467
|
-
expect(registeredTools).toContain('
|
|
468
|
-
expect(registeredTools).toContain('
|
|
461
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
462
|
+
const registeredTools = registerToolsSpy.mock.calls.map((call) => call[0].title);
|
|
463
|
+
expect(registeredTools).toContain('Get Error');
|
|
464
|
+
expect(registeredTools).toContain('Get Event Details');
|
|
465
|
+
expect(registeredTools).toContain('List Project Errors');
|
|
466
|
+
expect(registeredTools).toContain('List Project Event Filters');
|
|
467
|
+
expect(registeredTools).toContain('Update Error');
|
|
469
468
|
});
|
|
470
469
|
});
|
|
471
470
|
describe('resource registration', () => {
|
|
472
|
-
let
|
|
471
|
+
let registerResourcesSpy;
|
|
473
472
|
beforeEach(() => {
|
|
474
|
-
|
|
475
|
-
registerTool: vi.fn(),
|
|
476
|
-
resource: vi.fn()
|
|
477
|
-
};
|
|
473
|
+
registerResourcesSpy = vi.fn();
|
|
478
474
|
});
|
|
479
|
-
it('should register
|
|
480
|
-
client.registerResources(
|
|
481
|
-
expect(
|
|
475
|
+
it('should register event resource', () => {
|
|
476
|
+
client.registerResources(registerResourcesSpy);
|
|
477
|
+
expect(registerResourcesSpy).toHaveBeenCalledWith('event', '{id}', expect.any(Function));
|
|
482
478
|
});
|
|
483
479
|
});
|
|
484
480
|
describe('tool handlers', () => {
|
|
485
|
-
let
|
|
481
|
+
let registerToolsSpy;
|
|
482
|
+
let getInputFunctionSpy;
|
|
486
483
|
beforeEach(() => {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
resource: vi.fn()
|
|
490
|
-
};
|
|
484
|
+
registerToolsSpy = vi.fn();
|
|
485
|
+
getInputFunctionSpy = vi.fn();
|
|
491
486
|
});
|
|
492
|
-
describe('
|
|
487
|
+
describe('list_projects tool handler', () => {
|
|
493
488
|
it('should return projects with pagination', async () => {
|
|
494
489
|
const mockProjects = [
|
|
495
490
|
{ id: 'proj-1', name: 'Project 1' },
|
|
@@ -497,9 +492,9 @@ describe('InsightHubClient', () => {
|
|
|
497
492
|
{ id: 'proj-3', name: 'Project 3' }
|
|
498
493
|
];
|
|
499
494
|
mockCache.get.mockReturnValue(mockProjects);
|
|
500
|
-
client.registerTools(
|
|
501
|
-
const toolHandler =
|
|
502
|
-
.find((call) => call[0] === '
|
|
495
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
496
|
+
const toolHandler = registerToolsSpy.mock.calls
|
|
497
|
+
.find((call) => call[0].title === 'List Projects')[1];
|
|
503
498
|
const result = await toolHandler({ page_size: 2, page: 1 });
|
|
504
499
|
const expectedResult = {
|
|
505
500
|
data: mockProjects.slice(0, 2),
|
|
@@ -510,9 +505,9 @@ describe('InsightHubClient', () => {
|
|
|
510
505
|
it('should return all projects when no pagination specified', async () => {
|
|
511
506
|
const mockProjects = [{ id: 'proj-1', name: 'Project 1' }];
|
|
512
507
|
mockCache.get.mockReturnValue(mockProjects);
|
|
513
|
-
client.registerTools(
|
|
514
|
-
const toolHandler =
|
|
515
|
-
.find((call) => call[0] === '
|
|
508
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
509
|
+
const toolHandler = registerToolsSpy.mock.calls
|
|
510
|
+
.find((call) => call[0].title === 'List Projects')[1];
|
|
516
511
|
const result = await toolHandler({});
|
|
517
512
|
const expectedResult = {
|
|
518
513
|
data: mockProjects,
|
|
@@ -522,9 +517,9 @@ describe('InsightHubClient', () => {
|
|
|
522
517
|
});
|
|
523
518
|
it('should handle no projects found', async () => {
|
|
524
519
|
mockCache.get.mockReturnValue([]);
|
|
525
|
-
client.registerTools(
|
|
526
|
-
const toolHandler =
|
|
527
|
-
.find((call) => call[0] === '
|
|
520
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
521
|
+
const toolHandler = registerToolsSpy.mock.calls
|
|
522
|
+
.find((call) => call[0].title === 'List Projects')[1];
|
|
528
523
|
const result = await toolHandler({});
|
|
529
524
|
expect(result.content[0].text).toBe('No projects found.');
|
|
530
525
|
});
|
|
@@ -535,9 +530,9 @@ describe('InsightHubClient', () => {
|
|
|
535
530
|
{ id: 'proj-3', name: 'Project 3' }
|
|
536
531
|
];
|
|
537
532
|
mockCache.get.mockReturnValue(mockProjects);
|
|
538
|
-
client.registerTools(
|
|
539
|
-
const toolHandler =
|
|
540
|
-
.find((call) => call[0] === '
|
|
533
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
534
|
+
const toolHandler = registerToolsSpy.mock.calls
|
|
535
|
+
.find((call) => call[0].title === 'List Projects')[1];
|
|
541
536
|
const result = await toolHandler({ page_size: 2 });
|
|
542
537
|
const expectedResult = {
|
|
543
538
|
data: mockProjects.slice(0, 2),
|
|
@@ -551,9 +546,9 @@ describe('InsightHubClient', () => {
|
|
|
551
546
|
name: `Project ${i + 1}`
|
|
552
547
|
}));
|
|
553
548
|
mockCache.get.mockReturnValue(mockProjects);
|
|
554
|
-
client.registerTools(
|
|
555
|
-
const toolHandler =
|
|
556
|
-
.find((call) => call[0] === '
|
|
549
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
550
|
+
const toolHandler = registerToolsSpy.mock.calls
|
|
551
|
+
.find((call) => call[0].title === 'List Projects')[1];
|
|
557
552
|
const result = await toolHandler({ page: 2 });
|
|
558
553
|
// Default page_size is 10, so page 2 should return projects 10-19
|
|
559
554
|
const expectedResult = {
|
|
@@ -563,7 +558,7 @@ describe('InsightHubClient', () => {
|
|
|
563
558
|
expect(result.content[0].text).toBe(JSON.stringify(expectedResult));
|
|
564
559
|
});
|
|
565
560
|
});
|
|
566
|
-
describe('
|
|
561
|
+
describe('get_error tool handler', () => {
|
|
567
562
|
it('should get error details with project from cache', async () => {
|
|
568
563
|
const mockProject = { id: 'proj-1', name: 'Project 1', slug: 'my-project' };
|
|
569
564
|
const mockError = { id: 'error-1', message: 'Test error' };
|
|
@@ -575,9 +570,9 @@ describe('InsightHubClient', () => {
|
|
|
575
570
|
mockErrorAPI.viewErrorOnProject.mockResolvedValue({ body: mockError });
|
|
576
571
|
mockErrorAPI.listEventsOnProject.mockResolvedValue({ body: mockEvents });
|
|
577
572
|
mockErrorAPI.listErrorPivots.mockResolvedValue({ body: mockPivots });
|
|
578
|
-
client.registerTools(
|
|
579
|
-
const toolHandler =
|
|
580
|
-
.find((call) => call[0] === '
|
|
573
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
574
|
+
const toolHandler = registerToolsSpy.mock.calls
|
|
575
|
+
.find((call) => call[0].title === 'Get Error')[1];
|
|
581
576
|
const result = await toolHandler({ errorId: 'error-1' });
|
|
582
577
|
const queryString = '?filters[error][][type]=eq&filters[error][][value]=error-1';
|
|
583
578
|
const encodedQueryString = encodeURI(queryString);
|
|
@@ -590,9 +585,9 @@ describe('InsightHubClient', () => {
|
|
|
590
585
|
}));
|
|
591
586
|
});
|
|
592
587
|
it('should throw error when required arguments missing', async () => {
|
|
593
|
-
client.registerTools(
|
|
594
|
-
const toolHandler =
|
|
595
|
-
.find((call) => call[0] === '
|
|
588
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
589
|
+
const toolHandler = registerToolsSpy.mock.calls
|
|
590
|
+
.find((call) => call[0].title === 'Get Error')[1];
|
|
596
591
|
await expect(toolHandler({})).rejects.toThrow('Both projectId and errorId arguments are required');
|
|
597
592
|
});
|
|
598
593
|
});
|
|
@@ -602,9 +597,9 @@ describe('InsightHubClient', () => {
|
|
|
602
597
|
const mockEvent = { id: 'event-1', project_id: 'proj-1' };
|
|
603
598
|
mockCache.get.mockReturnValue(mockProjects);
|
|
604
599
|
mockErrorAPI.viewEventById.mockResolvedValue({ body: mockEvent });
|
|
605
|
-
client.registerTools(
|
|
606
|
-
const toolHandler =
|
|
607
|
-
.find((call) => call[0] === '
|
|
600
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
601
|
+
const toolHandler = registerToolsSpy.mock.calls
|
|
602
|
+
.find((call) => call[0].title === 'Get Event Details')[1];
|
|
608
603
|
const result = await toolHandler({
|
|
609
604
|
link: 'https://app.bugsnag.com/my-org/my-project/errors/error-123?event_id=event-1'
|
|
610
605
|
});
|
|
@@ -612,30 +607,30 @@ describe('InsightHubClient', () => {
|
|
|
612
607
|
expect(result.content[0].text).toBe(JSON.stringify(mockEvent));
|
|
613
608
|
});
|
|
614
609
|
it('should throw error when link is invalid', async () => {
|
|
615
|
-
client.registerTools(
|
|
616
|
-
const toolHandler =
|
|
617
|
-
.find((call) => call[0] === '
|
|
610
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
611
|
+
const toolHandler = registerToolsSpy.mock.calls
|
|
612
|
+
.find((call) => call[0].title === 'Get Event Details')[1];
|
|
618
613
|
await expect(toolHandler({ link: 'invalid-url' })).rejects.toThrow();
|
|
619
614
|
});
|
|
620
615
|
it('should throw error when project not found', async () => {
|
|
621
616
|
mockCache.get.mockReturnValue([{ id: 'proj-1', slug: 'other-project', name: 'Other Project' }]);
|
|
622
|
-
client.registerTools(
|
|
623
|
-
const toolHandler =
|
|
624
|
-
.find((call) => call[0] === '
|
|
617
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
618
|
+
const toolHandler = registerToolsSpy.mock.calls
|
|
619
|
+
.find((call) => call[0].title === 'Get Event Details')[1];
|
|
625
620
|
await expect(toolHandler({
|
|
626
621
|
link: 'https://app.bugsnag.com/my-org/my-project/errors/error-123?event_id=event-1'
|
|
627
622
|
})).rejects.toThrow('Project with the specified slug not found.');
|
|
628
623
|
});
|
|
629
624
|
it('should throw error when URL is missing required parameters', async () => {
|
|
630
|
-
client.registerTools(
|
|
631
|
-
const toolHandler =
|
|
632
|
-
.find((call) => call[0] === '
|
|
625
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
626
|
+
const toolHandler = registerToolsSpy.mock.calls
|
|
627
|
+
.find((call) => call[0].title === 'Get Event Details')[1];
|
|
633
628
|
await expect(toolHandler({
|
|
634
629
|
link: 'https://app.bugsnag.com/my-org/my-project/errors/error-123' // Missing event_id
|
|
635
630
|
})).rejects.toThrow('Both projectSlug and eventId must be present in the link');
|
|
636
631
|
});
|
|
637
632
|
});
|
|
638
|
-
describe('
|
|
633
|
+
describe('list_project_errors tool handler', () => {
|
|
639
634
|
it('should list project errors with filters', async () => {
|
|
640
635
|
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
641
636
|
const mockEventFields = [
|
|
@@ -648,9 +643,9 @@ describe('InsightHubClient', () => {
|
|
|
648
643
|
.mockReturnValueOnce(mockProject) // current project
|
|
649
644
|
.mockReturnValueOnce(mockEventFields); // event fields
|
|
650
645
|
mockErrorAPI.listProjectErrors.mockResolvedValue({ body: mockErrors });
|
|
651
|
-
client.registerTools(
|
|
652
|
-
const toolHandler =
|
|
653
|
-
.find((call) => call[0] === '
|
|
646
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
647
|
+
const toolHandler = registerToolsSpy.mock.calls
|
|
648
|
+
.find((call) => call[0].title === 'List Project Errors')[1];
|
|
654
649
|
const result = await toolHandler({ filters });
|
|
655
650
|
expect(mockErrorAPI.listProjectErrors).toHaveBeenCalledWith('proj-1', { filters });
|
|
656
651
|
const expectedResult = {
|
|
@@ -666,16 +661,16 @@ describe('InsightHubClient', () => {
|
|
|
666
661
|
mockCache.get
|
|
667
662
|
.mockReturnValueOnce(mockProject)
|
|
668
663
|
.mockReturnValueOnce(mockEventFields);
|
|
669
|
-
client.registerTools(
|
|
670
|
-
const toolHandler =
|
|
671
|
-
.find((call) => call[0] === '
|
|
664
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
665
|
+
const toolHandler = registerToolsSpy.mock.calls
|
|
666
|
+
.find((call) => call[0].title === 'List Project Errors')[1];
|
|
672
667
|
await expect(toolHandler({ filters })).rejects.toThrow('Invalid filter key: invalid.field');
|
|
673
668
|
});
|
|
674
669
|
it('should throw error when no project ID available', async () => {
|
|
675
670
|
mockCache.get.mockReturnValue(null);
|
|
676
|
-
client.registerTools(
|
|
677
|
-
const toolHandler =
|
|
678
|
-
.find((call) => call[0] === '
|
|
671
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
672
|
+
const toolHandler = registerToolsSpy.mock.calls
|
|
673
|
+
.find((call) => call[0].title === 'List Project Errors')[1];
|
|
679
674
|
await expect(toolHandler({})).rejects.toThrow('No current project found. Please provide a projectId or configure a project API key.');
|
|
680
675
|
});
|
|
681
676
|
});
|
|
@@ -686,17 +681,17 @@ describe('InsightHubClient', () => {
|
|
|
686
681
|
{ display_id: 'user.email', custom: false }
|
|
687
682
|
];
|
|
688
683
|
mockCache.get.mockReturnValue(mockEventFields);
|
|
689
|
-
client.registerTools(
|
|
690
|
-
const toolHandler =
|
|
691
|
-
.find((call) => call[0] === '
|
|
684
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
685
|
+
const toolHandler = registerToolsSpy.mock.calls
|
|
686
|
+
.find((call) => call[0].title === 'List Project Event Filters')[1];
|
|
692
687
|
const result = await toolHandler({});
|
|
693
688
|
expect(result.content[0].text).toBe(JSON.stringify(mockEventFields));
|
|
694
689
|
});
|
|
695
690
|
it('should throw error when no event filters in cache', async () => {
|
|
696
691
|
mockCache.get.mockReturnValue(null);
|
|
697
|
-
client.registerTools(
|
|
698
|
-
const toolHandler =
|
|
699
|
-
.find((call) => call[0] === '
|
|
692
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
693
|
+
const toolHandler = registerToolsSpy.mock.calls
|
|
694
|
+
.find((call) => call[0].title === 'List Project Event Filters')[1];
|
|
700
695
|
await expect(toolHandler({})).rejects.toThrow('No event filters found in cache.');
|
|
701
696
|
});
|
|
702
697
|
});
|
|
@@ -705,9 +700,9 @@ describe('InsightHubClient', () => {
|
|
|
705
700
|
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
706
701
|
mockCache.get.mockReturnValue(mockProject);
|
|
707
702
|
mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 200 });
|
|
708
|
-
client.registerTools(
|
|
709
|
-
const toolHandler =
|
|
710
|
-
.find((call) => call[0] === '
|
|
703
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
704
|
+
const toolHandler = registerToolsSpy.mock.calls
|
|
705
|
+
.find((call) => call[0].title === 'Update Error')[1];
|
|
711
706
|
const result = await toolHandler({
|
|
712
707
|
errorId: 'error-1',
|
|
713
708
|
operation: 'fix'
|
|
@@ -720,9 +715,9 @@ describe('InsightHubClient', () => {
|
|
|
720
715
|
const mockProjects = [mockProject];
|
|
721
716
|
mockCache.get.mockReturnValue(mockProjects);
|
|
722
717
|
mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 204 });
|
|
723
|
-
client.registerTools(
|
|
724
|
-
const toolHandler =
|
|
725
|
-
.find((call) => call[0] === '
|
|
718
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
719
|
+
const toolHandler = registerToolsSpy.mock.calls
|
|
720
|
+
.find((call) => call[0].title === 'Update Error')[1];
|
|
726
721
|
const result = await toolHandler({
|
|
727
722
|
projectId: 'proj-1',
|
|
728
723
|
errorId: 'error-1',
|
|
@@ -737,9 +732,9 @@ describe('InsightHubClient', () => {
|
|
|
737
732
|
const operations = ['open', 'fix', 'ignore', 'discard', 'undiscard'];
|
|
738
733
|
mockCache.get.mockReturnValue(mockProject);
|
|
739
734
|
mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 200 });
|
|
740
|
-
client.registerTools(
|
|
741
|
-
const toolHandler =
|
|
742
|
-
.find((call) => call[0] === '
|
|
735
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
736
|
+
const toolHandler = registerToolsSpy.mock.calls
|
|
737
|
+
.find((call) => call[0].title === 'Update Error')[1];
|
|
743
738
|
for (const operation of operations) {
|
|
744
739
|
await toolHandler({
|
|
745
740
|
errorId: 'error-1',
|
|
@@ -751,25 +746,20 @@ describe('InsightHubClient', () => {
|
|
|
751
746
|
});
|
|
752
747
|
it('should handle override_severity operation with elicitInput', async () => {
|
|
753
748
|
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
action: 'accept',
|
|
759
|
-
content: { severity: 'warning' }
|
|
760
|
-
})
|
|
761
|
-
}
|
|
762
|
-
};
|
|
749
|
+
getInputFunctionSpy.mockResolvedValue({
|
|
750
|
+
action: 'accept',
|
|
751
|
+
content: { severity: 'warning' }
|
|
752
|
+
});
|
|
763
753
|
mockCache.get.mockReturnValue(mockProject);
|
|
764
754
|
mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 200 });
|
|
765
|
-
client.registerTools(
|
|
766
|
-
const toolHandler =
|
|
767
|
-
.find((call) => call[0] === '
|
|
755
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
756
|
+
const toolHandler = registerToolsSpy.mock.calls
|
|
757
|
+
.find((call) => call[0].title === 'Update Error')[1];
|
|
768
758
|
const result = await toolHandler({
|
|
769
759
|
errorId: 'error-1',
|
|
770
760
|
operation: 'override_severity'
|
|
771
761
|
});
|
|
772
|
-
expect(
|
|
762
|
+
expect(getInputFunctionSpy).toHaveBeenCalledWith({
|
|
773
763
|
message: "Please provide the new severity for the error (e.g. 'info', 'warning', 'error', 'critical')",
|
|
774
764
|
requestedSchema: {
|
|
775
765
|
type: "object",
|
|
@@ -788,19 +778,14 @@ describe('InsightHubClient', () => {
|
|
|
788
778
|
});
|
|
789
779
|
it('should handle override_severity operation when elicitInput is rejected', async () => {
|
|
790
780
|
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
elicitInput: vi.fn().mockResolvedValue({
|
|
795
|
-
action: 'reject'
|
|
796
|
-
})
|
|
797
|
-
}
|
|
798
|
-
};
|
|
781
|
+
getInputFunctionSpy.mockResolvedValue({
|
|
782
|
+
action: 'reject'
|
|
783
|
+
});
|
|
799
784
|
mockCache.get.mockReturnValue(mockProject);
|
|
800
785
|
mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 200 });
|
|
801
|
-
client.registerTools(
|
|
802
|
-
const toolHandler =
|
|
803
|
-
.find((call) => call[0] === '
|
|
786
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
787
|
+
const toolHandler = registerToolsSpy.mock.calls
|
|
788
|
+
.find((call) => call[0].title === 'Update Error')[1];
|
|
804
789
|
const result = await toolHandler({
|
|
805
790
|
errorId: 'error-1',
|
|
806
791
|
operation: 'override_severity'
|
|
@@ -812,9 +797,9 @@ describe('InsightHubClient', () => {
|
|
|
812
797
|
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
813
798
|
mockCache.get.mockReturnValue(mockProject);
|
|
814
799
|
mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 400 });
|
|
815
|
-
client.registerTools(
|
|
816
|
-
const toolHandler =
|
|
817
|
-
.find((call) => call[0] === '
|
|
800
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
801
|
+
const toolHandler = registerToolsSpy.mock.calls
|
|
802
|
+
.find((call) => call[0].title === 'Update Error')[1];
|
|
818
803
|
const result = await toolHandler({
|
|
819
804
|
errorId: 'error-1',
|
|
820
805
|
operation: 'fix'
|
|
@@ -823,9 +808,9 @@ describe('InsightHubClient', () => {
|
|
|
823
808
|
});
|
|
824
809
|
it('should throw error when no project found', async () => {
|
|
825
810
|
mockCache.get.mockReturnValue(null);
|
|
826
|
-
client.registerTools(
|
|
827
|
-
const toolHandler =
|
|
828
|
-
.find((call) => call[0] === '
|
|
811
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
812
|
+
const toolHandler = registerToolsSpy.mock.calls
|
|
813
|
+
.find((call) => call[0].title === 'Update Error')[1];
|
|
829
814
|
await expect(toolHandler({
|
|
830
815
|
errorId: 'error-1',
|
|
831
816
|
operation: 'fix'
|
|
@@ -834,74 +819,21 @@ describe('InsightHubClient', () => {
|
|
|
834
819
|
it('should throw error when project ID not found', async () => {
|
|
835
820
|
const mockProjects = [{ id: 'proj-1', name: 'Project 1' }];
|
|
836
821
|
mockCache.get.mockReturnValue(mockProjects);
|
|
837
|
-
client.registerTools(
|
|
838
|
-
const toolHandler =
|
|
839
|
-
.find((call) => call[0] === '
|
|
822
|
+
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
823
|
+
const toolHandler = registerToolsSpy.mock.calls
|
|
824
|
+
.find((call) => call[0].title === 'Update Error')[1];
|
|
840
825
|
await expect(toolHandler({
|
|
841
826
|
projectId: 'non-existent-project',
|
|
842
827
|
errorId: 'error-1',
|
|
843
828
|
operation: 'fix'
|
|
844
829
|
})).rejects.toThrow('Project with ID non-existent-project not found.');
|
|
845
830
|
});
|
|
846
|
-
it('should notify Bugsnag when API call fails', async () => {
|
|
847
|
-
const Bugsnag = (await import('../../../common/bugsnag.js')).default;
|
|
848
|
-
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
849
|
-
const mockError = new Error('API error');
|
|
850
|
-
mockCache.get.mockReturnValue(mockProject);
|
|
851
|
-
mockErrorAPI.updateErrorOnProject.mockRejectedValue(mockError);
|
|
852
|
-
client.registerTools(mockServer);
|
|
853
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
854
|
-
.find((call) => call[0] === 'update_error')[2];
|
|
855
|
-
await expect(toolHandler({
|
|
856
|
-
errorId: 'error-1',
|
|
857
|
-
operation: 'fix'
|
|
858
|
-
})).rejects.toThrow('API error');
|
|
859
|
-
expect(Bugsnag.notify).toHaveBeenCalledWith(mockError);
|
|
860
|
-
});
|
|
861
|
-
});
|
|
862
|
-
describe('error handling in tool handlers', () => {
|
|
863
|
-
it('should notify Bugsnag when error occurs in list_insight_hub_projects', async () => {
|
|
864
|
-
const Bugsnag = (await import('../../../common/bugsnag.js')).default;
|
|
865
|
-
const mockError = new Error('Test error');
|
|
866
|
-
mockCache.get.mockImplementation(() => {
|
|
867
|
-
throw mockError;
|
|
868
|
-
});
|
|
869
|
-
client.registerTools(mockServer);
|
|
870
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
871
|
-
.find((call) => call[0] === 'list_insight_hub_projects')[2];
|
|
872
|
-
await expect(toolHandler({})).rejects.toThrow('Test error');
|
|
873
|
-
expect(Bugsnag.notify).toHaveBeenCalledWith(mockError);
|
|
874
|
-
});
|
|
875
|
-
it('should notify Bugsnag when error occurs in get_insight_hub_error', async () => {
|
|
876
|
-
const Bugsnag = (await import('../../../common/bugsnag.js')).default;
|
|
877
|
-
const mockError = new Error('API error');
|
|
878
|
-
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
879
|
-
mockCache.get.mockReturnValue(mockProject);
|
|
880
|
-
mockErrorAPI.viewErrorOnProject.mockRejectedValue(mockError);
|
|
881
|
-
client.registerTools(mockServer);
|
|
882
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
883
|
-
.find((call) => call[0] === 'get_insight_hub_error')[2];
|
|
884
|
-
await expect(toolHandler({ errorId: 'error-1' })).rejects.toThrow('API error');
|
|
885
|
-
expect(Bugsnag.notify).toHaveBeenCalledWith(mockError);
|
|
886
|
-
});
|
|
887
|
-
it('should notify Bugsnag when error occurs in resource handler', async () => {
|
|
888
|
-
const Bugsnag = (await import('../../../common/bugsnag.js')).default;
|
|
889
|
-
const mockError = new Error('Resource error');
|
|
890
|
-
mockCache.get.mockRejectedValue(mockError);
|
|
891
|
-
client.registerResources(mockServer);
|
|
892
|
-
const resourceHandler = mockServer.resource.mock.calls[0][2];
|
|
893
|
-
await expect(resourceHandler({ href: 'insighthub://event/event-1' }, { id: 'event-1' })).rejects.toThrow('Resource error');
|
|
894
|
-
expect(Bugsnag.notify).toHaveBeenCalledWith(mockError);
|
|
895
|
-
});
|
|
896
831
|
});
|
|
897
832
|
});
|
|
898
833
|
describe('resource handlers', () => {
|
|
899
|
-
let
|
|
834
|
+
let registerResourcesSpy;
|
|
900
835
|
beforeEach(() => {
|
|
901
|
-
|
|
902
|
-
registerTool: vi.fn(),
|
|
903
|
-
resource: vi.fn()
|
|
904
|
-
};
|
|
836
|
+
registerResourcesSpy = vi.fn();
|
|
905
837
|
});
|
|
906
838
|
describe('insight_hub_event resource handler', () => {
|
|
907
839
|
it('should find event by ID across projects', async () => {
|
|
@@ -909,8 +841,8 @@ describe('InsightHubClient', () => {
|
|
|
909
841
|
const mockProjects = [{ id: 'proj-1', name: 'Project 1' }];
|
|
910
842
|
mockCache.get.mockReturnValueOnce(mockProjects);
|
|
911
843
|
mockErrorAPI.viewEventById.mockResolvedValue({ body: mockEvent });
|
|
912
|
-
client.registerResources(
|
|
913
|
-
const resourceHandler =
|
|
844
|
+
client.registerResources(registerResourcesSpy);
|
|
845
|
+
const resourceHandler = registerResourcesSpy.mock.calls[0][2];
|
|
914
846
|
const result = await resourceHandler({ href: 'insighthub://event/event-1' }, { id: 'event-1' });
|
|
915
847
|
expect(result.contents[0].uri).toBe('insighthub://event/event-1');
|
|
916
848
|
expect(result.contents[0].text).toBe(JSON.stringify(mockEvent));
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { RefineInputSchema } from "../../../pactflow/client/ai.js";
|
|
3
|
+
describe("AI zod schemas validation tests", () => {
|
|
4
|
+
it("Parses RefineInputSchema with partial input", () => {
|
|
5
|
+
const result = RefineInputSchema.safeParse({
|
|
6
|
+
pactTests: {
|
|
7
|
+
filename: "test.js",
|
|
8
|
+
language: "javascript",
|
|
9
|
+
body: "describe('API', () => { it('works', () => { }); });"
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
expect(result.success).toBe(true);
|
|
13
|
+
expect(result.data).toEqual({
|
|
14
|
+
pactTests: {
|
|
15
|
+
filename: "test.js",
|
|
16
|
+
language: "javascript",
|
|
17
|
+
body: "describe('API', () => { it('works', () => { }); });"
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { PactflowClient } from "../../../pactflow/client.js";
|
|
3
|
+
import * as toolsModule from "../../../pactflow/client/tools.js";
|
|
4
|
+
describe("PactflowClient.registerTools", () => {
|
|
5
|
+
const mockRegister = vi.fn();
|
|
6
|
+
const mockGetInput = vi.fn();
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.resetAllMocks();
|
|
9
|
+
});
|
|
10
|
+
it("registers only tools matching the given clientType", () => {
|
|
11
|
+
// Arrange — mock TOOLS with multiple client types
|
|
12
|
+
const fakeTools = [
|
|
13
|
+
{
|
|
14
|
+
title: "tool1",
|
|
15
|
+
summary: "summary1",
|
|
16
|
+
purpose: "purpose1",
|
|
17
|
+
parameters: [],
|
|
18
|
+
handler: "generate",
|
|
19
|
+
clients: ["pactflow"], // should be registered
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
title: "tool2",
|
|
23
|
+
summary: "summary2",
|
|
24
|
+
purpose: "purpose2",
|
|
25
|
+
parameters: [],
|
|
26
|
+
handler: "generate",
|
|
27
|
+
clients: ["pact_broker"], // should NOT be registered
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
vi.spyOn(toolsModule, "TOOLS", "get").mockReturnValue(fakeTools);
|
|
31
|
+
const client = new PactflowClient("token", "https://example.com", "pactflow");
|
|
32
|
+
client.registerTools(mockRegister, mockGetInput);
|
|
33
|
+
expect(mockRegister).toHaveBeenCalledTimes(1);
|
|
34
|
+
expect(mockRegister.mock.calls[0][0].title).toBe("tool1");
|
|
35
|
+
expect(mockRegister.mock.calls[0][0].summary).toBe("summary1");
|
|
36
|
+
});
|
|
37
|
+
it("registers no tools if none match the clientType", () => {
|
|
38
|
+
const fakeTools = [
|
|
39
|
+
{
|
|
40
|
+
title: "tool2",
|
|
41
|
+
summary: "summary2",
|
|
42
|
+
purpose: "purpose2",
|
|
43
|
+
parameters: [],
|
|
44
|
+
handler: "generate",
|
|
45
|
+
clients: ["pact_broker"],
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
vi.spyOn(toolsModule, "TOOLS", "get").mockReturnValue(fakeTools);
|
|
49
|
+
const client = new PactflowClient("token", "https://example.com", "pactflow");
|
|
50
|
+
client.registerTools(mockRegister, mockGetInput);
|
|
51
|
+
expect(mockRegister).not.toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
it("sets correct headers for pactflow", () => {
|
|
54
|
+
const client = new PactflowClient("my-token", "https://example.com", "pactflow");
|
|
55
|
+
expect(client["headers"]).toEqual(expect.objectContaining({
|
|
56
|
+
Authorization: expect.stringContaining("Bearer my-token"),
|
|
57
|
+
"Content-Type": expect.stringContaining("application/json"),
|
|
58
|
+
}));
|
|
59
|
+
});
|
|
60
|
+
it("sets correct headers for pact_broker", () => {
|
|
61
|
+
const client = new PactflowClient({ username: "user", password: "pass" }, "https://example.com", "pact_broker");
|
|
62
|
+
expect(client["headers"]).toEqual(expect.objectContaining({
|
|
63
|
+
Authorization: expect.stringContaining(`Basic ${Buffer.from("user:pass").toString("base64")}`),
|
|
64
|
+
"Content-Type": expect.stringContaining("application/json"),
|
|
65
|
+
}));
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { TOOLS } from "../../../pactflow/client/tools.js";
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
describe("TOOLS definition for 'Generate Pact Tests'", () => {
|
|
4
|
+
it("defines the generate pact tests tool with correct metadata", () => {
|
|
5
|
+
const tool = TOOLS.find((t) => t.title === "Generate Pact Tests");
|
|
6
|
+
expect(tool).toBeDefined();
|
|
7
|
+
expect(tool?.summary).toMatch(/Generate Pact tests using PactFlow AI/);
|
|
8
|
+
expect(tool?.clients).toEqual(["pactflow"]);
|
|
9
|
+
expect(tool?.handler).toBe("generate");
|
|
10
|
+
});
|
|
11
|
+
it("enforces presence of matcher when openapi input is provided", () => {
|
|
12
|
+
const tool = TOOLS.find((t) => t.title === "Generate Pact Tests");
|
|
13
|
+
const schema = tool?.zodSchema;
|
|
14
|
+
const openapiSchema = schema.shape["openapi"];
|
|
15
|
+
expect(openapiSchema).toBeDefined();
|
|
16
|
+
const invalidOpenapi = {
|
|
17
|
+
openapi: {
|
|
18
|
+
document: {
|
|
19
|
+
openapi: "3.0.0",
|
|
20
|
+
paths: {},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
expect(() => openapiSchema?.parse(invalidOpenapi.openapi)).toThrow(/matcher/);
|
|
25
|
+
});
|
|
26
|
+
it("rejects unsupported language values in the language field", () => {
|
|
27
|
+
const tool = TOOLS.find((t) => t.title === "Generate Pact Tests");
|
|
28
|
+
const schema = tool?.zodSchema;
|
|
29
|
+
const languageSchema = schema.shape["language"];
|
|
30
|
+
expect(languageSchema).toBeDefined();
|
|
31
|
+
const invalidData = "ruby"; // not in enum
|
|
32
|
+
expect(() => languageSchema?.parse(invalidData)).toThrow(/Invalid enum value/);
|
|
33
|
+
});
|
|
34
|
+
});
|