@ai-ecoverse/slicc-browsers 1.22.0 → 1.22.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 +3 -3
- package/fixture-server.mjs +7 -0
- package/guard-runner.mjs +609 -122
- package/guard-vectors.json +1716 -9
- package/package.json +5 -4
- package/protected-worker.mjs +511 -0
- package/schema/guard-vectors.schema.json +79 -0
package/guard-runner.mjs
CHANGED
|
@@ -31,12 +31,48 @@ import {
|
|
|
31
31
|
} from './guard-driver.mjs';
|
|
32
32
|
import { createObserverTarget, discoveryViolation } from './observer-target.mjs';
|
|
33
33
|
import { protectedErrorPage } from './protected-error-page.mjs';
|
|
34
|
+
import {
|
|
35
|
+
leftoverPersistentWorker,
|
|
36
|
+
persistentWorkerTypes,
|
|
37
|
+
probeHarnessIncomplete,
|
|
38
|
+
probeWorkerSessions,
|
|
39
|
+
protectedWorker,
|
|
40
|
+
releasePersistentWorkerSessions,
|
|
41
|
+
workerTypeForKind,
|
|
42
|
+
} from './protected-worker.mjs';
|
|
34
43
|
import { redirectSecret } from './redirect-secret.mjs';
|
|
35
44
|
import { canonicalURL, unparseableURLs } from './unparseable-url.mjs';
|
|
36
45
|
|
|
46
|
+
export { leftoverPersistentWorker, releasePersistentWorkerSessions, workerTypeForKind };
|
|
47
|
+
|
|
37
48
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
38
49
|
const namedResultMs = 10000;
|
|
39
50
|
const observeMs = 5000;
|
|
51
|
+
|
|
52
|
+
function autoAttachSession(events, start, targetId) {
|
|
53
|
+
return events
|
|
54
|
+
.slice(start)
|
|
55
|
+
.filter(
|
|
56
|
+
(event) =>
|
|
57
|
+
event.method === 'Target.attachedToTarget' &&
|
|
58
|
+
event.params?.targetInfo?.targetId === targetId &&
|
|
59
|
+
event.params?.sessionId
|
|
60
|
+
)
|
|
61
|
+
.at(-1)?.params?.sessionId;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function reusableAttachSession(step, out, params, events, start) {
|
|
65
|
+
if (!step?.reuseSession || step.cdp !== 'Target.attachToTarget' || !params?.targetId)
|
|
66
|
+
return undefined;
|
|
67
|
+
if (out?.ok && out.result?.sessionId) return undefined;
|
|
68
|
+
const message = String(out?.error?.message ?? '');
|
|
69
|
+
if (/refused|not allowed/i.test(message)) return undefined;
|
|
70
|
+
const already =
|
|
71
|
+
(out?.ok === true && !out.result?.sessionId) ||
|
|
72
|
+
/already attached|attached already|Target is attaching or attached/i.test(message);
|
|
73
|
+
if (!already) return undefined;
|
|
74
|
+
return autoAttachSession(events, start, params.targetId);
|
|
75
|
+
}
|
|
40
76
|
const browserUiAttempts = 3;
|
|
41
77
|
const browserUiMs = 5000;
|
|
42
78
|
const q = JSON.stringify;
|
|
@@ -171,12 +207,71 @@ class Execution {
|
|
|
171
207
|
this.refusalScale = options.refusalScale ?? 1;
|
|
172
208
|
this.childRuns = new Map();
|
|
173
209
|
this.named = new Map();
|
|
210
|
+
this.eventMarks = new Map();
|
|
174
211
|
this.processed = new Set();
|
|
175
212
|
this.eventStart = this.client.events.length;
|
|
176
213
|
this.actionEventStart = this.eventStart;
|
|
177
214
|
this.requestStart = undefined;
|
|
178
215
|
this.trace = [];
|
|
179
216
|
this.stopped = false;
|
|
217
|
+
this.cdpTrace = [];
|
|
218
|
+
this.cdpSessionTypes = new Map();
|
|
219
|
+
this.cdpTraceInstalled = false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
note(message) {
|
|
223
|
+
if (this.notes.length >= 80) this.notes.shift();
|
|
224
|
+
Array.prototype.push.call(this.notes, message);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
installClientTrace() {
|
|
228
|
+
if (!this.spawnWorkerKind() || this.cdpTraceInstalled) return;
|
|
229
|
+
this.cdpTraceInstalled = true;
|
|
230
|
+
const prev = this.client.__sliccOnSend;
|
|
231
|
+
this.client.__sliccBoundGetFrameTree = true;
|
|
232
|
+
this.client.__sliccOnSend = (entry) => {
|
|
233
|
+
prev?.(entry);
|
|
234
|
+
this.recordCdpTrace(entry);
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
recordCdpTrace(entry) {
|
|
239
|
+
let type = entry.sessionId ? this.cdpSessionTypes.get(entry.sessionId) : 'browser';
|
|
240
|
+
if (entry.sessionId && type === undefined) {
|
|
241
|
+
type = '';
|
|
242
|
+
const events = this.client.events;
|
|
243
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
244
|
+
const event = events[i];
|
|
245
|
+
if (
|
|
246
|
+
event.method === 'Target.attachedToTarget' &&
|
|
247
|
+
event.params?.sessionId === entry.sessionId
|
|
248
|
+
) {
|
|
249
|
+
type = event.params.targetInfo?.type ?? '';
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
this.cdpSessionTypes.set(entry.sessionId, type);
|
|
254
|
+
}
|
|
255
|
+
if (this.cdpTrace.length >= 40) this.cdpTrace.shift();
|
|
256
|
+
this.cdpTrace.push({
|
|
257
|
+
method: entry.method,
|
|
258
|
+
sessionId: entry.sessionId ?? '',
|
|
259
|
+
type: type || (entry.sessionId ? '?' : 'browser'),
|
|
260
|
+
ms: entry.ms,
|
|
261
|
+
status: String(entry.status).slice(0, 200),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
noteClientTrace() {
|
|
266
|
+
if (!this.cdpTrace.length) return;
|
|
267
|
+
this.note(
|
|
268
|
+
`client cdp: ${this.cdpTrace
|
|
269
|
+
.map(
|
|
270
|
+
(call) =>
|
|
271
|
+
`${call.method}@${call.sessionId || 'browser'}:${call.type || '?'} ${call.ms}ms ${call.status}`
|
|
272
|
+
)
|
|
273
|
+
.join(' | ')}`
|
|
274
|
+
);
|
|
180
275
|
}
|
|
181
276
|
|
|
182
277
|
actor() {
|
|
@@ -199,8 +294,7 @@ class Execution {
|
|
|
199
294
|
);
|
|
200
295
|
return read.ok && read.result.targetInfo?.attached === false;
|
|
201
296
|
}, 5000);
|
|
202
|
-
if (!released)
|
|
203
|
-
this.notes.push(`client exit: the page stayed attached for ${this.bound(5000)}ms`);
|
|
297
|
+
if (!released) this.note(`client exit: the page stayed attached for ${this.bound(5000)}ms`);
|
|
204
298
|
const { sessionId } = await this.reader.send('Target.attachToTarget', {
|
|
205
299
|
targetId: this.pageTarget,
|
|
206
300
|
flatten: true,
|
|
@@ -407,53 +501,55 @@ class Execution {
|
|
|
407
501
|
);
|
|
408
502
|
}
|
|
409
503
|
|
|
504
|
+
async readerFrameTree(targetId, label) {
|
|
505
|
+
const attached = await outcome(
|
|
506
|
+
this.reader.send('Target.attachToTarget', { targetId, flatten: true })
|
|
507
|
+
);
|
|
508
|
+
if (!attached.ok) return { ok: false, sessionId: undefined, frameTree: undefined };
|
|
509
|
+
const sessionId = attached.result.sessionId;
|
|
510
|
+
await outcome(this.reader.send('Runtime.runIfWaitingForDebugger', {}, sessionId));
|
|
511
|
+
const read = await outcome(this.reader.send('Page.getFrameTree', {}, sessionId));
|
|
512
|
+
if (!read.ok) {
|
|
513
|
+
await outcome(this.reader.send('Target.detachFromTarget', { sessionId }));
|
|
514
|
+
throw new Error(
|
|
515
|
+
`Page.getFrameTree timed out on ${label} target ${targetId} session ${sessionId}: ${read.error?.message ?? 'unknown'}`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
return { ok: true, sessionId, frameTree: read.result.frameTree };
|
|
519
|
+
}
|
|
520
|
+
|
|
410
521
|
async blocked(spec, observer) {
|
|
411
522
|
const urls = (spec.anyOf ?? [spec.url]).map(address);
|
|
412
523
|
if (observer === 'client') return this.clientBlocked(spec, urls);
|
|
413
524
|
const targetId = spec.session === 'child' ? this.targets.at(-1) : this.pageTarget;
|
|
414
525
|
if (!targetId) return false;
|
|
415
|
-
const attached = await
|
|
416
|
-
this.reader.send('Target.attachToTarget', { targetId, flatten: true })
|
|
417
|
-
);
|
|
526
|
+
const attached = await this.readerFrameTree(targetId, 'blocked page');
|
|
418
527
|
if (!attached.ok) return false;
|
|
419
528
|
try {
|
|
420
|
-
const read = await outcome(
|
|
421
|
-
this.reader.send('Page.getFrameTree', {}, attached.result.sessionId)
|
|
422
|
-
);
|
|
423
529
|
const blockedFrame = (frame) =>
|
|
424
530
|
Boolean(
|
|
425
531
|
frame && urls.includes(frame.unreachableUrl) && frame.url.startsWith('chrome-error://')
|
|
426
532
|
);
|
|
427
533
|
const visit = (tree) =>
|
|
428
534
|
Boolean(tree && (blockedFrame(tree.frame) || (tree.childFrames ?? []).some(visit)));
|
|
429
|
-
if (!read.ok) return false;
|
|
430
535
|
if (spec.frame) {
|
|
431
|
-
if ((
|
|
536
|
+
if ((attached.frameTree.childFrames ?? []).some(visit)) return true;
|
|
432
537
|
return Boolean(await this.blockedFrameTarget(urls, targetId));
|
|
433
538
|
}
|
|
434
|
-
return blockedFrame(
|
|
539
|
+
return blockedFrame(attached.frameTree.frame);
|
|
435
540
|
} finally {
|
|
436
|
-
await outcome(
|
|
437
|
-
this.reader.send('Target.detachFromTarget', { sessionId: attached.result.sessionId })
|
|
438
|
-
);
|
|
541
|
+
await outcome(this.reader.send('Target.detachFromTarget', { sessionId: attached.sessionId }));
|
|
439
542
|
}
|
|
440
543
|
}
|
|
441
544
|
|
|
442
545
|
async frameIds(targetId) {
|
|
443
|
-
const attached = await
|
|
444
|
-
this.reader.send('Target.attachToTarget', { targetId, flatten: true })
|
|
445
|
-
);
|
|
546
|
+
const attached = await this.readerFrameTree(targetId, 'frameIds page');
|
|
446
547
|
if (!attached.ok) return [];
|
|
447
548
|
try {
|
|
448
|
-
const read = await outcome(
|
|
449
|
-
this.reader.send('Page.getFrameTree', {}, attached.result.sessionId)
|
|
450
|
-
);
|
|
451
549
|
const ids = (tree) => [tree.frame.id, ...(tree.childFrames ?? []).flatMap(ids)];
|
|
452
|
-
return
|
|
550
|
+
return ids(attached.frameTree);
|
|
453
551
|
} finally {
|
|
454
|
-
await outcome(
|
|
455
|
-
this.reader.send('Target.detachFromTarget', { sessionId: attached.result.sessionId })
|
|
456
|
-
);
|
|
552
|
+
await outcome(this.reader.send('Target.detachFromTarget', { sessionId: attached.sessionId }));
|
|
457
553
|
}
|
|
458
554
|
}
|
|
459
555
|
|
|
@@ -463,21 +559,16 @@ class Execution {
|
|
|
463
559
|
if (!listed.ok) return undefined;
|
|
464
560
|
for (const info of listed.result.targetInfos) {
|
|
465
561
|
if (info.type !== 'iframe' || !parents.includes(info.parentFrameId)) continue;
|
|
466
|
-
|
|
467
|
-
this.reader.send('Target.attachToTarget', { targetId: info.targetId, flatten: true })
|
|
468
|
-
);
|
|
469
|
-
if (!attached.ok) continue;
|
|
562
|
+
let sessionId;
|
|
470
563
|
try {
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const frame =
|
|
564
|
+
const attached = await this.readerFrameTree(info.targetId, `iframe ${info.type}`);
|
|
565
|
+
if (!attached.ok) continue;
|
|
566
|
+
sessionId = attached.sessionId;
|
|
567
|
+
const frame = attached.frameTree?.frame;
|
|
475
568
|
if (frame?.url.startsWith('chrome-error://') && urls.includes(frame.unreachableUrl))
|
|
476
569
|
return info.targetId;
|
|
477
570
|
} finally {
|
|
478
|
-
await outcome(
|
|
479
|
-
this.reader.send('Target.detachFromTarget', { sessionId: attached.result.sessionId })
|
|
480
|
-
);
|
|
571
|
+
if (sessionId) await outcome(this.reader.send('Target.detachFromTarget', { sessionId }));
|
|
481
572
|
}
|
|
482
573
|
}
|
|
483
574
|
return undefined;
|
|
@@ -531,7 +622,7 @@ class Execution {
|
|
|
531
622
|
} catch (error) {
|
|
532
623
|
const message = String(error?.message ?? error);
|
|
533
624
|
if (!/timed? ?out/i.test(message)) throw error;
|
|
534
|
-
this.
|
|
625
|
+
this.note(`Target.createBrowserContext retried once after: ${message}`);
|
|
535
626
|
return (await client.send('Target.createBrowserContext', {})).browserContextId;
|
|
536
627
|
}
|
|
537
628
|
}
|
|
@@ -593,17 +684,38 @@ class Execution {
|
|
|
593
684
|
}
|
|
594
685
|
}
|
|
595
686
|
|
|
687
|
+
async resumeWaiting(from = this.eventStart) {
|
|
688
|
+
for (const event of this.client.events.slice(from)) {
|
|
689
|
+
if (
|
|
690
|
+
event.method === 'Target.attachedToTarget' &&
|
|
691
|
+
event.params?.waitingForDebugger &&
|
|
692
|
+
event.params?.sessionId
|
|
693
|
+
)
|
|
694
|
+
await outcome(
|
|
695
|
+
this.client.send('Runtime.runIfWaitingForDebugger', {}, event.params.sessionId)
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
596
700
|
adopt(event) {
|
|
597
701
|
const info = event.params.targetInfo;
|
|
598
702
|
const child = event.params.sessionId;
|
|
599
703
|
if (info?.type === 'browser_ui') return;
|
|
600
704
|
const paused = event.params.waitingForDebugger || this.tabSessions.has(event.sessionId);
|
|
705
|
+
const workerRow = Boolean(this.spawnWorkerKind());
|
|
601
706
|
if (this.expectWhere && !this.childWhere) {
|
|
602
707
|
this.deferred.push(event);
|
|
603
708
|
return;
|
|
604
709
|
}
|
|
605
|
-
if (
|
|
710
|
+
if (
|
|
711
|
+
info?.browserContextId &&
|
|
712
|
+
this.vars.context &&
|
|
713
|
+
info.browserContextId !== this.vars.context
|
|
714
|
+
) {
|
|
715
|
+
if (paused && workerRow)
|
|
716
|
+
this.pending.push(outcome(this.client.send('Runtime.runIfWaitingForDebugger', {}, child)));
|
|
606
717
|
return;
|
|
718
|
+
}
|
|
607
719
|
const matched = !this.childWhere || this.childMatches(info);
|
|
608
720
|
const isTab = info?.type === 'tab';
|
|
609
721
|
if (!matched && !isTab) {
|
|
@@ -620,6 +732,27 @@ class Execution {
|
|
|
620
732
|
? this.childSteps
|
|
621
733
|
: this.childSteps.filter((step) => step.cdp === 'Target.setAutoAttach');
|
|
622
734
|
const started = (async () => {
|
|
735
|
+
if (workerRow) {
|
|
736
|
+
if (paused) await outcome(this.client.send('Runtime.runIfWaitingForDebugger', {}, child));
|
|
737
|
+
const reload = nested.findIndex((step) => step.cdp === 'Page.reload');
|
|
738
|
+
if (reload < 0) await this.execute(nested, child);
|
|
739
|
+
else {
|
|
740
|
+
await this.execute(nested.slice(0, reload), child);
|
|
741
|
+
const start = this.client.events.length;
|
|
742
|
+
await this.poll(
|
|
743
|
+
() =>
|
|
744
|
+
this.client.events
|
|
745
|
+
.slice(start)
|
|
746
|
+
.some((e) => e.method === 'Page.loadEventFired' && e.sessionId === child),
|
|
747
|
+
1500
|
|
748
|
+
);
|
|
749
|
+
await this.execute(nested.slice(reload), child);
|
|
750
|
+
}
|
|
751
|
+
const resumed = this.client.events.length;
|
|
752
|
+
if (nested.some((step) => step.cdp === 'Page.enable'))
|
|
753
|
+
await this.poll(() => this.firstLoad(child, resumed), 5000);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
623
756
|
const reload = nested.findIndex((step) => step.cdp === 'Page.reload');
|
|
624
757
|
if (reload < 0) await this.execute(nested, child);
|
|
625
758
|
else {
|
|
@@ -639,7 +772,13 @@ class Execution {
|
|
|
639
772
|
if (paused) await outcome(this.client.send('Runtime.runIfWaitingForDebugger', {}, child));
|
|
640
773
|
if (nested.some((step) => step.cdp === 'Page.enable'))
|
|
641
774
|
await this.poll(() => this.firstLoad(child, resumed), 5000);
|
|
642
|
-
})().catch((error) =>
|
|
775
|
+
})().catch((error) =>
|
|
776
|
+
this.errors.push(
|
|
777
|
+
workerRow
|
|
778
|
+
? `adopt ${info?.type ?? 'target'} ${child}: ${String(error.message ?? error)}`
|
|
779
|
+
: String(error.message ?? error)
|
|
780
|
+
)
|
|
781
|
+
);
|
|
643
782
|
this.childRuns.set(child, started);
|
|
644
783
|
this.pending.push(started);
|
|
645
784
|
}
|
|
@@ -704,7 +843,11 @@ class Execution {
|
|
|
704
843
|
}
|
|
705
844
|
|
|
706
845
|
childMatches(info) {
|
|
707
|
-
if (this.childWhere.
|
|
846
|
+
if (this.childWhere.types !== undefined) {
|
|
847
|
+
if (!this.childWhere.types.includes(info?.type)) return false;
|
|
848
|
+
} else if (this.childWhere.type !== undefined) {
|
|
849
|
+
if (this.childWhere.type !== info?.type) return false;
|
|
850
|
+
}
|
|
708
851
|
if (this.childWhere.openerId === 'page') return info?.openerId === this.pageTarget;
|
|
709
852
|
if (this.childWhere.urlPrefix !== undefined)
|
|
710
853
|
return String(info?.url ?? '').startsWith(this.childWhere.urlPrefix);
|
|
@@ -791,7 +934,7 @@ class Execution {
|
|
|
791
934
|
Boolean(childSession) && !out.ok && this.detached(childSession)
|
|
792
935
|
);
|
|
793
936
|
if ((step.isolatedWorld || step.observer) && !out.ok)
|
|
794
|
-
this.
|
|
937
|
+
this.note(
|
|
795
938
|
`${step.isolatedWorld ? `isolatedWorld ${step.isolatedWorld}` : `observer ${step.observer}`}: ${out.error?.message ?? 'failed'}`
|
|
796
939
|
);
|
|
797
940
|
this.options.debug?.({ step, out, side: this.side });
|
|
@@ -826,7 +969,7 @@ class Execution {
|
|
|
826
969
|
return Boolean(next);
|
|
827
970
|
}, browserUiMs);
|
|
828
971
|
if (!next) return undefined;
|
|
829
|
-
this.
|
|
972
|
+
this.note(
|
|
830
973
|
`{browserUi} ${vanished} was gone when ${step.cdp} reached it; retried with ${next.targetId}`
|
|
831
974
|
);
|
|
832
975
|
this.vars.browserUi = next.targetId;
|
|
@@ -857,48 +1000,9 @@ class Execution {
|
|
|
857
1000
|
return;
|
|
858
1001
|
}
|
|
859
1002
|
if (step.client) return outcome(this.exit());
|
|
860
|
-
if (step.redirectSecret)
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
return await outcome(
|
|
864
|
-
redirectSecret({
|
|
865
|
-
client: this.client,
|
|
866
|
-
reader: this.reader,
|
|
867
|
-
session,
|
|
868
|
-
targetId: this.pageTarget,
|
|
869
|
-
ordinary: this.vars.ordinary,
|
|
870
|
-
tag: this.vars.tag,
|
|
871
|
-
fixture: this.options.fixture,
|
|
872
|
-
mode: step.redirectSecret,
|
|
873
|
-
side: this.side,
|
|
874
|
-
settleScale: this.scale,
|
|
875
|
-
controls: (this.clients.redirectSecretControls ??= {}),
|
|
876
|
-
internalSessions: this.internalSessions,
|
|
877
|
-
})
|
|
878
|
-
);
|
|
879
|
-
} finally {
|
|
880
|
-
this.internalSessions.delete(session);
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
if (step.errorPage)
|
|
884
|
-
return outcome(
|
|
885
|
-
protectedErrorPage({
|
|
886
|
-
client: this.client,
|
|
887
|
-
reader: this.reader,
|
|
888
|
-
session,
|
|
889
|
-
targetId: this.pageTarget,
|
|
890
|
-
ordinary: this.vars.ordinary,
|
|
891
|
-
protectedOrigin: this.vars.seven,
|
|
892
|
-
tag: this.vars.tag,
|
|
893
|
-
internalSessions: this.internalSessions,
|
|
894
|
-
mode: step.errorPage,
|
|
895
|
-
side: this.side,
|
|
896
|
-
secret: step.secret,
|
|
897
|
-
discovery: supported(['discovery'], this.options.capabilities),
|
|
898
|
-
notes: this.notes,
|
|
899
|
-
budget: this.bound(5000),
|
|
900
|
-
})
|
|
901
|
-
);
|
|
1003
|
+
if (step.redirectSecret || step.errorPage) return this.originProbe(step, session);
|
|
1004
|
+
if (step.spawnWorker) return this.spawnWorker(step);
|
|
1005
|
+
if (step.probeSessions) return this.probeSessions(step);
|
|
902
1006
|
if (step.bfcacheRestore)
|
|
903
1007
|
return outcome(
|
|
904
1008
|
bfcacheRestore({
|
|
@@ -1009,12 +1113,10 @@ class Execution {
|
|
|
1009
1113
|
|
|
1010
1114
|
async isolatedWorld(step) {
|
|
1011
1115
|
const targetId = step.session === 'child' ? this.targets.at(-1) : this.pageTarget;
|
|
1012
|
-
const
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
});
|
|
1116
|
+
const attached = await this.readerFrameTree(targetId, 'isolatedWorld page');
|
|
1117
|
+
if (!attached.ok) throw new Error('isolatedWorld attach failed');
|
|
1118
|
+
const { sessionId, frameTree } = attached;
|
|
1016
1119
|
try {
|
|
1017
|
-
const { frameTree } = await this.reader.send('Page.getFrameTree', {}, sessionId);
|
|
1018
1120
|
const { executionContextId } = await this.reader.send(
|
|
1019
1121
|
'Page.createIsolatedWorld',
|
|
1020
1122
|
{ frameId: frameTree.frame.id, worldName: step.isolatedWorld },
|
|
@@ -1041,6 +1143,224 @@ class Execution {
|
|
|
1041
1143
|
}
|
|
1042
1144
|
}
|
|
1043
1145
|
|
|
1146
|
+
async originProbe(step, session) {
|
|
1147
|
+
if (step.redirectSecret) {
|
|
1148
|
+
this.internalSessions.add(session);
|
|
1149
|
+
try {
|
|
1150
|
+
return await outcome(
|
|
1151
|
+
redirectSecret({
|
|
1152
|
+
client: this.client,
|
|
1153
|
+
reader: this.reader,
|
|
1154
|
+
session,
|
|
1155
|
+
targetId: this.pageTarget,
|
|
1156
|
+
ordinary: this.vars.ordinary,
|
|
1157
|
+
tag: this.vars.tag,
|
|
1158
|
+
fixture: this.options.fixture,
|
|
1159
|
+
mode: step.redirectSecret,
|
|
1160
|
+
side: this.side,
|
|
1161
|
+
settleScale: this.scale,
|
|
1162
|
+
controls: (this.clients.redirectSecretControls ??= {}),
|
|
1163
|
+
internalSessions: this.internalSessions,
|
|
1164
|
+
})
|
|
1165
|
+
);
|
|
1166
|
+
} finally {
|
|
1167
|
+
this.internalSessions.delete(session);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return outcome(
|
|
1171
|
+
protectedErrorPage({
|
|
1172
|
+
client: this.client,
|
|
1173
|
+
reader: this.reader,
|
|
1174
|
+
session,
|
|
1175
|
+
targetId: this.pageTarget,
|
|
1176
|
+
ordinary: this.vars.ordinary,
|
|
1177
|
+
protectedOrigin: this.vars.seven,
|
|
1178
|
+
tag: this.vars.tag,
|
|
1179
|
+
internalSessions: this.internalSessions,
|
|
1180
|
+
mode: step.errorPage,
|
|
1181
|
+
side: this.side,
|
|
1182
|
+
secret: step.secret,
|
|
1183
|
+
discovery: supported(['discovery'], this.options.capabilities),
|
|
1184
|
+
notes: this.notes,
|
|
1185
|
+
budget: this.bound(5000),
|
|
1186
|
+
})
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
spawnWorkerKind() {
|
|
1191
|
+
return allSteps([...(this.vector.setup ?? []), ...(this.vector.trigger ?? [])]).find(
|
|
1192
|
+
(step) => step.spawnWorker
|
|
1193
|
+
)?.spawnWorker;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
async ensurePriorWorkersGone(kind) {
|
|
1197
|
+
if (this.side !== 'guarded') return;
|
|
1198
|
+
const type = workerTypeForKind(kind);
|
|
1199
|
+
if (!persistentWorkerTypes.has(type)) return;
|
|
1200
|
+
const prior = this.clients.priorWorker;
|
|
1201
|
+
const leftover = await leftoverPersistentWorker(this.reader ?? this.client, {
|
|
1202
|
+
type,
|
|
1203
|
+
...(prior?.url || prior?.targetId ? { url: prior?.url, targetId: prior?.targetId } : {}),
|
|
1204
|
+
});
|
|
1205
|
+
if (leftover)
|
|
1206
|
+
throw new Error(
|
|
1207
|
+
`leftover ${type} still present before guarded spawn: ${leftover.url ?? leftover.targetId}`
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
async spawnWorker(step) {
|
|
1212
|
+
if (step.as) this.eventMarks.set(step.as, this.client.events.length);
|
|
1213
|
+
const pageUrl = expand(step.url ?? '{target}/doc?n=host-{tag}', this.vars);
|
|
1214
|
+
const hostUrl = step.frame ? expand(step.frame, this.vars) : undefined;
|
|
1215
|
+
await this.ensurePriorWorkersGone(step.spawnWorker);
|
|
1216
|
+
const out = await outcome(
|
|
1217
|
+
protectedWorker({
|
|
1218
|
+
reader: this.reader,
|
|
1219
|
+
kind: step.spawnWorker,
|
|
1220
|
+
pageUrl,
|
|
1221
|
+
hostUrl,
|
|
1222
|
+
tag: this.vars.tag,
|
|
1223
|
+
context: this.vars.context,
|
|
1224
|
+
notes: this.notes,
|
|
1225
|
+
budget: this.bound(5000),
|
|
1226
|
+
})
|
|
1227
|
+
);
|
|
1228
|
+
if (out.ok && out.result?.browserContextId) {
|
|
1229
|
+
this.contexts.push(out.result.browserContextId);
|
|
1230
|
+
this.vars.context = out.result.browserContextId;
|
|
1231
|
+
}
|
|
1232
|
+
if (out.ok && out.result?.pageTargetId) this.targets.push(out.result.pageTargetId);
|
|
1233
|
+
if (out.ok && out.result?.workerTargetId) {
|
|
1234
|
+
this.vars.workerTarget = out.result.workerTargetId;
|
|
1235
|
+
this.targets.push(out.result.workerTargetId);
|
|
1236
|
+
}
|
|
1237
|
+
if (out.ok && out.result?.workerUrl && this.side === 'control')
|
|
1238
|
+
this.clients.priorWorker = {
|
|
1239
|
+
type: out.result.type,
|
|
1240
|
+
url: out.result.workerUrl,
|
|
1241
|
+
targetId: out.result.workerTargetId,
|
|
1242
|
+
};
|
|
1243
|
+
if (!out.ok) this.errors.push(`spawnWorker: ${out.error?.message ?? 'probe failed'}`);
|
|
1244
|
+
if (out.ok && out.result?.incomplete)
|
|
1245
|
+
this.incomplete.push(`spawnWorker: ${out.result.incomplete}`);
|
|
1246
|
+
return out;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
probeSessionIds(step) {
|
|
1250
|
+
const mark = this.eventMarks.get(step.probeSessions.since) ?? this.eventStart;
|
|
1251
|
+
const sessions = [];
|
|
1252
|
+
for (const event of this.client.events.slice(mark)) {
|
|
1253
|
+
if (event.method === 'Target.attachedToTarget' && event.params?.sessionId)
|
|
1254
|
+
sessions.push(event.params.sessionId);
|
|
1255
|
+
}
|
|
1256
|
+
for (const name of step.probeSessions.attach ?? ['attach']) {
|
|
1257
|
+
const sessionId = this.calls[name]?.result?.sessionId;
|
|
1258
|
+
if (sessionId) sessions.push(sessionId);
|
|
1259
|
+
}
|
|
1260
|
+
return sessions;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
async probeSessions(step) {
|
|
1264
|
+
const out = await outcome(
|
|
1265
|
+
probeWorkerSessions({
|
|
1266
|
+
client: this.client,
|
|
1267
|
+
sessions: this.probeSessionIds(step),
|
|
1268
|
+
path: expand(step.probeSessions.path, this.vars),
|
|
1269
|
+
tag: this.vars.tag,
|
|
1270
|
+
detached: (sessionId) => this.detached(sessionId),
|
|
1271
|
+
})
|
|
1272
|
+
);
|
|
1273
|
+
this.probedSessionIds ??= new Set();
|
|
1274
|
+
for (const sessionId of out.result?.probedIds ?? []) this.probedSessionIds.add(sessionId);
|
|
1275
|
+
if (!out.ok) {
|
|
1276
|
+
this.errors.push(`probeSessions: ${out.error?.message ?? 'probe failed'}`);
|
|
1277
|
+
return out;
|
|
1278
|
+
}
|
|
1279
|
+
if (out.result?.incomplete) {
|
|
1280
|
+
const reason = probeHarnessIncomplete(this.side, out.result.incomplete);
|
|
1281
|
+
if (reason) this.incomplete.push(`probeSessions: ${reason}`);
|
|
1282
|
+
const { incomplete: _drop, ...result } = out.result;
|
|
1283
|
+
return { ...out, result };
|
|
1284
|
+
}
|
|
1285
|
+
return out;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
probeStep() {
|
|
1289
|
+
return [...(this.vector.setup ?? []), ...(this.vector.trigger ?? [])].find(
|
|
1290
|
+
(entry) => entry.probeSessions
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
async probeFreshSessions() {
|
|
1295
|
+
const step = this.probeStep();
|
|
1296
|
+
if (!step?.probeSessions) return false;
|
|
1297
|
+
const expanded = expand(step, this.vars);
|
|
1298
|
+
this.probedSessionIds ??= new Set();
|
|
1299
|
+
const fresh = this.probeSessionIds(expanded).filter(
|
|
1300
|
+
(sessionId) => sessionId && !this.probedSessionIds.has(sessionId)
|
|
1301
|
+
);
|
|
1302
|
+
if (!fresh.length) return this.calls.probe?.result?.reachable === true;
|
|
1303
|
+
const out = await probeWorkerSessions({
|
|
1304
|
+
client: this.client,
|
|
1305
|
+
sessions: fresh,
|
|
1306
|
+
path: expand(expanded.probeSessions.path, this.vars),
|
|
1307
|
+
tag: this.vars.tag,
|
|
1308
|
+
detached: (sessionId) => this.detached(sessionId),
|
|
1309
|
+
});
|
|
1310
|
+
for (const sessionId of out.probedIds ?? []) this.probedSessionIds.add(sessionId);
|
|
1311
|
+
const sessions = [
|
|
1312
|
+
...new Set([...(this.calls.probe?.result?.sessions ?? []), ...(out.probedIds ?? fresh)]),
|
|
1313
|
+
];
|
|
1314
|
+
if (out.incomplete) {
|
|
1315
|
+
this.calls.probe = {
|
|
1316
|
+
ok: true,
|
|
1317
|
+
result: { ...(this.calls.probe?.result ?? {}), ...out, sessions },
|
|
1318
|
+
};
|
|
1319
|
+
const reason = probeHarnessIncomplete(this.side, out.incomplete);
|
|
1320
|
+
if (reason) {
|
|
1321
|
+
this.incomplete.push(`probeSessions: ${reason}`);
|
|
1322
|
+
return 'incomplete';
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
const reachable = out.reachable || this.calls.probe?.result?.reachable === true;
|
|
1326
|
+
this.calls.probe = {
|
|
1327
|
+
ok: true,
|
|
1328
|
+
result: {
|
|
1329
|
+
...(this.calls.probe?.result ?? {}),
|
|
1330
|
+
reachable,
|
|
1331
|
+
probed: (this.calls.probe?.result?.probed ?? 0) + (out.probed ?? 0),
|
|
1332
|
+
sessions,
|
|
1333
|
+
},
|
|
1334
|
+
};
|
|
1335
|
+
return reachable;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
async observeProbeReachable(want, check) {
|
|
1339
|
+
if (want !== true && want !== false) return this.check(check);
|
|
1340
|
+
let seen = await this.probeFreshSessions();
|
|
1341
|
+
if (seen === 'incomplete') return this.side === 'control' ? false : 'incomplete';
|
|
1342
|
+
if (seen === true) return true;
|
|
1343
|
+
const met = await this.poll(
|
|
1344
|
+
async () => {
|
|
1345
|
+
if (this.clients.flight?.abandoned) return true;
|
|
1346
|
+
seen = await this.probeFreshSessions();
|
|
1347
|
+
if (seen === 'incomplete') return true;
|
|
1348
|
+
return seen === true;
|
|
1349
|
+
},
|
|
1350
|
+
observeMs,
|
|
1351
|
+
false
|
|
1352
|
+
);
|
|
1353
|
+
if (seen === 'incomplete') return this.side === 'control' ? false : 'incomplete';
|
|
1354
|
+
if (want === true && !met && !this.clients.flight?.abandoned) {
|
|
1355
|
+
if (this.side === 'control') return false;
|
|
1356
|
+
this.incomplete.push(
|
|
1357
|
+
`workerReachable: probe evidence not observed within ${this.bound(observeMs)}ms`
|
|
1358
|
+
);
|
|
1359
|
+
return 'incomplete';
|
|
1360
|
+
}
|
|
1361
|
+
return seen === true;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1044
1364
|
async watchPage(step) {
|
|
1045
1365
|
if (!this.watch) {
|
|
1046
1366
|
const start = this.reader.events.length;
|
|
@@ -1108,7 +1428,7 @@ class Execution {
|
|
|
1108
1428
|
async settle(settle) {
|
|
1109
1429
|
const refused = () => (settle.dependsOn ?? []).some((name) => this.refused[name]);
|
|
1110
1430
|
const skipped = () => {
|
|
1111
|
-
this.
|
|
1431
|
+
this.note(
|
|
1112
1432
|
`settle:skipped-refused ${settle.beacon ? `beacon ${settle.beacon}` : settle.served ? `request ${settle.served}` : settle.title ? `title ${settle.title}` : 'load'}`
|
|
1113
1433
|
);
|
|
1114
1434
|
};
|
|
@@ -1224,6 +1544,14 @@ class Execution {
|
|
|
1224
1544
|
this.contexts.push(this.vars.context);
|
|
1225
1545
|
await this.seedContext(this.vars.context);
|
|
1226
1546
|
}
|
|
1547
|
+
if (step.reuseSession) {
|
|
1548
|
+
const reused = reusableAttachSession(step, out, params, this.client.events, this.eventStart);
|
|
1549
|
+
if (reused) {
|
|
1550
|
+
await outcome(this.client.send('Runtime.runIfWaitingForDebugger', {}, reused));
|
|
1551
|
+
this.note(`Target.attachToTarget reused auto-attach session for ${params.targetId}`);
|
|
1552
|
+
return { ok: true, result: { sessionId: reused, reused: true } };
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1227
1555
|
return out;
|
|
1228
1556
|
}
|
|
1229
1557
|
|
|
@@ -1234,6 +1562,27 @@ class Execution {
|
|
|
1234
1562
|
});
|
|
1235
1563
|
}
|
|
1236
1564
|
|
|
1565
|
+
namedCallSummaries() {
|
|
1566
|
+
return Object.fromEntries(
|
|
1567
|
+
['attach', 'spawn', 'targets', 'probe', 'tabs']
|
|
1568
|
+
.filter((name) => this.calls[name])
|
|
1569
|
+
.map((name) => {
|
|
1570
|
+
const out = this.calls[name];
|
|
1571
|
+
return [
|
|
1572
|
+
name,
|
|
1573
|
+
{
|
|
1574
|
+
ok: out.ok,
|
|
1575
|
+
...(out.result?.sessionId ? { sessionId: true } : {}),
|
|
1576
|
+
...(out.result?.exists !== undefined ? { exists: out.result.exists } : {}),
|
|
1577
|
+
...(out.result?.reachable !== undefined ? { reachable: out.result.reachable } : {}),
|
|
1578
|
+
...(out.result?.incomplete ? { incomplete: out.result.incomplete } : {}),
|
|
1579
|
+
...(out.error ? { error: String(out.error?.message ?? out.error) } : {}),
|
|
1580
|
+
},
|
|
1581
|
+
];
|
|
1582
|
+
})
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1237
1586
|
async page(step, session, phase) {
|
|
1238
1587
|
const client = this.actor();
|
|
1239
1588
|
this.actionEventStart = client.events.length;
|
|
@@ -1345,20 +1694,30 @@ class Execution {
|
|
|
1345
1694
|
return !!late;
|
|
1346
1695
|
}
|
|
1347
1696
|
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
];
|
|
1697
|
+
observeEarly(name, check, want) {
|
|
1698
|
+
if (name === 'workerReachable' && this.probeStep())
|
|
1699
|
+
return this.observeProbeReachable(want, check);
|
|
1352
1700
|
if (check?.forwarded) return this.forwardedCheck(name, check, want);
|
|
1353
1701
|
if (
|
|
1354
1702
|
check?.result &&
|
|
1355
1703
|
(at(this.calls[check.result], check.path) === 'incomplete' || this.unmeasured(check))
|
|
1356
1704
|
)
|
|
1357
1705
|
return 'incomplete';
|
|
1706
|
+
return undefined;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
async observe(name, check) {
|
|
1710
|
+
const want = (this.side === 'control' ? this.vector.control.expected : this.vector.expected)?.[
|
|
1711
|
+
name
|
|
1712
|
+
];
|
|
1713
|
+
const early = this.observeEarly(name, check, want);
|
|
1714
|
+
if (early !== undefined) return early;
|
|
1358
1715
|
const plan = steps(this.vector, this.side, this.vars);
|
|
1359
1716
|
const names = resultNames(check);
|
|
1717
|
+
const absencePoll = this.vector.observe?.[name]?.absencePoll === true;
|
|
1360
1718
|
const snapshots =
|
|
1361
|
-
|
|
1719
|
+
!this.clients.flight?.abandoned &&
|
|
1720
|
+
((this.side === 'control' && want === true) || (want === false && absencePoll))
|
|
1362
1721
|
? [...plan.setup, ...plan.trigger].filter(
|
|
1363
1722
|
(step) =>
|
|
1364
1723
|
names.includes(step.as) &&
|
|
@@ -1367,17 +1726,28 @@ class Execution {
|
|
|
1367
1726
|
supported(step.requires, this.options.capabilities)
|
|
1368
1727
|
)
|
|
1369
1728
|
: [];
|
|
1729
|
+
const hasEvent = (node) =>
|
|
1730
|
+
Boolean(node?.event) ||
|
|
1731
|
+
[...(node?.all ?? []), ...(node?.any ?? []), ...(node?.not ? [node.not] : [])].some(hasEvent);
|
|
1370
1732
|
const polled =
|
|
1371
1733
|
check?.observer === 'fixture' || check?.world !== undefined || check?.script !== undefined;
|
|
1372
|
-
|
|
1734
|
+
const shouldPoll =
|
|
1735
|
+
(want === true && (polled || snapshots.length > 0)) ||
|
|
1736
|
+
(want === false && absencePoll && snapshots.length > 0);
|
|
1737
|
+
if (
|
|
1738
|
+
want === false &&
|
|
1739
|
+
!shouldPoll &&
|
|
1740
|
+
(check?.world !== undefined || check?.script !== undefined || hasEvent(check))
|
|
1741
|
+
)
|
|
1373
1742
|
return this.absenceCheck(check);
|
|
1374
|
-
if (
|
|
1743
|
+
if (!shouldPoll) return this.check(check);
|
|
1375
1744
|
let seen = false;
|
|
1376
1745
|
const met = await this.poll(
|
|
1377
1746
|
async () => {
|
|
1378
1747
|
if (this.clients.flight?.abandoned) return true;
|
|
1379
1748
|
seen = await this.check(check);
|
|
1380
1749
|
if (seen === 'unwatched' || seen === 'incomplete') return true;
|
|
1750
|
+
if (want === false && seen === true) return true;
|
|
1381
1751
|
if (seen !== true && snapshots.length) {
|
|
1382
1752
|
await this.execute(snapshots, undefined, 'observation');
|
|
1383
1753
|
if (this.clients.flight?.abandoned) return true;
|
|
@@ -1390,7 +1760,7 @@ class Execution {
|
|
|
1390
1760
|
snapshots.length > 0
|
|
1391
1761
|
);
|
|
1392
1762
|
if (seen === 'unwatched' || seen === 'incomplete') return seen;
|
|
1393
|
-
if ((!met || this.clients.flight?.abandoned) && snapshots.length) {
|
|
1763
|
+
if (want === true && (!met || this.clients.flight?.abandoned) && snapshots.length) {
|
|
1394
1764
|
this.incomplete.push(
|
|
1395
1765
|
`${name}: snapshot evidence not observed within ${this.bound(observeMs)}ms`
|
|
1396
1766
|
);
|
|
@@ -1675,6 +2045,7 @@ class Execution {
|
|
|
1675
2045
|
|
|
1676
2046
|
async run() {
|
|
1677
2047
|
this.requestStart = (await this.options.fixture.log()).length;
|
|
2048
|
+
this.installClientTrace();
|
|
1678
2049
|
let pumping;
|
|
1679
2050
|
try {
|
|
1680
2051
|
await this.seed();
|
|
@@ -1697,6 +2068,7 @@ class Execution {
|
|
|
1697
2068
|
side: this.side,
|
|
1698
2069
|
});
|
|
1699
2070
|
const { live, liveWhy } = await this.liveness(unseen, incomplete);
|
|
2071
|
+
if (this.errors.length) this.noteClientTrace();
|
|
1700
2072
|
return {
|
|
1701
2073
|
observed,
|
|
1702
2074
|
live,
|
|
@@ -1705,6 +2077,7 @@ class Execution {
|
|
|
1705
2077
|
notes: this.notes,
|
|
1706
2078
|
incomplete: this.incomplete,
|
|
1707
2079
|
unsupported: this.unsupported,
|
|
2080
|
+
calls: this.namedCallSummaries(),
|
|
1708
2081
|
evidence: Object.fromEntries(
|
|
1709
2082
|
Object.entries(this.calls)
|
|
1710
2083
|
.filter(
|
|
@@ -1713,13 +2086,25 @@ class Execution {
|
|
|
1713
2086
|
.map(([name, out]) => [name, out.result.diagnostics ?? out.result])
|
|
1714
2087
|
),
|
|
1715
2088
|
};
|
|
2089
|
+
} catch (error) {
|
|
2090
|
+
this.noteClientTrace();
|
|
2091
|
+
throw error;
|
|
1716
2092
|
} finally {
|
|
1717
2093
|
this.stopped = true;
|
|
1718
2094
|
await pumping;
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
2095
|
+
await this.cleanupLeg();
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
async cleanupLeg() {
|
|
2100
|
+
if (this.watch)
|
|
2101
|
+
await outcome(
|
|
2102
|
+
this.reader.send('Target.detachFromTarget', { sessionId: this.watch.sessionId })
|
|
2103
|
+
);
|
|
2104
|
+
if (this.spawnWorkerKind()) {
|
|
2105
|
+
await Promise.allSettled(this.pending);
|
|
2106
|
+
await this.cleanupSpawnedWorkerLeg();
|
|
2107
|
+
} else {
|
|
1723
2108
|
for (const session of this.attachSettings.keys())
|
|
1724
2109
|
await outcome(
|
|
1725
2110
|
this.client.send(
|
|
@@ -1730,23 +2115,49 @@ class Execution {
|
|
|
1730
2115
|
);
|
|
1731
2116
|
for (const targetId of new Set(this.targets))
|
|
1732
2117
|
await outcome((this.reader ?? this.client).send('Target.closeTarget', { targetId }));
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
)
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
)
|
|
1746
|
-
|
|
1747
|
-
|
|
2118
|
+
}
|
|
2119
|
+
for (const browserContextId of new Set(this.contexts))
|
|
2120
|
+
await outcome(
|
|
2121
|
+
(this.reader ?? this.client).send('Target.disposeBrowserContext', { browserContextId })
|
|
2122
|
+
);
|
|
2123
|
+
this.contexts = [];
|
|
2124
|
+
if (!this.spawnWorkerKind()) await Promise.allSettled(this.pending);
|
|
2125
|
+
if (
|
|
2126
|
+
this.side === 'guarded' &&
|
|
2127
|
+
this.vector.observe?.eventsRedacted?.requires?.includes('discovery')
|
|
2128
|
+
) {
|
|
2129
|
+
const violation = discoveryViolation(
|
|
2130
|
+
this.client.events.slice(this.eventStart),
|
|
2131
|
+
supported(['discovery'], this.options.capabilities)
|
|
2132
|
+
);
|
|
2133
|
+
if (violation) this.errors.push(violation);
|
|
1748
2134
|
}
|
|
1749
2135
|
}
|
|
2136
|
+
|
|
2137
|
+
async cleanupSpawnedWorkerLeg() {
|
|
2138
|
+
await this.resumeWaiting();
|
|
2139
|
+
await releasePersistentWorkerSessions({
|
|
2140
|
+
client: this.client,
|
|
2141
|
+
reader: this.reader,
|
|
2142
|
+
events: this.client.events,
|
|
2143
|
+
from: this.eventStart,
|
|
2144
|
+
tag: this.vars.tag,
|
|
2145
|
+
targets: this.targets,
|
|
2146
|
+
attachSessions: [...this.attachSettings.keys()],
|
|
2147
|
+
});
|
|
2148
|
+
if (this.reader && this.reader !== this.client)
|
|
2149
|
+
await releasePersistentWorkerSessions({
|
|
2150
|
+
client: this.reader,
|
|
2151
|
+
reader: this.reader,
|
|
2152
|
+
events: this.reader.events,
|
|
2153
|
+
from: 0,
|
|
2154
|
+
tag: this.vars.tag,
|
|
2155
|
+
targets: this.targets,
|
|
2156
|
+
attachSessions: [],
|
|
2157
|
+
});
|
|
2158
|
+
this.targets = [];
|
|
2159
|
+
this.attachSettings.clear();
|
|
2160
|
+
}
|
|
1750
2161
|
}
|
|
1751
2162
|
|
|
1752
2163
|
const usesPre = (value) =>
|
|
@@ -1764,7 +2175,7 @@ const observerRequired = (check) => {
|
|
|
1764
2175
|
const needsObserver = (vector) =>
|
|
1765
2176
|
vector.control.via === 'unguarded' ||
|
|
1766
2177
|
allSteps([...(vector.setup ?? []), ...(vector.trigger ?? [])]).some(
|
|
1767
|
-
(step) => step.isolatedWorld || step.observer
|
|
2178
|
+
(step) => step.isolatedWorld || step.observer || step.spawnWorker
|
|
1768
2179
|
) ||
|
|
1769
2180
|
observerRequired(vector.liveCheck) ||
|
|
1770
2181
|
Object.values(vector.observe ?? {}).some(
|
|
@@ -1870,14 +2281,75 @@ function variables(targets, target, side, slot, index) {
|
|
|
1870
2281
|
return vars;
|
|
1871
2282
|
}
|
|
1872
2283
|
|
|
2284
|
+
function sessionTypeOf(client, sessionId) {
|
|
2285
|
+
if (!sessionId) return 'browser';
|
|
2286
|
+
const events = client.events ?? [];
|
|
2287
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
2288
|
+
const event = events[i];
|
|
2289
|
+
if (event.method === 'Target.attachedToTarget' && event.params?.sessionId === sessionId)
|
|
2290
|
+
return event.params.targetInfo?.type || 'unknown';
|
|
2291
|
+
}
|
|
2292
|
+
return 'unknown';
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
function boundFrameTree(rawSend, client, params, sessionId) {
|
|
2296
|
+
let timer;
|
|
2297
|
+
const type = sessionTypeOf(client, sessionId);
|
|
2298
|
+
return Promise.race([
|
|
2299
|
+
rawSend('Page.getFrameTree', params, sessionId),
|
|
2300
|
+
new Promise((_, reject) => {
|
|
2301
|
+
timer = setTimeout(
|
|
2302
|
+
() =>
|
|
2303
|
+
reject(
|
|
2304
|
+
new Error(
|
|
2305
|
+
`Page.getFrameTree timed out on ${type} session ${sessionId ?? 'browser'} after 3000ms`
|
|
2306
|
+
)
|
|
2307
|
+
),
|
|
2308
|
+
3000
|
|
2309
|
+
);
|
|
2310
|
+
}),
|
|
2311
|
+
]).finally(() => clearTimeout(timer));
|
|
2312
|
+
}
|
|
2313
|
+
|
|
1873
2314
|
function tracked(client, calls, name) {
|
|
1874
2315
|
if (!client || typeof client.send !== 'function') return client;
|
|
1875
2316
|
let next = 0;
|
|
2317
|
+
const rawSend = client.send.bind(client);
|
|
1876
2318
|
const send = (method, params, sessionId) => {
|
|
1877
2319
|
const id = ++next;
|
|
1878
2320
|
calls.set(id, { name, method, session: sessionId, since: Date.now() });
|
|
2321
|
+
if (!client.__sliccOnSend && !client.__sliccBoundGetFrameTree) {
|
|
2322
|
+
return Promise.resolve()
|
|
2323
|
+
.then(() => client.send(method, params, sessionId))
|
|
2324
|
+
.finally(() => calls.delete(id));
|
|
2325
|
+
}
|
|
2326
|
+
const started = Date.now();
|
|
2327
|
+
const run =
|
|
2328
|
+
method === 'Page.getFrameTree' && client.__sliccBoundGetFrameTree
|
|
2329
|
+
? () => boundFrameTree(rawSend, client, params, sessionId)
|
|
2330
|
+
: () => rawSend(method, params, sessionId);
|
|
1879
2331
|
return Promise.resolve()
|
|
1880
|
-
.then(
|
|
2332
|
+
.then(run)
|
|
2333
|
+
.then(
|
|
2334
|
+
(result) => {
|
|
2335
|
+
client.__sliccOnSend?.({
|
|
2336
|
+
method,
|
|
2337
|
+
sessionId,
|
|
2338
|
+
ms: Date.now() - started,
|
|
2339
|
+
status: 'ok',
|
|
2340
|
+
});
|
|
2341
|
+
return result;
|
|
2342
|
+
},
|
|
2343
|
+
(error) => {
|
|
2344
|
+
client.__sliccOnSend?.({
|
|
2345
|
+
method,
|
|
2346
|
+
sessionId,
|
|
2347
|
+
ms: Date.now() - started,
|
|
2348
|
+
status: String(error?.message ?? error).slice(0, 200),
|
|
2349
|
+
});
|
|
2350
|
+
throw error;
|
|
2351
|
+
}
|
|
2352
|
+
)
|
|
1881
2353
|
.finally(() => calls.delete(id));
|
|
1882
2354
|
};
|
|
1883
2355
|
return new Proxy(client, {
|
|
@@ -2025,12 +2497,27 @@ async function executeRow(job, opts, made, set, slot, index, flights) {
|
|
|
2025
2497
|
}
|
|
2026
2498
|
if (legs.control.unsupported === 'trustedEmulation')
|
|
2027
2499
|
return skipRow(base, 'not-supported:trustedEmulation');
|
|
2500
|
+
const row = finishRow(vector, base, legs, opts);
|
|
2501
|
+
opts.log?.(row);
|
|
2502
|
+
return row;
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
function finishRow(vector, base, legs, opts) {
|
|
2506
|
+
const caps = Array.isArray(opts.capabilities)
|
|
2507
|
+
? opts.capabilities
|
|
2508
|
+
: Object.keys(opts.capabilities).filter((k) => opts.capabilities[k] === true);
|
|
2509
|
+
const calls = {
|
|
2510
|
+
...(legs.control.calls && Object.keys(legs.control.calls).length
|
|
2511
|
+
? { control: legs.control.calls }
|
|
2512
|
+
: {}),
|
|
2513
|
+
...(legs.guarded.calls && Object.keys(legs.guarded.calls).length
|
|
2514
|
+
? { guarded: legs.guarded.calls }
|
|
2515
|
+
: {}),
|
|
2516
|
+
};
|
|
2028
2517
|
const row = {
|
|
2029
2518
|
...result(vector, {
|
|
2030
2519
|
...base,
|
|
2031
|
-
capabilities:
|
|
2032
|
-
? opts.capabilities
|
|
2033
|
-
: Object.keys(opts.capabilities).filter((k) => opts.capabilities[k] === true),
|
|
2520
|
+
capabilities: caps,
|
|
2034
2521
|
control: legs.control.observed,
|
|
2035
2522
|
guarded: legs.guarded.observed,
|
|
2036
2523
|
live: legs.control.live && legs.guarded.live,
|
|
@@ -2040,6 +2527,7 @@ async function executeRow(job, opts, made, set, slot, index, flights) {
|
|
|
2040
2527
|
observations: { control: legs.control.observed, guarded: legs.guarded.observed },
|
|
2041
2528
|
errors: [...legs.control.errors, ...legs.guarded.errors],
|
|
2042
2529
|
designDependent: vector.designDependent ?? [],
|
|
2530
|
+
...(Object.keys(calls).length ? { calls } : {}),
|
|
2043
2531
|
...probeEvidence(legs),
|
|
2044
2532
|
};
|
|
2045
2533
|
const notes = [
|
|
@@ -2052,7 +2540,6 @@ async function executeRow(job, opts, made, set, slot, index, flights) {
|
|
|
2052
2540
|
row.control = 'fail';
|
|
2053
2541
|
row.guarded = 'fail';
|
|
2054
2542
|
}
|
|
2055
|
-
opts.log?.(row);
|
|
2056
2543
|
return row;
|
|
2057
2544
|
}
|
|
2058
2545
|
|