@ai-ecoverse/slicc-browsers 1.7.10 → 1.7.12
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 -1
- package/guard-runner.mjs +48 -9
- package/guard-vectors.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -170,7 +170,7 @@ Any `cdp`, `page` or `children` step can take `requires`, the capabilities it ne
|
|
|
170
170
|
- `eval`: evaluate `expression`.
|
|
171
171
|
`hops` routes `url` through `/hop` on each `origin`, with its `code`.
|
|
172
172
|
- `{ "answer": { "url", "stage"?, "cdp"?, "params"?, "hold"? } }`: answer each paused request or auth challenge for `url` with that call; `hold` leaves it paused. Every other paused request continues unchanged.
|
|
173
|
-
- `{ "children": steps, "where"? }`: run these steps on each target auto-attached from then on, then resume it. `where` scopes them to the row's own target: `{ "openerId": "page" }` (opened by the row's page) or `{ "urlPrefix": url }`; other attached targets are resumed and otherwise left alone.
|
|
173
|
+
- `{ "children": steps, "where"? }`: run these steps on each target auto-attached from then on, then resume it. `where` scopes them to the row's own target: `{ "openerId": "page" }` (opened by the row's page) or `{ "urlPrefix": url }`; other attached targets are resumed and otherwise left alone. When a vector has a `children` step with `where`, a target that attaches before that step runs waits until it does, and is then matched against `where`. A target is only resumed when it attached waiting for the debugger, and a `browser_ui` target (Chrome's own UI, such as `chrome://omnibox-popup.top-chrome/`) is never adopted or sent anything.
|
|
174
174
|
- `{ "settle": { "beacon"?, "served"?, "title"?, "ms"?, "variant"?, "session"?, "dependsOn"?, "or"? } }`: wait for a beacon from `beacon`, for the fixture to log a request for the URL `served`, for the page `title`, or for the session to detach. When a refusal in `dependsOn` was recorded, the beacon can't come: a settle with `or` (a check) then waits until `or` holds, and one without it is skipped. Without such a refusal, `or` plays no part. Each beacon settle after an init script depends on that script's registration, and its `or` is what the row reads: the hops and the destination for the redirect chains, or the served page otherwise. It waits up to `ms` (10000 by default), then another 600 ms. `variant` waits the run's detach variant instead.
|
|
175
175
|
- `{ "wait": ms }`.
|
|
176
176
|
- `{ "paused": { "url", "ms" } }`: wait until the client has seen the pause that an `answer` rule for `url` matches (`Fetch.requestPaused` at the rule's stage, or `Fetch.authRequired` for a `Fetch.continueWithAuth` rule). It waits up to `ms`. On the control leg, a pause that never comes fails the row with `paused: no pause for <url> within <ms>ms`. On the guarded leg the guard may hide that pause, so the step waits the full `ms` and doesn't fail. The C5 guessed-id vectors and `C5.auth` use it before their second request, so the probe's interception id falls between the two ids the guess spans.
|
|
@@ -287,6 +287,8 @@ every alternative of an `any`, or a required (`all`, `not` or plain) check, need
|
|
|
287
287
|
selects one variant or an array; omission selects all. `parallel` defaults to 1.
|
|
288
288
|
With the `browserContexts` capability each leg runs in its own browser context. A `Target.createBrowserContext` that fails with a timeout is retried once, and the row's `notes` records it; any other failure fails the row.
|
|
289
289
|
|
|
290
|
+
`settleScale` (default 1) multiplies every bound the runner waits on: settles (beacon, served, title and load), `paused`, page and child loads, the wait for a background step's `as` result (`<as> did not complete within <ms>ms`), and the wait for an observation. Adopters on slow hosts can set 3 or 4. Every wait polls and returns as soon as its event arrives, so a larger bound only costs time when something is really missing, and each timeout error names the bound it used. An observation read from the fixture (`observer: "fixture"`, such as a beacon or a served request) that the leg expects to be `true` is polled for up to 5000 ms (scaled) instead of read once. One that the leg expects to be `false` is still read once.
|
|
291
|
+
|
|
290
292
|
`onStart(row)` receives each row's `id`, `target`, `variant`, `transport` and `via` as the row starts. `run()` returns a promise that also has `inFlight()`, and its result has `inFlight` too. `inFlight()` lists the rows that are running now, each as `{ id, target, variant, leg, phase, step, clients }`, where `step` is the index of the current step in its `phase` (`setup`, `trigger` or `child`), and `clients` lists each client's pending CDP calls as `{ method, session, ageMs }` (`client`, plus `unguarded` when it is a separate client). With `rowTimeoutMs`, a row still running after that many milliseconds fails on its own. Its `errors` hold `row timed out after <ms>ms: <the in-flight view>`, `inFlight` holds the view, and the clients that `clientFactory` made for it are closed. The run then goes on to the next row. It is off by default, and a client passed as `client` is never closed.
|
|
291
293
|
|
|
292
294
|
`log(row)` receives completed rows. Besides `control`, `guarded`, `live`, `why` and `errors`, a row may carry `ran` (steps that ran on one leg only) and `notes` (settles skipped because their registration was refused). Both are informational: they explain a row's verdict, and adopters (bios, slicc-cli, slicc-cdp) must not count them as failures, or as passes. The verdict is `control`, `guarded` and `live`. A variant supplies the wait before an
|
package/guard-runner.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
|
|
14
14
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
15
|
const namedResultMs = 10000;
|
|
16
|
+
const observeMs = 5000;
|
|
16
17
|
const q = JSON.stringify;
|
|
17
18
|
const at = (value, path = '') =>
|
|
18
19
|
path
|
|
@@ -111,6 +112,11 @@ class Execution {
|
|
|
111
112
|
this.answers = [];
|
|
112
113
|
this.attachSettings = new Map();
|
|
113
114
|
this.childSteps = [];
|
|
115
|
+
this.deferred = [];
|
|
116
|
+
this.expectWhere = [...(vector.setup ?? []), ...(vector.trigger ?? [])].some(
|
|
117
|
+
(step) => step.children && step.where
|
|
118
|
+
);
|
|
119
|
+
this.scale = options.settleScale ?? 1;
|
|
114
120
|
this.childRuns = new Map();
|
|
115
121
|
this.named = new Map();
|
|
116
122
|
this.processed = new Set();
|
|
@@ -163,8 +169,12 @@ class Execution {
|
|
|
163
169
|
return out;
|
|
164
170
|
}
|
|
165
171
|
|
|
172
|
+
bound(ms) {
|
|
173
|
+
return Math.round(ms * this.scale);
|
|
174
|
+
}
|
|
175
|
+
|
|
166
176
|
async poll(predicate, ms = 1500) {
|
|
167
|
-
const deadline = Date.now() + ms;
|
|
177
|
+
const deadline = Date.now() + this.bound(ms);
|
|
168
178
|
do {
|
|
169
179
|
if (await predicate()) return true;
|
|
170
180
|
await sleep(25);
|
|
@@ -447,6 +457,11 @@ class Execution {
|
|
|
447
457
|
adopt(event) {
|
|
448
458
|
const info = event.params.targetInfo;
|
|
449
459
|
const child = event.params.sessionId;
|
|
460
|
+
if (info?.type === 'browser_ui') return;
|
|
461
|
+
if (this.expectWhere && !this.childWhere) {
|
|
462
|
+
this.deferred.push(event);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
450
465
|
if (info?.browserContextId && this.vars.context && info.browserContextId !== this.vars.context)
|
|
451
466
|
return;
|
|
452
467
|
if (this.childWhere && !this.childMatches(info)) {
|
|
@@ -462,7 +477,8 @@ class Execution {
|
|
|
462
477
|
else {
|
|
463
478
|
await this.execute(this.childSteps.slice(0, reload), child);
|
|
464
479
|
const start = this.client.events.length;
|
|
465
|
-
|
|
480
|
+
if (event.params.waitingForDebugger)
|
|
481
|
+
await outcome(this.client.send('Runtime.runIfWaitingForDebugger', {}, child));
|
|
466
482
|
await this.poll(
|
|
467
483
|
() =>
|
|
468
484
|
this.client.events
|
|
@@ -473,7 +489,8 @@ class Execution {
|
|
|
473
489
|
await this.execute(this.childSteps.slice(reload), child);
|
|
474
490
|
}
|
|
475
491
|
const resumed = this.client.events.length;
|
|
476
|
-
|
|
492
|
+
if (event.params.waitingForDebugger)
|
|
493
|
+
await outcome(this.client.send('Runtime.runIfWaitingForDebugger', {}, child));
|
|
477
494
|
if (this.childSteps.some((step) => step.cdp === 'Page.enable'))
|
|
478
495
|
await this.poll(() => this.firstLoad(child, resumed), 5000);
|
|
479
496
|
})().catch((error) => this.errors.push(String(error.message ?? error)));
|
|
@@ -502,7 +519,7 @@ class Execution {
|
|
|
502
519
|
this.rule(event)?.url === url
|
|
503
520
|
);
|
|
504
521
|
if (!(await this.poll(seen, ms)) && this.side === 'control')
|
|
505
|
-
this.errors.push(`paused: no pause for ${url} within ${ms}ms`);
|
|
522
|
+
this.errors.push(`paused: no pause for ${url} within ${this.bound(ms)}ms`);
|
|
506
523
|
}
|
|
507
524
|
|
|
508
525
|
async answer(event) {
|
|
@@ -604,6 +621,7 @@ class Execution {
|
|
|
604
621
|
if (step.children) {
|
|
605
622
|
this.childSteps = step.children;
|
|
606
623
|
this.childWhere = step.where;
|
|
624
|
+
for (const event of this.deferred.splice(0)) this.adopt(event);
|
|
607
625
|
return;
|
|
608
626
|
}
|
|
609
627
|
if (step.answer) {
|
|
@@ -639,7 +657,7 @@ class Execution {
|
|
|
639
657
|
if (refused() && !alt) return skipped();
|
|
640
658
|
if (settle.variant) await sleep(this.delay);
|
|
641
659
|
if (settle.title && !this.reader) {
|
|
642
|
-
await sleep(settle.ms ?? 1500);
|
|
660
|
+
await sleep(this.bound(settle.ms ?? 1500));
|
|
643
661
|
return;
|
|
644
662
|
}
|
|
645
663
|
const met = await this.poll(async () => {
|
|
@@ -665,7 +683,7 @@ class Execution {
|
|
|
665
683
|
if (refused() && !alt) return skipped();
|
|
666
684
|
if (settle.required && !met)
|
|
667
685
|
this.errors.push(
|
|
668
|
-
`settle: no ${settle.beacon ? `beacon from ${settle.beacon}` : settle.served ? `request for ${settle.served}` : settle.title ? `title ${settle.title}` : 'load'} within ${settle.ms ?? 1500}ms`
|
|
686
|
+
`settle: no ${settle.beacon ? `beacon from ${settle.beacon}` : settle.served ? `request for ${settle.served}` : settle.title ? `title ${settle.title}` : 'load'} within ${this.bound(settle.ms ?? 1500)}ms`
|
|
669
687
|
);
|
|
670
688
|
}
|
|
671
689
|
|
|
@@ -783,7 +801,10 @@ class Execution {
|
|
|
783
801
|
if (!waits.length) return true;
|
|
784
802
|
let timer;
|
|
785
803
|
const expired = new Promise((resolve) => {
|
|
786
|
-
timer = setTimeout(
|
|
804
|
+
timer = setTimeout(
|
|
805
|
+
() => resolve(false),
|
|
806
|
+
this.bound(this.options.namedResultMs ?? namedResultMs)
|
|
807
|
+
);
|
|
787
808
|
});
|
|
788
809
|
const done = await Promise.race([Promise.all(waits).then(() => true), expired]);
|
|
789
810
|
clearTimeout(timer);
|
|
@@ -802,11 +823,24 @@ class Execution {
|
|
|
802
823
|
.filter((name) => typeof name === 'string' && this.named.has(name));
|
|
803
824
|
if (!names.length || (await this.namedSettled(names))) return true;
|
|
804
825
|
this.errors.push(
|
|
805
|
-
`${names.join(', ')} did not complete within ${this.options.namedResultMs ?? namedResultMs}ms`
|
|
826
|
+
`${names.join(', ')} did not complete within ${this.bound(this.options.namedResultMs ?? namedResultMs)}ms`
|
|
806
827
|
);
|
|
807
828
|
return false;
|
|
808
829
|
}
|
|
809
830
|
|
|
831
|
+
async observe(name, check) {
|
|
832
|
+
const want = (this.side === 'control' ? this.vector.control.expected : this.vector.expected)?.[
|
|
833
|
+
name
|
|
834
|
+
];
|
|
835
|
+
if (want !== true || check?.observer !== 'fixture') return this.check(check);
|
|
836
|
+
let seen = false;
|
|
837
|
+
await this.poll(async () => {
|
|
838
|
+
seen = await this.check(check);
|
|
839
|
+
return seen === true;
|
|
840
|
+
}, observeMs);
|
|
841
|
+
return seen;
|
|
842
|
+
}
|
|
843
|
+
|
|
810
844
|
async check(check) {
|
|
811
845
|
const c = expand(check, this.vars);
|
|
812
846
|
if (!(await this.ready(c))) return false;
|
|
@@ -1010,7 +1044,7 @@ class Execution {
|
|
|
1010
1044
|
for (const [name, observation] of Object.entries(this.vector.observe ?? {})) {
|
|
1011
1045
|
if (!supported(observation.requires, this.options.capabilities)) continue;
|
|
1012
1046
|
if ((observation.dependsOn ?? []).some((dep) => this.refused[dep])) continue;
|
|
1013
|
-
observed[name] = await this.
|
|
1047
|
+
observed[name] = await this.observe(name, observation.check);
|
|
1014
1048
|
if (observation.eitherWhen && this.side === 'guarded')
|
|
1015
1049
|
observed[`${name}.either`] = await this.check(observation.eitherWhen);
|
|
1016
1050
|
}
|
|
@@ -1379,6 +1413,11 @@ function validateBlocked(vectors) {
|
|
|
1379
1413
|
|
|
1380
1414
|
function validate(set, opts) {
|
|
1381
1415
|
if (!opts.fixture?.log) throw new TypeError('fixture.log is required');
|
|
1416
|
+
if (
|
|
1417
|
+
opts.settleScale !== undefined &&
|
|
1418
|
+
!(Number.isFinite(opts.settleScale) && opts.settleScale > 0)
|
|
1419
|
+
)
|
|
1420
|
+
throw new TypeError('settleScale must be a positive number');
|
|
1382
1421
|
for (const vector of set.vectors)
|
|
1383
1422
|
for (const [name, observation] of Object.entries(vector.observe ?? {}))
|
|
1384
1423
|
if (!observation.check) throw new TypeError(`${vector.id}: ${name} lacks a check`);
|
package/guard-vectors.json
CHANGED