@supatest/cypress-reporter 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/dist/index.cjs +919 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +109 -0
- package/dist/index.d.ts +109 -0
- package/dist/index.js +884 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../../reporter-core/src/retry.ts","../../reporter-core/src/api-client.ts","../../reporter-core/src/error-collector.ts","../../reporter-core/src/git-utils.ts","../../reporter-core/src/source-reader.ts","../../reporter-core/src/uploader.ts","../../reporter-core/src/utils.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport {\n SupatestApiClient,\n ErrorCollector,\n getLocalGitInfo,\n AttachmentUploader,\n hashKey,\n getFileSize,\n getErrorMessage,\n relativePath,\n logInfo,\n logWarn,\n getCIInfo,\n} from \"@supatest/reporter-core\";\nimport type {\n AttachmentKind,\n AttachmentMeta,\n CreateRunRequest,\n EnvironmentInfo,\n ErrorInfo,\n ParsedTestMetadata,\n ReporterOptions,\n RunSummary,\n StepInfo,\n TestOutcome,\n TestResultEntry,\n TestResultPayload,\n TestStatus,\n} from \"./types.js\";\n\n// Cypress types\ninterface CypressSpec {\n name: string;\n relative: string;\n absolute: string;\n}\n\ninterface CypressTestResult {\n title: string[];\n state: \"passed\" | \"failed\" | \"pending\" | \"skipped\";\n body: string;\n displayError: string | null;\n attempts: Array<{\n state: \"passed\" | \"failed\" | \"pending\";\n error: { message: string; stack: string } | null;\n wallClockStartedAt: string;\n wallClockDuration: number;\n videoTimestamp: number;\n }>;\n}\n\ninterface CypressRunResult {\n status: \"finished\" | \"failed\";\n startedTestsAt: string;\n endedTestsAt: string;\n totalDuration: number;\n totalSuites: number;\n totalTests: number;\n totalFailed: number;\n totalPassed: number;\n totalPending: number;\n totalSkipped: number;\n runs: Array<{\n spec: CypressSpec;\n stats: {\n suites: number;\n tests: number;\n passes: number;\n pending: number;\n skipped: number;\n failures: number;\n wallClockDuration: number;\n };\n tests: CypressTestResult[];\n video: string | null;\n screenshots: Array<{\n name: string;\n path: string;\n height: number;\n width: number;\n }>;\n }>;\n config: {\n version: string;\n baseUrl: string;\n projectRoot: string;\n specPattern: string;\n screenshotsFolder: string;\n videosFolder: string;\n video: boolean;\n };\n browserName: string;\n browserVersion: string;\n}\n\ninterface CypressBeforeRunDetails {\n config: CypressRunResult[\"config\"];\n specs: CypressSpec[];\n browser: { name: string; version: string };\n}\n\ninterface CypressAfterSpecDetails {\n spec: CypressSpec;\n results: {\n stats: CypressRunResult[\"runs\"][0][\"stats\"];\n tests: CypressTestResult[];\n video: string | null;\n screenshots: Array<{ name: string; path: string }>;\n };\n}\n\nconst DEFAULT_API_URL = \"https://code-api.supatest.ai\";\n\nexport type SupatestCypressOptions = {\n projectId?: string;\n apiKey?: string;\n apiUrl?: string;\n uploadAssets?: boolean;\n maxConcurrentUploads?: number;\n retryAttempts?: number;\n timeoutMs?: number;\n dryRun?: boolean;\n};\n\nclass SupatestCypressReporter {\n private options: ReporterOptions;\n private client!: SupatestApiClient;\n private uploader!: AttachmentUploader;\n private errorCollector = new ErrorCollector();\n private runId?: string;\n private uploadQueue: Promise<void>[] = [];\n private startedAt?: string;\n private firstTestStartTime?: number;\n private firstFailureTime?: number;\n private testsProcessed = new Map<string, { status: TestStatus; outcome: TestOutcome; retries: number }>();\n private disabled = false;\n private rootDir?: string;\n private stepIdCounter = 0;\n private cypressVersion?: string;\n private browserName?: string;\n private browserVersion?: string;\n\n constructor(options: SupatestCypressOptions = {}) {\n this.options = {\n projectId: options.projectId || process.env.SUPATEST_PROJECT_ID || \"\",\n apiKey: options.apiKey || process.env.SUPATEST_API_KEY || \"\",\n apiUrl: options.apiUrl || process.env.SUPATEST_API_URL || DEFAULT_API_URL,\n uploadAssets: options.uploadAssets ?? true,\n maxConcurrentUploads: options.maxConcurrentUploads ?? 5,\n retryAttempts: options.retryAttempts ?? 3,\n timeoutMs: options.timeoutMs ?? 30000,\n dryRun: options.dryRun ?? (process.env.SUPATEST_DRY_RUN === \"true\"),\n };\n }\n\n async onBeforeRun(details: CypressBeforeRunDetails): Promise<void> {\n this.rootDir = details.config.projectRoot;\n this.cypressVersion = details.config.version;\n this.browserName = details.browser.name;\n this.browserVersion = details.browser.version;\n\n if (!this.options.projectId || !this.options.apiKey) {\n if (!this.options.dryRun) {\n logWarn(\"Missing projectId or apiKey. Set SUPATEST_PROJECT_ID and SUPATEST_API_KEY.\");\n this.disabled = true;\n return;\n }\n }\n\n this.client = new SupatestApiClient({\n apiKey: this.options.apiKey,\n apiUrl: this.options.apiUrl!,\n timeoutMs: this.options.timeoutMs!,\n retryAttempts: this.options.retryAttempts!,\n dryRun: this.options.dryRun!,\n });\n\n this.uploader = new AttachmentUploader({\n maxConcurrent: this.options.maxConcurrentUploads!,\n timeoutMs: this.options.timeoutMs!,\n dryRun: this.options.dryRun!,\n });\n\n this.startedAt = new Date().toISOString();\n\n try {\n const runRequest: CreateRunRequest = {\n projectId: this.options.projectId,\n startedAt: this.startedAt,\n cypress: {\n version: this.cypressVersion || \"unknown\",\n browser: this.browserName || \"electron\",\n },\n projects: [{\n name: this.browserName || \"electron\",\n browserName: this.browserName,\n }],\n testStats: {\n totalFiles: details.specs.length,\n totalTests: 0, // Unknown at this point\n totalProjects: 1,\n },\n environment: this.getEnvironmentInfo(),\n git: await this.getGitInfo(),\n rootDir: this.rootDir,\n };\n\n const response = await this.client.createRun(runRequest);\n this.runId = response.runId;\n logInfo(`Run ${this.runId} started (${details.specs.length} spec files)`);\n } catch (error) {\n this.errorCollector.recordError(\"RUN_CREATE\", getErrorMessage(error), { error });\n this.disabled = true;\n }\n }\n\n async onAfterSpec(spec: CypressSpec, results: CypressAfterSpecDetails[\"results\"]): Promise<void> {\n if (this.disabled || !this.runId) return;\n\n for (const test of results.tests) {\n await this.processTestResult(spec, test, results);\n }\n }\n\n async onAfterRun(results: CypressRunResult): Promise<void> {\n if (this.disabled || !this.runId) {\n if (this.errorCollector.hasErrors()) {\n console.log(this.errorCollector.formatSummary());\n }\n return;\n }\n\n // Wait for all pending uploads\n await Promise.allSettled(this.uploadQueue);\n\n const summary: RunSummary = {\n total: results.totalTests,\n passed: results.totalPassed,\n failed: results.totalFailed,\n flaky: 0,\n skipped: results.totalSkipped + results.totalPending,\n timedOut: 0,\n interrupted: 0,\n durationMs: results.totalDuration,\n };\n\n try {\n await this.client.completeRun(this.runId, {\n status: results.status === \"finished\" ? \"complete\" : \"errored\",\n endedAt: results.endedTestsAt,\n summary,\n timing: {\n totalDurationMs: results.totalDuration,\n timeToFirstTest: this.firstTestStartTime\n ? this.firstTestStartTime - new Date(this.startedAt!).getTime()\n : undefined,\n timeToFirstFailure: this.firstFailureTime\n ? this.firstFailureTime - new Date(this.startedAt!).getTime()\n : undefined,\n },\n });\n logInfo(`Run ${this.runId} completed`);\n } catch (error) {\n this.errorCollector.recordError(\"RUN_COMPLETE\", getErrorMessage(error), { error });\n }\n\n if (this.errorCollector.hasErrors()) {\n console.log(this.errorCollector.formatSummary());\n }\n }\n\n private async processTestResult(\n spec: CypressSpec,\n test: CypressTestResult,\n specResults: CypressAfterSpecDetails[\"results\"]\n ): Promise<void> {\n const testId = this.getTestId(spec.relative, test.title);\n const status = this.mapCypressStatus(test.state);\n\n // Track first test/failure times\n if (!this.firstTestStartTime && test.attempts[0]) {\n this.firstTestStartTime = new Date(test.attempts[0].wallClockStartedAt).getTime();\n }\n if (!this.firstFailureTime && status === \"failed\") {\n this.firstFailureTime = Date.now();\n }\n\n const lastAttempt = test.attempts[test.attempts.length - 1];\n const resultEntry: TestResultEntry = {\n resultId: hashKey(`${testId}:${test.attempts.length - 1}`),\n retry: test.attempts.length - 1,\n status,\n startTime: lastAttempt?.wallClockStartedAt,\n durationMs: lastAttempt?.wallClockDuration || 0,\n errors: this.extractErrors(test),\n steps: [], // Cypress doesn't expose steps like Playwright\n attachments: this.buildAttachmentMeta(test, specResults),\n };\n\n // Extract tags from test title\n const tags = this.extractTags(test.title.join(\" \"));\n const metadata = this.parseTestMetadata(tags);\n\n const payload: TestResultPayload = {\n testId,\n cypressId: testId,\n file: spec.relative,\n location: { file: spec.relative, line: 0, column: 0 },\n title: test.title[test.title.length - 1],\n titlePath: test.title,\n tags,\n annotations: [],\n expectedStatus: \"passed\",\n outcome: this.getOutcome(test),\n timeout: 0,\n retries: test.attempts.length - 1,\n status,\n durationMs: test.attempts.reduce((sum, a) => sum + (a.wallClockDuration || 0), 0),\n retryCount: test.attempts.length - 1,\n results: [resultEntry],\n projectName: this.browserName || \"electron\",\n metadata,\n };\n\n try {\n await this.client.submitTest(this.runId!, payload);\n\n // Upload attachments\n if (this.options.uploadAssets) {\n await this.uploadAttachments(testId, resultEntry.resultId, specResults, test);\n }\n } catch (error) {\n this.errorCollector.recordError(\"TEST_SUBMISSION\", getErrorMessage(error), {\n testId,\n testTitle: test.title.join(\" > \"),\n error,\n });\n }\n }\n\n private getTestId(file: string, titlePath: string[]): string {\n // Check for @id:XXX in title\n const fullTitle = titlePath.join(\" \");\n const idMatch = fullTitle.match(/@id:([^\\s@]+)/);\n if (idMatch) return idMatch[1];\n\n // Fallback: hash of file + titlePath\n return hashKey(`${file}::${titlePath.join(\"::\")}`);\n }\n\n private mapCypressStatus(state: CypressTestResult[\"state\"]): TestStatus {\n switch (state) {\n case \"passed\": return \"passed\";\n case \"failed\": return \"failed\";\n case \"pending\":\n case \"skipped\": return \"skipped\";\n default: return \"failed\";\n }\n }\n\n private getOutcome(test: CypressTestResult): TestOutcome {\n if (test.state === \"skipped\" || test.state === \"pending\") return \"skipped\";\n if (test.state === \"passed\") {\n // If it passed after retries, it's flaky\n if (test.attempts.length > 1 && test.attempts.some(a => a.state === \"failed\")) {\n return \"flaky\";\n }\n return \"expected\";\n }\n return \"unexpected\";\n }\n\n private extractErrors(test: CypressTestResult): ErrorInfo[] {\n const errors: ErrorInfo[] = [];\n for (const attempt of test.attempts) {\n if (attempt.error) {\n errors.push({\n message: attempt.error.message,\n stack: attempt.error.stack,\n });\n }\n }\n if (test.displayError) {\n errors.push({ message: test.displayError });\n }\n return errors;\n }\n\n private extractTags(title: string): string[] {\n const tagRegex = /@([a-zA-Z][a-zA-Z0-9_:-]*)/g;\n const tags: string[] = [];\n let match;\n while ((match = tagRegex.exec(title)) !== null) {\n tags.push(`@${match[1]}`);\n }\n return tags;\n }\n\n private parseTestMetadata(tags: string[]): ParsedTestMetadata {\n const metadata: ParsedTestMetadata = {\n isSlow: false,\n isFlakyTagged: false,\n customMetadata: {},\n };\n\n for (const tag of tags) {\n const lower = tag.toLowerCase();\n if (lower === \"@slow\") metadata.isSlow = true;\n if (lower === \"@flaky\") metadata.isFlakyTagged = true;\n\n const ownerMatch = tag.match(/@owner:([^\\s@]+)/i);\n if (ownerMatch) metadata.owner = ownerMatch[1];\n\n const priorityMatch = tag.match(/@priority:(critical|high|medium|low)/i);\n if (priorityMatch) metadata.priority = priorityMatch[1].toLowerCase() as any;\n\n const featureMatch = tag.match(/@feature:([^\\s@]+)/i);\n if (featureMatch) metadata.feature = featureMatch[1];\n\n const typeMatch = tag.match(/@test_type:(smoke|e2e|regression|integration|unit)/i);\n if (typeMatch) metadata.testType = typeMatch[1].toLowerCase() as any;\n }\n\n return metadata;\n }\n\n private buildAttachmentMeta(\n test: CypressTestResult,\n specResults: CypressAfterSpecDetails[\"results\"]\n ): AttachmentMeta[] {\n const attachments: AttachmentMeta[] = [];\n\n // Add screenshots for this test\n for (const screenshot of specResults.screenshots) {\n if (screenshot.path) {\n attachments.push({\n name: screenshot.name || \"screenshot\",\n filename: path.basename(screenshot.path),\n contentType: \"image/png\",\n sizeBytes: getFileSize(screenshot.path),\n kind: \"screenshot\",\n });\n }\n }\n\n // Add video if exists\n if (specResults.video) {\n attachments.push({\n name: \"video\",\n filename: path.basename(specResults.video),\n contentType: \"video/mp4\",\n sizeBytes: getFileSize(specResults.video),\n kind: \"video\",\n });\n }\n\n return attachments;\n }\n\n private async uploadAttachments(\n testId: string,\n testResultId: string,\n specResults: CypressAfterSpecDetails[\"results\"],\n test: CypressTestResult\n ): Promise<void> {\n const attachments: { path: string; meta: AttachmentMeta }[] = [];\n\n // Collect screenshots\n for (const screenshot of specResults.screenshots) {\n if (screenshot.path && fs.existsSync(screenshot.path)) {\n attachments.push({\n path: screenshot.path,\n meta: {\n name: screenshot.name || \"screenshot\",\n filename: path.basename(screenshot.path),\n contentType: \"image/png\",\n sizeBytes: getFileSize(screenshot.path),\n kind: \"screenshot\",\n },\n });\n }\n }\n\n // Add video\n if (specResults.video && fs.existsSync(specResults.video)) {\n attachments.push({\n path: specResults.video,\n meta: {\n name: \"video\",\n filename: path.basename(specResults.video),\n contentType: \"video/mp4\",\n sizeBytes: getFileSize(specResults.video),\n kind: \"video\",\n },\n });\n }\n\n if (attachments.length === 0) return;\n\n try {\n const { uploads } = await this.client.signAttachments(this.runId!, {\n testResultId,\n attachments: attachments.map(a => a.meta),\n });\n\n const uploadItems = uploads.map((u, i) => ({\n signedUrl: u.signedUrl,\n filePath: attachments[i].path,\n contentType: attachments[i].meta.contentType,\n }));\n\n await this.uploader.uploadBatch(uploadItems, uploads);\n } catch (error) {\n this.errorCollector.recordError(\"ATTACHMENT_SIGN\", getErrorMessage(error), { error });\n }\n }\n\n private getEnvironmentInfo(): EnvironmentInfo {\n return {\n os: {\n platform: os.platform(),\n release: os.release(),\n arch: os.arch(),\n },\n node: {\n version: process.version,\n },\n machine: {\n cpus: os.cpus().length,\n memory: os.totalmem(),\n hostname: os.hostname(),\n },\n cypress: {\n version: this.cypressVersion || \"unknown\",\n },\n ci: getCIInfo(),\n };\n }\n\n private async getGitInfo() {\n const ciGitInfo = {\n branch: process.env.GITHUB_REF_NAME ?? process.env.CI_COMMIT_BRANCH ?? process.env.GIT_BRANCH,\n commit: process.env.GITHUB_SHA ?? process.env.CI_COMMIT_SHA ?? process.env.GIT_COMMIT,\n repo: process.env.GITHUB_REPOSITORY ?? process.env.CI_PROJECT_PATH,\n author: process.env.GITHUB_ACTOR ?? process.env.GITLAB_USER_NAME,\n };\n\n if (ciGitInfo.branch || ciGitInfo.commit) {\n return ciGitInfo;\n }\n\n return await getLocalGitInfo(this.rootDir);\n }\n}\n\n// Cypress plugin event types\ntype CypressPluginEvents = {\n (action: \"before:run\", fn: (details: CypressBeforeRunDetails) => void | Promise<void>): void;\n (action: \"after:spec\", fn: (spec: CypressSpec, results: CypressAfterSpecDetails[\"results\"]) => void | Promise<void>): void;\n (action: \"after:run\", fn: (results: CypressRunResult) => void | Promise<void>): void;\n (action: string, fn: (...args: unknown[]) => unknown): void;\n};\n\ntype CypressPluginConfigOptions = Record<string, unknown>;\n\n// Main export - the plugin function\nexport default function supatestPlugin(\n on: CypressPluginEvents,\n config: CypressPluginConfigOptions,\n options: SupatestCypressOptions = {}\n): void {\n const reporter = new SupatestCypressReporter(options);\n\n on(\"before:run\", async (details: CypressBeforeRunDetails) => {\n await reporter.onBeforeRun(details);\n });\n\n on(\"after:spec\", async (spec: CypressSpec, results: CypressAfterSpecDetails[\"results\"]) => {\n await reporter.onAfterSpec(spec, results);\n });\n\n on(\"after:run\", async (results: CypressRunResult) => {\n await reporter.onAfterRun(results);\n });\n}\n\n// Re-export ErrorCollector for consumers\nexport { ErrorCollector } from \"@supatest/reporter-core\";\nexport type { ErrorCategory, ErrorSummary } from \"@supatest/reporter-core\";\nexport type { ReporterOptions } from \"./types.js\";\n","export type RetryConfig = {\n\tmaxAttempts: number;\n\tbaseDelayMs: number;\n\tmaxDelayMs: number;\n\tretryOnStatusCodes: number[];\n};\n\nexport const defaultRetryConfig: RetryConfig = {\n\tmaxAttempts: 3,\n\tbaseDelayMs: 1000,\n\tmaxDelayMs: 10000,\n\tretryOnStatusCodes: [408, 429, 500, 502, 503, 504],\n};\n\nfunction sleep(ms: number): Promise<void> {\n\treturn new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction shouldRetry(error: unknown, config: RetryConfig): boolean {\n\tif (error instanceof Error && \"statusCode\" in error) {\n\t\tconst statusCode = (error as Error & { statusCode: number }).statusCode;\n\t\treturn config.retryOnStatusCodes.includes(statusCode);\n\t}\n\t// Network errors should be retried\n\tif (error instanceof Error) {\n\t\tconst networkErrors = [\"ECONNRESET\", \"ETIMEDOUT\", \"ECONNREFUSED\", \"ENOTFOUND\"];\n\t\treturn networkErrors.some((e) => error.message.includes(e));\n\t}\n\treturn false;\n}\n\nexport async function withRetry<T>(\n\tfn: () => Promise<T>,\n\tconfig: RetryConfig = defaultRetryConfig,\n): Promise<T> {\n\tlet lastError: unknown;\n\n\tfor (let attempt = 1; attempt <= config.maxAttempts; attempt++) {\n\t\ttry {\n\t\t\treturn await fn();\n\t\t} catch (error) {\n\t\t\tlastError = error;\n\n\t\t\tif (attempt === config.maxAttempts) {\n\t\t\t\tthrow error;\n\t\t\t}\n\n\t\t\tif (!shouldRetry(error, config)) {\n\t\t\t\tthrow error;\n\t\t\t}\n\n\t\t\tconst delay = Math.min(\n\t\t\t\tconfig.baseDelayMs * Math.pow(2, attempt - 1),\n\t\t\t\tconfig.maxDelayMs,\n\t\t\t);\n\n\t\t\tawait sleep(delay);\n\t\t}\n\t}\n\n\tthrow lastError;\n}\n","import { defaultRetryConfig, type RetryConfig, withRetry } from \"./retry.js\";\nimport type {\n\tCompleteRunRequest,\n\tCreateRunResponse,\n\tSignAttachmentsRequest,\n\tSignAttachmentsResponse,\n} from \"./types.js\";\n\nexport type ApiClientOptions = {\n\tapiKey: string;\n\tapiUrl: string;\n\ttimeoutMs: number;\n\tretryAttempts: number;\n\tdryRun: boolean;\n};\n\nexport class SupatestApiClient {\n\tprivate readonly options: ApiClientOptions;\n\tprivate readonly retryConfig: RetryConfig;\n\n\tconstructor(options: ApiClientOptions) {\n\t\tthis.options = options;\n\t\tthis.retryConfig = {\n\t\t\t...defaultRetryConfig,\n\t\t\tmaxAttempts: options.retryAttempts,\n\t\t};\n\t}\n\n\tasync createRun(data: unknown): Promise<CreateRunResponse> {\n\t\tif (this.options.dryRun) {\n\t\t\tthis.logPayload(\"POST /v1/runs\", data);\n\t\t\t// Return mock response for dry run\n\t\t\treturn {\n\t\t\t\trunId: `mock_run_${Date.now()}`,\n\t\t\t\tstatus: \"running\",\n\t\t\t};\n\t\t}\n\n\t\treturn this.request<CreateRunResponse>(\"POST\", \"/v1/runs\", data);\n\t}\n\n\tasync submitTest(runId: string, data: unknown): Promise<void> {\n\t\tif (this.options.dryRun) {\n\t\t\tthis.logPayload(`POST /v1/runs/${runId}/tests`, data);\n\t\t\treturn;\n\t\t}\n\n\t\tawait this.request(\"POST\", `/v1/runs/${runId}/tests`, data);\n\t}\n\n\tasync signAttachments(\n\t\trunId: string,\n\t\tdata: SignAttachmentsRequest,\n\t): Promise<SignAttachmentsResponse> {\n\t\tif (this.options.dryRun) {\n\t\t\tthis.logPayload(`POST /v1/runs/${runId}/attachments/sign`, data);\n\t\t\t// Return mock signed URLs for dry run\n\t\t\treturn {\n\t\t\t\tuploads: data.attachments.map((att, i) => ({\n\t\t\t\t\tattachmentId: `mock_att_${i}_${Date.now()}`,\n\t\t\t\t\tsignedUrl: `https://mock-s3.example.com/uploads/${att.filename}`,\n\t\t\t\t\texpiresAt: new Date(Date.now() + 15 * 60 * 1000).toISOString(),\n\t\t\t\t})),\n\t\t\t};\n\t\t}\n\n\t\treturn this.request<SignAttachmentsResponse>(\n\t\t\t\"POST\",\n\t\t\t`/v1/runs/${runId}/attachments/sign`,\n\t\t\tdata,\n\t\t);\n\t}\n\n\tasync completeRun(runId: string, data: CompleteRunRequest): Promise<void> {\n\t\tif (this.options.dryRun) {\n\t\t\tthis.logPayload(`POST /v1/runs/${runId}/complete`, data);\n\t\t\treturn;\n\t\t}\n\n\t\tawait this.request(\"POST\", `/v1/runs/${runId}/complete`, data);\n\t}\n\n\tprivate async request<T>(\n\t\tmethod: string,\n\t\tpath: string,\n\t\tbody?: unknown,\n\t): Promise<T> {\n\t\tconst url = `${this.options.apiUrl}${path}`;\n\n\t\treturn withRetry(async () => {\n\t\t\tconst controller = new AbortController();\n\t\t\tconst timeoutId = setTimeout(\n\t\t\t\t() => controller.abort(),\n\t\t\t\tthis.options.timeoutMs,\n\t\t\t);\n\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(url, {\n\t\t\t\t\tmethod,\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t\tAuthorization: `Bearer ${this.options.apiKey}`,\n\t\t\t\t\t},\n\t\t\t\t\tbody: body ? JSON.stringify(body) : undefined,\n\t\t\t\t\tsignal: controller.signal,\n\t\t\t\t});\n\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tconst error = new Error(\n\t\t\t\t\t\t`API request failed: ${response.status} ${response.statusText}`,\n\t\t\t\t\t) as Error & { statusCode: number };\n\t\t\t\t\terror.statusCode = response.status;\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\n\t\t\t\tconst text = await response.text();\n\t\t\t\treturn text ? (JSON.parse(text) as T) : ({} as T);\n\t\t\t} finally {\n\t\t\t\tclearTimeout(timeoutId);\n\t\t\t}\n\t\t}, this.retryConfig);\n\t}\n\n\tprivate logPayload(endpoint: string, data: unknown): void {\n\t\tconsole.log(`\\n[supatest][dry-run] ${endpoint}`);\n\t\tconsole.log(JSON.stringify(data, null, 2));\n\t}\n}\n","import type { ErrorCategory, ErrorSummary, ReporterError } from \"./types.js\";\n\nexport class ErrorCollector {\n\tprivate errors: ReporterError[] = [];\n\n\trecordError(\n\t\tcategory: ErrorCategory,\n\t\tmessage: string,\n\t\tcontext?: {\n\t\t\ttestId?: string;\n\t\t\ttestTitle?: string;\n\t\t\tattachmentName?: string;\n\t\t\tfilePath?: string;\n\t\t\terror?: unknown;\n\t\t},\n\t): void {\n\t\tconst error: ReporterError = {\n\t\t\tcategory,\n\t\t\tmessage,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\ttestId: context?.testId,\n\t\t\ttestTitle: context?.testTitle,\n\t\t\tattachmentName: context?.attachmentName,\n\t\t\tfilePath: context?.filePath,\n\t\t\toriginalError:\n\t\t\t\tcontext?.error instanceof Error ? context.error.stack : undefined,\n\t\t};\n\n\t\tthis.errors.push(error);\n\t}\n\n\tgetSummary(): ErrorSummary {\n\t\tconst byCategory: Record<ErrorCategory, number> = {\n\t\t\tRUN_CREATE: 0,\n\t\t\tTEST_SUBMISSION: 0,\n\t\t\tATTACHMENT_SIGN: 0,\n\t\t\tATTACHMENT_UPLOAD: 0,\n\t\t\tFILE_READ: 0,\n\t\t\tRUN_COMPLETE: 0,\n\t\t};\n\n\t\tfor (const error of this.errors) {\n\t\t\tbyCategory[error.category]++;\n\t\t}\n\n\t\treturn {\n\t\t\ttotalErrors: this.errors.length,\n\t\t\tbyCategory,\n\t\t\terrors: this.errors,\n\t\t};\n\t}\n\n\thasErrors(): boolean {\n\t\treturn this.errors.length > 0;\n\t}\n\n\tformatSummary(): string {\n\t\tif (this.errors.length === 0) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tconst summary = this.getSummary();\n\t\tconst lines: string[] = [\n\t\t\t\"\",\n\t\t\t\"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\",\n\t\t\t\"⚠️ Supatest Reporter - Error Summary\",\n\t\t\t\"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\",\n\t\t\t`Total Errors: ${summary.totalErrors}`,\n\t\t\t\"\",\n\t\t];\n\n\t\t// Show errors by category\n\t\tfor (const [category, count] of Object.entries(summary.byCategory)) {\n\t\t\tif (count === 0) continue;\n\n\t\t\tconst icon = this.getCategoryIcon(category as ErrorCategory);\n\t\t\tconst label = this.getCategoryLabel(category as ErrorCategory);\n\t\t\tlines.push(`${icon} ${label}: ${count}`);\n\n\t\t\t// Show first 3 errors in this category\n\t\t\tconst categoryErrors = this.errors\n\t\t\t\t.filter((e) => e.category === category)\n\t\t\t\t.slice(0, 3);\n\n\t\t\tfor (const error of categoryErrors) {\n\t\t\t\tlines.push(` • ${this.formatError(error)}`);\n\t\t\t}\n\n\t\t\tconst remaining = count - categoryErrors.length;\n\t\t\tif (remaining > 0) {\n\t\t\t\tlines.push(` ... and ${remaining} more`);\n\t\t\t}\n\t\t\tlines.push(\"\");\n\t\t}\n\n\t\tlines.push(\n\t\t\t\"ℹ️ Note: Test execution was not interrupted. Some results may not be visible in the dashboard.\",\n\t\t);\n\t\tlines.push(\n\t\t\t\"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\",\n\t\t);\n\n\t\treturn lines.join(\"\\n\");\n\t}\n\n\tprivate getCategoryIcon(category: ErrorCategory): string {\n\t\tconst icons: Record<ErrorCategory, string> = {\n\t\t\tRUN_CREATE: \"🚀\",\n\t\t\tTEST_SUBMISSION: \"📝\",\n\t\t\tATTACHMENT_SIGN: \"🔐\",\n\t\t\tATTACHMENT_UPLOAD: \"📎\",\n\t\t\tFILE_READ: \"📁\",\n\t\t\tRUN_COMPLETE: \"🏁\",\n\t\t};\n\t\treturn icons[category];\n\t}\n\n\tprivate getCategoryLabel(category: ErrorCategory): string {\n\t\tconst labels: Record<ErrorCategory, string> = {\n\t\t\tRUN_CREATE: \"Run Initialization\",\n\t\t\tTEST_SUBMISSION: \"Test Result Submission\",\n\t\t\tATTACHMENT_SIGN: \"Attachment Signing\",\n\t\t\tATTACHMENT_UPLOAD: \"Attachment Upload\",\n\t\t\tFILE_READ: \"File Access\",\n\t\t\tRUN_COMPLETE: \"Run Completion\",\n\t\t};\n\t\treturn labels[category];\n\t}\n\n\tprivate formatError(error: ReporterError): string {\n\t\tconst parts: string[] = [];\n\n\t\tif (error.testTitle) {\n\t\t\tparts.push(`Test: \"${error.testTitle}\"`);\n\t\t}\n\n\t\tif (error.attachmentName) {\n\t\t\tparts.push(`Attachment: \"${error.attachmentName}\"`);\n\t\t}\n\n\t\tif (error.filePath) {\n\t\t\tparts.push(`File: ${error.filePath}`);\n\t\t}\n\n\t\tparts.push(error.message);\n\n\t\treturn parts.join(\" - \");\n\t}\n}\n","import { type SimpleGit, simpleGit } from \"simple-git\";\n\nexport type GitInfo = {\n\tbranch?: string;\n\tcommit?: string;\n\tcommitMessage?: string;\n\trepo?: string;\n\tauthor?: string;\n\tauthorEmail?: string;\n\ttag?: string;\n\tdirty?: boolean;\n};\n\n/**\n * Get git information from the local repository\n * Falls back gracefully if git is not available or not a git repo\n */\nexport async function getLocalGitInfo(rootDir?: string): Promise<GitInfo> {\n\ttry {\n\t\tconst git: SimpleGit = simpleGit(rootDir || process.cwd());\n\n\t\t// Check if it's a git repository\n\t\tconst isRepo = await git.checkIsRepo();\n\t\tif (!isRepo) {\n\t\t\treturn {};\n\t\t}\n\n\t\tconst [branchResult, commitResult, status, remotes] = await Promise.all([\n\t\t\tgit.revparse([\"--abbrev-ref\", \"HEAD\"]).catch(() => undefined),\n\t\t\tgit.revparse([\"HEAD\"]).catch(() => undefined),\n\t\t\tgit.status().catch(() => undefined),\n\t\t\tgit.getRemotes(true).catch(() => []),\n\t\t]);\n\n\t\tconst branch = typeof branchResult === \"string\" ? branchResult.trim() : undefined;\n\t\tconst commit = typeof commitResult === \"string\" ? commitResult.trim() : undefined;\n\n\t\t// Get commit message (subject line)\n\t\tlet commitMessage: string | undefined;\n\t\ttry {\n\t\t\tconst logOutput = await git.raw([\"log\", \"-1\", \"--format=%s\"]);\n\t\t\tcommitMessage = typeof logOutput === \"string\" ? logOutput.trim() : undefined;\n\t\t} catch {\n\t\t\t// Ignore error\n\t\t}\n\n\t\t// Get author info from config\n\t\tlet author: string | undefined;\n\t\tlet authorEmail: string | undefined;\n\t\ttry {\n\t\t\tconst authorConfig = await git\n\t\t\t\t.getConfig(\"user.name\")\n\t\t\t\t.catch(() => undefined);\n\t\t\tconst emailConfig = await git\n\t\t\t\t.getConfig(\"user.email\")\n\t\t\t\t.catch(() => undefined);\n\t\t\tauthor =\n\t\t\t\tauthorConfig && typeof (authorConfig as any).value === \"string\"\n\t\t\t\t\t? (authorConfig as any).value.trim()\n\t\t\t\t\t: undefined;\n\t\t\tauthorEmail =\n\t\t\t\temailConfig && typeof (emailConfig as any).value === \"string\"\n\t\t\t\t\t? (emailConfig as any).value.trim()\n\t\t\t\t\t: undefined;\n\t\t} catch {\n\t\t\t// Ignore error\n\t\t}\n\n\t\t// Get current tag if on a tag\n\t\tlet tag: string | undefined;\n\t\ttry {\n\t\t\tconst tagResult = await git.raw([\"describe\", \"--tags\", \"--exact-match\"]).catch(() => undefined);\n\t\t\ttag = typeof tagResult === \"string\" ? tagResult.trim() : undefined;\n\t\t} catch {\n\t\t\t// Ignore error - not on a tag\n\t\t}\n\n\t\t// Get repository URL\n\t\tlet repo: string | undefined;\n\t\tconst remote = remotes && remotes.length > 0 ? remotes[0] : null;\n\t\tif (remote && remote.refs && remote.refs.fetch) {\n\t\t\trepo = remote.refs.fetch;\n\t\t}\n\n\t\t// Check for uncommitted changes\n\t\tconst dirty = status ? status.modified.length > 0 || status.created.length > 0 || status.deleted.length > 0 : false;\n\n\t\treturn {\n\t\t\tbranch,\n\t\t\tcommit,\n\t\t\tcommitMessage,\n\t\t\trepo,\n\t\t\tauthor,\n\t\t\tauthorEmail,\n\t\t\ttag,\n\t\t\tdirty,\n\t\t};\n\t} catch (error) {\n\t\t// If anything goes wrong, return empty object\n\t\t// This is a best-effort operation\n\t\treturn {};\n\t}\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { SourceSnippet } from \"./types.js\";\n\nconst CONTEXT_LINES_BEFORE = 3;\nconst CONTEXT_LINES_AFTER = 3;\nconst MAX_SNIPPET_SIZE = 2000; // Characters\n\ntype SourceCache = Map<string, string[]>;\ntype FileIndex = Map<string, string>; // filename -> absolute path\n\n/**\n * Utility class for reading source files and extracting code snippets.\n * Caches file contents to avoid repeated disk reads.\n * Builds an index of source files to resolve partial paths.\n */\nexport class SourceReader {\n\tprivate cache: SourceCache = new Map();\n\tprivate fileIndex: FileIndex = new Map();\n\tprivate rootDir: string;\n\n\tconstructor(rootDir: string) {\n\t\tthis.rootDir = rootDir;\n\t\tthis.buildFileIndex(rootDir);\n\t}\n\n\t/**\n\t * Recursively scan directory and build index of .ts/.tsx/.js/.jsx files.\n\t */\n\tprivate buildFileIndex(dir: string): void {\n\t\ttry {\n\t\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\t\t\tfor (const entry of entries) {\n\t\t\t\tconst fullPath = path.join(dir, entry.name);\n\t\t\t\tif (entry.isDirectory()) {\n\t\t\t\t\t// Skip node_modules and hidden directories\n\t\t\t\t\tif (entry.name === \"node_modules\" || entry.name.startsWith(\".\")) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tthis.buildFileIndex(fullPath);\n\t\t\t\t} else if (entry.isFile()) {\n\t\t\t\t\tconst ext = path.extname(entry.name).toLowerCase();\n\t\t\t\t\tif ([\".ts\", \".tsx\", \".js\", \".jsx\"].includes(ext)) {\n\t\t\t\t\t\t// Store by filename (for partial path matching)\n\t\t\t\t\t\tconst filename = entry.name;\n\t\t\t\t\t\t// If multiple files have same name, keep first found\n\t\t\t\t\t\tif (!this.fileIndex.has(filename)) {\n\t\t\t\t\t\t\tthis.fileIndex.set(filename, fullPath);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Also store by relative path from rootDir\n\t\t\t\t\t\tconst relativePath = path.relative(this.rootDir, fullPath);\n\t\t\t\t\t\tthis.fileIndex.set(relativePath, fullPath);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Directory not readable, skip\n\t\t}\n\t}\n\n\t/**\n\t * Resolve a file path to an absolute path.\n\t * Handles absolute paths, relative paths, and filename-only paths.\n\t */\n\tprivate resolvePath(file: string): string | undefined {\n\t\t// Already absolute and exists\n\t\tif (path.isAbsolute(file)) {\n\t\t\tif (fs.existsSync(file)) {\n\t\t\t\treturn file;\n\t\t\t}\n\t\t\t// Try just the filename\n\t\t\tconst filename = path.basename(file);\n\t\t\treturn this.fileIndex.get(filename);\n\t\t}\n\n\t\t// Try as relative path from rootDir\n\t\tconst asRelative = path.join(this.rootDir, file);\n\t\tif (fs.existsSync(asRelative)) {\n\t\t\treturn asRelative;\n\t\t}\n\n\t\t// Try from index (exact match on relative path or filename)\n\t\tif (this.fileIndex.has(file)) {\n\t\t\treturn this.fileIndex.get(file);\n\t\t}\n\n\t\t// Try just the filename\n\t\tconst filename = path.basename(file);\n\t\treturn this.fileIndex.get(filename);\n\t}\n\n\t/**\n\t * Get a code snippet around a specific line in a file.\n\t * Returns undefined if the file cannot be read or snippet is too large.\n\t */\n\tgetSnippet(file: string, line: number): SourceSnippet | undefined {\n\t\ttry {\n\t\t\tconst absolutePath = this.resolvePath(file);\n\t\t\tif (!absolutePath) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\t// Get or cache file lines\n\t\t\tlet lines = this.cache.get(absolutePath);\n\t\t\tif (!lines) {\n\t\t\t\tconst content = fs.readFileSync(absolutePath, \"utf-8\");\n\t\t\t\tlines = content.split(\"\\n\");\n\t\t\t\tthis.cache.set(absolutePath, lines);\n\t\t\t}\n\n\t\t\t// Validate line number\n\t\t\tif (line < 1 || line > lines.length) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\t// Calculate snippet range\n\t\t\tconst startLine = Math.max(1, line - CONTEXT_LINES_BEFORE);\n\t\t\tconst endLine = Math.min(lines.length, line + CONTEXT_LINES_AFTER);\n\n\t\t\t// Extract snippet\n\t\t\tconst snippetLines = lines.slice(startLine - 1, endLine);\n\t\t\tconst code = snippetLines.join(\"\\n\");\n\n\t\t\t// Check size limit\n\t\t\tif (code.length > MAX_SNIPPET_SIZE) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\t// Detect language from file extension\n\t\t\tconst ext = path.extname(file).toLowerCase();\n\t\t\tconst language =\n\t\t\t\text === \".ts\" || ext === \".tsx\" ? \"typescript\" : \"javascript\";\n\n\t\t\treturn {\n\t\t\t\tcode,\n\t\t\t\tstartLine,\n\t\t\t\thighlightLine: line,\n\t\t\t\tlanguage,\n\t\t\t};\n\t\t} catch {\n\t\t\t// Silently fail - snippet is optional\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Clear the file cache and index to free memory.\n\t */\n\tclear(): void {\n\t\tthis.cache.clear();\n\t\tthis.fileIndex.clear();\n\t}\n}\n","import fs from \"node:fs\";\nimport { defaultRetryConfig, withRetry } from \"./retry.js\";\nimport type { SignedUpload, UploadItem, UploadResult } from \"./types.js\";\n\nexport type UploaderOptions = {\n\tmaxConcurrent: number;\n\ttimeoutMs: number;\n\tdryRun: boolean;\n};\n\nexport class AttachmentUploader {\n\tprivate readonly options: UploaderOptions;\n\n\tconstructor(options: UploaderOptions) {\n\t\tthis.options = options;\n\t}\n\n\tasync upload(\n\t\tsignedUrl: string,\n\t\tfilePath: string,\n\t\tcontentType: string,\n\t): Promise<void> {\n\t\tif (this.options.dryRun) {\n\t\t\ttry {\n\t\t\t\tconst stats = fs.statSync(filePath);\n\t\t\t\tconsole.log(\n\t\t\t\t\t`[supatest][dry-run] Would upload ${filePath} (${stats.size} bytes) to S3`,\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`[supatest][dry-run] Cannot access file ${filePath}: ${message}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tlet fileBuffer: Buffer;\n\t\ttry {\n\t\t\tfileBuffer = await fs.promises.readFile(filePath);\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tthrow new Error(`Failed to read file ${filePath}: ${message}`);\n\t\t}\n\n\t\tawait withRetry(async () => {\n\t\t\tconst controller = new AbortController();\n\t\t\tconst timeoutId = setTimeout(\n\t\t\t\t() => controller.abort(),\n\t\t\t\tthis.options.timeoutMs,\n\t\t\t);\n\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(signedUrl, {\n\t\t\t\t\tmethod: \"PUT\",\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t\"Content-Type\": contentType,\n\t\t\t\t\t\t\"Content-Length\": String(fileBuffer.length),\n\t\t\t\t\t},\n\t\t\t\t\tbody: new Uint8Array(fileBuffer),\n\t\t\t\t\tsignal: controller.signal,\n\t\t\t\t});\n\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tconst error = new Error(\n\t\t\t\t\t\t`S3 upload failed: ${response.status} ${response.statusText}`,\n\t\t\t\t\t) as Error & { statusCode: number };\n\t\t\t\t\terror.statusCode = response.status;\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\tclearTimeout(timeoutId);\n\t\t\t}\n\t\t}, defaultRetryConfig);\n\t}\n\n\tasync uploadBatch(\n\t\titems: UploadItem[],\n\t\tsignedUploads: SignedUpload[],\n\t): Promise<UploadResult[]> {\n\t\t// Use dynamic import for p-limit (ESM module)\n\t\tconst pLimit = (await import(\"p-limit\")).default;\n\t\tconst limit = pLimit(this.options.maxConcurrent);\n\n\t\tconst results = await Promise.allSettled(\n\t\t\titems.map((item, index) =>\n\t\t\t\tlimit(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait this.upload(item.signedUrl, item.filePath, item.contentType);\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tsuccess: true,\n\t\t\t\t\t\t\tattachmentId: signedUploads[index]?.attachmentId,\n\t\t\t\t\t\t};\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t// Enhanced error with file context\n\t\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tsuccess: false,\n\t\t\t\t\t\t\tattachmentId: signedUploads[index]?.attachmentId,\n\t\t\t\t\t\t\terror: `${item.filePath}: ${message}`,\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\t\t\t}),\n\t\t\t),\n\t\t);\n\n\t\treturn results.map((result, index) => {\n\t\t\tif (result.status === \"fulfilled\") {\n\t\t\t\treturn result.value;\n\t\t\t}\n\t\t\t// Shouldn't happen since we catch inside, but handle it\n\t\t\tconst message = result.reason instanceof Error ? result.reason.message : String(result.reason);\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\tattachmentId: signedUploads[index]?.attachmentId,\n\t\t\t\terror: message,\n\t\t\t};\n\t\t});\n\t}\n}\n","import { createHash } from \"node:crypto\";\nimport fs from \"node:fs\";\nimport os from \"node:os\";\nimport type { BaseEnvironmentInfo, CIInfo } from \"./types.js\";\n\n/**\n * SHA256 hash, truncated to 12 characters.\n */\nexport function hashKey(value: string): string {\n\treturn createHash(\"sha256\").update(value).digest(\"hex\").slice(0, 12);\n}\n\n/**\n * Get file size in bytes, returns 0 on error.\n */\nexport function getFileSize(filePath: string): number {\n\ttry {\n\t\treturn fs.statSync(filePath).size;\n\t} catch {\n\t\treturn 0;\n\t}\n}\n\n/**\n * Extract error message from unknown error.\n */\nexport function getErrorMessage(error: unknown): string {\n\tif (error instanceof Error) return error.message;\n\treturn String(error);\n}\n\n/**\n * Make a file path relative to rootDir.\n * Strips `file://` or `file:///` prefixes before processing.\n */\nexport function relativePath(filePath: string, rootDir?: string): string {\n\t// Remove file:// or file:/// prefix if present\n\tlet normalizedPath = filePath;\n\tif (normalizedPath.startsWith(\"file:///\")) {\n\t\tnormalizedPath = normalizedPath.slice(7);\n\t} else if (normalizedPath.startsWith(\"file://\")) {\n\t\tnormalizedPath = normalizedPath.slice(7);\n\t}\n\n\t// Make relative to rootDir\n\tif (rootDir && normalizedPath.startsWith(rootDir)) {\n\t\treturn normalizedPath.slice(rootDir.length + 1);\n\t}\n\treturn filePath;\n}\n\n/**\n * Log an informational message with [supatest] prefix.\n */\nexport function logInfo(message: string): void {\n\tconsole.log(`[supatest] ${message}`);\n}\n\n/**\n * Log a warning message with [supatest] prefix.\n */\nexport function logWarn(message: string): void {\n\tconsole.warn(`[supatest] ${message}`);\n}\n\n/**\n * Detect CI environment and return CI info for 7 providers:\n * GitHub Actions, GitLab CI, Jenkins, CircleCI, Travis CI, Buildkite, Azure Pipelines.\n */\nexport function getCIInfo(): CIInfo | undefined {\n\tif (process.env.GITHUB_ACTIONS) {\n\t\treturn {\n\t\t\tprovider: \"github-actions\",\n\t\t\trunId: process.env.GITHUB_RUN_ID,\n\t\t\tjobId: process.env.GITHUB_JOB,\n\t\t\tjobUrl: `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`,\n\t\t\tbuildNumber: process.env.GITHUB_RUN_NUMBER,\n\t\t\tbranch: process.env.GITHUB_REF_NAME,\n\t\t\tpullRequest: process.env.GITHUB_EVENT_NAME === \"pull_request\"\n\t\t\t\t? {\n\t\t\t\t\t\tnumber: process.env.GITHUB_PR_NUMBER ?? \"\",\n\t\t\t\t\t\turl: `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/pull/${process.env.GITHUB_PR_NUMBER}`,\n\t\t\t\t }\n\t\t\t\t: undefined,\n\t\t};\n\t}\n\tif (process.env.GITLAB_CI) {\n\t\treturn {\n\t\t\tprovider: \"gitlab-ci\",\n\t\t\trunId: process.env.CI_PIPELINE_ID,\n\t\t\tjobId: process.env.CI_JOB_ID,\n\t\t\tjobUrl: process.env.CI_JOB_URL,\n\t\t\tbuildNumber: process.env.CI_PIPELINE_IID,\n\t\t\tbranch: process.env.CI_COMMIT_BRANCH,\n\t\t\tpullRequest: process.env.CI_MERGE_REQUEST_IID\n\t\t\t\t? {\n\t\t\t\t\t\tnumber: process.env.CI_MERGE_REQUEST_IID,\n\t\t\t\t\t\turl: process.env.CI_MERGE_REQUEST_PROJECT_URL + \"/-/merge_requests/\" + process.env.CI_MERGE_REQUEST_IID,\n\t\t\t\t\t\ttitle: process.env.CI_MERGE_REQUEST_TITLE,\n\t\t\t\t }\n\t\t\t\t: undefined,\n\t\t};\n\t}\n\tif (process.env.JENKINS_URL) {\n\t\treturn {\n\t\t\tprovider: \"jenkins\",\n\t\t\trunId: process.env.BUILD_ID,\n\t\t\tjobUrl: process.env.BUILD_URL,\n\t\t\tbuildNumber: process.env.BUILD_NUMBER,\n\t\t\tbranch: process.env.GIT_BRANCH,\n\t\t};\n\t}\n\tif (process.env.CIRCLECI) {\n\t\treturn {\n\t\t\tprovider: \"circleci\",\n\t\t\trunId: process.env.CIRCLE_WORKFLOW_ID,\n\t\t\tjobId: process.env.CIRCLE_JOB,\n\t\t\tjobUrl: process.env.CIRCLE_BUILD_URL,\n\t\t\tbuildNumber: process.env.CIRCLE_BUILD_NUM,\n\t\t\tbranch: process.env.CIRCLE_BRANCH,\n\t\t\tpullRequest: process.env.CIRCLE_PULL_REQUEST\n\t\t\t\t? {\n\t\t\t\t\t\tnumber: process.env.CIRCLE_PR_NUMBER ?? \"\",\n\t\t\t\t\t\turl: process.env.CIRCLE_PULL_REQUEST,\n\t\t\t\t }\n\t\t\t\t: undefined,\n\t\t};\n\t}\n\tif (process.env.TRAVIS) {\n\t\treturn {\n\t\t\tprovider: \"travis\",\n\t\t\trunId: process.env.TRAVIS_BUILD_ID,\n\t\t\tjobId: process.env.TRAVIS_JOB_ID,\n\t\t\tjobUrl: process.env.TRAVIS_JOB_WEB_URL,\n\t\t\tbuildNumber: process.env.TRAVIS_BUILD_NUMBER,\n\t\t\tbranch: process.env.TRAVIS_BRANCH,\n\t\t\tpullRequest: process.env.TRAVIS_PULL_REQUEST !== \"false\"\n\t\t\t\t? {\n\t\t\t\t\t\tnumber: process.env.TRAVIS_PULL_REQUEST ?? \"\",\n\t\t\t\t\t\turl: `https://github.com/${process.env.TRAVIS_REPO_SLUG}/pull/${process.env.TRAVIS_PULL_REQUEST}`,\n\t\t\t\t }\n\t\t\t\t: undefined,\n\t\t};\n\t}\n\tif (process.env.BUILDKITE) {\n\t\treturn {\n\t\t\tprovider: \"buildkite\",\n\t\t\trunId: process.env.BUILDKITE_BUILD_ID,\n\t\t\tjobId: process.env.BUILDKITE_JOB_ID,\n\t\t\tjobUrl: process.env.BUILDKITE_BUILD_URL,\n\t\t\tbuildNumber: process.env.BUILDKITE_BUILD_NUMBER,\n\t\t\tbranch: process.env.BUILDKITE_BRANCH,\n\t\t};\n\t}\n\tif (process.env.AZURE_PIPELINES || process.env.TF_BUILD) {\n\t\treturn {\n\t\t\tprovider: \"azure-pipelines\",\n\t\t\trunId: process.env.BUILD_BUILDID,\n\t\t\tjobUrl: `${process.env.SYSTEM_COLLECTIONURI}${process.env.SYSTEM_TEAMPROJECT}/_build/results?buildId=${process.env.BUILD_BUILDID}`,\n\t\t\tbuildNumber: process.env.BUILD_BUILDNUMBER,\n\t\t\tbranch: process.env.BUILD_SOURCEBRANCH,\n\t\t};\n\t}\n\treturn undefined;\n}\n\n/**\n * Get base environment info (OS, Node, machine) without framework-specific fields.\n */\nexport function getBaseEnvironmentInfo(): BaseEnvironmentInfo {\n\treturn {\n\t\tos: {\n\t\t\tplatform: os.platform(),\n\t\t\trelease: os.release(),\n\t\t\tarch: os.arch(),\n\t\t},\n\t\tnode: {\n\t\t\tversion: process.version,\n\t\t},\n\t\tmachine: {\n\t\t\tcpus: os.cpus().length,\n\t\t\tmemory: os.totalmem(),\n\t\t\thostname: os.hostname(),\n\t\t},\n\t\tci: getCIInfo(),\n\t};\n}\n"],"mappings":"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","names":["import_fs","path","fs","path","fs","os"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { BaseReporterOptions } from '@supatest/reporter-core';
|
|
2
|
+
export { ErrorCategory, ErrorCollector, ErrorSummary } from '@supatest/reporter-core';
|
|
3
|
+
|
|
4
|
+
type ReporterOptions = BaseReporterOptions;
|
|
5
|
+
|
|
6
|
+
interface CypressSpec {
|
|
7
|
+
name: string;
|
|
8
|
+
relative: string;
|
|
9
|
+
absolute: string;
|
|
10
|
+
}
|
|
11
|
+
interface CypressTestResult {
|
|
12
|
+
title: string[];
|
|
13
|
+
state: "passed" | "failed" | "pending" | "skipped";
|
|
14
|
+
body: string;
|
|
15
|
+
displayError: string | null;
|
|
16
|
+
attempts: Array<{
|
|
17
|
+
state: "passed" | "failed" | "pending";
|
|
18
|
+
error: {
|
|
19
|
+
message: string;
|
|
20
|
+
stack: string;
|
|
21
|
+
} | null;
|
|
22
|
+
wallClockStartedAt: string;
|
|
23
|
+
wallClockDuration: number;
|
|
24
|
+
videoTimestamp: number;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
interface CypressRunResult {
|
|
28
|
+
status: "finished" | "failed";
|
|
29
|
+
startedTestsAt: string;
|
|
30
|
+
endedTestsAt: string;
|
|
31
|
+
totalDuration: number;
|
|
32
|
+
totalSuites: number;
|
|
33
|
+
totalTests: number;
|
|
34
|
+
totalFailed: number;
|
|
35
|
+
totalPassed: number;
|
|
36
|
+
totalPending: number;
|
|
37
|
+
totalSkipped: number;
|
|
38
|
+
runs: Array<{
|
|
39
|
+
spec: CypressSpec;
|
|
40
|
+
stats: {
|
|
41
|
+
suites: number;
|
|
42
|
+
tests: number;
|
|
43
|
+
passes: number;
|
|
44
|
+
pending: number;
|
|
45
|
+
skipped: number;
|
|
46
|
+
failures: number;
|
|
47
|
+
wallClockDuration: number;
|
|
48
|
+
};
|
|
49
|
+
tests: CypressTestResult[];
|
|
50
|
+
video: string | null;
|
|
51
|
+
screenshots: Array<{
|
|
52
|
+
name: string;
|
|
53
|
+
path: string;
|
|
54
|
+
height: number;
|
|
55
|
+
width: number;
|
|
56
|
+
}>;
|
|
57
|
+
}>;
|
|
58
|
+
config: {
|
|
59
|
+
version: string;
|
|
60
|
+
baseUrl: string;
|
|
61
|
+
projectRoot: string;
|
|
62
|
+
specPattern: string;
|
|
63
|
+
screenshotsFolder: string;
|
|
64
|
+
videosFolder: string;
|
|
65
|
+
video: boolean;
|
|
66
|
+
};
|
|
67
|
+
browserName: string;
|
|
68
|
+
browserVersion: string;
|
|
69
|
+
}
|
|
70
|
+
interface CypressBeforeRunDetails {
|
|
71
|
+
config: CypressRunResult["config"];
|
|
72
|
+
specs: CypressSpec[];
|
|
73
|
+
browser: {
|
|
74
|
+
name: string;
|
|
75
|
+
version: string;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
interface CypressAfterSpecDetails {
|
|
79
|
+
spec: CypressSpec;
|
|
80
|
+
results: {
|
|
81
|
+
stats: CypressRunResult["runs"][0]["stats"];
|
|
82
|
+
tests: CypressTestResult[];
|
|
83
|
+
video: string | null;
|
|
84
|
+
screenshots: Array<{
|
|
85
|
+
name: string;
|
|
86
|
+
path: string;
|
|
87
|
+
}>;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
type SupatestCypressOptions = {
|
|
91
|
+
projectId?: string;
|
|
92
|
+
apiKey?: string;
|
|
93
|
+
apiUrl?: string;
|
|
94
|
+
uploadAssets?: boolean;
|
|
95
|
+
maxConcurrentUploads?: number;
|
|
96
|
+
retryAttempts?: number;
|
|
97
|
+
timeoutMs?: number;
|
|
98
|
+
dryRun?: boolean;
|
|
99
|
+
};
|
|
100
|
+
type CypressPluginEvents = {
|
|
101
|
+
(action: "before:run", fn: (details: CypressBeforeRunDetails) => void | Promise<void>): void;
|
|
102
|
+
(action: "after:spec", fn: (spec: CypressSpec, results: CypressAfterSpecDetails["results"]) => void | Promise<void>): void;
|
|
103
|
+
(action: "after:run", fn: (results: CypressRunResult) => void | Promise<void>): void;
|
|
104
|
+
(action: string, fn: (...args: unknown[]) => unknown): void;
|
|
105
|
+
};
|
|
106
|
+
type CypressPluginConfigOptions = Record<string, unknown>;
|
|
107
|
+
declare function supatestPlugin(on: CypressPluginEvents, config: CypressPluginConfigOptions, options?: SupatestCypressOptions): void;
|
|
108
|
+
|
|
109
|
+
export { type ReporterOptions, type SupatestCypressOptions, supatestPlugin as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { BaseReporterOptions } from '@supatest/reporter-core';
|
|
2
|
+
export { ErrorCategory, ErrorCollector, ErrorSummary } from '@supatest/reporter-core';
|
|
3
|
+
|
|
4
|
+
type ReporterOptions = BaseReporterOptions;
|
|
5
|
+
|
|
6
|
+
interface CypressSpec {
|
|
7
|
+
name: string;
|
|
8
|
+
relative: string;
|
|
9
|
+
absolute: string;
|
|
10
|
+
}
|
|
11
|
+
interface CypressTestResult {
|
|
12
|
+
title: string[];
|
|
13
|
+
state: "passed" | "failed" | "pending" | "skipped";
|
|
14
|
+
body: string;
|
|
15
|
+
displayError: string | null;
|
|
16
|
+
attempts: Array<{
|
|
17
|
+
state: "passed" | "failed" | "pending";
|
|
18
|
+
error: {
|
|
19
|
+
message: string;
|
|
20
|
+
stack: string;
|
|
21
|
+
} | null;
|
|
22
|
+
wallClockStartedAt: string;
|
|
23
|
+
wallClockDuration: number;
|
|
24
|
+
videoTimestamp: number;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
interface CypressRunResult {
|
|
28
|
+
status: "finished" | "failed";
|
|
29
|
+
startedTestsAt: string;
|
|
30
|
+
endedTestsAt: string;
|
|
31
|
+
totalDuration: number;
|
|
32
|
+
totalSuites: number;
|
|
33
|
+
totalTests: number;
|
|
34
|
+
totalFailed: number;
|
|
35
|
+
totalPassed: number;
|
|
36
|
+
totalPending: number;
|
|
37
|
+
totalSkipped: number;
|
|
38
|
+
runs: Array<{
|
|
39
|
+
spec: CypressSpec;
|
|
40
|
+
stats: {
|
|
41
|
+
suites: number;
|
|
42
|
+
tests: number;
|
|
43
|
+
passes: number;
|
|
44
|
+
pending: number;
|
|
45
|
+
skipped: number;
|
|
46
|
+
failures: number;
|
|
47
|
+
wallClockDuration: number;
|
|
48
|
+
};
|
|
49
|
+
tests: CypressTestResult[];
|
|
50
|
+
video: string | null;
|
|
51
|
+
screenshots: Array<{
|
|
52
|
+
name: string;
|
|
53
|
+
path: string;
|
|
54
|
+
height: number;
|
|
55
|
+
width: number;
|
|
56
|
+
}>;
|
|
57
|
+
}>;
|
|
58
|
+
config: {
|
|
59
|
+
version: string;
|
|
60
|
+
baseUrl: string;
|
|
61
|
+
projectRoot: string;
|
|
62
|
+
specPattern: string;
|
|
63
|
+
screenshotsFolder: string;
|
|
64
|
+
videosFolder: string;
|
|
65
|
+
video: boolean;
|
|
66
|
+
};
|
|
67
|
+
browserName: string;
|
|
68
|
+
browserVersion: string;
|
|
69
|
+
}
|
|
70
|
+
interface CypressBeforeRunDetails {
|
|
71
|
+
config: CypressRunResult["config"];
|
|
72
|
+
specs: CypressSpec[];
|
|
73
|
+
browser: {
|
|
74
|
+
name: string;
|
|
75
|
+
version: string;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
interface CypressAfterSpecDetails {
|
|
79
|
+
spec: CypressSpec;
|
|
80
|
+
results: {
|
|
81
|
+
stats: CypressRunResult["runs"][0]["stats"];
|
|
82
|
+
tests: CypressTestResult[];
|
|
83
|
+
video: string | null;
|
|
84
|
+
screenshots: Array<{
|
|
85
|
+
name: string;
|
|
86
|
+
path: string;
|
|
87
|
+
}>;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
type SupatestCypressOptions = {
|
|
91
|
+
projectId?: string;
|
|
92
|
+
apiKey?: string;
|
|
93
|
+
apiUrl?: string;
|
|
94
|
+
uploadAssets?: boolean;
|
|
95
|
+
maxConcurrentUploads?: number;
|
|
96
|
+
retryAttempts?: number;
|
|
97
|
+
timeoutMs?: number;
|
|
98
|
+
dryRun?: boolean;
|
|
99
|
+
};
|
|
100
|
+
type CypressPluginEvents = {
|
|
101
|
+
(action: "before:run", fn: (details: CypressBeforeRunDetails) => void | Promise<void>): void;
|
|
102
|
+
(action: "after:spec", fn: (spec: CypressSpec, results: CypressAfterSpecDetails["results"]) => void | Promise<void>): void;
|
|
103
|
+
(action: "after:run", fn: (results: CypressRunResult) => void | Promise<void>): void;
|
|
104
|
+
(action: string, fn: (...args: unknown[]) => unknown): void;
|
|
105
|
+
};
|
|
106
|
+
type CypressPluginConfigOptions = Record<string, unknown>;
|
|
107
|
+
declare function supatestPlugin(on: CypressPluginEvents, config: CypressPluginConfigOptions, options?: SupatestCypressOptions): void;
|
|
108
|
+
|
|
109
|
+
export { type ReporterOptions, type SupatestCypressOptions, supatestPlugin as default };
|