@pixeyedev/core 0.4.4 → 0.4.5

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/dist/client.d.ts CHANGED
@@ -3,7 +3,23 @@ export declare class PixeyeError extends Error {
3
3
  status: number;
4
4
  path: string;
5
5
  body: string;
6
- constructor(status: number, path: string, body: string);
6
+ /**
7
+ * Where the failure came from. `'api'` = the Pixeye control-plane (a 4xx
8
+ * here can be a customer misconfig — token/plan/permissions). `'r2'` = a
9
+ * data-plane PUT to R2 via a presigned URL (a 403/404 here is a Pixeye-side
10
+ * fault — expired URL, bucket/credential, clock skew — NEVER the customer's).
11
+ * Used by `isUploadMisconfig` so an R2 outage can't fail a customer's CI.
12
+ */
13
+ origin: 'api' | 'r2';
14
+ constructor(status: number, path: string, body: string,
15
+ /**
16
+ * Where the failure came from. `'api'` = the Pixeye control-plane (a 4xx
17
+ * here can be a customer misconfig — token/plan/permissions). `'r2'` = a
18
+ * data-plane PUT to R2 via a presigned URL (a 403/404 here is a Pixeye-side
19
+ * fault — expired URL, bucket/credential, clock skew — NEVER the customer's).
20
+ * Used by `isUploadMisconfig` so an R2 outage can't fail a customer's CI.
21
+ */
22
+ origin?: 'api' | 'r2');
7
23
  }
