@qualflare/cypress 0.1.0 → 0.2.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.
- package/README.md +39 -11
- package/dist/index.cjs +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin/index.cjs +126 -275
- package/dist/plugin/index.cjs.map +1 -1
- package/dist/plugin/index.d.cts +22 -20
- package/dist/plugin/index.d.ts +22 -20
- package/dist/plugin/index.js +126 -275
- package/dist/plugin/index.js.map +1 -1
- package/package.json +5 -5
package/dist/plugin/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/plugin/attachment-reader.ts","../../src/shared/logger.ts","../../src/http/client.ts","../../src/shared/constants.ts","../../src/http/backoff.ts","../../src/http/errors.ts","../../src/http/idempotency.ts","../../src/shared/duration.ts","../../src/plugin/version.ts","../../src/plugin/collect-builder.ts","../../src/plugin/state.ts","../../src/plugin/events.ts","../../src/plugin/ci-detect.ts","../../src/plugin/git-detect.ts","../../src/plugin/resolve-config.ts","../../src/plugin/tasks.ts","../../src/plugin/index.ts"],"sourcesContent":["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';\n\n/** Extensions/mime-prefixes this reporter must never attach — qualflare-cypress\n * has no video/blob-attachment support yet (a separate, unbuilt backend\n * feature). Nothing in this codebase should ever produce one of these (only\n * `events.ts`'s `after:screenshot` handler enqueues a pending attachment,\n * and it always hardcodes `image/png`), but this is enforced here too,\n * defensively, as a belt-and-suspenders guard rather than relying purely on\n * that convention. */\nconst VIDEO_EXTENSIONS = new Set(['.mp4', '.webm', '.mov', '.avi', '.mkv']);\n\nexport interface AttachmentReaderConfig {\n attachScreenshots: boolean;\n maxAttachmentBytes: number;\n maxTotalAttachmentBytes: number;\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 inline base64 `content`, or\n * drops them. Attachment references arrive with only a `path` (never\n * bytes — screenshots are captured entirely Node-side via the\n * `after:screenshot` plugin event in `events.ts`, so there is no browser\n * context involved in producing them at all), so all file I/O and\n * size-guarding happens here, at the point a finished Case is received from\n * `cy.task(TASK_REPORT_CASE, ...)` — see `tasks.ts`.\n *\n * Per the plan's resolved decision, an oversized or over-budget attachment\n * is skipped ENTIRELY (not degraded to a contentless path-only entry): the\n * server's `path` field is explicitly informational/never-fetched, so a\n * contentless entry has little value and this keeps the behavior simple\n * and predictable.\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 logger.warn(\n `refusing to attach \"${attachment.name}\": video attachments are not supported yet ` +\n `(${attachment.path ?? attachment.mimeType ?? 'unknown'}).`,\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 { request } from 'undici';\n\nimport {\n HEADER_ACCEPT,\n HEADER_CONTENT_TYPE,\n HEADER_IDEMPOTENCY_KEY,\n HEADER_TOKEN,\n HEADER_USER_AGENT,\n} from '../shared/constants.js';\nimport { logger } from '../shared/logger.js';\nimport type { ApiErrorResponse, Collect, CollectResult } from '../shared/types.js';\nimport { computeDelay } from './backoff.js';\nimport { buildApiError, QualflareApiError } from './errors.js';\nimport { newIdempotencyKey } from './idempotency.js';\n\nexport interface RetryOptions {\n max: number;\n baseDelayMs: number;\n maxDelayMs: number;\n}\n\nexport interface SendOptions {\n endpoint: string;\n token: string;\n timeoutMs: number;\n retry: RetryOptions;\n userAgent: string;\n debug: boolean;\n}\n\n/** HTTP status codes worth retrying: a transient/transport-level condition\n * that a later attempt might succeed at. Mirrors\n * `qualflare-cli/internal/adapters/http/client.go`'s `AddRetryConditions`\n * exactly. 400/401/403/404/413/422 are all client-error conditions a retry\n * cannot fix. */\nconst RETRYABLE_STATUS_CODES = new Set([429, 500, 502, 503, 504]);\n\nfunction redactToken(token: string): string {\n return token.length > 0 ? '***REDACTED***' : '(none)';\n}\n\n/** POSTs a `Collect` payload to `/api/v1/collect`, mirroring the Go CLI's\n * resty-based HTTP client behavior: a stable per-call Idempotency-Key reused\n * across retries, exponential backoff with jitter on transport errors and\n * 429/500/502/503/504, disabled redirects (the auth token must never leak to\n * a different host via a redirect hop), and a generous default timeout that\n * leaves real headroom over the server's own ~30s /collect transaction\n * budget. */\nexport class QualflareHttpClient {\n constructor(private readonly opts: SendOptions) {}\n\n async send(collect: Collect): Promise<CollectResult> {\n const url = `${this.opts.endpoint.replace(/\\/+$/, '')}/api/v1/collect`;\n const idempotencyKey = newIdempotencyKey();\n const body = JSON.stringify(collect);\n const maxAttempts = Math.max(1, this.opts.retry.max + 1);\n\n let lastError: unknown;\n\n for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {\n if (this.opts.debug) {\n logger.debug(\n `POST ${url} (attempt ${attempt}/${maxAttempts}, QF_TOKEN: ${redactToken(this.opts.token)})`,\n );\n }\n\n let statusCode: number;\n let responseBody: string;\n let responseHeaders: Record<string, string | string[] | undefined>;\n try {\n const res = await request(url, {\n method: 'POST',\n headers: {\n [HEADER_TOKEN]: this.opts.token,\n [HEADER_CONTENT_TYPE]: 'application/json',\n [HEADER_ACCEPT]: 'application/json',\n [HEADER_USER_AGENT]: this.opts.userAgent,\n [HEADER_IDEMPOTENCY_KEY]: idempotencyKey,\n },\n body,\n maxRedirections: 0,\n signal: AbortSignal.timeout(this.opts.timeoutMs),\n });\n statusCode = res.statusCode;\n responseHeaders = res.headers;\n responseBody = await res.body.text();\n } catch (err) {\n lastError = err;\n if (this.opts.debug) {\n logger.debug(`attempt ${attempt} transport error: ${(err as Error)?.message ?? err}`);\n }\n if (attempt < maxAttempts) {\n await sleep(computeDelay(attempt, this.opts.retry.baseDelayMs, this.opts.retry.maxDelayMs));\n continue;\n }\n throw new QualflareApiError({\n message: `failed to send request to ${url}`,\n cause: err,\n });\n }\n\n if (this.opts.debug) {\n logger.debug(`attempt ${attempt} response: ${statusCode}`);\n }\n\n if (statusCode >= 200 && statusCode < 300) {\n return parseSuccess(responseBody);\n }\n\n const parsedError = parseErrorBody(responseBody);\n\n if (!RETRYABLE_STATUS_CODES.has(statusCode) || attempt === maxAttempts) {\n throw buildApiError(statusCode, parsedError);\n }\n\n const retryAfterMs = parseRetryAfter(responseHeaders['retry-after']);\n const delay = computeDelay(\n attempt,\n this.opts.retry.baseDelayMs,\n this.opts.retry.maxDelayMs,\n retryAfterMs,\n );\n if (this.opts.debug) {\n logger.debug(`retrying after ${Math.round(delay)}ms (status ${statusCode})`);\n }\n await sleep(delay);\n }\n\n // Unreachable in practice (the loop always returns or throws), but keeps\n // the function's return type honest without a non-null assertion.\n throw lastError instanceof Error\n ? lastError\n : new QualflareApiError({ message: 'request failed for an unknown reason' });\n }\n}\n\nfunction parseSuccess(responseBody: string): CollectResult {\n try {\n const parsed = JSON.parse(responseBody) as CollectResult;\n if (typeof parsed.seq !== 'number') {\n throw new Error('response body missing numeric \"seq\"');\n }\n return parsed;\n } catch (err) {\n throw new QualflareApiError({\n message: 'server returned a success status but an unparseable body',\n cause: err,\n });\n }\n}\n\nfunction parseErrorBody(responseBody: string): ApiErrorResponse | undefined {\n if (!responseBody) {\n return undefined;\n }\n try {\n return JSON.parse(responseBody) as ApiErrorResponse;\n } catch {\n return undefined;\n }\n}\n\nfunction parseRetryAfter(value: string | string[] | undefined): number | undefined {\n const raw = Array.isArray(value) ? value[0] : value;\n if (!raw) {\n return undefined;\n }\n // Retry-After is either a number of seconds or an HTTP-date; only the\n // (far more common, and simpler to trust) numeric-seconds form is honored\n // here — an HTTP-date value is ignored and falls back to the computed\n // exponential-backoff delay instead.\n const seconds = Number(raw);\n return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1000 : undefined;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\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/** HTTP headers used against `/api/v1/collect`. */\nexport const HEADER_TOKEN = 'QF_TOKEN';\nexport const HEADER_IDEMPOTENCY_KEY = 'Idempotency-Key';\nexport const HEADER_CONTENT_TYPE = 'Content-Type';\nexport const HEADER_ACCEPT = 'Accept';\nexport const HEADER_USER_AGENT = 'User-Agent';\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;\nexport const MAX_IDEMPOTENCY_KEY_CHARS = 255;\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","/**\n * Computes the delay before retry attempt `attempt` (1-based: the delay\n * before the SECOND request, i.e. after the first failure). Exponential\n * backoff with full jitter, capped at `maxDelayMs` — intent-parity with the\n * Go CLI's resty-based exponential 1s->30s backoff (not necessarily\n * byte-identical; resty's internal jitter algorithm isn't something worth\n * reverse-engineering).\n *\n * When the server sent a `Retry-After` value (in milliseconds, already\n * parsed by the caller), the returned delay is never shorter than that —\n * honoring it is a defensible improvement the Go CLI doesn't make, cheap to\n * add, and directly useful against the known \"questionable retry-on-429/503\n * behavior server-side\" gap noted in api-service's own code-quality review.\n */\nexport function computeDelay(\n attempt: number,\n baseDelayMs: number,\n maxDelayMs: number,\n retryAfterMs?: number,\n): number {\n const exponential = baseDelayMs * 2 ** Math.max(0, attempt - 1);\n const jittered = Math.random() * Math.min(exponential, maxDelayMs);\n const floor = retryAfterMs !== undefined ? retryAfterMs : 0;\n // Bounded by maxDelayMs even when honoring a server Retry-After: a\n // pathological/misconfigured server value should never be able to hang a\n // CI job indefinitely.\n return Math.min(Math.max(jittered, floor), maxDelayMs);\n}\n","import type { ApiErrorResponse, ApiFieldError } from '../shared/types.js';\n\n/** CLI-side friendly hints for a few well-known server error codes — only\n * used as a fallback when the server sent no `message` of its own. Mirrors\n * `qualflare-cli/internal/adapters/http/client.go`'s `getUserFriendlyMessage`.\n * `common.resource_not_found` is deliberately NOT aliased to a specific\n * resource here — that's the generic 404 code and aliasing it caused a\n * documented bug in the Go CLI (every 404 rendering as \"Language not found\"). */\nfunction friendlyHint(code: string | undefined): string | undefined {\n switch (code) {\n case 'environment.not_found':\n return 'Environment not found. Check the `environment` option or create it in Qualflare.';\n case 'milestone.not_found':\n return 'Milestone not found. Check the `milestone` option or its sequence number in Qualflare.';\n case 'common.validation_failed':\n return 'Validation failed. Check the request data below.';\n default:\n return undefined;\n }\n}\n\nfunction actionHint(statusCode: number | undefined): string | undefined {\n switch (statusCode) {\n case 401:\n return 'the configured token is missing or invalid — check `token`/QUALFLARE_TOKEN';\n case 403:\n return 'the token lacks access to this project';\n case 402:\n return 'a plan limit was reached — check your Qualflare subscription';\n default:\n return undefined;\n }\n}\n\n/** Renders the server's per-field validation errors into one readable line.\n * This is a real, confirmed gap the existing Go CLI has (it parses `fields`\n * off the wire but never renders them) — this package renders them from\n * day one instead of just showing the top-level `message`. */\nfunction renderFields(fields: ApiFieldError[] | undefined): string | undefined {\n if (!fields || fields.length === 0) {\n return undefined;\n }\n return fields\n .map((f) => {\n const rule = f.rule ? ` (${f.rule})` : '';\n const msg = f.message ? `: ${f.message}` : '';\n return `${f.field}${rule}${msg}`;\n })\n .join('; ');\n}\n\nexport interface QualflareApiErrorInit {\n message: string;\n code?: string;\n statusCode?: number;\n requestId?: string;\n fields?: ApiFieldError[];\n cause?: unknown;\n}\n\n/** Thrown by `QualflareHttpClient.send()` on any terminal (non-retried, or\n * retries-exhausted) failure. Carries everything needed to log something\n * actionable without the caller having to know the wire error shape. */\nexport class QualflareApiError extends Error {\n readonly code?: string;\n readonly statusCode?: number;\n readonly requestId?: string;\n readonly fields?: ApiFieldError[];\n\n constructor(init: QualflareApiErrorInit) {\n const parts = [init.message];\n const fieldsRendered = renderFields(init.fields);\n if (fieldsRendered) {\n parts.push(`fields: ${fieldsRendered}`);\n }\n const hint = actionHint(init.statusCode);\n if (hint) {\n parts.push(`(${hint})`);\n }\n if (init.requestId) {\n parts.push(`[request_id: ${init.requestId}]`);\n }\n super(parts.join(' — '), init.cause !== undefined ? { cause: init.cause } : undefined);\n this.name = 'QualflareApiError';\n this.code = init.code;\n this.statusCode = init.statusCode;\n this.requestId = init.requestId;\n this.fields = init.fields;\n }\n}\n\n/** Parses the server's `{code, message, fields, request_id}` envelope (also\n * tolerating the legacy `{error}` key) into a `QualflareApiError`. `body`\n * may be `undefined` if the response wasn't valid JSON at all. */\nexport function buildApiError(statusCode: number, body: ApiErrorResponse | undefined): QualflareApiError {\n const code = body?.code;\n const message = body?.message || friendlyHint(code) || body?.error || `request failed with status ${statusCode}`;\n return new QualflareApiError({\n message,\n code,\n statusCode,\n requestId: body?.request_id,\n fields: body?.fields,\n });\n}\n","import { randomUUID } from 'node:crypto';\n\nimport { MAX_IDEMPOTENCY_KEY_CHARS } from '../shared/constants.js';\n\n/**\n * Returns a fresh RFC 4122 v4 UUID string for use as an `Idempotency-Key`.\n * Call ONCE per logical `send()` attempt and reuse the same value across\n * every retry of that call — the server resolves a retried key to the\n * already-created launch, so a mid-flight 5xx retry cannot double-create.\n * Mirrors `qualflare-cli/internal/adapters/http/client.go`'s\n * `newIdempotencyKey`, using the platform RNG instead of a hand-rolled one.\n */\nexport function newIdempotencyKey(): string {\n const key = randomUUID();\n // A UUID is always 36 chars, well under the server's 255-char cap — this\n // assertion exists purely to fail loudly if that invariant ever changes\n // upstream, not because truncation here would ever be reachable.\n if (key.length > MAX_IDEMPOTENCY_KEY_CHARS) {\n return key.slice(0, MAX_IDEMPOTENCY_KEY_CHARS);\n }\n return key;\n}\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","// `__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 * 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 },\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","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 { QualflareHttpClient } from '../http/client.js';\nimport { logger } from '../shared/logger.js';\nimport { msToNs } from '../shared/duration.js';\nimport type { Suite } from '../shared/types.js';\nimport { PACKAGE_VERSION } from './version.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';\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 POSTing it\n * exactly once at `after:run`.\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 uploaded.',\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', (spec, results) => {\n const cases = buffer.drain();\n const suite: Suite = {\n name: spec.relative,\n category: 'e2e',\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 uploaded 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 uploaded.',\n );\n }\n\n if (results.video) {\n logger.info(\n `spec ${spec.relative} recorded a video at ${results.video} — not uploaded ` +\n '(qualflare-cypress does not support video attachments yet).',\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 upload.');\n return;\n }\n\n const collect = buildCollectPayload(accumulator, config, browserInfo);\n const client = new QualflareHttpClient({\n endpoint: config.apiEndpoint,\n token: config.token,\n timeoutMs: config.timeoutMs,\n retry: config.retry,\n userAgent: `qualflare-cypress/${PACKAGE_VERSION}`,\n debug: config.debug,\n });\n\n try {\n const result = await client.send(collect);\n logger.info(`uploaded launch #${result.seq} to Qualflare.`);\n } catch (err) {\n logger.error(`failed to upload results to Qualflare: ${(err as Error).message}`);\n if (config.failOnUploadError) {\n throw err;\n }\n }\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}\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 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 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 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 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 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 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 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 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 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 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 token?: string;\n apiEndpoint?: string;\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 timeoutMs?: number;\n retry?: {\n max?: number;\n baseDelayMs?: number;\n maxDelayMs?: number;\n };\n failOnUploadError?: boolean;\n attachScreenshots?: boolean;\n maxAttachmentBytes?: number;\n maxTotalAttachmentBytes?: number;\n debug?: boolean;\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}\n\nexport interface ResolvedPluginConfig {\n token: string;\n apiEndpoint: string;\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 timeoutMs: number;\n retry: { max: number; baseDelayMs: number; maxDelayMs: number };\n failOnUploadError: boolean;\n attachScreenshots: boolean;\n maxAttachmentBytes: number;\n maxTotalAttachmentBytes: number;\n debug: boolean;\n enabled: boolean;\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/** Thrown when a required value (currently only `token`) can't be resolved.\n * Deliberately thrown synchronously at `qualflareCypress()` call time\n * (config-load time) rather than deferred to the final `after:run` POST —\n * failing fast before any spec runs wastes far less CI time than\n * discovering a misconfiguration only at the very end of the run. */\nexport class QualflareConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'QualflareConfigError';\n }\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\n const token = options.token ?? firstEnv('QUALFLARE_TOKEN', 'QF_TOKEN') ?? '';\n if (enabled && token === '') {\n throw new QualflareConfigError(\n 'qualflare-cypress: no token configured. Set the `token` option or the QUALFLARE_TOKEN ' +\n '(or QF_TOKEN) environment variable, or pass `enabled: false` to disable this plugin.',\n );\n }\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 return {\n token,\n apiEndpoint: options.apiEndpoint ?? firstEnv('QUALFLARE_API_ENDPOINT') ?? 'https://api.qualflare.com',\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 `failOnUploadError` defaults `false` — failing the entire\n // upload with no visible error by default. Found via deep adversarial\n // self-review.\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 timeoutMs: options.timeoutMs ?? envInt('QUALFLARE_TIMEOUT_MS') ?? 120_000,\n retry: {\n max: options.retry?.max ?? envInt('QUALFLARE_RETRY_MAX', 'QF_RETRY_MAX') ?? 3,\n baseDelayMs: options.retry?.baseDelayMs ?? envInt('QUALFLARE_RETRY_BASE_DELAY_MS') ?? 1000,\n maxDelayMs: options.retry?.maxDelayMs ?? envInt('QUALFLARE_RETRY_MAX_DELAY_MS') ?? 30_000,\n },\n failOnUploadError: options.failOnUploadError ?? envBool('QUALFLARE_FAIL_ON_UPLOAD_ERROR') ?? false,\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 debug: options.debug ?? envBool('QUALFLARE_DEBUG', 'QF_DEBUG') ?? false,\n enabled,\n };\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 [TASK_REPORT_CASE](testCase: Case): 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 future author-provided attachment\n // with inline content — not populated by anything yet in this\n // milestone, but resolveAttachments already passes those through\n // unchanged since they carry `content`, not just a `path`).\n const attachments = [...(testCase.attachments ?? []), ...pendingAttachments.drain()];\n const resolved = 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","import { AttachmentBudget } 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';\nexport { QualflareConfigError } 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\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, resolved, attachmentBudget, pendingAttachments, testPhaseGate);\n\n if (resolved.enabled) {\n registerEvents(on, resolved, buffer, pendingAttachments, testPhaseGate);\n }\n\n return config;\n}\n"],"mappings":"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","names":["name","firstEnv","name"]}
|
|
1
|
+
{"version":3,"sources":["../../src/plugin/attachment-reader.ts","../../src/shared/logger.ts","../../src/plugin/video-uploader.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/ci-detect.ts","../../src/plugin/git-detect.ts","../../src/plugin/resolve-config.ts","../../src/plugin/tasks.ts","../../src/plugin/index.ts"],"sourcesContent":["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-uploader.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-uploader.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-uploader.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;\nexport const MAX_IDEMPOTENCY_KEY_CHARS = 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 },\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 * 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}\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 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 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 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 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 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 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 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 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 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_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 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 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/** Thrown when a required value (currently only `token`) can't be resolved.\n * Deliberately thrown synchronously at `qualflareCypress()` call time\n * (config-load time) rather than deferred to the final `after:run` POST —\n * failing fast before any spec runs wastes far less CI time than\n * discovering a misconfiguration only at the very end of the run. */\nexport class QualflareConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'QualflareConfigError';\n }\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 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 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 { 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","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';\nexport { QualflareConfigError } 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"],"mappings":";AAAA,YAAYA,SAAQ;AACpB,YAAYC,WAAU;;;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,YAAY,QAAQ;AACpB,YAAY,UAAU;AACtB,SAAS,kBAAkB;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,GAAG,WAAW,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,YAAYC,SAAQ;AACpB,YAAYC,WAAU;AACtB,SAAS,cAAAC,mBAAkB;;;ACOpB,IAAM,mBAAmB;AASzB,IAAM,+BAA+B;AAIrC,IAAM,wBAAwB;AAC9B,IAAM,sBAAsB;AAG5B,IAAM,2BAA2B;AASjC,IAAM,yBAAyB,KAAK,OAAO;;;ACjClD,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,YAAY,QAAQ;;;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,IACX;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;;;AE9DO,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,GAAGC,YAAW,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,YAAY,YAAY;AAmBxB,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,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,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,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,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,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,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,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,WAAO;AAAA,EACT;AAOA,MAAW,aAAM;AACf,WAAO,EAAE,YAAmB,YAAK;AAAA,EACnC;AACA,SAAO,CAAC;AACV;;;AC9JA,SAAS,oBAAoB;AAkB7B,IAAM,iBAA0B,CAAC,MAAM,QACrC,aAAa,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;;;ACNA,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;AAOO,IAAM,uBAAN,cAAmC,MAAM;AAAA,EAC9C,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;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;AAEpD,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,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;;;AC1LO,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;;;AC3EO,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","randomUUID","randomUUID","name","firstEnv","name"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qualflare/cypress",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Native Cypress reporter for the Qualflare test-management platform.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"qualflare",
|
|
@@ -51,21 +51,21 @@
|
|
|
51
51
|
"provenance": true
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"cypress": ">=12.0.0
|
|
54
|
+
"cypress": ">=12.0.0"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"ci-info": "^4.0.0"
|
|
58
|
-
"undici": "^6.19.8"
|
|
57
|
+
"ci-info": "^4.0.0"
|
|
59
58
|
},
|
|
60
59
|
"devDependencies": {
|
|
61
60
|
"@types/node": "^18.19.50",
|
|
62
|
-
"cypress": "^
|
|
61
|
+
"cypress": "^15.0.0",
|
|
63
62
|
"eslint": "^9.9.1",
|
|
64
63
|
"execa": "^9.3.1",
|
|
65
64
|
"prettier": "^3.3.3",
|
|
66
65
|
"tsup": "^8.2.4",
|
|
67
66
|
"typescript": "^5.6.2",
|
|
68
67
|
"typescript-eslint": "^8.5.0",
|
|
68
|
+
"undici": "^6.19.8",
|
|
69
69
|
"vitest": "^2.0.5"
|
|
70
70
|
},
|
|
71
71
|
"scripts": {
|