@xera-ai/web 0.1.6 → 0.1.7

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.
Files changed (54) hide show
  1. package/dist/core/src/adapter/types.d.ts +69 -0
  2. package/dist/core/src/artifact/hash.d.ts +3 -0
  3. package/dist/core/src/artifact/meta.d.ts +45 -0
  4. package/dist/core/src/artifact/paths.d.ts +24 -0
  5. package/dist/core/src/artifact/status.d.ts +95 -0
  6. package/dist/core/src/auth/encrypt.d.ts +3 -0
  7. package/dist/core/src/auth/key.d.ts +2 -0
  8. package/dist/core/src/auth/refresh.d.ts +8 -0
  9. package/dist/core/src/auth/state.d.ts +23 -0
  10. package/dist/core/src/config/define.d.ts +2 -0
  11. package/dist/core/src/config/load.d.ts +2 -0
  12. package/dist/core/src/config/schema.d.ts +325 -0
  13. package/dist/core/src/index.d.ts +19 -0
  14. package/dist/core/src/jira/client.d.ts +10 -0
  15. package/dist/core/src/jira/fields.d.ts +6 -0
  16. package/dist/core/src/jira/mcp-backend.d.ts +2 -0
  17. package/dist/core/src/jira/rest-backend.d.ts +7 -0
  18. package/dist/core/src/jira/retry.d.ts +7 -0
  19. package/dist/core/src/jira/types.d.ts +28 -0
  20. package/dist/core/src/lock/file-lock.d.ts +11 -0
  21. package/dist/core/src/logging/ndjson-logger.d.ts +10 -0
  22. package/dist/index.js +44 -12
  23. package/dist/{trace-normalizer → web/src/trace-normalizer}/scrub-rules.d.ts +4 -0
  24. package/package.json +1 -1
  25. package/src/adapter.ts +16 -5
  26. package/src/auth-setup/playwright-state.ts +1 -1
  27. package/src/auth-setup/runner.ts +1 -1
  28. package/src/executor/index.ts +4 -1
  29. package/src/generator/lint.ts +2 -2
  30. package/src/generator/pom-scan.ts +1 -1
  31. package/src/generator/promote.ts +4 -2
  32. package/src/generator/selector-rules.ts +18 -3
  33. package/src/trace-normalizer/normalize.ts +20 -7
  34. package/src/trace-normalizer/parse.ts +36 -11
  35. package/src/trace-normalizer/scrub-rules.ts +11 -2
  36. package/src/trace-normalizer/scrub.ts +6 -3
  37. package/src/trace-normalizer/unzip.ts +6 -1
  38. /package/dist/{adapter.d.ts → web/src/adapter.d.ts} +0 -0
  39. /package/dist/{auth-setup → web/src/auth-setup}/define.d.ts +0 -0
  40. /package/dist/{auth-setup → web/src/auth-setup}/playwright-state.d.ts +0 -0
  41. /package/dist/{auth-setup → web/src/auth-setup}/runner.d.ts +0 -0
  42. /package/dist/{executor → web/src/executor}/index.d.ts +0 -0
  43. /package/dist/{executor → web/src/executor}/playwright-args.d.ts +0 -0
  44. /package/dist/{generator → web/src/generator}/gherkin-validate.d.ts +0 -0
  45. /package/dist/{generator → web/src/generator}/lint.d.ts +0 -0
  46. /package/dist/{generator → web/src/generator}/pom-scan.d.ts +0 -0
  47. /package/dist/{generator → web/src/generator}/promote.d.ts +0 -0
  48. /package/dist/{generator → web/src/generator}/selector-rules.d.ts +0 -0
  49. /package/dist/{generator → web/src/generator}/typecheck.d.ts +0 -0
  50. /package/dist/{index.d.ts → web/src/index.d.ts} +0 -0
  51. /package/dist/{trace-normalizer → web/src/trace-normalizer}/normalize.d.ts +0 -0
  52. /package/dist/{trace-normalizer → web/src/trace-normalizer}/parse.d.ts +0 -0
  53. /package/dist/{trace-normalizer → web/src/trace-normalizer}/scrub.d.ts +0 -0
  54. /package/dist/{trace-normalizer → web/src/trace-normalizer}/unzip.d.ts +0 -0
