UNPKG

qase-mcp-server

Version:

Model Context Protocol server for Qase TMS - Enables AI assistants to manage test cases, runs, and defects in Qase

299 lines (298 loc) 11.5 kB
import { z } from 'zod'; import { toResult } from '../utils.js'; import { client } from '../utils.js'; import { ResultAsync } from 'neverthrow'; // Schema for getting failed results from a specific project export const GetFailedResultsSchema = z.object({ code: z.string().describe('Project code'), runId: z .string() .or(z.number()) .optional() .describe('Specific run ID to filter by'), limit: z .string() .or(z.number()) .optional() .describe('Number of results to return (1-100)'), offset: z .string() .or(z.number()) .optional() .describe('Number of results to skip'), fromEndTime: z .string() .optional() .describe('From end time in format Y-m-d H:i:s'), toEndTime: z .string() .optional() .describe('To end time in format Y-m-d H:i:s'), }); // Schema for getting detailed failed results with test case information export const GetFailedResultsDetailedSchema = z.object({ code: z.string().describe('Project code'), runId: z .string() .or(z.number()) .optional() .describe('Specific run ID to filter by'), limit: z .string() .or(z.number()) .optional() .describe('Number of results to return (1-100)'), offset: z .string() .or(z.number()) .optional() .describe('Number of results to skip'), fromEndTime: z .string() .optional() .describe('From end time in format Y-m-d H:i:s'), toEndTime: z .string() .optional() .describe('To end time in format Y-m-d H:i:s'), includeSteps: z .boolean() .optional() .describe('Include step details in the results'), includeAttachments: z .boolean() .optional() .describe('Include attachment details'), }); // Schema for analyzing failures in a specific run export const AnalyzeRunFailuresSchema = z.object({ code: z.string().describe('Project code'), runId: z.string().or(z.number()).describe('Run ID to analyze'), includeStacktraces: z .boolean() .optional() .describe('Include stacktrace details'), categorizeFailures: z .boolean() .optional() .describe('Categorize failures by type'), }); /** * Get failed test results from a project */ export const getFailedResults = (args) => { const { code, runId, limit, offset, fromEndTime, toEndTime } = args; // Build filters for failed results only const filters = ['status=failed']; if (runId) { filters.push(`run=${runId}`); } if (fromEndTime) { filters.push(`from_end_time=${fromEndTime}`); } if (toEndTime) { filters.push(`to_end_time=${toEndTime}`); } const filterString = filters.join('&'); return toResult(client.results.getResults(code, limit ? String(limit) : '50', offset ? String(offset) : '0', filterString)).map((response) => { // Process the response to filter and enhance failed results const entities = response.data.result?.entities || []; const failedResults = entities .filter((entity) => entity.status === 'failed') .map((entity) => ({ hash: entity.hash, runId: entity.run_id, caseId: entity.case_id, status: entity.status, comment: entity.comment, stacktrace: entity.stacktrace, timeSpentMs: entity.time_spent_ms, endTime: entity.end_time, attachments: entity.attachments?.map((att) => ({ filename: att.filename, size: att.size, mime: att.mime, url: att.url, })) || [], failedSteps: entity.steps ?.filter((step) => step.status === 2) .map((step) => ({ position: step.position, status: step.status, attachments: step.attachments || [], })) || [], })); return { ...response, data: { ...response.data, result: { total: response.data.result?.total || 0, filtered: response.data.result?.filtered || 0, count: failedResults.length, failedResults: failedResults, }, }, }; }); }; /** * Get detailed failed test results with test case information */ export const getFailedResultsDetailed = (args) => { const { code, runId, limit, offset, fromEndTime, toEndTime, includeSteps, } = args; // First get the failed results return getFailedResults({ code, runId, limit, offset, fromEndTime, toEndTime, }).andThen((failedResponse) => { // Extract failed results from the response const failedResults = failedResponse.data.result?.failedResults || []; // Get test case details for each failed result const caseDetailsPromises = failedResults.map(async (result) => { try { // Get test case details const testCaseResponse = await client.cases.getCase(code, result.caseId); const testCase = testCaseResponse.data.result; return { ...result, testCase: { id: testCase?.id, title: testCase?.title, description: testCase?.description, suite: testCase?.suite_id ? { id: testCase.suite_id, title: 'Suite information not available', } : null, severity: testCase?.severity, priority: testCase?.priority, type: testCase?.type, behavior: testCase?.behavior, automation: testCase?.automation, steps: includeSteps ? testCase?.steps : undefined, }, }; } catch { // If we can't get case details, still return the result return { ...result, testCase: { id: result.caseId, title: 'Unable to fetch test case details', description: null, error: 'Failed to retrieve test case information', }, }; } }); return ResultAsync.fromPromise(Promise.all(caseDetailsPromises), (error) => `Failed to get detailed results: ${error.message}`).map((detailedResults) => ({ ...failedResponse, data: { ...failedResponse.data, result: { total: failedResponse.data.result?.total || 0, filtered: failedResponse.data.result?.filtered || 0, count: detailedResults.length, failedResultsDetailed: detailedResults, }, }, })); }); }; /** * Analyze failures in a specific test run */ export const analyzeRunFailures = (args) => { const { code, runId, includeStacktraces, categorizeFailures } = args; // Get all results for the specific run with filter const filterString = `run=${runId}`; return toResult(client.results.getResults(code, '100', // Get more results for analysis '0', filterString)).map((response) => { const allResults = response.data.result?.entities || []; const failedResults = allResults.filter((entity) => entity.status === 'failed'); const passedResults = allResults.filter((entity) => entity.status === 'passed'); const skippedResults = allResults.filter((entity) => entity.status === 'skipped'); const blockedResults = allResults.filter((entity) => entity.status === 'blocked'); // Basic statistics const stats = { total: allResults.length, passed: passedResults.length, failed: failedResults.length, skipped: skippedResults.length, blocked: blockedResults.length, passRate: allResults.length > 0 ? ((passedResults.length / allResults.length) * 100).toFixed(2) : '0.00', }; // Analyze failure patterns if requested let failureCategories = {}; if (categorizeFailures) { failureCategories = failedResults.reduce((categories, result) => { // Categorize by stacktrace patterns if (result.stacktrace) { const stacktrace = result.stacktrace.toLowerCase(); if (stacktrace.includes('assertion') || stacktrace.includes('assert')) { categories.assertionErrors = (categories.assertionErrors || 0) + 1; } else if (stacktrace.includes('timeout')) { categories.timeoutErrors = (categories.timeoutErrors || 0) + 1; } else if (stacktrace.includes('connection') || stacktrace.includes('network')) { categories.networkErrors = (categories.networkErrors || 0) + 1; } else if (stacktrace.includes('null') || stacktrace.includes('undefined')) { categories.nullPointerErrors = (categories.nullPointerErrors || 0) + 1; } else { categories.otherErrors = (categories.otherErrors || 0) + 1; } } else { categories.noStacktraceErrors = (categories.noStacktraceErrors || 0) + 1; } return categories; }, {}); } // Get detailed failure information const detailedFailures = failedResults.map((result) => ({ hash: result.hash, caseId: result.case_id, comment: result.comment, stacktrace: includeStacktraces ? result.stacktrace : result.stacktrace ? 'Available' : 'Not available', timeSpentMs: result.time_spent_ms, endTime: result.end_time, hasAttachments: result.attachments && result.attachments.length > 0, failedStepsCount: result.steps?.filter((step) => step.status === 2).length || 0, })); return { ...response, data: { ...response.data, result: { runId: Number(runId), statistics: stats, failureCategories: categorizeFailures ? failureCategories : undefined, failures: detailedFailures, analysisTimestamp: new Date().toISOString(), }, }, }; }); };