Skip to content

Commit c5ae304

Browse files
therealbradclaude
andauthored
enhance: Features/cli test result and test run attachments (#44)
* feat(cli): add support for uploading attachments during test result import - Introduced functionality to upload test result attachments and test run attachments via the CLI. - Enhanced the import command to include options for specifying attachment directories and skipping uploads. - Updated the API to handle attachment mappings and responses for better integration with the CLI. - Added new types and utility functions for managing attachment uploads and summaries. - Improved documentation to reflect new attachment handling features and usage examples. * fix(prisma): add workflowType to seedWorkflows function - Introduced a new property `workflowType` with the value "IN_PROGRESS" in the seedWorkflows function to enhance workflow management. - Updated the workflow object structure to include the new `workflowType` for better clarity and functionality. * fix(JunitTableSection): update translation key for cancel action - Changed the translation key for the cancel button from "common.actions.cancel" to "common.cancel" for improved clarity and consistency in the user interface. * feat(test-results): add extended data parsing for test cases - Implemented functionality to parse extended test case data, including raw system-out, system-err, and assertions, which were previously not exposed by the main parser. - Introduced new types and utility functions for managing extended data, enhancing the detail available in test results. - Updated the POST request handler to incorporate extended data parsing and handle potential parsing errors gracefully. * fix(JunitTableSection): replace textarea with Textarea component - Updated the JunitTableSection to use the new Textarea component for improved styling and consistency. - Ensured the textarea retains its read-only functionality while enhancing the user interface. * fix(import): remove JUnitAttachment creation during import JUnitAttachment records stored text paths from JUnit XML and were linked to the test case (repositoryCaseId), causing them to appear on the test case details page. Since actual attachment files are uploaded via CLI to the Attachments table (linked to junitTestResultId), the JUnitAttachment records are redundant and confusing. Attachments now only appear on the test result, not the test case. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore(dependencies): update package versions and add new SDK - Added '@modelcontextprotocol/sdk' version 1.25.2 to package.json and pnpm-lock.yaml. - Updated '@aws-sdk/client-s3' and '@aws-sdk/s3-request-presigner' to version 3.964.0. - Upgraded various '@tiptap' packages to version 3.15.3 for improved functionality and consistency. - Updated 'ai' package to version 6.0.19 and 'framer-motion' to version 12.24.10. - Updated 'happy-dom' to version 20.1.0 and 'baseline-browser-mapping' to version 2.9.12. - Upgraded '@typescript-eslint/eslint-plugin' and '@typescript-eslint/parser' to version 8.52.0 for better linting support. --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 5b8e8b3 commit c5ae304

File tree

19 files changed

+3865
-753
lines changed

19 files changed

+3865
-753
lines changed

cli/dist/index.js

Lines changed: 399 additions & 10 deletions
Large diffs are not rendered by default.

cli/src/commands/import.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ import * as path from "path";
1111
import * as config from "../lib/config.js";
1212
import * as api from "../lib/api.js";
1313
import * as logger from "../lib/logger.js";
14+
import {
15+
resolveAttachments,
16+
filterExistingAttachments,
17+
getAttachmentSummary,
18+
formatFileSize,
19+
resolveTestRunAttachmentFiles,
20+
} from "../lib/attachments.js";
1421
import { TEST_RESULT_FORMATS, type TestResultFormat, type SSEProgressEvent, type ImportOptions } from "../types.js";
1522

1623
const VALID_FORMATS = ["auto", ...Object.keys(TEST_RESULT_FORMATS)] as const;
@@ -32,6 +39,9 @@ export function createImportCommand(): Command {
3239
.option("-f, --folder <value>", "Parent folder for test cases (ID or exact name)")
3340
.option("-t, --tags <values>", "Tags (comma-separated IDs or names, use quotes for names with commas)")
3441
.option("-r, --test-run <value>", "Existing test run to append results (ID or exact name)")
42+
.option("-d, --attachments-dir <path>", "Base directory for resolving attachment paths (default: directory of test result file)")
43+
.option("--no-attachments", "Skip uploading attachments")
44+
.option("-a, --run-attachments <files...>", "Files to attach to the test run (e.g., test plans, reports)")
3545
.addHelpText("after", `
3646
Examples:
3747
@@ -65,6 +75,15 @@ Examples:
6575
$ TESTPLANIT_URL=https://testplanit.example.com \\
6676
TESTPLANIT_TOKEN=tpi_xxx \\
6777
testplanit import ./junit.xml -p 1 -n "CI Build $BUILD_NUMBER"
78+
79+
Import with attachments from a custom directory:
80+
$ testplanit import ./results.xml -p "My Project" -n "Build" -d ./test-artifacts
81+
82+
Import without uploading attachments:
83+
$ testplanit import ./results.xml -p "My Project" -n "Build" --no-attachments
84+
85+
Attach files to the test run (test plans, reports, etc.):
86+
$ testplanit import ./results.xml -p "My Project" -n "Build" -a ./test-plan.pdf ./coverage-report.html
6887
`)
6988
.action(async (filePatterns: string[], options) => {
7089
// Validate configuration
@@ -208,7 +227,142 @@ Examples:
208227
console.log();
209228
logger.success(`Test run created with ID: ${logger.formatNumber(result.testRunId)}`);
210229

230+
// Handle attachment uploads if present and not disabled
231+
if (
232+
options.attachments !== false &&
233+
result.attachmentMappings &&
234+
result.attachmentMappings.length > 0
235+
) {
236+
console.log();
237+
logger.info("Processing attachments...");
238+
239+
// Resolve attachment paths
240+
const resolvedAttachments = resolveAttachments(
241+
result.attachmentMappings,
242+
filteredFiles,
243+
options.attachmentsDir
244+
);
245+
246+
const summary = getAttachmentSummary(resolvedAttachments);
247+
248+
if (summary.total > 0) {
249+
logger.info(` Found: ${logger.formatNumber(summary.existing)} attachment(s)`);
250+
251+
// Warn about missing files
252+
if (summary.missing > 0) {
253+
logger.warn(` Missing: ${logger.formatNumber(summary.missing)} attachment(s) (skipped)`);
254+
const { missing } = filterExistingAttachments(resolvedAttachments);
255+
for (const attachment of missing.slice(0, 5)) {
256+
logger.dim(` - ${attachment.name}`);
257+
}
258+
if (missing.length > 5) {
259+
logger.dim(` ... and ${missing.length - 5} more`);
260+
}
261+
}
262+
263+
// Upload existing attachments
264+
if (summary.existing > 0) {
265+
const attachSpinner = logger.startSpinner(
266+
`Uploading ${summary.existing} attachment(s) (${formatFileSize(summary.totalSize)})...`
267+
);
268+
269+
try {
270+
const { existing } = filterExistingAttachments(resolvedAttachments);
271+
const uploadResult = await api.uploadAttachmentsBulk(existing);
272+
273+
if (uploadResult.summary.failed > 0) {
274+
logger.succeedSpinner(
275+
`Uploaded ${logger.formatNumber(uploadResult.summary.success)} attachment(s), ${logger.formatNumber(uploadResult.summary.failed)} failed`
276+
);
277+
// Show failed uploads
278+
for (const r of uploadResult.results.filter((r) => !r.success)) {
279+
logger.warn(` Failed: ${r.fileName} - ${r.error || "Unknown error"}`);
280+
}
281+
} else {
282+
logger.succeedSpinner(
283+
`Uploaded ${logger.formatNumber(uploadResult.summary.success)} attachment(s)`
284+
);
285+
}
286+
} catch (attachError) {
287+
logger.failSpinner("Attachment upload failed");
288+
if (attachError instanceof Error) {
289+
logger.warn(` ${attachError.message}`);
290+
}
291+
// Don't exit - import was successful, just attachments failed
292+
}
293+
}
294+
}
295+
}
296+
297+
// Handle test run attachments if provided
298+
if (options.runAttachments && options.runAttachments.length > 0) {
299+
console.log();
300+
logger.info("Processing test run attachments...");
301+
302+
// Expand glob patterns for run attachments
303+
const runAttachmentPaths: string[] = [];
304+
for (const pattern of options.runAttachments as string[]) {
305+
const matches = await glob(pattern, { nodir: true });
306+
if (matches.length === 0) {
307+
// Check if it's a literal file path
308+
if (fs.existsSync(pattern)) {
309+
runAttachmentPaths.push(pattern);
310+
} else {
311+
logger.warn(` No files matched pattern: ${pattern}`);
312+
}
313+
} else {
314+
runAttachmentPaths.push(...matches);
315+
}
316+
}
317+
318+
if (runAttachmentPaths.length > 0) {
319+
const { files: runAttachmentFiles, missing, totalSize } = resolveTestRunAttachmentFiles(runAttachmentPaths);
320+
321+
if (missing.length > 0) {
322+
logger.warn(` Missing: ${logger.formatNumber(missing.length)} file(s) (skipped)`);
323+
for (const missingPath of missing.slice(0, 5)) {
324+
logger.dim(` - ${missingPath}`);
325+
}
326+
if (missing.length > 5) {
327+
logger.dim(` ... and ${missing.length - 5} more`);
328+
}
329+
}
330+
331+
if (runAttachmentFiles.length > 0) {
332+
const runAttachSpinner = logger.startSpinner(
333+
`Uploading ${runAttachmentFiles.length} test run attachment(s) (${formatFileSize(totalSize)})...`
334+
);
335+
336+
try {
337+
const uploadResult = await api.uploadTestRunAttachments(result.testRunId, runAttachmentFiles);
338+
339+
if (uploadResult.summary.failed > 0) {
340+
logger.succeedSpinner(
341+
`Uploaded ${logger.formatNumber(uploadResult.summary.success)} test run attachment(s), ${logger.formatNumber(uploadResult.summary.failed)} failed`
342+
);
343+
// Show failed uploads
344+
for (const r of uploadResult.results.filter((r) => !r.success)) {
345+
logger.warn(` Failed: ${r.fileName} - ${r.error || "Unknown error"}`);
346+
}
347+
} else {
348+
logger.succeedSpinner(
349+
`Uploaded ${logger.formatNumber(uploadResult.summary.success)} test run attachment(s)`
350+
);
351+
}
352+
} catch (runAttachError) {
353+
logger.failSpinner("Test run attachment upload failed");
354+
if (runAttachError instanceof Error) {
355+
logger.warn(` ${runAttachError.message}`);
356+
}
357+
// Don't exit - import was successful, just attachments failed
358+
}
359+
}
360+
}
361+
}
362+
363+
// Show final URL
211364
if (url) {
365+
console.log();
212366
const testRunUrl = `${url}/projects/runs/${projectId}/${result.testRunId}`;
213367
logger.info(`View at: ${logger.formatUrl(testRunUrl)}`);
214368
}

0 commit comments

Comments
 (0)