@empiricalrun/playwright-utils 0.46.3 → 0.47.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @empiricalrun/playwright-utils
2
2
 
3
+ ## 0.47.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9a1c6e3: fix: attachment cleanup should track usage before uploads
8
+
9
+ ## 0.46.4
10
+
11
+ ### Patch Changes
12
+
13
+ - 9a7ff3f: fix: ibr does not kill process group on completion
14
+
3
15
  ## 0.46.3
4
16
 
5
17
  ### Patch Changes
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Test-only reporter used by attachment-cleanup integration tests.
3
+ *
4
+ * Records a timestamped event log of register/markUploaded/eviction events
5
+ * so tests can make precise assertions about ordering and timing.
6
+ */
7
+ import type { FullResult, Reporter, TestCase, TestResult } from "@playwright/test/reporter";
8
+ declare class AttachmentCleanupTestReporter implements Reporter {
9
+ private _timeline;
10
+ private _cleanup;
11
+ private _allAttachmentPaths;
12
+ private _pendingUploads;
13
+ private _queue;
14
+ constructor();
15
+ onTestEnd(_test: TestCase, result: TestResult): void;
16
+ onEnd(_result: FullResult): Promise<void>;
17
+ }
18
+ export default AttachmentCleanupTestReporter;
19
+ //# sourceMappingURL=attachment-cleanup-test-reporter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment-cleanup-test-reporter.d.ts","sourceRoot":"","sources":["../../src/reporter/attachment-cleanup-test-reporter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,UAAU,EACX,MAAM,2BAA2B,CAAC;AAwCnC,cAAM,6BAA8B,YAAW,QAAQ;IACrD,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,QAAQ,CAA2B;IAC3C,OAAO,CAAC,mBAAmB,CAAgB;IAC3C,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,MAAM,CAAkC;;IAYhD,SAAS,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU;IA2BvC,KAAK,CAAC,OAAO,EAAE,UAAU;CA0BhC;AAED,eAAe,6BAA6B,CAAC"}
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ /**
3
+ * Test-only reporter used by attachment-cleanup integration tests.
4
+ *
5
+ * Records a timestamped event log of register/markUploaded/eviction events
6
+ * so tests can make precise assertions about ordering and timing.
7
+ */
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const path_1 = __importDefault(require("path"));
14
+ const attachment_cleanup_1 = require("./attachment-cleanup");
15
+ const SUMMARY_FILE = path_1.default.join(process.cwd(), "attachment-cleanup-summary.json");
16
+ const UPLOAD_DELAY_MS = parseInt(process.env.TEST_UPLOAD_DELAY_MS || "100", 10);
17
+ const MAX_CONCURRENT = parseInt(process.env.UPLOAD_MAX_QUEUE_SIZE || "10", 10);
18
+ class AsyncQueue {
19
+ _maxConcurrent;
20
+ _running = 0;
21
+ _waiting = [];
22
+ constructor(_maxConcurrent) {
23
+ this._maxConcurrent = _maxConcurrent;
24
+ }
25
+ async run(fn) {
26
+ while (this._running >= this._maxConcurrent) {
27
+ await new Promise((resolve) => this._waiting.push(resolve));
28
+ }
29
+ this._running++;
30
+ try {
31
+ return await fn();
32
+ }
33
+ finally {
34
+ this._running--;
35
+ const next = this._waiting.shift();
36
+ if (next)
37
+ next();
38
+ }
39
+ }
40
+ }
41
+ class AttachmentCleanupTestReporter {
42
+ _timeline = [];
43
+ _cleanup = new attachment_cleanup_1.AttachmentCleanup();
44
+ _allAttachmentPaths = [];
45
+ _pendingUploads = [];
46
+ _queue = new AsyncQueue(MAX_CONCURRENT);
47
+ constructor() {
48
+ this._cleanup.onEvict = (evictedPath) => {
49
+ this._timeline.push({
50
+ type: "eviction",
51
+ path: evictedPath,
52
+ timestamp: Date.now(),
53
+ });
54
+ };
55
+ }
56
+ onTestEnd(_test, result) {
57
+ for (const attachment of result.attachments) {
58
+ if (attachment.path) {
59
+ this._allAttachmentPaths.push(attachment.path);
60
+ this._cleanup.register(attachment);
61
+ this._timeline.push({
62
+ type: "register",
63
+ path: attachment.path,
64
+ timestamp: Date.now(),
65
+ });
66
+ const attachmentPath = attachment.path;
67
+ const uploadPromise = this._queue.run(async () => {
68
+ await new Promise((r) => setTimeout(r, UPLOAD_DELAY_MS));
69
+ this._cleanup.markUploaded(attachmentPath);
70
+ this._timeline.push({
71
+ type: "markUploaded",
72
+ path: attachmentPath,
73
+ timestamp: Date.now(),
74
+ });
75
+ });
76
+ this._pendingUploads.push(uploadPromise);
77
+ }
78
+ }
79
+ }
80
+ async onEnd(_result) {
81
+ await Promise.allSettled(this._pendingUploads);
82
+ this._cleanup.logSummary();
83
+ const stats = this._cleanup.stats;
84
+ const existingPaths = this._allAttachmentPaths.filter((p) => fs_1.default.existsSync(p));
85
+ const deletedPaths = this._allAttachmentPaths.filter((p) => !fs_1.default.existsSync(p));
86
+ fs_1.default.writeFileSync(SUMMARY_FILE, JSON.stringify({
87
+ ...stats,
88
+ allAttachmentPaths: this._allAttachmentPaths,
89
+ existingPaths,
90
+ deletedPaths,
91
+ timeline: this._timeline,
92
+ }, null, 2));
93
+ }
94
+ }
95
+ exports.default = AttachmentCleanupTestReporter;
@@ -1,7 +1,17 @@
1
1
  /**
2
- * Tracks uploaded attachments and evicts the oldest ones from disk
2
+ * Tracks test attachments and evicts the oldest uploaded ones from disk
3
3
  * when total size exceeds the configured limit.
4
4
  *
5
+ * Two-phase tracking:
6
+ * 1. register() — called immediately when an attachment is created.
7
+ * Adds the file to the total size accounting so the LRU knows about
8
+ * all disk usage, even while uploads are still queued.
9
+ * 2. markUploaded() — called after the file is uploaded to R2.
10
+ * Marks the file as safe to evict (delete from disk).
11
+ *
12
+ * Eviction only deletes files that have been uploaded. This ensures we
13
+ * never delete a file before its upload completes.
14
+ *
5
15
  * Why this exists:
6
16
  * Test attachments (videos, traces, screenshots) accumulate on disk during a
7
17
  * test run. On Fargate workers with 50 GB ephemeral storage, large test suites
@@ -9,23 +19,47 @@
9
19
  * safely delete them from disk — but we can't delete them immediately because
10
20
  * Playwright's blob reporter reads them during onEnd to embed as resources/.
11
21
  *
12
- * Instead, we use an LRU approach: only evict the oldest files when total size
13
- * exceeds the limit. The blob reporter's statSync guard (Playwright 1.57+)
14
- * gracefully skips missing files, and patchBlobZip replaces resources/ with
22
+ * The blob reporter's statSync guard (Playwright 1.57+) gracefully skips
23
+ * missing files, and patchBlobZip replaces resources/ with
15
24
  * _empirical_urls.json pointing to R2 URLs anyway.
16
- *
17
- * Future: when the incremental blob reporter is mature enough to replace the
18
- * standard blob reporter for all runs (not just spot instances), this class
19
- * can be removed — the incremental reporter never embeds attachments as resources.
20
25
  */
