clearotron 0.2.1 → 0.3.0-beta.0
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/INSTALL.md +5 -4
- package/bin/example.mjs +9 -5
- package/bin/onboard.mjs +19 -19
- package/bin/stop.mjs +65 -3
- package/build-info.json +2 -2
- package/driver/CHANGELOG.md +21 -0
- package/driver/declination-call.mjs +32 -0
- package/driver/driver.config.mjs +20 -0
- package/driver/engine/mcp/recording-server.mjs +4 -0
- package/driver/gateway.mjs +8 -3
- package/driver/knockout-assess-record.mjs +5 -1
- package/driver/package.json +1 -1
- package/driver/pipeline.mjs +83 -2
- package/driver/predelivery-lint.mjs +22 -4
- package/driver/publish/knockout.mjs +12 -6
- package/driver/publish/render-knockout.mjs +133 -23
- package/driver/publish/report-data.mjs +13 -3
- package/driver/record-carry.mjs +2 -2
- package/driver/reference-score.mjs +1 -1
- package/driver/result-noun-fields.mjs +7 -0
- package/driver/skills/knockout-assess/SKILL.md +10 -4
- package/driver/stages-knockout.mjs +1 -1
- package/driver/stages.mjs +1 -1
- package/driver/suite-census.json +66 -18
- package/driver/unit-inventory.mjs +47 -0
- package/driver/unit-state-verdict.mjs +8 -8
- package/driver/verify-knockout.mjs +9 -1
- package/driver/whatif-memo-run.mjs +45 -4
- package/mcp-server/CHANGELOG.md +2 -0
- package/mcp-server/lib/brief.mjs +15 -0
- package/mcp-server/lib/driver.mjs +6 -0
- package/mcp-server/lib/knockout.mjs +435 -0
- package/mcp-server/lib/scrub.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +69 -4
- package/package.json +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +2 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/drain-preflight.mjs +2 -2
- package/scripts/freeze-example-run.mjs +3 -3
- package/scripts/headless-page.mjs +51 -2
- package/scripts/live-surface-check.mjs +86 -17
- package/scripts/render-check.mjs +61 -2
- package/scripts/deploy-test.sh +0 -309
|
@@ -47,7 +47,7 @@ import { join, dirname, relative, basename } from "node:path";
|
|
|
47
47
|
import { createHash } from "node:crypto";
|
|
48
48
|
import { driverDir } from "../shared/driver-dir.mjs"; //
|
|
49
49
|
import { tmpdir } from "node:os";
|
|
50
|
-
import { fileURLToPath } from "node:url";
|
|
50
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
51
51
|
|
|
52
52
|
const REPO = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
53
53
|
|
|
@@ -123,7 +123,7 @@ const FROZEN_DIRS = [
|
|
|
123
123
|
//
|
|
124
124
|
// research/ IS REQUIRED AND THE PROOF IS WHAT FOUND IT. publish/knockout.mjs
|
|
125
125
|
// traces every finding citation back to the run's own research payload — `research/<mark>.md`, read from
|
|
126
|
-
// the workspace (knockout.mjs:317) — and REFUSES the publish when a citation cannot be traced. The first
|
|
126
|
+
// the workspace (publish/knockout.mjs:317) — and REFUSES the publish when a citation cannot be traced. The first
|
|
127
127
|
// knockout freeze copied nine files, left research/ behind, and the republish proof threw:
|
|
128
128
|
//
|
|
129
129
|
// knockout publish REFUSED: 2 finding citation(s) could not be traced to this run's own research
|
|
@@ -506,7 +506,7 @@ const poolFull = join(scratch, "full");
|
|
|
506
506
|
const poolFrozen = join(scratch, "frozen");
|
|
507
507
|
const meta = { runId, codename, customerKey, template };
|
|
508
508
|
|
|
509
|
-
const { republishRun } = await import(join(REPO, "driver", "publish", "report-registry.mjs"));
|
|
509
|
+
const { republishRun } = await import(pathToFileURL(join(REPO, "driver", "publish", "report-registry.mjs")).href);
|
|
510
510
|
const publishInto = async (pool, dir) => {
|
|
511
511
|
mkdirSync(pool, { recursive: true });
|
|
512
512
|
return republishRun({ runId, meta, pool, poolUrl: "", runDir: dir, skipRegen: true });
|
|
@@ -36,6 +36,12 @@ import { execFileSync } from "node:child_process";
|
|
|
36
36
|
|
|
37
37
|
/** Chrome's own error pages live under this scheme. Nothing a real document is served from does. */
|
|
38
38
|
export const CHROME_ERROR_SCHEME = "chrome-error:";
|
|
39
|
+
/** What Chrome shows before it has navigated. Not a document, and not a wrong one. */
|
|
40
|
+
export const START_PAGE = "about:blank";
|
|
41
|
+
/** How long `assertPageLoaded` will wait for the browser to leave its start page. */
|
|
42
|
+
export const NAVIGATION_GRACE_MS = 5000;
|
|
43
|
+
/** How often it asks, inside that grace. */
|
|
44
|
+
export const NAVIGATION_POLL_MS = 100;
|
|
39
45
|
|
|
40
46
|
/**
|
|
41
47
|
* PURE. Given what the page says about itself, is it the document we asked for?
|
|
@@ -74,6 +80,25 @@ export function pageVerdict({ href = "", expected = "", marker = null, markerNam
|
|
|
74
80
|
+ "captured is Chrome's interstitial, not the artefact — and that page carries an `<h1>`, a "
|
|
75
81
|
+ "`<title>` and a body, so a content check alone reads it as a success." };
|
|
76
82
|
}
|
|
83
|
+
// ── THE BROWSER'S START PAGE IS NOT A WRONG DOCUMENT (tracker issue 273) ────────────────────────
|
|
84
|
+
//
|
|
85
|
+
// `about:blank` is what Chrome shows before it has navigated anywhere. Reaching the check below, it
|
|
86
|
+
// compares unequal to the expected URL and was reported as `wrong-document` — "a redirect, a stale tab
|
|
87
|
+
// or a second page target" — which is a finding about a page. It is not one. Nothing was ever loaded,
|
|
88
|
+
// so nothing about the target document has been measured either way.
|
|
89
|
+
//
|
|
90
|
+
// This mattered because it is what a LOADED BOX produces: three of these scripts launch Chrome with the
|
|
91
|
+
// URL as a command-line argument and cannot wait for a navigation event, so under load the address is
|
|
92
|
+
// read before the browser has moved. A real defect and a slow browser then arrived as the same message,
|
|
93
|
+
// and the arms that exist to catch a wrong page were the ones that fired.
|
|
94
|
+
if (said === START_PAGE || said.startsWith(`${START_PAGE}?`) || said.startsWith(`${START_PAGE}#`)) {
|
|
95
|
+
return { ok: false, kind: "not-navigated",
|
|
96
|
+
why: `chrome is on its start page (${said}) and never reached ${expected}. Nothing about that `
|
|
97
|
+
+ "document has been measured, so this is not a finding about the page. TWO THINGS LOOK LIKE "
|
|
98
|
+
+ "THIS and the address cannot tell them apart: a navigation that failed without saying so, and "
|
|
99
|
+
+ "one that had not happened yet. That is why the caller waits before asking — a verdict of this "
|
|
100
|
+
+ "kind means it waited and the browser never left the start page." };
|
|
101
|
+
}
|
|
77
102
|
// NORMALISED ON BOTH SIDES. Chrome resolves and percent-encodes a `file://` argument, so a raw string
|
|
78
103
|
// comparison fails on a path with a space and reports "the wrong document" about the right one.
|
|
79
104
|
const norm = (u) => { try { return new URL(u).href; } catch { return String(u); } };
|
|
@@ -103,8 +128,32 @@ export function pageVerdict({ href = "", expected = "", marker = null, markerNam
|
|
|
103
128
|
* handshake before this file existed and rewriting seven of them to share one is a bigger change than the
|
|
104
129
|
* defect warrants. What they must share is the QUESTION.
|
|
105
130
|
*/
|
|
106
|
-
export async function assertPageLoaded(evaluate, { expected, marker = null, markerName = "the page's own content", what = "this page", errorText = null
|
|
107
|
-
|
|
131
|
+
export async function assertPageLoaded(evaluate, { expected, marker = null, markerName = "the page's own content", what = "this page", errorText = null,
|
|
132
|
+
graceMs = NAVIGATION_GRACE_MS, pollMs = NAVIGATION_POLL_MS,
|
|
133
|
+
sleep = (ms) => new Promise((r) => setTimeout(r, ms)), now = () => Date.now() } = {}) {
|
|
134
|
+
// ── WAIT FOR THE BROWSER TO LEAVE ITS START PAGE, THEN JUDGE (tracker issue 273) ─────────────────
|
|
135
|
+
//
|
|
136
|
+
// Three of these scripts launch Chrome with the URL as an argument and get no response to wait on, so
|
|
137
|
+
// the first read of `location.href` can land before the browser has moved. On an idle box it never
|
|
138
|
+
// does; under a full parallel suite it did, repeatedly, and the arms reported the start page as a
|
|
139
|
+
// wrong document.
|
|
140
|
+
//
|
|
141
|
+
// BOUNDED, AND THE BOUND IS THE POINT. This waits for the browser to become ready — it does not wait
|
|
142
|
+
// for the page to become correct. If the address is anything other than the start page it is judged
|
|
143
|
+
// immediately, so a genuinely wrong document still fails on the first read and fails as fast as it did
|
|
144
|
+
// before. Only the "nothing has happened yet" case costs time, and only up to the grace.
|
|
145
|
+
//
|
|
146
|
+
// When the grace runs out the verdict is `not-navigated`, which is a could-not-look and says so.
|
|
147
|
+
// Deadline arithmetic is the caller's to drive: `sleep` and `now` are injected so the exhausted path
|
|
148
|
+
// can be exercised without a browser and without waiting.
|
|
149
|
+
let href = await evaluate("location.href");
|
|
150
|
+
if (String(href ?? "") === START_PAGE) {
|
|
151
|
+
const until = now() + graceMs;
|
|
152
|
+
while (String(href ?? "") === START_PAGE && now() < until) {
|
|
153
|
+
await sleep(pollMs);
|
|
154
|
+
href = await evaluate("location.href");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
108
157
|
const found = marker == null ? null : Boolean(await evaluate(`Boolean(${marker})`));
|
|
109
158
|
const verdict = pageVerdict({ href, expected, marker: found, markerName, errorText });
|
|
110
159
|
if (!verdict.ok) {
|
|
@@ -90,7 +90,9 @@ import { unitsActiveVerdict } from "../driver/unit-state-verdict.mjs";
|
|
|
90
90
|
import { deploymentBox } from "../shared/deployment-box.mjs"; // — extracted; one allowlist, two readers
|
|
91
91
|
import { unitFileDriftVerdict } from "../driver/unit-file-drift.mjs"; //
|
|
92
92
|
import { placeholdersIn, resolveValues, renderUnit } from "../driver/systemd/render-units.mjs"; //
|
|
93
|
-
import { CHECKED_UNITS, unitInventoryVerdict, serviceCommitVerdict, unitWorkingDirectory } from "../driver/unit-inventory.mjs"; // · -bundle ·
|
|
93
|
+
import { CHECKED_UNITS, unitInventoryVerdict, serviceCommitVerdict, unitWorkingDirectory, unitClone } from "../driver/unit-inventory.mjs"; // · -bundle ·
|
|
94
|
+
import { entrypointOf } from "../driver/systemd/install-census.mjs"; // the ONE ExecStart parser — a unit says which module it runs
|
|
95
|
+
import { treeOfRunning } from "../shared/checkout-move.mjs"; // …and the live argv says which tree that module came from
|
|
94
96
|
import { findUnitFiles, unitFilePath } from "../driver/unit-files.mjs"; //
|
|
95
97
|
import { managerGroupsVerdict } from "../driver/manager-groups-verdict.mjs"; //
|
|
96
98
|
import { config } from "../driver/driver.config.mjs"; //
|
|
@@ -234,14 +236,20 @@ function serviceClones() {
|
|
|
234
236
|
const out = [];
|
|
235
237
|
let reached = 0, lastErr = null;
|
|
236
238
|
for (const u of units) {
|
|
237
|
-
let wd = null, active = null, type = null, since = null;
|
|
239
|
+
let wd = null, active = null, type = null, since = null, mainPid = null, fragment = null;
|
|
238
240
|
try {
|
|
239
241
|
// — `Type` and `StateChangeTimestamp` ride along on a call that was already being made. Both
|
|
240
242
|
// are for the MESSAGE, never for the verdict: Type tells a reader whether an `activating` unit is a
|
|
241
243
|
// oneshot mid-fire or a service mid-restart, and the timestamp lets a human judge a long
|
|
242
244
|
// `activating` that this check deliberately does not judge (see driver/unit-state-verdict.mjs).
|
|
245
|
+
//
|
|
246
|
+
// `MainPID` and `FragmentPath` ride along for the SECOND way to attribute a unit to a checkout,
|
|
247
|
+
// below. Same call, no extra round trip. Deliberately NOT `ExecStart`: systemd renders it
|
|
248
|
+
// unexpanded — `argv[]=/usr/bin/node ${CLEAROTRON_CHECKOUT_DIR}/driver/runner.mjs` — so the one
|
|
249
|
+
// field that looks like it names the tree is the one field that does not.
|
|
243
250
|
const shown = execFileSync("systemctl", ["--user", "show", u,
|
|
244
|
-
"-p", "WorkingDirectory", "-p", "ActiveState", "-p", "Type", "-p", "StateChangeTimestamp"
|
|
251
|
+
"-p", "WorkingDirectory", "-p", "ActiveState", "-p", "Type", "-p", "StateChangeTimestamp",
|
|
252
|
+
"-p", "MainPID", "-p", "FragmentPath"],
|
|
245
253
|
{ encoding: "utf8", env, stdio: ["ignore", "pipe", "pipe"] });
|
|
246
254
|
// `show` answers for a unit that does not exist too (ActiveState=inactive), so a PARSED answer is
|
|
247
255
|
// proof the bus was reachable — which is exactly the fact the old catch destroyed.
|
|
@@ -252,19 +260,73 @@ function serviceClones() {
|
|
|
252
260
|
if (k === "ActiveState") active = v.join("=") || null;
|
|
253
261
|
if (k === "Type") type = v.join("=") || null;
|
|
254
262
|
if (k === "StateChangeTimestamp") since = v.join("=") || null;
|
|
263
|
+
if (k === "MainPID") mainPid = v.join("=") || null;
|
|
264
|
+
if (k === "FragmentPath") fragment = v.join("=") || null;
|
|
255
265
|
}
|
|
256
266
|
} catch (e) { lastErr = String(e?.stderr || e?.message || e).replace(/\s+/g, " ").trim().slice(0, 160); }
|
|
257
|
-
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
267
|
+
|
|
268
|
+
// ── ATTRIBUTION ONE: THE DECLARATION ────────────────────────────────────────────────────────────
|
|
269
|
+
let declaredTree = null, declaredWhy = null;
|
|
270
|
+
if (!wd) declaredWhy = "the unit reported no WorkingDirectory";
|
|
271
|
+
else {
|
|
272
|
+
const parsed = unitWorkingDirectory(wd);
|
|
273
|
+
if (!parsed.path) declaredWhy = parsed.why;
|
|
274
|
+
else {
|
|
275
|
+
const top = gitTry(parsed.path, "rev-parse", "--show-toplevel");
|
|
276
|
+
if (top.ok) declaredTree = top.out;
|
|
277
|
+
else declaredWhy = `git could not read ${parsed.path}: ${top.err}`;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── ATTRIBUTION TWO: WHAT THE PROCESS IS ACTUALLY RUNNING ───────────────────────────────────────
|
|
282
|
+
//
|
|
283
|
+
// The unit file names its entrypoint as `${CLEAROTRON_CHECKOUT_DIR}/<module>`; the LIVE process was
|
|
284
|
+
// started with that expanded, and `/proc/<pid>/cmdline` carries the resolved absolute path. So the
|
|
285
|
+
// unit file says WHICH module to look for and the process says WHERE it came from. Both readers
|
|
286
|
+
// already exist and are tested — this composes them rather than parsing anything new.
|
|
287
|
+
let runningTree = null, runningWhy = null;
|
|
288
|
+
const pid = Number(mainPid);
|
|
289
|
+
if (!pid) runningWhy = "the unit reported no MainPID, so no running process could be read";
|
|
290
|
+
else if (!fragment) runningWhy = `pid ${pid} is running but the unit reported no FragmentPath, so nothing says which module to look for`;
|
|
291
|
+
else {
|
|
292
|
+
let unitText = null;
|
|
293
|
+
try { unitText = readFileSync(fragment, "utf8"); }
|
|
294
|
+
catch (e) { runningWhy = `the unit file ${fragment} could not be read: ${String(e?.message ?? e).slice(0, 80)}`; }
|
|
295
|
+
if (unitText !== null) {
|
|
296
|
+
const { rel, unreadable } = entrypointOf(unitText);
|
|
297
|
+
if (!rel) runningWhy = unreadable;
|
|
298
|
+
else {
|
|
299
|
+
let cmdline = null;
|
|
300
|
+
try { cmdline = readFileSync(`/proc/${pid}/cmdline`, "utf8"); }
|
|
301
|
+
catch (e) { runningWhy = `/proc/${pid}/cmdline could not be read: ${String(e?.message ?? e).slice(0, 80)}`; }
|
|
302
|
+
if (cmdline !== null) {
|
|
303
|
+
const { tree, why } = treeOfRunning(cmdline, rel);
|
|
304
|
+
if (!tree) runningWhy = why;
|
|
305
|
+
else {
|
|
306
|
+
const top = gitTry(tree, "rev-parse", "--show-toplevel");
|
|
307
|
+
if (top.ok) runningTree = top.out;
|
|
308
|
+
else runningWhy = `git could not read ${tree}, which pid ${pid} is running from: ${top.err}`;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const chosen = unitClone({ declaredTree, runningTree, declaredWhy, runningWhy });
|
|
316
|
+
// — an inactive unit that reported nothing is NOT a gap in the population; there is genuinely
|
|
317
|
+
// nothing to compare, and `unreadable: null` says so. A unit that IS running and could not be
|
|
318
|
+
// attributed is a gap, and the reason names both halves.
|
|
319
|
+
if (!chosen.clone) {
|
|
320
|
+
const idle = !wd && !pid;
|
|
321
|
+
out.push({ unit: u, active, type, since, clone: null, head: null, source: null,
|
|
322
|
+
unreadable: idle ? null : chosen.why });
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const head = gitTry(chosen.clone, "rev-parse", "HEAD");
|
|
326
|
+
out.push({ unit: u, active, type, since, clone: chosen.clone, source: chosen.source,
|
|
327
|
+
disagreement: chosen.disagreement,
|
|
328
|
+
head: head.ok ? head.out : null,
|
|
329
|
+
unreadable: head.ok ? null : `git could not read HEAD in ${chosen.clone}: ${head.err}` });
|
|
268
330
|
}
|
|
269
331
|
const probe = reached > 0
|
|
270
332
|
? { ok: true, why: null }
|
|
@@ -441,7 +503,12 @@ try {
|
|
|
441
503
|
//
|
|
442
504
|
// The arm above already travels the route the harness uses (`mcpToolCall` on `MCP_URL`, the same
|
|
443
505
|
// client `scripts/e2e.mjs` enqueues through), so a door that 401s every caller ALREADY fails this
|
|
444
|
-
// check and already fails the deploy —
|
|
506
|
+
// check and already fails the deploy — WHEN a deploy runs this script, which at the time of writing
|
|
507
|
+
// nothing does. `scripts/deploy-test.sh` gated on this script's exit code; that script is retired, and
|
|
508
|
+
// what replaced it was measured on the operations side and does NOT invoke this one. Nothing here is
|
|
509
|
+
// wrong: the check is a real instrument and its reasoning holds. It is simply not reached, and a caller
|
|
510
|
+
// is being wired back in. Until it is, read this as what the check is FOR, not as evidence that
|
|
511
|
+
// something enforces it.
|
|
445
512
|
// That is the issue's first criterion, by its sanctioned second branch, and its third and fourth.
|
|
446
513
|
//
|
|
447
514
|
// What neither arm could say is WHY the door has the posture it has. This face reaches the auth-proxy
|
|
@@ -592,8 +659,10 @@ const { clones, probe: unitProbe } = serviceClones();
|
|
|
592
659
|
const running = clones.filter((c) => c.head);
|
|
593
660
|
const heads = [...new Set(running.map((c) => c.head))];
|
|
594
661
|
// — THREE OUTCOMES, NOT TWO. This arm is the one whose entire purpose is to catch a service still
|
|
595
|
-
// running an old bundle after a deploy
|
|
596
|
-
// deploys itself every hour
|
|
662
|
+
// running an old bundle after a deploy. `deploy-test.sh` ran it as the final gate on an instance that
|
|
663
|
+
// deploys itself every hour; that script is retired and its replacement does not invoke this one, so at
|
|
664
|
+
// the time of writing nothing reaches this arm on a deploy — see the note above. It had been degrading to
|
|
665
|
+
// `skip` with a reason that ASSERTED the deployment
|
|
597
666
|
// was not systemd --user — on a box where it is, and where the units are running.
|
|
598
667
|
// · could not look → skip, naming the error. Not probed is not passed.
|
|
599
668
|
// · looked, found none → skip, saying so. A genuinely non-systemd deployment lands here honestly.
|
package/scripts/render-check.mjs
CHANGED
|
@@ -64,7 +64,7 @@ import { execFileSync, spawn } from "node:child_process";
|
|
|
64
64
|
import { createServer } from "node:http";
|
|
65
65
|
import { basename, extname } from "node:path";
|
|
66
66
|
import { join, dirname } from "node:path";
|
|
67
|
-
import { fileURLToPath } from "node:url";
|
|
67
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
68
68
|
import { tmpdir } from "node:os";
|
|
69
69
|
import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — one entry-point test, all spellings
|
|
70
70
|
import { envFrom } from "../shared/env-aliases.mjs"; // — the name a reader is told to set is the one in force
|
|
@@ -447,12 +447,36 @@ async function measureAtZoom(zoom, deadlineMs = 90_000) {
|
|
|
447
447
|
}
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
+
/**
|
|
451
|
+
* PURE. Did the in-frame probe post, and if not, which of the two causes does the evidence name?
|
|
452
|
+
*
|
|
453
|
+
* SEPARATED SO EVERY BRANCH CAN BE DRIVEN. The whole finding here is a check that reported three failed
|
|
454
|
+
* measurements about values it never read, and an arm that could only reach this through a real browser on
|
|
455
|
+
* a starved runner would be the same shape one level up: untestable except by luck.
|
|
456
|
+
*
|
|
457
|
+
* `heightMsgs` is the discriminator and was already being collected. It counts messages from the report's
|
|
458
|
+
* OWN height bridge, which travels the same postMessage path as the probe — so non-zero means the frame
|
|
459
|
+
* loaded, ran scripts and reached this shell, and a missing probe is then the probe's problem. Zero means
|
|
460
|
+
* nothing arrived from inside at all, and the probe is not the thing to look at.
|
|
461
|
+
*/
|
|
462
|
+
export function probeVerdict({ innerScrollbar, hOverflowPx, slackPx, heightMsgs = 0 } = {}) {
|
|
463
|
+
const missing = innerScrollbar === "no-probe" || hOverflowPx === "no-probe" || slackPx === null;
|
|
464
|
+
if (!missing) return { measured: true, cause: null, why: null };
|
|
465
|
+
return heightMsgs > 0
|
|
466
|
+
? { measured: false, cause: "probe-only",
|
|
467
|
+
why: `${heightMsgs} height post(s) DID arrive, so the frame loaded and its scripts ran and reached `
|
|
468
|
+
+ "this shell. The probe alone is missing — look at the probe, not at the report." }
|
|
469
|
+
: { measured: false, cause: "nothing-from-inside",
|
|
470
|
+
why: "No message of any kind arrived from inside the frame. The frame did not load, or its scripts "
|
|
471
|
+
+ "did not run. The probe is not the thing to look at." };
|
|
472
|
+
}
|
|
473
|
+
|
|
450
474
|
async function main() {
|
|
451
475
|
const { pool: POOL, built } = resolvePool();
|
|
452
476
|
const runId = pickRun(POOL);
|
|
453
477
|
mkdirSync(WORK, { recursive: true });
|
|
454
478
|
|
|
455
|
-
const { readReport } = await import(join(REPO, "driver", "portal-report.mjs"));
|
|
479
|
+
const { readReport } = await import(pathToFileURL(join(REPO, "driver", "portal-report.mjs")).href);
|
|
456
480
|
const html = readReport(join(POOL, runId), { staff: true, poolRoot: POOL });
|
|
457
481
|
// chrome.css carries 35KB of typography; without it the layout is not the one users see. What must be
|
|
458
482
|
// true is that NO external stylesheet link survives into the measured document — either it was inlined,
|
|
@@ -473,12 +497,40 @@ async function main() {
|
|
|
473
497
|
console.log(" Expect the sideways assertion to FAIL. A clean run here means the instrument is blind.\n");
|
|
474
498
|
}
|
|
475
499
|
let failures = 0;
|
|
500
|
+
// A COULD-NOT-LOOK IS NOT A FAILED MEASUREMENT (tracker issue 239). Counted apart from `failures`
|
|
501
|
+
// because the two mean different things to whoever reads the exit code: 1 says the layout is wrong,
|
|
502
|
+
// 2 says nothing was measured. Merging them is how a starved runner sends a reader to look at CSS.
|
|
503
|
+
let unmeasured = 0;
|
|
476
504
|
|
|
477
505
|
for (const zoom of ZOOMS) {
|
|
478
506
|
const measured = await measureAtZoom(zoom);
|
|
479
507
|
if (!measured.ok) { console.log(` zoom ${zoom}: FAILED — ${measured.why}`); failures++; continue; }
|
|
480
508
|
const r = measured.state;
|
|
481
509
|
|
|
510
|
+
// ── THE THREE ASSERTIONS BELOW READ THE IN-FRAME PROBE, AND IT MAY NEVER HAVE POSTED ────────────
|
|
511
|
+
//
|
|
512
|
+
// `no-probe` and `null` are not measurements that disagreed with the expectation — they are the
|
|
513
|
+
// expectation never being tested. Rendering them as `FAIL … (got "no-probe")` is a check that could
|
|
514
|
+
// not look, reported as a check that looked and disliked what it saw, and it sends a reader to the
|
|
515
|
+
// report's CSS where there is nothing to find. Measured on a starved runner: three assertions failed
|
|
516
|
+
// on a branch whose diff was comments, a documentation line and a new test file.
|
|
517
|
+
//
|
|
518
|
+
// `heightMsgs` separates the two causes and was already being collected. It counts messages from the
|
|
519
|
+
// report's OWN height bridge, which travels the same postMessage path as the probe: non-zero means
|
|
520
|
+
// the frame loaded, ran scripts and reached the parent, so a missing probe is the probe's problem.
|
|
521
|
+
// Zero means nothing from inside arrived at all, and the probe is not the thing to look at.
|
|
522
|
+
const probe = probeVerdict(r);
|
|
523
|
+
if (!probe.measured) {
|
|
524
|
+
unmeasured++;
|
|
525
|
+
console.log(` zoom ${zoom}: (ready by ${r.readyBy}, heights ${r.heightsQuiet}, `
|
|
526
|
+
+ `settled after ${r.settleTries} × 25ms, ${r.heightMsgs} height post(s), ${r.probeMsgs} probe post(s))`);
|
|
527
|
+
console.log(` COULD NOT MEASURE — the in-frame probe never posted, so the report's own scrollbar,`);
|
|
528
|
+
console.log(` its sideways overflow and the frame's slack were not read at zoom ${zoom}.`);
|
|
529
|
+
console.log(` ${probe.why}`);
|
|
530
|
+
console.log(` ── raw state: ${JSON.stringify(r)}`);
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
|
|
482
534
|
const checks = [
|
|
483
535
|
["no border stealing from the frame's viewport", r.borderSteals === 0, r.borderSteals],
|
|
484
536
|
["the report has NO scrollbar of its own", r.innerScrollbar === false, r.innerScrollbar],
|
|
@@ -540,6 +592,13 @@ async function main() {
|
|
|
540
592
|
+ "clean run of this check is not evidence the report is clean.");
|
|
541
593
|
process.exit(caught ? 0 : 1);
|
|
542
594
|
}
|
|
595
|
+
if (unmeasured) {
|
|
596
|
+
// EXIT 2, THE HOUSE MEANING FOR COULD-NOT-LOOK. It still stops CI — nothing is waved through — but it
|
|
597
|
+
// does not claim the layout was measured and found wrong.
|
|
598
|
+
console.error(`\nrender-check: nothing was measured at ${unmeasured} zoom level(s) — the in-frame probe `
|
|
599
|
+
+ `never posted. This is a failure to LOOK, not a finding about the report.`);
|
|
600
|
+
process.exit(2);
|
|
601
|
+
}
|
|
543
602
|
console.log(failures ? `\nrender-check: ${failures} FAILED` : "\nrender-check: all checks passed");
|
|
544
603
|
process.exit(failures ? 1 : 0);
|
|
545
604
|
}
|