@empiricalrun/playwright-utils 0.58.4 → 0.58.6

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 (36) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/api-worker-client.d.ts +17 -0
  3. package/dist/api-worker-client.d.ts.map +1 -1
  4. package/dist/api-worker-client.js +34 -2
  5. package/dist/generated/api-worker-openapi.d.ts +96 -177
  6. package/dist/generated/api-worker-openapi.d.ts.map +1 -1
  7. package/dist/overlay-tests/cache.spec.js +28 -4
  8. package/dist/reporter/harness.d.ts +4 -0
  9. package/dist/reporter/harness.d.ts.map +1 -1
  10. package/dist/reporter/harness.js +20 -8
  11. package/dist/reporter/ibr-utils.d.ts +3 -0
  12. package/dist/reporter/ibr-utils.d.ts.map +1 -1
  13. package/dist/reporter/ibr-utils.js +25 -18
  14. package/dist/reporter/incremental-blob-reporter.d.ts +1 -0
  15. package/dist/reporter/incremental-blob-reporter.d.ts.map +1 -1
  16. package/dist/reporter/incremental-blob-reporter.js +5 -4
  17. package/dist/reporter/lifecycle-events.d.ts +15 -5
  18. package/dist/reporter/lifecycle-events.d.ts.map +1 -1
  19. package/dist/reporter/lifecycle-events.js +91 -64
  20. package/dist/reporter/parity-utils.d.ts +21 -0
  21. package/dist/reporter/parity-utils.d.ts.map +1 -1
  22. package/dist/reporter/parity-utils.js +73 -1
  23. package/dist/reporter/test-case-sync.d.ts +2 -25
  24. package/dist/reporter/test-case-sync.d.ts.map +1 -1
  25. package/dist/reporter/uploader.d.ts.map +1 -1
  26. package/dist/reporter/uploader.js +22 -1
  27. package/dist/test/expect/visual.d.ts.map +1 -1
  28. package/dist/test/expect/visual.js +4 -1
  29. package/dist/test/scripts/pw-locator-patch/dismiss-overlays/index.d.ts.map +1 -1
  30. package/dist/test/scripts/pw-locator-patch/dismiss-overlays/index.js +5 -0
  31. package/dist/test/scripts/pw-locator-patch/vision/query.d.ts.map +1 -1
  32. package/dist/test/scripts/pw-locator-patch/vision/query.js +4 -1
  33. package/package.json +5 -5
  34. package/upstream-oracle/README.md +36 -7
  35. package/upstream-oracle/ibr-adapter.js +60 -5
  36. package/upstream-oracle/run.mjs +84 -7
