@a11ign/screenreader-fleet 0.4.2 → 0.5.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/dist/check-worker-code.mjs +3 -3
- package/dist/doctor.d.mts +15 -23
- package/dist/doctor.mjs +39 -88
- package/dist/worker-code-check.d.mts +28 -2
- package/dist/worker-code-check.mjs +46 -5
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ import { execFileSync } from "node:child_process";
|
|
|
5
5
|
import { errorText } from "@a11ign/screenreader-worker/error-text";
|
|
6
6
|
import { fleetScriptPaths } from "./src_fleet-scripts_mjs.mjs";
|
|
7
7
|
import { inventoryWorkerUrls, configuredWorkers, resolveWorkerPool } from "./fleet-env.mjs";
|
|
8
|
-
import {
|
|
8
|
+
import { remedyLines, codeDrift, resolveExpectedWorkerCode } from "./worker-code-check.mjs";
|
|
9
9
|
import { refuseUnknownFlags } from "./cli-flags.mjs";
|
|
10
10
|
import { requestJson } from "./worker-http.mjs";
|
|
11
11
|
refuseUnknownFlags([], {
|
|
@@ -42,9 +42,9 @@ async function versionOf(url) {
|
|
|
42
42
|
return response.json.code ?? "absent";
|
|
43
43
|
}
|
|
44
44
|
async function main() {
|
|
45
|
-
const expected =
|
|
45
|
+
const { code: expected, note } = await resolveExpectedWorkerCode();
|
|
46
46
|
const { urls, source } = workerUrls();
|
|
47
|
-
console.log(`this checkout: ${expected}`);
|
|
47
|
+
console.log(`this checkout: ${expected} (from ${note})`);
|
|
48
48
|
if (!urls.length) {
|
|
49
49
|
console.log("no worker configured, running locally, or listed in inventory.yml — nothing to compare");
|
|
50
50
|
process.exit(0);
|
package/dist/doctor.d.mts
CHANGED
|
@@ -91,30 +91,22 @@ export function resolvesToThisCheckout(resolvedRealPath: string, thisCheckoutRoo
|
|
|
91
91
|
*/
|
|
92
92
|
export function checkoutRootFor(resolvedRealPath: string): string | null;
|
|
93
93
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* "could not tell" and "not up to date" need opposite responses (investigate vs. rebuild), and this repo's
|
|
109
|
-
* own rule is that a lookup failure is never silently read as a clean answer.
|
|
110
|
-
*
|
|
111
|
-
* @param {string} tsconfigPath
|
|
112
|
-
* @param {{ run?: (cmd: string, args: string[]) => string }} [deps]
|
|
113
|
-
* @returns {boolean | null}
|
|
94
|
+
* The `exports` targets of `packageDir` that are NOT on disk, or `null` when the package's manifest could not be
|
|
95
|
+
* read or declares none -- "could not tell" and "missing" need opposite responses (investigate vs. rebuild), and a
|
|
96
|
+
* lookup failure is never read as a clean answer.
|
|
97
|
+
*
|
|
98
|
+
* WHY EXISTENCE AND NOT FRESHNESS. This asked `tsc --build --dry` whether the package's outputs were current, and
|
|
99
|
+
* a11ign builds `judge` with Rslib now (a11ign/a11ign#3580): its `tsconfig.json` is `noEmit`, so tsc has no output to
|
|
100
|
+
* be up to date about and answered wrongly on every run (a11ign/a11ign#3810). Rslib writes no stamp of the source it
|
|
101
|
+
* built from, and a timestamp comparison is the wrong first version this check once had (#256: `git checkout` moves
|
|
102
|
+
* source mtimes with no content change, so every worktree would read stale the moment it switched branches). So what
|
|
103
|
+
* is read is what Rslib's output actually IS: the files `exports` promises. A present but out-of-date `dist` is
|
|
104
|
+
* therefore NOT caught here, and the check says "present", never "up to date".
|
|
105
|
+
*
|
|
106
|
+
* @param {string} packageDir
|
|
107
|
+
* @returns {string[] | null}
|
|
114
108
|
*/
|
|
115
|
-
export function
|
|
116
|
-
run?: (cmd: string, args: string[]) => string;
|
|
117
|
-
}): boolean | null;
|
|
109
|
+
export function missingExportTargets(packageDir: string): string[] | null;
|
|
118
110
|
/**
|
|
119
111
|
* WHOSE dist a cross-package import actually resolves to, and is IT stale (#256) -- both computed from
|
|
120
112
|
* the exact SPECIFIER a real import site in this repo uses, never the bare package name. CLAUDE.md's own
|
package/dist/doctor.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFile, execFileSync } from "node:child_process";
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
|
-
import { existsSync
|
|
5
|
-
import {
|
|
4
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { createRequire } from "node:module";
|
|
@@ -26,51 +26,6 @@ function controlPlaneIsolation({ hasNodeModules, hasFleetKey, packages, isWorksp
|
|
|
26
26
|
why: `ADR 0012 VIOLATED: node_modules${count} sits beside the fleet SSH key. A compromised transitive dependency here can reach the key that reconfigures every Windows worker, each of which auto-logs-in to an unlocked desktop. ${remedy}`
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
-
function pnpmCliInvocation(args) {
|
|
30
|
-
const fromParent = process.env.npm_execpath ?? "";
|
|
31
|
-
if (/pnpm\.c?js$/.test(fromParent) && external_node_fs_existsSync(fromParent)) return {
|
|
32
|
-
command: process.execPath,
|
|
33
|
-
args: [
|
|
34
|
-
fromParent,
|
|
35
|
-
...args
|
|
36
|
-
]
|
|
37
|
-
};
|
|
38
|
-
const shim = onPath("pnpm");
|
|
39
|
-
if (null !== shim) return shimInvocation(shim, external_node_path_join("node_modules", "pnpm", "bin", "pnpm.cjs"), args);
|
|
40
|
-
const corepack = onPath("corepack");
|
|
41
|
-
if (null !== corepack) return shimInvocation(corepack, external_node_path_join("node_modules", "corepack", "dist", "corepack.js"), [
|
|
42
|
-
"pnpm",
|
|
43
|
-
...args
|
|
44
|
-
]);
|
|
45
|
-
throw new Error("could not find pnpm: `npm_execpath` is not a pnpm script, and neither `pnpm` nor `corepack` is on PATH. `packageManager` in the root package.json names the version -- `corepack enable` or `pnpm/action-setup` provides it.");
|
|
46
|
-
}
|
|
47
|
-
function onPath(name) {
|
|
48
|
-
for (const dir of (process.env.PATH ?? "").split(external_node_path_delimiter))if ("" !== dir) {
|
|
49
|
-
for (const candidate of [
|
|
50
|
-
external_node_path_join(dir, name),
|
|
51
|
-
external_node_path_join(dir, `${name}.cmd`)
|
|
52
|
-
])if (external_node_fs_existsSync(candidate)) return candidate;
|
|
53
|
-
}
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
function shimInvocation(shim, script, args) {
|
|
57
|
-
if (!shim.endsWith(".cmd")) return {
|
|
58
|
-
command: shim,
|
|
59
|
-
args
|
|
60
|
-
};
|
|
61
|
-
const found = [
|
|
62
|
-
external_node_path_join(external_node_path_dirname(shim), script),
|
|
63
|
-
external_node_path_join(external_node_path_dirname(process.execPath), script)
|
|
64
|
-
].find((path)=>external_node_fs_existsSync(path));
|
|
65
|
-
if (void 0 === found) throw new Error(`${shim} is a .cmd shim and its script ${script} is not beside it or beside node`);
|
|
66
|
-
return {
|
|
67
|
-
command: process.execPath,
|
|
68
|
-
args: [
|
|
69
|
-
found,
|
|
70
|
-
...args
|
|
71
|
-
]
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
29
|
refuseUnknownFlags([
|
|
75
30
|
"--json",
|
|
76
31
|
"--runs-dir="
|
|
@@ -78,14 +33,14 @@ refuseUnknownFlags([
|
|
|
78
33
|
entry: import.meta.url,
|
|
79
34
|
command: "pnpm run doctor"
|
|
80
35
|
});
|
|
81
|
-
const
|
|
36
|
+
const run = promisify(execFile);
|
|
82
37
|
const JSON_OUT = process.argv.includes("--json");
|
|
83
38
|
const REFUSED = 2;
|
|
84
39
|
const WORKERS_ENV = configuredWorkers();
|
|
85
40
|
const PAGES_PORT = Number(process.env.DATASET_PAGES_PORT || 5050);
|
|
86
41
|
const CTL = fleetScriptPaths().workerCtl;
|
|
87
42
|
const MODULE_DIR = fileURLToPath(new URL(".", import.meta.url));
|
|
88
|
-
const isMonorepoRoot = (root)=>
|
|
43
|
+
const isMonorepoRoot = (root)=>existsSync(resolve(root, "packages")) && existsSync(resolve(root, "package.json"));
|
|
89
44
|
function checkoutLayout(baseDir = MODULE_DIR) {
|
|
90
45
|
const root = resolve(baseDir, "..", "..", "..");
|
|
91
46
|
return {
|
|
@@ -99,7 +54,7 @@ const notACheckout = (names, root)=>{
|
|
|
99
54
|
function runsDirFor({ argv = process.argv.slice(2), baseDir = MODULE_DIR } = {}) {
|
|
100
55
|
const supplied = flagValue(argv, "runs-dir");
|
|
101
56
|
if (void 0 !== supplied) {
|
|
102
|
-
if (
|
|
57
|
+
if (existsSync(supplied)) return resolve(supplied);
|
|
103
58
|
throw new Error(`doctor: --runs-dir=${supplied} does not exist. Pass --runs-dir=<the runs directory>.`);
|
|
104
59
|
}
|
|
105
60
|
const root = resolve(baseDir, "..", "..", "..");
|
|
@@ -169,7 +124,7 @@ function workerControlFix(observed) {
|
|
|
169
124
|
fix: `${CTL} pool`,
|
|
170
125
|
note: "UTM has no registered worker VM; re-register the existing a11y-worker*.utm bundles in UTM first"
|
|
171
126
|
};
|
|
172
|
-
return
|
|
127
|
+
return existsSync("/Applications/UTM.app") ? {
|
|
173
128
|
fix: `${CTL} pool`,
|
|
174
129
|
note: "unlock the Mac first if it is locked"
|
|
175
130
|
} : {
|
|
@@ -178,7 +133,7 @@ function workerControlFix(observed) {
|
|
|
178
133
|
};
|
|
179
134
|
}
|
|
180
135
|
async function shell(cmd, args, timeout = 30000) {
|
|
181
|
-
const { stdout } = await
|
|
136
|
+
const { stdout } = await run(cmd, args, {
|
|
182
137
|
timeout,
|
|
183
138
|
encoding: "utf8"
|
|
184
139
|
});
|
|
@@ -229,8 +184,8 @@ function checkControlPlaneIsolation({ baseDir = MODULE_DIR } = {}) {
|
|
|
229
184
|
], root);
|
|
230
185
|
const raw = process.env.A11Y_SSH_KEY || "~/.ssh/a11y-witness_ed25519";
|
|
231
186
|
const keyPath = raw.startsWith("~/") ? resolve(homedir(), raw.slice(2)) : raw;
|
|
232
|
-
const hasFleetKey =
|
|
233
|
-
const hasNodeModules =
|
|
187
|
+
const hasFleetKey = existsSync(keyPath);
|
|
188
|
+
const hasNodeModules = existsSync(resolve(root, "node_modules"));
|
|
234
189
|
const verdict = controlPlaneIsolation({
|
|
235
190
|
hasNodeModules,
|
|
236
191
|
hasFleetKey,
|
|
@@ -246,27 +201,23 @@ function checkoutRootFor(resolvedRealPath) {
|
|
|
246
201
|
const idx = resolvedRealPath.indexOf("/packages/");
|
|
247
202
|
return -1 === idx ? null : resolvedRealPath.slice(0, idx);
|
|
248
203
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
204
|
+
function exportTargets(exportsField) {
|
|
205
|
+
if ("string" == typeof exportsField) return [
|
|
206
|
+
exportsField
|
|
207
|
+
];
|
|
208
|
+
if (null === exportsField || "object" != typeof exportsField) return [];
|
|
209
|
+
return Object.values(exportsField).flatMap(exportTargets);
|
|
210
|
+
}
|
|
211
|
+
function missingExportTargets(packageDir) {
|
|
212
|
+
let manifest;
|
|
254
213
|
try {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
"--build",
|
|
259
|
-
"--dry",
|
|
260
|
-
tsconfigPath
|
|
261
|
-
]);
|
|
262
|
-
output = run(tsc.command, tsc.args);
|
|
263
|
-
} catch (error) {
|
|
264
|
-
output = error?.stdout ?? "";
|
|
265
|
-
if (!output) return null;
|
|
214
|
+
manifest = JSON.parse(readFileSync(resolve(packageDir, "package.json"), "utf8"));
|
|
215
|
+
} catch {
|
|
216
|
+
return null;
|
|
266
217
|
}
|
|
267
|
-
const
|
|
268
|
-
if (
|
|
269
|
-
return
|
|
218
|
+
const targets = exportTargets(manifest?.exports);
|
|
219
|
+
if (0 === targets.length) return null;
|
|
220
|
+
return targets.filter((target)=>!existsSync(resolve(packageDir, target)));
|
|
270
221
|
}
|
|
271
222
|
function behindOriginMainNote(otherRoot) {
|
|
272
223
|
if (!otherRoot) return "";
|
|
@@ -294,25 +245,25 @@ function checkCrossPackageDist({ baseDir = MODULE_DIR } = {}) {
|
|
|
294
245
|
], thisCheckoutRoot);
|
|
295
246
|
let resolvedRealPath;
|
|
296
247
|
try {
|
|
297
|
-
resolvedRealPath =
|
|
248
|
+
resolvedRealPath = realpathSync(createRequire(import.meta.url).resolve(specifier));
|
|
298
249
|
} catch (error) {
|
|
299
250
|
return advise("dist-resolution", `could not resolve ${specifier} to check whose dist it comes from -- ${error.message}`, "pnpm run build");
|
|
300
251
|
}
|
|
301
|
-
if (resolvesToThisCheckout(resolvedRealPath,
|
|
252
|
+
if (resolvesToThisCheckout(resolvedRealPath, realpathSync(thisCheckoutRoot))) add("dist-resolution", true, `${specifier} resolves to this checkout's own dist`);
|
|
302
253
|
else {
|
|
303
254
|
const behindNote = behindOriginMainNote(checkoutRootFor(resolvedRealPath));
|
|
304
255
|
advise("dist-resolution", `${specifier} resolves to ${resolvedRealPath} (NOT this checkout${behindNote})`, "pnpm run primary:update && pnpm run build # if that is the primary checkout");
|
|
305
256
|
}
|
|
306
257
|
const distRoot = checkoutRootFor(resolvedRealPath) ?? thisCheckoutRoot;
|
|
307
|
-
const
|
|
308
|
-
const
|
|
309
|
-
if (null ===
|
|
310
|
-
if (
|
|
311
|
-
advise("dist-freshness", `packages/judge under ${distRoot} is
|
|
258
|
+
const judgeDir = resolve(distRoot, "packages/judge");
|
|
259
|
+
const missing = missingExportTargets(judgeDir);
|
|
260
|
+
if (null === missing) return advise("dist-freshness", `could not read the exports of ${judgeDir}/package.json to see whether its dist is built`, "pnpm run build");
|
|
261
|
+
if (0 === missing.length) return add("dist-freshness", true, `every exports target of packages/judge under ${distRoot} is present (existence only: Rslib leaves no stamp to read freshness from)`);
|
|
262
|
+
advise("dist-freshness", `packages/judge under ${distRoot} is missing ${missing.length} exports target(s): ${missing.join(", ")}`, "pnpm run build # in that checkout");
|
|
312
263
|
}
|
|
313
264
|
async function scorerWeightsFor({ from = import.meta.url } = {}) {
|
|
314
|
-
const judgeEntry =
|
|
315
|
-
const scorerEntry =
|
|
265
|
+
const judgeEntry = realpathSync(createRequire(from).resolve("@a11ign/judge"));
|
|
266
|
+
const scorerEntry = realpathSync(createRequire(judgeEntry).resolve("@a11ign/scorer"));
|
|
316
267
|
const { scorerPaths } = await import(pathToFileURL(scorerEntry).href);
|
|
317
268
|
return scorerPaths().weights;
|
|
318
269
|
}
|
|
@@ -327,7 +278,7 @@ async function checkJudge({ from = import.meta.url } = {}) {
|
|
|
327
278
|
} catch (error) {
|
|
328
279
|
return add("judge", false, `backend=local, but @a11ign/scorer could not be resolved from @a11ign/judge: ${error.message}`, "install @a11ign/scorer (a peer dependency of @a11ign/judge) beside @a11ign/judge");
|
|
329
280
|
}
|
|
330
|
-
return add("judge",
|
|
281
|
+
return add("judge", existsSync(weights), existsSync(weights) ? "backend=local, trained scorer present" : "backend=local, but the trained scorer is missing", `expected weights at ${weights} (where @a11ign/scorer says they are) — reinstall @a11ign/scorer`);
|
|
331
282
|
}
|
|
332
283
|
if ("anthropic" === backend || "openai" === backend) {
|
|
333
284
|
const key = "anthropic" === backend ? "ANTHROPIC_API_KEY" : "JUDGE_BASE_URL";
|
|
@@ -339,7 +290,7 @@ async function checkWorker() {
|
|
|
339
290
|
if (WORKERS_ENV.length) return checkConfiguredFleet(WORKERS_ENV);
|
|
340
291
|
const inventory = namedInventoryWorkers();
|
|
341
292
|
if (inventory.length) return checkConfiguredFleet(inventory);
|
|
342
|
-
if ("darwin" !== process.platform || !
|
|
293
|
+
if ("darwin" !== process.platform || !existsSync(CTL)) return add("worker", false, "no A11Y_WORKERS set, no inventory.yml fleet, and no local VM tooling here", "set A11Y_WORKERS=http://host:8765[,http://host2:8765], or see docs/getting-started.md");
|
|
343
294
|
let pool;
|
|
344
295
|
try {
|
|
345
296
|
pool = JSON.parse(await shell(CTL, [
|
|
@@ -466,7 +417,7 @@ function checkHostCapacity(pool) {
|
|
|
466
417
|
}
|
|
467
418
|
async function checkDatasetPages() {
|
|
468
419
|
const manifestPath = resolve(datasetDir(), "manifest.json");
|
|
469
|
-
if (!
|
|
420
|
+
if (!existsSync(manifestPath)) return add("dataset", false, "no manifest — the dataset has not been generated", "pnpm run training:generate");
|
|
470
421
|
const sample = JSON.parse(readFileSync(manifestPath, "utf8")).cases?.[0]?.id;
|
|
471
422
|
const probe = sample ? `${sample}/good.html` : "";
|
|
472
423
|
try {
|
|
@@ -481,7 +432,7 @@ async function checkDatasetPages() {
|
|
|
481
432
|
}
|
|
482
433
|
function checkRunState() {
|
|
483
434
|
const progress = resolve(datasetDir(), "capture-progress.json");
|
|
484
|
-
if (!
|
|
435
|
+
if (!existsSync(progress)) return add("run", true, "no capture run recorded");
|
|
485
436
|
const p = JSON.parse(readFileSync(progress, "utf8"));
|
|
486
437
|
if (!p.startedAt) return add("run", true, "no capture run recorded");
|
|
487
438
|
if (!p.finishedAt) return add("run", false, `a run is UNFINISHED (started ${p.startedAt})`, "pnpm run training:wait, or pnpm run training:capture -- --resume --no-cache");
|
|
@@ -563,5 +514,5 @@ function renderDoctor({ json, out }) {
|
|
|
563
514
|
async function main() {
|
|
564
515
|
process.exit(await doctorRun());
|
|
565
516
|
}
|
|
566
|
-
if (import.meta.url === pathToFileURL(process.argv[1] ?
|
|
567
|
-
export { addCheck, allChecks, checkControlPlaneIsolation, checkCrossPackageDist, checkJudge, checkPrimaryCheckoutMark, checkoutRootFor, doctorRun, errorDocument, fleetAgreementLine, gatingChecks, isRunnableCommand, nextCommand, readyFrom, recordedChecks, resolvesToThisCheckout, runsDirFor, scorerWeightsFor,
|
|
517
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ? realpathSync(process.argv[1]) : "").href) await main();
|
|
518
|
+
export { addCheck, allChecks, checkControlPlaneIsolation, checkCrossPackageDist, checkJudge, checkPrimaryCheckoutMark, checkoutRootFor, doctorRun, errorDocument, fleetAgreementLine, gatingChecks, isRunnableCommand, missingExportTargets, nextCommand, readyFrom, recordedChecks, resolvesToThisCheckout, runsDirFor, scorerWeightsFor, workerControlFix };
|
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hash every worker is expected to be serving, and WHERE IT CAME FROM. ONE function, asked by `a11ign-worker-code` and by
|
|
3
|
+
* `assertFleetRunsThisCheckout` alike, so the two cannot be given different hashers again (a11ign/a11ign#3781).
|
|
4
|
+
*
|
|
5
|
+
* With a layer clone present it is the CLONE's: the clone's own `code-version.mjs` over the clone's `src/`, which is what
|
|
6
|
+
* `layerCodeVersion("nvda-worker")` computes for the deploy and the lab, and what a guest is told to be on (`--layer-ref`). Asking the
|
|
7
|
+
* installed package instead made every worker read stale the first time a guest was deployed at a sha whose `.mjs` differed from the
|
|
8
|
+
* release, with a remedy ("redeploy") that could not clear it.
|
|
9
|
+
*
|
|
10
|
+
* With none it is the installed package's (a11ign/a11ign#3740: the one the package was RELEASED with, `codeVersion()` and no
|
|
11
|
+
* directory, because the built package's `workerSourceDir()` is `dist/` and holds none of the files a guest runs), and `source` and
|
|
12
|
+
* `note` say so, so a reading is never silent about which it was.
|
|
13
|
+
*
|
|
14
|
+
* ASYNC because the clone's hasher can only be imported dynamically, as `layerCodeVersion` does.
|
|
15
|
+
*
|
|
16
|
+
* @param {{ checkoutRoot?: string }} [options]
|
|
17
|
+
* @returns {Promise<{ code: string, source: "clone" | "installed", sourceDir: string, note: string }>}
|
|
18
|
+
*/
|
|
19
|
+
export function resolveExpectedWorkerCode({ checkoutRoot }?: {
|
|
20
|
+
checkoutRoot?: string;
|
|
21
|
+
}): Promise<{
|
|
22
|
+
code: string;
|
|
23
|
+
source: "clone" | "installed";
|
|
24
|
+
sourceDir: string;
|
|
25
|
+
note: string;
|
|
26
|
+
}>;
|
|
1
27
|
/**
|
|
2
28
|
* Refuse to capture with a fleet that is not running this checkout.
|
|
3
29
|
*
|
|
@@ -10,15 +36,15 @@
|
|
|
10
36
|
* A thin wrapper over `assertWorkersServe`, supplying the one thing only this file can compute: the hash.
|
|
11
37
|
*
|
|
12
38
|
* @param {string[]} workers
|
|
13
|
-
* @param {{when?: string, allow?: boolean, read?: (url: string) => Promise<string|null>, bareMetalUrls?: string[]}} options
|
|
39
|
+
* @param {{when?: string, allow?: boolean, read?: (url: string) => Promise<string|null>, bareMetalUrls?: string[], checkoutRoot?: string}} options
|
|
14
40
|
*/
|
|
15
41
|
export function assertFleetRunsThisCheckout(workers: string[], options?: {
|
|
16
42
|
when?: string;
|
|
17
43
|
allow?: boolean;
|
|
18
44
|
read?: (url: string) => Promise<string | null>;
|
|
19
45
|
bareMetalUrls?: string[];
|
|
46
|
+
checkoutRoot?: string;
|
|
20
47
|
}): Promise<void>;
|
|
21
|
-
export function expectedWorkerCode(): string;
|
|
22
48
|
import { codeDrift } from "./code-drift.mjs";
|
|
23
49
|
import { describeCodeDrift } from "./code-drift.mjs";
|
|
24
50
|
import { describeEmptyPool } from "./code-drift.mjs";
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { codeVersion, workerSourceDir } from "@a11ign/screenreader-worker/code-version";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
3
6
|
import { requestJson } from "./worker-http.mjs";
|
|
4
7
|
import { sandboxGitEnv } from "./src_git-safe-env_mjs.mjs";
|
|
5
8
|
const HEALTH_TIMEOUT_MS = 15000;
|
|
@@ -119,11 +122,49 @@ async function assertWorkersServe(expected, workers, options) {
|
|
|
119
122
|
process.stderr.write(refusal);
|
|
120
123
|
process.exit(3);
|
|
121
124
|
}
|
|
122
|
-
const
|
|
125
|
+
const NVDA_WORKER_LAYER = "nvda-worker";
|
|
126
|
+
function layerClone(checkoutRoot) {
|
|
127
|
+
const manifestPath = join(checkoutRoot, "packages", "control", "layers.json");
|
|
128
|
+
if (!existsSync(manifestPath)) return {
|
|
129
|
+
absent: `no packages/control/layers.json under ${checkoutRoot}`
|
|
130
|
+
};
|
|
131
|
+
const layer = JSON.parse(readFileSync(manifestPath, "utf8")).layers?.[NVDA_WORKER_LAYER];
|
|
132
|
+
if (!layer?.path) return {
|
|
133
|
+
absent: `${manifestPath} does not declare "${NVDA_WORKER_LAYER}"`
|
|
134
|
+
};
|
|
135
|
+
const dir = resolve(checkoutRoot, layer.path);
|
|
136
|
+
return existsSync(dir) ? {
|
|
137
|
+
dir
|
|
138
|
+
} : {
|
|
139
|
+
absent: `no layer clone at ${dir}`
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
async function resolveExpectedWorkerCode({ checkoutRoot = process.cwd() } = {}) {
|
|
143
|
+
const clone = layerClone(checkoutRoot);
|
|
144
|
+
if ("absent" in clone) return {
|
|
145
|
+
code: codeVersion(),
|
|
146
|
+
source: "installed",
|
|
147
|
+
sourceDir: workerSourceDir(),
|
|
148
|
+
note: `the installed @a11ign/screenreader-worker (${clone.absent})`
|
|
149
|
+
};
|
|
150
|
+
const sourceDir = `${join(clone.dir, "src")}/`;
|
|
151
|
+
const hasher = await import(pathToFileURL(join(sourceDir, "code-version.mjs")).href);
|
|
152
|
+
return {
|
|
153
|
+
code: hasher.codeVersion(sourceDir),
|
|
154
|
+
source: "clone",
|
|
155
|
+
sourceDir,
|
|
156
|
+
note: `the layer clone at ${clone.dir}`
|
|
157
|
+
};
|
|
158
|
+
}
|
|
123
159
|
async function assertFleetRunsThisCheckout(workers, options = {}) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
160
|
+
const { checkoutRoot, ...rest } = options;
|
|
161
|
+
const expected = await resolveExpectedWorkerCode({
|
|
162
|
+
checkoutRoot
|
|
163
|
+
});
|
|
164
|
+
if (!options.allow) process.stdout.write(`Expected worker code ${expected.code}, from ${expected.note}.\n`);
|
|
165
|
+
return assertWorkersServe(expected.code, workers, {
|
|
166
|
+
...rest,
|
|
167
|
+
sourceDir: expected.sourceDir
|
|
127
168
|
});
|
|
128
169
|
}
|
|
129
|
-
export { assertFleetRunsThisCheckout, codeDrift, describeCodeDrift, describeEmptyPool,
|
|
170
|
+
export { assertFleetRunsThisCheckout, codeDrift, describeCodeDrift, describeEmptyPool, readWorkerCode, remedyLines, resolveExpectedWorkerCode, workerSourceDirty };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a11ign/screenreader-fleet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Host-side lifecycle, health and capacity for a fleet of Windows NVDA capture workers: lease one, judge whether it is degrading, and know how many the host can afford.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|