@qualflare/cypress 0.3.0 → 0.4.0

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/plugin/index.ts","../../src/plugin/attachment-reader.ts","../../src/shared/logger.ts","../../src/plugin/video-writer.ts","../../src/plugin/events.ts","../../src/shared/constants.ts","../../src/shared/duration.ts","../../src/plugin/collect-builder.ts","../../src/plugin/version.ts","../../src/plugin/state.ts","../../src/plugin/resolve-config.ts","../../src/plugin/ci-detect.ts","../../src/plugin/git-detect.ts","../../src/plugin/tasks.ts"],"sourcesContent":["import { AttachmentBudget, type AttachmentReaderConfig } from './attachment-reader.js';\nimport { registerEvents } from './events.js';\nimport { resolveConfig, type QualflareCypressOptions } from './resolve-config.js';\nimport { PendingAttachmentQueue, TestPhaseGate } from './state.js';\nimport { CaseBuffer, registerTasks } from './tasks.js';\n\nexport type { QualflareCypressOptions, ResolvedPluginConfig } from './resolve-config.js';\n\n/**\n * Wires qualflare-cypress into `setupNodeEvents`. Returns `config`\n * unmodified so it composes with a user's own `setupNodeEvents` body and\n * with other plugins:\n *\n * ```ts\n * // cypress.config.ts\n * import { defineConfig } from 'cypress';\n * import { qualflareCypress } from '@qualflare/cypress/plugin';\n *\n * export default defineConfig({\n * e2e: {\n * setupNodeEvents(on, config) {\n * return qualflareCypress(on, config, { environment: 'staging' });\n * },\n * },\n * });\n * ```\n */\nexport function qualflareCypress(\n on: Cypress.PluginEvents,\n config: Cypress.PluginConfigOptions,\n options: QualflareCypressOptions = {},\n): Cypress.PluginConfigOptions {\n const resolved = resolveConfig(options);\n const buffer = new CaseBuffer();\n const pendingAttachments = new PendingAttachmentQueue();\n const testPhaseGate = new TestPhaseGate();\n const attachmentBudget = new AttachmentBudget(resolved.maxTotalAttachmentBytes);\n // resolved already has every field AttachmentReaderConfig needs.\n const attachmentConfig: AttachmentReaderConfig = resolved;\n\n // Task handlers are always registered — even when disabled — so a\n // cy.task() call from the browser side never errors with \"no handler\n // registered for task\" just because the plugin is turned off.\n registerTasks(on, buffer, attachmentConfig, attachmentBudget, pendingAttachments, testPhaseGate);\n\n if (resolved.enabled) {\n registerEvents(on, resolved, buffer, pendingAttachments, testPhaseGate);\n }\n\n return config;\n}\n","import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { logger } from '../shared/logger.js';\nimport type { Attachment } from '../shared/types.js';\nimport { copyVideoAttachment } from './video-writer.js';\n\n/** Extensions/mime-prefixes routed through the video-copy flow instead of\n * the inline-base64 path below. Broader than the server's own MIME\n * allowlist (`.avi`/`.mkv` included) so this still correctly IDENTIFIES a\n * video attachment even in a format the server can't accept —\n * `copyVideoAttachment` is what actually enforces the narrower allowlist and\n * warns/skips a format outside it. Nothing in this codebase currently\n * produces `.avi`/`.mkv` (`events.ts`'s `after:screenshot` handler always\n * hardcodes `image/png`, and Cypress itself only ever records `.mp4`), but a\n * `qualflare.attachmentFromFile()` call can point at any local file. */\nconst VIDEO_EXTENSIONS = new Set(['.mp4', '.webm', '.mov', '.avi', '.mkv']);\n\nexport interface AttachmentReaderConfig {\n attachScreenshots: boolean;\n maxAttachmentBytes: number;\n maxTotalAttachmentBytes: number;\n maxVideoBytes: number;\n outputDir: string;\n}\n\n/**\n * Tracks cumulative attached bytes across the whole `cypress run` process\n * (one instance per `qualflareCypress()` call, threaded through every\n * `TASK_REPORT_CASE` resolution), so the final POST doesn't silently exceed\n * the request body limit. `maxTotalAttachmentBytes` defaults well under the\n * documented 10MB specifically because production currently has a known,\n * confirmed effective ~1MB body-limit bug (see the plan's CRIT-01\n * reference) — don't raise the default until that's confirmed fixed\n * server-side.\n */\nexport class AttachmentBudget {\n private used = 0;\n\n constructor(private readonly maxTotalBytes: number) {}\n\n /** Atomically checks-and-reserves `bytes` against the remaining budget.\n * Returns false (reserving nothing) if it would exceed the total. */\n tryReserve(bytes: number): boolean {\n if (this.used + bytes > this.maxTotalBytes) {\n return false;\n }\n this.used += bytes;\n return true;\n }\n\n get usedBytes(): number {\n return this.used;\n }\n}\n\ntype ReadResult = { skipped: false; content: string } | { skipped: true; reason: string };\n\nfunction isVideoLike(attachment: Attachment): boolean {\n if (attachment.mimeType?.toLowerCase().startsWith('video/')) {\n return true;\n }\n if (attachment.path && VIDEO_EXTENSIONS.has(path.extname(attachment.path).toLowerCase())) {\n return true;\n }\n return false;\n}\n\nfunction readAttachmentFile(filePath: string, maxAttachmentBytes: number, budget: AttachmentBudget): ReadResult {\n let size: number;\n try {\n // Stat BEFORE reading — an oversized file must never be loaded into\n // memory just to discover it should be skipped.\n size = fs.statSync(filePath).size;\n } catch (err) {\n return { skipped: true, reason: `could not stat file: ${(err as Error).message}` };\n }\n if (size > maxAttachmentBytes) {\n return {\n skipped: true,\n reason: `${size} bytes exceeds the configured per-attachment cap of ${maxAttachmentBytes} bytes`,\n };\n }\n if (!budget.tryReserve(size)) {\n return {\n skipped: true,\n reason: `would exceed this run's total attachment budget (${budget.usedBytes} bytes already used)`,\n };\n }\n try {\n const content = fs.readFileSync(filePath).toString('base64');\n return { skipped: false, content };\n } catch (err) {\n return { skipped: true, reason: `could not read file: ${(err as Error).message}` };\n }\n}\n\n/**\n * Resolves a Case's attachment references into either inline base64\n * `content` (small files) or a `localVideoPath` pointing at a copy made\n * alongside the report output (video — see `video-writer.ts`'s\n * `copyVideoAttachment`), or drops them. Attachment references arrive with\n * only a `path` (never bytes — screenshots are captured entirely Node-side\n * via the `after:screenshot` plugin event in `events.ts`, and an author's\n * `qualflare.attachmentFromFile()` call carries only the path it was given\n * too), so all file I/O and size-guarding happens here, at the point a\n * finished Case is received from `cy.task(TASK_REPORT_CASE, ...)` — see\n * `tasks.ts`.\n *\n * Per the plan's resolved decision, an oversized or over-budget INLINE\n * attachment is skipped ENTIRELY (not degraded to a contentless path-only\n * entry): the server's `path` field is explicitly informational/never-fetched,\n * so a contentless entry has little value and this keeps the behavior\n * simple and predictable. A video attachment that fails to copy (oversized\n * per `maxVideoBytes`, unsupported format, or an unreadable source file) is\n * skipped the same way — `copyVideoAttachment` already logs why.\n */\nexport function resolveAttachments(\n attachments: Attachment[] | undefined,\n config: AttachmentReaderConfig,\n budget: AttachmentBudget,\n): Attachment[] | undefined {\n if (!attachments || attachments.length === 0) {\n return undefined;\n }\n if (!config.attachScreenshots) {\n return undefined;\n }\n\n const resolved: Attachment[] = [];\n for (const attachment of attachments) {\n if (isVideoLike(attachment)) {\n if (!attachment.path) {\n logger.warn(`skipping video attachment \"${attachment.name}\": no local file path to copy.`);\n continue;\n }\n const copied = copyVideoAttachment(attachment.path, config.outputDir, config.maxVideoBytes);\n if (!copied) {\n // copyVideoAttachment already logged the specific reason.\n continue;\n }\n resolved.push({\n ...attachment,\n mimeType: copied.mimeType,\n localVideoPath: copied.localVideoPath,\n fileSize: copied.fileSize,\n });\n continue;\n }\n if (attachment.content !== undefined || !attachment.path) {\n // Already has inline content (e.g. from a future metadata-API call\n // that provides content directly), or nothing to read — pass through\n // unchanged.\n resolved.push(attachment);\n continue;\n }\n const result = readAttachmentFile(attachment.path, config.maxAttachmentBytes, budget);\n if (result.skipped) {\n logger.warn(`skipping attachment \"${attachment.name}\" (${attachment.path}): ${result.reason}`);\n continue;\n }\n resolved.push({ ...attachment, content: result.content });\n }\n return resolved.length > 0 ? resolved : undefined;\n}\n","/**\n * A minimal logger writing to stderr (Node) / console (browser). Node-side\n * output deliberately avoids stdout, since that's typically Cypress's own\n * test-output stream and shouldn't be polluted with plugin diagnostics.\n *\n * Safe to import from both browser-side and Node-side code (isomorphic) —\n * `console.*` exists in both environments; only the underlying stream\n * differs, which is not something this module needs to control explicitly\n * since `console.error`/`console.warn` already default to stderr in Node.\n */\n\nconst PREFIX = '[qualflare-cypress]';\n\nexport const logger = {\n debug(...args: unknown[]): void {\n console.debug(PREFIX, ...args);\n },\n info(...args: unknown[]): void {\n console.log(PREFIX, ...args);\n },\n warn(...args: unknown[]): void {\n console.warn(PREFIX, ...args);\n },\n error(...args: unknown[]): void {\n console.error(PREFIX, ...args);\n },\n};\n","import * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { randomUUID } from 'node:crypto';\n\nimport { logger } from '../shared/logger.js';\n\n/** Extension -> MIME type for the video formats the server accepts (see\n * `launch.AllowedAttachmentUploadMimeTypes` server-side). Cypress itself\n * always records `.mp4` today, but `.webm`/`.mov` are listed for parity with\n * the server's own allowlist and in case that ever changes. An extension not\n * in this map (a user could point `qualflare.attachmentFromFile()` at an\n * arbitrary file) is skipped — see `copyVideoAttachment`'s doc comment. */\nconst VIDEO_MIME_TYPES_BY_EXTENSION: Record<string, string> = {\n '.mp4': 'video/mp4',\n '.webm': 'video/webm',\n '.mov': 'video/quicktime',\n};\n\nexport interface VideoCopyResult {\n /** Filename relative to the `outputDir` this was copied into — never an\n * absolute path, since the whole directory travels together as one CI\n * artifact bundle (see the design spec's \"Why no backend changes\"\n * section). */\n localVideoPath: string;\n fileSize: number;\n mimeType: string;\n}\n\n/**\n * Copies one video file into `outputDir` under a unique filename (Allure's\n * `FileSystemWriter.writeAttachmentFromPath` pattern: `fs.copyFileSync`,\n * never read into memory) and returns enough to build that `Attachment`\n * entry's `localVideoPath`. `qualflare-cli` is what actually uploads this\n * file later, once it has a real auth token — see the design spec.\n *\n * Best-effort, like the rest of this reporter's attachment handling\n * (`attachment-reader.ts`'s oversized/unreadable-file skip): any failure —\n * oversized file, unsupported extension, an unreadable source file — is\n * logged as a warning and resolves to `undefined` rather than throwing, so a\n * video problem never fails the whole run.\n */\nexport function copyVideoAttachment(\n filePath: string,\n outputDir: string,\n maxVideoBytes: number,\n): VideoCopyResult | undefined {\n const ext = path.extname(filePath).toLowerCase();\n const mimeType = VIDEO_MIME_TYPES_BY_EXTENSION[ext];\n if (!mimeType) {\n logger.warn(`skipping video attachment \"${filePath}\": unsupported video format.`);\n return undefined;\n }\n\n let fileSize: number;\n try {\n // Stat BEFORE copying — an oversized file must never be copied just to\n // discover it should be skipped.\n fileSize = fs.statSync(filePath).size;\n } catch (err) {\n logger.warn(`skipping video attachment \"${filePath}\": could not stat file: ${(err as Error).message}`);\n return undefined;\n }\n if (fileSize > maxVideoBytes) {\n logger.warn(\n `skipping video attachment \"${filePath}\": ${fileSize} bytes exceeds the configured ` +\n `maxVideoBytes cap of ${maxVideoBytes} bytes.`,\n );\n return undefined;\n }\n\n const localVideoPath = `${randomUUID()}${ext}`;\n try {\n fs.mkdirSync(outputDir, { recursive: true });\n fs.copyFileSync(filePath, path.join(outputDir, localVideoPath));\n } catch (err) {\n logger.warn(`skipping video attachment \"${filePath}\": could not copy file: ${(err as Error).message}`);\n return undefined;\n }\n\n return { localVideoPath, fileSize, mimeType };\n}\n","import * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { randomUUID } from 'node:crypto';\n\nimport { MAX_ATTACHMENTS_PER_CASE } from '../shared/constants.js';\nimport { logger } from '../shared/logger.js';\nimport { msToNs } from '../shared/duration.js';\nimport type { Case, CaseStatus, Suite } from '../shared/types.js';\nimport { buildCollectPayload, type BrowserInfo } from './collect-builder.js';\nimport type { ResolvedPluginConfig } from './resolve-config.js';\nimport { LaunchAccumulator, PendingAttachmentQueue, TestPhaseGate } from './state.js';\nimport type { CaseBuffer } from './tasks.js';\nimport { copyVideoAttachment } from './video-writer.js';\n\n/** Case statuses a video recording is worth attaching to — mirrors the\n * \"this test needs investigating\" set, not just literally 'failed'. */\nconst FAILURE_STATUSES: ReadonlySet<CaseStatus> = new Set(['failed', 'error', 'timeout']);\n\n/**\n * Registers `before:run` / `before:spec` / `after:spec` / `after:run` /\n * `after:screenshot` on the given Cypress plugin events, wiring the\n * spec-by-spec case buffer into one accumulated `Launch` and writing it as a\n * single Collect JSON file into `config.outputDir` exactly once at\n * `after:run` — this process never uploads anything itself; see\n * `resolve-config.ts`'s `outputDir` doc comment.\n */\nexport function registerEvents(\n on: Cypress.PluginEvents,\n config: ResolvedPluginConfig,\n buffer: CaseBuffer,\n pendingAttachments: PendingAttachmentQueue,\n testPhaseGate: TestPhaseGate,\n): void {\n const accumulator = new LaunchAccumulator();\n let browserInfo: BrowserInfo | undefined;\n let currentSpecStart = 0;\n\n on('before:run', (details) => {\n browserInfo = {\n browserName: details.browser?.displayName,\n browserVersion: details.browser?.version,\n osName: details.system?.osName,\n osVersion: details.system?.osVersion,\n };\n });\n\n // Node-side event — fires for BOTH manual cy.screenshot() calls and\n // Cypress's own automatic on-failure screenshot (screenshotOnRunFailure,\n // on by default in `cypress run`), distinguished only by `details.testFailure`.\n // No cy.task()/browser-side involvement needed: this event already runs\n // in the Node process with the file already written to disk. Queued here\n // and drained by `tasks.ts`'s TASK_REPORT_CASE handler, which attaches\n // whatever's pending to the Case currently being reported.\n //\n // EXCEPT when it fires before the first test has even started (see\n // TestPhaseGate in state.ts) — a screenshot taken in a root `before()`\n // hook. That can never be correctly attributed to any specific test (the\n // first test hasn't begun yet), so it's treated as orphaned immediately,\n // the same way a screenshot taken in an `after()` hook already is (below,\n // in `after:spec`) — rather than silently getting swept into whichever\n // test's TASK_REPORT_CASE happens to arrive first.\n on('after:screenshot', (details) => {\n if (!testPhaseGate.hasStarted()) {\n logger.warn(\n `a screenshot (\"${details.name || 'unnamed'}\") was captured before any test in this spec had ` +\n 'started (likely in a root `before()` hook) and cannot be attributed to a specific test — it was not included in the report.',\n );\n return;\n }\n pendingAttachments.enqueue({\n name: details.name || (details.testFailure ? 'failure-screenshot' : 'screenshot'),\n path: details.path,\n mimeType: 'image/png',\n });\n });\n\n on('before:spec', () => {\n currentSpecStart = Date.now();\n testPhaseGate.reset();\n // Ensure no stale cases from a prior spec leak into this one, in case\n // something upstream ever calls before:spec without a matching\n // after:spec having fired first.\n buffer.drain();\n });\n\n on('after:spec', async (spec, results) => {\n const cases = buffer.drain();\n\n // Cypress records one video per SPEC, not per test, so there is no\n // exact owning Case — attaching it to the first failing case in the\n // spec is the most useful available attribution (that's the recording a\n // QA engineer actually wants to watch) and, being a single Case row,\n // avoids double-counting the same copied file's bytes toward workspace\n // storage quota the way attaching it to every failing case would.\n // Skipped entirely for an all-passing spec: a video with nothing to\n // investigate has little diagnostic value and isn't worth copying.\n if (results.video) {\n const failedCase = cases.find((c) => FAILURE_STATUSES.has(c.status));\n if (failedCase) {\n const copied = copyVideoAttachment(results.video, config.outputDir, config.maxVideoBytes);\n if (copied) {\n attachVideo(failedCase, copied);\n }\n } else {\n logger.info(`spec ${spec.relative} recorded a video but no test failed — not attached.`);\n }\n }\n\n const suite: Suite = {\n name: spec.relative,\n category: 'cypress',\n duration: msToNs(results.stats.duration ?? Date.now() - currentSpecStart),\n timestamp: new Date(results.stats.startedAt ?? Date.now()).toISOString(),\n cases,\n };\n\n if (cases.length !== results.stats.tests) {\n logger.warn(\n `spec ${spec.relative}: captured ${cases.length} case(s) but Cypress reported ` +\n `${results.stats.tests} test(s) — some results may be missing from the report.`,\n );\n }\n\n accumulator.addSuite(suite);\n\n // Any attachment still sitting in the queue at this point was never\n // claimed by a Case's TASK_REPORT_CASE (e.g. a screenshot taken in an\n // `after`-hook, after the last test's report already went out) — drop\n // it with a warning rather than silently attributing it to a case in\n // the NEXT spec file.\n const orphaned = pendingAttachments.drain();\n if (orphaned.length > 0) {\n logger.warn(\n `spec ${spec.relative}: ${orphaned.length} screenshot(s) could not be attributed to a ` +\n 'specific test (likely taken outside a test body, e.g. in an `after` hook) and were not included in the report.',\n );\n }\n });\n\n on('after:run', async () => {\n const suites = accumulator.getSuites();\n if (suites.length === 0) {\n logger.info('no test results were captured this run — skipping file write.');\n return;\n }\n\n const collect = buildCollectPayload(accumulator, config, browserInfo);\n if (config.shardIndex !== undefined) {\n for (const suite of collect.suites) {\n for (const c of suite.cases) {\n c.shardIndex = config.shardIndex;\n }\n }\n }\n\n fs.mkdirSync(config.outputDir, { recursive: true });\n const outputPath = path.join(config.outputDir, `${randomUUID()}.json`);\n fs.writeFileSync(outputPath, JSON.stringify(collect));\n logger.info(`wrote Collect payload to ${outputPath} — run \\`qualflare-cli collect ${config.outputDir}\\` to upload it.`);\n });\n}\n\n/** Appends a video's `Attachment` entry to a Case, respecting the server's\n * per-case attachment cap — a spec-level video competing with the case's\n * own screenshots for that budget is an edge case (one video vs. up to 50\n * screenshots), but silently exceeding the cap would 400 the whole launch. */\nfunction attachVideo(testCase: Case, copied: { localVideoPath: string; fileSize: number; mimeType: string }): void {\n const attachments = testCase.attachments ?? [];\n if (attachments.length >= MAX_ATTACHMENTS_PER_CASE) {\n logger.warn(\n `not attaching video to \"${testCase.name}\": already at the server's ${MAX_ATTACHMENTS_PER_CASE}-attachment-per-case cap.`,\n );\n return;\n }\n testCase.attachments = [\n ...attachments,\n {\n name: 'video',\n mimeType: copied.mimeType,\n localVideoPath: copied.localVideoPath,\n fileSize: copied.fileSize,\n },\n ];\n}\n","/**\n * Shared constants that both the browser-side support script and the\n * Node-side plugin must agree on exactly (task names in particular — a\n * typo on either side silently breaks `cy.task()` at runtime with no\n * compile-time signal, since Cypress tasks are looked up by string).\n */\n\n/** `cy.task()` name the browser side uses to hand a finished test's Case\n * object over to the Node side. */\nexport const TASK_REPORT_CASE = 'qualflareReportCase';\n\n/** `cy.task()` name a one-shot root-level `beforeEach` (registered by\n * `src/browser/index.ts`) uses to tell the Node side \"the first test of this\n * spec has started (all applicable `before()` hooks have already run)\" — see\n * `src/plugin/state.ts`'s `TestPhaseGate` for why this exists: it lets\n * `events.ts` distinguish a screenshot taken in a `before()` hook (which\n * should be treated as orphaned, like an `after()`-hook screenshot already\n * is) from one taken during a real test's own execution. */\nexport const TASK_MARK_TEST_PHASE_STARTED = 'qualflareMarkTestPhaseStarted';\n\n/** Server-side caps this client should respect defensively (see\n * `api-service/internal/core/domain/launch/launch.go`). */\nexport const MAX_SUITES_PER_LAUNCH = 2000;\nexport const MAX_CASES_PER_SUITE = 5000;\nexport const MAX_STEPS_PER_CASE = 1000;\nexport const MAX_PARAMETERS_PER_STEP = 50;\nexport const MAX_ATTACHMENTS_PER_CASE = 50;\nexport const MAX_LABELS_PER_CASE = 100;\nexport const MAX_LINKS_PER_CASE = 20;\nexport const MAX_TAGS_PER_CASE = 64;\nexport const MAX_TAG_LENGTH = 255;\n\n/** Mirrors `launch.MaxAttachmentUploadFileSize` — the server's hard cap on a\n * single `POST /api/v1/attachments/upload-url` request (video). */\nexport const MAX_VIDEO_UPLOAD_BYTES = 50 * 1024 * 1024;\n\n/** Client-side SOFT cap on steps recorded per test attempt — well under the\n * server's 1000-per-case hard cap (`MAX_STEPS_PER_CASE`). There's no reason\n * to build/serialize thousands of command-log entries for one test; once hit,\n * further entries within that attempt are dropped (with a one-time warning),\n * not queued and truncated later. */\nexport const MAX_STEPS_PER_TEST_ATTEMPT = 300;\n","import type { NanosecondDuration } from './types.js';\n\nconst NS_PER_MS = 1_000_000;\n\n/**\n * Converts a Cypress/Mocha millisecond duration into the wire format's\n * raw-nanosecond integer (see `NanosecondDuration` in ./types.ts).\n *\n * Rounds (not truncates) so fractional-ms input doesn't lose precision by\n * always rounding toward zero. Negative input is clamped to 0 — a negative\n * duration is never legitimate and silently clamping is safer for an\n * ingest payload than throwing and aborting an otherwise-good report.\n */\nexport function msToNs(ms: number): NanosecondDuration {\n if (!Number.isFinite(ms) || ms <= 0) {\n return 0;\n }\n return Math.round(ms * NS_PER_MS);\n}\n","import * as os from 'node:os';\n\nimport type { ResolvedPluginConfig } from './resolve-config.js';\nimport type { LaunchAccumulator } from './state.js';\nimport type { Collect } from '../shared/types.js';\nimport { PACKAGE_VERSION } from './version.js';\n\n/** Browser/system info captured at Cypress's `before:run` event — richer\n * and more accurate than any Node-side guess when available. */\nexport interface BrowserInfo {\n browserName?: string;\n browserVersion?: string;\n osName?: string;\n osVersion?: string;\n}\n\nfunction resolveOs(config: ResolvedPluginConfig, info: BrowserInfo | undefined): string {\n if (config.os) {\n return config.os;\n }\n if (info?.osName) {\n return info.osVersion ? `${info.osName} ${info.osVersion}` : info.osName;\n }\n return `${os.type()} ${os.release()}`;\n}\n\nfunction resolveBrowser(config: ResolvedPluginConfig, info: BrowserInfo | undefined): string {\n if (config.browser) {\n return config.browser;\n }\n if (info?.browserName) {\n return info.browserVersion ? `${info.browserName} ${info.browserVersion}` : info.browserName;\n }\n return '';\n}\n\n/**\n * Assembles the final `Collect` payload from everything accumulated over\n * the `cypress run` process, at `after:run`. CI metadata\n * (`ciProvider`/`ciBuildNumber`/`ciRunUrl`/`ciPrNumber`) and branch/commit\n * auto-detection are already fully resolved by `resolve-config.ts` (the\n * single source of truth for this launch's metadata, consistent with how\n * `branch`/`commit` are handled) — this function just reads the resolved\n * config through, it does not call `ci-detect.ts`/`git-detect.ts` itself.\n */\nexport function buildCollectPayload(\n accumulator: LaunchAccumulator,\n config: ResolvedPluginConfig,\n browserInfo: BrowserInfo | undefined,\n): Collect {\n return {\n framework: config.framework,\n platform: config.platform,\n os: resolveOs(config, browserInfo),\n browser: resolveBrowser(config, browserInfo),\n branch: config.branch,\n commit: config.commit,\n environment: config.environment,\n language: config.language,\n milestone: config.milestone,\n metadata: {\n version: PACKAGE_VERSION,\n timestamp: new Date().toISOString(),\n cliName: 'qualflare-cypress',\n runId: config.runId,\n },\n properties: config.properties,\n suites: accumulator.getSuites(),\n ciProvider: config.ciProvider,\n ciBuildNumber: config.ciBuildNumber,\n ciRunUrl: config.ciRunUrl,\n ciPrNumber: config.ciPrNumber,\n };\n}\n","// `__PACKAGE_VERSION__` is injected at build time by tsup's `define` option\n// (see tsup.config.ts) from package.json's `version` field. Deliberately\n// NOT read at runtime via `import.meta.url` + `createRequire` — that breaks\n// the CJS build output (`import.meta` is empty/unavailable once esbuild\n// compiles to CommonJS), which is exactly the class of dual-CJS/ESM-package\n// bug a build-time constant sidesteps entirely. Under Vitest (which never\n// goes through tsup), `vitest.config.ts` defines the same constant so this\n// module behaves identically in tests and in the built package.\ndeclare const __PACKAGE_VERSION__: string;\n\nexport const PACKAGE_VERSION: string = __PACKAGE_VERSION__;\n","import { MAX_SUITES_PER_LAUNCH } from '../shared/constants.js';\nimport { logger } from '../shared/logger.js';\nimport type { Attachment, Suite } from '../shared/types.js';\n\n/**\n * Accumulates `Suite[]` across every spec file in the current `cypress run`\n * process. Process-lifetime, single instance — matches the plan's\n * \"one `cypress run` process = one Launch\" decision: there is no\n * cross-process aggregation in v1 (see docs/LIMITATIONS.md).\n */\nexport class LaunchAccumulator {\n private readonly suites: Suite[] = [];\n private truncated = false;\n\n addSuite(suite: Suite): void {\n if (this.suites.length >= MAX_SUITES_PER_LAUNCH) {\n if (!this.truncated) {\n this.truncated = true;\n logger.warn(\n `reached the server's ${MAX_SUITES_PER_LAUNCH}-suite-per-launch cap — further spec files' ` +\n 'results will not be uploaded this run.',\n );\n }\n return;\n }\n this.suites.push(suite);\n }\n\n getSuites(): Suite[] {\n return this.suites;\n }\n}\n\n/**\n * Tracks whether the first test of the current spec has started (i.e. every\n * applicable `before()` hook for it has already run) — set via a one-shot\n * `cy.task(TASK_MARK_TEST_PHASE_STARTED, ...)` call from a root-level\n * `beforeEach` the browser side registers (`src/browser/index.ts`), reset at\n * `before:spec`.\n *\n * Exists to fix a real misattribution bug: a screenshot taken in a root\n * `before()` hook fires `after:screenshot` before ANY test has started, and\n * without this signal `events.ts` cannot tell that apart from a screenshot\n * taken during the first real test's own execution — both would otherwise\n * sit in `PendingAttachmentQueue` and get drained together into whichever\n * test's `TASK_REPORT_CASE` happens to arrive first. `before()`-hook\n * screenshots should instead be treated as orphaned, exactly like an\n * `after()`-hook screenshot already correctly is (see `events.ts`'s\n * `after:spec` handler).\n *\n * A `beforeEach` (not a raw `runner.on('test', ...)` Mocha listener) is\n * deliberately the signal source: it's a real Cypress command executed as\n * part of that test's own command-queue processing, the same\n * queue-integrated mechanism `queue.ts`'s already-proven-safe `flushCase`\n * (called from a real `afterEach`) uses — unlike calling `cy.task()`\n * directly from a raw Mocha runner event listener, which Tier 1 of this same\n * remediation effort found (by actually running a real `cypress run`) can\n * hang the whole process for a test whose body never executes.\n */\nexport class TestPhaseGate {\n private started = false;\n\n markStarted(): void {\n this.started = true;\n }\n\n hasStarted(): boolean {\n return this.started;\n }\n\n /** Called at `before:spec`, so each spec file gets its own fresh\n * before()-hook-vs-real-test boundary. */\n reset(): void {\n this.started = false;\n }\n}\n\n/**\n * Buffers screenshot references captured via the Node-side `after:screenshot`\n * plugin event (see `events.ts`) between one finished test's report and the\n * next. `after:screenshot` fires in real time as Cypress captures each\n * screenshot (both manual `cy.screenshot()` calls and its own automatic\n * on-failure capture funnel through this single event) — since a\n * screenshot taken during a test's execution always fires before that\n * test's `TASK_REPORT_CASE` arrives (the test's own command queue, and\n * therefore any screenshot capture within it, completes before its\n * `afterEach` runs), draining this queue at the moment a Case is received\n * correctly attributes any screenshots taken during that test's run to it.\n *\n * This whole flow is Node-side only — no `cy.task()` round-trip or\n * browser-side wiring is needed for screenshots, unlike Case data itself.\n */\nexport class PendingAttachmentQueue {\n private pending: Attachment[] = [];\n\n enqueue(attachment: Attachment): void {\n this.pending.push(attachment);\n }\n\n /** Returns the buffered attachments and clears the queue. */\n drain(): Attachment[] {\n const drained = this.pending;\n this.pending = [];\n return drained;\n }\n}\n","import { randomUUID } from 'node:crypto';\n\nimport { MAX_VIDEO_UPLOAD_BYTES } from '../shared/constants.js';\nimport type { Platform } from '../shared/types.js';\nimport { detectCi, type CiMetadata } from './ci-detect.js';\nimport { detectGit, type GitInfo } from './git-detect.js';\n\n/** Options passed to `qualflareCypress(on, config, options)` in the user's\n * `cypress.config.ts`. Every field here also has an environment-variable\n * override — see the precedence table in the README / plan. */\nexport interface QualflareCypressOptions {\n environment?: string;\n language?: string;\n milestone?: number | null;\n branch?: string | null;\n commit?: string | null;\n platform?: Platform;\n framework?: string;\n os?: string;\n browser?: string;\n properties?: Record<string, string>;\n /** Max 64 chars. Free text, no enum — an unrecognized CI provider must\n * never be rejected. Auto-detected via `ci-detect.ts` when omitted. */\n ciProvider?: string;\n ciBuildNumber?: string;\n ciRunUrl?: string;\n ciPrNumber?: number;\n /** Identifier shared by every shard of one run, written into the report as\n * `metadata.runId`. `qualflare-cli collect` groups files by it and refuses\n * to merge a stale report from an earlier run into this launch.\n *\n * Auto-detected from CI. Outside CI it falls back to a per-process UUID,\n * which is correct there: every local run is a distinct run, so a leftover\n * file is still caught. */\n runId?: string;\n attachScreenshots?: boolean;\n maxAttachmentBytes?: number;\n maxTotalAttachmentBytes?: number;\n /** Per-video byte cap, checked before upload (via `fs.statSync`, never by\n * reading the file first). Default 50MB, matching the server's own hard\n * cap — raising this past 50MB only wastes an upload attempt the server\n * will reject. */\n maxVideoBytes?: number;\n /** `false` fully disables accumulation/POST (a complete no-op) but still\n * registers no-op `on('task', ...)` handlers so `cy.task()` calls from the\n * browser side never error with \"no handler registered for task.\" */\n enabled?: boolean;\n /** Directory `after:run` writes this process's report file (and any video\n * attachments) into. Default `./qualflare-results`. Always active — this\n * reporter never uploads anything itself; `qualflare-cli` reads whatever\n * ends up in this directory. Every JSON file this process writes is\n * uniquely named, so multiple shards can safely share one `outputDir`\n * without colliding — see docs/LIMITATIONS.md. */\n outputDir?: string;\n /** This process's 0-based position among parallel shards of the same CI\n * run, stamped onto every case it reports. Resolved only from this option\n * or the `QUALFLARE_SHARD_INDEX` env var — no shard concept is\n * auto-detected beyond that (set it yourself from your CI's own matrix\n * index; see docs/CONFIGURATION.md). A normal single-process run needs no\n * shard concept at all and can leave this unset. */\n shardIndex?: number;\n}\n\nexport interface ResolvedPluginConfig {\n environment: string;\n language: string;\n milestone: number | null;\n branch: string | null;\n commit: string | null;\n platform: Platform;\n framework: string;\n os?: string;\n browser?: string;\n properties?: Record<string, string>;\n ciProvider?: string;\n ciBuildNumber?: string;\n ciRunUrl?: string;\n ciPrNumber?: number;\n runId: string;\n attachScreenshots: boolean;\n maxAttachmentBytes: number;\n maxTotalAttachmentBytes: number;\n maxVideoBytes: number;\n enabled: boolean;\n outputDir: string;\n shardIndex?: number;\n}\n\nfunction firstEnv(...names: string[]): string | undefined {\n for (const name of names) {\n const value = process.env[name];\n if (value !== undefined && value !== '') {\n return value;\n }\n }\n return undefined;\n}\n\nfunction envBool(...names: string[]): boolean | undefined {\n const raw = firstEnv(...names);\n if (raw === undefined) {\n return undefined;\n }\n return raw === 'true' || raw === '1';\n}\n\nfunction envInt(...names: string[]): number | undefined {\n const raw = firstEnv(...names);\n if (raw === undefined) {\n return undefined;\n }\n const parsed = Number.parseInt(raw, 10);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\n/** Resolves the full plugin configuration from, in order: the explicit\n * `options` passed to `qualflareCypress()`, then `QUALFLARE_*` environment\n * variables, then `QF_*` (compat alias with the existing Go CLI, where an\n * equivalent exists), then a hardcoded default.\n *\n * Branch/commit precedence: `options.branch`/`.commit` (including an\n * explicit `null`, which is respected as \"no auto-detection wanted\" rather\n * than triggering the fallback tiers below it) > `QUALFLARE_BRANCH`/\n * `QF_BRANCH` env (and the commit equivalent) > CI-provider env vars > a\n * local `git` subprocess (`git-detect.ts`) > `null`. The subprocess tier is\n * skipped entirely — no `git` process is forked — once both branch and\n * commit are already resolved from options/env, mirroring\n * `qualflare-cli/internal/config/config.go`'s `DetectGit`'s early return.\n *\n * CI-metadata precedence (`ciProvider`/`ciBuildNumber`/`ciRunUrl`/\n * `ciPrNumber`): the corresponding `options.ci*` field, else `ci-detect.ts`'s\n * auto-detection (per-provider extraction table, falling back to the\n * `ci-info` package's ~70-provider free-text name).\n *\n * `deps` lets tests inject fake `detectGit`/`detectCi` implementations\n * instead of the real ones (which shell out to `git` and read the real\n * `process.env`/`ci-info` module state) — defaults to the real detectors,\n * so every production call site (`plugin/index.ts` calls `resolveConfig(options)`\n * with no second argument) is unaffected.\n */\nexport function resolveConfig(\n options: QualflareCypressOptions,\n deps: { detectGit?: () => GitInfo; detectCi?: () => CiMetadata } = {},\n): ResolvedPluginConfig {\n const doDetectGit = deps.detectGit ?? detectGit;\n const doDetectCi = deps.detectCi ?? detectCi;\n\n const enabled = options.enabled ?? envBool('QUALFLARE_ENABLED') ?? true;\n const outputDir = options.outputDir || firstEnv('QUALFLARE_OUTPUT_DIR') || './qualflare-results';\n const shardIndex = options.shardIndex ?? envInt('QUALFLARE_SHARD_INDEX');\n\n const milestoneRaw = options.milestone !== undefined ? options.milestone : envInt('QUALFLARE_MILESTONE', 'QF_MILESTONE');\n const milestone = milestoneRaw !== undefined && milestoneRaw !== null && milestoneRaw >= 1 ? milestoneRaw : null;\n\n const envBranch = firstEnv('QUALFLARE_BRANCH', 'QF_BRANCH');\n const envCommit = firstEnv('QUALFLARE_COMMIT', 'QF_COMMIT');\n const needsGitDetection =\n (options.branch === undefined && envBranch === undefined) ||\n (options.commit === undefined && envCommit === undefined);\n const detectedGit = needsGitDetection ? doDetectGit() : {};\n\n const branch = options.branch !== undefined ? options.branch : (envBranch ?? detectedGit.branch ?? null);\n const commit = options.commit !== undefined ? options.commit : (envCommit ?? detectedGit.commit ?? null);\n\n const detectedCi = doDetectCi();\n const ciProvider = options.ciProvider ?? detectedCi.ciProvider;\n const ciBuildNumber = options.ciBuildNumber ?? detectedCi.ciBuildNumber;\n const ciRunUrl = options.ciRunUrl ?? detectedCi.ciRunUrl;\n const ciPrNumber = options.ciPrNumber ?? detectedCi.ciPrNumber;\n\n // Never empty on purpose: `qf collect` treats a report with no runId as\n // \"unknown run\" and never lets it block a merge, so defaulting to '' would\n // quietly opt local runs out of the very check this exists for.\n const runId = options.runId ?? firstEnv('QUALFLARE_RUN_ID') ?? detectedCi.ciRunId ?? randomUUID();\n\n return {\n // `||` (truthy check), not `??`, for these three REQUIRED-non-empty wire\n // fields — matching `collect-builder.ts`'s `resolveOs`/`resolveBrowser`,\n // which already correctly treat an explicit `''` option as \"not set.\"\n // `??` only falls back on `null`/`undefined`, so `environment: ''` would\n // previously win outright over the `'development'` default, silently\n // 400ing the whole launch (the server rejects an empty `environment`)\n // and — since this process no longer attempts uploads — the error would\n // be deferred until qualflare-cli tries to upload.\n environment: (options.environment || undefined) ?? firstEnv('QUALFLARE_ENVIRONMENT', 'QF_ENVIRONMENT') ?? 'development',\n language: (options.language || undefined) ?? firstEnv('QUALFLARE_LANGUAGE', 'QF_LANGUAGE') ?? 'en-US',\n milestone,\n branch,\n commit,\n platform: options.platform ?? 'web',\n framework: options.framework || 'cypress',\n os: options.os,\n browser: options.browser,\n properties: options.properties,\n ciProvider,\n ciBuildNumber,\n ciRunUrl,\n ciPrNumber,\n runId,\n attachScreenshots: options.attachScreenshots ?? envBool('QUALFLARE_ATTACH_SCREENSHOTS') ?? true,\n maxAttachmentBytes: options.maxAttachmentBytes ?? envInt('QUALFLARE_MAX_ATTACHMENT_BYTES') ?? 1_500_000,\n maxTotalAttachmentBytes:\n options.maxTotalAttachmentBytes ?? envInt('QUALFLARE_MAX_TOTAL_ATTACHMENT_BYTES') ?? 750_000,\n maxVideoBytes: options.maxVideoBytes ?? envInt('QUALFLARE_MAX_VIDEO_BYTES') ?? MAX_VIDEO_UPLOAD_BYTES,\n enabled,\n outputDir,\n shardIndex,\n };\n}\n","import * as ciInfo from 'ci-info';\n\n/** Detected CI pipeline metadata, matching the wire contract's `ciProvider`/\n * `ciBuildNumber`/`ciRunUrl`/`ciPrNumber` fields exactly (`src/shared/types.ts`). */\nexport interface CiMetadata {\n ciProvider?: string;\n ciBuildNumber?: string;\n ciRunUrl?: string;\n ciPrNumber?: number;\n /** Identifier every shard of ONE CI run shares, and which differs between\n * runs. Distinct from `ciBuildNumber`: a build number is the human-facing\n * counter (GitHub's run NUMBER repeats across re-runs of a workflow),\n * whereas this is the unique run id. `qualflare-cli collect` groups report\n * files by it to refuse merging a stale file from an earlier run into the\n * current launch. */\n ciRunId?: string;\n}\n\ninterface ProviderExtractor {\n detect: (env: NodeJS.ProcessEnv) => boolean;\n providerName: string;\n buildNumber?: (env: NodeJS.ProcessEnv) => string | undefined;\n runUrl?: (env: NodeJS.ProcessEnv) => string | undefined;\n runId?: (env: NodeJS.ProcessEnv) => string | undefined;\n prNumber?: (env: NodeJS.ProcessEnv) => number | undefined;\n}\n\nfunction parsePositiveInt(raw: string | undefined): number | undefined {\n if (!raw) {\n return undefined;\n }\n const n = Number.parseInt(raw, 10);\n return Number.isFinite(n) && n >= 1 ? n : undefined;\n}\n\nfunction nonEmpty(raw: string | undefined): string | undefined {\n return raw && raw.length > 0 ? raw : undefined;\n}\n\n/** Explicit per-provider extraction for the fields `ci-info` doesn't\n * standardize (build number / run URL / PR number). Each `detect` reads\n * directly off the passed-in `env` — deliberately NOT delegating to\n * `ci-info`'s own per-vendor booleans (e.g. `ciInfo.GITHUB_ACTIONS`), which\n * are computed once against the real `process.env` at module-import time\n * and can't be re-evaluated against an injected env — see the module-level\n * comment on `detectCi` below. Checked in order; first match wins. */\nconst PROVIDERS: ProviderExtractor[] = [\n {\n detect: (env) => env.GITHUB_ACTIONS === 'true',\n providerName: 'GitHub Actions',\n buildNumber: (env) => nonEmpty(env.GITHUB_RUN_NUMBER),\n runId: (env) => nonEmpty(env.GITHUB_RUN_ID),\n runUrl: (env) =>\n env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY && env.GITHUB_RUN_ID\n ? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}`\n : undefined,\n prNumber: (env) => {\n const match = /^refs\\/pull\\/(\\d+)\\/merge$/.exec(env.GITHUB_REF ?? '');\n return match ? parsePositiveInt(match[1]) : undefined;\n },\n },\n {\n detect: (env) => env.GITLAB_CI === 'true',\n providerName: 'GitLab CI',\n buildNumber: (env) => nonEmpty(env.CI_PIPELINE_IID),\n runId: (env) => nonEmpty(env.CI_PIPELINE_ID),\n runUrl: (env) => nonEmpty(env.CI_PIPELINE_URL),\n prNumber: (env) => parsePositiveInt(env.CI_MERGE_REQUEST_IID),\n },\n {\n detect: (env) => env.CIRCLECI === 'true',\n providerName: 'CircleCI',\n buildNumber: (env) => nonEmpty(env.CIRCLE_BUILD_NUM),\n runId: (env) => nonEmpty(env.CIRCLE_WORKFLOW_ID ?? env.CIRCLE_BUILD_NUM),\n runUrl: (env) => nonEmpty(env.CIRCLE_BUILD_URL),\n prNumber: (env) => parsePositiveInt(env.CIRCLE_PR_NUMBER),\n },\n {\n detect: (env) => env.BUILDKITE === 'true',\n providerName: 'Buildkite',\n buildNumber: (env) => nonEmpty(env.BUILDKITE_BUILD_NUMBER),\n runId: (env) => nonEmpty(env.BUILDKITE_BUILD_ID),\n runUrl: (env) => nonEmpty(env.BUILDKITE_BUILD_URL),\n prNumber: (env) => {\n const raw = env.BUILDKITE_PULL_REQUEST;\n if (!raw || raw === 'false') {\n return undefined;\n }\n return parsePositiveInt(raw);\n },\n },\n {\n // Jenkins has no simple `JENKINS=true`-style flag; JENKINS_URL is always\n // set by the Jenkins agent and is the conventional detection signal.\n detect: (env) => Boolean(env.JENKINS_URL),\n providerName: 'Jenkins',\n buildNumber: (env) => nonEmpty(env.BUILD_NUMBER),\n runId: (env) => nonEmpty(env.BUILD_TAG ?? env.BUILD_NUMBER),\n runUrl: (env) => nonEmpty(env.BUILD_URL),\n // Jenkins has no standardized PR-number env var across its many PR\n // plugins (Multibranch, GitHub Branch Source, etc.) — deliberately\n // omitted rather than guessing at a plugin-specific variable.\n },\n {\n detect: (env) => env.TF_BUILD === 'True' || env.TF_BUILD === 'true',\n providerName: 'Azure Pipelines',\n buildNumber: (env) => nonEmpty(env.BUILD_BUILDID),\n runId: (env) => nonEmpty(env.BUILD_BUILDID),\n runUrl: (env) => {\n const collectionUri = env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;\n const project = env.SYSTEM_TEAMPROJECT;\n const buildId = env.BUILD_BUILDID;\n if (!collectionUri || !project || !buildId) {\n return undefined;\n }\n return `${collectionUri.replace(/\\/+$/, '')}/${encodeURIComponent(project)}/_build/results?buildId=${buildId}`;\n },\n prNumber: (env) => parsePositiveInt(env.SYSTEM_PULLREQUEST_PULLREQUESTNUMBER),\n },\n {\n detect: (env) => Boolean(env.BITBUCKET_BUILD_NUMBER),\n providerName: 'Bitbucket Pipelines',\n buildNumber: (env) => nonEmpty(env.BITBUCKET_BUILD_NUMBER),\n runId: (env) => nonEmpty(env.BITBUCKET_BUILD_NUMBER),\n runUrl: (env) => {\n const origin = env.BITBUCKET_GIT_HTTP_ORIGIN;\n if (!origin) {\n return undefined;\n }\n const resultsId = env.BITBUCKET_PIPELINE_UUID ?? env.BITBUCKET_BUILD_NUMBER;\n return resultsId ? `${origin}/addon/pipelines/home#!/results/${resultsId}` : undefined;\n },\n prNumber: (env) => parsePositiveInt(env.BITBUCKET_PR_ID),\n },\n];\n\n/**\n * Detects CI pipeline metadata for the `Collect.ciProvider`/`ciBuildNumber`/\n * `ciRunUrl`/`ciPrNumber` fields.\n *\n * IMPORTANT, non-obvious limitation: the `env` parameter only governs the\n * `PROVIDERS` table above (this module's own, directly-env-reading logic).\n * The `ci-info` fallback below does NOT honor it — `ci-info`'s package source\n * computes `exports.name`/`exports.isCI` exactly once, against the REAL\n * `process.env`, at module-import time (`const env = process.env` at its top\n * level) — there is no API to re-evaluate it against a different env object.\n * This is a non-issue in production (this function is always called against\n * the real `process.env` there); `ci-detect.test.ts` exercises the `ci-info`\n * fallback path via `vi.stubEnv` + `vi.resetModules()` (forcing a fresh\n * `ci-info` evaluation) rather than via this function's `env` parameter.\n */\nexport function detectCi(env: NodeJS.ProcessEnv = process.env): CiMetadata {\n const provider = PROVIDERS.find((p) => p.detect(env));\n if (provider) {\n const result: CiMetadata = { ciProvider: provider.providerName };\n const buildNumber = provider.buildNumber?.(env);\n if (buildNumber !== undefined) result.ciBuildNumber = buildNumber;\n const runUrl = provider.runUrl?.(env);\n if (runUrl !== undefined) result.ciRunUrl = runUrl;\n const prNumber = provider.prNumber?.(env);\n if (prNumber !== undefined) result.ciPrNumber = prNumber;\n const runId = provider.runId?.(env);\n if (runId !== undefined) result.ciRunId = runId;\n return result;\n }\n\n // Fallback: ci-info's ~70-provider detection gives us a free-text provider\n // name (the server's `ciProvider` field has no enum — an unrecognized\n // value is always accepted, per its doc comment in shared/types.ts) even\n // for providers our explicit table above doesn't cover a full\n // build-number/run-URL/PR-number extraction for.\n if (ciInfo.name) {\n return { ciProvider: ciInfo.name };\n }\n return {};\n}\n","import { execFileSync } from 'node:child_process';\n\n/** Auto-detected branch/commit — the bottom tier of `resolve-config.ts`'s\n * precedence chain (option > QUALFLARE_BRANCH/QF_BRANCH env > CI env vars >\n * local git subprocess > null). Does NOT include the QUALFLARE_BRANCH/\n * QF_BRANCH-style env aliases — those are resolved at a higher tier,\n * directly in `resolve-config.ts`, before this module is ever consulted. */\nexport interface GitInfo {\n branch?: string;\n commit?: string;\n}\n\n/** Injectable so tests never actually shell out. `stdio: ['ignore', 'pipe',\n * 'ignore']` suppresses git's own stderr chatter (e.g. \"fatal: not a git\n * repository\") from leaking into the CI log for what is, from this plugin's\n * perspective, an entirely expected, non-fatal outcome. */\nexport type ExecGit = (args: string[], cwd: string) => string;\n\nconst defaultExecGit: ExecGit = (args, cwd) =>\n execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });\n\nfunction firstEnv(env: NodeJS.ProcessEnv, ...names: string[]): string | undefined {\n for (const name of names) {\n const value = env[name];\n if (value) {\n return value;\n }\n }\n return undefined;\n}\n\nfunction detectBranchFromGit(exec: ExecGit, cwd: string): string | undefined {\n try {\n // Empty on detached HEAD (the `-q` flag suppresses the error and the\n // command still exits 0 with no output in that case on some git\n // versions, hence the explicit empty-string check as well as the\n // try/catch for versions that exit non-zero instead).\n const out = exec(['symbolic-ref', '--short', '-q', 'HEAD'], cwd).trim();\n return out || undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction detectCommitFromGit(exec: ExecGit, cwd: string): string | undefined {\n try {\n const out = exec(['rev-parse', 'HEAD'], cwd).trim();\n return out || undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Auto-detects branch/commit: CI-provider env vars first (cheap, no\n * subprocess), falling back to a local `git` subprocess only for whichever\n * of branch/commit the env vars didn't resolve — mirroring\n * `qualflare-cli/internal/config/config.go`'s `LoadFromEnv`\n * (`getFirstEnv(\"GIT_BRANCH\", \"GITHUB_REF_NAME\", \"CI_COMMIT_REF_NAME\",\n * \"BITBUCKET_BRANCH\")` / the equivalent commit chain) and its `DetectGit`'s\n * \"only shell out for what's actually missing\" behavior (BUG-39 in that\n * file: forking `git` on every CLI invocation, even `--help`, was wasteful —\n * the same reasoning applies here, this plugin should not fork two `git`\n * processes on every `cypress run` when CI env vars already cover both\n * values, or when the caller already resolved both from options/env at a\n * higher precedence tier and doesn't need this module at all).\n */\nexport function detectGit(\n env: NodeJS.ProcessEnv = process.env,\n cwd: string = process.cwd(),\n exec: ExecGit = defaultExecGit,\n): GitInfo {\n const branch =\n firstEnv(env, 'GIT_BRANCH', 'GITHUB_REF_NAME', 'CI_COMMIT_REF_NAME', 'BITBUCKET_BRANCH') ??\n detectBranchFromGit(exec, cwd);\n const commit =\n firstEnv(env, 'GIT_COMMIT', 'GITHUB_SHA', 'CI_COMMIT_SHA', 'BITBUCKET_COMMIT') ??\n detectCommitFromGit(exec, cwd);\n\n const result: GitInfo = {};\n if (branch !== undefined) result.branch = branch;\n if (commit !== undefined) result.commit = commit;\n return result;\n}\n","import { MAX_CASES_PER_SUITE, TASK_MARK_TEST_PHASE_STARTED, TASK_REPORT_CASE } from '../shared/constants.js';\nimport { logger } from '../shared/logger.js';\nimport type { Case } from '../shared/types.js';\nimport { resolveAttachments, type AttachmentBudget, type AttachmentReaderConfig } from './attachment-reader.js';\nimport type { PendingAttachmentQueue, TestPhaseGate } from './state.js';\n\n/**\n * Buffers finished `Case` objects the browser side hands over via\n * `cy.task(TASK_REPORT_CASE, ...)`, one spec file's worth at a time.\n * `events.ts`'s `after:spec` handler drains this into a `Suite` and clears\n * it before the next spec starts.\n *\n * `Suite.cases` is REJECT-not-truncate server-side (`max=5000`) — exceeding\n * it 400s the whole launch, not just this one spec's results. Warn-and-drop\n * further cases past that point, mirroring `state.ts`'s `LaunchAccumulator`\n * (2000-suite cap) exactly.\n */\nexport class CaseBuffer {\n private cases: Case[] = [];\n private truncated = false;\n\n add(testCase: Case): void {\n if (this.cases.length >= MAX_CASES_PER_SUITE) {\n if (!this.truncated) {\n this.truncated = true;\n logger.warn(\n `reached the server's ${MAX_CASES_PER_SUITE}-case-per-suite cap — further test results in this ` +\n 'spec file will not be uploaded.',\n );\n }\n return;\n }\n this.cases.push(testCase);\n }\n\n /** Returns the buffered cases and clears the buffer (including the\n * truncation-warned flag, so a later spec that also hits the cap warns\n * again rather than staying silent for the rest of the run). */\n drain(): Case[] {\n const drained = this.cases;\n this.cases = [];\n this.truncated = false;\n return drained;\n }\n}\n\n/**\n * Registers the `on('task', {...})` handlers the browser-side support\n * script calls via `cy.task()`. Cypress requires every task handler to\n * return a non-`undefined` value (a Promise resolving to `null` is fine)\n * or it throws.\n *\n * The TASK_REPORT_CASE handler also drains `pendingAttachments` (populated\n * Node-side by `events.ts`'s `after:screenshot` listener — see its comment\n * for why screenshot capture needs no browser-side/`cy.task()` involvement\n * at all) and resolves each into inline base64 content (or drops it) before\n * buffering the case — this is the one place both \"a finished test\" and\n * \"screenshots taken during that test\" are known at the same time.\n *\n * `Attachment.stepIndex` (correlating a screenshot to the specific step\n * executing when it was taken) is deliberately left unset here. Screenshots\n * are captured entirely Node-side via `after:screenshot`, while steps are\n * built entirely browser-side (`command-log-listener.ts`) and only reach\n * Node bundled inside the already-finished `Case` — by the time\n * `after:screenshot` fires, Node has no visibility into which step index\n * the browser's still-in-progress step buffer is currently on, and no cheap\n * way to ask it without adding a new `cy.task()` round-trip purely to answer\n * \"what step are we on,\" which isn't justified for this milestone. Every\n * screenshot is attached at the case level instead (works today, matches\n * Milestone 2's existing behavior) — worth revisiting only if step-level\n * screenshot attribution becomes a real, requested feature. */\nexport function registerTasks(\n on: Cypress.PluginEvents,\n buffer: CaseBuffer,\n attachmentConfig: AttachmentReaderConfig,\n attachmentBudget: AttachmentBudget,\n pendingAttachments: PendingAttachmentQueue,\n testPhaseGate: TestPhaseGate,\n): void {\n on('task', {\n async [TASK_REPORT_CASE](testCase: Case): Promise<null> {\n // Merge screenshots captured Node-side during this test (via\n // after:screenshot) with any attachments the browser side already\n // set directly on the Case (e.g. a `qualflare.attachmentFromFile()`\n // call, which carries only a `path` — resolveAttachments reads/\n // uploads it here, same as a screenshot).\n const attachments = [...(testCase.attachments ?? []), ...pendingAttachments.drain()];\n const resolved = await resolveAttachments(\n attachments.length > 0 ? attachments : undefined,\n attachmentConfig,\n attachmentBudget,\n );\n buffer.add({ ...testCase, attachments: resolved });\n return null;\n },\n // One-shot signal from the browser side (see TestPhaseGate's doc\n // comment in state.ts) — fired from a root-level beforeEach right\n // before the first test's body runs.\n [TASK_MARK_TEST_PHASE_STARTED](): null {\n testPhaseGate.markStarted();\n return null;\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,IAAAA,MAAoB;AACpB,IAAAC,QAAsB;;;ACUtB,IAAM,SAAS;AAER,IAAM,SAAS;AAAA,EACpB,SAAS,MAAuB;AAC9B,YAAQ,MAAM,QAAQ,GAAG,IAAI;AAAA,EAC/B;AAAA,EACA,QAAQ,MAAuB;AAC7B,YAAQ,IAAI,QAAQ,GAAG,IAAI;AAAA,EAC7B;AAAA,EACA,QAAQ,MAAuB;AAC7B,YAAQ,KAAK,QAAQ,GAAG,IAAI;AAAA,EAC9B;AAAA,EACA,SAAS,MAAuB;AAC9B,YAAQ,MAAM,QAAQ,GAAG,IAAI;AAAA,EAC/B;AACF;;;AC1BA,SAAoB;AACpB,WAAsB;AACtB,yBAA2B;AAU3B,IAAM,gCAAwD;AAAA,EAC5D,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AACV;AAyBO,SAAS,oBACd,UACA,WACA,eAC6B;AAC7B,QAAM,MAAW,aAAQ,QAAQ,EAAE,YAAY;AAC/C,QAAM,WAAW,8BAA8B,GAAG;AAClD,MAAI,CAAC,UAAU;AACb,WAAO,KAAK,8BAA8B,QAAQ,8BAA8B;AAChF,WAAO;AAAA,EACT;AAEA,MAAI;AACJ,MAAI;AAGF,eAAc,YAAS,QAAQ,EAAE;AAAA,EACnC,SAAS,KAAK;AACZ,WAAO,KAAK,8BAA8B,QAAQ,2BAA4B,IAAc,OAAO,EAAE;AACrG,WAAO;AAAA,EACT;AACA,MAAI,WAAW,eAAe;AAC5B,WAAO;AAAA,MACL,8BAA8B,QAAQ,MAAM,QAAQ,sDAC1B,aAAa;AAAA,IACzC;AACA,WAAO;AAAA,EACT;AAEA,QAAM,iBAAiB,OAAG,+BAAW,CAAC,GAAG,GAAG;AAC5C,MAAI;AACF,IAAG,aAAU,WAAW,EAAE,WAAW,KAAK,CAAC;AAC3C,IAAG,gBAAa,UAAe,UAAK,WAAW,cAAc,CAAC;AAAA,EAChE,SAAS,KAAK;AACZ,WAAO,KAAK,8BAA8B,QAAQ,2BAA4B,IAAc,OAAO,EAAE;AACrG,WAAO;AAAA,EACT;AAEA,SAAO,EAAE,gBAAgB,UAAU,SAAS;AAC9C;;;AFhEA,IAAM,mBAAmB,oBAAI,IAAI,CAAC,QAAQ,SAAS,QAAQ,QAAQ,MAAM,CAAC;AAoBnE,IAAM,mBAAN,MAAuB;AAAA,EAG5B,YAA6B,eAAuB;AAAvB;AAAA,EAAwB;AAAA,EAAxB;AAAA,EAFrB,OAAO;AAAA;AAAA;AAAA,EAMf,WAAW,OAAwB;AACjC,QAAI,KAAK,OAAO,QAAQ,KAAK,eAAe;AAC1C,aAAO;AAAA,IACT;AACA,SAAK,QAAQ;AACb,WAAO;AAAA,EACT;AAAA,EAEA,IAAI,YAAoB;AACtB,WAAO,KAAK;AAAA,EACd;AACF;AAIA,SAAS,YAAY,YAAiC;AACpD,MAAI,WAAW,UAAU,YAAY,EAAE,WAAW,QAAQ,GAAG;AAC3D,WAAO;AAAA,EACT;AACA,MAAI,WAAW,QAAQ,iBAAiB,IAAS,cAAQ,WAAW,IAAI,EAAE,YAAY,CAAC,GAAG;AACxF,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,SAAS,mBAAmB,UAAkB,oBAA4B,QAAsC;AAC9G,MAAI;AACJ,MAAI;AAGF,WAAU,aAAS,QAAQ,EAAE;AAAA,EAC/B,SAAS,KAAK;AACZ,WAAO,EAAE,SAAS,MAAM,QAAQ,wBAAyB,IAAc,OAAO,GAAG;AAAA,EACnF;AACA,MAAI,OAAO,oBAAoB;AAC7B,WAAO;AAAA,MACL,SAAS;AAAA,MACT,QAAQ,GAAG,IAAI,uDAAuD,kBAAkB;AAAA,IAC1F;AAAA,EACF;AACA,MAAI,CAAC,OAAO,WAAW,IAAI,GAAG;AAC5B,WAAO;AAAA,MACL,SAAS;AAAA,MACT,QAAQ,oDAAoD,OAAO,SAAS;AAAA,IAC9E;AAAA,EACF;AACA,MAAI;AACF,UAAM,UAAa,iBAAa,QAAQ,EAAE,SAAS,QAAQ;AAC3D,WAAO,EAAE,SAAS,OAAO,QAAQ;AAAA,EACnC,SAAS,KAAK;AACZ,WAAO,EAAE,SAAS,MAAM,QAAQ,wBAAyB,IAAc,OAAO,GAAG;AAAA,EACnF;AACF;AAsBO,SAAS,mBACd,aACA,QACA,QAC0B;AAC1B,MAAI,CAAC,eAAe,YAAY,WAAW,GAAG;AAC5C,WAAO;AAAA,EACT;AACA,MAAI,CAAC,OAAO,mBAAmB;AAC7B,WAAO;AAAA,EACT;AAEA,QAAM,WAAyB,CAAC;AAChC,aAAW,cAAc,aAAa;AACpC,QAAI,YAAY,UAAU,GAAG;AAC3B,UAAI,CAAC,WAAW,MAAM;AACpB,eAAO,KAAK,8BAA8B,WAAW,IAAI,gCAAgC;AACzF;AAAA,MACF;AACA,YAAM,SAAS,oBAAoB,WAAW,MAAM,OAAO,WAAW,OAAO,aAAa;AAC1F,UAAI,CAAC,QAAQ;AAEX;AAAA,MACF;AACA,eAAS,KAAK;AAAA,QACZ,GAAG;AAAA,QACH,UAAU,OAAO;AAAA,QACjB,gBAAgB,OAAO;AAAA,QACvB,UAAU,OAAO;AAAA,MACnB,CAAC;AACD;AAAA,IACF;AACA,QAAI,WAAW,YAAY,UAAa,CAAC,WAAW,MAAM;AAIxD,eAAS,KAAK,UAAU;AACxB;AAAA,IACF;AACA,UAAM,SAAS,mBAAmB,WAAW,MAAM,OAAO,oBAAoB,MAAM;AACpF,QAAI,OAAO,SAAS;AAClB,aAAO,KAAK,wBAAwB,WAAW,IAAI,MAAM,WAAW,IAAI,MAAM,OAAO,MAAM,EAAE;AAC7F;AAAA,IACF;AACA,aAAS,KAAK,EAAE,GAAG,YAAY,SAAS,OAAO,QAAQ,CAAC;AAAA,EAC1D;AACA,SAAO,SAAS,SAAS,IAAI,WAAW;AAC1C;;;AGpKA,IAAAC,MAAoB;AACpB,IAAAC,QAAsB;AACtB,IAAAC,sBAA2B;;;ACOpB,IAAM,mBAAmB;AASzB,IAAM,+BAA+B;AAIrC,IAAM,wBAAwB;AAC9B,IAAM,sBAAsB;AAG5B,IAAM,2BAA2B;AAQjC,IAAM,yBAAyB,KAAK,OAAO;;;AChClD,IAAM,YAAY;AAWX,SAAS,OAAO,IAAgC;AACrD,MAAI,CAAC,OAAO,SAAS,EAAE,KAAK,MAAM,GAAG;AACnC,WAAO;AAAA,EACT;AACA,SAAO,KAAK,MAAM,KAAK,SAAS;AAClC;;;AClBA,SAAoB;;;ACUb,IAAM,kBAA0B;;;ADMvC,SAAS,UAAU,QAA8B,MAAuC;AACtF,MAAI,OAAO,IAAI;AACb,WAAO,OAAO;AAAA,EAChB;AACA,MAAI,MAAM,QAAQ;AAChB,WAAO,KAAK,YAAY,GAAG,KAAK,MAAM,IAAI,KAAK,SAAS,KAAK,KAAK;AAAA,EACpE;AACA,SAAO,GAAM,QAAK,CAAC,IAAO,WAAQ,CAAC;AACrC;AAEA,SAAS,eAAe,QAA8B,MAAuC;AAC3F,MAAI,OAAO,SAAS;AAClB,WAAO,OAAO;AAAA,EAChB;AACA,MAAI,MAAM,aAAa;AACrB,WAAO,KAAK,iBAAiB,GAAG,KAAK,WAAW,IAAI,KAAK,cAAc,KAAK,KAAK;AAAA,EACnF;AACA,SAAO;AACT;AAWO,SAAS,oBACd,aACA,QACA,aACS;AACT,SAAO;AAAA,IACL,WAAW,OAAO;AAAA,IAClB,UAAU,OAAO;AAAA,IACjB,IAAI,UAAU,QAAQ,WAAW;AAAA,IACjC,SAAS,eAAe,QAAQ,WAAW;AAAA,IAC3C,QAAQ,OAAO;AAAA,IACf,QAAQ,OAAO;AAAA,IACf,aAAa,OAAO;AAAA,IACpB,UAAU,OAAO;AAAA,IACjB,WAAW,OAAO;AAAA,IAClB,UAAU;AAAA,MACR,SAAS;AAAA,MACT,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,SAAS;AAAA,MACT,OAAO,OAAO;AAAA,IAChB;AAAA,IACA,YAAY,OAAO;AAAA,IACnB,QAAQ,YAAY,UAAU;AAAA,IAC9B,YAAY,OAAO;AAAA,IACnB,eAAe,OAAO;AAAA,IACtB,UAAU,OAAO;AAAA,IACjB,YAAY,OAAO;AAAA,EACrB;AACF;;;AE/DO,IAAM,oBAAN,MAAwB;AAAA,EACZ,SAAkB,CAAC;AAAA,EAC5B,YAAY;AAAA,EAEpB,SAAS,OAAoB;AAC3B,QAAI,KAAK,OAAO,UAAU,uBAAuB;AAC/C,UAAI,CAAC,KAAK,WAAW;AACnB,aAAK,YAAY;AACjB,eAAO;AAAA,UACL,wBAAwB,qBAAqB;AAAA,QAE/C;AAAA,MACF;AACA;AAAA,IACF;AACA,SAAK,OAAO,KAAK,KAAK;AAAA,EACxB;AAAA,EAEA,YAAqB;AACnB,WAAO,KAAK;AAAA,EACd;AACF;AA4BO,IAAM,gBAAN,MAAoB;AAAA,EACjB,UAAU;AAAA,EAElB,cAAoB;AAClB,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,aAAsB;AACpB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA,EAIA,QAAc;AACZ,SAAK,UAAU;AAAA,EACjB;AACF;AAiBO,IAAM,yBAAN,MAA6B;AAAA,EAC1B,UAAwB,CAAC;AAAA,EAEjC,QAAQ,YAA8B;AACpC,SAAK,QAAQ,KAAK,UAAU;AAAA,EAC9B;AAAA;AAAA,EAGA,QAAsB;AACpB,UAAM,UAAU,KAAK;AACrB,SAAK,UAAU,CAAC;AAChB,WAAO;AAAA,EACT;AACF;;;ALzFA,IAAM,mBAA4C,oBAAI,IAAI,CAAC,UAAU,SAAS,SAAS,CAAC;AAUjF,SAAS,eACd,IACA,QACA,QACA,oBACA,eACM;AACN,QAAM,cAAc,IAAI,kBAAkB;AAC1C,MAAI;AACJ,MAAI,mBAAmB;AAEvB,KAAG,cAAc,CAAC,YAAY;AAC5B,kBAAc;AAAA,MACZ,aAAa,QAAQ,SAAS;AAAA,MAC9B,gBAAgB,QAAQ,SAAS;AAAA,MACjC,QAAQ,QAAQ,QAAQ;AAAA,MACxB,WAAW,QAAQ,QAAQ;AAAA,IAC7B;AAAA,EACF,CAAC;AAiBD,KAAG,oBAAoB,CAAC,YAAY;AAClC,QAAI,CAAC,cAAc,WAAW,GAAG;AAC/B,aAAO;AAAA,QACL,kBAAkB,QAAQ,QAAQ,SAAS;AAAA,MAE7C;AACA;AAAA,IACF;AACA,uBAAmB,QAAQ;AAAA,MACzB,MAAM,QAAQ,SAAS,QAAQ,cAAc,uBAAuB;AAAA,MACpE,MAAM,QAAQ;AAAA,MACd,UAAU;AAAA,IACZ,CAAC;AAAA,EACH,CAAC;AAED,KAAG,eAAe,MAAM;AACtB,uBAAmB,KAAK,IAAI;AAC5B,kBAAc,MAAM;AAIpB,WAAO,MAAM;AAAA,EACf,CAAC;AAED,KAAG,cAAc,OAAO,MAAM,YAAY;AACxC,UAAM,QAAQ,OAAO,MAAM;AAU3B,QAAI,QAAQ,OAAO;AACjB,YAAM,aAAa,MAAM,KAAK,CAAC,MAAM,iBAAiB,IAAI,EAAE,MAAM,CAAC;AACnE,UAAI,YAAY;AACd,cAAM,SAAS,oBAAoB,QAAQ,OAAO,OAAO,WAAW,OAAO,aAAa;AACxF,YAAI,QAAQ;AACV,sBAAY,YAAY,MAAM;AAAA,QAChC;AAAA,MACF,OAAO;AACL,eAAO,KAAK,QAAQ,KAAK,QAAQ,2DAAsD;AAAA,MACzF;AAAA,IACF;AAEA,UAAM,QAAe;AAAA,MACnB,MAAM,KAAK;AAAA,MACX,UAAU;AAAA,MACV,UAAU,OAAO,QAAQ,MAAM,YAAY,KAAK,IAAI,IAAI,gBAAgB;AAAA,MACxE,WAAW,IAAI,KAAK,QAAQ,MAAM,aAAa,KAAK,IAAI,CAAC,EAAE,YAAY;AAAA,MACvE;AAAA,IACF;AAEA,QAAI,MAAM,WAAW,QAAQ,MAAM,OAAO;AACxC,aAAO;AAAA,QACL,QAAQ,KAAK,QAAQ,cAAc,MAAM,MAAM,iCAC1C,QAAQ,MAAM,KAAK;AAAA,MAC1B;AAAA,IACF;AAEA,gBAAY,SAAS,KAAK;AAO1B,UAAM,WAAW,mBAAmB,MAAM;AAC1C,QAAI,SAAS,SAAS,GAAG;AACvB,aAAO;AAAA,QACL,QAAQ,KAAK,QAAQ,KAAK,SAAS,MAAM;AAAA,MAE3C;AAAA,IACF;AAAA,EACF,CAAC;AAED,KAAG,aAAa,YAAY;AAC1B,UAAM,SAAS,YAAY,UAAU;AACrC,QAAI,OAAO,WAAW,GAAG;AACvB,aAAO,KAAK,oEAA+D;AAC3E;AAAA,IACF;AAEA,UAAM,UAAU,oBAAoB,aAAa,QAAQ,WAAW;AACpE,QAAI,OAAO,eAAe,QAAW;AACnC,iBAAW,SAAS,QAAQ,QAAQ;AAClC,mBAAW,KAAK,MAAM,OAAO;AAC3B,YAAE,aAAa,OAAO;AAAA,QACxB;AAAA,MACF;AAAA,IACF;AAEA,IAAG,cAAU,OAAO,WAAW,EAAE,WAAW,KAAK,CAAC;AAClD,UAAM,aAAkB,WAAK,OAAO,WAAW,OAAG,gCAAW,CAAC,OAAO;AACrE,IAAG,kBAAc,YAAY,KAAK,UAAU,OAAO,CAAC;AACpD,WAAO,KAAK,4BAA4B,UAAU,uCAAkC,OAAO,SAAS,kBAAkB;AAAA,EACxH,CAAC;AACH;AAMA,SAAS,YAAY,UAAgB,QAA8E;AACjH,QAAM,cAAc,SAAS,eAAe,CAAC;AAC7C,MAAI,YAAY,UAAU,0BAA0B;AAClD,WAAO;AAAA,MACL,2BAA2B,SAAS,IAAI,8BAA8B,wBAAwB;AAAA,IAChG;AACA;AAAA,EACF;AACA,WAAS,cAAc;AAAA,IACrB,GAAG;AAAA,IACH;AAAA,MACE,MAAM;AAAA,MACN,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,IACnB;AAAA,EACF;AACF;;;AMvLA,IAAAC,sBAA2B;;;ACA3B,aAAwB;AA2BxB,SAAS,iBAAiB,KAA6C;AACrE,MAAI,CAAC,KAAK;AACR,WAAO;AAAA,EACT;AACA,QAAM,IAAI,OAAO,SAAS,KAAK,EAAE;AACjC,SAAO,OAAO,SAAS,CAAC,KAAK,KAAK,IAAI,IAAI;AAC5C;AAEA,SAAS,SAAS,KAA6C;AAC7D,SAAO,OAAO,IAAI,SAAS,IAAI,MAAM;AACvC;AASA,IAAM,YAAiC;AAAA,EACrC;AAAA,IACE,QAAQ,CAAC,QAAQ,IAAI,mBAAmB;AAAA,IACxC,cAAc;AAAA,IACd,aAAa,CAAC,QAAQ,SAAS,IAAI,iBAAiB;AAAA,IACpD,OAAO,CAAC,QAAQ,SAAS,IAAI,aAAa;AAAA,IAC1C,QAAQ,CAAC,QACP,IAAI,qBAAqB,IAAI,qBAAqB,IAAI,gBAClD,GAAG,IAAI,iBAAiB,IAAI,IAAI,iBAAiB,iBAAiB,IAAI,aAAa,KACnF;AAAA,IACN,UAAU,CAAC,QAAQ;AACjB,YAAM,QAAQ,6BAA6B,KAAK,IAAI,cAAc,EAAE;AACpE,aAAO,QAAQ,iBAAiB,MAAM,CAAC,CAAC,IAAI;AAAA,IAC9C;AAAA,EACF;AAAA,EACA;AAAA,IACE,QAAQ,CAAC,QAAQ,IAAI,cAAc;AAAA,IACnC,cAAc;AAAA,IACd,aAAa,CAAC,QAAQ,SAAS,IAAI,eAAe;AAAA,IAClD,OAAO,CAAC,QAAQ,SAAS,IAAI,cAAc;AAAA,IAC3C,QAAQ,CAAC,QAAQ,SAAS,IAAI,eAAe;AAAA,IAC7C,UAAU,CAAC,QAAQ,iBAAiB,IAAI,oBAAoB;AAAA,EAC9D;AAAA,EACA;AAAA,IACE,QAAQ,CAAC,QAAQ,IAAI,aAAa;AAAA,IAClC,cAAc;AAAA,IACd,aAAa,CAAC,QAAQ,SAAS,IAAI,gBAAgB;AAAA,IACnD,OAAO,CAAC,QAAQ,SAAS,IAAI,sBAAsB,IAAI,gBAAgB;AAAA,IACvE,QAAQ,CAAC,QAAQ,SAAS,IAAI,gBAAgB;AAAA,IAC9C,UAAU,CAAC,QAAQ,iBAAiB,IAAI,gBAAgB;AAAA,EAC1D;AAAA,EACA;AAAA,IACE,QAAQ,CAAC,QAAQ,IAAI,cAAc;AAAA,IACnC,cAAc;AAAA,IACd,aAAa,CAAC,QAAQ,SAAS,IAAI,sBAAsB;AAAA,IACzD,OAAO,CAAC,QAAQ,SAAS,IAAI,kBAAkB;AAAA,IAC/C,QAAQ,CAAC,QAAQ,SAAS,IAAI,mBAAmB;AAAA,IACjD,UAAU,CAAC,QAAQ;AACjB,YAAM,MAAM,IAAI;AAChB,UAAI,CAAC,OAAO,QAAQ,SAAS;AAC3B,eAAO;AAAA,MACT;AACA,aAAO,iBAAiB,GAAG;AAAA,IAC7B;AAAA,EACF;AAAA,EACA;AAAA;AAAA;AAAA,IAGE,QAAQ,CAAC,QAAQ,QAAQ,IAAI,WAAW;AAAA,IACxC,cAAc;AAAA,IACd,aAAa,CAAC,QAAQ,SAAS,IAAI,YAAY;AAAA,IAC/C,OAAO,CAAC,QAAQ,SAAS,IAAI,aAAa,IAAI,YAAY;AAAA,IAC1D,QAAQ,CAAC,QAAQ,SAAS,IAAI,SAAS;AAAA;AAAA;AAAA;AAAA,EAIzC;AAAA,EACA;AAAA,IACE,QAAQ,CAAC,QAAQ,IAAI,aAAa,UAAU,IAAI,aAAa;AAAA,IAC7D,cAAc;AAAA,IACd,aAAa,CAAC,QAAQ,SAAS,IAAI,aAAa;AAAA,IAChD,OAAO,CAAC,QAAQ,SAAS,IAAI,aAAa;AAAA,IAC1C,QAAQ,CAAC,QAAQ;AACf,YAAM,gBAAgB,IAAI;AAC1B,YAAM,UAAU,IAAI;AACpB,YAAM,UAAU,IAAI;AACpB,UAAI,CAAC,iBAAiB,CAAC,WAAW,CAAC,SAAS;AAC1C,eAAO;AAAA,MACT;AACA,aAAO,GAAG,cAAc,QAAQ,QAAQ,EAAE,CAAC,IAAI,mBAAmB,OAAO,CAAC,2BAA2B,OAAO;AAAA,IAC9G;AAAA,IACA,UAAU,CAAC,QAAQ,iBAAiB,IAAI,oCAAoC;AAAA,EAC9E;AAAA,EACA;AAAA,IACE,QAAQ,CAAC,QAAQ,QAAQ,IAAI,sBAAsB;AAAA,IACnD,cAAc;AAAA,IACd,aAAa,CAAC,QAAQ,SAAS,IAAI,sBAAsB;AAAA,IACzD,OAAO,CAAC,QAAQ,SAAS,IAAI,sBAAsB;AAAA,IACnD,QAAQ,CAAC,QAAQ;AACf,YAAM,SAAS,IAAI;AACnB,UAAI,CAAC,QAAQ;AACX,eAAO;AAAA,MACT;AACA,YAAM,YAAY,IAAI,2BAA2B,IAAI;AACrD,aAAO,YAAY,GAAG,MAAM,mCAAmC,SAAS,KAAK;AAAA,IAC/E;AAAA,IACA,UAAU,CAAC,QAAQ,iBAAiB,IAAI,eAAe;AAAA,EACzD;AACF;AAiBO,SAAS,SAAS,MAAyB,QAAQ,KAAiB;AACzE,QAAM,WAAW,UAAU,KAAK,CAAC,MAAM,EAAE,OAAO,GAAG,CAAC;AACpD,MAAI,UAAU;AACZ,UAAM,SAAqB,EAAE,YAAY,SAAS,aAAa;AAC/D,UAAM,cAAc,SAAS,cAAc,GAAG;AAC9C,QAAI,gBAAgB,OAAW,QAAO,gBAAgB;AACtD,UAAM,SAAS,SAAS,SAAS,GAAG;AACpC,QAAI,WAAW,OAAW,QAAO,WAAW;AAC5C,UAAM,WAAW,SAAS,WAAW,GAAG;AACxC,QAAI,aAAa,OAAW,QAAO,aAAa;AAChD,UAAM,QAAQ,SAAS,QAAQ,GAAG;AAClC,QAAI,UAAU,OAAW,QAAO,UAAU;AAC1C,WAAO;AAAA,EACT;AAOA,MAAW,aAAM;AACf,WAAO,EAAE,YAAmB,YAAK;AAAA,EACnC;AACA,SAAO,CAAC;AACV;;;AC/KA,gCAA6B;AAkB7B,IAAM,iBAA0B,CAAC,MAAM,YACrC,wCAAa,OAAO,MAAM,EAAE,KAAK,UAAU,QAAQ,OAAO,CAAC,UAAU,QAAQ,QAAQ,EAAE,CAAC;AAE1F,SAAS,SAAS,QAA2B,OAAqC;AAChF,aAAWC,SAAQ,OAAO;AACxB,UAAM,QAAQ,IAAIA,KAAI;AACtB,QAAI,OAAO;AACT,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,oBAAoB,MAAe,KAAiC;AAC3E,MAAI;AAKF,UAAM,MAAM,KAAK,CAAC,gBAAgB,WAAW,MAAM,MAAM,GAAG,GAAG,EAAE,KAAK;AACtE,WAAO,OAAO;AAAA,EAChB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,oBAAoB,MAAe,KAAiC;AAC3E,MAAI;AACF,UAAM,MAAM,KAAK,CAAC,aAAa,MAAM,GAAG,GAAG,EAAE,KAAK;AAClD,WAAO,OAAO;AAAA,EAChB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAgBO,SAAS,UACd,MAAyB,QAAQ,KACjC,MAAc,QAAQ,IAAI,GAC1B,OAAgB,gBACP;AACT,QAAM,SACJ,SAAS,KAAK,cAAc,mBAAmB,sBAAsB,kBAAkB,KACvF,oBAAoB,MAAM,GAAG;AAC/B,QAAM,SACJ,SAAS,KAAK,cAAc,cAAc,iBAAiB,kBAAkB,KAC7E,oBAAoB,MAAM,GAAG;AAE/B,QAAM,SAAkB,CAAC;AACzB,MAAI,WAAW,OAAW,QAAO,SAAS;AAC1C,MAAI,WAAW,OAAW,QAAO,SAAS;AAC1C,SAAO;AACT;;;AFKA,SAASC,aAAY,OAAqC;AACxD,aAAWC,SAAQ,OAAO;AACxB,UAAM,QAAQ,QAAQ,IAAIA,KAAI;AAC9B,QAAI,UAAU,UAAa,UAAU,IAAI;AACvC,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,WAAW,OAAsC;AACxD,QAAM,MAAMD,UAAS,GAAG,KAAK;AAC7B,MAAI,QAAQ,QAAW;AACrB,WAAO;AAAA,EACT;AACA,SAAO,QAAQ,UAAU,QAAQ;AACnC;AAEA,SAAS,UAAU,OAAqC;AACtD,QAAM,MAAMA,UAAS,GAAG,KAAK;AAC7B,MAAI,QAAQ,QAAW;AACrB,WAAO;AAAA,EACT;AACA,QAAM,SAAS,OAAO,SAAS,KAAK,EAAE;AACtC,SAAO,OAAO,SAAS,MAAM,IAAI,SAAS;AAC5C;AA2BO,SAAS,cACd,SACA,OAAmE,CAAC,GAC9C;AACtB,QAAM,cAAc,KAAK,aAAa;AACtC,QAAM,aAAa,KAAK,YAAY;AAEpC,QAAM,UAAU,QAAQ,WAAW,QAAQ,mBAAmB,KAAK;AACnE,QAAM,YAAY,QAAQ,aAAaA,UAAS,sBAAsB,KAAK;AAC3E,QAAM,aAAa,QAAQ,cAAc,OAAO,uBAAuB;AAEvE,QAAM,eAAe,QAAQ,cAAc,SAAY,QAAQ,YAAY,OAAO,uBAAuB,cAAc;AACvH,QAAM,YAAY,iBAAiB,UAAa,iBAAiB,QAAQ,gBAAgB,IAAI,eAAe;AAE5G,QAAM,YAAYA,UAAS,oBAAoB,WAAW;AAC1D,QAAM,YAAYA,UAAS,oBAAoB,WAAW;AAC1D,QAAM,oBACH,QAAQ,WAAW,UAAa,cAAc,UAC9C,QAAQ,WAAW,UAAa,cAAc;AACjD,QAAM,cAAc,oBAAoB,YAAY,IAAI,CAAC;AAEzD,QAAM,SAAS,QAAQ,WAAW,SAAY,QAAQ,SAAU,aAAa,YAAY,UAAU;AACnG,QAAM,SAAS,QAAQ,WAAW,SAAY,QAAQ,SAAU,aAAa,YAAY,UAAU;AAEnG,QAAM,aAAa,WAAW;AAC9B,QAAM,aAAa,QAAQ,cAAc,WAAW;AACpD,QAAM,gBAAgB,QAAQ,iBAAiB,WAAW;AAC1D,QAAM,WAAW,QAAQ,YAAY,WAAW;AAChD,QAAM,aAAa,QAAQ,cAAc,WAAW;AAKpD,QAAM,QAAQ,QAAQ,SAASA,UAAS,kBAAkB,KAAK,WAAW,eAAW,gCAAW;AAEhG,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASL,cAAc,QAAQ,eAAe,WAAcA,UAAS,yBAAyB,gBAAgB,KAAK;AAAA,IAC1G,WAAW,QAAQ,YAAY,WAAcA,UAAS,sBAAsB,aAAa,KAAK;AAAA,IAC9F;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU,QAAQ,YAAY;AAAA,IAC9B,WAAW,QAAQ,aAAa;AAAA,IAChC,IAAI,QAAQ;AAAA,IACZ,SAAS,QAAQ;AAAA,IACjB,YAAY,QAAQ;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,mBAAmB,QAAQ,qBAAqB,QAAQ,8BAA8B,KAAK;AAAA,IAC3F,oBAAoB,QAAQ,sBAAsB,OAAO,gCAAgC,KAAK;AAAA,IAC9F,yBACE,QAAQ,2BAA2B,OAAO,sCAAsC,KAAK;AAAA,IACvF,eAAe,QAAQ,iBAAiB,OAAO,2BAA2B,KAAK;AAAA,IAC/E;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;;;AG/LO,IAAM,aAAN,MAAiB;AAAA,EACd,QAAgB,CAAC;AAAA,EACjB,YAAY;AAAA,EAEpB,IAAI,UAAsB;AACxB,QAAI,KAAK,MAAM,UAAU,qBAAqB;AAC5C,UAAI,CAAC,KAAK,WAAW;AACnB,aAAK,YAAY;AACjB,eAAO;AAAA,UACL,wBAAwB,mBAAmB;AAAA,QAE7C;AAAA,MACF;AACA;AAAA,IACF;AACA,SAAK,MAAM,KAAK,QAAQ;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAKA,QAAgB;AACd,UAAM,UAAU,KAAK;AACrB,SAAK,QAAQ,CAAC;AACd,SAAK,YAAY;AACjB,WAAO;AAAA,EACT;AACF;AA2BO,SAAS,cACd,IACA,QACA,kBACA,kBACA,oBACA,eACM;AACN,KAAG,QAAQ;AAAA,IACT,OAAO,gBAAgB,EAAE,UAA+B;AAMtD,YAAM,cAAc,CAAC,GAAI,SAAS,eAAe,CAAC,GAAI,GAAG,mBAAmB,MAAM,CAAC;AACnF,YAAM,WAAW,MAAM;AAAA,QACrB,YAAY,SAAS,IAAI,cAAc;AAAA,QACvC;AAAA,QACA;AAAA,MACF;AACA,aAAO,IAAI,EAAE,GAAG,UAAU,aAAa,SAAS,CAAC;AACjD,aAAO;AAAA,IACT;AAAA;AAAA;AAAA;AAAA,IAIA,CAAC,4BAA4B,IAAU;AACrC,oBAAc,YAAY;AAC1B,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AACH;;;Ab5EO,SAAS,iBACd,IACA,QACA,UAAmC,CAAC,GACP;AAC7B,QAAM,WAAW,cAAc,OAAO;AACtC,QAAM,SAAS,IAAI,WAAW;AAC9B,QAAM,qBAAqB,IAAI,uBAAuB;AACtD,QAAM,gBAAgB,IAAI,cAAc;AACxC,QAAM,mBAAmB,IAAI,iBAAiB,SAAS,uBAAuB;AAE9E,QAAM,mBAA2C;AAKjD,gBAAc,IAAI,QAAQ,kBAAkB,kBAAkB,oBAAoB,aAAa;AAE/F,MAAI,SAAS,SAAS;AACpB,mBAAe,IAAI,UAAU,QAAQ,oBAAoB,aAAa;AAAA,EACxE;AAEA,SAAO;AACT;","names":["fs","path","fs","path","import_node_crypto","import_node_crypto","name","firstEnv","name"]}
1
+ {"version":3,"sources":["../../src/plugin/index.ts","../../src/plugin/attachment-reader.ts","../../src/shared/logger.ts","../../src/plugin/video-writer.ts","../../src/plugin/events.ts","../../src/shared/constants.ts","../../src/shared/duration.ts","../../src/plugin/collect-builder.ts","../../src/plugin/version.ts","../../src/plugin/state.ts","../../src/plugin/resolve-config.ts","../../src/plugin/ci-detect.ts","../../src/plugin/git-detect.ts","../../src/plugin/tasks.ts"],"sourcesContent":["import { AttachmentBudget, type AttachmentReaderConfig } from './attachment-reader.js';\nimport { registerEvents } from './events.js';\nimport { resolveConfig, type QualflareCypressOptions } from './resolve-config.js';\nimport { PendingAttachmentQueue, TestPhaseGate } from './state.js';\nimport { CaseBuffer, registerTasks } from './tasks.js';\n\nexport type { QualflareCypressOptions, ResolvedPluginConfig } from './resolve-config.js';\n\n/**\n * Wires qualflare-cypress into `setupNodeEvents`. Returns `config`\n * unmodified so it composes with a user's own `setupNodeEvents` body and\n * with other plugins:\n *\n * ```ts\n * // cypress.config.ts\n * import { defineConfig } from 'cypress';\n * import { qualflareCypress } from '@qualflare/cypress/plugin';\n *\n * export default defineConfig({\n * e2e: {\n * setupNodeEvents(on, config) {\n * return qualflareCypress(on, config, { environment: 'staging' });\n * },\n * },\n * });\n * ```\n */\nexport function qualflareCypress(\n on: Cypress.PluginEvents,\n config: Cypress.PluginConfigOptions,\n options: QualflareCypressOptions = {},\n): Cypress.PluginConfigOptions {\n const resolved = resolveConfig(options);\n const buffer = new CaseBuffer();\n const pendingAttachments = new PendingAttachmentQueue();\n const testPhaseGate = new TestPhaseGate();\n const attachmentBudget = new AttachmentBudget(resolved.maxTotalAttachmentBytes);\n // resolved already has every field AttachmentReaderConfig needs.\n const attachmentConfig: AttachmentReaderConfig = resolved;\n\n // Task handlers are always registered — even when disabled — so a\n // cy.task() call from the browser side never errors with \"no handler\n // registered for task\" just because the plugin is turned off.\n registerTasks(on, buffer, attachmentConfig, attachmentBudget, pendingAttachments, testPhaseGate);\n\n if (resolved.enabled) {\n registerEvents(on, resolved, buffer, pendingAttachments, testPhaseGate);\n }\n\n return config;\n}\n","import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { logger } from '../shared/logger.js';\nimport type { Attachment } from '../shared/types.js';\nimport { copyVideoAttachment } from './video-writer.js';\n\n/** Extensions/mime-prefixes routed through the video-copy flow instead of\n * the inline-base64 path below. Broader than the server's own MIME\n * allowlist (`.avi`/`.mkv` included) so this still correctly IDENTIFIES a\n * video attachment even in a format the server can't accept —\n * `copyVideoAttachment` is what actually enforces the narrower allowlist and\n * warns/skips a format outside it. Nothing in this codebase currently\n * produces `.avi`/`.mkv` (`events.ts`'s `after:screenshot` handler always\n * hardcodes `image/png`, and Cypress itself only ever records `.mp4`), but a\n * `qualflare.attachmentFromFile()` call can point at any local file. */\nconst VIDEO_EXTENSIONS = new Set(['.mp4', '.webm', '.mov', '.avi', '.mkv']);\n\nexport interface AttachmentReaderConfig {\n attachScreenshots: boolean;\n maxAttachmentBytes: number;\n maxTotalAttachmentBytes: number;\n maxVideoBytes: number;\n outputDir: string;\n}\n\n/**\n * Tracks cumulative attached bytes across the whole `cypress run` process\n * (one instance per `qualflareCypress()` call, threaded through every\n * `TASK_REPORT_CASE` resolution), so the final POST doesn't silently exceed\n * the request body limit. `maxTotalAttachmentBytes` defaults well under the\n * documented 10MB specifically because production currently has a known,\n * confirmed effective ~1MB body-limit bug (see the plan's CRIT-01\n * reference) — don't raise the default until that's confirmed fixed\n * server-side.\n */\nexport class AttachmentBudget {\n private used = 0;\n\n constructor(private readonly maxTotalBytes: number) {}\n\n /** Atomically checks-and-reserves `bytes` against the remaining budget.\n * Returns false (reserving nothing) if it would exceed the total. */\n tryReserve(bytes: number): boolean {\n if (this.used + bytes > this.maxTotalBytes) {\n return false;\n }\n this.used += bytes;\n return true;\n }\n\n get usedBytes(): number {\n return this.used;\n }\n}\n\ntype ReadResult = { skipped: false; content: string } | { skipped: true; reason: string };\n\nfunction isVideoLike(attachment: Attachment): boolean {\n if (attachment.mimeType?.toLowerCase().startsWith('video/')) {\n return true;\n }\n if (attachment.path && VIDEO_EXTENSIONS.has(path.extname(attachment.path).toLowerCase())) {\n return true;\n }\n return false;\n}\n\nfunction readAttachmentFile(filePath: string, maxAttachmentBytes: number, budget: AttachmentBudget): ReadResult {\n let size: number;\n try {\n // Stat BEFORE reading — an oversized file must never be loaded into\n // memory just to discover it should be skipped.\n size = fs.statSync(filePath).size;\n } catch (err) {\n return { skipped: true, reason: `could not stat file: ${(err as Error).message}` };\n }\n if (size > maxAttachmentBytes) {\n return {\n skipped: true,\n reason: `${size} bytes exceeds the configured per-attachment cap of ${maxAttachmentBytes} bytes`,\n };\n }\n if (!budget.tryReserve(size)) {\n return {\n skipped: true,\n reason: `would exceed this run's total attachment budget (${budget.usedBytes} bytes already used)`,\n };\n }\n try {\n const content = fs.readFileSync(filePath).toString('base64');\n return { skipped: false, content };\n } catch (err) {\n return { skipped: true, reason: `could not read file: ${(err as Error).message}` };\n }\n}\n\n/**\n * Resolves a Case's attachment references into either inline base64\n * `content` (small files) or a `localVideoPath` pointing at a copy made\n * alongside the report output (video — see `video-writer.ts`'s\n * `copyVideoAttachment`), or drops them. Attachment references arrive with\n * only a `path` (never bytes — screenshots are captured entirely Node-side\n * via the `after:screenshot` plugin event in `events.ts`, and an author's\n * `qualflare.attachmentFromFile()` call carries only the path it was given\n * too), so all file I/O and size-guarding happens here, at the point a\n * finished Case is received from `cy.task(TASK_REPORT_CASE, ...)` — see\n * `tasks.ts`.\n *\n * Per the plan's resolved decision, an oversized or over-budget INLINE\n * attachment is skipped ENTIRELY (not degraded to a contentless path-only\n * entry): the server's `path` field is explicitly informational/never-fetched,\n * so a contentless entry has little value and this keeps the behavior\n * simple and predictable. A video attachment that fails to copy (oversized\n * per `maxVideoBytes`, unsupported format, or an unreadable source file) is\n * skipped the same way — `copyVideoAttachment` already logs why.\n */\nexport function resolveAttachments(\n attachments: Attachment[] | undefined,\n config: AttachmentReaderConfig,\n budget: AttachmentBudget,\n): Attachment[] | undefined {\n if (!attachments || attachments.length === 0) {\n return undefined;\n }\n if (!config.attachScreenshots) {\n return undefined;\n }\n\n const resolved: Attachment[] = [];\n for (const attachment of attachments) {\n if (isVideoLike(attachment)) {\n if (!attachment.path) {\n logger.warn(`skipping video attachment \"${attachment.name}\": no local file path to copy.`);\n continue;\n }\n const copied = copyVideoAttachment(attachment.path, config.outputDir, config.maxVideoBytes);\n if (!copied) {\n // copyVideoAttachment already logged the specific reason.\n continue;\n }\n resolved.push({\n ...attachment,\n mimeType: copied.mimeType,\n localVideoPath: copied.localVideoPath,\n fileSize: copied.fileSize,\n });\n continue;\n }\n if (attachment.content !== undefined || !attachment.path) {\n // Already has inline content (e.g. from a future metadata-API call\n // that provides content directly), or nothing to read — pass through\n // unchanged.\n resolved.push(attachment);\n continue;\n }\n const result = readAttachmentFile(attachment.path, config.maxAttachmentBytes, budget);\n if (result.skipped) {\n logger.warn(`skipping attachment \"${attachment.name}\" (${attachment.path}): ${result.reason}`);\n continue;\n }\n resolved.push({ ...attachment, content: result.content });\n }\n return resolved.length > 0 ? resolved : undefined;\n}\n","/**\n * A minimal logger writing to stderr (Node) / console (browser). Node-side\n * output deliberately avoids stdout, since that's typically Cypress's own\n * test-output stream and shouldn't be polluted with plugin diagnostics.\n *\n * Safe to import from both browser-side and Node-side code (isomorphic) —\n * `console.*` exists in both environments; only the underlying stream\n * differs, which is not something this module needs to control explicitly\n * since `console.error`/`console.warn` already default to stderr in Node.\n */\n\nconst PREFIX = '[qualflare-cypress]';\n\nexport const logger = {\n debug(...args: unknown[]): void {\n console.debug(PREFIX, ...args);\n },\n info(...args: unknown[]): void {\n console.log(PREFIX, ...args);\n },\n warn(...args: unknown[]): void {\n console.warn(PREFIX, ...args);\n },\n error(...args: unknown[]): void {\n console.error(PREFIX, ...args);\n },\n};\n","import * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { randomUUID } from 'node:crypto';\n\nimport { logger } from '../shared/logger.js';\n\n/** Extension -> MIME type for the video formats the server accepts (see\n * `launch.AllowedAttachmentUploadMimeTypes` server-side). Cypress itself\n * always records `.mp4` today, but `.webm`/`.mov` are listed for parity with\n * the server's own allowlist and in case that ever changes. An extension not\n * in this map (a user could point `qualflare.attachmentFromFile()` at an\n * arbitrary file) is skipped — see `copyVideoAttachment`'s doc comment. */\nconst VIDEO_MIME_TYPES_BY_EXTENSION: Record<string, string> = {\n '.mp4': 'video/mp4',\n '.webm': 'video/webm',\n '.mov': 'video/quicktime',\n};\n\nexport interface VideoCopyResult {\n /** Filename relative to the `outputDir` this was copied into — never an\n * absolute path, since the whole directory travels together as one CI\n * artifact bundle (see the design spec's \"Why no backend changes\"\n * section). */\n localVideoPath: string;\n fileSize: number;\n mimeType: string;\n}\n\n/**\n * Copies one video file into `outputDir` under a unique filename (Allure's\n * `FileSystemWriter.writeAttachmentFromPath` pattern: `fs.copyFileSync`,\n * never read into memory) and returns enough to build that `Attachment`\n * entry's `localVideoPath`. `qualflare-cli` is what actually uploads this\n * file later, once it has a real auth token — see the design spec.\n *\n * Best-effort, like the rest of this reporter's attachment handling\n * (`attachment-reader.ts`'s oversized/unreadable-file skip): any failure —\n * oversized file, unsupported extension, an unreadable source file — is\n * logged as a warning and resolves to `undefined` rather than throwing, so a\n * video problem never fails the whole run.\n */\nexport function copyVideoAttachment(\n filePath: string,\n outputDir: string,\n maxVideoBytes: number,\n): VideoCopyResult | undefined {\n const ext = path.extname(filePath).toLowerCase();\n const mimeType = VIDEO_MIME_TYPES_BY_EXTENSION[ext];\n if (!mimeType) {\n logger.warn(`skipping video attachment \"${filePath}\": unsupported video format.`);\n return undefined;\n }\n\n let fileSize: number;\n try {\n // Stat BEFORE copying — an oversized file must never be copied just to\n // discover it should be skipped.\n fileSize = fs.statSync(filePath).size;\n } catch (err) {\n logger.warn(`skipping video attachment \"${filePath}\": could not stat file: ${(err as Error).message}`);\n return undefined;\n }\n if (fileSize > maxVideoBytes) {\n logger.warn(\n `skipping video attachment \"${filePath}\": ${fileSize} bytes exceeds the configured ` +\n `maxVideoBytes cap of ${maxVideoBytes} bytes.`,\n );\n return undefined;\n }\n\n const localVideoPath = `${randomUUID()}${ext}`;\n try {\n fs.mkdirSync(outputDir, { recursive: true });\n fs.copyFileSync(filePath, path.join(outputDir, localVideoPath));\n } catch (err) {\n logger.warn(`skipping video attachment \"${filePath}\": could not copy file: ${(err as Error).message}`);\n return undefined;\n }\n\n return { localVideoPath, fileSize, mimeType };\n}\n","import * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { randomUUID } from 'node:crypto';\n\nimport { MAX_ATTACHMENTS_PER_CASE } from '../shared/constants.js';\nimport { logger } from '../shared/logger.js';\nimport { msToNs } from '../shared/duration.js';\nimport type { Case, CaseStatus, Suite } from '../shared/types.js';\nimport { buildCollectPayload, type BrowserInfo } from './collect-builder.js';\nimport type { ResolvedPluginConfig } from './resolve-config.js';\nimport { LaunchAccumulator, PendingAttachmentQueue, TestPhaseGate } from './state.js';\nimport type { CaseBuffer } from './tasks.js';\nimport { copyVideoAttachment } from './video-writer.js';\n\n/** Case statuses a video recording is worth attaching to — mirrors the\n * \"this test needs investigating\" set, not just literally 'failed'. */\nconst FAILURE_STATUSES: ReadonlySet<CaseStatus> = new Set(['failed', 'error', 'timeout']);\n\n/**\n * Registers `before:run` / `before:spec` / `after:spec` / `after:run` /\n * `after:screenshot` on the given Cypress plugin events, wiring the\n * spec-by-spec case buffer into one accumulated `Launch` and writing it as a\n * single Collect JSON file into `config.outputDir` exactly once at\n * `after:run` — this process never uploads anything itself; see\n * `resolve-config.ts`'s `outputDir` doc comment.\n */\nexport function registerEvents(\n on: Cypress.PluginEvents,\n config: ResolvedPluginConfig,\n buffer: CaseBuffer,\n pendingAttachments: PendingAttachmentQueue,\n testPhaseGate: TestPhaseGate,\n): void {\n const accumulator = new LaunchAccumulator();\n let browserInfo: BrowserInfo | undefined;\n let currentSpecStart = 0;\n\n on('before:run', (details) => {\n browserInfo = {\n browserName: details.browser?.displayName,\n browserVersion: details.browser?.version,\n osName: details.system?.osName,\n osVersion: details.system?.osVersion,\n };\n });\n\n // Node-side event — fires for BOTH manual cy.screenshot() calls and\n // Cypress's own automatic on-failure screenshot (screenshotOnRunFailure,\n // on by default in `cypress run`), distinguished only by `details.testFailure`.\n // No cy.task()/browser-side involvement needed: this event already runs\n // in the Node process with the file already written to disk. Queued here\n // and drained by `tasks.ts`'s TASK_REPORT_CASE handler, which attaches\n // whatever's pending to the Case currently being reported.\n //\n // EXCEPT when it fires before the first test has even started (see\n // TestPhaseGate in state.ts) — a screenshot taken in a root `before()`\n // hook. That can never be correctly attributed to any specific test (the\n // first test hasn't begun yet), so it's treated as orphaned immediately,\n // the same way a screenshot taken in an `after()` hook already is (below,\n // in `after:spec`) — rather than silently getting swept into whichever\n // test's TASK_REPORT_CASE happens to arrive first.\n on('after:screenshot', (details) => {\n if (!testPhaseGate.hasStarted()) {\n logger.warn(\n `a screenshot (\"${details.name || 'unnamed'}\") was captured before any test in this spec had ` +\n 'started (likely in a root `before()` hook) and cannot be attributed to a specific test — it was not included in the report.',\n );\n return;\n }\n pendingAttachments.enqueue({\n name: details.name || (details.testFailure ? 'failure-screenshot' : 'screenshot'),\n path: details.path,\n mimeType: 'image/png',\n });\n });\n\n on('before:spec', () => {\n currentSpecStart = Date.now();\n testPhaseGate.reset();\n // Ensure no stale cases from a prior spec leak into this one, in case\n // something upstream ever calls before:spec without a matching\n // after:spec having fired first.\n buffer.drain();\n });\n\n on('after:spec', async (spec, results) => {\n const cases = buffer.drain();\n\n // Cypress records one video per SPEC, not per test, so there is no\n // exact owning Case — attaching it to the first failing case in the\n // spec is the most useful available attribution (that's the recording a\n // QA engineer actually wants to watch) and, being a single Case row,\n // avoids double-counting the same copied file's bytes toward workspace\n // storage quota the way attaching it to every failing case would.\n // Skipped entirely for an all-passing spec: a video with nothing to\n // investigate has little diagnostic value and isn't worth copying.\n if (results.video) {\n const failedCase = cases.find((c) => FAILURE_STATUSES.has(c.status));\n if (failedCase) {\n const copied = copyVideoAttachment(results.video, config.outputDir, config.maxVideoBytes);\n if (copied) {\n attachVideo(failedCase, copied);\n }\n } else {\n logger.info(`spec ${spec.relative} recorded a video but no test failed — not attached.`);\n }\n }\n\n const suite: Suite = {\n name: spec.relative,\n category: 'cypress',\n duration: msToNs(results.stats.duration ?? Date.now() - currentSpecStart),\n timestamp: new Date(results.stats.startedAt ?? Date.now()).toISOString(),\n cases,\n };\n\n if (cases.length !== results.stats.tests) {\n logger.warn(\n `spec ${spec.relative}: captured ${cases.length} case(s) but Cypress reported ` +\n `${results.stats.tests} test(s) — some results may be missing from the report.`,\n );\n }\n\n accumulator.addSuite(suite);\n\n // Any attachment still sitting in the queue at this point was never\n // claimed by a Case's TASK_REPORT_CASE (e.g. a screenshot taken in an\n // `after`-hook, after the last test's report already went out) — drop\n // it with a warning rather than silently attributing it to a case in\n // the NEXT spec file.\n const orphaned = pendingAttachments.drain();\n if (orphaned.length > 0) {\n logger.warn(\n `spec ${spec.relative}: ${orphaned.length} screenshot(s) could not be attributed to a ` +\n 'specific test (likely taken outside a test body, e.g. in an `after` hook) and were not included in the report.',\n );\n }\n });\n\n on('after:run', async () => {\n const suites = accumulator.getSuites();\n if (suites.length === 0) {\n logger.info('no test results were captured this run — skipping file write.');\n return;\n }\n\n const collect = buildCollectPayload(accumulator, config, browserInfo);\n if (config.shardIndex !== undefined) {\n for (const suite of collect.suites) {\n for (const c of suite.cases) {\n c.shardIndex = config.shardIndex;\n }\n }\n }\n\n fs.mkdirSync(config.outputDir, { recursive: true });\n const outputPath = path.join(config.outputDir, `${randomUUID()}.json`);\n fs.writeFileSync(outputPath, JSON.stringify(collect));\n logger.info(`wrote Collect payload to ${outputPath} — run \\`qualflare-cli collect ${config.outputDir}\\` to upload it.`);\n });\n}\n\n/** Appends a video's `Attachment` entry to a Case, respecting the server's\n * per-case attachment cap — a spec-level video competing with the case's\n * own screenshots for that budget is an edge case (one video vs. up to 50\n * screenshots), but silently exceeding the cap would 400 the whole launch. */\nfunction attachVideo(testCase: Case, copied: { localVideoPath: string; fileSize: number; mimeType: string }): void {\n const attachments = testCase.attachments ?? [];\n if (attachments.length >= MAX_ATTACHMENTS_PER_CASE) {\n logger.warn(\n `not attaching video to \"${testCase.name}\": already at the server's ${MAX_ATTACHMENTS_PER_CASE}-attachment-per-case cap.`,\n );\n return;\n }\n testCase.attachments = [\n ...attachments,\n {\n name: 'video',\n mimeType: copied.mimeType,\n localVideoPath: copied.localVideoPath,\n fileSize: copied.fileSize,\n },\n ];\n}\n","/**\n * Shared constants that both the browser-side support script and the\n * Node-side plugin must agree on exactly (task names in particular — a\n * typo on either side silently breaks `cy.task()` at runtime with no\n * compile-time signal, since Cypress tasks are looked up by string).\n */\n\n/** `cy.task()` name the browser side uses to hand a finished test's Case\n * object over to the Node side. */\nexport const TASK_REPORT_CASE = 'qualflareReportCase';\n\n/** `cy.task()` name a one-shot root-level `beforeEach` (registered by\n * `src/browser/index.ts`) uses to tell the Node side \"the first test of this\n * spec has started (all applicable `before()` hooks have already run)\" — see\n * `src/plugin/state.ts`'s `TestPhaseGate` for why this exists: it lets\n * `events.ts` distinguish a screenshot taken in a `before()` hook (which\n * should be treated as orphaned, like an `after()`-hook screenshot already\n * is) from one taken during a real test's own execution. */\nexport const TASK_MARK_TEST_PHASE_STARTED = 'qualflareMarkTestPhaseStarted';\n\n/** Server-side caps this client should respect defensively (see\n * `api-service/internal/core/domain/launch/launch.go`). */\nexport const MAX_SUITES_PER_LAUNCH = 2000;\nexport const MAX_CASES_PER_SUITE = 5000;\nexport const MAX_STEPS_PER_CASE = 1000;\nexport const MAX_PARAMETERS_PER_STEP = 50;\nexport const MAX_ATTACHMENTS_PER_CASE = 50;\nexport const MAX_LABELS_PER_CASE = 100;\nexport const MAX_LINKS_PER_CASE = 20;\nexport const MAX_TAGS_PER_CASE = 64;\nexport const MAX_TAG_LENGTH = 255;\n\n/** Mirrors `launch.MaxCaseAttempts`. Beyond this the server keeps the first\n * 49 attempts plus the final one and drops the middle, so sending more is\n * wasted payload rather than an error. */\nexport const MAX_ATTEMPTS_PER_CASE = 50;\n\n/** Mirrors the server's per-attempt text bounds (`launch.MaxAttempt*Runes`).\n *\n * Clamped CLIENT-side, not left to the server, because attempts are the only\n * repeated-per-case payload with no size budget of its own. Measured: one\n * retried test with a deep stack and a chatty log serializes to ~630KB\n * unclamped — most of it text the server discards on write — against a 10MB\n * request body limit that, once exceeded, loses the ENTIRE launch. Sending\n * bytes the server will throw away is pure risk. */\nexport const MAX_ATTEMPT_MESSAGE_RUNES = 8192;\nexport const MAX_ATTEMPT_TRACE_RUNES = 32768;\nexport const MAX_ATTEMPT_SNIPPET_RUNES = 4096;\nexport const MAX_ATTEMPT_OUTPUT_RUNES = 16384;\nexport const MAX_ATTEMPT_OUTPUT_LINES = 200;\n\n/** Mirrors `launch.MaxAttachmentUploadFileSize` — the server's hard cap on a\n * single `POST /api/v1/attachments/upload-url` request (video). */\nexport const MAX_VIDEO_UPLOAD_BYTES = 50 * 1024 * 1024;\n\n/** Client-side SOFT cap on steps recorded per test attempt — well under the\n * server's 1000-per-case hard cap (`MAX_STEPS_PER_CASE`). There's no reason\n * to build/serialize thousands of command-log entries for one test; once hit,\n * further entries within that attempt are dropped (with a one-time warning),\n * not queued and truncated later. */\nexport const MAX_STEPS_PER_TEST_ATTEMPT = 300;\n","import type { NanosecondDuration } from './types.js';\n\nconst NS_PER_MS = 1_000_000;\n\n/**\n * Converts a Cypress/Mocha millisecond duration into the wire format's\n * raw-nanosecond integer (see `NanosecondDuration` in ./types.ts).\n *\n * Rounds (not truncates) so fractional-ms input doesn't lose precision by\n * always rounding toward zero. Negative input is clamped to 0 — a negative\n * duration is never legitimate and silently clamping is safer for an\n * ingest payload than throwing and aborting an otherwise-good report.\n */\nexport function msToNs(ms: number): NanosecondDuration {\n if (!Number.isFinite(ms) || ms <= 0) {\n return 0;\n }\n return Math.round(ms * NS_PER_MS);\n}\n","import * as os from 'node:os';\n\nimport type { ResolvedPluginConfig } from './resolve-config.js';\nimport type { LaunchAccumulator } from './state.js';\nimport type { Collect } from '../shared/types.js';\nimport { PACKAGE_VERSION } from './version.js';\n\n/** Browser/system info captured at Cypress's `before:run` event — richer\n * and more accurate than any Node-side guess when available. */\nexport interface BrowserInfo {\n browserName?: string;\n browserVersion?: string;\n osName?: string;\n osVersion?: string;\n}\n\nfunction resolveOs(config: ResolvedPluginConfig, info: BrowserInfo | undefined): string {\n if (config.os) {\n return config.os;\n }\n if (info?.osName) {\n return info.osVersion ? `${info.osName} ${info.osVersion}` : info.osName;\n }\n return `${os.type()} ${os.release()}`;\n}\n\nfunction resolveBrowser(config: ResolvedPluginConfig, info: BrowserInfo | undefined): string {\n if (config.browser) {\n return config.browser;\n }\n if (info?.browserName) {\n return info.browserVersion ? `${info.browserName} ${info.browserVersion}` : info.browserName;\n }\n return '';\n}\n\n/**\n * Assembles the final `Collect` payload from everything accumulated over\n * the `cypress run` process, at `after:run`. CI metadata\n * (`ciProvider`/`ciBuildNumber`/`ciRunUrl`/`ciPrNumber`) and branch/commit\n * auto-detection are already fully resolved by `resolve-config.ts` (the\n * single source of truth for this launch's metadata, consistent with how\n * `branch`/`commit` are handled) — this function just reads the resolved\n * config through, it does not call `ci-detect.ts`/`git-detect.ts` itself.\n */\nexport function buildCollectPayload(\n accumulator: LaunchAccumulator,\n config: ResolvedPluginConfig,\n browserInfo: BrowserInfo | undefined,\n): Collect {\n return {\n framework: config.framework,\n platform: config.platform,\n os: resolveOs(config, browserInfo),\n browser: resolveBrowser(config, browserInfo),\n branch: config.branch,\n commit: config.commit,\n environment: config.environment,\n language: config.language,\n milestone: config.milestone,\n metadata: {\n version: PACKAGE_VERSION,\n timestamp: new Date().toISOString(),\n cliName: 'qualflare-cypress',\n runId: config.runId,\n },\n properties: config.properties,\n suites: accumulator.getSuites(),\n ciProvider: config.ciProvider,\n ciBuildNumber: config.ciBuildNumber,\n ciRunUrl: config.ciRunUrl,\n ciPrNumber: config.ciPrNumber,\n };\n}\n","// `__PACKAGE_VERSION__` is injected at build time by tsup's `define` option\n// (see tsup.config.ts) from package.json's `version` field. Deliberately\n// NOT read at runtime via `import.meta.url` + `createRequire` — that breaks\n// the CJS build output (`import.meta` is empty/unavailable once esbuild\n// compiles to CommonJS), which is exactly the class of dual-CJS/ESM-package\n// bug a build-time constant sidesteps entirely. Under Vitest (which never\n// goes through tsup), `vitest.config.ts` defines the same constant so this\n// module behaves identically in tests and in the built package.\ndeclare const __PACKAGE_VERSION__: string;\n\nexport const PACKAGE_VERSION: string = __PACKAGE_VERSION__;\n","import { MAX_SUITES_PER_LAUNCH } from '../shared/constants.js';\nimport { logger } from '../shared/logger.js';\nimport type { Attachment, Suite } from '../shared/types.js';\n\n/**\n * Accumulates `Suite[]` across every spec file in the current `cypress run`\n * process. Process-lifetime, single instance — matches the plan's\n * \"one `cypress run` process = one Launch\" decision: there is no\n * cross-process aggregation in v1 (see docs/LIMITATIONS.md).\n */\nexport class LaunchAccumulator {\n private readonly suites: Suite[] = [];\n private truncated = false;\n\n addSuite(suite: Suite): void {\n if (this.suites.length >= MAX_SUITES_PER_LAUNCH) {\n if (!this.truncated) {\n this.truncated = true;\n logger.warn(\n `reached the server's ${MAX_SUITES_PER_LAUNCH}-suite-per-launch cap — further spec files' ` +\n 'results will not be uploaded this run.',\n );\n }\n return;\n }\n this.suites.push(suite);\n }\n\n getSuites(): Suite[] {\n return this.suites;\n }\n}\n\n/**\n * Tracks whether the first test of the current spec has started (i.e. every\n * applicable `before()` hook for it has already run) — set via a one-shot\n * `cy.task(TASK_MARK_TEST_PHASE_STARTED, ...)` call from a root-level\n * `beforeEach` the browser side registers (`src/browser/index.ts`), reset at\n * `before:spec`.\n *\n * Exists to fix a real misattribution bug: a screenshot taken in a root\n * `before()` hook fires `after:screenshot` before ANY test has started, and\n * without this signal `events.ts` cannot tell that apart from a screenshot\n * taken during the first real test's own execution — both would otherwise\n * sit in `PendingAttachmentQueue` and get drained together into whichever\n * test's `TASK_REPORT_CASE` happens to arrive first. `before()`-hook\n * screenshots should instead be treated as orphaned, exactly like an\n * `after()`-hook screenshot already correctly is (see `events.ts`'s\n * `after:spec` handler).\n *\n * A `beforeEach` (not a raw `runner.on('test', ...)` Mocha listener) is\n * deliberately the signal source: it's a real Cypress command executed as\n * part of that test's own command-queue processing, the same\n * queue-integrated mechanism `queue.ts`'s already-proven-safe `flushCase`\n * (called from a real `afterEach`) uses — unlike calling `cy.task()`\n * directly from a raw Mocha runner event listener, which Tier 1 of this same\n * remediation effort found (by actually running a real `cypress run`) can\n * hang the whole process for a test whose body never executes.\n */\nexport class TestPhaseGate {\n private started = false;\n\n markStarted(): void {\n this.started = true;\n }\n\n hasStarted(): boolean {\n return this.started;\n }\n\n /** Called at `before:spec`, so each spec file gets its own fresh\n * before()-hook-vs-real-test boundary. */\n reset(): void {\n this.started = false;\n }\n}\n\n/**\n * Buffers screenshot references captured via the Node-side `after:screenshot`\n * plugin event (see `events.ts`) between one finished test's report and the\n * next. `after:screenshot` fires in real time as Cypress captures each\n * screenshot (both manual `cy.screenshot()` calls and its own automatic\n * on-failure capture funnel through this single event) — since a\n * screenshot taken during a test's execution always fires before that\n * test's `TASK_REPORT_CASE` arrives (the test's own command queue, and\n * therefore any screenshot capture within it, completes before its\n * `afterEach` runs), draining this queue at the moment a Case is received\n * correctly attributes any screenshots taken during that test's run to it.\n *\n * This whole flow is Node-side only — no `cy.task()` round-trip or\n * browser-side wiring is needed for screenshots, unlike Case data itself.\n */\nexport class PendingAttachmentQueue {\n private pending: Attachment[] = [];\n\n enqueue(attachment: Attachment): void {\n this.pending.push(attachment);\n }\n\n /** Returns the buffered attachments and clears the queue. */\n drain(): Attachment[] {\n const drained = this.pending;\n this.pending = [];\n return drained;\n }\n}\n","import { randomUUID } from 'node:crypto';\n\nimport { MAX_VIDEO_UPLOAD_BYTES } from '../shared/constants.js';\nimport type { Platform } from '../shared/types.js';\nimport { detectCi, type CiMetadata } from './ci-detect.js';\nimport { detectGit, type GitInfo } from './git-detect.js';\n\n/** Options passed to `qualflareCypress(on, config, options)` in the user's\n * `cypress.config.ts`. Every field here also has an environment-variable\n * override — see the precedence table in the README / plan. */\nexport interface QualflareCypressOptions {\n environment?: string;\n language?: string;\n milestone?: number | null;\n branch?: string | null;\n commit?: string | null;\n platform?: Platform;\n framework?: string;\n os?: string;\n browser?: string;\n properties?: Record<string, string>;\n /** Max 64 chars. Free text, no enum — an unrecognized CI provider must\n * never be rejected. Auto-detected via `ci-detect.ts` when omitted. */\n ciProvider?: string;\n ciBuildNumber?: string;\n ciRunUrl?: string;\n ciPrNumber?: number;\n /** Identifier shared by every shard of one run, written into the report as\n * `metadata.runId`. `qualflare-cli collect` groups files by it and refuses\n * to merge a stale report from an earlier run into this launch.\n *\n * Auto-detected from CI. Outside CI it falls back to a per-process UUID,\n * which is correct there: every local run is a distinct run, so a leftover\n * file is still caught. */\n runId?: string;\n attachScreenshots?: boolean;\n maxAttachmentBytes?: number;\n maxTotalAttachmentBytes?: number;\n /** Per-video byte cap, checked before upload (via `fs.statSync`, never by\n * reading the file first). Default 50MB, matching the server's own hard\n * cap — raising this past 50MB only wastes an upload attempt the server\n * will reject. */\n maxVideoBytes?: number;\n /** `false` fully disables accumulation/POST (a complete no-op) but still\n * registers no-op `on('task', ...)` handlers so `cy.task()` calls from the\n * browser side never error with \"no handler registered for task.\" */\n enabled?: boolean;\n /** Directory `after:run` writes this process's report file (and any video\n * attachments) into. Default `./qualflare-results`. Always active — this\n * reporter never uploads anything itself; `qualflare-cli` reads whatever\n * ends up in this directory. Every JSON file this process writes is\n * uniquely named, so multiple shards can safely share one `outputDir`\n * without colliding — see docs/LIMITATIONS.md. */\n outputDir?: string;\n /** This process's 0-based position among parallel shards of the same CI\n * run, stamped onto every case it reports. Resolved only from this option\n * or the `QUALFLARE_SHARD_INDEX` env var — no shard concept is\n * auto-detected beyond that (set it yourself from your CI's own matrix\n * index; see docs/CONFIGURATION.md). A normal single-process run needs no\n * shard concept at all and can leave this unset. */\n shardIndex?: number;\n}\n\nexport interface ResolvedPluginConfig {\n environment: string;\n language: string;\n milestone: number | null;\n branch: string | null;\n commit: string | null;\n platform: Platform;\n framework: string;\n os?: string;\n browser?: string;\n properties?: Record<string, string>;\n ciProvider?: string;\n ciBuildNumber?: string;\n ciRunUrl?: string;\n ciPrNumber?: number;\n runId: string;\n attachScreenshots: boolean;\n maxAttachmentBytes: number;\n maxTotalAttachmentBytes: number;\n maxVideoBytes: number;\n enabled: boolean;\n outputDir: string;\n shardIndex?: number;\n}\n\nfunction firstEnv(...names: string[]): string | undefined {\n for (const name of names) {\n const value = process.env[name];\n if (value !== undefined && value !== '') {\n return value;\n }\n }\n return undefined;\n}\n\nfunction envBool(...names: string[]): boolean | undefined {\n const raw = firstEnv(...names);\n if (raw === undefined) {\n return undefined;\n }\n return raw === 'true' || raw === '1';\n}\n\nfunction envInt(...names: string[]): number | undefined {\n const raw = firstEnv(...names);\n if (raw === undefined) {\n return undefined;\n }\n const parsed = Number.parseInt(raw, 10);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\n/** Resolves the full plugin configuration from, in order: the explicit\n * `options` passed to `qualflareCypress()`, then `QUALFLARE_*` environment\n * variables, then `QF_*` (compat alias with the existing Go CLI, where an\n * equivalent exists), then a hardcoded default.\n *\n * Branch/commit precedence: `options.branch`/`.commit` (including an\n * explicit `null`, which is respected as \"no auto-detection wanted\" rather\n * than triggering the fallback tiers below it) > `QUALFLARE_BRANCH`/\n * `QF_BRANCH` env (and the commit equivalent) > CI-provider env vars > a\n * local `git` subprocess (`git-detect.ts`) > `null`. The subprocess tier is\n * skipped entirely — no `git` process is forked — once both branch and\n * commit are already resolved from options/env, mirroring\n * `qualflare-cli/internal/config/config.go`'s `DetectGit`'s early return.\n *\n * CI-metadata precedence (`ciProvider`/`ciBuildNumber`/`ciRunUrl`/\n * `ciPrNumber`): the corresponding `options.ci*` field, else `ci-detect.ts`'s\n * auto-detection (per-provider extraction table, falling back to the\n * `ci-info` package's ~70-provider free-text name).\n *\n * `deps` lets tests inject fake `detectGit`/`detectCi` implementations\n * instead of the real ones (which shell out to `git` and read the real\n * `process.env`/`ci-info` module state) — defaults to the real detectors,\n * so every production call site (`plugin/index.ts` calls `resolveConfig(options)`\n * with no second argument) is unaffected.\n */\nexport function resolveConfig(\n options: QualflareCypressOptions,\n deps: { detectGit?: () => GitInfo; detectCi?: () => CiMetadata } = {},\n): ResolvedPluginConfig {\n const doDetectGit = deps.detectGit ?? detectGit;\n const doDetectCi = deps.detectCi ?? detectCi;\n\n const enabled = options.enabled ?? envBool('QUALFLARE_ENABLED') ?? true;\n const outputDir = options.outputDir || firstEnv('QUALFLARE_OUTPUT_DIR') || './qualflare-results';\n const shardIndex = options.shardIndex ?? envInt('QUALFLARE_SHARD_INDEX');\n\n const milestoneRaw = options.milestone !== undefined ? options.milestone : envInt('QUALFLARE_MILESTONE', 'QF_MILESTONE');\n const milestone = milestoneRaw !== undefined && milestoneRaw !== null && milestoneRaw >= 1 ? milestoneRaw : null;\n\n const envBranch = firstEnv('QUALFLARE_BRANCH', 'QF_BRANCH');\n const envCommit = firstEnv('QUALFLARE_COMMIT', 'QF_COMMIT');\n const needsGitDetection =\n (options.branch === undefined && envBranch === undefined) ||\n (options.commit === undefined && envCommit === undefined);\n const detectedGit = needsGitDetection ? doDetectGit() : {};\n\n const branch = options.branch !== undefined ? options.branch : (envBranch ?? detectedGit.branch ?? null);\n const commit = options.commit !== undefined ? options.commit : (envCommit ?? detectedGit.commit ?? null);\n\n const detectedCi = doDetectCi();\n const ciProvider = options.ciProvider ?? detectedCi.ciProvider;\n const ciBuildNumber = options.ciBuildNumber ?? detectedCi.ciBuildNumber;\n const ciRunUrl = options.ciRunUrl ?? detectedCi.ciRunUrl;\n const ciPrNumber = options.ciPrNumber ?? detectedCi.ciPrNumber;\n\n // Never empty on purpose: `qf collect` treats a report with no runId as\n // \"unknown run\" and never lets it block a merge, so defaulting to '' would\n // quietly opt local runs out of the very check this exists for.\n const runId = options.runId ?? firstEnv('QUALFLARE_RUN_ID') ?? detectedCi.ciRunId ?? randomUUID();\n\n return {\n // `||` (truthy check), not `??`, for these three REQUIRED-non-empty wire\n // fields — matching `collect-builder.ts`'s `resolveOs`/`resolveBrowser`,\n // which already correctly treat an explicit `''` option as \"not set.\"\n // `??` only falls back on `null`/`undefined`, so `environment: ''` would\n // previously win outright over the `'development'` default, silently\n // 400ing the whole launch (the server rejects an empty `environment`)\n // and — since this process no longer attempts uploads — the error would\n // be deferred until qualflare-cli tries to upload.\n environment: (options.environment || undefined) ?? firstEnv('QUALFLARE_ENVIRONMENT', 'QF_ENVIRONMENT') ?? 'development',\n language: (options.language || undefined) ?? firstEnv('QUALFLARE_LANGUAGE', 'QF_LANGUAGE') ?? 'en-US',\n milestone,\n branch,\n commit,\n platform: options.platform ?? 'web',\n framework: options.framework || 'cypress',\n os: options.os,\n browser: options.browser,\n properties: options.properties,\n ciProvider,\n ciBuildNumber,\n ciRunUrl,\n ciPrNumber,\n runId,\n attachScreenshots: options.attachScreenshots ?? envBool('QUALFLARE_ATTACH_SCREENSHOTS') ?? true,\n maxAttachmentBytes: options.maxAttachmentBytes ?? envInt('QUALFLARE_MAX_ATTACHMENT_BYTES') ?? 1_500_000,\n maxTotalAttachmentBytes:\n options.maxTotalAttachmentBytes ?? envInt('QUALFLARE_MAX_TOTAL_ATTACHMENT_BYTES') ?? 750_000,\n maxVideoBytes: options.maxVideoBytes ?? envInt('QUALFLARE_MAX_VIDEO_BYTES') ?? MAX_VIDEO_UPLOAD_BYTES,\n enabled,\n outputDir,\n shardIndex,\n };\n}\n","import * as ciInfo from 'ci-info';\n\n/** Detected CI pipeline metadata, matching the wire contract's `ciProvider`/\n * `ciBuildNumber`/`ciRunUrl`/`ciPrNumber` fields exactly (`src/shared/types.ts`). */\nexport interface CiMetadata {\n ciProvider?: string;\n ciBuildNumber?: string;\n ciRunUrl?: string;\n ciPrNumber?: number;\n /** Identifier every shard of ONE CI run shares, and which differs between\n * runs. Distinct from `ciBuildNumber`: a build number is the human-facing\n * counter (GitHub's run NUMBER repeats across re-runs of a workflow),\n * whereas this is the unique run id. `qualflare-cli collect` groups report\n * files by it to refuse merging a stale file from an earlier run into the\n * current launch. */\n ciRunId?: string;\n}\n\ninterface ProviderExtractor {\n detect: (env: NodeJS.ProcessEnv) => boolean;\n providerName: string;\n buildNumber?: (env: NodeJS.ProcessEnv) => string | undefined;\n runUrl?: (env: NodeJS.ProcessEnv) => string | undefined;\n runId?: (env: NodeJS.ProcessEnv) => string | undefined;\n prNumber?: (env: NodeJS.ProcessEnv) => number | undefined;\n}\n\nfunction parsePositiveInt(raw: string | undefined): number | undefined {\n if (!raw) {\n return undefined;\n }\n const n = Number.parseInt(raw, 10);\n return Number.isFinite(n) && n >= 1 ? n : undefined;\n}\n\nfunction nonEmpty(raw: string | undefined): string | undefined {\n return raw && raw.length > 0 ? raw : undefined;\n}\n\n/** Explicit per-provider extraction for the fields `ci-info` doesn't\n * standardize (build number / run URL / PR number). Each `detect` reads\n * directly off the passed-in `env` — deliberately NOT delegating to\n * `ci-info`'s own per-vendor booleans (e.g. `ciInfo.GITHUB_ACTIONS`), which\n * are computed once against the real `process.env` at module-import time\n * and can't be re-evaluated against an injected env — see the module-level\n * comment on `detectCi` below. Checked in order; first match wins. */\nconst PROVIDERS: ProviderExtractor[] = [\n {\n detect: (env) => env.GITHUB_ACTIONS === 'true',\n providerName: 'GitHub Actions',\n buildNumber: (env) => nonEmpty(env.GITHUB_RUN_NUMBER),\n runId: (env) => nonEmpty(env.GITHUB_RUN_ID),\n runUrl: (env) =>\n env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY && env.GITHUB_RUN_ID\n ? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}`\n : undefined,\n prNumber: (env) => {\n const match = /^refs\\/pull\\/(\\d+)\\/merge$/.exec(env.GITHUB_REF ?? '');\n return match ? parsePositiveInt(match[1]) : undefined;\n },\n },\n {\n detect: (env) => env.GITLAB_CI === 'true',\n providerName: 'GitLab CI',\n buildNumber: (env) => nonEmpty(env.CI_PIPELINE_IID),\n runId: (env) => nonEmpty(env.CI_PIPELINE_ID),\n runUrl: (env) => nonEmpty(env.CI_PIPELINE_URL),\n prNumber: (env) => parsePositiveInt(env.CI_MERGE_REQUEST_IID),\n },\n {\n detect: (env) => env.CIRCLECI === 'true',\n providerName: 'CircleCI',\n buildNumber: (env) => nonEmpty(env.CIRCLE_BUILD_NUM),\n runId: (env) => nonEmpty(env.CIRCLE_WORKFLOW_ID ?? env.CIRCLE_BUILD_NUM),\n runUrl: (env) => nonEmpty(env.CIRCLE_BUILD_URL),\n prNumber: (env) => parsePositiveInt(env.CIRCLE_PR_NUMBER),\n },\n {\n detect: (env) => env.BUILDKITE === 'true',\n providerName: 'Buildkite',\n buildNumber: (env) => nonEmpty(env.BUILDKITE_BUILD_NUMBER),\n runId: (env) => nonEmpty(env.BUILDKITE_BUILD_ID),\n runUrl: (env) => nonEmpty(env.BUILDKITE_BUILD_URL),\n prNumber: (env) => {\n const raw = env.BUILDKITE_PULL_REQUEST;\n if (!raw || raw === 'false') {\n return undefined;\n }\n return parsePositiveInt(raw);\n },\n },\n {\n // Jenkins has no simple `JENKINS=true`-style flag; JENKINS_URL is always\n // set by the Jenkins agent and is the conventional detection signal.\n detect: (env) => Boolean(env.JENKINS_URL),\n providerName: 'Jenkins',\n buildNumber: (env) => nonEmpty(env.BUILD_NUMBER),\n runId: (env) => nonEmpty(env.BUILD_TAG ?? env.BUILD_NUMBER),\n runUrl: (env) => nonEmpty(env.BUILD_URL),\n // Jenkins has no standardized PR-number env var across its many PR\n // plugins (Multibranch, GitHub Branch Source, etc.) — deliberately\n // omitted rather than guessing at a plugin-specific variable.\n },\n {\n detect: (env) => env.TF_BUILD === 'True' || env.TF_BUILD === 'true',\n providerName: 'Azure Pipelines',\n buildNumber: (env) => nonEmpty(env.BUILD_BUILDID),\n runId: (env) => nonEmpty(env.BUILD_BUILDID),\n runUrl: (env) => {\n const collectionUri = env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;\n const project = env.SYSTEM_TEAMPROJECT;\n const buildId = env.BUILD_BUILDID;\n if (!collectionUri || !project || !buildId) {\n return undefined;\n }\n return `${collectionUri.replace(/\\/+$/, '')}/${encodeURIComponent(project)}/_build/results?buildId=${buildId}`;\n },\n prNumber: (env) => parsePositiveInt(env.SYSTEM_PULLREQUEST_PULLREQUESTNUMBER),\n },\n {\n detect: (env) => Boolean(env.BITBUCKET_BUILD_NUMBER),\n providerName: 'Bitbucket Pipelines',\n buildNumber: (env) => nonEmpty(env.BITBUCKET_BUILD_NUMBER),\n runId: (env) => nonEmpty(env.BITBUCKET_BUILD_NUMBER),\n runUrl: (env) => {\n const origin = env.BITBUCKET_GIT_HTTP_ORIGIN;\n if (!origin) {\n return undefined;\n }\n const resultsId = env.BITBUCKET_PIPELINE_UUID ?? env.BITBUCKET_BUILD_NUMBER;\n return resultsId ? `${origin}/addon/pipelines/home#!/results/${resultsId}` : undefined;\n },\n prNumber: (env) => parsePositiveInt(env.BITBUCKET_PR_ID),\n },\n];\n\n/**\n * Detects CI pipeline metadata for the `Collect.ciProvider`/`ciBuildNumber`/\n * `ciRunUrl`/`ciPrNumber` fields.\n *\n * IMPORTANT, non-obvious limitation: the `env` parameter only governs the\n * `PROVIDERS` table above (this module's own, directly-env-reading logic).\n * The `ci-info` fallback below does NOT honor it — `ci-info`'s package source\n * computes `exports.name`/`exports.isCI` exactly once, against the REAL\n * `process.env`, at module-import time (`const env = process.env` at its top\n * level) — there is no API to re-evaluate it against a different env object.\n * This is a non-issue in production (this function is always called against\n * the real `process.env` there); `ci-detect.test.ts` exercises the `ci-info`\n * fallback path via `vi.stubEnv` + `vi.resetModules()` (forcing a fresh\n * `ci-info` evaluation) rather than via this function's `env` parameter.\n */\nexport function detectCi(env: NodeJS.ProcessEnv = process.env): CiMetadata {\n const provider = PROVIDERS.find((p) => p.detect(env));\n if (provider) {\n const result: CiMetadata = { ciProvider: provider.providerName };\n const buildNumber = provider.buildNumber?.(env);\n if (buildNumber !== undefined) result.ciBuildNumber = buildNumber;\n const runUrl = provider.runUrl?.(env);\n if (runUrl !== undefined) result.ciRunUrl = runUrl;\n const prNumber = provider.prNumber?.(env);\n if (prNumber !== undefined) result.ciPrNumber = prNumber;\n const runId = provider.runId?.(env);\n if (runId !== undefined) result.ciRunId = runId;\n return result;\n }\n\n // Fallback: ci-info's ~70-provider detection gives us a free-text provider\n // name (the server's `ciProvider` field has no enum — an unrecognized\n // value is always accepted, per its doc comment in shared/types.ts) even\n // for providers our explicit table above doesn't cover a full\n // build-number/run-URL/PR-number extraction for.\n if (ciInfo.name) {\n return { ciProvider: ciInfo.name };\n }\n return {};\n}\n","import { execFileSync } from 'node:child_process';\n\n/** Auto-detected branch/commit — the bottom tier of `resolve-config.ts`'s\n * precedence chain (option > QUALFLARE_BRANCH/QF_BRANCH env > CI env vars >\n * local git subprocess > null). Does NOT include the QUALFLARE_BRANCH/\n * QF_BRANCH-style env aliases — those are resolved at a higher tier,\n * directly in `resolve-config.ts`, before this module is ever consulted. */\nexport interface GitInfo {\n branch?: string;\n commit?: string;\n}\n\n/** Injectable so tests never actually shell out. `stdio: ['ignore', 'pipe',\n * 'ignore']` suppresses git's own stderr chatter (e.g. \"fatal: not a git\n * repository\") from leaking into the CI log for what is, from this plugin's\n * perspective, an entirely expected, non-fatal outcome. */\nexport type ExecGit = (args: string[], cwd: string) => string;\n\nconst defaultExecGit: ExecGit = (args, cwd) =>\n execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });\n\nfunction firstEnv(env: NodeJS.ProcessEnv, ...names: string[]): string | undefined {\n for (const name of names) {\n const value = env[name];\n if (value) {\n return value;\n }\n }\n return undefined;\n}\n\nfunction detectBranchFromGit(exec: ExecGit, cwd: string): string | undefined {\n try {\n // Empty on detached HEAD (the `-q` flag suppresses the error and the\n // command still exits 0 with no output in that case on some git\n // versions, hence the explicit empty-string check as well as the\n // try/catch for versions that exit non-zero instead).\n const out = exec(['symbolic-ref', '--short', '-q', 'HEAD'], cwd).trim();\n return out || undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction detectCommitFromGit(exec: ExecGit, cwd: string): string | undefined {\n try {\n const out = exec(['rev-parse', 'HEAD'], cwd).trim();\n return out || undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Auto-detects branch/commit: CI-provider env vars first (cheap, no\n * subprocess), falling back to a local `git` subprocess only for whichever\n * of branch/commit the env vars didn't resolve — mirroring\n * `qualflare-cli/internal/config/config.go`'s `LoadFromEnv`\n * (`getFirstEnv(\"GIT_BRANCH\", \"GITHUB_REF_NAME\", \"CI_COMMIT_REF_NAME\",\n * \"BITBUCKET_BRANCH\")` / the equivalent commit chain) and its `DetectGit`'s\n * \"only shell out for what's actually missing\" behavior (BUG-39 in that\n * file: forking `git` on every CLI invocation, even `--help`, was wasteful —\n * the same reasoning applies here, this plugin should not fork two `git`\n * processes on every `cypress run` when CI env vars already cover both\n * values, or when the caller already resolved both from options/env at a\n * higher precedence tier and doesn't need this module at all).\n */\nexport function detectGit(\n env: NodeJS.ProcessEnv = process.env,\n cwd: string = process.cwd(),\n exec: ExecGit = defaultExecGit,\n): GitInfo {\n const branch =\n firstEnv(env, 'GIT_BRANCH', 'GITHUB_REF_NAME', 'CI_COMMIT_REF_NAME', 'BITBUCKET_BRANCH') ??\n detectBranchFromGit(exec, cwd);\n const commit =\n firstEnv(env, 'GIT_COMMIT', 'GITHUB_SHA', 'CI_COMMIT_SHA', 'BITBUCKET_COMMIT') ??\n detectCommitFromGit(exec, cwd);\n\n const result: GitInfo = {};\n if (branch !== undefined) result.branch = branch;\n if (commit !== undefined) result.commit = commit;\n return result;\n}\n","import { MAX_CASES_PER_SUITE, TASK_MARK_TEST_PHASE_STARTED, TASK_REPORT_CASE } from '../shared/constants.js';\nimport { logger } from '../shared/logger.js';\nimport type { Case } from '../shared/types.js';\nimport { resolveAttachments, type AttachmentBudget, type AttachmentReaderConfig } from './attachment-reader.js';\nimport type { PendingAttachmentQueue, TestPhaseGate } from './state.js';\n\n/**\n * Buffers finished `Case` objects the browser side hands over via\n * `cy.task(TASK_REPORT_CASE, ...)`, one spec file's worth at a time.\n * `events.ts`'s `after:spec` handler drains this into a `Suite` and clears\n * it before the next spec starts.\n *\n * `Suite.cases` is REJECT-not-truncate server-side (`max=5000`) — exceeding\n * it 400s the whole launch, not just this one spec's results. Warn-and-drop\n * further cases past that point, mirroring `state.ts`'s `LaunchAccumulator`\n * (2000-suite cap) exactly.\n */\nexport class CaseBuffer {\n private cases: Case[] = [];\n private truncated = false;\n\n add(testCase: Case): void {\n if (this.cases.length >= MAX_CASES_PER_SUITE) {\n if (!this.truncated) {\n this.truncated = true;\n logger.warn(\n `reached the server's ${MAX_CASES_PER_SUITE}-case-per-suite cap — further test results in this ` +\n 'spec file will not be uploaded.',\n );\n }\n return;\n }\n this.cases.push(testCase);\n }\n\n /** Returns the buffered cases and clears the buffer (including the\n * truncation-warned flag, so a later spec that also hits the cap warns\n * again rather than staying silent for the rest of the run). */\n drain(): Case[] {\n const drained = this.cases;\n this.cases = [];\n this.truncated = false;\n return drained;\n }\n}\n\n/**\n * Registers the `on('task', {...})` handlers the browser-side support\n * script calls via `cy.task()`. Cypress requires every task handler to\n * return a non-`undefined` value (a Promise resolving to `null` is fine)\n * or it throws.\n *\n * The TASK_REPORT_CASE handler also drains `pendingAttachments` (populated\n * Node-side by `events.ts`'s `after:screenshot` listener — see its comment\n * for why screenshot capture needs no browser-side/`cy.task()` involvement\n * at all) and resolves each into inline base64 content (or drops it) before\n * buffering the case — this is the one place both \"a finished test\" and\n * \"screenshots taken during that test\" are known at the same time.\n *\n * `Attachment.stepIndex` (correlating a screenshot to the specific step\n * executing when it was taken) is deliberately left unset here. Screenshots\n * are captured entirely Node-side via `after:screenshot`, while steps are\n * built entirely browser-side (`command-log-listener.ts`) and only reach\n * Node bundled inside the already-finished `Case` — by the time\n * `after:screenshot` fires, Node has no visibility into which step index\n * the browser's still-in-progress step buffer is currently on, and no cheap\n * way to ask it without adding a new `cy.task()` round-trip purely to answer\n * \"what step are we on,\" which isn't justified for this milestone. Every\n * screenshot is attached at the case level instead (works today, matches\n * Milestone 2's existing behavior) — worth revisiting only if step-level\n * screenshot attribution becomes a real, requested feature. */\nexport function registerTasks(\n on: Cypress.PluginEvents,\n buffer: CaseBuffer,\n attachmentConfig: AttachmentReaderConfig,\n attachmentBudget: AttachmentBudget,\n pendingAttachments: PendingAttachmentQueue,\n testPhaseGate: TestPhaseGate,\n): void {\n on('task', {\n async [TASK_REPORT_CASE](testCase: Case): Promise<null> {\n // Merge screenshots captured Node-side during this test (via\n // after:screenshot) with any attachments the browser side already\n // set directly on the Case (e.g. a `qualflare.attachmentFromFile()`\n // call, which carries only a `path` — resolveAttachments reads/\n // uploads it here, same as a screenshot).\n const attachments = [...(testCase.attachments ?? []), ...pendingAttachments.drain()];\n const resolved = await resolveAttachments(\n attachments.length > 0 ? attachments : undefined,\n attachmentConfig,\n attachmentBudget,\n );\n buffer.add({ ...testCase, attachments: resolved });\n return null;\n },\n // One-shot signal from the browser side (see TestPhaseGate's doc\n // comment in state.ts) — fired from a root-level beforeEach right\n // before the first test's body runs.\n [TASK_MARK_TEST_PHASE_STARTED](): null {\n testPhaseGate.markStarted();\n return null;\n },\n });\n}\n"],"mappings":"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","names":["fs","path","fs","path","import_node_crypto","import_node_crypto","name","firstEnv","name"]}
@@ -181,7 +181,7 @@ function msToNs(ms) {
181
181
  import * as os from "os";
182
182
 
183
183
  // src/plugin/version.ts
184
- var PACKAGE_VERSION = "0.3.0";
184
+ var PACKAGE_VERSION = "0.4.0";
185
185
 
186
186
  // src/plugin/collect-builder.ts
187
187
  function resolveOs(config, info) {