@@ -1 +1 @@
1
- {"version":3,"file":"parity-utils.d.ts","sourceRoot":"","sources":["../../src/reporter/parity-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAiD3C,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,WAAW,EAAE,oBAAoB,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACtD,OAAO,EAAE,gBAAgB,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,GAAG,EAAE,MAAM,CAAC;IACZ,qEAAqE;IACrE,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC;AAwED,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,IAAI,EAAE,gBAAgB,GACrB,gBAAgB,CAclB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CA+BzE;AAED,6EAA6E;AAC7E,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,SAAS,EAAE,EACtB,SAAS,EAAE,SAAS,EAAE,GACrB,MAAM,EAAE,CASV"}
1
+ {"version":3,"file":"parity-utils.d.ts","sourceRoot":"","sources":["../../src/reporter/parity-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAiD3C,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,WAAW,EAAE,oBAAoB,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACtD,OAAO,EAAE,gBAAgB,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,GAAG,EAAE,MAAM,CAAC;IACZ,qEAAqE;IACrE,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC;AAwED,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,IAAI,EAAE,gBAAgB,GACrB,gBAAgB,CAclB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAwCzE;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,SAAS,EAAE,GAClB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAgDvB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG;IACtD,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC,CAYA;AAED,6EAA6E;AAC7E,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,SAAS,EAAE,EACtB,SAAS,EAAE,SAAS,EAAE,GACrB,MAAM,EAAE,CASV"}
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.normalizeJsonReport = normalizeJsonReport;
4
4
  exports.stepSignatures = stepSignatures;
5
+ exports.stepAttachmentsAtStepEnd = stepAttachmentsAtStepEnd;
6
+ exports.configAndProjects = configAndProjects;
5
7
  exports.missingEventMethods = missingEventMethods;
6
8
  const ANSI_RE = /\u001b\[[0-9;]*m/g;
7
9
  function normalizeText(text, cwd) {
@@ -93,11 +95,81 @@ function stepSignatures(events) {
93
95
  continue;
94
96
  }
95
97
  const list = signatures.get(key) ?? [];
96
- list.push(`${params.step.category}:${params.step.title}`);
98
+ // The location is part of the signature: it travels relative to rootDir
99
+ // and the merged HTML report renders it as-is.
100
+ const location = params.step.location
101
+ ? `${params.step.location.file}:${params.step.location.line}`
102
+ : "";
103
+ list.push(`${params.step.category}:${params.step.title}@${location}`);
97
104
  signatures.set(key, list);
98
105
  }
99
106
  return signatures;
100
107
  }
108
+ /**
109
+ * What a reporter would see for `step.attachments` DURING onStepEnd, replayed
110
+ * from the raw event stream the way teleReceiver does it: onAttach pushes onto
111
+ * `result.attachments`, and a step end carries indices into that array
112
+ * (TeleTestStep.attachments resolves them at access time). An attachment that
113
+ * only arrives at test end therefore reads as `<missing>` here.
114
+ *
115
+ * Keyed by `testId:retry`; each entry is one `title -> [names]` line per
116
+ * onStepEnd, in stream order.
117
+ */
118
+ function stepAttachmentsAtStepEnd(events) {
119
+ const resultKey = new Map(); // resultId -> testId:retry
120
+ const attachmentNames = new Map(); // resultId -> names so far
121
+ const stepTitles = new Map(); // stepId -> title
122
+ const views = new Map();
123
+ for (const event of events) {
124
+ if (event.method === "onTestBegin") {
125
+ const params = event.params;
126
+ resultKey.set(params.result.id, `${params.testId}:${params.result.retry}`);
127
+ attachmentNames.set(params.result.id, []);
128
+ }
129
+ else if (event.method === "onAttach") {
130
+ const params = event.params;
131
+ const names = attachmentNames.get(params.resultId);
132
+ names?.push(...params.attachments.map((a) => a.name));
133
+ }
134
+ else if (event.method === "onStepBegin") {
135
+ const params = event.params;
136
+ stepTitles.set(params.step.id, params.step.title);
137
+ }
138
+ else if (event.method === "onStepEnd") {
139
+ const params = event.params;
140
+ const key = resultKey.get(params.resultId);
141
+ if (!key) {
142
+ continue;
143
+ }
144
+ const names = attachmentNames.get(params.resultId) ?? [];
145
+ const resolved = (params.step.attachments ?? []).map((index) => names[index] ?? "<missing>");
146
+ const list = views.get(key) ?? [];
147
+ list.push(`${stepTitles.get(params.step.id) ?? "?"} -> [${resolved.join(", ")}]`);
148
+ views.set(key, list);
149
+ }
150
+ }
151
+ return views;
152
+ }
153
+ /**
154
+ * The onConfigure config payload plus every onProject project payload, keyed
155
+ * by project name. The merged report's FullConfig/FullProject is rebuilt from
156
+ * exactly these, and normalizeJsonReport compares only the suite/stats body —
157
+ * so this is where config and project serialization drift shows up.
158
+ */
159
+ function configAndProjects(events) {
160
+ let config;
161
+ const projects = {};
162
+ for (const event of events) {
163
+ if (event.method === "onConfigure") {
164
+ config = event.params.config;
165
+ }
166
+ else if (event.method === "onProject") {
167
+ const project = event.params.project;
168
+ projects[project.name] = project;
169
+ }
170
+ }
171
+ return { config, projects };
172
+ }
101
173
  /** Event method names present in `reference` but absent from `candidate`. */
102
174
  function missingEventMethods(reference, candidate) {
103
175
  const candidateMethods = new Set(candidate.map((e) => e.method));
@@ -1,28 +1,5 @@
1
+ import type { TestCaseSyncManifest } from "@empiricalrun/shared-types/playwright-utils";
1
2
  import type { FullConfig, Suite } from "@playwright/test/reporter";
2
- export interface TestCaseSyncTestCase {
3
- id: string;
4
- name: string;
5
- file_path: string;
6
- suites: string[];
7
- playwright_project: string;
8
- tags: string[];
9
- }
10
- export interface TestCaseDependencyGroupMember {
11
- test_case_id: string;
12
- dependency_order: number;
13
- }
14
- export interface TestCaseDependencyGroup {
15
- id: string;
16
- kind: "playwright_serial";
17
- file_path: string;
18
- title_path: string[];
19
- playwright_project: string;
20
- members: TestCaseDependencyGroupMember[];
21
- }
22
- export interface TestCaseSyncManifest {
23
- version: 1;
24
- test_cases: TestCaseSyncTestCase[];
25
- dependency_groups: TestCaseDependencyGroup[];
26
- }
3
+ export type { TestCaseDependencyGroup, TestCaseDependencyGroupMember, TestCaseSyncManifest, TestCaseSyncTestCase, } from "@empiricalrun/shared-types/playwright-utils";
27
4
  export declare function collectTestCaseSyncManifest(config: FullConfig, rootSuite: Suite): TestCaseSyncManifest;
28
5
  //# sourceMappingURL=test-case-sync.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"test-case-sync.d.ts","sourceRoot":"","sources":["../../src/reporter/test-case-sync.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EAEV,KAAK,EAEN,MAAM,2BAA2B,CAAC;AAMnC,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,6BAA6B;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,mBAAmB,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,OAAO,EAAE,6BAA6B,EAAE,CAAC;CAC1C;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,CAAC,CAAC;IACX,UAAU,EAAE,oBAAoB,EAAE,CAAC;IACnC,iBAAiB,EAAE,uBAAuB,EAAE,CAAC;CAC9C;AA+JD,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,UAAU,EAClB,SAAS,EAAE,KAAK,GACf,oBAAoB,CAuBtB"}
1
+ {"version":3,"file":"test-case-sync.d.ts","sourceRoot":"","sources":["../../src/reporter/test-case-sync.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,oBAAoB,EAErB,MAAM,6CAA6C,CAAC;AACrD,OAAO,KAAK,EACV,UAAU,EAEV,KAAK,EAEN,MAAM,2BAA2B,CAAC;AAUnC,YAAY,EACV,uBAAuB,EACvB,6BAA6B,EAC7B,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,6CAA6C,CAAC;AA+JrD,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,UAAU,EAClB,SAAS,EAAE,KAAK,GACf,oBAAoB,CAuBtB"}
@@ -1 +1 @@
1
- {"version":3,"file":"uploader.d.ts","sourceRoot":"","sources":["../../src/reporter/uploader.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,kBAAkB,EAElB,kBAAkB,EAEnB,MAAM,2BAA2B,CAAC;AAKnC,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,kBAAkB,CAAC;CACjC;AAED,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,YAAY,CAAqB;IAEzC,YAAY,MAAM,EAAE,cAAc,EASjC;IAED,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAgBhC,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAUhC,MAAM,CAAC,qBAAqB,CAAC,QAAQ,EAAE,OAAO,GAAG,kBAAkB,GAAG,IAAI,CAKzE;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,IAAI,SAAS,IAAI,MAAM,CAEtB;IAEK,UAAU,CACd,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,MAAM,EACvB,YAAY,CAAC,EAAE,kBAAkB,GAChC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgCxB;IAEK,eAAe,CACnB,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,IAAI,CAAC,CAef;IAEK,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAEpC;CACF;AAID,wBAAgB,WAAW,IAAI,QAAQ,GAAG,IAAI,CAM7C"}
1
+ {"version":3,"file":"uploader.d.ts","sourceRoot":"","sources":["../../src/reporter/uploader.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,kBAAkB,EAElB,kBAAkB,EAEnB,MAAM,2BAA2B,CAAC;AAKnC,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,kBAAkB,CAAC;CACjC;AAED,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,YAAY,CAAqB;IAEzC,YAAY,MAAM,EAAE,cAAc,EASjC;IAED,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAgBhC,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAUhC,MAAM,CAAC,qBAAqB,CAAC,QAAQ,EAAE,OAAO,GAAG,kBAAkB,GAAG,IAAI,CAKzE;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,IAAI,SAAS,IAAI,MAAM,CAEtB;IAEK,UAAU,CACd,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,MAAM,EACvB,YAAY,CAAC,EAAE,kBAAkB,GAChC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgCxB;IAEK,eAAe,CACnB,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,IAAI,CAAC,CAef;IAEK,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CA4BpC;CACF;AAID,wBAAgB,WAAW,IAAI,QAAQ,GAAG,IAAI,CAM7C"}
@@ -100,7 +100,28 @@ class Uploader {
100
100
  await (0, r2_uploader_1.sendTaskToQueue)(uploadTask);
101
101
  }
102
102
  async waitForUploads() {
103
- await (0, r2_uploader_1.waitForTaskQueueToFinish)();
103
+ const { waiting, executing } = (0, r2_uploader_1.getTaskQueueStats)();
104
+ const pending = waiting + executing;
105
+ if (pending === 0) {
106
+ await (0, r2_uploader_1.waitForTaskQueueToFinish)();
107
+ return;
108
+ }
109
+ // This drain runs after the last test with no other output (observed at
110
+ // 4+ minutes on spot runs), so make it visible: count up front, progress
111
+ // every 30s, duration at the end.
112
+ const start = Date.now();
113
+ console.log(`[Uploader] Waiting for ${pending} pending uploads (${executing} executing, ${waiting} queued)...`);
114
+ const progress = setInterval(() => {
115
+ const stats = (0, r2_uploader_1.getTaskQueueStats)();
116
+ console.log(`[Uploader] Still draining: ${stats.executing + stats.waiting} uploads pending (${stats.executing} executing, ${stats.waiting} queued)`);
117
+ }, 30_000);
118
+ try {
119
+ await (0, r2_uploader_1.waitForTaskQueueToFinish)();
120
+ }
121
+ finally {
122
+ clearInterval(progress);
123
+ }
124
+ console.log(`[Uploader] Pending uploads drained in ${((Date.now() - start) / 1000).toFixed(1)}s`);
104
125
  }
105
126
  }
106
127
  exports.Uploader = Uploader;
@@ -1 +1 @@
1
- {"version":3,"file":"visual.d.ts","sourceRoot":"","sources":["../../../src/test/expect/visual.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAY,MAAM,kBAAkB,CAAC;AAShE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AA0E7C,wBAAsB,WAAW,CAC/B,MAAM,EAAE,IAAI,GAAG,OAAO,EACtB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,aAAa,CAAC,CAyFxB"}
1
+ {"version":3,"file":"visual.d.ts","sourceRoot":"","sources":["../../../src/test/expect/visual.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAY,MAAM,kBAAkB,CAAC;AAUhE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AA0E7C,wBAAsB,WAAW,CAC/B,MAAM,EAAE,IAAI,GAAG,OAAO,EACtB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,aAAa,CAAC,CA2FxB"}
@@ -10,6 +10,7 @@ const fs_1 = __importDefault(require("fs"));
10
10
  const openai_1 = __importDefault(require("openai"));
11
11
  const os_1 = __importDefault(require("os"));
12
12
  const path_1 = __importDefault(require("path"));
13
+ const api_worker_client_1 = require("../../api-worker-client");
13
14
  const mouse_pointer_1 = require("../scripts/mouse-pointer");
14
15
  function getCallerLine() {
15
16
  const stack = new Error().stack;
@@ -85,7 +86,9 @@ async function toLookRight(target, pageDescription) {
85
86
  const base64Image = screenshot.toString("base64");
86
87
  await (0, mouse_pointer_1.addHighlighterScriptsToPage)(page);
87
88
  const model = "gpt-4o";
88
- const openai = new openai_1.default({ maxRetries: 5 });
89
+ // Same api-worker proxy the overlay-dismissal agent uses, so a test run needs
90
+ // no OpenAI key of its own; undefined falls back to the SDK's env defaults.
91
+ const openai = new openai_1.default({ maxRetries: 5, ...(0, api_worker_client_1.llmProxyOpenAIConfig)() });
89
92
  const completion = await openai.chat.completions.create({
90
93
  model,
91
94
  messages: [
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/test/scripts/pw-locator-patch/dismiss-overlays/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAGxC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAG1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAK9C,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,WAKhE;AAED,qBAAa,mBAAmB;IAOlB,OAAO,CAAC,QAAQ,CAAC,IAAI;IANjC,OAAO,CAAC,iBAAiB,CAIhB;IAET,YAA6B,IAAI,EAAE,IAAI,EAAI;IAErC,WAAW,kBAIhB;IAEK,GAAG,CACP,aAAa,EAAE,GAAG,EAClB,QAAQ,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,EACvC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAuDf;YAEa,QAAQ;CAiCvB;AAED,wBAAgB,0BAA0B,CACxC,YAAY,EAAE,MAAM,GACnB,cAAc,GAAG,SAAS,CAiC5B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/test/scripts/pw-locator-patch/dismiss-overlays/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAIxC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAG1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAK9C,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,WAKhE;AAED,qBAAa,mBAAmB;IAOlB,OAAO,CAAC,QAAQ,CAAC,IAAI;IANjC,OAAO,CAAC,iBAAiB,CAIhB;IAET,YAA6B,IAAI,EAAE,IAAI,EAAI;IAErC,WAAW,kBAIhB;IAEK,GAAG,CACP,aAAa,EAAE,GAAG,EAClB,QAAQ,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,EACvC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAuDf;YAEa,QAAQ;CAqCvB;AAED,wBAAgB,0BAA0B,CACxC,YAAY,EAAE,MAAM,GACnB,cAAc,GAAG,SAAS,CAiC5B"}
@@ -4,6 +4,7 @@ exports.OverlayDismissAgent = void 0;
4
4
  exports.isErrorSupported = isErrorSupported;
5
5
  exports.extractInterceptingElement = extractInterceptingElement;
6
6
  const cua_1 = require("@empiricalrun/cua");
7
+ const api_worker_client_1 = require("../../../../api-worker-client");
7
8
  const telemetry_1 = require("../../../../telemetry");
8
9
  const cache_1 = require("./cache");
9
10
  const prompt_1 = require("./prompt");
@@ -99,6 +100,10 @@ class OverlayDismissAgent {
99
100
  // already on the current page. Including page_goto causes the model
100
101
  // to waste turns trying to navigate instead of clicking dismiss.
101
102
  includePageGotoTool: false,
103
+ // Route the model call through api-worker (Cloudflare AI Gateway) on the
104
+ // project API key, so test-run containers carry no OpenAI key. Undefined
105
+ // on a dev machine or in a pi sandbox — see the helper.
106
+ openAIClientConfig: (0, api_worker_client_1.llmProxyOpenAIConfig)(),
102
107
  });
103
108
  this.cachingCandidates.push({
104
109
  dom: element?.interceptor,
@@ -1 +1 @@
1
- {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../../../../src/test/scripts/pw-locator-patch/vision/query.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,wBAAgB,UAAU,CAAC,YAAY,EAAE,QAAQ,GAAG;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,QAOzE"}
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../../../../src/test/scripts/pw-locator-patch/vision/query.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAIhD,wBAAgB,UAAU,CAAC,YAAY,EAAE,QAAQ,GAAG;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,QASzE"}
@@ -2,11 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.patchQuery = patchQuery;
4
4
  const vision_1 = require("@empiricalrun/llm/vision");
5
+ const api_worker_client_1 = require("../../../../api-worker-client");
5
6
  function patchQuery(LocatorClass) {
6
7
  LocatorClass.prototype.query = async function (instruction) {
7
8
  await this.waitFor({ state: "visible" });
8
9
  const screenshot = await this.screenshot();
9
- const answer = await (0, vision_1.query)(screenshot.toString("base64"), instruction);
10
+ const answer = await (0, vision_1.query)(screenshot.toString("base64"), instruction, {
11
+ clientConfig: (0, api_worker_client_1.llmProxyOpenAIConfig)(),
12
+ });
10
13
  return answer;
11
14
  };
12
15
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empiricalrun/playwright-utils",
3
- "version": "0.58.4",
3
+ "version": "0.58.6",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -54,10 +54,10 @@
54
54
  "puppeteer-extra-plugin-recaptcha": "^3.6.8",
55
55
  "rimraf": "^6.0.1",
56
56
  "ts-morph": "^23.0.0",
57
- "@empiricalrun/cua": "^0.4.1",
58
- "@empiricalrun/llm": "^0.27.0",
59
- "@empiricalrun/r2-uploader": "^0.10.1",
60
- "@empiricalrun/reporter": "^0.29.1"
57
+ "@empiricalrun/cua": "^0.4.2",
58
+ "@empiricalrun/llm": "^0.27.1",
59
+ "@empiricalrun/r2-uploader": "^0.10.2",
60
+ "@empiricalrun/reporter": "^0.30.0"
61
61
  },
62
62
  "scripts": {
63
63
  "dev": "pnpm gen:api-worker-openapi && tsgo --build --watch",
@@ -20,16 +20,45 @@ Playwright version-pin bump).
20
20
 
21
21
  - `run.mjs` — orchestrator: applies the patch, runs the curated specs in both
22
22
  modes, fails on any IBR-only failure.
23
- - `ibr-adapter.js` — bridges IBR's env/paths contract to the blob reporter's
24
- (config.shard → SHARD_INDEX env, zip relocation to `blob-report/`).
23
+ - `ibr-adapter.js` — bridges IBR's output contract to the blob reporter's: it
24
+ mirrors blob's zip name (`_defaultReportName`), output-dir resolution
25
+ (package root, not cwd) and pre-write cleanup, then relocates IBR's zip into
26
+ `blob-report/`. Sharding needs no bridge — IBR falls back to `config.shard`.
25
27
  - `intermediate-mode-ibr.patch` — one-line upstream fixture change making the
26
28
  intermediate reporter selectable via `PWTEST_IBR_ADAPTER`.
27
29
 
28
- The job is EXPECTED to stay red until IBR reaches parity: today ~55 upstream
29
- tests fail only through IBR, all tracing to the known gaps (`onStdIO`,
30
- `onError`, result-level annotations, attachment bodies — encoded as `it.fails`
31
- in `src/reporter/blob-ibr-parity.test.ts`). The failure count shrinking to
32
- zero is the signal that the built-in blob reporter is safe to drop.
30
+ The failure count reaching zero is the signal that the built-in blob reporter
31
+ is safe to drop. It got there over #7093/#7094/#7095 (the parity gaps and the
32
+ full-suite sweep) and then #7110/#7111/#7114/this PR, which took the full
33
+ sweep from 19 IBR-only failures to 0.
34
+
35
+ ### Expected failures
36
+
37
+ `EXPECTED_IBR_ONLY` in `run.mjs` holds upstream tests that fail for reasons
38
+ outside the blob format — artifacts of injecting an external reporter into
39
+ upstream's harness. Each entry carries the reason it can never pass and is
40
+ printed at startup; the count is reported separately and does not turn the job
41
+ red. An entry whose spec runs and PASSES is reported as `[stale-expected]` and
42
+ fails the run, so the list cannot quietly absorb tests that have been fixed.
43
+ Anything that could be fixed does not belong there.
44
+
45
+ ### Why two slow specs are excluded
46
+
47
+ `only-changed.spec.ts` and `web-server.spec.ts` are in `EXCLUSION_RULES` for a
48
+ different reason from the rest: cost, not scope. The oracle runs ONE spec per
49
+ invocation, and Playwright keeps a file's tests on a single worker
50
+ (`fullyParallel` defaults to false), so a long file runs strictly serially no
51
+ matter what `--workers` says. Measured, `only-changed` gets through 7 of 13
52
+ tests in 7 minutes — steady progress, no wedge — and `web-server` has 39
53
+ server-booting tests. Neither fits the 10-minute `SPEC_TIMEOUT_MS`, so both
54
+ used to burn the cap, get quarantined, and contribute nothing but a hang that
55
+ kept the job red.
56
+
57
+ If the sweep's wall-clock ever needs fixing properly, the lever is `run.mjs`
58
+ running specs sequentially: several invocations could run concurrently with no
59
+ loss of accuracy (each spec's blob/IBR pair still compares like for like),
60
+ once each gets its own `--output` dir instead of sharing
61
+ `<pwRepo>/test-results`.
33
62
 
34
63
  ## Running locally
35
64
 
@@ -45,6 +45,55 @@ class IBRBlobAdapter extends IBR {
45
45
  super();
46
46
  this._adapterOptions = options;
47
47
  this._adapterCwd = process.cwd();
48
+ this._adapterShard = null;
49
+ }
50
+
51
+ onBegin(config, suite) {
52
+ this._adapterShard = config.shard;
53
+ super.onBegin(config, suite);
54
+ }
55
+
56
+ // Mirrors resolveOutputFile('BLOB', …) + resolveReporterOutputPath: an
57
+ // explicit outputDir resolves against the config dir, and the DEFAULT
58
+ // blob-report hangs off the nearest package.json at or above the config
59
+ // dir (process.cwd() only as a last resort). Upstream drives the CLI from
60
+ // a nested dir in several tests and then merges <packageRoot>/blob-report,
61
+ // so resolving against the cwd puts the zip where nothing looks for it.
62
+ _resolveOutputDir() {
63
+ const configDir = this._adapterOptions.configDir || this._adapterCwd;
64
+ if (this._adapterOptions.outputDir) {
65
+ return path.resolve(configDir, this._adapterOptions.outputDir);
66
+ }
67
+ return path.resolve(this._packageRoot(configDir), "blob-report");
68
+ }
69
+
70
+ _packageRoot(configDir) {
71
+ let dir = path.resolve(configDir);
72
+ for (;;) {
73
+ if (fs.existsSync(path.join(dir, "package.json"))) {
74
+ return dir;
75
+ }
76
+ const parent = path.dirname(dir);
77
+ if (parent === dir) {
78
+ return this._adapterCwd;
79
+ }
80
+ dir = parent;
81
+ }
82
+ }
83
+
84
+ // Mirrors blob's _defaultReportName: the shard suffix only appears on a
85
+ // sharded run, zero-padded to the width of the shard total. Upstream tests
86
+ // assert on the file name (reporter-blob.spec's "preserve steps in html
87
+ // report" expects exactly ['report.zip']).
88
+ _defaultReportName() {
89
+ if (!this._adapterShard) {
90
+ return "report.zip";
91
+ }
92
+ const padded = `${this._adapterShard.current}`.padStart(
93
+ `${this._adapterShard.total}`.length,
94
+ "0",
95
+ );
96
+ return `report-${padded}.zip`;
48
97
  }
49
98
 
50
99
  async onEnd(result) {
@@ -59,11 +108,17 @@ class IBRBlobAdapter extends IBR {
59
108
  // Mirrors blob: no zip when there was nothing to report.
60
109
  return;
61
110
  }
62
- const outputDir = path.resolve(
63
- this._adapterCwd,
64
- this._adapterOptions.outputDir || "blob-report",
65
- );
66
- const fileName = this._adapterOptions.fileName || `report-${shard}.zip`;
111
+ const outputDir = this._resolveOutputDir();
112
+ const fileName = this._adapterOptions.fileName || this._defaultReportName();
113
+ // Mirrors blob's _prepareOutputFile, down to the opt-out: it wipes the
114
+ // output dir before writing unless PWTEST_BLOB_DO_NOT_REMOVE is set.
115
+ // Upstream tests merge whatever sits in blob-report/, and they run
116
+ // several shards from one baseDir — those that want the shards to
117
+ // accumulate set the env var, the rest (shard.spec's "shard 2/3 then
118
+ // 3/3") expect each run to start from an empty dir.
119
+ if (!process.env.PWTEST_BLOB_DO_NOT_REMOVE) {
120
+ fs.rmSync(outputDir, { recursive: true, force: true });
121
+ }
67
122
  fs.mkdirSync(outputDir, { recursive: true });
68
123
  fs.copyFileSync(src, path.join(outputDir, fileName));
69
124
  }
@@ -12,7 +12,10 @@
12
12
  * Both modes run the same spec files; environment noise cancels in the
13
13
  * baseline diff. Any IBR-only failure fails the run — the job stays red
14
14
  * until IBR reaches parity on these specs. It staying green is the signal
15
- * that the built-in blob reporter is safe to drop.
15
+ * that the built-in blob reporter is safe to drop. The one carve-out is
16
+ * EXPECTED_IBR_ONLY below: failures caused by injecting an external reporter
17
+ * rather than by the blob format, each with the reason it cannot pass, and
18
+ * each policed by a staleness check so the list cannot rot.
16
19
  *
17
20
  * Usage:
18
21
  * node run.mjs --pw-repo <upstream clone at the pinned tag>
@@ -65,8 +68,51 @@ const EXCLUSION_RULES = [
65
68
  "semantics are covered by reporter-blob.spec.ts) and they busy-loop " +
66
69
  "workers under load, wedging the sweep",
67
70
  ],
71
+ // The two specs below are SLOW, not wedged — measured, they make steady
72
+ // progress and simply do not fit in SPEC_TIMEOUT_MS. The oracle runs one
73
+ // spec per invocation and Playwright keeps a file's tests on a single
74
+ // worker (fullyParallel defaults to false), so a long file runs strictly
75
+ // serially however many --workers it is given. Both were already
76
+ // contributing no signal, just a 10-minute timeout each.
77
+ [
78
+ (name) => name === "only-changed.spec.ts",
79
+ "git-based run selection (--only-changed), not report format; 13 tests " +
80
+ "that each shell out to git take ~13min serially here",
81
+ ],
82
+ [
83
+ (name) => name === "web-server.spec.ts",
84
+ "webServer config and lifecycle, not report format; 39 tests that each " +
85
+ "boot and tear down a server take longer still",
86
+ ],
68
87
  ];
69
88
 
89
+ // Upstream tests that fail through IBR for reasons OUTSIDE the blob format —
90
+ // artifacts of injecting an external reporter into upstream's harness, not
91
+ // gaps in what IBR writes. They are reported and subtracted from the failure
92
+ // count instead of turning the job red.
93
+ //
94
+ // Kept honest by the staleness check at the end: an entry whose spec ran and
95
+ // did NOT fail is reported and fails the run, so a fixed test cannot sit here
96
+ // pretending to be unfixable. Add an entry only with the reason it can never
97
+ // pass; anything that could be fixed belongs in the failure list.
98
+ const EXPECTED_IBR_ONLY = [
99
+ {
100
+ key:
101
+ "cache.spec.ts:53 › should automatically clean cached versions of a " +
102
+ "changed file",
103
+ reason:
104
+ "asserts the transform cache holds exactly one directory; loading IBR " +
105
+ "from a path runs its bundle through the same transform pipeline, so " +
106
+ "the cache gains a directory per module (32 observed). The built-in " +
107
+ "blob reporter ships inside Playwright and never enters that cache.",
108
+ },
109
+ ];
110
+
111
+ /** Spec file name out of a failure key ("cache.spec.ts:53 › title"). */
112
+ function specOfKey(key) {
113
+ return key.slice(0, key.indexOf(":"));
114
+ }
115
+
70
116
  // Tests skipped by title via --grep-invert. Reserved for tests that WEDGE
71
117
  // the sweep — a busy-looped worker no per-test timeout can interrupt — so
72
118
  // exclusion is surgical (exact title match) and every entry carries the
@@ -316,12 +362,18 @@ console.log(`Specs: ${args.specs.join(", ")}`);
316
362
  for (const { pattern, reason } of HANGING_TESTS) {
317
363
  console.log(` [skipped-by-title] /${pattern}/ — ${reason}`);
318
364
  }
365
+ for (const { key, reason } of EXPECTED_IBR_ONLY) {
366
+ console.log(` [expected-ibr-only] ${key} — ${reason}`);
367
+ }
319
368
  // Each spec runs both modes back to back as its own bounded invocation
320
369
  // (see SPEC_TIMEOUT_MS); a hang or count mismatch quarantines that spec and
321
370
  // the sweep continues.
322
371
  const ibrOnly = [];
323
372
  const hungSpecs = [];
324
373
  const mismatchedSpecs = [];
374
+ const expectedByKey = new Map(EXPECTED_IBR_ONLY.map((e) => [e.key, e]));
375
+ const matchedExpected = new Set();
376
+ const comparedSpecs = new Set();
325
377
  let baselineNoise = 0;
326
378
 
327
379
  for (const spec of args.specs) {
@@ -355,21 +407,31 @@ for (const spec of args.specs) {
355
407
  mismatchedSpecs.push(spec);
356
408
  continue;
357
409
  }
410
+ comparedSpecs.add(spec);
358
411
  const specIbrOnly = [...ibr.failures]
359
412
  .filter((key) => !blob.failures.has(key))
360
413
  .sort();
361
414
  baselineNoise += blob.failures.size;
362
- ibrOnly.push(...specIbrOnly);
415
+ const specExpected = specIbrOnly.filter((key) => expectedByKey.has(key));
416
+ for (const key of specExpected) {
417
+ matchedExpected.add(key);
418
+ }
419
+ const specUnexpected = specIbrOnly.filter((key) => !expectedByKey.has(key));
420
+ ibrOnly.push(...specUnexpected);
363
421
  console.log(
364
422
  ` ${spec}: ${blob.totalRun} tests, ` +
365
- `${blob.failures.size} baseline noise, ${specIbrOnly.length} ibr-only ` +
366
- `(blob ${blob.elapsedSeconds}s, ibr ${ibr.elapsedSeconds}s)`,
423
+ `${blob.failures.size} baseline noise, ${specUnexpected.length} ibr-only` +
424
+ (specExpected.length ? `, ${specExpected.length} expected` : "") +
425
+ ` (blob ${blob.elapsedSeconds}s, ibr ${ibr.elapsedSeconds}s)`,
367
426
  );
368
427
  }
369
428
 
370
429
  console.log(
371
430
  `\nBaseline failures (env noise, cancelled in diff): ${baselineNoise}`,
372
431
  );
432
+ console.log(
433
+ `Expected IBR-only failures (not counted): ${matchedExpected.size}`,
434
+ );
373
435
  console.log(`IBR-only failures: ${ibrOnly.length}`);
374
436
  for (const key of ibrOnly.sort()) {
375
437
  console.log(` [ibr-only] ${key}`);
@@ -381,12 +443,27 @@ for (const spec of mismatchedSpecs) {
381
443
  console.log(` [mismatch] ${spec}`);
382
444
  }
383
445
 
384
- if (ibrOnly.length > 0 || hungSpecs.length > 0 || mismatchedSpecs.length > 0) {
446
+ // An expected failure whose spec ran and passed means the list is stale —
447
+ // that is a fix to celebrate by deleting the entry, not a silent pass.
448
+ const staleExpected = EXPECTED_IBR_ONLY.filter(
449
+ ({ key }) => comparedSpecs.has(specOfKey(key)) && !matchedExpected.has(key),
450
+ );
451
+ for (const { key } of staleExpected) {
452
+ console.log(` [stale-expected] ${key} — now passes; drop it from the list`);
453
+ }
454
+
455
+ if (
456
+ ibrOnly.length > 0 ||
457
+ hungSpecs.length > 0 ||
458
+ mismatchedSpecs.length > 0 ||
459
+ staleExpected.length > 0
460
+ ) {
385
461
  console.error(
386
462
  `\nFAIL: ${ibrOnly.length} upstream test(s) fail only through IBR, ` +
387
463
  `${hungSpecs.length} spec run(s) hung, ${mismatchedSpecs.length} ` +
388
- `spec(s) mismatched. The job stays red until IBR reaches blob parity ` +
389
- `on these specs.`,
464
+ `spec(s) mismatched, ${staleExpected.length} stale expected-failure ` +
465
+ `entr${staleExpected.length === 1 ? "y" : "ies"}. The job stays red ` +
466
+ `until IBR reaches blob parity on these specs.`,
390
467
  );
391
468
  process.exit(1);
392
469
  }