@a11ign/screenreader-fleet 0.4.2 → 0.5.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.
@@ -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 { expectedWorkerCode, remedyLines, codeDrift } from "./worker-code-check.mjs";
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 = expectedWorkerCode();
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);
@@ -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 expectedWorkerCode = ()=>codeVersion();
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
- return assertWorkersServe(expectedWorkerCode(), workers, {
125
- ...options,
126
- sourceDir: workerSourceDir()
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, expectedWorkerCode, readWorkerCode, remedyLines, workerSourceDirty };
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.4.2",
3
+ "version": "0.5.0",
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",