21
26
  export declare class AttachmentCleanup {
22
- private _entries;
27
+ private _pending;
28
+ private _uploaded;
23
29
  private _totalSize;
24
30
  private _limitBytes;
31
+ private _registeredCount;
32
+ private _uploadedCount;
33
+ private _evictedCount;
34
+ private _evictedBytes;
35
+ private _diskLogTimer;
36
+ private _onEvict;
25
37
  constructor(limitBytes?: number);
26
- track(attachment: {
38
+ /**
39
+ * Phase 1: Register an attachment immediately when it's created.
40
+ * Adds to total size accounting and attempts eviction of already-uploaded files.
41
+ */
42
+ register(attachment: {
27
43
  path?: string | undefined;
28
44
  }): void;
45
+ /**
46
+ * Phase 2: Mark an attachment as uploaded (safe to evict).
47
+ * Moves from pending to the uploaded queue and attempts eviction.
48
+ */
49
+ markUploaded(attachmentPath: string): void;
29
50
  private _evict;
51
+ private _logDiskStatus;
52
+ logSummary(): void;
53
+ set onEvict(callback: (path: string) => void);
54
+ get stats(): {
55
+ registeredCount: number;
56
+ uploadedCount: number;
57
+ pendingCount: number;
58
+ evictedCount: number;
59
+ evictedBytes: number;
60
+ activeCount: number;
61
+ totalSize: number;
62
+ limitBytes: number;
63
+ };
30
64
  }
31
65
  //# sourceMappingURL=attachment-cleanup.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"attachment-cleanup.d.ts","sourceRoot":"","sources":["../../src/reporter/attachment-cleanup.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAwC;IACxD,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,WAAW,CAAS;gBAEhB,UAAU,GAAE,MAAiC;IAIzD,KAAK,CAAC,UAAU,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE;IAc/C,OAAO,CAAC,MAAM;CAcf"}
1
+ {"version":3,"file":"attachment-cleanup.d.ts","sourceRoot":"","sources":["../../src/reporter/attachment-cleanup.ts"],"names":[],"mappings":"AAqCA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,SAAS,CAAwC;IACzD,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,aAAa,CAA+C;IACpE,OAAO,CAAC,QAAQ,CAAyC;gBAE7C,UAAU,GAAE,MAAmC;IAY3D;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE;IAelD;;;OAGG;IACH,YAAY,CAAC,cAAc,EAAE,MAAM;IASnC,OAAO,CAAC,MAAM;IA4Bd,OAAO,CAAC,cAAc;IAMtB,UAAU;IAUV,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAE3C;IAED,IAAI,KAAK;;;;;;;;;MAWR;CACF"}
@@ -4,13 +4,53 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.AttachmentCleanup = void 0;
7
+ const child_process_1 = require("child_process");
7
8
  const fs_1 = __importDefault(require("fs"));
8
9
  const logger_1 = require("../logger");
9
- const DEFAULT_DISK_LIMIT_BYTES = 10 * 1024 * 1024 * 1024; // 10 GB
10
+ function getDefaultDiskLimitBytes() {
11
+ const raw = process.env.ATTACHMENT_CLEANUP_LIMIT_BYTES;
12
+ if (raw != null) {
13
+ const parsed = Number(raw);
14
+ if (Number.isSafeInteger(parsed) && parsed > 0)
15
+ return parsed;
16
+ logger_1.logger.warn(`[AttachmentCleanup] Invalid ATTACHMENT_CLEANUP_LIMIT_BYTES=${raw}, using default`);
17
+ }
18
+ return 10 * 1024 * 1024 * 1024; // 10 GB
19
+ }
20
+ const DISK_LOG_INTERVAL_MS = 30_000;
21
+ function formatBytes(bytes) {
22
+ if (bytes < 1024 * 1024)
23
+ return `${(bytes / 1024).toFixed(1)} KB`;
24
+ if (bytes < 1024 * 1024 * 1024)
25
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
26
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
27
+ }
28
+ function getDiskUsage() {
29
+ try {
30
+ const output = (0, child_process_1.execSync)("df -h / | tail -1", {
31
+ encoding: "utf8",
32
+ timeout: 5000,
33
+ }).trim();
34
+ return output;
35
+ }
36
+ catch {
37
+ return "unavailable";
38
+ }
39
+ }
10
40
  /**
11
- * Tracks uploaded attachments and evicts the oldest ones from disk
41
+ * Tracks test attachments and evicts the oldest uploaded ones from disk
12
42
  * when total size exceeds the configured limit.
13
43
  *
44
+ * Two-phase tracking:
45
+ * 1. register() — called immediately when an attachment is created.
46
+ * Adds the file to the total size accounting so the LRU knows about
47
+ * all disk usage, even while uploads are still queued.
48
+ * 2. markUploaded() — called after the file is uploaded to R2.
49
+ * Marks the file as safe to evict (delete from disk).
50
+ *
51
+ * Eviction only deletes files that have been uploaded. This ensures we
52
+ * never delete a file before its upload completes.
53
+ *
14
54
  * Why this exists:
15
55
  * Test attachments (videos, traces, screenshots) accumulate on disk during a
16
56
  * test run. On Fargate workers with 50 GB ephemeral storage, large test suites
@@ -18,48 +58,113 @@ const DEFAULT_DISK_LIMIT_BYTES = 10 * 1024 * 1024 * 1024; // 10 GB
18
58
  * safely delete them from disk — but we can't delete them immediately because
19
59
  * Playwright's blob reporter reads them during onEnd to embed as resources/.
20
60
  *
21
- * Instead, we use an LRU approach: only evict the oldest files when total size
22
- * exceeds the limit. The blob reporter's statSync guard (Playwright 1.57+)
23
- * gracefully skips missing files, and patchBlobZip replaces resources/ with
61
+ * The blob reporter's statSync guard (Playwright 1.57+) gracefully skips
62
+ * missing files, and patchBlobZip replaces resources/ with
24
63
  * _empirical_urls.json pointing to R2 URLs anyway.
25
- *
26
- * Future: when the incremental blob reporter is mature enough to replace the
27
- * standard blob reporter for all runs (not just spot instances), this class
28
- * can be removed — the incremental reporter never embeds attachments as resources.
29
64
  */
30
65
  class AttachmentCleanup {
31
- _entries = [];
66
+ _pending = new Map();
67
+ _uploaded = [];
32
68
  _totalSize = 0;
33
69
  _limitBytes;
34
- constructor(limitBytes = DEFAULT_DISK_LIMIT_BYTES) {
70
+ _registeredCount = 0;
71
+ _uploadedCount = 0;
72
+ _evictedCount = 0;
73
+ _evictedBytes = 0;
74
+ _diskLogTimer = null;
75
+ _onEvict = null;
76
+ constructor(limitBytes = getDefaultDiskLimitBytes()) {
35
77
  this._limitBytes = limitBytes;
78
+ logger_1.logger.info(`[AttachmentCleanup] Initialized with limit: ${formatBytes(limitBytes)}`);
79
+ this._diskLogTimer = setInterval(() => {
80
+ this._logDiskStatus();
81
+ }, DISK_LOG_INTERVAL_MS);
82
+ // Don't keep the process alive just for this timer
83
+ this._diskLogTimer.unref();
36
84
  }
37
- track(attachment) {
85
+ /**
86
+ * Phase 1: Register an attachment immediately when it's created.
87
+ * Adds to total size accounting and attempts eviction of already-uploaded files.
88
+ */
89
+ register(attachment) {
38
90
  try {
39
- if (attachment.path) {
40
- const stat = fs_1.default.statSync(attachment.path);
41
- const sizeBytes = stat.size;
42
- this._entries.push({ path: attachment.path, size: sizeBytes });
43
- this._totalSize += sizeBytes;
44
- this._evict();
45
- }
91
+ if (!attachment.path)
92
+ return;
93
+ if (this._pending.has(attachment.path))
94
+ return;
95
+ const stat = fs_1.default.statSync(attachment.path);
96
+ const sizeBytes = stat.size;
97
+ this._pending.set(attachment.path, sizeBytes);
98
+ this._totalSize += sizeBytes;
99
+ this._registeredCount++;
100
+ this._evict();
46
101
  }
47
102
  catch {
48
- // Ignore errors
103
+ // Ignore errors (file may not exist)
49
104
  }
50
105
  }
106
+ /**
107
+ * Phase 2: Mark an attachment as uploaded (safe to evict).
108
+ * Moves from pending to the uploaded queue and attempts eviction.
109
+ */
110
+ markUploaded(attachmentPath) {
111
+ const size = this._pending.get(attachmentPath);
112
+ if (size === undefined)
113
+ return;
114
+ this._pending.delete(attachmentPath);
115
+ this._uploaded.push({ path: attachmentPath, size });
116
+ this._uploadedCount++;
117
+ this._evict();
118
+ }
51
119
  _evict() {
52
- while (this._totalSize > this._limitBytes && this._entries.length > 0) {
53
- const oldest = this._entries.shift();
120
+ while (this._totalSize > this._limitBytes && this._uploaded.length > 0) {
121
+ const oldest = this._uploaded[0];
54
122
  try {
55
123
  fs_1.default.unlinkSync(oldest.path);
56
- logger_1.logger.debug(`[AttachmentCleanup] Evicted ${oldest.path} (${oldest.size} bytes)`);
124
+ this._uploaded.shift();
125
+ this._evictedCount++;
126
+ this._evictedBytes += oldest.size;
127
+ this._totalSize -= oldest.size;
128
+ logger_1.logger.info(`[AttachmentCleanup] Evicted ${oldest.path} (${formatBytes(oldest.size)}), totalSize=${formatBytes(this._totalSize)}, pending=${this._pending.size}, uploaded=${this._uploaded.length}`);
129
+ this._onEvict?.(oldest.path);
57
130
  }
58
- catch {
59
- // File may already be gone
131
+ catch (err) {
132
+ const code = err?.code;
133
+ if (code === "ENOENT") {
134
+ this._uploaded.shift();
135
+ this._totalSize -= oldest.size;
136
+ }
137
+ else {
138
+ logger_1.logger.warn(`[AttachmentCleanup] Failed to evict ${oldest.path}: ${code ?? err}`);
139
+ break;
140
+ }
60
141
  }
61
- this._totalSize -= oldest.size;
62
142
  }
63
143
  }
144
+ _logDiskStatus() {
145
+ logger_1.logger.info(`[AttachmentCleanup] Periodic: registered=${this._registeredCount}, pending=${this._pending.size}, uploaded=${this._uploadedCount}, evicted=${this._evictedCount} (${formatBytes(this._evictedBytes)}), totalSize=${formatBytes(this._totalSize)}, limit=${formatBytes(this._limitBytes)}, disk=${getDiskUsage()}`);
146
+ }
147
+ logSummary() {
148
+ if (this._diskLogTimer) {
149
+ clearInterval(this._diskLogTimer);
150
+ this._diskLogTimer = null;
151
+ }
152
+ logger_1.logger.info(`[AttachmentCleanup] Summary: registered=${this._registeredCount}, pending=${this._pending.size}, uploaded=${this._uploadedCount}, evicted=${this._evictedCount} (${formatBytes(this._evictedBytes)}), totalSize=${formatBytes(this._totalSize)}, disk=${getDiskUsage()}`);
153
+ }
154
+ set onEvict(callback) {
155
+ this._onEvict = callback;
156
+ }
157
+ get stats() {
158
+ return {
159
+ registeredCount: this._registeredCount,
160
+ uploadedCount: this._uploadedCount,
161
+ pendingCount: this._pending.size,
162
+ evictedCount: this._evictedCount,
163
+ evictedBytes: this._evictedBytes,
164
+ activeCount: this._uploaded.length,
165
+ totalSize: this._totalSize,
166
+ limitBytes: this._limitBytes,
167
+ };
168
+ }
64
169
  }
65
170
  exports.AttachmentCleanup = AttachmentCleanup;
@@ -1 +1 @@
1
- {"version":3,"file":"empirical-reporter.d.ts","sourceRoot":"","sources":["../../src/reporter/empirical-reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,UAAU,EACX,MAAM,2BAA2B,CAAC;AAuBnC,cAAM,iBAAkB,YAAW,QAAQ;IACzC,OAAO,CAAC,kBAAkB,CAAyB;IACnD,OAAO,CAAC,oBAAoB,CAG1B;IACF,OAAO,CAAC,sBAAsB,CAAuB;IACrD,OAAO,CAAC,YAAY,CAAgD;IACpE,OAAO,CAAC,iBAAiB,CAAyC;IAClE,OAAO,CAAC,gBAAgB,CAAkC;IAC1D,OAAO,CAAC,SAAS,CAAyB;IAC1C,OAAO,CAAC,kBAAkB,CAA2B;;IAMrD,OAAO,CAAC,6BAA6B,CAmCnC;IAEF,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU;IAwGtC,KAAK,CAAC,MAAM,EAAE,UAAU;IAkG9B,OAAO,CAAC,qBAAqB;YAkBf,gBAAgB;YAOhB,iBAAiB;CAmChC;AAED,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"empirical-reporter.d.ts","sourceRoot":"","sources":["../../src/reporter/empirical-reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,UAAU,EACX,MAAM,2BAA2B,CAAC;AAuBnC,cAAM,iBAAkB,YAAW,QAAQ;IACzC,OAAO,CAAC,kBAAkB,CAAyB;IACnD,OAAO,CAAC,oBAAoB,CAG1B;IACF,OAAO,CAAC,sBAAsB,CAAuB;IACrD,OAAO,CAAC,YAAY,CAAgD;IACpE,OAAO,CAAC,iBAAiB,CAAyC;IAClE,OAAO,CAAC,gBAAgB,CAAkC;IAC1D,OAAO,CAAC,SAAS,CAAyB;IAC1C,OAAO,CAAC,kBAAkB,CAA2B;;IAMrD,OAAO,CAAC,6BAA6B,CAqCnC;IAEF,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU;IAwGtC,KAAK,CAAC,MAAM,EAAE,UAAU;IAmG9B,OAAO,CAAC,qBAAqB;YAkBf,gBAAgB;YAOhB,iBAAiB;CAmChC;AAED,eAAe,iBAAiB,CAAC"}
@@ -38,11 +38,12 @@ class EmpiricalReporter {
38
38
  logger_1.logger.error(`[Empirical Reporter] Attachment does not exist: ${attachment.path}`);
39
39
  return;
40
40
  }
41
+ this._attachmentCleanup.register(attachment);
41
42
  const relativePath = path_1.default.relative(this._testResultSourceDir, attachment.path);
42
43
  const destinationPath = `data/${relativePath}`;
43
44
  const publicUrl = await this._uploader.uploadFile(attachment.path, destinationPath);
44
45
  if (publicUrl) {
45
- this._attachmentCleanup.track(attachment);
46
+ this._attachmentCleanup.markUploaded(attachment.path);
46
47
  return { [attachment.path]: publicUrl };
47
48
  }
48
49
  };
@@ -134,7 +135,7 @@ class EmpiricalReporter {
134
135
  return;
135
136
  if (!this._uploader)
136
137
  return;
137
- logger_1.logger.debug(`[Empirical Reporter] Test run completed with status: ${result.status}`);
138
+ logger_1.logger.info(`[Empirical Reporter] Test run completed with status: ${result.status}`);
138
139
  logger_1.logger.debug(`[Empirical Reporter] State: cwd=${this._currentWorkingDir}, hasSharding=${this._hasSharding}`);
139
140
  const startTime = Date.now();
140
141
  const reportDir = path_1.default.join(this._currentWorkingDir, "playwright-report");
@@ -186,6 +187,7 @@ class EmpiricalReporter {
186
187
  else {
187
188
  logger_1.logger.info(`[Empirical Reporter] Sharding not enabled (HAS_SHARDING=${process.env.HAS_SHARDING})`);
188
189
  }
190
+ this._attachmentCleanup.logSummary();
189
191
  logger_1.logger.debug("[Empirical Reporter] All uploads finished");
190
192
  const timeDiff = Date.now() - startTime;
191
193
  logger_1.logger.debug("[Empirical Reporter] Time taken to upload after tests ended: ", timeDiff, "ms");
@@ -1 +1 @@
1
- {"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../../src/reporter/harness.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAErD;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IACzD,GAAG,EAAE,cAAc,CAAC;IACpB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC,CA2BD;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAI3E;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,OAAO,GAAG,cAAc,GAAG,cAAc,CAAC;AAOtD;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,cAAc,EACnB,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC1C,OAAO,CAAC,SAAS,CAAC,CA8CpB;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,SAAS,EAAE,CAAC,CAUtB"}
1
+ {"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../../src/reporter/harness.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAErD;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IACzD,GAAG,EAAE,cAAc,CAAC;IACpB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC,CA2BD;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAI3E;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,OAAO,GAAG,cAAc,GAAG,cAAc,CAAC;AAOtD;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,cAAc,EACnB,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC1C,OAAO,CAAC,SAAS,CAAC,CAoDpB;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,SAAS,EAAE,CAAC,CAUtB"}
@@ -75,6 +75,10 @@ async function runPlaywrightWithSigint(ctx, opts) {
75
75
  TOTAL_SHARDS: "1",
76
76
  },
77
77
  stdio: ["pipe", "pipe", "pipe"],
78
+ // Must match production: detached creates a new process group so the
79
+ // reporter's SIGKILL on -process.pid only kills browsers + Playwright,
80
+ // not the test runner.
81
+ detached: true,
78
82
  });
79
83
  let stdout = "";
80
84
  let stderr = "";
@@ -99,7 +103,9 @@ async function runPlaywrightWithSigint(ctx, opts) {
99
103
  });
100
104
  // Waiting for 1 sec to ensure second test has started
101
105
  await sleep(1_000);
102
- child.kill("SIGINT");
106
+ // Send SIGINT to the process group (negative PID) so all processes
107
+ // (Playwright + browsers) receive it, matching production behavior.
108
+ process.kill(-child.pid, "SIGINT");
103
109
  await new Promise((resolve) => {
104
110
  child.on("close", () => resolve());
105
111
  });
@@ -1 +1 @@
1
- {"version":3,"file":"incremental-blob-reporter.d.ts","sourceRoot":"","sources":["../../src/reporter/incremental-blob-reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,UAAU,EACV,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,UAAU,EACV,QAAQ,EACT,MAAM,2BAA2B,CAAC;AAuBnC;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEnD,cAAM,uBAAwB,YAAW,QAAQ;IAC/C,OAAO,CAAC,kBAAkB,CAAyB;IACnD,OAAO,CAAC,WAAW,CAIjB;IACF,OAAO,CAAC,UAAU,CAGhB;IACF,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,iBAAiB,CAA+B;IACxD,OAAO,CAAC,YAAY,CAAkC;IACtD,OAAO,CAAC,UAAU,CAAoC;IACtD,OAAO,CAAC,SAAS,CAAyB;IAC1C,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,iBAAiB,CAA0B;IACnD,OAAO,CAAC,mBAAmB,CAA0B;IACrD,OAAO,CAAC,YAAY,CAAkC;IACtD,OAAO,CAAC,YAAY,CAGN;IACd,OAAO,CAAC,UAAU,CAAsB;;IAexC,OAAO,CAAC,cAAc,CAA6B;IAEnD,OAAO,CAAC,mBAAmB;IA0B3B,OAAO,CAAC,oBAAoB;YAOd,eAAe;IAwC7B,OAAO,CAAC,2BAA2B;IAyBnC,OAAO,CAAC,qBAAqB;IAY7B,OAAO,CAAC,8BAA8B;IAStC,OAAO,CAAC,mBAAmB;IAkB3B,OAAO,KAAK,QAAQ,GAKnB;IAED,OAAO,KAAK,UAAU,GAErB;IAED,OAAO,KAAK,aAAa,GAExB;IAED,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,aAAa;YAUP,SAAS;IAwBvB,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI;IAgB/C,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI;IAIrD,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI;IAyBnD,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI;IAYrE,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI;IAY7D,KAAK,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAiB9C;;OAEG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAI5D,aAAa,IAAI,OAAO;CAGzB;AAID,wBAAgB,0BAA0B,IAAI,uBAAuB,GAAG,IAAI,CAE3E;AAED,wBAAgB,kCAAkC,CAChD,QAAQ,EAAE,uBAAuB,GAChC,IAAI,CAEN;AAED,eAAe,uBAAuB,CAAC"}
1
+ {"version":3,"file":"incremental-blob-reporter.d.ts","sourceRoot":"","sources":["../../src/reporter/incremental-blob-reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,UAAU,EACV,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,UAAU,EACV,QAAQ,EACT,MAAM,2BAA2B,CAAC;AAuBnC;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEnD,cAAM,uBAAwB,YAAW,QAAQ;IAC/C,OAAO,CAAC,kBAAkB,CAAyB;IACnD,OAAO,CAAC,WAAW,CAIjB;IACF,OAAO,CAAC,UAAU,CAGhB;IACF,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,iBAAiB,CAA+B;IACxD,OAAO,CAAC,YAAY,CAAkC;IACtD,OAAO,CAAC,UAAU,CAAoC;IACtD,OAAO,CAAC,SAAS,CAAyB;IAC1C,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,iBAAiB,CAA0B;IACnD,OAAO,CAAC,mBAAmB,CAA0B;IACrD,OAAO,CAAC,YAAY,CAAkC;IACtD,OAAO,CAAC,YAAY,CAGN;IACd,OAAO,CAAC,UAAU,CAAsB;;IAexC,OAAO,CAAC,cAAc,CAA6B;IAEnD,OAAO,CAAC,mBAAmB;IAkC3B,OAAO,CAAC,oBAAoB;YAOd,eAAe;IAwC7B,OAAO,CAAC,2BAA2B;IAyBnC,OAAO,CAAC,qBAAqB;IAY7B,OAAO,CAAC,8BAA8B;IAStC,OAAO,CAAC,mBAAmB;IAkB3B,OAAO,KAAK,QAAQ,GAKnB;IAED,OAAO,KAAK,UAAU,GAErB;IAED,OAAO,KAAK,aAAa,GAExB;IAED,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,aAAa;YAUP,SAAS;IAwBvB,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI;IAgB/C,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI;IAIrD,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI;IAyBnD,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI;IAYrE,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI;IAY7D,KAAK,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAiB9C;;OAEG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAI5D,aAAa,IAAI,OAAO;CAGzB;AAID,wBAAgB,0BAA0B,IAAI,uBAAuB,GAAG,IAAI,CAE3E;AAED,wBAAgB,kCAAkC,CAChD,QAAQ,EAAE,uBAAuB,GAChC,IAAI,CAEN;AAED,eAAe,uBAAuB,CAAC"}
@@ -53,13 +53,22 @@ class IncrementalBlobReporter {
53
53
  this._flushPromise = this._flushAndUpload()
54
54
  .then(() => {
55
55
  console.log("[IncrementalBlobReporter] Flush and upload complete on SIGINT, exiting");
56
- process.exit(0);
57
56
  })
58
57
  .catch((err) => {
59
58
  console.error("[IncrementalBlobReporterFailure] Flush/upload failed on SIGINT:", err);
60
59
  })
61
60
  .finally(() => {
62
61
  this._removeSignalHandler();
62
+ // Kill the entire process group (browsers + self) to avoid
63
+ // orphaned browser processes writing to test-results/ after exit.
64
+ // Using process.exit(0) alone only exits the Node process, leaving
65
+ // browser processes alive which blocks cleanup on worker warm reuse.
66
+ try {
67
+ process.kill(-process.pid, "SIGKILL");
68
+ }
69
+ catch {
70
+ process.exit(0);
71
+ }
63
72
  });
64
73
  };
65
74
  process.on("SIGINT", this._sigintHandler);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empiricalrun/playwright-utils",
3
- "version": "0.46.3",
3
+ "version": "0.47.0",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "author": "Empirical Team <hey@empirical.run>",
24
24
  "devDependencies": {
25
- "@playwright/test": "1.57.0",
25
+ "@playwright/test": "1.58.2",
26
26
  "@types/async-retry": "^1.4.8",
27
27
  "@types/authenticator": "^1.1.4",
28
28
  "@types/babel__code-frame": "^7.0.6",
@@ -30,7 +30,7 @@
30
30
  "@types/node": "^20.14.9",
31
31
  "@types/serve-handler": "^6.1.4",
32
32
  "@types/pg": "^8.11.6",
33
- "playwright-core": "1.57.0",
33
+ "playwright-core": "1.58.2",
34
34
  "serve-handler": "^6.1.6",
35
35
  "@empiricalrun/shared-types": "0.12.1"
36
36
  },
@@ -43,10 +43,10 @@
43
43
  "puppeteer-extra-plugin-recaptcha": "^3.6.8",
44
44
  "rimraf": "^6.0.1",
45
45
  "ts-morph": "^23.0.0",
46
- "@empiricalrun/cua": "^0.3.0",
47
46
  "@empiricalrun/dashboard-client": "^0.2.0",
48
47
  "@empiricalrun/llm": "^0.26.0",
49
48
  "@empiricalrun/r2-uploader": "^0.9.1",
49
+ "@empiricalrun/cua": "^0.3.0",
50
50
  "@empiricalrun/reporter": "^0.28.1"
51
51
  },
52
52
  "scripts": {
@@ -1 +1 @@
1
- {"root":["./src/email.ts","./src/index.ts","./src/kv.ts","./src/logger.ts","./src/mailosaur-client.ts","./src/playwright-extensions.ts","./src/postgres.ts","./src/telemetry.ts","./src/webhook.ts","./src/auth/google.ts","./src/auth/index.ts","./src/auth/types.ts","./src/captcha/index.ts","./src/config/index.ts","./src/config/proxy.ts","./src/config/devices/types.ts","./src/overlay-tests/cache.spec.ts","./src/overlay-tests/click.spec.ts","./src/overlay-tests/fixtures.ts","./src/overlay-tests/patch.spec.ts","./src/reporter/attachment-cleanup.ts","./src/reporter/blob-utils.ts","./src/reporter/empirical-reporter.ts","./src/reporter/failing-line.ts","./src/reporter/harness.ts","./src/reporter/ibr-utils.ts","./src/reporter/incremental-blob-reporter.ts","./src/reporter/lifecycle-events.ts","./src/reporter/local-test.ts","./src/reporter/reporter-state.ts","./src/reporter/uploader.ts","./src/reporter/util.ts","./src/test/constants.ts","./src/test/coverage.ts","./src/test/index.ts","./src/test/types.ts","./src/test/video-labels.ts","./src/test/expect/index.ts","./src/test/expect/types.ts","./src/test/expect/visual.ts","./src/test/expect/webhook.ts","./src/test/scripts/agent-capabilities.ts","./src/test/scripts/index.ts","./src/test/scripts/locator-highlights.ts","./src/test/scripts/locator-vision.ts","./src/test/scripts/mouse-pointer.ts","./src/test/scripts/types.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/cache.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/index.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/prompt.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/types.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/utils.ts","./src/test/scripts/pw-locator-patch/highlight/click.ts","./src/test/scripts/pw-locator-patch/highlight/expect.ts","./src/test/scripts/pw-locator-patch/highlight/hover.ts","./src/test/scripts/pw-locator-patch/highlight/inner-text.ts","./src/test/scripts/pw-locator-patch/highlight/input-value.ts","./src/test/scripts/pw-locator-patch/highlight/is-checked.ts","./src/test/scripts/pw-locator-patch/highlight/is-disabled.ts","./src/test/scripts/pw-locator-patch/highlight/is-editable.ts","./src/test/scripts/pw-locator-patch/highlight/text-content.ts","./src/test/scripts/pw-locator-patch/utils/index.ts","./src/test/scripts/pw-locator-patch/vision/query.ts"],"version":"5.8.3"}
1
+ {"root":["./src/email.ts","./src/index.ts","./src/kv.ts","./src/logger.ts","./src/mailosaur-client.ts","./src/playwright-extensions.ts","./src/postgres.ts","./src/telemetry.ts","./src/webhook.ts","./src/auth/google.ts","./src/auth/index.ts","./src/auth/types.ts","./src/captcha/index.ts","./src/config/index.ts","./src/config/proxy.ts","./src/config/devices/types.ts","./src/overlay-tests/cache.spec.ts","./src/overlay-tests/click.spec.ts","./src/overlay-tests/fixtures.ts","./src/overlay-tests/patch.spec.ts","./src/reporter/attachment-cleanup-test-reporter.ts","./src/reporter/attachment-cleanup.ts","./src/reporter/blob-utils.ts","./src/reporter/empirical-reporter.ts","./src/reporter/failing-line.ts","./src/reporter/harness.ts","./src/reporter/ibr-utils.ts","./src/reporter/incremental-blob-reporter.ts","./src/reporter/lifecycle-events.ts","./src/reporter/local-test.ts","./src/reporter/reporter-state.ts","./src/reporter/uploader.ts","./src/reporter/util.ts","./src/test/constants.ts","./src/test/coverage.ts","./src/test/index.ts","./src/test/types.ts","./src/test/video-labels.ts","./src/test/expect/index.ts","./src/test/expect/types.ts","./src/test/expect/visual.ts","./src/test/expect/webhook.ts","./src/test/scripts/agent-capabilities.ts","./src/test/scripts/index.ts","./src/test/scripts/locator-highlights.ts","./src/test/scripts/locator-vision.ts","./src/test/scripts/mouse-pointer.ts","./src/test/scripts/types.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/cache.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/index.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/prompt.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/types.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/utils.ts","./src/test/scripts/pw-locator-patch/highlight/click.ts","./src/test/scripts/pw-locator-patch/highlight/expect.ts","./src/test/scripts/pw-locator-patch/highlight/hover.ts","./src/test/scripts/pw-locator-patch/highlight/inner-text.ts","./src/test/scripts/pw-locator-patch/highlight/input-value.ts","./src/test/scripts/pw-locator-patch/highlight/is-checked.ts","./src/test/scripts/pw-locator-patch/highlight/is-disabled.ts","./src/test/scripts/pw-locator-patch/highlight/is-editable.ts","./src/test/scripts/pw-locator-patch/highlight/text-content.ts","./src/test/scripts/pw-locator-patch/utils/index.ts","./src/test/scripts/pw-locator-patch/vision/query.ts"],"version":"5.8.3"}