@@ -0,0 +1,28 @@
1
+ export interface JiraTicket {
2
+ key: string;
3
+ summary: string;
4
+ story: string;
5
+ acceptanceCriteria?: string;
6
+ attachments: Array<{
7
+ filename: string;
8
+ url: string;
9
+ }>;
10
+ raw: Record<string, unknown>;
11
+ }
12
+ export interface JiraFieldMap {
13
+ story: string;
14
+ acceptanceCriteria?: string;
15
+ }
16
+ export interface JiraClient {
17
+ readonly backend: 'mcp' | 'rest';
18
+ fetchTicket(key: string, fields: JiraFieldMap): Promise<JiraTicket>;
19
+ postComment(key: string, body: string): Promise<{
20
+ id: string;
21
+ }>;
22
+ transitionStatus(key: string, statusName: string): Promise<void>;
23
+ listFields(sampleKey: string): Promise<Array<{
24
+ id: string;
25
+ name: string;
26
+ hasContent: boolean;
27
+ }>>;
28
+ }
@@ -0,0 +1,11 @@
1
+ export interface LockData {
2
+ pid: number;
3
+ hostname: string;
4
+ started_at: string;
5
+ run_id: string;
6
+ }
7
+ export declare function acquireLock(path: string, runId: string): boolean;
8
+ export declare function releaseLock(path: string): void;
9
+ export declare function readLock(path: string): LockData | null;
10
+ export declare function isLockStale(path: string): boolean;
11
+ export declare function forceUnlock(path: string): void;
@@ -0,0 +1,10 @@
1
+ export interface LogEntry {
2
+ ts: string;
3
+ [key: string]: unknown;
4
+ }
5
+ export declare class NdjsonLogger {
6
+ private readonly path;
7
+ constructor(path: string);
8
+ log(payload: Record<string, unknown>): void;
9
+ static readAll(path: string): LogEntry[];
10
+ }
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  // @bun
2
2
  var __require = import.meta.require;
3
3
 
4
+ // src/adapter.ts
5
+ import { join as join3 } from "path";
6
+
4
7
  // src/executor/index.ts
5
8
  import { join } from "path";
6
9
 
@@ -29,7 +32,10 @@ async function runPlaywright(input) {
29
32
  outputDir: input.outputDir
30
33
  });
31
34
  const spawn = input.spawn ?? defaultSpawn;
