#
tokens: 11335/50000 2/137 files (page 4/4)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 4 of 4. Use http://codebase.md/circleci-public/mcp-server-circleci?lines=true&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/runEvaluationTests/handler.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
  2 | import { runEvaluationTests } from './handler.js';
  3 | import * as projectDetection from '../../lib/project-detection/index.js';
  4 | import * as clientModule from '../../clients/client.js';
  5 | 
  6 | vi.mock('../../lib/project-detection/index.js');
  7 | vi.mock('../../clients/client.js');
  8 | 
  9 | describe('runEvaluationTests handler', () => {
 10 |   const mockCircleCIClient = {
 11 |     projects: {
 12 |       getProject: vi.fn(),
 13 |     },
 14 |     pipelines: {
 15 |       getPipelineDefinitions: vi.fn(),
 16 |       runPipeline: vi.fn(),
 17 |     },
 18 |   };
 19 | 
 20 |   beforeEach(() => {
 21 |     vi.resetAllMocks();
 22 |     vi.spyOn(clientModule, 'getCircleCIClient').mockReturnValue(
 23 |       mockCircleCIClient as any,
 24 |     );
 25 |   });
 26 | 
 27 |   it('should return a valid MCP error response when no inputs are provided', async () => {
 28 |     const args = {
 29 |       params: {},
 30 |     } as any;
 31 | 
 32 |     const controller = new AbortController();
 33 |     const response = await runEvaluationTests(args, {
 34 |       signal: controller.signal,
 35 |     });
 36 | 
 37 |     expect(response).toHaveProperty('content');
 38 |     expect(response).toHaveProperty('isError', true);
 39 |     expect(Array.isArray(response.content)).toBe(true);
 40 |     expect(response.content[0]).toHaveProperty('type', 'text');
 41 |     expect(typeof response.content[0].text).toBe('string');
 42 |   });
 43 | 
 44 |   it('should return a valid MCP error response when project is not found', async () => {
 45 |     vi.spyOn(projectDetection, 'identifyProjectSlug').mockResolvedValue(
 46 |       undefined,
 47 |     );
 48 | 
 49 |     const args = {
 50 |       params: {
 51 |         workspaceRoot: '/workspace',
 52 |         gitRemoteURL: 'https://github.com/org/repo.git',
 53 |         branch: 'main',
 54 |         promptFiles: [
 55 |           {
 56 |             fileName: 'test.prompt.yml',
 57 |             fileContent: 'test: content',
 58 |           },
 59 |         ],
 60 |       },
 61 |     } as any;
 62 | 
 63 |     const controller = new AbortController();
 64 |     const response = await runEvaluationTests(args, {
 65 |       signal: controller.signal,
 66 |     });
 67 | 
 68 |     expect(response).toHaveProperty('content');
 69 |     expect(response).toHaveProperty('isError', true);
 70 |     expect(Array.isArray(response.content)).toBe(true);
 71 |     expect(response.content[0]).toHaveProperty('type', 'text');
 72 |     expect(typeof response.content[0].text).toBe('string');
 73 |   });
 74 | 
 75 |   it('should return a valid MCP error response when no branch is provided', async () => {
 76 |     vi.spyOn(projectDetection, 'getProjectSlugFromURL').mockReturnValue(
 77 |       'gh/org/repo',
 78 |     );
 79 |     vi.spyOn(projectDetection, 'getBranchFromURL').mockReturnValue(undefined);
 80 | 
 81 |     const args = {
 82 |       params: {
 83 |         projectURL: 'https://app.circleci.com/pipelines/gh/org/repo',
 84 |         promptFiles: [
 85 |           {
 86 |             fileName: 'test.prompt.yml',
 87 |             fileContent: 'test: content',
 88 |           },
 89 |         ],
 90 |       },
 91 |     } as any;
 92 | 
 93 |     const controller = new AbortController();
 94 |     const response = await runEvaluationTests(args, {
 95 |       signal: controller.signal,
 96 |     });
 97 | 
 98 |     expect(response).toHaveProperty('content');
 99 |     expect(response).toHaveProperty('isError', true);
100 |     expect(Array.isArray(response.content)).toBe(true);
101 |     expect(response.content[0]).toHaveProperty('type', 'text');
102 |     expect(typeof response.content[0].text).toBe('string');
103 |     expect(response.content[0].text).toContain('No branch provided');
104 |   });
105 | 
106 |   it('should return a valid MCP error response when projectSlug is provided without branch', async () => {
107 |     const args = {
108 |       params: {
109 |         projectSlug: 'gh/org/repo',
110 |         promptFiles: [
111 |           {
112 |             fileName: 'test.prompt.yml',
113 |             fileContent: 'test: content',
114 |           },
115 |         ],
116 |         // No branch provided
117 |       },
118 |     } as any;
119 | 
120 |     const controller = new AbortController();
121 |     const response = await runEvaluationTests(args, {
122 |       signal: controller.signal,
123 |     });
124 | 
125 |     expect(response).toHaveProperty('content');
126 |     expect(response).toHaveProperty('isError', true);
127 |     expect(Array.isArray(response.content)).toBe(true);
128 |     expect(response.content[0]).toHaveProperty('type', 'text');
129 |     expect(response.content[0].text).toContain('Branch not provided');
130 | 
131 |     // Verify that CircleCI API was not called
132 |     expect(mockCircleCIClient.projects.getProject).not.toHaveBeenCalled();
133 |     expect(
134 |       mockCircleCIClient.pipelines.getPipelineDefinitions,
135 |     ).not.toHaveBeenCalled();
136 |     expect(mockCircleCIClient.pipelines.runPipeline).not.toHaveBeenCalled();
137 |   });
138 | 
139 |   it('should return a valid MCP error response when no prompt files are provided', async () => {
140 |     const args = {
141 |       params: {
142 |         projectSlug: 'gh/org/repo',
143 |         branch: 'main',
144 |         promptFiles: [], // Empty array
145 |       },
146 |     } as any;
147 | 
148 |     const controller = new AbortController();
149 |     const response = await runEvaluationTests(args, {
150 |       signal: controller.signal,
151 |     });
152 | 
153 |     expect(response).toHaveProperty('content');
154 |     expect(response).toHaveProperty('isError', true);
155 |     expect(Array.isArray(response.content)).toBe(true);
156 |     expect(response.content[0]).toHaveProperty('type', 'text');
157 |     expect(response.content[0].text).toContain(
158 |       'No prompt template files provided',
159 |     );
160 |     expect(response.content[0].text).toContain('./prompts directory');
161 | 
162 |     // Verify that CircleCI API was not called
163 |     expect(mockCircleCIClient.projects.getProject).not.toHaveBeenCalled();
164 |     expect(
165 |       mockCircleCIClient.pipelines.getPipelineDefinitions,
166 |     ).not.toHaveBeenCalled();
167 |     expect(mockCircleCIClient.pipelines.runPipeline).not.toHaveBeenCalled();
168 |   });
169 | 
170 |   it('should return a valid MCP error response when promptFiles is undefined', async () => {
171 |     const args = {
172 |       params: {
173 |         projectSlug: 'gh/org/repo',
174 |         branch: 'main',
175 |         // promptFiles is undefined
176 |       },
177 |     } as any;
178 | 
179 |     const controller = new AbortController();
180 |     const response = await runEvaluationTests(args, {
181 |       signal: controller.signal,
182 |     });
183 | 
184 |     expect(response).toHaveProperty('content');
185 |     expect(response).toHaveProperty('isError', true);
186 |     expect(Array.isArray(response.content)).toBe(true);
187 |     expect(response.content[0]).toHaveProperty('type', 'text');
188 |     expect(response.content[0].text).toContain(
189 |       'No prompt template files provided',
190 |     );
191 |     expect(response.content[0].text).toContain('./prompts directory');
192 | 
193 |     // Verify that CircleCI API was not called
194 |     expect(mockCircleCIClient.projects.getProject).not.toHaveBeenCalled();
195 |     expect(
196 |       mockCircleCIClient.pipelines.getPipelineDefinitions,
197 |     ).not.toHaveBeenCalled();
198 |     expect(mockCircleCIClient.pipelines.runPipeline).not.toHaveBeenCalled();
199 |   });
200 | 
201 |   it('should return a valid MCP error response when no pipeline definitions are found', async () => {
202 |     vi.spyOn(projectDetection, 'getProjectSlugFromURL').mockReturnValue(
203 |       'gh/org/repo',
204 |     );
205 |     vi.spyOn(projectDetection, 'getBranchFromURL').mockReturnValue('main');
206 | 
207 |     mockCircleCIClient.projects.getProject.mockResolvedValue({
208 |       id: 'project-id',
209 |     });
210 |     mockCircleCIClient.pipelines.getPipelineDefinitions.mockResolvedValue([]);
211 | 
212 |     const args = {
213 |       params: {
214 |         projectURL: 'https://app.circleci.com/pipelines/gh/org/repo',
215 |         promptFiles: [
216 |           {
217 |             fileName: 'test.prompt.yml',
218 |             fileContent: 'test: content',
219 |           },
220 |         ],
221 |       },
222 |     } as any;
223 | 
224 |     const controller = new AbortController();
225 |     const response = await runEvaluationTests(args, {
226 |       signal: controller.signal,
227 |     });
228 | 
229 |     expect(response).toHaveProperty('content');
230 |     expect(response).toHaveProperty('isError', true);
231 |     expect(Array.isArray(response.content)).toBe(true);
232 |     expect(response.content[0]).toHaveProperty('type', 'text');
233 |     expect(typeof response.content[0].text).toBe('string');
234 |     expect(response.content[0].text).toContain('No pipeline definitions found');
235 |   });
236 | 
237 |   it('should return a list of pipeline choices when multiple pipeline definitions are found and no choice is provided', async () => {
238 |     vi.spyOn(projectDetection, 'getProjectSlugFromURL').mockReturnValue(
239 |       'gh/org/repo',
240 |     );
241 |     vi.spyOn(projectDetection, 'getBranchFromURL').mockReturnValue('main');
242 | 
243 |     mockCircleCIClient.projects.getProject.mockResolvedValue({
244 |       id: 'project-id',
245 |     });
246 |     mockCircleCIClient.pipelines.getPipelineDefinitions.mockResolvedValue([
247 |       { id: 'def1', name: 'Pipeline 1' },
248 |       { id: 'def2', name: 'Pipeline 2' },
249 |     ]);
250 | 
251 |     const args = {
252 |       params: {
253 |         projectURL: 'https://app.circleci.com/pipelines/gh/org/repo',
254 |         promptFiles: [
255 |           {
256 |             fileName: 'test.prompt.yml',
257 |             fileContent: 'test: content',
258 |           },
259 |         ],
260 |       },
261 |     } as any;
262 | 
263 |     const controller = new AbortController();
264 |     const response = await runEvaluationTests(args, {
265 |       signal: controller.signal,
266 |     });
267 | 
268 |     expect(response).toHaveProperty('content');
269 |     expect(Array.isArray(response.content)).toBe(true);
270 |     expect(response.content[0]).toHaveProperty('type', 'text');
271 |     expect(typeof response.content[0].text).toBe('string');
272 |     expect(response.content[0].text).toContain(
273 |       'Multiple pipeline definitions found',
274 |     );
275 |     expect(response.content[0].text).toContain('Pipeline 1');
276 |     expect(response.content[0].text).toContain('Pipeline 2');
277 |   });
278 | 
279 |   it('should return an error when an invalid pipeline choice is provided', async () => {
280 |     vi.spyOn(projectDetection, 'getProjectSlugFromURL').mockReturnValue(
281 |       'gh/org/repo',
282 |     );
283 |     vi.spyOn(projectDetection, 'getBranchFromURL').mockReturnValue('main');
284 | 
285 |     mockCircleCIClient.projects.getProject.mockResolvedValue({
286 |       id: 'project-id',
287 |     });
288 |     mockCircleCIClient.pipelines.getPipelineDefinitions.mockResolvedValue([
289 |       { id: 'def1', name: 'Pipeline 1' },
290 |       { id: 'def2', name: 'Pipeline 2' },
291 |     ]);
292 | 
293 |     const args = {
294 |       params: {
295 |         projectURL: 'https://app.circleci.com/pipelines/gh/org/repo',
296 |         pipelineChoiceName: 'Non-existent Pipeline',
297 |         promptFiles: [
298 |           {
299 |             fileName: 'test.prompt.yml',
300 |             fileContent: 'test: content',
301 |           },
302 |         ],
303 |       },
304 |     } as any;
305 | 
306 |     const controller = new AbortController();
307 |     const response = await runEvaluationTests(args, {
308 |       signal: controller.signal,
309 |     });
310 | 
311 |     expect(response).toHaveProperty('content');
312 |     expect(response).toHaveProperty('isError', true);
313 |     expect(Array.isArray(response.content)).toBe(true);
314 |     expect(response.content[0]).toHaveProperty('type', 'text');
315 |     expect(typeof response.content[0].text).toBe('string');
316 |     expect(response.content[0].text).toContain(
317 |       'Pipeline definition with name Non-existent Pipeline not found',
318 |     );
319 |   });
320 | 
321 |   it('should run evaluation tests with multiple prompt files and correct parallelism', async () => {
322 |     vi.spyOn(projectDetection, 'getProjectSlugFromURL').mockReturnValue(
323 |       'gh/org/repo',
324 |     );
325 |     vi.spyOn(projectDetection, 'getBranchFromURL').mockReturnValue('main');
326 | 
327 |     mockCircleCIClient.projects.getProject.mockResolvedValue({
328 |       id: 'project-id',
329 |     });
330 |     mockCircleCIClient.pipelines.getPipelineDefinitions.mockResolvedValue([
331 |       { id: 'def1', name: 'Pipeline 1' },
332 |     ]);
333 |     mockCircleCIClient.pipelines.runPipeline.mockResolvedValue({
334 |       number: 123,
335 |       state: 'pending',
336 |       id: 'pipeline-id',
337 |     });
338 | 
339 |     const args = {
340 |       params: {
341 |         projectURL: 'https://app.circleci.com/pipelines/gh/org/repo',
342 |         promptFiles: [
343 |           {
344 |             fileName: 'test1.prompt.json',
345 |             fileContent: '{"template": "test content 1"}',
346 |           },
347 |           {
348 |             fileName: 'test2.prompt.yml',
349 |             fileContent: 'template: test content 2',
350 |           },
351 |         ],
352 |       },
353 |     } as any;
354 | 
355 |     const controller = new AbortController();
356 |     const response = await runEvaluationTests(args, {
357 |       signal: controller.signal,
358 |     });
359 | 
360 |     expect(response).toHaveProperty('content');
361 |     expect(Array.isArray(response.content)).toBe(true);
362 |     expect(response.content[0]).toHaveProperty('type', 'text');
363 |     expect(typeof response.content[0].text).toBe('string');
364 |     expect(response.content[0].text).toContain('Pipeline run successfully');
365 | 
366 |     // Verify that the pipeline was called with correct configuration
367 |     expect(mockCircleCIClient.pipelines.runPipeline).toHaveBeenCalledWith({
368 |       projectSlug: 'gh/org/repo',
369 |       branch: 'main',
370 |       definitionId: 'def1',
371 |       configContent: expect.stringContaining('parallelism: 2'), // Should match number of files
372 |     });
373 | 
374 |     // Verify the config contains conditional file creation logic
375 |     const configContent =
376 |       mockCircleCIClient.pipelines.runPipeline.mock.calls[0][0].configContent;
377 |     expect(configContent).toContain('CIRCLE_NODE_INDEX');
378 |     expect(configContent).toContain('test1.prompt.json');
379 |     expect(configContent).toContain('test2.prompt.yml');
380 |     expect(configContent).toContain('python eval.py');
381 |   });
382 | 
383 |   it('should process JSON files with proper formatting', async () => {
384 |     vi.spyOn(projectDetection, 'getProjectSlugFromURL').mockReturnValue(
385 |       'gh/org/repo',
386 |     );
387 |     vi.spyOn(projectDetection, 'getBranchFromURL').mockReturnValue('main');
388 | 
389 |     mockCircleCIClient.projects.getProject.mockResolvedValue({
390 |       id: 'project-id',
391 |     });
392 |     mockCircleCIClient.pipelines.getPipelineDefinitions.mockResolvedValue([
393 |       { id: 'def1', name: 'Pipeline 1' },
394 |     ]);
395 |     mockCircleCIClient.pipelines.runPipeline.mockResolvedValue({
396 |       number: 123,
397 |       state: 'pending',
398 |       id: 'pipeline-id',
399 |     });
400 | 
401 |     const args = {
402 |       params: {
403 |         projectSlug: 'gh/org/repo',
404 |         branch: 'main',
405 |         promptFiles: [
406 |           {
407 |             fileName: 'test.prompt.json',
408 |             fileContent: '{"template":"test","vars":["a","b"]}',
409 |           },
410 |         ],
411 |       },
412 |     } as any;
413 | 
414 |     const controller = new AbortController();
415 |     await runEvaluationTests(args, {
416 |       signal: controller.signal,
417 |     });
418 | 
419 |     // Verify that the pipeline was called
420 |     expect(mockCircleCIClient.pipelines.runPipeline).toHaveBeenCalled();
421 | 
422 |     const configContent =
423 |       mockCircleCIClient.pipelines.runPipeline.mock.calls[0][0].configContent;
424 |     expect(configContent).toContain('parallelism: 1');
425 |     expect(configContent).toContain('test.prompt.json');
426 |   });
427 | 
428 |   it('should detect project from git remote and run evaluation tests', async () => {
429 |     vi.spyOn(projectDetection, 'identifyProjectSlug').mockResolvedValue(
430 |       'gh/org/repo',
431 |     );
432 | 
433 |     mockCircleCIClient.projects.getProject.mockResolvedValue({
434 |       id: 'project-id',
435 |     });
436 |     mockCircleCIClient.pipelines.getPipelineDefinitions.mockResolvedValue([
437 |       { id: 'def1', name: 'Pipeline 1' },
438 |     ]);
439 |     mockCircleCIClient.pipelines.runPipeline.mockResolvedValue({
440 |       number: 123,
441 |       state: 'pending',
442 |       id: 'pipeline-id',
443 |     });
444 | 
445 |     const args = {
446 |       params: {
447 |         workspaceRoot: '/workspace',
448 |         gitRemoteURL: 'https://github.com/org/repo.git',
449 |         branch: 'feature-branch',
450 |         promptFiles: [
451 |           {
452 |             fileName: 'test.prompt.yml',
453 |             fileContent: 'template: test content',
454 |           },
455 |         ],
456 |       },
457 |     } as any;
458 | 
459 |     const controller = new AbortController();
460 |     const response = await runEvaluationTests(args, {
461 |       signal: controller.signal,
462 |     });
463 | 
464 |     expect(response).toHaveProperty('content');
465 |     expect(Array.isArray(response.content)).toBe(true);
466 |     expect(response.content[0]).toHaveProperty('type', 'text');
467 |     expect(typeof response.content[0].text).toBe('string');
468 |     expect(response.content[0].text).toContain('Pipeline run successfully');
469 |     expect(mockCircleCIClient.pipelines.runPipeline).toHaveBeenCalledWith({
470 |       projectSlug: 'gh/org/repo',
471 |       branch: 'feature-branch',
472 |       definitionId: 'def1',
473 |       configContent: expect.any(String),
474 |     });
475 |   });
476 | });
477 | 
```

--------------------------------------------------------------------------------
/src/tools/listComponentVersions/handler.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
  2 | import { listComponentVersions } from './handler.js';
  3 | import * as clientModule from '../../clients/client.js';
  4 | 
  5 | vi.mock('../../clients/client.js');
  6 | 
  7 | describe('listComponentVersions handler', () => {
  8 |   const mockCircleCIClient = {
  9 |     deploys: {
 10 |       fetchComponentVersions: vi.fn(),
 11 |       fetchEnvironments: vi.fn(),
 12 |       fetchProjectComponents: vi.fn(),
 13 |     },
 14 |     projects: {
 15 |       getProject: vi.fn(),
 16 |       getProjectByID: vi.fn(),
 17 |     },
 18 |   };
 19 | 
 20 |   const mockExtra = {
 21 |     signal: new AbortController().signal,
 22 |     requestId: 'test-id',
 23 |     sendNotification: vi.fn(),
 24 |     sendRequest: vi.fn(),
 25 |   };
 26 | 
 27 |   beforeEach(() => {
 28 |     vi.resetAllMocks();
 29 |     vi.spyOn(clientModule, 'getCircleCIClient').mockReturnValue(
 30 |       mockCircleCIClient as any,
 31 |     );
 32 |   });
 33 | 
 34 |   it('should return the formatted component versions when found', async () => {
 35 |     const mockComponentVersions = {
 36 |       items: [
 37 |         {
 38 |           id: 'version-1',
 39 |           component_id: 'test-component-id',
 40 |           environment_id: 'test-environment-id',
 41 |           version: '1.0.0',
 42 |           sha: 'abc123',
 43 |           created_at: '2023-01-01T00:00:00Z',
 44 |           updated_at: '2023-01-02T00:00:00Z',
 45 |           is_live: true,
 46 |         },
 47 |         {
 48 |           id: 'version-2',
 49 |           component_id: 'test-component-id',
 50 |           environment_id: 'test-environment-id',
 51 |           version: '1.1.0',
 52 |           sha: 'def456',
 53 |           created_at: '2023-01-03T00:00:00Z',
 54 |           updated_at: '2023-01-04T00:00:00Z',
 55 |           is_live: false,
 56 |         },
 57 |       ],
 58 |       next_page_token: null,
 59 |     };
 60 | 
 61 |     // Mock project resolution
 62 |     mockCircleCIClient.projects.getProject.mockResolvedValue({
 63 |       id: 'test-project-id',
 64 |       organization_id: 'test-org-id',
 65 |     });
 66 | 
 67 |     mockCircleCIClient.deploys.fetchComponentVersions.mockResolvedValue(mockComponentVersions);
 68 | 
 69 |     const args = {
 70 |       params: {
 71 |         projectSlug: 'gh/test-org/test-repo',
 72 |         componentID: 'test-component-id',
 73 |         environmentID: 'test-environment-id',
 74 |       },
 75 |     } as any;
 76 | 
 77 |     const response = await listComponentVersions(args, mockExtra);
 78 | 
 79 |     expect(response).toHaveProperty('content');
 80 |     expect(Array.isArray(response.content)).toBe(true);
 81 |     expect(response.content[0]).toHaveProperty('type', 'text');
 82 |     expect(typeof response.content[0].text).toBe('string');
 83 |     expect(response.content[0].text).toContain('Versions for the component:');
 84 |     expect(response.content[0].text).toContain(JSON.stringify(mockComponentVersions));
 85 |     
 86 |     expect(mockCircleCIClient.projects.getProject).toHaveBeenCalledTimes(1);
 87 |     expect(mockCircleCIClient.projects.getProject).toHaveBeenCalledWith({
 88 |       projectSlug: 'gh/test-org/test-repo',
 89 |     });
 90 |     expect(mockCircleCIClient.deploys.fetchComponentVersions).toHaveBeenCalledTimes(1);
 91 |     expect(mockCircleCIClient.deploys.fetchComponentVersions).toHaveBeenCalledWith({
 92 |         componentID: 'test-component-id',
 93 |         environmentID: 'test-environment-id',
 94 |       });
 95 |   });
 96 | 
 97 |   it('should return "No component versions found" when component versions list is empty', async () => {
 98 |     // Mock project resolution
 99 |     mockCircleCIClient.projects.getProject.mockResolvedValue({
100 |       id: 'test-project-id',
101 |       organization_id: 'test-org-id',
102 |     });
103 | 
104 |     mockCircleCIClient.deploys.fetchComponentVersions.mockResolvedValue({
105 |       items: [],
106 |       next_page_token: null,
107 |     });
108 | 
109 |     const args = {
110 |       params: {
111 |         projectSlug: 'gh/test-org/test-repo',
112 |         componentID: 'test-component-id',
113 |         environmentID: 'test-environment-id',
114 |       },
115 |     } as any;
116 | 
117 |     const response = await listComponentVersions(args, mockExtra);
118 | 
119 |     expect(response).toHaveProperty('content');
120 |     expect(Array.isArray(response.content)).toBe(true);
121 |     expect(response.content[0]).toHaveProperty('type', 'text');
122 |     expect(typeof response.content[0].text).toBe('string');
123 |     expect(response.content[0].text).toBe('No component versions found');
124 |   });
125 | 
126 |   it('should handle API errors gracefully', async () => {
127 |     const errorMessage = 'Component versions API request failed';
128 |     
129 |     // Mock project resolution
130 |     mockCircleCIClient.projects.getProject.mockResolvedValue({
131 |       id: 'test-project-id',
132 |       organization_id: 'test-org-id',
133 |     });
134 | 
135 |     mockCircleCIClient.deploys.fetchComponentVersions.mockRejectedValue(
136 |       new Error(errorMessage),
137 |     );
138 | 
139 |     const args = {
140 |       params: {
141 |         projectSlug: 'gh/test-org/test-repo',
142 |         componentID: 'test-component-id',
143 |         environmentID: 'test-environment-id',
144 |       },
145 |     } as any;
146 | 
147 |     const response = await listComponentVersions(args, mockExtra);
148 | 
149 |     expect(response).toHaveProperty('content');
150 |     expect(response).toHaveProperty('isError', true);
151 |     expect(Array.isArray(response.content)).toBe(true);
152 |     expect(response.content[0]).toHaveProperty('type', 'text');
153 |     expect(typeof response.content[0].text).toBe('string');
154 |     expect(response.content[0].text).toContain('Failed to list component versions:');
155 |     expect(response.content[0].text).toContain(errorMessage);
156 |   });
157 | 
158 |   it('should handle non-Error exceptions gracefully', async () => {
159 |     // Mock project resolution
160 |     mockCircleCIClient.projects.getProject.mockResolvedValue({
161 |       id: 'test-project-id',
162 |       organization_id: 'test-org-id',
163 |     });
164 | 
165 |     mockCircleCIClient.deploys.fetchComponentVersions.mockRejectedValue(
166 |       'Unexpected error',
167 |     );
168 | 
169 |     const args = {
170 |       params: {
171 |         projectSlug: 'gh/test-org/test-repo',
172 |         componentID: 'test-component-id',
173 |         environmentID: 'test-environment-id',
174 |       },
175 |     } as any;
176 | 
177 |     const response = await listComponentVersions(args, mockExtra);
178 | 
179 |     expect(response).toHaveProperty('content');
180 |     expect(response).toHaveProperty('isError', true);
181 |     expect(Array.isArray(response.content)).toBe(true);
182 |     expect(response.content[0]).toHaveProperty('type', 'text');
183 |     expect(typeof response.content[0].text).toBe('string');
184 |     expect(response.content[0].text).toContain('Failed to list component versions:');
185 |     expect(response.content[0].text).toContain('Unknown error');
186 |   });
187 | 
188 |   describe('Project resolution scenarios', () => {
189 |     it('should use projectID and orgID directly when both are provided', async () => {
190 |       const mockComponentVersions = {
191 |         items: [
192 |           {
193 |             id: 'version-1',
194 |             component_id: 'test-component-id',
195 |             environment_id: 'test-environment-id',
196 |             version: '1.0.0',
197 |             sha: 'abc123',
198 |             created_at: '2023-01-01T00:00:00Z',
199 |             updated_at: '2023-01-02T00:00:00Z',
200 |             is_live: true,
201 |           },
202 |         ],
203 |         next_page_token: null,
204 |       };
205 | 
206 |       mockCircleCIClient.deploys.fetchComponentVersions.mockResolvedValue(mockComponentVersions);
207 | 
208 |       const args = {
209 |         params: {
210 |           projectID: 'test-project-id',
211 |           orgID: 'test-org-id',
212 |           componentID: 'test-component-id',
213 |           environmentID: 'test-environment-id',
214 |         },
215 |       } as any;
216 | 
217 |       const response = await listComponentVersions(args, mockExtra);
218 | 
219 |       expect(response).toHaveProperty('content');
220 |       expect(response.content[0].text).toContain('Versions for the component:');
221 |       
222 |       // Should not call project resolution methods when both IDs are provided
223 |       expect(mockCircleCIClient.projects.getProject).not.toHaveBeenCalled();
224 |       expect(mockCircleCIClient.projects.getProjectByID).not.toHaveBeenCalled();
225 |       expect(mockCircleCIClient.deploys.fetchComponentVersions).toHaveBeenCalledWith({
226 |         componentID: 'test-component-id',
227 |         environmentID: 'test-environment-id',
228 |       });
229 |     });
230 | 
231 |     it('should resolve orgID from projectID when only projectID is provided', async () => {
232 |       const mockComponentVersions = {
233 |         items: [
234 |           {
235 |             id: 'version-1',
236 |             component_id: 'test-component-id',
237 |             environment_id: 'test-environment-id',
238 |             version: '1.0.0',
239 |             sha: 'abc123',
240 |             created_at: '2023-01-01T00:00:00Z',
241 |             updated_at: '2023-01-02T00:00:00Z',
242 |             is_live: true,
243 |           },
244 |         ],
245 |         next_page_token: null,
246 |       };
247 | 
248 |       mockCircleCIClient.projects.getProjectByID.mockResolvedValue({
249 |         id: 'test-project-id',
250 |         organization_id: 'resolved-org-id',
251 |       });
252 | 
253 |       mockCircleCIClient.deploys.fetchComponentVersions.mockResolvedValue(mockComponentVersions);
254 | 
255 |       const args = {
256 |         params: {
257 |           projectID: 'test-project-id',
258 |           componentID: 'test-component-id',
259 |           environmentID: 'test-environment-id',
260 |         },
261 |       } as any;
262 | 
263 |       const response = await listComponentVersions(args, mockExtra);
264 | 
265 |       expect(response).toHaveProperty('content');
266 |       expect(response.content[0].text).toContain('Versions for the component:');
267 |       
268 |       expect(mockCircleCIClient.projects.getProjectByID).toHaveBeenCalledWith({
269 |         projectID: 'test-project-id',
270 |       });
271 |       expect(mockCircleCIClient.projects.getProject).not.toHaveBeenCalled();
272 |     });
273 | 
274 |     it('should handle project resolution errors for projectSlug', async () => {
275 |       const errorMessage = 'Project not found';
276 |       
277 |       mockCircleCIClient.projects.getProject.mockRejectedValue(
278 |         new Error(errorMessage),
279 |       );
280 | 
281 |       const args = {
282 |         params: {
283 |           projectSlug: 'gh/invalid/repo',
284 |           componentID: 'test-component-id',
285 |           environmentID: 'test-environment-id',
286 |         },
287 |       } as any;
288 | 
289 |       const response = await listComponentVersions(args, mockExtra);
290 | 
291 |       expect(response).toHaveProperty('content');
292 |       expect(response).toHaveProperty('isError', true);
293 |       expect(response.content[0].text).toContain('Failed to resolve project information for gh/invalid/repo');
294 |       expect(response.content[0].text).toContain(errorMessage);
295 |     });
296 | 
297 |     it('should handle project resolution errors for projectID', async () => {
298 |       const errorMessage = 'Project ID not found';
299 |       
300 |       mockCircleCIClient.projects.getProjectByID.mockRejectedValue(
301 |         new Error(errorMessage),
302 |       );
303 | 
304 |       const args = {
305 |         params: {
306 |           projectID: 'invalid-project-id',
307 |           componentID: 'test-component-id',
308 |           environmentID: 'test-environment-id',
309 |         },
310 |       } as any;
311 | 
312 |       const response = await listComponentVersions(args, mockExtra);
313 | 
314 |       expect(response).toHaveProperty('content');
315 |       expect(response).toHaveProperty('isError', true);
316 |       expect(response.content[0].text).toContain('Failed to resolve project information for project ID invalid-project-id');
317 |       expect(response.content[0].text).toContain(errorMessage);
318 |     });
319 | 
320 |     it('should return error when neither projectSlug nor projectID is provided', async () => {
321 |       const args = {
322 |         params: {
323 |           componentID: 'test-component-id',
324 |           environmentID: 'test-environment-id',
325 |         },
326 |       } as any;
327 | 
328 |       const response = await listComponentVersions(args, mockExtra);
329 | 
330 |       expect(response).toHaveProperty('content');
331 |       expect(response).toHaveProperty('isError', true);
332 |       expect(response.content[0].text).toContain('Invalid request. Please specify either a project slug or a project ID.');
333 |     });
334 |   });
335 | 
336 |   describe('Missing environmentID scenarios', () => {
337 |     it('should list environments when environmentID is not provided', async () => {
338 |       const mockEnvironments = {
339 |         items: [
340 |           { id: 'env-1', name: 'production' },
341 |           { id: 'env-2', name: 'staging' },
342 |         ],
343 |         next_page_token: null,
344 |       };
345 | 
346 |       mockCircleCIClient.projects.getProject.mockResolvedValue({
347 |         id: 'test-project-id',
348 |         organization_id: 'test-org-id',
349 |       });
350 | 
351 |       mockCircleCIClient.deploys.fetchEnvironments.mockResolvedValue(mockEnvironments);
352 | 
353 |       const args = {
354 |         params: {
355 |           projectSlug: 'gh/test-org/test-repo',
356 |           componentID: 'test-component-id',
357 |         },
358 |       } as any;
359 | 
360 |       const response = await listComponentVersions(args, mockExtra);
361 | 
362 |       expect(response).toHaveProperty('content');
363 |       expect(response.content[0].text).toContain('Please provide an environmentID. Available environments:');
364 |       expect(response.content[0].text).toContain('1. production (ID: env-1)');
365 |       expect(response.content[0].text).toContain('2. staging (ID: env-2)');
366 |       
367 |       expect(mockCircleCIClient.deploys.fetchEnvironments).toHaveBeenCalledWith({
368 |         orgID: 'test-org-id',
369 |       });
370 |     });
371 | 
372 |     it('should handle empty environments list', async () => {
373 |       mockCircleCIClient.projects.getProject.mockResolvedValue({
374 |         id: 'test-project-id',
375 |         organization_id: 'test-org-id',
376 |       });
377 | 
378 |       mockCircleCIClient.deploys.fetchEnvironments.mockResolvedValue({
379 |         items: [],
380 |         next_page_token: null,
381 |       });
382 | 
383 |       const args = {
384 |         params: {
385 |           projectSlug: 'gh/test-org/test-repo',
386 |           componentID: 'test-component-id',
387 |         },
388 |       } as any;
389 | 
390 |       const response = await listComponentVersions(args, mockExtra);
391 | 
392 |       expect(response).toHaveProperty('content');
393 |       expect(response.content[0].text).toBe('No environments found');
394 |     });
395 | 
396 |     it('should handle fetchEnvironments API errors', async () => {
397 |       const errorMessage = 'Failed to fetch environments';
398 | 
399 |       mockCircleCIClient.projects.getProject.mockResolvedValue({
400 |         id: 'test-project-id',
401 |         organization_id: 'test-org-id',
402 |       });
403 | 
404 |       mockCircleCIClient.deploys.fetchEnvironments.mockRejectedValue(
405 |         new Error(errorMessage),
406 |       );
407 | 
408 |       const args = {
409 |         params: {
410 |           projectSlug: 'gh/test-org/test-repo',
411 |           componentID: 'test-component-id',
412 |         },
413 |       } as any;
414 | 
415 |       const response = await listComponentVersions(args, mockExtra);
416 | 
417 |       expect(response).toHaveProperty('content');
418 |       expect(response).toHaveProperty('isError', true);
419 |       expect(response.content[0].text).toContain('Failed to list component versions:');
420 |       expect(response.content[0].text).toContain(errorMessage);
421 |     });
422 |   });
423 | 
424 |   describe('Missing componentID scenarios', () => {
425 |     it('should list components when componentID is not provided', async () => {
426 |       const mockComponents = {
427 |         items: [
428 |           { id: 'comp-1', name: 'frontend' },
429 |           { id: 'comp-2', name: 'backend' },
430 |         ],
431 |         next_page_token: null,
432 |       };
433 | 
434 |       mockCircleCIClient.projects.getProject.mockResolvedValue({
435 |         id: 'test-project-id',
436 |         organization_id: 'test-org-id',
437 |       });
438 | 
439 |       mockCircleCIClient.deploys.fetchProjectComponents.mockResolvedValue(mockComponents);
440 | 
441 |       const args = {
442 |         params: {
443 |           projectSlug: 'gh/test-org/test-repo',
444 |           environmentID: 'test-environment-id',
445 |         },
446 |       } as any;
447 | 
448 |       const response = await listComponentVersions(args, mockExtra);
449 | 
450 |       expect(response).toHaveProperty('content');
451 |       expect(response.content[0].text).toContain('Please provide a componentID. Available components:');
452 |       expect(response.content[0].text).toContain('1. frontend (ID: comp-1)');
453 |       expect(response.content[0].text).toContain('2. backend (ID: comp-2)');
454 |       
455 |       expect(mockCircleCIClient.deploys.fetchProjectComponents).toHaveBeenCalledWith({
456 |         projectID: 'test-project-id',
457 |         orgID: 'test-org-id',
458 |       });
459 |     });
460 | 
461 |     it('should handle empty components list', async () => {
462 |       mockCircleCIClient.projects.getProject.mockResolvedValue({
463 |         id: 'test-project-id',
464 |         organization_id: 'test-org-id',
465 |       });
466 | 
467 |       mockCircleCIClient.deploys.fetchProjectComponents.mockResolvedValue({
468 |         items: [],
469 |         next_page_token: null,
470 |       });
471 | 
472 |       const args = {
473 |         params: {
474 |           projectSlug: 'gh/test-org/test-repo',
475 |           environmentID: 'test-environment-id',
476 |         },
477 |       } as any;
478 | 
479 |       const response = await listComponentVersions(args, mockExtra);
480 | 
481 |       expect(response).toHaveProperty('content');
482 |       expect(response.content[0].text).toBe('No components found');
483 |     });
484 | 
485 |     it('should handle fetchProjectComponents API errors', async () => {
486 |       const errorMessage = 'Failed to fetch components';
487 | 
488 |       mockCircleCIClient.projects.getProject.mockResolvedValue({
489 |         id: 'test-project-id',
490 |         organization_id: 'test-org-id',
491 |       });
492 | 
493 |       mockCircleCIClient.deploys.fetchProjectComponents.mockRejectedValue(
494 |         new Error(errorMessage),
495 |       );
496 | 
497 |       const args = {
498 |         params: {
499 |           projectSlug: 'gh/test-org/test-repo',
500 |           environmentID: 'test-environment-id',
501 |         },
502 |       } as any;
503 | 
504 |       const response = await listComponentVersions(args, mockExtra);
505 | 
506 |       expect(response).toHaveProperty('content');
507 |       expect(response).toHaveProperty('isError', true);
508 |       expect(response.content[0].text).toContain('Failed to list component versions:');
509 |       expect(response.content[0].text).toContain(errorMessage);
510 |     });
511 |   });
512 | });
513 | 
```
Page 4/4FirstPrevNextLast