@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.
@@ -441,55 +441,50 @@ describe('InsightHubClient', () => {
441
441
  });
442
442
  });
443
443
  describe('tool registration', () => {
444
- let mockServer;
444
+ let registerToolsSpy;
445
+ let getInputFunctionSpy;
445
446
  beforeEach(() => {
446
- mockServer = {
447
- registerTool: vi.fn(),
448
- resource: vi.fn()
449
- };
447
+ registerToolsSpy = vi.fn();
448
+ getInputFunctionSpy = vi.fn();
450
449
  });
451
- it('should register list_insight_hub_projects tool when no project API key', () => {
452
- client.registerTools(mockServer);
453
- expect(mockServer.registerTool).toHaveBeenCalledWith('list_insight_hub_projects', expect.any(Object), expect.any(Function));
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 list_insight_hub_projects tool when project API key is provided', () => {
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(mockServer);
458
- const registeredTools = mockServer.registerTool.mock.calls.map((call) => call[0]);
459
- expect(registeredTools).not.toContain('list_insight_hub_projects');
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(mockServer);
463
- const registeredTools = mockServer.registerTool.mock.calls.map((call) => call[0]);
464
- expect(registeredTools).toContain('get_insight_hub_error');
465
- expect(registeredTools).toContain('get_insight_hub_event_details');
466
- expect(registeredTools).toContain('list_insight_hub_project_errors');
467
- expect(registeredTools).toContain('get_project_event_filters');
468
- expect(registeredTools).toContain('update_error');
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 mockServer;
471
+ let registerResourcesSpy;
473
472
  beforeEach(() => {
474
- mockServer = {
475
- registerTool: vi.fn(),
476
- resource: vi.fn()
477
- };
473
+ registerResourcesSpy = vi.fn();
478
474
  });
479
- it('should register insight_hub_event resource', () => {
480
- client.registerResources(mockServer);
481
- expect(mockServer.resource).toHaveBeenCalledWith('insight_hub_event', expect.any(Object), expect.any(Function));
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 mockServer;
481
+ let registerToolsSpy;
482
+ let getInputFunctionSpy;
486
483
  beforeEach(() => {
487
- mockServer = {
488
- registerTool: vi.fn(),
489
- resource: vi.fn()
490
- };
484
+ registerToolsSpy = vi.fn();
485
+ getInputFunctionSpy = vi.fn();
491
486
  });
492
- describe('list_insight_hub_projects tool handler', () => {
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(mockServer);
501
- const toolHandler = mockServer.registerTool.mock.calls
502
- .find((call) => call[0] === 'list_insight_hub_projects')[2];
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(mockServer);
514
- const toolHandler = mockServer.registerTool.mock.calls
515
- .find((call) => call[0] === 'list_insight_hub_projects')[2];
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(mockServer);
526
- const toolHandler = mockServer.registerTool.mock.calls
527
- .find((call) => call[0] === 'list_insight_hub_projects')[2];
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(mockServer);
539
- const toolHandler = mockServer.registerTool.mock.calls
540
- .find((call) => call[0] === 'list_insight_hub_projects')[2];
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(mockServer);
555
- const toolHandler = mockServer.registerTool.mock.calls
556
- .find((call) => call[0] === 'list_insight_hub_projects')[2];
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('get_insight_hub_error tool handler', () => {
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(mockServer);
579
- const toolHandler = mockServer.registerTool.mock.calls
580
- .find((call) => call[0] === 'get_insight_hub_error')[2];
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(mockServer);
594
- const toolHandler = mockServer.registerTool.mock.calls
595
- .find((call) => call[0] === 'get_insight_hub_error')[2];
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(mockServer);
606
- const toolHandler = mockServer.registerTool.mock.calls
607
- .find((call) => call[0] === 'get_insight_hub_event_details')[2];
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(mockServer);
616
- const toolHandler = mockServer.registerTool.mock.calls
617
- .find((call) => call[0] === 'get_insight_hub_event_details')[2];
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(mockServer);
623
- const toolHandler = mockServer.registerTool.mock.calls
624
- .find((call) => call[0] === 'get_insight_hub_event_details')[2];
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(mockServer);
631
- const toolHandler = mockServer.registerTool.mock.calls
632
- .find((call) => call[0] === 'get_insight_hub_event_details')[2];
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('list_insight_hub_project_errors tool handler', () => {
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(mockServer);
652
- const toolHandler = mockServer.registerTool.mock.calls
653
- .find((call) => call[0] === 'list_insight_hub_project_errors')[2];
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(mockServer);
670
- const toolHandler = mockServer.registerTool.mock.calls
671
- .find((call) => call[0] === 'list_insight_hub_project_errors')[2];
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(mockServer);
677
- const toolHandler = mockServer.registerTool.mock.calls
678
- .find((call) => call[0] === 'list_insight_hub_project_errors')[2];
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(mockServer);
690
- const toolHandler = mockServer.registerTool.mock.calls
691
- .find((call) => call[0] === 'get_project_event_filters')[2];
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(mockServer);
698
- const toolHandler = mockServer.registerTool.mock.calls
699
- .find((call) => call[0] === 'get_project_event_filters')[2];
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(mockServer);
709
- const toolHandler = mockServer.registerTool.mock.calls
710
- .find((call) => call[0] === 'update_error')[2];
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(mockServer);
724
- const toolHandler = mockServer.registerTool.mock.calls
725
- .find((call) => call[0] === 'update_error')[2];
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(mockServer);
741
- const toolHandler = mockServer.registerTool.mock.calls
742
- .find((call) => call[0] === 'update_error')[2];
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
- const mockServerWithElicitInput = {
755
- registerTool: vi.fn(),
756
- server: {
757
- elicitInput: vi.fn().mockResolvedValue({
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(mockServerWithElicitInput);
766
- const toolHandler = mockServerWithElicitInput.registerTool.mock.calls
767
- .find((call) => call[0] === 'update_error')[2];
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(mockServerWithElicitInput.server.elicitInput).toHaveBeenCalledWith({
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
- const mockServerWithElicitInput = {
792
- registerTool: vi.fn(),
793
- server: {
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(mockServerWithElicitInput);
802
- const toolHandler = mockServerWithElicitInput.registerTool.mock.calls
803
- .find((call) => call[0] === 'update_error')[2];
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(mockServer);
816
- const toolHandler = mockServer.registerTool.mock.calls
817
- .find((call) => call[0] === 'update_error')[2];
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(mockServer);
827
- const toolHandler = mockServer.registerTool.mock.calls
828
- .find((call) => call[0] === 'update_error')[2];
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(mockServer);
838
- const toolHandler = mockServer.registerTool.mock.calls
839
- .find((call) => call[0] === 'update_error')[2];
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 mockServer;
834
+ let registerResourcesSpy;
900
835
  beforeEach(() => {
901
- mockServer = {
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(mockServer);
913
- const resourceHandler = mockServer.resource.mock.calls[0][2];
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
+ });