@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 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; the rows
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 only = (a, b) => (a?.refused ?? []).filter((name) => !(b?.refused ?? []).includes(name));
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 { ...row, control, guarded, live, ...(why.length ? { why: why.join('; ') } : {}) };
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
- async answer(event) {
475
+ rule(event) {
475
476
  const p = event.params;
476
- const rule = this.answers.find(
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
- if ((settle.dependsOn ?? []).some((name) => this.refused[name])) return;
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';
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "setVersion": "1.7.5",
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
- "wait": 500
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
- "wait": 300
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
- "wait": 300
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
- "wait": 300
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
- "wait": 300
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.5",
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.21.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,