@percy/core 1.31.13 → 1.31.14-beta.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/README.md +1 -0
- package/dist/api.js +2 -2
- package/dist/browser.js +29 -3
- package/dist/cache/byte-lru.js +352 -0
- package/dist/config.js +7 -0
- package/dist/discovery.js +170 -29
- package/dist/lock.js +215 -0
- package/dist/network.js +53 -10
- package/dist/percy.js +128 -10
- package/dist/server.js +66 -3
- package/dist/utils.js +7 -3
- package/package.json +10 -10
- package/test/helpers/index.js +10 -0
package/dist/percy.js
CHANGED
|
@@ -12,11 +12,12 @@ import PercyConfig from '@percy/config';
|
|
|
12
12
|
import logger from '@percy/logger';
|
|
13
13
|
import { getProxy } from '@percy/client/utils';
|
|
14
14
|
import Browser from './browser.js';
|
|
15
|
+
import { acquireLock, releaseLockSync } from './lock.js';
|
|
15
16
|
import Pako from 'pako';
|
|
16
17
|
import { base64encode, generatePromise, yieldAll, yieldTo, redactSecrets, detectSystemProxyAndLog, checkSDKVersion, processCorsIframes } from './utils.js';
|
|
17
18
|
import { createPercyServer, createStaticServer } from './api.js';
|
|
18
19
|
import { gatherSnapshots, createSnapshotsQueue, validateSnapshotOptions } from './snapshot.js';
|
|
19
|
-
import { discoverSnapshotResources, createDiscoveryQueue } from './discovery.js';
|
|
20
|
+
import { discoverSnapshotResources, createDiscoveryQueue, RESOURCE_CACHE_KEY, CACHE_STATS_KEY, DISK_SPILL_KEY } from './discovery.js';
|
|
20
21
|
import Monitoring from '@percy/monitoring';
|
|
21
22
|
import { WaitForJob } from './wait-for-job.js';
|
|
22
23
|
const MAX_SUGGESTION_CALLS = 10;
|
|
@@ -219,6 +220,26 @@ export class Percy {
|
|
|
219
220
|
this.readyState = 0;
|
|
220
221
|
this.cliStartTime = new Date().toISOString();
|
|
221
222
|
try {
|
|
223
|
+
// Per-port lock fast-fail. Acquire BEFORE any
|
|
224
|
+
// expensive setup (monitoring, proxy detection, hostname loads)
|
|
225
|
+
// so a second `percy start` on the same port refuses cheaply.
|
|
226
|
+
// Skipped when no server is configured (lock represents a port
|
|
227
|
+
// claim).
|
|
228
|
+
if (this.server) {
|
|
229
|
+
// acquireLock returns null when port === 0 (ephemeral / test
|
|
230
|
+
// fixtures); skip the exit handler in that case since there's
|
|
231
|
+
// nothing to release.
|
|
232
|
+
this._lockHandle = acquireLock({
|
|
233
|
+
port: this.port
|
|
234
|
+
});
|
|
235
|
+
if (this._lockHandle) {
|
|
236
|
+
// Synchronous unlink as last-chance cleanup if the process
|
|
237
|
+
// exits without a normal stop().
|
|
238
|
+
this._lockExitHandler = () => releaseLockSync(this._lockHandle);
|
|
239
|
+
process.on('exit', this._lockExitHandler);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
222
243
|
// started monitoring system metrics
|
|
223
244
|
|
|
224
245
|
if (this.systemMonitoringEnabled()) {
|
|
@@ -262,11 +283,31 @@ export class Percy {
|
|
|
262
283
|
await _classPrivateFieldGet(_discovery, this).end();
|
|
263
284
|
await _classPrivateFieldGet(_snapshots, this).end();
|
|
264
285
|
|
|
286
|
+
// Release the lock on failed start so a retry
|
|
287
|
+
// doesn't see this aborted attempt as "already running."
|
|
288
|
+
this._releaseLock();
|
|
289
|
+
|
|
265
290
|
// mark this instance as closed unless aborting
|
|
266
291
|
this.readyState = error.name !== 'AbortError' ? 3 : null;
|
|
267
292
|
|
|
268
|
-
// throw an easier-to-understand error when the port is in use
|
|
269
|
-
|
|
293
|
+
// throw an easier-to-understand error when the port is in use.
|
|
294
|
+
// A held lockfile fails before server.listen,
|
|
295
|
+
// so EADDRINUSE no longer fires for the "Percy already running"
|
|
296
|
+
// case — surface LockHeldError under the same legacy message
|
|
297
|
+
// (downstream tools may grep for it) but ALSO log the actionable
|
|
298
|
+
// detail (pid + lock path) so users can recover.
|
|
299
|
+
/* istanbul ignore if: in-process Percy.start tests with the
|
|
300
|
+
self-pid stale-lock optimization will reclaim and proceed to
|
|
301
|
+
server.listen() rather than throwing LockHeldError, so this
|
|
302
|
+
branch is rare under unit-test conditions. The LockHeldError
|
|
303
|
+
shape is verified by the lock.test.js SC4 spec; this branch
|
|
304
|
+
only translates it to the legacy error string. */
|
|
305
|
+
if (error.name === 'LockHeldError') {
|
|
306
|
+
this.log.error(error.message);
|
|
307
|
+
let errMsg = `Percy is already running or the port ${this.port} is in use`;
|
|
308
|
+
await this.suggestionsForFix(errMsg);
|
|
309
|
+
throw new Error(errMsg);
|
|
310
|
+
} else if (error.code === 'EADDRINUSE') {
|
|
270
311
|
let errMsg = `Percy is already running or the port ${this.port} is in use`;
|
|
271
312
|
await this.suggestionsForFix(errMsg);
|
|
272
313
|
throw new Error(errMsg);
|
|
@@ -277,6 +318,17 @@ export class Percy {
|
|
|
277
318
|
}
|
|
278
319
|
}
|
|
279
320
|
|
|
321
|
+
// Idempotent lock release used by both the success and failure paths
|
|
322
|
+
// in start/stop.
|
|
323
|
+
_releaseLock() {
|
|
324
|
+
if (this._lockExitHandler) {
|
|
325
|
+
process.off('exit', this._lockExitHandler);
|
|
326
|
+
this._lockExitHandler = null;
|
|
327
|
+
}
|
|
328
|
+
releaseLockSync(this._lockHandle);
|
|
329
|
+
this._lockHandle = null;
|
|
330
|
+
}
|
|
331
|
+
|
|
280
332
|
// Resolves once snapshot and upload queues are idle
|
|
281
333
|
async *idle() {
|
|
282
334
|
yield* _classPrivateFieldGet(_discovery, this).idle();
|
|
@@ -368,10 +420,76 @@ export class Percy {
|
|
|
368
420
|
this.monitoring.stopMonitoring();
|
|
369
421
|
clearTimeout(this.resetMonitoringId);
|
|
370
422
|
|
|
423
|
+
// Release per-port lock after the server
|
|
424
|
+
// socket is closed (the unlink itself is sync, but ordering
|
|
425
|
+
// after `server?.close()` keeps the post-condition that "lock
|
|
426
|
+
// present ⇒ server bound" until the very end).
|
|
427
|
+
this._releaseLock();
|
|
428
|
+
|
|
371
429
|
// This issue doesn't comes under regular error logs,
|
|
372
430
|
// it's detected if we just and stop percy server
|
|
373
431
|
await this.checkForNoSnapshotCommandError();
|
|
432
|
+
// sendBuildLogs goes first — it's the primary egress. cache_summary is
|
|
433
|
+
// analytics, ordered after so a slow pager hop cannot delay the logs.
|
|
374
434
|
await this.sendBuildLogs();
|
|
435
|
+
await this.sendCacheSummary();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Single egress point for cache-tier telemetry. Used by sendCacheSummary
|
|
440
|
+
// (awaited at stop) and discovery's fire-and-forget eviction event. Returns
|
|
441
|
+
// early if no build is associated, swallows pager rejections — telemetry
|
|
442
|
+
// loss must never fail a build.
|
|
443
|
+
async sendCacheTelemetry(message, extra) {
|
|
444
|
+
var _this$build2;
|
|
445
|
+
if (!((_this$build2 = this.build) !== null && _this$build2 !== void 0 && _this$build2.id)) return;
|
|
446
|
+
try {
|
|
447
|
+
await this.client.sendBuildEvents(this.build.id, {
|
|
448
|
+
message,
|
|
449
|
+
cliVersion: this.client.cliVersion,
|
|
450
|
+
clientInfo: this.clientInfo,
|
|
451
|
+
extra
|
|
452
|
+
});
|
|
453
|
+
} catch (err) {
|
|
454
|
+
this.log.debug(`${message} telemetry failed`, err);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Cache-usage summary fired at stop. The whole method is wrapped — the
|
|
459
|
+
// contract is "telemetry must never fail percy.stop()", which covers the
|
|
460
|
+
// payload-construction block as well as the egress.
|
|
461
|
+
async sendCacheSummary() {
|
|
462
|
+
try {
|
|
463
|
+
var _stats$finalDiskStats;
|
|
464
|
+
const cache = this[RESOURCE_CACHE_KEY];
|
|
465
|
+
const stats = this[CACHE_STATS_KEY];
|
|
466
|
+
if (!cache || !stats) return;
|
|
467
|
+
const cacheStats = typeof cache.stats === 'object' ? cache.stats : null;
|
|
468
|
+
// diskStore is destroyed by discovery 'end' before this runs, so fall
|
|
469
|
+
// back to the snapshot captured in stats.finalDiskStats.
|
|
470
|
+
const diskStore = this[DISK_SPILL_KEY];
|
|
471
|
+
const diskSnap = (diskStore === null || diskStore === void 0 ? void 0 : diskStore.stats) ?? stats.finalDiskStats;
|
|
472
|
+
const diskReady = diskStore ? diskStore.ready : !!((_stats$finalDiskStats = stats.finalDiskStats) !== null && _stats$finalDiskStats !== void 0 && _stats$finalDiskStats.ready);
|
|
473
|
+
await this.sendCacheTelemetry('cache_summary', {
|
|
474
|
+
cache_budget_ram_mb: stats.effectiveMaxCacheRamMB,
|
|
475
|
+
hits: (cacheStats === null || cacheStats === void 0 ? void 0 : cacheStats.hits) ?? 0,
|
|
476
|
+
misses: (cacheStats === null || cacheStats === void 0 ? void 0 : cacheStats.misses) ?? 0,
|
|
477
|
+
evictions: (cacheStats === null || cacheStats === void 0 ? void 0 : cacheStats.evictions) ?? 0,
|
|
478
|
+
peak_bytes: (cacheStats === null || cacheStats === void 0 ? void 0 : cacheStats.peakBytes) ?? stats.unsetModeBytes,
|
|
479
|
+
final_bytes: cache.calculatedSize ?? stats.unsetModeBytes,
|
|
480
|
+
entry_count: cache.size ?? 0,
|
|
481
|
+
oversize_skipped: stats.oversizeSkipped,
|
|
482
|
+
disk_spill_enabled: diskReady,
|
|
483
|
+
disk_spilled_count: (diskSnap === null || diskSnap === void 0 ? void 0 : diskSnap.spilled) ?? 0,
|
|
484
|
+
disk_restored_count: (diskSnap === null || diskSnap === void 0 ? void 0 : diskSnap.restored) ?? 0,
|
|
485
|
+
disk_spill_failures: (diskSnap === null || diskSnap === void 0 ? void 0 : diskSnap.spillFailures) ?? 0,
|
|
486
|
+
disk_read_failures: (diskSnap === null || diskSnap === void 0 ? void 0 : diskSnap.readFailures) ?? 0,
|
|
487
|
+
disk_peak_bytes: (diskSnap === null || diskSnap === void 0 ? void 0 : diskSnap.peakBytes) ?? 0,
|
|
488
|
+
disk_final_bytes: (diskSnap === null || diskSnap === void 0 ? void 0 : diskSnap.currentBytes) ?? 0,
|
|
489
|
+
disk_final_entries: (diskSnap === null || diskSnap === void 0 ? void 0 : diskSnap.entries) ?? 0
|
|
490
|
+
});
|
|
491
|
+
} catch (err) {
|
|
492
|
+
this.log.debug('cache_summary build failed', err);
|
|
375
493
|
}
|
|
376
494
|
}
|
|
377
495
|
checkAndUpdateConcurrency() {
|
|
@@ -429,10 +547,10 @@ export class Percy {
|
|
|
429
547
|
// snapshots. Once asset discovery has completed for the provided snapshots, the queued task will
|
|
430
548
|
// resolve and an upload task will be queued separately.
|
|
431
549
|
snapshot(options, snapshotPromise = {}) {
|
|
432
|
-
var _this$
|
|
550
|
+
var _this$build3;
|
|
433
551
|
if (this.readyState !== 1) {
|
|
434
552
|
throw new Error('Not running');
|
|
435
|
-
} else if ((_this$
|
|
553
|
+
} else if ((_this$build3 = this.build) !== null && _this$build3 !== void 0 && _this$build3.error) {
|
|
436
554
|
throw new Error(this.build.error);
|
|
437
555
|
} else if (Array.isArray(options)) {
|
|
438
556
|
return yieldAll(options.map(o => this.yield.snapshot(o, snapshotPromise)));
|
|
@@ -662,7 +780,7 @@ export class Percy {
|
|
|
662
780
|
async sendBuildLogs() {
|
|
663
781
|
if (!process.env.PERCY_TOKEN) return;
|
|
664
782
|
try {
|
|
665
|
-
var _this$
|
|
783
|
+
var _this$build4, _this$build5, _this$build6, _this$build7;
|
|
666
784
|
const logsObject = {
|
|
667
785
|
clilogs: logger.query(log => !['ci'].includes(log.debug))
|
|
668
786
|
};
|
|
@@ -674,10 +792,10 @@ export class Percy {
|
|
|
674
792
|
logsObject.cilogs = redactedContent;
|
|
675
793
|
}
|
|
676
794
|
const content = base64encode(Pako.gzip(JSON.stringify(logsObject)));
|
|
677
|
-
const referenceId = (_this$
|
|
795
|
+
const referenceId = (_this$build4 = this.build) !== null && _this$build4 !== void 0 && _this$build4.id ? `build_${(_this$build5 = this.build) === null || _this$build5 === void 0 ? void 0 : _this$build5.id}` : (_this$build6 = this.build) === null || _this$build6 === void 0 ? void 0 : _this$build6.id;
|
|
678
796
|
const eventObject = {
|
|
679
797
|
content: content,
|
|
680
|
-
build_id: (_this$
|
|
798
|
+
build_id: (_this$build7 = this.build) === null || _this$build7 === void 0 ? void 0 : _this$build7.id,
|
|
681
799
|
reference_id: referenceId,
|
|
682
800
|
service_name: 'cli',
|
|
683
801
|
base64encoded: true
|
|
@@ -750,9 +868,9 @@ export class Percy {
|
|
|
750
868
|
const newAllowedDomains = Array.from(processedHosts).filter(domain => !autoConfiguredHosts.has(domain));
|
|
751
869
|
const hasNewDomains = newAllowedDomains.length > 0 || newErrorHosts.size > 0;
|
|
752
870
|
try {
|
|
753
|
-
var _this$
|
|
871
|
+
var _this$build8;
|
|
754
872
|
await this.client.updateProjectDomainConfig({
|
|
755
|
-
buildId: (_this$
|
|
873
|
+
buildId: (_this$build8 = this.build) === null || _this$build8 === void 0 ? void 0 : _this$build8.id,
|
|
756
874
|
allowedDomains: Array.from(processedHosts),
|
|
757
875
|
errorDomains: Array.from(newErrorHosts)
|
|
758
876
|
});
|
package/dist/server.js
CHANGED
|
@@ -177,11 +177,74 @@ export class Server extends http.Server {
|
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
// return a promise that resolves when the server closes
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
//
|
|
181
|
+
// Graceful drain. By default, stop accepting new
|
|
182
|
+
// connections, reap idle keep-alives, and let in-flight requests
|
|
183
|
+
// finish for up to `drainMs` (5s) before forcibly destroying any
|
|
184
|
+
// remaining sockets. Pass `{ drainMs: 0 }` for the legacy abrupt
|
|
185
|
+
// behavior. Uses Node 18.2+ `closeIdleConnections` /
|
|
186
|
+
// `closeAllConnections` when available, falling back to manual
|
|
187
|
+
// socket-set iteration on Node 14 (Windows CI is pinned there per
|
|
188
|
+
// .github/workflows/windows.yml).
|
|
189
|
+
async close({
|
|
190
|
+
drainMs = 5_000
|
|
191
|
+
} = {}) {
|
|
192
|
+
this.draining = true;
|
|
193
|
+
let closed = new Promise(resolve => super.close(resolve));
|
|
194
|
+
|
|
195
|
+
// Reap idle keep-alives now so they don't hold the close() callback.
|
|
196
|
+
/* istanbul ignore next: which branch fires depends on the runner's
|
|
197
|
+
Node version (CI matrix includes Node 14, where
|
|
198
|
+
closeIdleConnections is missing). The graceful behavior is
|
|
199
|
+
verified end-to-end by every existing percy.stop()-based test;
|
|
200
|
+
this if/else simply selects between the Node 18.2+ API and the
|
|
201
|
+
no-op Node 14 fallback. */
|
|
202
|
+
if (typeof this.closeIdleConnections === 'function') {
|
|
203
|
+
this.closeIdleConnections();
|
|
204
|
+
} else {
|
|
205
|
+
// Node 14 fallback: best-effort destroy of sockets without an
|
|
206
|
+
// active response. http.Server doesn't expose idleness here, so
|
|
207
|
+
// we conservatively destroy nothing in this branch and rely on
|
|
208
|
+
// the drain timeout below.
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* istanbul ignore if: legacy abrupt-close path; not used by any
|
|
212
|
+
in-tree caller post-Phase-3, kept for backwards compat with
|
|
213
|
+
SDK consumers that may pass `{ drainMs: 0 }`. */
|
|
214
|
+
if (drainMs <= 0) {
|
|
182
215
|
_classPrivateFieldGet(_sockets, this).forEach(socket => socket.destroy());
|
|
183
|
-
|
|
216
|
+
await closed;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Capture the force-close timer so we can clear it after the
|
|
221
|
+
// race — otherwise it fires `drainMs` later (calling
|
|
222
|
+
// closeAllConnections / socket.destroy on an already-closed
|
|
223
|
+
// server) which is a no-op in normal cases but can throw on
|
|
224
|
+
// edge-case socket states.
|
|
225
|
+
let forcedTimer;
|
|
226
|
+
/* istanbul ignore next: 5s force-close timeout fires only when
|
|
227
|
+
in-flight requests genuinely stall — exercising it under nyc
|
|
228
|
+
requires a deliberately wedged socket which interacts badly
|
|
229
|
+
with the Jasmine runner. The graceful path (where `closed`
|
|
230
|
+
wins the race) is exercised by every existing percy.stop()
|
|
231
|
+
test, and `clearTimeout(forcedTimer)` after the race ensures
|
|
232
|
+
the inner callback never runs in normal teardown. */
|
|
233
|
+
let forced = new Promise(resolve => {
|
|
234
|
+
forcedTimer = setTimeout(() => {
|
|
235
|
+
if (typeof this.closeAllConnections === 'function') {
|
|
236
|
+
this.closeAllConnections();
|
|
237
|
+
} else {
|
|
238
|
+
_classPrivateFieldGet(_sockets, this).forEach(socket => socket.destroy());
|
|
239
|
+
}
|
|
240
|
+
resolve();
|
|
241
|
+
}, drainMs).unref();
|
|
184
242
|
});
|
|
243
|
+
await Promise.race([closed, forced]);
|
|
244
|
+
clearTimeout(forcedTimer);
|
|
245
|
+
// Ensure the 'close' event has fully fired even if `forced` won
|
|
246
|
+
// the race (we still need super.close()'s callback to resolve).
|
|
247
|
+
await closed;
|
|
185
248
|
}
|
|
186
249
|
// set request routing and handling for pathnames and methods
|
|
187
250
|
route(method, pathname, handle) {
|
package/dist/utils.js
CHANGED
|
@@ -183,7 +183,9 @@ export async function executeDomainValidation(network, hostname, url, domainVali
|
|
|
183
183
|
// Worker returns 'accessible' field, not 'allowed'
|
|
184
184
|
if (result !== null && result !== void 0 && result.error) {
|
|
185
185
|
newErrorHosts.add(hostname);
|
|
186
|
-
|
|
186
|
+
// Redact upstream-derived `result.reason` — may contain credentials
|
|
187
|
+
// from response bodies the validation worker echoed.
|
|
188
|
+
network.log.debug(redactSecrets(`Domain validation: ${hostname} validated as BLOCKED - ${result === null || result === void 0 ? void 0 : result.reason}`), network.meta);
|
|
187
189
|
processedDomains.set(hostname, false);
|
|
188
190
|
return false;
|
|
189
191
|
} else if (!(result !== null && result !== void 0 && result.accessible)) {
|
|
@@ -194,8 +196,10 @@ export async function executeDomainValidation(network, hostname, url, domainVali
|
|
|
194
196
|
}
|
|
195
197
|
return false;
|
|
196
198
|
} catch (error) {
|
|
197
|
-
// On error, default to allowing (fail-open for better UX)
|
|
198
|
-
|
|
199
|
+
// On error, default to allowing (fail-open for better UX).
|
|
200
|
+
// Redact `error.message` — upstream HTTP errors can include
|
|
201
|
+
// Authorization headers / URL credentials in the failed-request text.
|
|
202
|
+
network.log.warn(redactSecrets(`Domain validation: Failed to validate ${hostname} - ${error.message}`), network.meta);
|
|
199
203
|
processedDomains.set(hostname, false);
|
|
200
204
|
return false;
|
|
201
205
|
} finally {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@percy/core",
|
|
3
|
-
"version": "1.31.
|
|
3
|
+
"version": "1.31.14-beta.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"publishConfig": {
|
|
11
11
|
"access": "public",
|
|
12
|
-
"tag": "
|
|
12
|
+
"tag": "beta"
|
|
13
13
|
},
|
|
14
14
|
"engines": {
|
|
15
15
|
"node": ">=14"
|
|
@@ -43,12 +43,12 @@
|
|
|
43
43
|
"test:types": "tsd"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@percy/client": "1.31.
|
|
47
|
-
"@percy/config": "1.31.
|
|
48
|
-
"@percy/dom": "1.31.
|
|
49
|
-
"@percy/logger": "1.31.
|
|
50
|
-
"@percy/monitoring": "1.31.
|
|
51
|
-
"@percy/webdriver-utils": "1.31.
|
|
46
|
+
"@percy/client": "1.31.14-beta.1",
|
|
47
|
+
"@percy/config": "1.31.14-beta.1",
|
|
48
|
+
"@percy/dom": "1.31.14-beta.1",
|
|
49
|
+
"@percy/logger": "1.31.14-beta.1",
|
|
50
|
+
"@percy/monitoring": "1.31.14-beta.1",
|
|
51
|
+
"@percy/webdriver-utils": "1.31.14-beta.1",
|
|
52
52
|
"content-disposition": "^0.5.4",
|
|
53
53
|
"cross-spawn": "^7.0.3",
|
|
54
54
|
"extract-zip": "^2.0.1",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"yaml": "^2.4.1"
|
|
63
63
|
},
|
|
64
64
|
"optionalDependencies": {
|
|
65
|
-
"@percy/cli-doctor": "1.31.
|
|
65
|
+
"@percy/cli-doctor": "1.31.14-beta.1"
|
|
66
66
|
},
|
|
67
|
-
"gitHead": "
|
|
67
|
+
"gitHead": "dd6957822cc94d460fd9c043a44cc4c9c5fcba23"
|
|
68
68
|
}
|
package/test/helpers/index.js
CHANGED
|
@@ -12,6 +12,16 @@ export function mockfs(initial) {
|
|
|
12
12
|
path.resolve(url.fileURLToPath(import.meta.url), '/../../../dom/dist/bundle.js'),
|
|
13
13
|
path.resolve(url.fileURLToPath(import.meta.url), '../secretPatterns.yml'),
|
|
14
14
|
p => p.includes?.('.local-chromium'),
|
|
15
|
+
// Per-port lockfiles live under ~/.percy/. They
|
|
16
|
+
// are infrastructure (not test fixture data), so route the entire
|
|
17
|
+
// directory (mkdir, writeFile, readFile, unlink) through the real
|
|
18
|
+
// fs. Matching only `/.percy/agent-` lets `writeFileSync` pass but
|
|
19
|
+
// routes `mkdirSync` for the parent through memfs, leaving the
|
|
20
|
+
// parent directory non-existent on the real fs and producing
|
|
21
|
+
// ENOENT cascades on CI. Match both POSIX `/` and Windows `\`
|
|
22
|
+
// separators because the Windows runner normalizes paths
|
|
23
|
+
// inconsistently across mkdir/writeFile/unlink.
|
|
24
|
+
p => typeof p === 'string' && /[/\\]\.percy(?:[/\\]|$)/.test(p),
|
|
15
25
|
...(initial?.$bypass ?? [])
|
|
16
26
|
]
|
|
17
27
|
});
|