8
24
  export interface PixeyeClientOptions {
9
25
  serverUrl: string;
package/dist/client.js CHANGED
@@ -4,11 +4,21 @@ export class PixeyeError extends Error {
4
4
  status;
5
5
  path;
6
6
  body;
7
- constructor(status, path, body) {
7
+ origin;
8
+ constructor(status, path, body,
9
+ /**
10
+ * Where the failure came from. `'api'` = the Pixeye control-plane (a 4xx
11
+ * here can be a customer misconfig — token/plan/permissions). `'r2'` = a
12
+ * data-plane PUT to R2 via a presigned URL (a 403/404 here is a Pixeye-side
13
+ * fault — expired URL, bucket/credential, clock skew — NEVER the customer's).
14
+ * Used by `isUploadMisconfig` so an R2 outage can't fail a customer's CI.
15
+ */
16
+ origin = 'api') {
8
17
  super(`Pixeye API ${path} failed (${status}): ${body}`);
9
18
  this.status = status;
10
19
  this.path = path;
11
20
  this.body = body;
21
+ this.origin = origin;
12
22
  this.name = 'PixeyeError';
13
23
  }
14
24
  }
@@ -67,7 +77,9 @@ export class PixeyeClient {
67
77
  headers: { 'Content-Type': contentType }
68
78
  });
69
79
  if (!res.ok) {
70
- throw new PixeyeError(res.status, maskedUrl, await res.text());
80
+ // 'r2' origin — a PUT failure here is Pixeye-side (expired presigned
81
+ // URL, bucket/creds, clock skew), never the customer's misconfig.
82
+ throw new PixeyeError(res.status, maskedUrl, await res.text(), 'r2');
71
83
  }
72
84
  }, isRetryableHttpError, {
73
85
  maxAttempts: 3,
package/dist/index.d.ts CHANGED
@@ -10,3 +10,4 @@ export { runParallel } from './parallel.js';
10
10
  export type { RetryOptions } from './retry.js';
11
11
  export { isRetryableHttpError, maskPresignedUrl, withRetry } from './retry.js';
12
12
  export { DEFAULT_SERVER_URL, DEFAULT_UPLOAD_CONCURRENCY } from './constants.js';
13
+ export { isUploadMisconfig, renderUploadFailureBanner, resolveFailOnError, uploadErrorSummary } from './uploadError.js';
package/dist/index.js CHANGED
@@ -7,3 +7,4 @@ export { runParallel } from './parallel.js';
7
7
  export { isRetryableHttpError, maskPresignedUrl, withRetry } from './retry.js';
8
8
  // Constants
9
9
  export { DEFAULT_SERVER_URL, DEFAULT_UPLOAD_CONCURRENCY } from './constants.js';
10
+ export { isUploadMisconfig, renderUploadFailureBanner, resolveFailOnError, uploadErrorSummary } from './uploadError.js';
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Classify an upload failure and render a loud banner — shared by the Playwright
3
+ * reporter and the CLI so a lapsed plan / bad token surfaces identically (P0-2).
4
+ *
5
+ * The rule: distinguish the CUSTOMER's misconfiguration (they must act) from a
6
+ * transient / our-side fault (their CI should not break over our hiccup).
7
+ */
8
+ /**
9
+ * True when the failure is the customer's configuration/billing, not ours.
10
+ * A `PixeyeError` from the R2 data-plane (`origin: 'r2'`) is EXCLUDED — a
11
+ * 403/404 on a presigned PUT (expired URL, bucket/creds, clock skew) is a
12
+ * Pixeye-side fault and must never fail the customer's CI.
13
+ */
14
+ export declare const isUploadMisconfig: (err: unknown) => boolean;
15
+ /**
16
+ * One-line `HTTP <status> — <detail>` summary. Prefers the error's `body`
17
+ * (the meaningful server code, e.g. `SUBSCRIPTION_CANCELLED`) over the full
18
+ * `PixeyeError.message`, which already embeds the status (avoids `HTTP 402 —
19
+ * Pixeye API … failed (402): …`).
20
+ */
21
+ export declare const uploadErrorSummary: (err: unknown) => string;
22
+ /**
23
+ * Resolve the fail-on-error setting identically for the reporter and CLI: fail
24
+ * by default; opt out via EITHER an explicit `false` option OR
25
+ * `PIXEYE_FAIL_ON_ERROR=false`. (The CLI's commander always supplies a boolean
26
+ * option; the reporter's is optional — this keeps both consistent.)
27
+ */
28
+ export declare const resolveFailOnError: (option: boolean | undefined, env: string | undefined) => boolean;
29
+ /**
30
+ * A bright, unmissable banner for a rejected upload. `willFail` toggles the
31
+ * closing line between "failing this step" and "opted out — passing anyway".
32
+ */
33
+ export declare const renderUploadFailureBanner: (err: unknown, opts: {
34
+ willFail: boolean;
35
+ }) => string;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Classify an upload failure and render a loud banner — shared by the Playwright
3
+ * reporter and the CLI so a lapsed plan / bad token surfaces identically (P0-2).
4
+ *
5
+ * The rule: distinguish the CUSTOMER's misconfiguration (they must act) from a
6
+ * transient / our-side fault (their CI should not break over our hiccup).
7
+ */
8
+ /**
9
+ * HTTP statuses that mean the CUSTOMER must act — a bad/absent token (401),
10
+ * a lapsed subscription or exceeded quota (402), insufficient permissions
11
+ * (403), or a wrong project/endpoint (404). These fail CI by default so a run
12
+ * can't pass green while zero screenshots reached Pixeye. EVERYTHING else
13
+ * (429 rate-limit, 5xx, network, timeouts, no status) is transient / our fault
14
+ * → warn, never break their CI.
15
+ */
16
+ const MISCONFIG_STATUSES = [401, 402, 403, 404];
17
+ const statusOf = (err) => {
18
+ const status = err?.status;
19
+ return typeof status === 'number' ? status : undefined;
20
+ };
21
+ /**
22
+ * True when the failure is the customer's configuration/billing, not ours.
23
+ * A `PixeyeError` from the R2 data-plane (`origin: 'r2'`) is EXCLUDED — a
24
+ * 403/404 on a presigned PUT (expired URL, bucket/creds, clock skew) is a
25
+ * Pixeye-side fault and must never fail the customer's CI.
26
+ */
27
+ export const isUploadMisconfig = (err) => {
28
+ if (err?.origin === 'r2') {
29
+ return false;
30
+ }
31
+ const status = statusOf(err);
32
+ return status !== undefined && MISCONFIG_STATUSES.includes(status);
33
+ };
34
+ /**
35
+ * One-line `HTTP <status> — <detail>` summary. Prefers the error's `body`
36
+ * (the meaningful server code, e.g. `SUBSCRIPTION_CANCELLED`) over the full
37
+ * `PixeyeError.message`, which already embeds the status (avoids `HTTP 402 —
38
+ * Pixeye API … failed (402): …`).
39
+ */
40
+ export const uploadErrorSummary = (err) => {
41
+ const status = statusOf(err);
42
+ const body = err?.body;
43
+ const detail = typeof body === 'string' && body.length > 0
44
+ ? body
45
+ : err instanceof Error
46
+ ? err.message
47
+ : String(err);
48
+ return status !== undefined ? `HTTP ${status} — ${detail}` : detail;
49
+ };
50
+ /**
51
+ * Resolve the fail-on-error setting identically for the reporter and CLI: fail
52
+ * by default; opt out via EITHER an explicit `false` option OR
53
+ * `PIXEYE_FAIL_ON_ERROR=false`. (The CLI's commander always supplies a boolean
54
+ * option; the reporter's is optional — this keeps both consistent.)
55
+ */
56
+ export const resolveFailOnError = (option, env) => option !== false && env !== 'false';
57
+ /**
58
+ * A bright, unmissable banner for a rejected upload. `willFail` toggles the
59
+ * closing line between "failing this step" and "opted out — passing anyway".
60
+ */
61
+ export const renderUploadFailureBanner = (err, opts) => {
62
+ const line = '━'.repeat(64);
63
+ const footer = opts.willFail
64
+ ? [
65
+ ' Failing this step so it cannot pass silently. Fix the above, or pass',
66
+ ' failOnError: false (reporter) / --no-fail-on-error (CLI) to opt out.'
67
+ ]
68
+ : [' failOnError is OFF — this step passes despite the lost coverage.'];
69
+ return [
70
+ '',
71
+ `\x1b[31m\x1b[1m${line}`,
72
+ ` ✗ PIXEYE UPLOAD REJECTED — ${uploadErrorSummary(err)}`,
73
+ '',
74
+ ' Your screenshots did NOT reach Pixeye. Visual coverage for this run is',
75
+ ' LOST. This is a configuration or billing problem on your account (API',
76
+ ' token, subscription/quota, or project permissions) — not a transient',
77
+ ' Pixeye outage.',
78
+ '',
79
+ ...footer,
80
+ `${line}\x1b[0m`,
81
+ ''
82
+ ].join('\n');
83
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * F-095: parse shard-related env vars (PIXEYE_SHARD_INDEX,
3
+ * PIXEYE_SHARD_TOTAL, CYPRESS_SPLIT_INDEX, CURRENTS_PARALLEL_INDEX, …)
4
+ * with a NaN guard.
5
+ *
6
+ * Bare `parseInt('abc', 10)` returns NaN; `JSON.stringify({ index: NaN })`
7
+ * coerces to `null` and the API rejects with a confusing
8
+ * "Validation failed: index must be a number" error 30s deep into a CI
9
+ * pipeline. Failing fast at parse time with a clear message identifying
10
+ * the offending env var lets users fix `PIXEYE_SHARD_INDEX=` typos
11
+ * without running the test suite.
12
+ *
13
+ * The helper is intentionally permissive about the shape: callers pass
14
+ * the env var name so the error message is self-explanatory. Returns
15
+ * `undefined` when the value is empty/missing (so callers can default).
16
+ */
17
+ export declare const parseShardInt: (value: string | undefined | null, name: string) => number;
18
+ /**
19
+ * Same as `parseShardInt` but returns `undefined` when the value is
20
+ * missing — useful when the env var is optional and the caller will
21
+ * fall back to a default. Still throws on a non-numeric explicit value.
22
+ */
23
+ export declare const parseShardIntOptional: (value: string | undefined | null, name: string) => number | undefined;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * F-095: parse shard-related env vars (PIXEYE_SHARD_INDEX,
3
+ * PIXEYE_SHARD_TOTAL, CYPRESS_SPLIT_INDEX, CURRENTS_PARALLEL_INDEX, …)
4
+ * with a NaN guard.
5
+ *
6
+ * Bare `parseInt('abc', 10)` returns NaN; `JSON.stringify({ index: NaN })`
7
+ * coerces to `null` and the API rejects with a confusing
8
+ * "Validation failed: index must be a number" error 30s deep into a CI
9
+ * pipeline. Failing fast at parse time with a clear message identifying
10
+ * the offending env var lets users fix `PIXEYE_SHARD_INDEX=` typos
11
+ * without running the test suite.
12
+ *
13
+ * The helper is intentionally permissive about the shape: callers pass
14
+ * the env var name so the error message is self-explanatory. Returns
15
+ * `undefined` when the value is empty/missing (so callers can default).
16
+ */
17
+ export const parseShardInt = (value, name) => {
18
+ if (value === undefined || value === null || value === '') {
19
+ throw new Error(`${name} must be set`);
20
+ }
21
+ const parsed = parseInt(value, 10);
22
+ if (Number.isNaN(parsed)) {
23
+ throw new Error(`${name} must be an integer (got ${JSON.stringify(value)})`);
24
+ }
25
+ return parsed;
26
+ };
27
+ /**
28
+ * Same as `parseShardInt` but returns `undefined` when the value is
29
+ * missing — useful when the env var is optional and the caller will
30
+ * fall back to a default. Still throws on a non-numeric explicit value.
31
+ */
32
+ export const parseShardIntOptional = (value, name) => {
33
+ if (value === undefined || value === null || value === '') {
34
+ return undefined;
35
+ }
36
+ const parsed = parseInt(value, 10);
37
+ if (Number.isNaN(parsed)) {
38
+ throw new Error(`${name} must be an integer (got ${JSON.stringify(value)})`);
39
+ }
40
+ return parsed;
41
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixeyedev/core",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "files": [