This is page 3 of 3. Use http://codebase.md/circleci-public/mcp-server-circleci?page={x} to view the full context. # Directory Structure ``` ├── .circleci │ └── config.yml ├── .dockerignore ├── .github │ ├── CODEOWNERS │ ├── ISSUE_TEMPLATE │ │ ├── BUG.yml │ │ └── FEATURE_REQUEST.yml │ └── PULL_REQUEST_TEMPLATE │ └── PULL_REQUEST.md ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── eslint.config.js ├── LICENSE ├── package.json ├── pnpm-lock.yaml ├── README.md ├── renovate.json ├── scripts │ └── create-tool.js ├── smithery.yaml ├── src │ ├── circleci-tools.ts │ ├── clients │ │ ├── circleci │ │ │ ├── configValidate.ts │ │ │ ├── deploys.ts │ │ │ ├── httpClient.test.ts │ │ │ ├── httpClient.ts │ │ │ ├── index.ts │ │ │ ├── insights.ts │ │ │ ├── jobs.ts │ │ │ ├── jobsV1.ts │ │ │ ├── pipelines.ts │ │ │ ├── projects.ts │ │ │ ├── tests.ts │ │ │ ├── usage.ts │ │ │ └── workflows.ts │ │ ├── circleci-private │ │ │ ├── index.ts │ │ │ ├── jobsPrivate.ts │ │ │ └── me.ts │ │ ├── circlet │ │ │ ├── circlet.ts │ │ │ └── index.ts │ │ ├── client.ts │ │ └── schemas.ts │ ├── index.ts │ ├── lib │ │ ├── flaky-tests │ │ │ └── getFlakyTests.ts │ │ ├── getWorkflowIdFromURL.test.ts │ │ ├── getWorkflowIdFromURL.ts │ │ ├── latest-pipeline │ │ │ ├── formatLatestPipelineStatus.ts │ │ │ └── getLatestPipelineWorkflows.ts │ │ ├── mcpErrorOutput.test.ts │ │ ├── mcpErrorOutput.ts │ │ ├── mcpResponse.test.ts │ │ ├── mcpResponse.ts │ │ ├── outputTextTruncated.test.ts │ │ ├── outputTextTruncated.ts │ │ ├── pipeline-job-logs │ │ │ ├── getJobLogs.ts │ │ │ └── getPipelineJobLogs.ts │ │ ├── pipeline-job-tests │ │ │ ├── formatJobTests.ts │ │ │ └── getJobTests.ts │ │ ├── project-detection │ │ │ ├── index.test.ts │ │ │ ├── index.ts │ │ │ └── vcsTool.ts │ │ ├── rateLimitedRequests │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ └── usage-api │ │ ├── findUnderusedResourceClasses.test.ts │ │ ├── findUnderusedResourceClasses.ts │ │ ├── getUsageApiData.test.ts │ │ ├── getUsageApiData.ts │ │ └── parseDateTimeString.ts │ ├── tools │ │ ├── analyzeDiff │ │ │ ├── handler.test.ts │ │ │ ├── handler.ts │ │ │ ├── inputSchema.ts │ │ │ └── tool.ts │ │ ├── configHelper │ │ │ ├── handler.test.ts │ │ │ ├── handler.ts │ │ │ ├── inputSchema.ts │ │ │ └── tool.ts │ │ ├── createPromptTemplate │ │ │ ├── handler.test.ts │ │ │ ├── handler.ts │ │ │ ├── inputSchema.ts │ │ │ └── tool.ts │ │ ├── downloadUsageApiData │ │ │ ├── handler.test.ts │ │ │ ├── handler.ts │ │ │ ├── inputSchema.ts │ │ │ └── tool.ts │ │ ├── findUnderusedResourceClasses │ │ │ ├── handler.test.ts │ │ │ ├── handler.ts │ │ │ ├── inputSchema.ts │ │ │ └── tool.ts │ │ ├── getBuildFailureLogs │ │ │ ├── handler.test.ts │ │ │ ├── handler.ts │ │ │ ├── inputSchema.ts │ │ │ └── tool.ts │ │ ├── getFlakyTests │ │ │ ├── handler.test.ts │ │ │ ├── handler.ts │ │ │ ├── inputSchema.ts │ │ │ └── tool.ts │ │ ├── getJobTestResults │ │ │ ├── handler.test.ts │ │ │ ├── handler.ts │ │ │ ├── inputSchema.ts │ │ │ └── tool.ts │ │ ├── getLatestPipelineStatus │ │ │ ├── handler.test.ts │ │ │ ├── handler.ts │ │ │ ├── inputSchema.ts │ │ │ └── tool.ts │ │ ├── listComponentVersions │ │ │ ├── handler.test.ts │ │ │ ├── handler.ts │ │ │ ├── inputSchema.ts │ │ │ └── tool.ts │ │ ├── listFollowedProjects │ │ │ ├── handler.test.ts │ │ │ ├── handler.ts │ │ │ ├── inputSchema.ts │ │ │ └── tool.ts │ │ ├── recommendPromptTemplateTests │ │ │ ├── handler.test.ts │ │ │ ├── handler.ts │ │ │ ├── inputSchema.ts │ │ │ └── tool.ts │ │ ├── rerunWorkflow │ │ │ ├── handler.test.ts │ │ │ ├── handler.ts │ │ │ ├── inputSchema.ts │ │ │ └── tool.ts │ │ ├── runEvaluationTests │ │ │ ├── handler.test.ts │ │ │ ├── handler.ts │ │ │ ├── inputSchema.ts │ │ │ └── tool.ts │ │ ├── runPipeline │ │ │ ├── handler.test.ts │ │ │ ├── handler.ts │ │ │ ├── inputSchema.ts │ │ │ └── tool.ts │ │ ├── runRollbackPipeline │ │ │ ├── handler.test.ts │ │ │ ├── handler.ts │ │ │ ├── inputSchema.ts │ │ │ └── tool.ts │ │ └── shared │ │ └── constants.ts │ └── transports │ ├── stdio.ts │ └── unified.ts ├── tsconfig.json ├── tsconfig.test.json └── vitest.config.js ``` # Files -------------------------------------------------------------------------------- /src/tools/analyzeDiff/handler.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it, expect, vi, beforeEach } from 'vitest'; import { FilterBy } from '../shared/constants.js'; import { analyzeDiff } from './handler.js'; import { analyzeDiffInputSchema } from './inputSchema.js'; import { CircletClient } from '../../clients/circlet/index.js'; import { RuleReview } from '../../clients/schemas.js'; // Mock the CircletClient vi.mock('../../clients/circlet/index.js'); describe('analyzeDiff', () => { beforeEach(() => { vi.clearAllMocks(); }); it('should return no rules message when rules is an empty string', async () => { const mockCircletInstance = { circlet: { ruleReview: vi.fn(), }, }; vi.mocked(CircletClient).mockImplementation( () => mockCircletInstance as any, ); const mockArgs = { params: { speedMode: false, filterBy: FilterBy.none, diff: 'diff --git a/test.ts b/test.ts\n+console.log("test");', rules: '', }, }; const controller = new AbortController(); const result = await analyzeDiff(mockArgs, { signal: controller.signal }); expect(result).toEqual({ content: [ { type: 'text', text: 'No rules found. Please add rules to your repository.', }, ], }); }); it('should return no diff message when diff is an empty string', async () => { const mockCircletInstance = { circlet: { ruleReview: vi.fn(), }, }; vi.mocked(CircletClient).mockImplementation( () => mockCircletInstance as any, ); const mockArgs = { params: { speedMode: false, filterBy: FilterBy.none, diff: '', rules: '', }, }; const controller = new AbortController(); const result = await analyzeDiff(mockArgs, { signal: controller.signal }); expect(result).toEqual({ content: [ { type: 'text', text: 'No diff found. Please provide a diff to analyze.', }, ], }); }); it('should handle complex diff content with multiple rules', async () => { const mockRuleReview: RuleReview = { isRuleCompliant: true, relatedRules: { compliant: [ { rule: 'Rule 1: No console.log statements', reason: 'No console.log statements found', confidenceScore: 0.95, }, ], violations: [], requiresHumanReview: [], }, unrelatedRules: [], }; const mockCircletInstance = { circlet: { ruleReview: vi.fn().mockResolvedValue(mockRuleReview), }, }; vi.mocked(CircletClient).mockImplementation( () => mockCircletInstance as any, ); const mockArgs = { params: { speedMode: false, filterBy: FilterBy.none, diff: `diff --git a/src/component.ts b/src/component.ts index 1234567..abcdefg 100644 --- a/src/component.ts +++ b/src/component.ts @@ -1,5 +1,8 @@ export class Component { + private data: any = {}; + constructor() { + console.log("Component created"); } }`, rules: `Rule 1: No console.log statements Rule 2: Avoid using 'any' type Rule 3: Use proper TypeScript types --- Rule 4: All functions must have JSDoc comments`, }, }; const controller = new AbortController(); const result = await analyzeDiff(mockArgs, { signal: controller.signal }); expect(mockCircletInstance.circlet.ruleReview).toHaveBeenCalledWith({ speedMode: false, filterBy: FilterBy.none, diff: mockArgs.params.diff, rules: mockArgs.params.rules, }); expect(result).toEqual({ content: [ { type: 'text', text: 'All rules are compliant.', }, ], }); }); it('should handle multiline rules and preserve formatting', async () => { const mockRuleReview: RuleReview = { isRuleCompliant: false, relatedRules: { compliant: [], violations: [ { rule: 'No Console Logs', reason: 'Console.log statements found in code', confidenceScore: 0.98, violationInstances: [ { file: 'src/component.ts', lineNumbersInDiff: ['2'], violatingCodeSnippet: 'console.log(x);', explanationOfViolation: 'Direct console.log usage', }, ], }, ], requiresHumanReview: [], }, unrelatedRules: [], }; const mockCircletInstance = { circlet: { ruleReview: vi.fn().mockResolvedValue(mockRuleReview), }, }; vi.mocked(CircletClient).mockImplementation( () => mockCircletInstance as any, ); const mockArgs = { params: { speedMode: false, filterBy: FilterBy.none, diff: '+const x = 5;\n+console.log(x);', rules: `# IDE Rules Example ## Rule: No Console Logs Description: Remove all console.log statements before committing code. ## Rule: TypeScript Safety Description: Avoid using 'any' type.`, }, }; const controller = new AbortController(); const result = await analyzeDiff(mockArgs, { signal: controller.signal }); expect(mockCircletInstance.circlet.ruleReview).toHaveBeenCalledWith({ speedMode: false, filterBy: FilterBy.none, diff: mockArgs.params.diff, rules: mockArgs.params.rules, }); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('Rule: No Console Logs'); expect(result.content[0].text).toContain( 'Reason: Console.log statements found in code', ); expect(result.content[0].text).toContain('Confidence Score: 0.98'); }); it('should return compliant message when all rules are followed', async () => { const mockRuleReview: RuleReview = { isRuleCompliant: true, relatedRules: { compliant: [ { rule: 'No console.log statements', reason: 'Code follows proper logging practices', confidenceScore: 0.95, }, ], violations: [], requiresHumanReview: [], }, unrelatedRules: [], }; const mockCircletInstance = { circlet: { ruleReview: vi.fn().mockResolvedValue(mockRuleReview), }, }; vi.mocked(CircletClient).mockImplementation( () => mockCircletInstance as any, ); const mockArgs = { params: { speedMode: false, filterBy: FilterBy.none, diff: 'diff --git a/test.ts b/test.ts\n+const logger = new Logger();', rules: 'Rule 1: No console.log statements\nRule 2: Use proper logging', }, }; const controller = new AbortController(); const result = await analyzeDiff(mockArgs, { signal: controller.signal }); expect(mockCircletInstance.circlet.ruleReview).toHaveBeenCalledWith({ speedMode: false, filterBy: FilterBy.none, diff: mockArgs.params.diff, rules: mockArgs.params.rules, }); expect(result).toEqual({ content: [ { type: 'text', text: 'All rules are compliant.', }, ], }); }); it('should return formatted violations when rules are violated', async () => { const mockRuleReview: RuleReview = { isRuleCompliant: false, relatedRules: { compliant: [], violations: [ { rule: 'No console.log statements', reason: 'Console.log statements found in the code', confidenceScore: 0.98, violationInstances: [ { file: 'src/component.ts', lineNumbersInDiff: ['5'], violatingCodeSnippet: 'console.log("test");', explanationOfViolation: 'Direct console.log usage', }, ], }, { rule: 'Avoid using any type', reason: 'Any type usage reduces type safety', confidenceScore: 0.92, violationInstances: [ { file: 'src/component.ts', lineNumbersInDiff: ['3'], violatingCodeSnippet: 'private data: any = {};', explanationOfViolation: 'Variable declared with any type', }, ], }, ], requiresHumanReview: [], }, unrelatedRules: [], }; const mockCircletInstance = { circlet: { ruleReview: vi.fn().mockResolvedValue(mockRuleReview), }, }; vi.mocked(CircletClient).mockImplementation( () => mockCircletInstance as any, ); const mockArgs = { params: { speedMode: false, filterBy: FilterBy.none, diff: `diff --git a/src/component.ts b/src/component.ts index 1234567..abcdefg 100644 --- a/src/component.ts +++ b/src/component.ts @@ -1,5 +1,8 @@ export class Component { + private data: any = {}; + constructor() { + console.log("Component created"); } }`, rules: `Rule 1: No console.log statements Rule 2: Avoid using 'any' type Rule 3: Use proper TypeScript types`, }, }; const controller = new AbortController(); const result = await analyzeDiff(mockArgs, { signal: controller.signal }); expect(mockCircletInstance.circlet.ruleReview).toHaveBeenCalledWith({ filterBy: FilterBy.none, speedMode: false, diff: mockArgs.params.diff, rules: mockArgs.params.rules, }); expect(result).toEqual({ content: [ { type: 'text', text: `Rule: No console.log statements Reason: Console.log statements found in the code Confidence Score: 0.98 Rule: Avoid using any type Reason: Any type usage reduces type safety Confidence Score: 0.92`, }, ], }); }); it('should handle single violation correctly', async () => { const mockRuleReview: RuleReview = { isRuleCompliant: false, relatedRules: { compliant: [], violations: [ { rule: 'No magic numbers', reason: 'Magic numbers make code less maintainable', confidenceScore: 0.85, violationInstances: [ { file: 'src/component.ts', lineNumbersInDiff: ['2'], violatingCodeSnippet: 'const timeout = 5000;', explanationOfViolation: 'Hardcoded timeout value', }, ], }, ], requiresHumanReview: [], }, unrelatedRules: [], }; const mockCircletInstance = { circlet: { ruleReview: vi.fn().mockResolvedValue(mockRuleReview), }, }; vi.mocked(CircletClient).mockImplementation( () => mockCircletInstance as any, ); const mockArgs = { params: { speedMode: false, filterBy: FilterBy.none, diff: '+const timeout = 5000;', rules: 'Rule: No magic numbers', }, }; const controller = new AbortController(); const result = await analyzeDiff(mockArgs, { signal: controller.signal }); expect(result).toEqual({ content: [ { type: 'text', text: `Rule: No magic numbers Reason: Magic numbers make code less maintainable Confidence Score: 0.85`, }, ], }); }); it('should set default values for speedMode and filterBy when not provided', async () => { const mockRuleReview: RuleReview = { isRuleCompliant: true, relatedRules: { compliant: [], violations: [], requiresHumanReview: [], }, unrelatedRules: [], }; const mockCircletInstance = { circlet: { ruleReview: vi.fn().mockResolvedValue(mockRuleReview), }, }; vi.mocked(CircletClient).mockImplementation( () => mockCircletInstance as any, ); const rawParams = { diff: '+const timeout = 5000;', rules: 'Rule: No magic numbers', }; const parsedParams = analyzeDiffInputSchema.parse(rawParams); const mockArgs = { params: parsedParams, }; const controller = new AbortController(); await analyzeDiff(mockArgs, { signal: controller.signal }); // Verify default values (filterBy: FilterBy.none & speedMode: false) are applied when not explictly stated expect(mockCircletInstance.circlet.ruleReview).toHaveBeenCalledWith({ diff: rawParams.diff, rules: rawParams.rules, filterBy: FilterBy.none, speedMode: false, }); }); }); ``` -------------------------------------------------------------------------------- /src/tools/runPipeline/handler.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it, expect, vi, beforeEach } from 'vitest'; import { runPipeline } from './handler.js'; import * as projectDetection from '../../lib/project-detection/index.js'; import * as clientModule from '../../clients/client.js'; vi.mock('../../lib/project-detection/index.js'); vi.mock('../../clients/client.js'); describe('runPipeline handler', () => { const mockCircleCIClient = { projects: { getProject: vi.fn(), }, pipelines: { getPipelineDefinitions: vi.fn(), runPipeline: vi.fn(), }, }; beforeEach(() => { vi.resetAllMocks(); vi.spyOn(clientModule, 'getCircleCIClient').mockReturnValue( mockCircleCIClient as any, ); }); it('should return a valid MCP error response when no inputs are provided', async () => { const args = { params: {}, } as any; const controller = new AbortController(); const response = await runPipeline(args, { signal: controller.signal, }); expect(response).toHaveProperty('content'); expect(response).toHaveProperty('isError', true); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(typeof response.content[0].text).toBe('string'); }); it('should return a valid MCP error response when project is not found', async () => { vi.spyOn(projectDetection, 'identifyProjectSlug').mockResolvedValue( undefined, ); const args = { params: { workspaceRoot: '/workspace', gitRemoteURL: 'https://github.com/org/repo.git', branch: 'main', }, } as any; const controller = new AbortController(); const response = await runPipeline(args, { signal: controller.signal, }); expect(response).toHaveProperty('content'); expect(response).toHaveProperty('isError', true); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(typeof response.content[0].text).toBe('string'); }); it('should return a valid MCP error response when no branch is provided', async () => { vi.spyOn(projectDetection, 'getProjectSlugFromURL').mockReturnValue( 'gh/org/repo', ); vi.spyOn(projectDetection, 'getBranchFromURL').mockReturnValue(undefined); const args = { params: { projectURL: 'https://app.circleci.com/pipelines/gh/org/repo', }, } as any; const controller = new AbortController(); const response = await runPipeline(args, { signal: controller.signal, }); expect(response).toHaveProperty('content'); expect(response).toHaveProperty('isError', true); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(typeof response.content[0].text).toBe('string'); expect(response.content[0].text).toContain('No branch provided'); }); it('should return a valid MCP error response when projectSlug is provided without branch', async () => { const args = { params: { projectSlug: 'gh/org/repo', // No branch provided }, } as any; const controller = new AbortController(); const response = await runPipeline(args, { signal: controller.signal, }); expect(response).toHaveProperty('content'); expect(response).toHaveProperty('isError', true); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(response.content[0].text).toContain('Branch not provided'); // Verify that CircleCI API was not called expect(mockCircleCIClient.projects.getProject).not.toHaveBeenCalled(); expect( mockCircleCIClient.pipelines.getPipelineDefinitions, ).not.toHaveBeenCalled(); expect(mockCircleCIClient.pipelines.runPipeline).not.toHaveBeenCalled(); }); it('should return a valid MCP error response when no pipeline definitions are found', async () => { vi.spyOn(projectDetection, 'getProjectSlugFromURL').mockReturnValue( 'gh/org/repo', ); vi.spyOn(projectDetection, 'getBranchFromURL').mockReturnValue('main'); mockCircleCIClient.projects.getProject.mockResolvedValue({ id: 'project-id', }); mockCircleCIClient.pipelines.getPipelineDefinitions.mockResolvedValue([]); const args = { params: { projectURL: 'https://app.circleci.com/pipelines/gh/org/repo', }, } as any; const controller = new AbortController(); const response = await runPipeline(args, { signal: controller.signal, }); expect(response).toHaveProperty('content'); expect(response).toHaveProperty('isError', true); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(typeof response.content[0].text).toBe('string'); expect(response.content[0].text).toContain('No pipeline definitions found'); }); it('should return a list of pipeline choices when multiple pipeline definitions are found and no choice is provided', async () => { vi.spyOn(projectDetection, 'getProjectSlugFromURL').mockReturnValue( 'gh/org/repo', ); vi.spyOn(projectDetection, 'getBranchFromURL').mockReturnValue('main'); mockCircleCIClient.projects.getProject.mockResolvedValue({ id: 'project-id', }); mockCircleCIClient.pipelines.getPipelineDefinitions.mockResolvedValue([ { id: 'def1', name: 'Pipeline 1' }, { id: 'def2', name: 'Pipeline 2' }, ]); const args = { params: { projectURL: 'https://app.circleci.com/pipelines/gh/org/repo', }, } as any; const controller = new AbortController(); const response = await runPipeline(args, { signal: controller.signal, }); expect(response).toHaveProperty('content'); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(typeof response.content[0].text).toBe('string'); expect(response.content[0].text).toContain( 'Multiple pipeline definitions found', ); expect(response.content[0].text).toContain('Pipeline 1'); expect(response.content[0].text).toContain('Pipeline 2'); }); it('should return an error when an invalid pipeline choice is provided', async () => { vi.spyOn(projectDetection, 'getProjectSlugFromURL').mockReturnValue( 'gh/org/repo', ); vi.spyOn(projectDetection, 'getBranchFromURL').mockReturnValue('main'); mockCircleCIClient.projects.getProject.mockResolvedValue({ id: 'project-id', }); mockCircleCIClient.pipelines.getPipelineDefinitions.mockResolvedValue([ { id: 'def1', name: 'Pipeline 1' }, { id: 'def2', name: 'Pipeline 2' }, ]); const args = { params: { projectURL: 'https://app.circleci.com/pipelines/gh/org/repo', pipelineChoiceName: 'Non-existent Pipeline', }, } as any; const controller = new AbortController(); const response = await runPipeline(args, { signal: controller.signal, }); expect(response).toHaveProperty('content'); expect(response).toHaveProperty('isError', true); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(typeof response.content[0].text).toBe('string'); expect(response.content[0].text).toContain( 'Pipeline definition with name Non-existent Pipeline not found', ); }); it('should run a pipeline with a specific choice when valid pipeline choice is provided', async () => { vi.spyOn(projectDetection, 'getProjectSlugFromURL').mockReturnValue( 'gh/org/repo', ); vi.spyOn(projectDetection, 'getBranchFromURL').mockReturnValue('main'); mockCircleCIClient.projects.getProject.mockResolvedValue({ id: 'project-id', }); mockCircleCIClient.pipelines.getPipelineDefinitions.mockResolvedValue([ { id: 'def1', name: 'Pipeline 1' }, { id: 'def2', name: 'Pipeline 2' }, ]); mockCircleCIClient.pipelines.runPipeline.mockResolvedValue({ number: 123, state: 'pending', id: 'pipeline-id', }); const args = { params: { projectURL: 'https://app.circleci.com/pipelines/gh/org/repo', pipelineChoiceName: 'Pipeline 2', }, } as any; const controller = new AbortController(); const response = await runPipeline(args, { signal: controller.signal, }); expect(response).toHaveProperty('content'); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(typeof response.content[0].text).toBe('string'); expect(response.content[0].text).toContain('Pipeline run successfully'); expect(mockCircleCIClient.pipelines.runPipeline).toHaveBeenCalledWith({ projectSlug: 'gh/org/repo', branch: 'main', definitionId: 'def2', }); }); it('should run a pipeline with the first choice when only one pipeline definition is found', async () => { vi.spyOn(projectDetection, 'getProjectSlugFromURL').mockReturnValue( 'gh/org/repo', ); vi.spyOn(projectDetection, 'getBranchFromURL').mockReturnValue('main'); mockCircleCIClient.projects.getProject.mockResolvedValue({ id: 'project-id', }); mockCircleCIClient.pipelines.getPipelineDefinitions.mockResolvedValue([ { id: 'def1', name: 'Pipeline 1' }, ]); mockCircleCIClient.pipelines.runPipeline.mockResolvedValue({ number: 123, state: 'pending', id: 'pipeline-id', }); const args = { params: { projectURL: 'https://app.circleci.com/pipelines/gh/org/repo', }, } as any; const controller = new AbortController(); const response = await runPipeline(args, { signal: controller.signal, }); expect(response).toHaveProperty('content'); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(typeof response.content[0].text).toBe('string'); expect(response.content[0].text).toContain('Pipeline run successfully'); expect(mockCircleCIClient.pipelines.runPipeline).toHaveBeenCalledWith({ projectSlug: 'gh/org/repo', branch: 'main', definitionId: 'def1', }); }); it('should detect project from git remote and run pipeline', async () => { vi.spyOn(projectDetection, 'identifyProjectSlug').mockResolvedValue( 'gh/org/repo', ); mockCircleCIClient.projects.getProject.mockResolvedValue({ id: 'project-id', }); mockCircleCIClient.pipelines.getPipelineDefinitions.mockResolvedValue([ { id: 'def1', name: 'Pipeline 1' }, ]); mockCircleCIClient.pipelines.runPipeline.mockResolvedValue({ number: 123, state: 'pending', id: 'pipeline-id', }); const args = { params: { workspaceRoot: '/workspace', gitRemoteURL: 'https://github.com/org/repo.git', branch: 'feature-branch', }, } as any; const controller = new AbortController(); const response = await runPipeline(args, { signal: controller.signal, }); expect(response).toHaveProperty('content'); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(typeof response.content[0].text).toBe('string'); expect(response.content[0].text).toContain('Pipeline run successfully'); expect(mockCircleCIClient.pipelines.runPipeline).toHaveBeenCalledWith({ projectSlug: 'gh/org/repo', branch: 'feature-branch', definitionId: 'def1', }); }); it('should run a pipeline using projectSlug and branch inputs correctly', async () => { mockCircleCIClient.projects.getProject.mockResolvedValue({ id: 'project-id', }); mockCircleCIClient.pipelines.getPipelineDefinitions.mockResolvedValue([ { id: 'def1', name: 'Pipeline 1' }, ]); mockCircleCIClient.pipelines.runPipeline.mockResolvedValue({ number: 123, state: 'pending', id: 'pipeline-id', }); const args = { params: { projectSlug: 'gh/org/repo', branch: 'feature/new-feature', }, } as any; const controller = new AbortController(); const response = await runPipeline(args, { signal: controller.signal, }); expect(mockCircleCIClient.projects.getProject).toHaveBeenCalledWith({ projectSlug: 'gh/org/repo', }); expect(mockCircleCIClient.pipelines.runPipeline).toHaveBeenCalledWith({ projectSlug: 'gh/org/repo', branch: 'feature/new-feature', definitionId: 'def1', }); expect(response).toHaveProperty('content'); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(typeof response.content[0].text).toBe('string'); expect(response.content[0].text).toContain('Pipeline run successfully'); }); }); ``` -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.14.1] - 2025-09-17 ### Fixed - Output log messages to stderr instead of stdout, following MCP server specification. This avoids MCP server startup issues in some agents, such as Warp. Fixes #125 ## [0.14.0] - 2025-08-11 ### Added - Added `download_usage_api_data` tool to start and retrieve CircleCI usage export jobs - Added `find_underused_resource_classes` tool to analyze usage CSV for underused resource classes (default threshold 40%) ## [0.13.0] - 2025-08-05 ### Added - Added `listComponentVersions` tool to list all versions for a CircleCI component ### Changed - Simplified `runRollbackPipeline` tool by moving part of its inner logic to `listComponentVersions`. ## [0.12.2] - 2025-08-01 ### Added - Added support for rerunning workflow in `runRollbackPipeline` when no rollback pipeline is defined. ## [0.12.1] - 2025-07-30 ### Fixed - Fixed `runRollbackPipeline` tool to not suggest other projects ## [0.12.0] - 2025-07-29 ### Added - Added `runRollbackPipeline` tool to run a rollback pipeline ## [0.11.4] - 2025-07-28 ### Fixed - Remove `tool/` prefix from tool list entries, this was breaking tool name resolution in some MCP clients. ## [0.11.3] - 2025-07-24 ### Changed - Make the diff reviewer tool work with other IDE based rule systems. ## [0.11.2] - 2025-06-20 ### Added - Add `file` property to the `RuleReviewSchema` to align with updated endpoint ## [0.11.1] - 2025-06-18 ### Fixed - Fixed bug in `get_flaky_tests` tool where unrelated tests were being returned - Fixed bug in `get_flaky_tests` tool where if the output directory cannot be created the tool would respond with an error. We now throw in that case, which makes us fallback to the text output. ## [0.11.0] - 2025-06-18 ### Fixed - Fixed bug in `get_flaky_tests` tool where the same job number was being fetched multiple times - Fixed bug in `get_flaky_tests` where the output directory was not being created when using file output mode ## [0.10.2] - 2025-06-18 ### Added - Add `speedMode` and `filterBy` parameters to the `analyze_diff` tool ## [0.10.1] - 2025-06-17 ### Fixed - Add a .gitignore file to the flaky-tests-output directory to ignore all files in the directory ## [0.10.0] - 2025-06-17 ### Added - Added `USE_FILE_OUTPUT` environment variable to `get_flaky_tests` tool - When set to `true`, the tool will write flaky tests to files in the `./flaky-tests-output` directory instead of returning the results in the response - The tool will return the file paths of the written files in the response ## [0.9.2] - 2025-06-17 ### Added - Anthropic support on prompt eval script (w. auto-detection for OpenAI and Anthropic models) - Added `temperature` parameter support to prompt template tools - Enhanced `create_prompt_template` tool with configurable temperature setting - Enhanced `recommend_prompt_template_tests` tool with temperature parameter - Default temperature value set to 1.0 for consistent prompt template generation ### Updated - Updated default model from `gpt-4o-mini` to `gpt-4.1-mini` for prompt template tools - Enhanced evaluation script dependencies for improved compatibility - Updated `deepeval` to version 3.0.3+ (from 2.8.2+) - Updated `openai` to version 1.84.0+ (from 1.76.2+) - Added `anthropic` version 0.54.0+ for Anthropic model support - Updated `PyYAML` to version 6.0.2+ ## [0.9.1] - 2025-06-12 ### Added - Added `analyze_diff` tool to analyze git diffs against cursor rules to identify rule violations - Evaluates code changes against repository coding standards and best practices - Provides detailed violation reports with confidence scores and explanations - Supports both staged and unstaged changes and all changes analysis - Returns actionable feedback for maintaining code quality consistency ## [0.9.0] - 2025-06-03 ### Added - Added `run_evaluation_tests` tool to run evaluation tests on CircleCI pipelines - Support for running prompt template evaluation tests in CircleCI - Integration with prompt template files from `./prompts` directory - Dynamic CircleCI configuration generation for evaluation workflows - Support for multiple prompt files with automatic parallelism configuration - Compatible with both JSON and YAML prompt template formats - Comprehensive error handling and validation for prompt template files - Enhanced `runPipeline` API to support custom configuration content - Added `configContent` parameter to override default pipeline configuration - Enables dynamic pipeline configuration for specialized use cases ## [0.8.1] - 2025-05-28 ### Added - Enhanced prompt template tools with support for existing codebase prompts - Added `promptOrigin` parameter to distinguish between new requirements and existing codebase prompts - Added `model` parameter to specify target model for testing (defaults to gpt-4o-mini) - Enhanced documentation and examples for prompt template creation - Added integration guidance for codebase-sourced prompts - Improved prompt templates file location, naming conventions, and structure ## [0.8.0] - 2025-05-22 ### Added - Added `rerun_workflow` tool to rerun a workflow from its start or from the failed job ## [0.7.1] - 2025-05-14 ### Updated - Updated `get_build_failure_logs`, `get_job_test_results`, and `get_latest_pipeline_status` tools to require a branch parameter when using projectSlug option ## [0.7.0] - 2025-05-13 ### Added - Added `list_followed_projects` tool to list all projects that the user is following on CircleCI ## [0.6.2] - 2025-05-13 ### Fixed - Fixed `get_job_test_results` tool to filter tests by result when a job number is provided ## [0.6.1] - 2025-05-13 ### Updated - Updated `get_build_failure_logs` tool to support legacy job url format like `https://circleci.com/gh/organization/project/123` ## [0.6.0] - 2025-05-13 ### Added - Added `filterByTestsResult` parameter to `get_job_test_results` tool - Filter the tests by result - Support for filtering by `failure` or `success` ## [0.5.1] - 2025-05-12 ### Added - Fix handling of legacy job url format in tools - Fix handling of pagination of test results when no test results are found ## [0.5.0] - 2025-05-09 ### Added - Added `run_pipeline` tool to run a pipeline - Support for triggering pipelines using project URL or local git repository context - Branch detection from URLs or local git context - Handles multiple pipeline definitions with interactive selection - Provides direct link to monitor pipeline execution ## [0.4.4] - 2025-05-08 ### Fixed - Fixed project detection and pipeline number extraction from URLs with custom server domains ## [0.4.3] - 2025-05-08 ### Fixed - Fixed project detection when branch is provided in URL but not in params - Improved error handling for failed pipeline workflow fetches - Enhanced error messaging when project is not found or inputs are missing ## [0.4.2] - 2025-05-08 ### Improvements - Enhanced prompt template file structure and organization for consistency - Added standardized file naming convention for prompt templates - Implemented structured JSON format with required fields (name, description, version, template, contextSchema, tests, sampleInputs, etc.) - Added support for test case naming in Title Case format - Improved documentation requirements for prompt templates ## [0.4.1] - 2025-05-05 ### Added - Update project detection to correctly paginate the followed projects ## [0.4.0] - 2025-04-30 ### Added - Added `get_job_test_results` tool to retrieve and analyze test metadata from CircleCI jobs - Support for retrieving test results using job, workflow, or pipeline URLs - Support for retrieving test results using local git repository context - Displays comprehensive test result summary (total, successful, failed) - Provides detailed information for failed tests including name, class, file, error messages, and runtime - Lists successful tests with timing information - Offers actionable guidance when no test results are found - Includes documentation link to help users properly configure test metadata collection ## [0.3.0] - 2025-04-30 ### Added - Added `get_latest_pipeline_status` tool to get the latest pipeline status - Support for both project URL and local git repository context - Displays all workflows within the latest pipeline - Provides formatted details including pipeline number, workflow status, duration, and timestamps ## [0.2.0] - 2025-04-18 ### Added - Added `create_prompt_template` tool to help generate structured prompt templates - Converts feature requirements into optimized prompt templates - Generates context schema for input parameters - Enables building robust AI-powered features - Integrates with prompt template testing workflow - Added `recommend_prompt_template_tests` tool for prompt template validation - Creates diverse test scenarios based on templates - Generates test cases with varied parameter combinations - Helps identify edge cases and potential issues - Ensures consistent AI responses across inputs ## [0.1.10] - 2025-04-17 ### Fixed - Fixed rate limiting issues when fetching job logs and flaky tests (#32) - Implemented `rateLimitedRequests` utility for controlled API request batching - Added configurable batch size and interval controls - Improved error handling for rate-limited responses - Added progress tracking for batch operations - Applied rate limiting fix to both job logs and flaky test detection - Enhanced reliability of test results retrieval ### Improvements - Enhanced HTTP client configuration flexibility - Configurable base URL through environment variables - Better support for different CircleCI deployment scenarios - Streamlined client initialization process - Added output text truncation - Prevents response overload by limiting output size - Includes clear warning when content is truncated - Preserves most recent and relevant information ## [0.1.9] - 2025-04-16 ### Added - Added support for API subdomain configuration in CircleCI client - New `useAPISubdomain` option in HTTP client configuration - Automatic subdomain handling for API-specific endpoints - Improved support for CircleCI enterprise and on-premise installations - Added `config_helper` tool to assist with CircleCI configuration tasks - Support for validating .circleci/config.yml files - Integration with CircleCI Config Validation API - Detailed validation results and configuration recommendations - Helpful error messages and best practice suggestions ## [0.1.8] - 2025-04-10 ### Fixed - Fixed bug in flaky test detection where pipelineNumber was incorrectly used instead of projectSlug when URL not provided ### Improvements - Consolidated project slug detection functions into a single `getPipelineNumberFromURL` function with enhanced test coverage - Simplified build logs tool to use only `projectURL` parameter instead of separate pipeline and job URLs - Updated tool descriptions to provide clearer guidance on accepted URL formats - Removed redundant error handling wrapper ## [0.1.7] - 2025-04-10 ### Added - Added `find_flaky_tests` tool to identify and analyze flaky tests in CircleCI projects - Support for both project URL and local git repository context - Integration with CircleCI Insights API for flaky test detection - Integration with CircleCI Tests API to fetch detailed test execution results - Formatted output of flaky test analysis results with complete test logs ## [0.1.6] - 2025-04-09 ### Added - Added User-Agent header to CircleCI API requests ## [0.1.5] - 2025-04-08 ### Added - Support for configurable CircleCI base URL through `CIRCLECI_BASE_URL` environment variable ## [0.1.4] - 2025-04-08 ### Fixed - Handle missing job numbers in CircleCI API responses by making job_number optional in schema - Skip jobs without job numbers when fetching job logs instead of failing ## [0.1.3] - 2025-04-04 ### Added - Improved schema validation and output formatting for job information ## [0.1.2] - 2025-04-04 ### Fixed - More permissive schema validation for CircleCI API parameters - Allow optional parameters in API requests ### Documentation - Updated documentation around package publishing - Removed note about package not being published ## [0.1.1] - 2025-04-04 ### Fixed - Non functional fixes ## [0.1.0] - 2025-04-04 Initial release of the CircleCI MCP Server, enabling natural language interactions with CircleCI functionality through MCP-enabled clients. ### Added - Core MCP server implementation with CircleCI integration - Support for MCP protocol version 1.8.0 - Robust error handling and response formatting - Standardized HTTP client for CircleCI API interactions - CircleCI API Integration - Support for both CircleCI API v1.1 and v2 - Comprehensive API client implementation for Jobs, Pipelines, and Workflows - Private API integration for enhanced functionality - Secure token-based authentication - Build Failure Analysis Tool - Implemented `get_build_failure_logs` tool for retrieving detailed failure logs - Support for both URL-based and local project context-based queries - Intelligent project detection from git repository information - Formatted log output with job names and step-by-step execution details - Development Tools and Infrastructure - Comprehensive test suite with Vitest - ESLint and Prettier configuration for code quality - TypeScript configuration for type safety - Development workflow with MCP Inspector support - Watch mode for rapid development ### Security - Secure handling of CircleCI API tokens - Masked sensitive data in log outputs - Proper error handling to prevent information leakage ``` -------------------------------------------------------------------------------- /src/tools/runEvaluationTests/handler.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it, expect, vi, beforeEach } from 'vitest'; import { runEvaluationTests } from './handler.js'; import * as projectDetection from '../../lib/project-detection/index.js'; import * as clientModule from '../../clients/client.js'; vi.mock('../../lib/project-detection/index.js'); vi.mock('../../clients/client.js'); describe('runEvaluationTests handler', () => { const mockCircleCIClient = { projects: { getProject: vi.fn(), }, pipelines: { getPipelineDefinitions: vi.fn(), runPipeline: vi.fn(), }, }; beforeEach(() => { vi.resetAllMocks(); vi.spyOn(clientModule, 'getCircleCIClient').mockReturnValue( mockCircleCIClient as any, ); }); it('should return a valid MCP error response when no inputs are provided', async () => { const args = { params: {}, } as any; const controller = new AbortController(); const response = await runEvaluationTests(args, { signal: controller.signal, }); expect(response).toHaveProperty('content'); expect(response).toHaveProperty('isError', true); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(typeof response.content[0].text).toBe('string'); }); it('should return a valid MCP error response when project is not found', async () => { vi.spyOn(projectDetection, 'identifyProjectSlug').mockResolvedValue( undefined, ); const args = { params: { workspaceRoot: '/workspace', gitRemoteURL: 'https://github.com/org/repo.git', branch: 'main', promptFiles: [ { fileName: 'test.prompt.yml', fileContent: 'test: content', }, ], }, } as any; const controller = new AbortController(); const response = await runEvaluationTests(args, { signal: controller.signal, }); expect(response).toHaveProperty('content'); expect(response).toHaveProperty('isError', true); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(typeof response.content[0].text).toBe('string'); }); it('should return a valid MCP error response when no branch is provided', async () => { vi.spyOn(projectDetection, 'getProjectSlugFromURL').mockReturnValue( 'gh/org/repo', ); vi.spyOn(projectDetection, 'getBranchFromURL').mockReturnValue(undefined); const args = { params: { projectURL: 'https://app.circleci.com/pipelines/gh/org/repo', promptFiles: [ { fileName: 'test.prompt.yml', fileContent: 'test: content', }, ], }, } as any; const controller = new AbortController(); const response = await runEvaluationTests(args, { signal: controller.signal, }); expect(response).toHaveProperty('content'); expect(response).toHaveProperty('isError', true); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(typeof response.content[0].text).toBe('string'); expect(response.content[0].text).toContain('No branch provided'); }); it('should return a valid MCP error response when projectSlug is provided without branch', async () => { const args = { params: { projectSlug: 'gh/org/repo', promptFiles: [ { fileName: 'test.prompt.yml', fileContent: 'test: content', }, ], // No branch provided }, } as any; const controller = new AbortController(); const response = await runEvaluationTests(args, { signal: controller.signal, }); expect(response).toHaveProperty('content'); expect(response).toHaveProperty('isError', true); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(response.content[0].text).toContain('Branch not provided'); // Verify that CircleCI API was not called expect(mockCircleCIClient.projects.getProject).not.toHaveBeenCalled(); expect( mockCircleCIClient.pipelines.getPipelineDefinitions, ).not.toHaveBeenCalled(); expect(mockCircleCIClient.pipelines.runPipeline).not.toHaveBeenCalled(); }); it('should return a valid MCP error response when no prompt files are provided', async () => { const args = { params: { projectSlug: 'gh/org/repo', branch: 'main', promptFiles: [], // Empty array }, } as any; const controller = new AbortController(); const response = await runEvaluationTests(args, { signal: controller.signal, }); expect(response).toHaveProperty('content'); expect(response).toHaveProperty('isError', true); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(response.content[0].text).toContain( 'No prompt template files provided', ); expect(response.content[0].text).toContain('./prompts directory'); // Verify that CircleCI API was not called expect(mockCircleCIClient.projects.getProject).not.toHaveBeenCalled(); expect( mockCircleCIClient.pipelines.getPipelineDefinitions, ).not.toHaveBeenCalled(); expect(mockCircleCIClient.pipelines.runPipeline).not.toHaveBeenCalled(); }); it('should return a valid MCP error response when promptFiles is undefined', async () => { const args = { params: { projectSlug: 'gh/org/repo', branch: 'main', // promptFiles is undefined }, } as any; const controller = new AbortController(); const response = await runEvaluationTests(args, { signal: controller.signal, }); expect(response).toHaveProperty('content'); expect(response).toHaveProperty('isError', true); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(response.content[0].text).toContain( 'No prompt template files provided', ); expect(response.content[0].text).toContain('./prompts directory'); // Verify that CircleCI API was not called expect(mockCircleCIClient.projects.getProject).not.toHaveBeenCalled(); expect( mockCircleCIClient.pipelines.getPipelineDefinitions, ).not.toHaveBeenCalled(); expect(mockCircleCIClient.pipelines.runPipeline).not.toHaveBeenCalled(); }); it('should return a valid MCP error response when no pipeline definitions are found', async () => { vi.spyOn(projectDetection, 'getProjectSlugFromURL').mockReturnValue( 'gh/org/repo', ); vi.spyOn(projectDetection, 'getBranchFromURL').mockReturnValue('main'); mockCircleCIClient.projects.getProject.mockResolvedValue({ id: 'project-id', }); mockCircleCIClient.pipelines.getPipelineDefinitions.mockResolvedValue([]); const args = { params: { projectURL: 'https://app.circleci.com/pipelines/gh/org/repo', promptFiles: [ { fileName: 'test.prompt.yml', fileContent: 'test: content', }, ], }, } as any; const controller = new AbortController(); const response = await runEvaluationTests(args, { signal: controller.signal, }); expect(response).toHaveProperty('content'); expect(response).toHaveProperty('isError', true); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(typeof response.content[0].text).toBe('string'); expect(response.content[0].text).toContain('No pipeline definitions found'); }); it('should return a list of pipeline choices when multiple pipeline definitions are found and no choice is provided', async () => { vi.spyOn(projectDetection, 'getProjectSlugFromURL').mockReturnValue( 'gh/org/repo', ); vi.spyOn(projectDetection, 'getBranchFromURL').mockReturnValue('main'); mockCircleCIClient.projects.getProject.mockResolvedValue({ id: 'project-id', }); mockCircleCIClient.pipelines.getPipelineDefinitions.mockResolvedValue([ { id: 'def1', name: 'Pipeline 1' }, { id: 'def2', name: 'Pipeline 2' }, ]); const args = { params: { projectURL: 'https://app.circleci.com/pipelines/gh/org/repo', promptFiles: [ { fileName: 'test.prompt.yml', fileContent: 'test: content', }, ], }, } as any; const controller = new AbortController(); const response = await runEvaluationTests(args, { signal: controller.signal, }); expect(response).toHaveProperty('content'); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(typeof response.content[0].text).toBe('string'); expect(response.content[0].text).toContain( 'Multiple pipeline definitions found', ); expect(response.content[0].text).toContain('Pipeline 1'); expect(response.content[0].text).toContain('Pipeline 2'); }); it('should return an error when an invalid pipeline choice is provided', async () => { vi.spyOn(projectDetection, 'getProjectSlugFromURL').mockReturnValue( 'gh/org/repo', ); vi.spyOn(projectDetection, 'getBranchFromURL').mockReturnValue('main'); mockCircleCIClient.projects.getProject.mockResolvedValue({ id: 'project-id', }); mockCircleCIClient.pipelines.getPipelineDefinitions.mockResolvedValue([ { id: 'def1', name: 'Pipeline 1' }, { id: 'def2', name: 'Pipeline 2' }, ]); const args = { params: { projectURL: 'https://app.circleci.com/pipelines/gh/org/repo', pipelineChoiceName: 'Non-existent Pipeline', promptFiles: [ { fileName: 'test.prompt.yml', fileContent: 'test: content', }, ], }, } as any; const controller = new AbortController(); const response = await runEvaluationTests(args, { signal: controller.signal, }); expect(response).toHaveProperty('content'); expect(response).toHaveProperty('isError', true); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(typeof response.content[0].text).toBe('string'); expect(response.content[0].text).toContain( 'Pipeline definition with name Non-existent Pipeline not found', ); }); it('should run evaluation tests with multiple prompt files and correct parallelism', async () => { vi.spyOn(projectDetection, 'getProjectSlugFromURL').mockReturnValue( 'gh/org/repo', ); vi.spyOn(projectDetection, 'getBranchFromURL').mockReturnValue('main'); mockCircleCIClient.projects.getProject.mockResolvedValue({ id: 'project-id', }); mockCircleCIClient.pipelines.getPipelineDefinitions.mockResolvedValue([ { id: 'def1', name: 'Pipeline 1' }, ]); mockCircleCIClient.pipelines.runPipeline.mockResolvedValue({ number: 123, state: 'pending', id: 'pipeline-id', }); const args = { params: { projectURL: 'https://app.circleci.com/pipelines/gh/org/repo', promptFiles: [ { fileName: 'test1.prompt.json', fileContent: '{"template": "test content 1"}', }, { fileName: 'test2.prompt.yml', fileContent: 'template: test content 2', }, ], }, } as any; const controller = new AbortController(); const response = await runEvaluationTests(args, { signal: controller.signal, }); expect(response).toHaveProperty('content'); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(typeof response.content[0].text).toBe('string'); expect(response.content[0].text).toContain('Pipeline run successfully'); // Verify that the pipeline was called with correct configuration expect(mockCircleCIClient.pipelines.runPipeline).toHaveBeenCalledWith({ projectSlug: 'gh/org/repo', branch: 'main', definitionId: 'def1', configContent: expect.stringContaining('parallelism: 2'), // Should match number of files }); // Verify the config contains conditional file creation logic const configContent = mockCircleCIClient.pipelines.runPipeline.mock.calls[0][0].configContent; expect(configContent).toContain('CIRCLE_NODE_INDEX'); expect(configContent).toContain('test1.prompt.json'); expect(configContent).toContain('test2.prompt.yml'); expect(configContent).toContain('python eval.py'); }); it('should process JSON files with proper formatting', async () => { vi.spyOn(projectDetection, 'getProjectSlugFromURL').mockReturnValue( 'gh/org/repo', ); vi.spyOn(projectDetection, 'getBranchFromURL').mockReturnValue('main'); mockCircleCIClient.projects.getProject.mockResolvedValue({ id: 'project-id', }); mockCircleCIClient.pipelines.getPipelineDefinitions.mockResolvedValue([ { id: 'def1', name: 'Pipeline 1' }, ]); mockCircleCIClient.pipelines.runPipeline.mockResolvedValue({ number: 123, state: 'pending', id: 'pipeline-id', }); const args = { params: { projectSlug: 'gh/org/repo', branch: 'main', promptFiles: [ { fileName: 'test.prompt.json', fileContent: '{"template":"test","vars":["a","b"]}', }, ], }, } as any; const controller = new AbortController(); await runEvaluationTests(args, { signal: controller.signal, }); // Verify that the pipeline was called expect(mockCircleCIClient.pipelines.runPipeline).toHaveBeenCalled(); const configContent = mockCircleCIClient.pipelines.runPipeline.mock.calls[0][0].configContent; expect(configContent).toContain('parallelism: 1'); expect(configContent).toContain('test.prompt.json'); }); it('should detect project from git remote and run evaluation tests', async () => { vi.spyOn(projectDetection, 'identifyProjectSlug').mockResolvedValue( 'gh/org/repo', ); mockCircleCIClient.projects.getProject.mockResolvedValue({ id: 'project-id', }); mockCircleCIClient.pipelines.getPipelineDefinitions.mockResolvedValue([ { id: 'def1', name: 'Pipeline 1' }, ]); mockCircleCIClient.pipelines.runPipeline.mockResolvedValue({ number: 123, state: 'pending', id: 'pipeline-id', }); const args = { params: { workspaceRoot: '/workspace', gitRemoteURL: 'https://github.com/org/repo.git', branch: 'feature-branch', promptFiles: [ { fileName: 'test.prompt.yml', fileContent: 'template: test content', }, ], }, } as any; const controller = new AbortController(); const response = await runEvaluationTests(args, { signal: controller.signal, }); expect(response).toHaveProperty('content'); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(typeof response.content[0].text).toBe('string'); expect(response.content[0].text).toContain('Pipeline run successfully'); expect(mockCircleCIClient.pipelines.runPipeline).toHaveBeenCalledWith({ projectSlug: 'gh/org/repo', branch: 'feature-branch', definitionId: 'def1', configContent: expect.any(String), }); }); }); ``` -------------------------------------------------------------------------------- /src/tools/listComponentVersions/handler.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it, expect, vi, beforeEach } from 'vitest'; import { listComponentVersions } from './handler.js'; import * as clientModule from '../../clients/client.js'; vi.mock('../../clients/client.js'); describe('listComponentVersions handler', () => { const mockCircleCIClient = { deploys: { fetchComponentVersions: vi.fn(), fetchEnvironments: vi.fn(), fetchProjectComponents: vi.fn(), }, projects: { getProject: vi.fn(), getProjectByID: vi.fn(), }, }; const mockExtra = { signal: new AbortController().signal, requestId: 'test-id', sendNotification: vi.fn(), sendRequest: vi.fn(), }; beforeEach(() => { vi.resetAllMocks(); vi.spyOn(clientModule, 'getCircleCIClient').mockReturnValue( mockCircleCIClient as any, ); }); it('should return the formatted component versions when found', async () => { const mockComponentVersions = { items: [ { id: 'version-1', component_id: 'test-component-id', environment_id: 'test-environment-id', version: '1.0.0', sha: 'abc123', created_at: '2023-01-01T00:00:00Z', updated_at: '2023-01-02T00:00:00Z', is_live: true, }, { id: 'version-2', component_id: 'test-component-id', environment_id: 'test-environment-id', version: '1.1.0', sha: 'def456', created_at: '2023-01-03T00:00:00Z', updated_at: '2023-01-04T00:00:00Z', is_live: false, }, ], next_page_token: null, }; // Mock project resolution mockCircleCIClient.projects.getProject.mockResolvedValue({ id: 'test-project-id', organization_id: 'test-org-id', }); mockCircleCIClient.deploys.fetchComponentVersions.mockResolvedValue(mockComponentVersions); const args = { params: { projectSlug: 'gh/test-org/test-repo', componentID: 'test-component-id', environmentID: 'test-environment-id', }, } as any; const response = await listComponentVersions(args, mockExtra); expect(response).toHaveProperty('content'); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(typeof response.content[0].text).toBe('string'); expect(response.content[0].text).toContain('Versions for the component:'); expect(response.content[0].text).toContain(JSON.stringify(mockComponentVersions)); expect(mockCircleCIClient.projects.getProject).toHaveBeenCalledTimes(1); expect(mockCircleCIClient.projects.getProject).toHaveBeenCalledWith({ projectSlug: 'gh/test-org/test-repo', }); expect(mockCircleCIClient.deploys.fetchComponentVersions).toHaveBeenCalledTimes(1); expect(mockCircleCIClient.deploys.fetchComponentVersions).toHaveBeenCalledWith({ componentID: 'test-component-id', environmentID: 'test-environment-id', }); }); it('should return "No component versions found" when component versions list is empty', async () => { // Mock project resolution mockCircleCIClient.projects.getProject.mockResolvedValue({ id: 'test-project-id', organization_id: 'test-org-id', }); mockCircleCIClient.deploys.fetchComponentVersions.mockResolvedValue({ items: [], next_page_token: null, }); const args = { params: { projectSlug: 'gh/test-org/test-repo', componentID: 'test-component-id', environmentID: 'test-environment-id', }, } as any; const response = await listComponentVersions(args, mockExtra); expect(response).toHaveProperty('content'); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(typeof response.content[0].text).toBe('string'); expect(response.content[0].text).toBe('No component versions found'); }); it('should handle API errors gracefully', async () => { const errorMessage = 'Component versions API request failed'; // Mock project resolution mockCircleCIClient.projects.getProject.mockResolvedValue({ id: 'test-project-id', organization_id: 'test-org-id', }); mockCircleCIClient.deploys.fetchComponentVersions.mockRejectedValue( new Error(errorMessage), ); const args = { params: { projectSlug: 'gh/test-org/test-repo', componentID: 'test-component-id', environmentID: 'test-environment-id', }, } as any; const response = await listComponentVersions(args, mockExtra); expect(response).toHaveProperty('content'); expect(response).toHaveProperty('isError', true); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(typeof response.content[0].text).toBe('string'); expect(response.content[0].text).toContain('Failed to list component versions:'); expect(response.content[0].text).toContain(errorMessage); }); it('should handle non-Error exceptions gracefully', async () => { // Mock project resolution mockCircleCIClient.projects.getProject.mockResolvedValue({ id: 'test-project-id', organization_id: 'test-org-id', }); mockCircleCIClient.deploys.fetchComponentVersions.mockRejectedValue( 'Unexpected error', ); const args = { params: { projectSlug: 'gh/test-org/test-repo', componentID: 'test-component-id', environmentID: 'test-environment-id', }, } as any; const response = await listComponentVersions(args, mockExtra); expect(response).toHaveProperty('content'); expect(response).toHaveProperty('isError', true); expect(Array.isArray(response.content)).toBe(true); expect(response.content[0]).toHaveProperty('type', 'text'); expect(typeof response.content[0].text).toBe('string'); expect(response.content[0].text).toContain('Failed to list component versions:'); expect(response.content[0].text).toContain('Unknown error'); }); describe('Project resolution scenarios', () => { it('should use projectID and orgID directly when both are provided', async () => { const mockComponentVersions = { items: [ { id: 'version-1', component_id: 'test-component-id', environment_id: 'test-environment-id', version: '1.0.0', sha: 'abc123', created_at: '2023-01-01T00:00:00Z', updated_at: '2023-01-02T00:00:00Z', is_live: true, }, ], next_page_token: null, }; mockCircleCIClient.deploys.fetchComponentVersions.mockResolvedValue(mockComponentVersions); const args = { params: { projectID: 'test-project-id', orgID: 'test-org-id', componentID: 'test-component-id', environmentID: 'test-environment-id', }, } as any; const response = await listComponentVersions(args, mockExtra); expect(response).toHaveProperty('content'); expect(response.content[0].text).toContain('Versions for the component:'); // Should not call project resolution methods when both IDs are provided expect(mockCircleCIClient.projects.getProject).not.toHaveBeenCalled(); expect(mockCircleCIClient.projects.getProjectByID).not.toHaveBeenCalled(); expect(mockCircleCIClient.deploys.fetchComponentVersions).toHaveBeenCalledWith({ componentID: 'test-component-id', environmentID: 'test-environment-id', }); }); it('should resolve orgID from projectID when only projectID is provided', async () => { const mockComponentVersions = { items: [ { id: 'version-1', component_id: 'test-component-id', environment_id: 'test-environment-id', version: '1.0.0', sha: 'abc123', created_at: '2023-01-01T00:00:00Z', updated_at: '2023-01-02T00:00:00Z', is_live: true, }, ], next_page_token: null, }; mockCircleCIClient.projects.getProjectByID.mockResolvedValue({ id: 'test-project-id', organization_id: 'resolved-org-id', }); mockCircleCIClient.deploys.fetchComponentVersions.mockResolvedValue(mockComponentVersions); const args = { params: { projectID: 'test-project-id', componentID: 'test-component-id', environmentID: 'test-environment-id', }, } as any; const response = await listComponentVersions(args, mockExtra); expect(response).toHaveProperty('content'); expect(response.content[0].text).toContain('Versions for the component:'); expect(mockCircleCIClient.projects.getProjectByID).toHaveBeenCalledWith({ projectID: 'test-project-id', }); expect(mockCircleCIClient.projects.getProject).not.toHaveBeenCalled(); }); it('should handle project resolution errors for projectSlug', async () => { const errorMessage = 'Project not found'; mockCircleCIClient.projects.getProject.mockRejectedValue( new Error(errorMessage), ); const args = { params: { projectSlug: 'gh/invalid/repo', componentID: 'test-component-id', environmentID: 'test-environment-id', }, } as any; const response = await listComponentVersions(args, mockExtra); expect(response).toHaveProperty('content'); expect(response).toHaveProperty('isError', true); expect(response.content[0].text).toContain('Failed to resolve project information for gh/invalid/repo'); expect(response.content[0].text).toContain(errorMessage); }); it('should handle project resolution errors for projectID', async () => { const errorMessage = 'Project ID not found'; mockCircleCIClient.projects.getProjectByID.mockRejectedValue( new Error(errorMessage), ); const args = { params: { projectID: 'invalid-project-id', componentID: 'test-component-id', environmentID: 'test-environment-id', }, } as any; const response = await listComponentVersions(args, mockExtra); expect(response).toHaveProperty('content'); expect(response).toHaveProperty('isError', true); expect(response.content[0].text).toContain('Failed to resolve project information for project ID invalid-project-id'); expect(response.content[0].text).toContain(errorMessage); }); it('should return error when neither projectSlug nor projectID is provided', async () => { const args = { params: { componentID: 'test-component-id', environmentID: 'test-environment-id', }, } as any; const response = await listComponentVersions(args, mockExtra); expect(response).toHaveProperty('content'); expect(response).toHaveProperty('isError', true); expect(response.content[0].text).toContain('Invalid request. Please specify either a project slug or a project ID.'); }); }); describe('Missing environmentID scenarios', () => { it('should list environments when environmentID is not provided', async () => { const mockEnvironments = { items: [ { id: 'env-1', name: 'production' }, { id: 'env-2', name: 'staging' }, ], next_page_token: null, }; mockCircleCIClient.projects.getProject.mockResolvedValue({ id: 'test-project-id', organization_id: 'test-org-id', }); mockCircleCIClient.deploys.fetchEnvironments.mockResolvedValue(mockEnvironments); const args = { params: { projectSlug: 'gh/test-org/test-repo', componentID: 'test-component-id', }, } as any; const response = await listComponentVersions(args, mockExtra); expect(response).toHaveProperty('content'); expect(response.content[0].text).toContain('Please provide an environmentID. Available environments:'); expect(response.content[0].text).toContain('1. production (ID: env-1)'); expect(response.content[0].text).toContain('2. staging (ID: env-2)'); expect(mockCircleCIClient.deploys.fetchEnvironments).toHaveBeenCalledWith({ orgID: 'test-org-id', }); }); it('should handle empty environments list', async () => { mockCircleCIClient.projects.getProject.mockResolvedValue({ id: 'test-project-id', organization_id: 'test-org-id', }); mockCircleCIClient.deploys.fetchEnvironments.mockResolvedValue({ items: [], next_page_token: null, }); const args = { params: { projectSlug: 'gh/test-org/test-repo', componentID: 'test-component-id', }, } as any; const response = await listComponentVersions(args, mockExtra); expect(response).toHaveProperty('content'); expect(response.content[0].text).toBe('No environments found'); }); it('should handle fetchEnvironments API errors', async () => { const errorMessage = 'Failed to fetch environments'; mockCircleCIClient.projects.getProject.mockResolvedValue({ id: 'test-project-id', organization_id: 'test-org-id', }); mockCircleCIClient.deploys.fetchEnvironments.mockRejectedValue( new Error(errorMessage), ); const args = { params: { projectSlug: 'gh/test-org/test-repo', componentID: 'test-component-id', }, } as any; const response = await listComponentVersions(args, mockExtra); expect(response).toHaveProperty('content'); expect(response).toHaveProperty('isError', true); expect(response.content[0].text).toContain('Failed to list component versions:'); expect(response.content[0].text).toContain(errorMessage); }); }); describe('Missing componentID scenarios', () => { it('should list components when componentID is not provided', async () => { const mockComponents = { items: [ { id: 'comp-1', name: 'frontend' }, { id: 'comp-2', name: 'backend' }, ], next_page_token: null, }; mockCircleCIClient.projects.getProject.mockResolvedValue({ id: 'test-project-id', organization_id: 'test-org-id', }); mockCircleCIClient.deploys.fetchProjectComponents.mockResolvedValue(mockComponents); const args = { params: { projectSlug: 'gh/test-org/test-repo', environmentID: 'test-environment-id', }, } as any; const response = await listComponentVersions(args, mockExtra); expect(response).toHaveProperty('content'); expect(response.content[0].text).toContain('Please provide a componentID. Available components:'); expect(response.content[0].text).toContain('1. frontend (ID: comp-1)'); expect(response.content[0].text).toContain('2. backend (ID: comp-2)'); expect(mockCircleCIClient.deploys.fetchProjectComponents).toHaveBeenCalledWith({ projectID: 'test-project-id', orgID: 'test-org-id', }); }); it('should handle empty components list', async () => { mockCircleCIClient.projects.getProject.mockResolvedValue({ id: 'test-project-id', organization_id: 'test-org-id', }); mockCircleCIClient.deploys.fetchProjectComponents.mockResolvedValue({ items: [], next_page_token: null, }); const args = { params: { projectSlug: 'gh/test-org/test-repo', environmentID: 'test-environment-id', }, } as any; const response = await listComponentVersions(args, mockExtra); expect(response).toHaveProperty('content'); expect(response.content[0].text).toBe('No components found'); }); it('should handle fetchProjectComponents API errors', async () => { const errorMessage = 'Failed to fetch components'; mockCircleCIClient.projects.getProject.mockResolvedValue({ id: 'test-project-id', organization_id: 'test-org-id', }); mockCircleCIClient.deploys.fetchProjectComponents.mockRejectedValue( new Error(errorMessage), ); const args = { params: { projectSlug: 'gh/test-org/test-repo', environmentID: 'test-environment-id', }, } as any; const response = await listComponentVersions(args, mockExtra); expect(response).toHaveProperty('content'); expect(response).toHaveProperty('isError', true); expect(response.content[0].text).toContain('Failed to list component versions:'); expect(response.content[0].text).toContain(errorMessage); }); }); }); ```