@empiricalrun/playwright-utils 0.46.4 → 0.47.1

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.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 370c14a: chore: more logs on disk space
8
+
9
+ ## 0.47.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 9a1c6e3: fix: attachment cleanup should track usage before uploads
14
+
3
15
  ## 0.46.4
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":"AAgDA;;;;;;;;;;;;;;;;;;;;;;;;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,61 @@ 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
+ return (0, child_process_1.execSync)("df -h / | tail -1", {
31
+ encoding: "utf8",
32
+ timeout: 5000,
33
+ }).trim();
34
+ }
35
+ catch {
36
+ return "unavailable";
37
+ }
38
+ }
39
+ function getTracingDirCount() {
40
+ try {
41
+ const output = (0, child_process_1.execSync)("ls -d /tmp/playwright-tracing-* 2>/dev/null | wc -l", { encoding: "utf8", timeout: 3000 }).trim();
42
+ return parseInt(output, 10) || 0;
43
+ }
44
+ catch {
45
+ return 0;
46
+ }
47
+ }
10
48
  /**
11
- * Tracks uploaded attachments and evicts the oldest ones from disk
49
+ * Tracks test attachments and evicts the oldest uploaded ones from disk
12
50
  * when total size exceeds the configured limit.
13
51
  *
52
+ * Two-phase tracking:
53
+ * 1. register() — called immediately when an attachment is created.
54
+ * Adds the file to the total size accounting so the LRU knows about
55
+ * all disk usage, even while uploads are still queued.
56
+ * 2. markUploaded() — called after the file is uploaded to R2.
57
+ * Marks the file as safe to evict (delete from disk).
58
+ *
59
+ * Eviction only deletes files that have been uploaded. This ensures we
60
+ * never delete a file before its upload completes.
61
+ *
14
62
  * Why this exists:
15
63
  * Test attachments (videos, traces, screenshots) accumulate on disk during a
16
64
  * test run. On Fargate workers with 50 GB ephemeral storage, large test suites
@@ -18,48 +66,113 @@ const DEFAULT_DISK_LIMIT_BYTES = 10 * 1024 * 1024 * 1024; // 10 GB
18
66
  * safely delete them from disk — but we can't delete them immediately because
19
67
  * Playwright's blob reporter reads them during onEnd to embed as resources/.
20
68
  *
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
69
+ * The blob reporter's statSync guard (Playwright 1.57+) gracefully skips
70
+ * missing files, and patchBlobZip replaces resources/ with
24
71
  * _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
72
  */
30
73
  class AttachmentCleanup {
31
- _entries = [];
74
+ _pending = new Map();
75
+ _uploaded = [];
32
76
  _totalSize = 0;
33
77
  _limitBytes;
34
- constructor(limitBytes = DEFAULT_DISK_LIMIT_BYTES) {
78
+ _registeredCount = 0;
79
+ _uploadedCount = 0;
80
+ _evictedCount = 0;
81
+ _evictedBytes = 0;
82
+ _diskLogTimer = null;
83
+ _onEvict = null;
84
+ constructor(limitBytes = getDefaultDiskLimitBytes()) {
35
85
  this._limitBytes = limitBytes;
86
+ logger_1.logger.info(`[AttachmentCleanup] Initialized with limit: ${formatBytes(limitBytes)}`);
87
+ this._diskLogTimer = setInterval(() => {
88
+ this._logDiskStatus();
89
+ }, DISK_LOG_INTERVAL_MS);
90
+ // Don't keep the process alive just for this timer
91
+ this._diskLogTimer.unref();
36
92
  }
37
- track(attachment) {
93
+ /**
94
+ * Phase 1: Register an attachment immediately when it's created.
95
+ * Adds to total size accounting and attempts eviction of already-uploaded files.
96
+ */
97
+ register(attachment) {
38
98
  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
- }
99
+ if (!attachment.path)
100
+ return;
101
+ if (this._pending.has(attachment.path))
102
+ return;
103
+ const stat = fs_1.default.statSync(attachment.path);
104
+ const sizeBytes = stat.size;
105
+ this._pending.set(attachment.path, sizeBytes);
106
+ this._totalSize += sizeBytes;
107
+ this._registeredCount++;
108
+ this._evict();
46
109
  }
47
110
  catch {
48
- // Ignore errors
111
+ // Ignore errors (file may not exist)
49
112
  }
50
113
  }
114
+ /**
115
+ * Phase 2: Mark an attachment as uploaded (safe to evict).
116
+ * Moves from pending to the uploaded queue and attempts eviction.
117
+ */
118
+ markUploaded(attachmentPath) {
119
+ const size = this._pending.get(attachmentPath);
120
+ if (size === undefined)
121
+ return;
122
+ this._pending.delete(attachmentPath);
123
+ this._uploaded.push({ path: attachmentPath, size });
124
+ this._uploadedCount++;
125
+ this._evict();
126
+ }
51
127
  _evict() {
52
- while (this._totalSize > this._limitBytes && this._entries.length > 0) {
53
- const oldest = this._entries.shift();
128
+ while (this._totalSize > this._limitBytes && this._uploaded.length > 0) {
129
+ const oldest = this._uploaded[0];
54
130
  try {
55
131
  fs_1.default.unlinkSync(oldest.path);
56
- logger_1.logger.debug(`[AttachmentCleanup] Evicted ${oldest.path} (${oldest.size} bytes)`);
132
+ this._uploaded.shift();
133
+ this._evictedCount++;
134
+ this._evictedBytes += oldest.size;
135
+ this._totalSize -= oldest.size;
136
+ logger_1.logger.info(`[AttachmentCleanup] Evicted ${oldest.path} (${formatBytes(oldest.size)}), totalSize=${formatBytes(this._totalSize)}, pending=${this._pending.size}, uploaded=${this._uploaded.length}`);
137
+ this._onEvict?.(oldest.path);
57
138
  }
58
- catch {
59
- // File may already be gone
139
+ catch (err) {
140
+ const code = err?.code;
141
+ if (code === "ENOENT") {
142
+ this._uploaded.shift();
143
+ this._totalSize -= oldest.size;
144
+ }
145
+ else {
146
+ logger_1.logger.warn(`[AttachmentCleanup] Failed to evict ${oldest.path}: ${code ?? err}`);
147
+ break;
148
+ }
60
149
  }
61
- this._totalSize -= oldest.size;
62
150
  }
63
151
  }
152
+ _logDiskStatus() {
153
+ 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()}, tracingDirs=${getTracingDirCount()}`);
154
+ }
155
+ logSummary() {
156
+ if (this._diskLogTimer) {
157
+ clearInterval(this._diskLogTimer);
158
+ this._diskLogTimer = null;
159
+ }
160
+ 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()}`);
161
+ }
162
+ set onEvict(callback) {
163
+ this._onEvict = callback;
164
+ }
165
+ get stats() {
166
+ return {
167
+ registeredCount: this._registeredCount,
168
+ uploadedCount: this._uploadedCount,
169
+ pendingCount: this._pending.size,
170
+ evictedCount: this._evictedCount,
171
+ evictedBytes: this._evictedBytes,
172
+ activeCount: this._uploaded.length,
173
+ totalSize: this._totalSize,
174
+ limitBytes: this._limitBytes,
175
+ };
176
+ }
64
177
  }
65
178
  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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empiricalrun/playwright-utils",
3
- "version": "0.46.4",
3
+ "version": "0.47.1",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -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"}