@ai-ecoverse/slicc-browsers 1.7.5 → 1.7.7
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 +4 -3
- package/guard-driver.mjs +20 -2
- package/guard-runner.mjs +41 -4
- package/guard-vectors.json +22 -7
- package/package.json +3 -3
- package/schema/guard-vectors.schema.json +24 -0
package/README.md
CHANGED
|
@@ -173,6 +173,7 @@ Any `cdp`, `page` or `children` step can take `requires`, the capabilities it ne
|
|
|
173
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.
|
|
174
174
|
- `{ "settle": { "beacon"?, "title"?, "ms"?, "variant"?, "session"? } }`: wait for a beacon from `beacon`, for the page `title`, or for the session to detach. 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
|
+
- `{ "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.
|
|
176
177
|
- `{ "guess": { "between", "cdp", "params" }, "as" }`: the call for each request id between the ids paused for the two URLs.
|
|
177
178
|
- `{ "text": … }`: a step in words.
|
|
178
179
|
|
|
@@ -190,7 +191,7 @@ Popups are observed by `Target.targetCreated` with the opener's `openerId`, or b
|
|
|
190
191
|
|
|
191
192
|
**The control rule.** Every vector has a control on an ordinary origin: the same setup and trigger against an unprotected origin must succeed. A missing or failing control fails the id. With `via: "guard"`, the control runs through the guard. With `via: "unguarded"`, the runner runs setup and trigger through its own CDP against the ordinary origin and against the protected stand-in, and both must show the effect. The runner reports the control's transport as `via`.
|
|
192
193
|
|
|
193
|
-
**Refusals.** Each refusable step has a refusal name: its `as`, or `refused.<i>`, where `i` is its index in the vector's steps, setup then trigger, with each `children` step's steps right after it, counted from 0 before `on` is applied. When a guard refuses the step and the error matches `refusal.match`, the runner sets that name to `true` in the leg's observations. On that leg, an observation whose `dependsOn` lists the name is skipped; every other observation is checked as usual. A guard that refuses the step on every origin passes in one of two ways: through the guard, with the step refused on both legs, or with an unguarded control and the guard's matched refusal on the guarded leg. With `via: "guard"`, a refusal on one leg only fails that leg. A guard that allows the step must pass its control through the guard.
|
|
194
|
+
**Refusals.** Each refusable step has a refusal name: its `as`, or `refused.<i>`, where `i` is its index in the vector's steps, setup then trigger, with each `children` step's steps right after it, counted from 0 before `on` is applied. When a guard refuses the step and the error matches `refusal.match`, the runner sets that name to `true` in the leg's observations. On that leg, an observation whose `dependsOn` lists the name is skipped; every other observation is checked as usual. A guard that refuses the step on every origin passes in one of two ways: through the guard, with the step refused on both legs, or with an unguarded control and the guard's matched refusal on the guarded leg. With `via: "guard"`, a refusal on one leg only fails that leg. The comparison only counts steps that ran on both legs. A step that ran on one leg only (for example a `children` step on the control's popup, when the guarded leg never gets that popup attached) doesn't fail the row by itself, and the row reports it in `ran`, as `{ "refused.5": "control-only" }` or `"guarded-only"`. Observations that depend on such a step follow their `dependsOn` on each leg. A guard that allows the step must pass its control through the guard.
|
|
194
195
|
|
|
195
196
|
**Liveness.** A runner checks `live` through its own CDP, never through the guard path: either the protected document loaded (title, marker or URL), or the guard returned its own refusal for a refusable step. A failing `live` fails the id.
|
|
196
197
|
|
|
@@ -284,7 +285,7 @@ target. In observations and liveness checks, an `any` alternative that reference
|
|
|
284
285
|
a `null` origin is dropped and the rest are evaluated; the row skips only when
|
|
285
286
|
every alternative of an `any`, or a required (`all`, `not` or plain) check, needs it. Tags and cookie names are unique per leg. `variant`
|
|
286
287
|
selects one variant or an array; omission selects all. `parallel` defaults to 1.
|
|
287
|
-
`log(row)` receives completed rows. A variant supplies the wait before an
|
|
288
|
+
`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). A variant supplies the wait before an
|
|
288
289
|
explicit `Target.detachFromTarget` step and for a `settle.variant` step.
|
|
289
290
|
|
|
290
291
|
Every runnable vector has a declarative `liveCheck`; every observation has a
|
|
@@ -335,7 +336,7 @@ load event never ends a wait. After a child's `children` steps that include `Pag
|
|
|
335
336
|
the runner waits, bounded at 5 s, for the child's first document to commit and load before
|
|
336
337
|
any later child step. `{ "settle": { "beacon", "required": true, "dependsOn"? } }` waits
|
|
337
338
|
for that beacon at the fixture and fails the row with `settle: no beacon from <origin>
|
|
338
|
-
within <ms>ms` when it never arrives, unless a refusal it `dependsOn` was recorded
|
|
339
|
+
within <ms>ms` when it never arrives, unless a refusal it `dependsOn` was recorded. It checks `dependsOn` again while it waits, since a child's `children` steps may still be running. When the registration it depends on was refused, the settle is skipped, the row's `notes` gets `<leg> settle:skipped-refused beacon <origin>`, and the row is judged on its other observations. The rows
|
|
339
340
|
whose `allowedBeacon` comes from an earlier document settle on it before their trigger.
|
|
340
341
|
Beacons use `navigator.sendBeacon`, with a `fetch(…, { keepalive: true })` fallback. An observation that reads a background step's `as` result
|
|
341
342
|
waits for that step, up to 10 s, and fails with `<as> did not complete within …` past it. Redirect
|
package/guard-driver.mjs
CHANGED
|
@@ -173,7 +173,18 @@ export function result(vector, run) {
|
|
|
173
173
|
}
|
|
174
174
|
const c = run.control ? judge(vector, 'control', run.control) : null;
|
|
175
175
|
const g = run.guarded ? judge(vector, 'guarded', run.guarded) : null;
|
|
176
|
-
const
|
|
176
|
+
const ranOn = (observed, name) => typeof observed?.[name] === 'boolean';
|
|
177
|
+
const both = (name) => ranOn(run.control, name) && ranOn(run.guarded, name);
|
|
178
|
+
const only = (a, b) =>
|
|
179
|
+
(a?.refused ?? []).filter((name) => both(name) && !(b?.refused ?? []).includes(name));
|
|
180
|
+
const ran = Object.fromEntries(
|
|
181
|
+
refusals(vector)
|
|
182
|
+
.filter(
|
|
183
|
+
(name) =>
|
|
184
|
+
run.control && run.guarded && ranOn(run.control, name) !== ranOn(run.guarded, name)
|
|
185
|
+
)
|
|
186
|
+
.map((name) => [name, ranOn(run.control, name) ? 'control-only' : 'guarded-only'])
|
|
187
|
+
);
|
|
177
188
|
const why = [];
|
|
178
189
|
let control = c?.pass ? 'pass' : 'fail';
|
|
179
190
|
if (!c) why.push('no control');
|
|
@@ -195,7 +206,14 @@ export function result(vector, run) {
|
|
|
195
206
|
} else why.push(...g.mismatches);
|
|
196
207
|
const live = run.live === true ? 'pass' : 'fail';
|
|
197
208
|
if (live === 'fail') why.push('not live');
|
|
198
|
-
return {
|
|
209
|
+
return {
|
|
210
|
+
...row,
|
|
211
|
+
control,
|
|
212
|
+
guarded,
|
|
213
|
+
live,
|
|
214
|
+
...(Object.keys(ran).length ? { ran } : {}),
|
|
215
|
+
...(why.length ? { why: why.join('; ') } : {}),
|
|
216
|
+
};
|
|
199
217
|
}
|
|
200
218
|
|
|
201
219
|
const passed = (r) =>
|
package/guard-runner.mjs
CHANGED
|
@@ -107,6 +107,7 @@ class Execution {
|
|
|
107
107
|
this.contexts = [];
|
|
108
108
|
this.pending = [];
|
|
109
109
|
this.errors = [];
|
|
110
|
+
this.notes = [];
|
|
110
111
|
this.answers = [];
|
|
111
112
|
this.attachSettings = new Map();
|
|
112
113
|
this.childSteps = [];
|
|
@@ -471,14 +472,33 @@ class Execution {
|
|
|
471
472
|
this.pending.push(started);
|
|
472
473
|
}
|
|
473
474
|
|
|
474
|
-
|
|
475
|
+
rule(event) {
|
|
475
476
|
const p = event.params;
|
|
476
|
-
|
|
477
|
+
return this.answers.find(
|
|
477
478
|
(a) =>
|
|
478
479
|
a.url === p.request.url &&
|
|
479
480
|
(!a.stage || (a.stage === 'Response') === Boolean(p.responseStatusCode)) &&
|
|
480
481
|
(a.cdp === 'Fetch.continueWithAuth') === (event.method === 'Fetch.authRequired')
|
|
481
482
|
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async paused({ url, ms }) {
|
|
486
|
+
const seen = () =>
|
|
487
|
+
this.client.events
|
|
488
|
+
.slice(this.eventStart)
|
|
489
|
+
.some(
|
|
490
|
+
(event) =>
|
|
491
|
+
(event.method === 'Fetch.requestPaused' || event.method === 'Fetch.authRequired') &&
|
|
492
|
+
event.params?.request?.url === url &&
|
|
493
|
+
this.rule(event)?.url === url
|
|
494
|
+
);
|
|
495
|
+
if (!(await this.poll(seen, ms)) && this.side === 'control')
|
|
496
|
+
this.errors.push(`paused: no pause for ${url} within ${ms}ms`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async answer(event) {
|
|
500
|
+
const p = event.params;
|
|
501
|
+
const rule = this.rule(event);
|
|
482
502
|
if (rule?.hold) return;
|
|
483
503
|
const method =
|
|
484
504
|
rule?.cdp ??
|
|
@@ -570,6 +590,10 @@ class Execution {
|
|
|
570
590
|
await sleep(step.wait);
|
|
571
591
|
return;
|
|
572
592
|
}
|
|
593
|
+
if (step.paused) {
|
|
594
|
+
await this.paused(step.paused);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
573
597
|
if (step.settle) {
|
|
574
598
|
await this.settle(step.settle);
|
|
575
599
|
return;
|
|
@@ -581,13 +605,20 @@ class Execution {
|
|
|
581
605
|
}
|
|
582
606
|
|
|
583
607
|
async settle(settle) {
|
|
584
|
-
|
|
608
|
+
const refused = () => (settle.dependsOn ?? []).some((name) => this.refused[name]);
|
|
609
|
+
const skipped = () => {
|
|
610
|
+
this.notes.push(
|
|
611
|
+
`settle:skipped-refused ${settle.beacon ? `beacon ${settle.beacon}` : settle.title ? `title ${settle.title}` : 'load'}`
|
|
612
|
+
);
|
|
613
|
+
};
|
|
614
|
+
if (refused()) return skipped();
|
|
585
615
|
if (settle.variant) await sleep(this.delay);
|
|
586
616
|
if (settle.title && !this.reader) {
|
|
587
617
|
await sleep(settle.ms ?? 1500);
|
|
588
618
|
return;
|
|
589
619
|
}
|
|
590
620
|
const met = await this.poll(async () => {
|
|
621
|
+
if (refused()) return true;
|
|
591
622
|
if (settle.beacon)
|
|
592
623
|
return (
|
|
593
624
|
(await this.requestCheck({ beacon: settle.beacon }, this.actionRequestStart)) &&
|
|
@@ -605,6 +636,7 @@ class Execution {
|
|
|
605
636
|
.some((e) => e.method === 'Page.loadEventFired' && e.sessionId === event.sessionId)
|
|
606
637
|
);
|
|
607
638
|
}, settle.ms ?? 1500);
|
|
639
|
+
if (refused()) return skipped();
|
|
608
640
|
if (settle.required && !met)
|
|
609
641
|
this.errors.push(
|
|
610
642
|
`settle: no ${settle.beacon ? `beacon from ${settle.beacon}` : settle.title ? `title ${settle.title}` : 'load'} within ${settle.ms ?? 1500}ms`
|
|
@@ -963,7 +995,7 @@ class Execution {
|
|
|
963
995
|
side: this.side,
|
|
964
996
|
});
|
|
965
997
|
const live = this.side === 'control' ? true : await this.check(this.vector.liveCheck);
|
|
966
|
-
return { observed, live, errors: this.errors };
|
|
998
|
+
return { observed, live, errors: this.errors, notes: this.notes };
|
|
967
999
|
} finally {
|
|
968
1000
|
this.stopped = true;
|
|
969
1001
|
await pumping;
|
|
@@ -1195,6 +1227,11 @@ async function executeRow(job, opts, clients, set, slot, index) {
|
|
|
1195
1227
|
errors: [...legs.control.errors, ...legs.guarded.errors],
|
|
1196
1228
|
designDependent: vector.designDependent ?? [],
|
|
1197
1229
|
};
|
|
1230
|
+
const notes = [
|
|
1231
|
+
...(legs.control.notes ?? []).map((note) => `control ${note}`),
|
|
1232
|
+
...(legs.guarded.notes ?? []).map((note) => `guarded ${note}`),
|
|
1233
|
+
];
|
|
1234
|
+
if (notes.length) row.notes = notes;
|
|
1198
1235
|
if (row.errors.length) {
|
|
1199
1236
|
row.control = 'fail';
|
|
1200
1237
|
row.guarded = 'fail';
|
package/guard-vectors.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"setVersion": "1.7.
|
|
3
|
+
"setVersion": "1.7.7",
|
|
4
4
|
"sources": [
|
|
5
5
|
{
|
|
6
6
|
"repo": "ai-ecoverse/slicc-cli",
|
|
@@ -5172,7 +5172,7 @@
|
|
|
5172
5172
|
},
|
|
5173
5173
|
"beacon": {
|
|
5174
5174
|
"means": "a beacon from {target}",
|
|
5175
|
-
"dependsOn": ["refused.7", "refused.8"],
|
|
5175
|
+
"dependsOn": ["refused.3", "refused.7", "refused.8"],
|
|
5176
5176
|
"requires": ["autoAttach"],
|
|
5177
5177
|
"check": {
|
|
5178
5178
|
"beacon": "{target}",
|
|
@@ -6811,7 +6811,10 @@
|
|
|
6811
6811
|
"background": true
|
|
6812
6812
|
},
|
|
6813
6813
|
{
|
|
6814
|
-
"
|
|
6814
|
+
"paused": {
|
|
6815
|
+
"url": "{target}/auth/{tag}?n={tag}",
|
|
6816
|
+
"ms": 5000
|
|
6817
|
+
}
|
|
6815
6818
|
},
|
|
6816
6819
|
{
|
|
6817
6820
|
"page": "fetch",
|
|
@@ -6949,7 +6952,10 @@
|
|
|
6949
6952
|
"as": "body"
|
|
6950
6953
|
},
|
|
6951
6954
|
{
|
|
6952
|
-
"
|
|
6955
|
+
"paused": {
|
|
6956
|
+
"url": "{target}/data?n={tag}",
|
|
6957
|
+
"ms": 5000
|
|
6958
|
+
}
|
|
6953
6959
|
},
|
|
6954
6960
|
{
|
|
6955
6961
|
"page": "fetch",
|
|
@@ -7120,7 +7126,10 @@
|
|
|
7120
7126
|
"as": "body"
|
|
7121
7127
|
},
|
|
7122
7128
|
{
|
|
7123
|
-
"
|
|
7129
|
+
"paused": {
|
|
7130
|
+
"url": "{target}/data?n={tag}",
|
|
7131
|
+
"ms": 5000
|
|
7132
|
+
}
|
|
7124
7133
|
},
|
|
7125
7134
|
{
|
|
7126
7135
|
"page": "fetch",
|
|
@@ -7280,7 +7289,10 @@
|
|
|
7280
7289
|
"status": true
|
|
7281
7290
|
},
|
|
7282
7291
|
{
|
|
7283
|
-
"
|
|
7292
|
+
"paused": {
|
|
7293
|
+
"url": "{target}/data?n={tag}",
|
|
7294
|
+
"ms": 5000
|
|
7295
|
+
}
|
|
7284
7296
|
},
|
|
7285
7297
|
{
|
|
7286
7298
|
"page": "fetch",
|
|
@@ -7465,7 +7477,10 @@
|
|
|
7465
7477
|
"status": true
|
|
7466
7478
|
},
|
|
7467
7479
|
{
|
|
7468
|
-
"
|
|
7480
|
+
"paused": {
|
|
7481
|
+
"url": "{target}/auth/{tag}?n={tag}",
|
|
7482
|
+
"ms": 5000
|
|
7483
|
+
}
|
|
7469
7484
|
},
|
|
7470
7485
|
{
|
|
7471
7486
|
"page": "fetch",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/slicc-browsers",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.7",
|
|
4
4
|
"description": "SLICC's shared browser catalog, JSON schemas and discovery conformance corpus",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -65,13 +65,13 @@
|
|
|
65
65
|
"trust:dry": "npx fledgling sync --dry-run -y --skip-publish",
|
|
66
66
|
"release": "semantic-release",
|
|
67
67
|
"test:guard": "npm run test:guard-integration && npm run test:guard-sweep",
|
|
68
|
-
"test:guard-integration": "node --test test/integration/guard-fit.test.mjs test/integration/guard-chain.test.mjs test/integration/guard-validation.test.mjs test/integration/guard-liveness.test.mjs test/integration/guard-observations.test.mjs test/integration/guard-child-latency.test.mjs test/integration/guard-adoption.test.mjs test/integration/guard-scoping.test.mjs",
|
|
68
|
+
"test:guard-integration": "node --test test/integration/guard-fit.test.mjs test/integration/guard-chain.test.mjs test/integration/guard-validation.test.mjs test/integration/guard-liveness.test.mjs test/integration/guard-observations.test.mjs test/integration/guard-child-latency.test.mjs test/integration/guard-adoption.test.mjs test/integration/guard-scoping.test.mjs test/integration/guard-late-pause.test.mjs test/integration/guard-legs.test.mjs",
|
|
69
69
|
"test:guard-sweep": "node scripts/test-guards.mjs",
|
|
70
70
|
"test:guard-target": "node --test test/integration/guard-runner.test.mjs",
|
|
71
71
|
"test:unit:guard": "node --experimental-test-coverage --test --test-coverage-include=guard-runner.mjs --test-coverage-include=fixture-server.mjs --test-coverage-lines=100 test/unit/guard-*.test.mjs"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
|
-
"@ai-ecoverse/slicc-shared-web": "1.
|
|
74
|
+
"@ai-ecoverse/slicc-shared-web": "1.23.1",
|
|
75
75
|
"@biomejs/biome": "2.5.15",
|
|
76
76
|
"ajv": "8.20.0",
|
|
77
77
|
"ajv-formats": "3.0.1",
|
|
@@ -616,6 +616,30 @@
|
|
|
616
616
|
}
|
|
617
617
|
}
|
|
618
618
|
},
|
|
619
|
+
{
|
|
620
|
+
"type": "object",
|
|
621
|
+
"additionalProperties": false,
|
|
622
|
+
"required": ["paused"],
|
|
623
|
+
"properties": {
|
|
624
|
+
"paused": {
|
|
625
|
+
"type": "object",
|
|
626
|
+
"additionalProperties": false,
|
|
627
|
+
"required": ["url", "ms"],
|
|
628
|
+
"properties": {
|
|
629
|
+
"url": {
|
|
630
|
+
"type": "string"
|
|
631
|
+
},
|
|
632
|
+
"ms": {
|
|
633
|
+
"type": "integer",
|
|
634
|
+
"minimum": 1
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
},
|
|
638
|
+
"on": {
|
|
639
|
+
"$ref": "#/$defs/on"
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
},
|
|
619
643
|
{
|
|
620
644
|
"type": "object",
|
|
621
645
|
"additionalProperties": false,
|