32
- const { exitCode } = await spawn("npx", ["playwright", ...args], { ...process.env, ...input.env });
35
+ const { exitCode } = await spawn("npx", ["playwright", ...args], {
36
+ ...process.env,
37
+ ...input.env
38
+ });
33
39
  return {
34
40
  outcome: exitCode === 0 ? "PASS" : "FAIL",
35
41
  rawReportPath: join(input.outputDir, "report.json"),
@@ -38,7 +44,7 @@ async function runPlaywright(input) {
38
44
  }
39
45
 
40
46
  // src/trace-normalizer/normalize.ts
41
- import { readFileSync as readFileSync2, existsSync, writeFileSync } from "fs";
47
+ import { existsSync, readFileSync as readFileSync2, writeFileSync } from "fs";
42
48
  import { join as join2 } from "path";
43
49
 
44
50
  // src/trace-normalizer/parse.ts
@@ -100,8 +106,12 @@ var SENSITIVE_BODY_KEYS = [
100
106
  ];
101
107
  var JWT_RE = /\beyJ[A-Za-z0-9_-]{7,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{5,}\b/;
102
108
  var CREDIT_CARD_RE = /\b(?:\d{4}[-\s]?){3}\d{4}\b/;
109
+ var EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
110
+ var PHONE_RE = /(?:\+?\d[\d\s().-]{6,}\d)/;
103
111
  var JWT_RE_G = new RegExp(JWT_RE.source, "g");
104
112
  var CREDIT_CARD_RE_G = new RegExp(CREDIT_CARD_RE.source, "g");
113
+ var EMAIL_RE_G = new RegExp(EMAIL_RE.source, "g");
114
+ var PHONE_RE_G = new RegExp(PHONE_RE.source, "g");
105
115
  var REDACTED = "[REDACTED]";
106
116
  function scrubHeaders(headers) {
107
117
  const out = {};
@@ -129,7 +139,7 @@ function scrubBodyJson(body) {
129
139
  return body;
130
140
  }
131
141
  function scrubFreeText(s) {
132
- return s.replace(JWT_RE_G, REDACTED).replace(CREDIT_CARD_RE_G, REDACTED);
142
+ return s.replace(JWT_RE_G, REDACTED).replace(CREDIT_CARD_RE_G, REDACTED).replace(EMAIL_RE_G, REDACTED).replace(PHONE_RE_G, REDACTED);
133
143
  }
134
144
 
135
145
  // src/trace-normalizer/scrub.ts
@@ -250,7 +260,6 @@ async function normalizeRun(input) {
250
260
  }
251
261
 
252
262
  // src/adapter.ts
253
- import { join as join3 } from "path";
254
263
  var WebAdapter = {
255
264
  id: "web",
256
265
  async generate(_input) {
@@ -282,7 +291,11 @@ var WebAdapter = {
282
291
  await import("@playwright/test");
283
292
  checks.push({ name: "@playwright/test installed", ok: true });
284
293
  } catch {
285
- checks.push({ name: "@playwright/test installed", ok: false, message: "Run `bun add -D @playwright/test`." });
294
+ checks.push({
295
+ name: "@playwright/test installed",
296
+ ok: false,
297
+ message: "Run `bun add -D @playwright/test`."
298
+ });
286
299
  }
287
300
  return { ok: checks.every((c) => c.ok), checks };
288
301
  }
@@ -319,7 +332,7 @@ async function runAuthSetup(input) {
319
332
  }
320
333
  }
321
334
  // src/auth-setup/playwright-state.ts
322
- import { writeFileSync as writeFileSync2, mkdirSync } from "fs";
335
+ import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
323
336
  import { join as join4 } from "path";
324
337
  import { readAuthState } from "@xera-ai/core";
325
338
  function stagePlaywrightState(authDir, role) {
@@ -394,7 +407,7 @@ async function typecheckTicket(ticketDir) {
394
407
  };
395
408
  }
396
409
  // src/generator/lint.ts
397
- import { readFileSync as readFileSync3, existsSync as existsSync3, readdirSync } from "fs";
410
+ import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
398
411
  import { join as join6 } from "path";
399
412
 
400
413
  // src/generator/selector-rules.ts
@@ -410,16 +423,31 @@ function lintSelectors(source) {
410
423
  const text = lines[i];
411
424
  const prev = lines[i - 1] ?? "";
412
425
  if (XPATH_RE.test(text)) {
413
- warnings.push({ rule: "no-xpath", line: i + 1, text, message: "XPath selectors are forbidden in v0.1." });
426
+ warnings.push({
427
+ rule: "no-xpath",
428
+ line: i + 1,
429
+ text,
430
+ message: "XPath selectors are forbidden in v0.1."
431
+ });
414
432
  continue;
415
433
  }
416
434
  const cssMatch = LOCATOR_CSS_RE.exec(text);
417
435
  if (cssMatch) {
418
436
  const sel = cssMatch[1];
419
437
  if (AUTO_CLASS_RE.test(sel)) {
420
- warnings.push({ rule: "no-auto-classname", line: i + 1, text, message: `Auto-generated class name "${sel}" \u2014 refactor to role/label/test-id.` });
438
+ warnings.push({
439
+ rule: "no-auto-classname",
440
+ line: i + 1,
441
+ text,
442
+ message: `Auto-generated class name "${sel}" \u2014 refactor to role/label/test-id.`
443
+ });
421
444
  } else if (!ALLOW_CSS_RE.test(prev)) {
422
- warnings.push({ rule: "prefer-role-over-css", line: i + 1, text, message: `Prefer getByRole/getByLabel over CSS "${sel}". If unavoidable, add "// xera-allow-css: <reason>" on the previous line.` });
445
+ warnings.push({
446
+ rule: "prefer-role-over-css",
447
+ line: i + 1,
448
+ text,
449
+ message: `Prefer getByRole/getByLabel over CSS "${sel}". If unavoidable, add "// xera-allow-css: <reason>" on the previous line.`
450
+ });
423
451
  }
424
452
  }
425
453
  }
@@ -452,7 +480,7 @@ async function lintTicket(ticketDir) {
452
480
  return { ok: warnings.length === 0, warnings };
453
481
  }
454
482
  // src/generator/pom-scan.ts
455
- import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
483
+ import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
456
484
  import { join as join7 } from "path";
457
485
  var CLASS_RE = /export\s+class\s+([A-Z][A-Za-z0-9_]*)/g;
458
486
  function scanSharedPoms(repoRoot) {
@@ -472,7 +500,7 @@ function scanSharedPoms(repoRoot) {
472
500
  return found;
473
501
  }
474
502
  // src/generator/promote.ts
475
- import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3, renameSync } from "fs";
503
+ import { existsSync as existsSync5, readFileSync as readFileSync5, renameSync, writeFileSync as writeFileSync3 } from "fs";
476
504
  import { join as join8 } from "path";
477
505
  async function promotePom(input) {
478
506
  const fromDir = join8(input.repoRoot, ".xera", input.ticket, "page-objects");
@@ -516,6 +544,10 @@ export {
516
544
  WebAdapter,
517
545
  SENSITIVE_HEADERS,
518
546
  SENSITIVE_BODY_KEYS,
547
+ PHONE_RE_G,
548
+ PHONE_RE,
519
549
  JWT_RE,
550
+ EMAIL_RE_G,
551
+ EMAIL_RE,
520
552
  CREDIT_CARD_RE
521
553
  };
@@ -2,6 +2,10 @@ export declare const SENSITIVE_HEADERS: readonly string[];
2
2
  export declare const SENSITIVE_BODY_KEYS: readonly RegExp[];
3
3
  export declare const JWT_RE: RegExp;
4
4
  export declare const CREDIT_CARD_RE: RegExp;
5
+ export declare const EMAIL_RE: RegExp;
6
+ export declare const PHONE_RE: RegExp;
7
+ export declare const EMAIL_RE_G: RegExp;
8
+ export declare const PHONE_RE_G: RegExp;
5
9
  export declare function scrubHeaders(headers: Record<string, string>): Record<string, string>;
6
10
  export declare function scrubBodyJson(body: unknown): unknown;
7
11
  export declare function scrubFreeText(s: string): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xera-ai/web",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
package/src/adapter.ts CHANGED
@@ -1,7 +1,14 @@
1
+ import { join } from 'node:path';
2
+ import type {
3
+ DoctorReport,
4
+ ExecuteInput,
5
+ GenerateInput,
6
+ GenerateResult,
7
+ RunResult,
8
+ TestAdapter,
9
+ } from '@xera-ai/core/adapter';
1
10
  import { runPlaywright } from './executor';
2
11
  import { normalizeRun } from './trace-normalizer/normalize';
3
- import type { TestAdapter, GenerateInput, GenerateResult, ExecuteInput, RunResult, DoctorReport } from '@xera-ai/core/adapter';
4
- import { join } from 'node:path';
5
12
 
6
13
  export const WebAdapter: TestAdapter = {
7
14
  id: 'web',
@@ -22,7 +29,7 @@ export const WebAdapter: TestAdapter = {
22
29
  return {
23
30
  runId: input.runId,
24
31
  outcome: normalized.outcome,
25
- scenarios: normalized.scenarios.map(s => {
32
+ scenarios: normalized.scenarios.map((s) => {
26
33
  const out: RunResult['scenarios'][number] = { name: s.name, outcome: s.outcome };
27
34
  if (s.failure !== undefined) out.failure = s.failure;
28
35
  return out;
@@ -39,8 +46,12 @@ export const WebAdapter: TestAdapter = {
39
46
  await import('@playwright/test');
40
47
  checks.push({ name: '@playwright/test installed', ok: true });
41
48
  } catch {
42
- checks.push({ name: '@playwright/test installed', ok: false, message: 'Run `bun add -D @playwright/test`.' });
49
+ checks.push({
50
+ name: '@playwright/test installed',
51
+ ok: false,
52
+ message: 'Run `bun add -D @playwright/test`.',
53
+ });
43
54
  }
44
- return { ok: checks.every(c => c.ok), checks };
55
+ return { ok: checks.every((c) => c.ok), checks };
45
56
  },
46
57
  };
@@ -1,4 +1,4 @@
1
- import { writeFileSync, mkdirSync } from 'node:fs';
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { readAuthState } from '@xera-ai/core';
4
4
 
@@ -1,5 +1,5 @@
1
- import type { Browser } from '@playwright/test';
2
1
  import { pathToFileURL } from 'node:url';
2
+ import type { Browser } from '@playwright/test';
3
3
  import { writeAuthState } from '@xera-ai/core';
4
4
  import type { AuthRoleCreds } from './define';
5
5
 
@@ -33,7 +33,10 @@ export async function runPlaywright(input: RunPlaywrightInput): Promise<RunPlayw
33
33
  outputDir: input.outputDir,
34
34
  });
35
35
  const spawn = input.spawn ?? defaultSpawn;
36
- const { exitCode } = await spawn('npx', ['playwright', ...args], { ...process.env, ...input.env });
36
+ const { exitCode } = await spawn('npx', ['playwright', ...args], {
37
+ ...process.env,
38
+ ...input.env,
39
+ });
37
40
  return {
38
41
  outcome: exitCode === 0 ? 'PASS' : 'FAIL',
39
42
  rawReportPath: join(input.outputDir, 'report.json'),
@@ -1,6 +1,6 @@
1
- import { readFileSync, existsSync, readdirSync } from 'node:fs';
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { lintSelectors, type SelectorWarning } from './selector-rules';
3
+ import { type SelectorWarning, lintSelectors } from './selector-rules';
4
4
 
5
5
  export interface LintResult {
6
6
  ok: boolean;
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync, readFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
 
4
4
  const CLASS_RE = /export\s+class\s+([A-Z][A-Za-z0-9_]*)/g;
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
1
+ import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
 
4
4
  export interface PromoteInput {
@@ -18,7 +18,9 @@ export async function promotePom(input: PromoteInput): Promise<void> {
18
18
  throw new Error(`POM ${file} not found at ${fromPath}`);
19
19
  }
20
20
  if (existsSync(toPath)) {
21
- throw new Error(`POM ${file} already exists at ${toPath}. Reconcile manually before promoting.`);
21
+ throw new Error(
22
+ `POM ${file} already exists at ${toPath}. Reconcile manually before promoting.`,
23
+ );
22
24
  }
23
25
 
24
26
  renameSync(fromPath, toPath);
@@ -17,16 +17,31 @@ export function lintSelectors(source: string): { warnings: SelectorWarning[] } {
17
17
  const text = lines[i]!;
18
18
  const prev = lines[i - 1] ?? '';
19
19
  if (XPATH_RE.test(text)) {
20
- warnings.push({ rule: 'no-xpath', line: i + 1, text, message: 'XPath selectors are forbidden in v0.1.' });
20
+ warnings.push({
21
+ rule: 'no-xpath',
22
+ line: i + 1,
23
+ text,
24
+ message: 'XPath selectors are forbidden in v0.1.',
25
+ });
21
26
  continue;
22
27
  }
23
28
  const cssMatch = LOCATOR_CSS_RE.exec(text);
24
29
  if (cssMatch) {
25
30
  const sel = cssMatch[1]!;
26
31
  if (AUTO_CLASS_RE.test(sel)) {
27
- warnings.push({ rule: 'no-auto-classname', line: i + 1, text, message: `Auto-generated class name "${sel}" — refactor to role/label/test-id.` });
32
+ warnings.push({
33
+ rule: 'no-auto-classname',
34
+ line: i + 1,
35
+ text,
36
+ message: `Auto-generated class name "${sel}" — refactor to role/label/test-id.`,
37
+ });
28
38
  } else if (!ALLOW_CSS_RE.test(prev)) {
29
- warnings.push({ rule: 'prefer-role-over-css', line: i + 1, text, message: `Prefer getByRole/getByLabel over CSS "${sel}". If unavoidable, add "// xera-allow-css: <reason>" on the previous line.` });
39
+ warnings.push({
40
+ rule: 'prefer-role-over-css',
41
+ line: i + 1,
42
+ text,
43
+ message: `Prefer getByRole/getByLabel over CSS "${sel}". If unavoidable, add "// xera-allow-css: <reason>" on the previous line.`,
44
+ });
30
45
  }
31
46
  }
32
47
  }
@@ -1,7 +1,7 @@
1
- import { readFileSync, existsSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { parsePlaywrightReport } from './parse';
4
- import { scrub, type NormalizedNetworkEntry, type NormalizedRun } from './scrub';
4
+ import { type NormalizedNetworkEntry, type NormalizedRun, scrub } from './scrub';
5
5
  import { unzipTrace } from './unzip';
6
6
 
7
7
  export interface NormalizeRunInput {
@@ -20,7 +20,10 @@ interface TraceNetworkEntry {
20
20
  responseBody?: unknown;
21
21
  }
22
22
 
23
- interface TraceConsoleEntry { type: 'console'; text: string; }
23
+ interface TraceConsoleEntry {
24
+ type: 'console';
25
+ text: string;
26
+ }
24
27
 
25
28
  export async function normalizeRun(input: NormalizeRunInput): Promise<NormalizedRun> {
26
29
  const reportPath = join(input.runDir, 'report.json');
@@ -34,17 +37,27 @@ export async function normalizeRun(input: NormalizeRunInput): Promise<Normalized
34
37
  const networkFile = Object.entries(files).find(([k]) => k.endsWith('.network'))?.[1];
35
38
  const traceFile = Object.entries(files).find(([k]) => k.endsWith('.trace'))?.[1];
36
39
  const network: TraceNetworkEntry[] = networkFile
37
- ? networkFile.trim().split('\n').filter(Boolean).map(l => JSON.parse(l)).filter((e: any) => e.type === 'request')
40
+ ? networkFile
41
+ .trim()
42
+ .split('\n')
43
+ .filter(Boolean)
44
+ .map((l) => JSON.parse(l))
45
+ .filter((e: any) => e.type === 'request')
38
46
  : [];
39
47
  const consoleEvents: TraceConsoleEntry[] = traceFile
40
- ? traceFile.trim().split('\n').filter(Boolean).map(l => JSON.parse(l)).filter((e: any) => e.type === 'console')
48
+ ? traceFile
49
+ .trim()
50
+ .split('\n')
51
+ .filter(Boolean)
52
+ .map((l) => JSON.parse(l))
53
+ .filter((e: any) => e.type === 'console')
41
54
  : [];
42
55
 
43
56
  // Attach to each failing scenario (all entries — v0.1 doesn't yet correlate by step time)
44
57
  for (const sc of normalized.scenarios) {
45
58
  if (sc.outcome !== 'FAIL') continue;
46
59
  sc.failure = sc.failure ?? {};
47
- sc.failure.networkAtFailure = network.map(n => {
60
+ sc.failure.networkAtFailure = network.map((n) => {
48
61
  const entry: NormalizedNetworkEntry = { method: n.method, url: n.url, status: n.status };
49
62
  if (n.requestHeaders !== undefined) entry.requestHeaders = n.requestHeaders;
50
63
  if (n.responseHeaders !== undefined) entry.responseHeaders = n.responseHeaders;
@@ -52,7 +65,7 @@ export async function normalizeRun(input: NormalizeRunInput): Promise<Normalized
52
65
  if (n.responseBody !== undefined) entry.responseBody = n.responseBody;
53
66
  return entry;
54
67
  });
55
- sc.failure.consoleAtFailure = consoleEvents.map(c => c.text);
68
+ sc.failure.consoleAtFailure = consoleEvents.map((c) => c.text);
56
69
  }
57
70
  }
58
71
 
@@ -1,11 +1,33 @@
1
1
  import type { NormalizedRun, NormalizedScenario } from './scrub';
2
2
 
3
- interface PWAttachment { name: string; path?: string; contentType?: string; }
4
- interface PWResult { status: string; duration: number; error?: { message?: string; stack?: string }; attachments?: PWAttachment[]; }
5
- interface PWTest { results: PWResult[]; }
6
- interface PWSpec { title: string; ok: boolean; tests: PWTest[]; }
7
- interface PWSuite { title: string; specs?: PWSpec[]; suites?: PWSuite[]; }
8
- interface PWReport { stats: { unexpected: number }; suites: PWSuite[]; }
3
+ interface PWAttachment {
4
+ name: string;
5
+ path?: string;
6
+ contentType?: string;
7
+ }
8
+ interface PWResult {
9
+ status: string;
10
+ duration: number;
11
+ error?: { message?: string; stack?: string };
12
+ attachments?: PWAttachment[];
13
+ }
14
+ interface PWTest {
15
+ results: PWResult[];
16
+ }
17
+ interface PWSpec {
18
+ title: string;
19
+ ok: boolean;
20
+ tests: PWTest[];
21
+ }
22
+ interface PWSuite {
23
+ title: string;
24
+ specs?: PWSpec[];
25
+ suites?: PWSuite[];
26
+ }
27
+ interface PWReport {
28
+ stats: { unexpected: number };
29
+ suites: PWSuite[];
30
+ }
9
31
 
10
32
  function* flatSpecs(suites: PWSuite[]): Generator<PWSpec> {
11
33
  for (const s of suites) {
@@ -18,13 +40,16 @@ export function parsePlaywrightReport(report: PWReport, runId: string): Normaliz
18
40
  const scenarios: NormalizedScenario[] = [];
19
41
  for (const spec of flatSpecs(report.suites)) {
20
42
  const lastResult = spec.tests[0]?.results[0];
21
- const outcome: 'PASS' | 'FAIL' | 'SKIPPED' =
22
- !lastResult ? 'SKIPPED' :
23
- lastResult.status === 'passed' ? 'PASS' :
24
- lastResult.status === 'skipped' ? 'SKIPPED' : 'FAIL';
43
+ const outcome: 'PASS' | 'FAIL' | 'SKIPPED' = !lastResult
44
+ ? 'SKIPPED'
45
+ : lastResult.status === 'passed'
46
+ ? 'PASS'
47
+ : lastResult.status === 'skipped'
48
+ ? 'SKIPPED'
49
+ : 'FAIL';
25
50
  const sc: NormalizedScenario = { name: spec.title, outcome };
26
51
  if (outcome === 'FAIL' && lastResult) {
27
- const screenshot = lastResult.attachments?.find(a => a.name === 'screenshot')?.path;
52
+ const screenshot = lastResult.attachments?.find((a) => a.name === 'screenshot')?.path;
28
53
  const failure: NormalizedScenario['failure'] = {};
29
54
  if (lastResult.error?.message !== undefined) failure!.errorMessage = lastResult.error.message;
30
55
  if (screenshot !== undefined) failure!.screenshotPath = screenshot;
@@ -24,9 +24,14 @@ export const SENSITIVE_BODY_KEYS: readonly RegExp[] = [
24
24
 
25
25
  export const JWT_RE = /\beyJ[A-Za-z0-9_-]{7,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{5,}\b/;
26
26
  export const CREDIT_CARD_RE = /\b(?:\d{4}[-\s]?){3}\d{4}\b/;
27
+ export const EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
28
+ // E.164-ish phone with optional + and separators. Conservative: require at least 7 digits.
29
+ export const PHONE_RE = /(?:\+?\d[\d\s().-]{6,}\d)/;
27
30
 
28
31
  const JWT_RE_G = new RegExp(JWT_RE.source, 'g');
29
32
  const CREDIT_CARD_RE_G = new RegExp(CREDIT_CARD_RE.source, 'g');
33
+ export const EMAIL_RE_G = new RegExp(EMAIL_RE.source, 'g');
34
+ export const PHONE_RE_G = new RegExp(PHONE_RE.source, 'g');
30
35
 
31
36
  const REDACTED = '[REDACTED]';
32
37
 
@@ -43,7 +48,7 @@ export function scrubBodyJson(body: unknown): unknown {
43
48
  if (body && typeof body === 'object') {
44
49
  const out: Record<string, unknown> = {};
45
50
  for (const [k, v] of Object.entries(body)) {
46
- if (SENSITIVE_BODY_KEYS.some(re => re.test(k))) {
51
+ if (SENSITIVE_BODY_KEYS.some((re) => re.test(k))) {
47
52
  out[k] = REDACTED;
48
53
  } else {
49
54
  out[k] = scrubBodyJson(v);
@@ -56,5 +61,9 @@ export function scrubBodyJson(body: unknown): unknown {
56
61
  }
57
62
 
58
63
  export function scrubFreeText(s: string): string {
59
- return s.replace(JWT_RE_G, REDACTED).replace(CREDIT_CARD_RE_G, REDACTED);
64
+ return s
65
+ .replace(JWT_RE_G, REDACTED)
66
+ .replace(CREDIT_CARD_RE_G, REDACTED)
67
+ .replace(EMAIL_RE_G, REDACTED)
68
+ .replace(PHONE_RE_G, REDACTED);
60
69
  }
@@ -1,4 +1,4 @@
1
- import { scrubHeaders, scrubBodyJson, scrubFreeText } from './scrub-rules';
1
+ import { scrubBodyJson, scrubFreeText, scrubHeaders } from './scrub-rules';
2
2
 
3
3
  export interface NormalizedNetworkEntry {
4
4
  method: string;
@@ -38,7 +38,10 @@ function countScrubbed(before: unknown, after: unknown): number {
38
38
  if (before && after && typeof before === 'object' && typeof after === 'object') {
39
39
  let n = 0;
40
40
  for (const k of Object.keys(before as Record<string, unknown>)) {
41
- n += countScrubbed((before as Record<string, unknown>)[k], (after as Record<string, unknown>)[k]);
41
+ n += countScrubbed(
42
+ (before as Record<string, unknown>)[k],
43
+ (after as Record<string, unknown>)[k],
44
+ );
42
45
  }
43
46
  return n;
44
47
  }
@@ -65,7 +68,7 @@ export function scrub(run: NormalizedRun): NormalizedRun {
65
68
  );
66
69
  }
67
70
  if (f.networkAtFailure) {
68
- newF.networkAtFailure = f.networkAtFailure.map(n => {
71
+ newF.networkAtFailure = f.networkAtFailure.map((n) => {
69
72
  const reqHeaders = n.requestHeaders ? scrubHeaders(n.requestHeaders) : undefined;
70
73
  const resHeaders = n.responseHeaders ? scrubHeaders(n.responseHeaders) : undefined;
71
74
  const reqBody = n.requestBody !== undefined ? scrubBodyJson(n.requestBody) : undefined;
@@ -12,7 +12,12 @@ export function unzipTrace(tracePath: string): TraceEntries {
12
12
  const files: Record<string, string> = {};
13
13
  for (const [name, data] of Object.entries(entries)) {
14
14
  if (name.endsWith('/')) continue;
15
- if (name.endsWith('.network') || name.endsWith('.trace') || name.endsWith('.txt') || name.endsWith('.json')) {
15
+ if (
16
+ name.endsWith('.network') ||
17
+ name.endsWith('.trace') ||
18
+ name.endsWith('.txt') ||
19
+ name.endsWith('.json')
20
+ ) {
16
21
  files[name] = new TextDecoder().decode(data);
17
22
  }
18
23
  }
File without changes
File without changes
File without changes
File without changes