@deeeed/metamask-harness 0.57.0 → 0.59.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.
@@ -1,572 +0,0 @@
1
- #!/usr/bin/env bash
2
- # verify.sh — extension slot verification (readiness/proof layer)
3
- #
4
- # Purpose:
5
- # Recipe-tree verification for ONE extension instance: static dist checks,
6
- # live readiness over CDP, and the smoke recipe run, aggregated into a
7
- # verify summary.
8
- #
9
- # Inputs (flags / env):
10
- # --target <metamask-extension> (default $PWD), --cdp-port <port>
11
- # (numeric; empty = static-only), --out <recipes-dir> (validated under
12
- # target), --static-only, --artifacts-dir <dir>
13
- # env RECIPE_HARNESS_ROOT, RECIPE_RUNTIME_DIR
14
- #
15
- # Outputs:
16
- # <artifacts>/summary.json + logs/ (readiness, recipe runs).
17
- # Exit 0 — verify pass; 1 — checks failed / helpers missing; 2 — bad args
18
- # or unsafe/missing --out.
19
- #
20
- # Never touches: product source files; the dist build (read-only);
21
- # anything outside the harness dir and artifacts dir.
22
- set -euo pipefail
23
-
24
- TARGET="$PWD"
25
- CDP_PORT=""
26
- ARTIFACTS=""
27
- STATIC_ONLY=false
28
- OUT=""
29
- require_value() { [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; }
30
- while [ "$#" -gt 0 ]; do
31
- case "$1" in
32
- --target) require_value "$@"; TARGET="$2"; shift 2 ;;
33
- --out) require_value "$@"; OUT="$2"; shift 2 ;;
34
- --cdp-port) require_value "$@"; CDP_PORT="$2"; shift 2 ;;
35
- --artifacts-dir) require_value "$@"; ARTIFACTS="$2"; shift 2 ;;
36
- --static-only) STATIC_ONLY=true; shift ;;
37
- -h|--help) echo "Usage: verify.sh [--target <metamask-extension>] [--out <recipes-dir>] [--cdp-port <port>] [--static-only]"; exit 0 ;;
38
- *) echo "Unknown arg: $1" >&2; exit 2 ;;
39
- esac
40
- done
41
-
42
- # Reject a non-numeric --cdp-port before it is interpolated into shell/HTTP/CDP
43
- # strings. Empty is allowed (static-only verify needs no port).
44
- if [ -n "$CDP_PORT" ]; then
45
- case "$CDP_PORT" in
46
- *[!0-9]*) echo "Invalid --cdp-port (must be numeric): $CDP_PORT" >&2; exit 2 ;;
47
- esac
48
- fi
49
-
50
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
51
- # shellcheck disable=SC1091
52
- for _hp in "$SCRIPT_DIR/lib/harness-path.sh" "$SCRIPT_DIR/../../adapters/shared/harness-path.sh" "$SCRIPT_DIR/../shared/harness-path.sh"; do
53
- [ -f "$_hp" ] && { . "$_hp"; break; }
54
- done
55
- unset _hp
56
- if ! command -v harness_root >/dev/null 2>&1; then
57
- echo "extension verify: shared lib adapters/shared/harness-path.sh not found; reinstall the harness." >&2
58
- exit 1
59
- fi
60
-
61
- # Safe task-local --out resolution.
62
- resolve_harness_out() {
63
- local target="$1"
64
- local out="$2"
65
- node - "$target" "$out" <<'NODE'
66
- const path = require('path');
67
-
68
- const target = path.resolve(process.argv[2]);
69
- const out = process.argv[3];
70
- if (!out) process.exit(1);
71
- if (out.split(/[\\/]+/).includes('..')) process.exit(1);
72
-
73
- const resolved = path.resolve(target, out);
74
- const relative = path.relative(target, resolved);
75
- if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
76
- process.exit(1);
77
- }
78
-
79
- process.stdout.write(resolved);
80
- NODE
81
- }
82
- TARGET="$(cd "$TARGET" && pwd)"
83
- RUNTIME_DIR="$(recipe_runtime_dir)"
84
- HARNESS_ROOT="$(harness_root)"
85
- HARNESS_REL="$HARNESS_ROOT/extension"
86
- HARNESS_DIR="$(harness_dir "$TARGET" extension)"
87
- RUNNER_BIN="$HARNESS_DIR/runner/bin/mm-harness"
88
- # --out (optional): a task-local recipes dir. Resolve it safely within the target
89
- # (resolve_harness_out rejects absolute/.. escapes) and prefer its smoke recipe so
90
- # `live --out <dir>` does not silently fall back to the installed default.
91
- SMOKE_RECIPE="$HARNESS_DIR/runner/recipes/extension/runner/smoke.recipe.json"
92
- if [ -n "$OUT" ]; then
93
- # Fail fast on a task-local --out that does not contain the requested recipe.
94
- # Silently falling back to the installed default would validate a different
95
- # (possibly stale) recipe than the caller explicitly asked for.
96
- OUT_ABS="$(resolve_harness_out "$TARGET" "$OUT" 2>/dev/null || true)"
97
- if [ -z "$OUT_ABS" ]; then
98
- echo "extension verify: --out '$OUT' did not resolve to a safe path under the target." >&2
99
- exit 2
100
- fi
101
- CANONICAL_SMOKE_RECIPE="$OUT_ABS/extension/runner/smoke.recipe.json"
102
- LEGACY_SMOKE_RECIPE="$OUT_ABS/runner/smoke.extension.recipe.json"
103
- if [ -f "$CANONICAL_SMOKE_RECIPE" ] && [ -f "$LEGACY_SMOKE_RECIPE" ]; then
104
- echo "extension verify: --out '$OUT' declares runner.smoke for extension in both canonical and legacy paths." >&2
105
- echo "Next: Remove $LEGACY_SMOKE_RECIPE and rerun verification." >&2
106
- exit 2
107
- fi
108
- if [ -f "$CANONICAL_SMOKE_RECIPE" ]; then
109
- SMOKE_RECIPE="$CANONICAL_SMOKE_RECIPE"
110
- elif [ -f "$LEGACY_SMOKE_RECIPE" ]; then
111
- SMOKE_RECIPE="$LEGACY_SMOKE_RECIPE"
112
- else
113
- echo "extension verify: --out '$OUT' has no extension runner.smoke recipe." >&2
114
- echo "Next: Add extension/runner/smoke.recipe.json under $OUT_ABS or omit --out." >&2
115
- exit 2
116
- fi
117
- unset CANONICAL_SMOKE_RECIPE LEGACY_SMOKE_RECIPE
118
- fi
119
- ARTIFACTS="${ARTIFACTS:-$HARNESS_DIR/verify/$(date -u +%Y%m%dT%H%M%SZ)}"
120
- mkdir -p "$ARTIFACTS/logs"
121
- EXTENSION_ID_FILE="$TARGET/$RUNTIME_DIR/extension.id"
122
- # Shared JSON reader: $SCRIPT_DIR/lib when running from the installed copy,
123
- # else the runner canonical at adapters/shared.
124
- # shellcheck disable=SC1091
125
- for _lib in "$SCRIPT_DIR/lib/json-field.sh" "$SCRIPT_DIR/../../adapters/shared/json-field.sh"; do
126
- [ -f "$_lib" ] && { . "$_lib"; break; }
127
- done
128
- unset _lib
129
- # Fail fast with an actionable message if neither path loaded (e.g. an install that
130
- # predates the lib co-location), instead of a cryptic later `set -e` abort.
131
- if ! command -v read_runtime_context_field >/dev/null 2>&1; then
132
- echo "extension verify: json-field helper missing (adapters/shared/json-field.sh). Run mm-harness extension prepare --target $TARGET" >&2
133
- exit 1
134
- fi
135
- # readiness probe is app control: co-located in installed copies, under
136
- # adapters/extension in a repo checkout.
137
- READINESS_MJS=""
138
- for _r in "$SCRIPT_DIR/readiness.mjs" "$SCRIPT_DIR/../../adapters/extension/readiness.mjs"; do
139
- [ -f "$_r" ] && { READINESS_MJS="$_r"; break; }
140
- done
141
- unset _r
142
- if [ -z "$READINESS_MJS" ]; then
143
- echo "extension verify: readiness.mjs missing next to $SCRIPT_DIR; reinstall the runner." >&2
144
- exit 1
145
- fi
146
- refresh_extension_id() {
147
- if [ -f "$EXTENSION_ID_FILE" ]; then
148
- RECIPE_HARNESS_EXTENSION_ID="$(tr -d '[:space:]' < "$EXTENSION_ID_FILE")"
149
- export RECIPE_HARNESS_EXTENSION_ID
150
- else
151
- unset RECIPE_HARNESS_EXTENSION_ID || true
152
- fi
153
- }
154
- CONTEXT_PATH="${RECIPE_RUNTIME_CONTEXT:-$TARGET/$RUNTIME_DIR/agentic-runtime.json}"
155
- if [ -f "$CONTEXT_PATH" ]; then
156
- CONTEXT_EXTENSION_ID="$(read_runtime_context_field "$CONTEXT_PATH" extensionId || true)"
157
- if [[ "$CONTEXT_EXTENSION_ID" =~ ^[a-p]{32}$ ]]; then
158
- mkdir -p "$(dirname "$EXTENSION_ID_FILE")"
159
- printf '%s\n' "$CONTEXT_EXTENSION_ID" > "$EXTENSION_ID_FILE"
160
- RECIPE_HARNESS_EXTENSION_ID="$CONTEXT_EXTENSION_ID"
161
- export RECIPE_HARNESS_EXTENSION_ID
162
- else
163
- refresh_extension_id
164
- fi
165
- unset CONTEXT_EXTENSION_ID
166
- else
167
- refresh_extension_id
168
- fi
169
-
170
- # Infer checkout-scoped CDP port when caller omitted --cdp-port.
171
- if [ -z "$CDP_PORT" ]; then
172
- for _rsp in "$SCRIPT_DIR/lib/resolve-slot-ports.sh" "$SCRIPT_DIR/../../adapters/shared/resolve-slot-ports.sh" "$SCRIPT_DIR/../shared/resolve-slot-ports.sh"; do
173
- [ -f "$_rsp" ] && { # shellcheck disable=SC1090
174
- . "$_rsp"; break; }
175
- done
176
- unset _rsp
177
- if command -v apply_extension_slot_ports >/dev/null 2>&1; then
178
- apply_extension_slot_ports "$TARGET"
179
- fi
180
- fi
181
- if [ -n "$CDP_PORT" ]; then
182
- case "$CDP_PORT" in
183
- *[!0-9]*) echo "Invalid inferred --cdp-port (must be numeric): $CDP_PORT" >&2; exit 2 ;;
184
- esac
185
- fi
186
-
187
- status="pass"
188
- checks=()
189
-
190
- fixture_status_json() {
191
- TARGET_FOR_FIXTURE="$TARGET" RECIPE_RUNTIME_DIR="$RUNTIME_DIR" node <<'NODE'
192
- const fs = require('fs');
193
- const crypto = require('crypto');
194
- const path = require('path');
195
- const target = process.env.TARGET_FOR_FIXTURE;
196
- const runtimeDir = process.env.RECIPE_RUNTIME_DIR;
197
- if (!runtimeDir) throw new Error('RECIPE_RUNTIME_DIR is required');
198
- const candidates = [
199
- runtimeDir + '/wallet-fixture.json',
200
- 'test/e2e/seeder/withFixtures.js',
201
- 'test/e2e/fixtures',
202
- 'fixtures',
203
- ].map((rel) => path.join(target, rel));
204
- const found = candidates.find((file) => fs.existsSync(file));
205
- const extensionId = path.join(target, runtimeDir, 'extension.id');
206
- const profileHints = [];
207
- if (fs.existsSync(extensionId)) profileHints.push({ path: `${runtimeDir}/extension.id`, type: 'extension-id' });
208
- if (!found) {
209
- console.log(JSON.stringify({
210
- status: 'MISSING_FIXTURES',
211
- message: 'Fixture status: MISSING_FIXTURES. This run may depend on an inherited browser/profile state. Prefer a prepared debug profile or fixture seed before spending time repairing state manually.',
212
- profileHints,
213
- }));
214
- process.exit(0);
215
- }
216
- const stat = fs.statSync(found);
217
- const isFile = stat.isFile();
218
- let sha256 = null;
219
- let validJson = null;
220
- let hasWalletPassword = false;
221
- let mobileAccountShape = false;
222
- const rel = path.relative(target, found);
223
- const isWalletFixture = rel === runtimeDir + '/wallet-fixture.json';
224
- if (isFile) {
225
- const bytes = fs.readFileSync(found);
226
- sha256 = crypto.createHash('sha256').update(bytes).digest('hex');
227
- if (found.endsWith('.json')) {
228
- try {
229
- const parsed = JSON.parse(bytes.toString('utf8'));
230
- validJson = true;
231
- const accounts = Array.isArray(parsed.accounts) ? parsed.accounts : [];
232
- hasWalletPassword = typeof parsed.password === 'string' && parsed.password.length > 0;
233
- mobileAccountShape = accounts.some((account) => account?.type === 'mnemonic') &&
234
- accounts.filter((account) => account?.type === 'privateKey').length >= 2;
235
- } catch {
236
- validJson = false;
237
- }
238
- }
239
- }
240
- const status = validJson === false
241
- ? 'STALE_OR_INVALID'
242
- : isWalletFixture && hasWalletPassword
243
- ? 'READY'
244
- : 'PROFILE_HINTS';
245
- console.log(JSON.stringify({
246
- status,
247
- path: rel,
248
- type: isFile ? 'file' : 'directory',
249
- sha256,
250
- modifiedAt: stat.mtime.toISOString(),
251
- profileHints,
252
- hasWalletPassword,
253
- mobileAccountShape,
254
- message: validJson === false
255
- ? `Fixture status: STALE_OR_INVALID (${rel}). Fix before relying on a clean sandbox.`
256
- : status === 'READY'
257
- ? `Fixture status: READY (${rel}).`
258
- : `Fixture status: PROFILE_HINTS (${rel}); no Mobile-shaped wallet fixture was found for automatic account parity validation.`,
259
- }));
260
- NODE
261
- }
262
-
263
- cdp_holder_json() {
264
- local port="$1"
265
- PORT_FOR_STATUS="$port" node <<'NODE'
266
- const cp = require('child_process');
267
- const http = require('http');
268
- const port = process.env.PORT_FOR_STATUS;
269
- function run(cmd) {
270
- try { return cp.execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); }
271
- catch { return ''; }
272
- }
273
- function getJson(path) {
274
- return new Promise((resolve) => {
275
- const req = http.get(`http://127.0.0.1:${port}${path}`, { timeout: 1000 }, (res) => {
276
- let data = '';
277
- res.on('data', (chunk) => { data += chunk; });
278
- res.on('end', () => {
279
- try { resolve(JSON.parse(data)); } catch { resolve(null); }
280
- });
281
- });
282
- req.on('timeout', () => { req.destroy(); resolve(null); });
283
- req.on('error', () => resolve(null));
284
- });
285
- }
286
- (async () => {
287
- const pid = run(`lsof -iTCP:${port} -sTCP:LISTEN -t | head -1`);
288
- // Validate pid is numeric before interpolating it into the `ps -p` shell string.
289
- const safePid = /^[0-9]+$/.test(pid) ? pid : '';
290
- const command = safePid ? run(`ps -p ${safePid} -o command=`) : '';
291
- const version = await getJson('/json/version');
292
- const targets = await getJson('/json/list');
293
- const extensionTargets = Array.isArray(targets)
294
- ? targets.filter((target) => String(target.url || '').startsWith('chrome-extension://')).length
295
- : 0;
296
- console.log(JSON.stringify({
297
- port,
298
- listening: Boolean(pid),
299
- pid: pid || null,
300
- command: command || null,
301
- cdpReachable: Boolean(version),
302
- browser: version?.Browser || null,
303
- targetCount: Array.isArray(targets) ? targets.length : null,
304
- extensionTargets,
305
- }));
306
- })();
307
- NODE
308
- }
309
-
310
- check_file() {
311
- local rel="$1"
312
- if [ -e "$TARGET/$rel" ]; then
313
- checks+=("{\"name\":\"$rel\",\"status\":\"pass\"}")
314
- else
315
- checks+=("{\"name\":\"$rel\",\"status\":\"fail\"}")
316
- status="fail"
317
- fi
318
- }
319
-
320
- check_file "$HARNESS_REL/manifest.json"
321
- check_file "$HARNESS_REL/action-manifest.json"
322
- check_file "$HARNESS_REL/runner/bin/mm-harness"
323
- check_file "$HARNESS_REL/runner/manifests/extension.action-manifest.json"
324
-
325
- # dist-freshness + build-health now come from the runner's single source of
326
- # truth (`runtime-decision`), which subsumes the probes this helper used to
327
- # hand-roll — so the dist-id/source-dirty + webpack-log logic lives in ONE place
328
- # (matching the runner readiness algorithm) and the three layers cannot disagree.
329
- # git/fs only (no --cdp-port) so this stays harness-independent. The derived
330
- # dist-freshness.json / build-health.json keep the same status vocab the case
331
- # mappings + summary below already consume, so behavior is unchanged.
332
- "$RUNNER_BIN" runtime-decision --adapter extension --target "$TARGET" --json \
333
- > "$ARTIFACTS/logs/runtime-decision.json" 2>/dev/null \
334
- || printf '%s' '{}' > "$ARTIFACTS/logs/runtime-decision.json"
335
- # Surface an empty/{} decision loudly: without it, dist-freshness/build-health
336
- # silently degrade to WARN and the runtime-proof gate is effectively bypassed.
337
- if [ ! -s "$ARTIFACTS/logs/runtime-decision.json" ] || [ "$(cat "$ARTIFACTS/logs/runtime-decision.json")" = "{}" ]; then
338
- echo "recipe-harness verify: runtime-decision returned empty/{} — dist-freshness and build-health cannot be derived from the runner and will degrade to WARN (runtime-proof gate NOT fully validated). Check the runner/build state." >&2
339
- fi
340
- node -e '
341
- const fs = require("fs");
342
- const dir = process.argv[process.argv.length - 1];
343
- let r = {};
344
- try { r = JSON.parse(fs.readFileSync(dir + "/runtime-decision.json", "utf8")); } catch {}
345
- const c = r.checks || {};
346
- const dist = c.dist || { status: "unknown" };
347
- const distMsg = dist.source === "release-artifact"
348
- ? ("release artifact " + (dist.manifestVersion || "?") + " sha256=" + (dist.artifactSha256 || "?") + " matches the loaded runtime snapshot.")
349
- : dist.status === "fresh" ? "dist id matches HEAD; no uncommitted source."
350
- : dist.status === "stale" ? (dist.reason === "uncommitted-source"
351
- ? ((dist.modified ? dist.modified.length : "some") + " uncommitted source file(s); rebuild or commit.")
352
- : ("dist id " + (dist.distGitId || "?") + " != HEAD " + (dist.head || "?") + "; rebuild."))
353
- : dist.status === "no-build" ? "no dist/chrome build."
354
- : "no git id in dist or not a git checkout; cannot prove parity.";
355
- fs.writeFileSync(dir + "/dist-freshness.json", JSON.stringify({ ...dist, message: distMsg }));
356
- const bl = c.releaseArtifact?.status === "valid"
357
- ? { status: "no-watch", source: "release-artifact" }
358
- : (c.buildLog || { status: "unknown" });
359
- const blMsg = bl.status === "ok" ? "webpack compiled."
360
- : bl.status === "no-watch" ? "no webpack watch log; build-health n/a (e.g. one-shot build)."
361
- : bl.status === "building" ? "webpack has not reported a successful compile yet."
362
- : bl.status === "errors" ? (bl.reason === "stale-cache"
363
- ? "webpack build failing on a stale cache (ENOENT on a deduped module). Run `mm-harness runtime-launch --adapter extension --target <repo> --cdp-port <port>` to auto-clear the cache and rebuild."
364
- : "webpack build has errors; fix the source/build before validating.")
365
- : "build-health unknown.";
366
- fs.writeFileSync(dir + "/build-health.json", JSON.stringify({ status: bl.status, reason: bl.reason, excerpt: bl.excerpt, message: blMsg }));
367
- ' "$ARTIFACTS/logs" 2>/dev/null \
368
- || { printf '%s' '{"status":"unknown","message":"dist-freshness probe error"}' > "$ARTIFACTS/logs/dist-freshness.json"; printf '%s' '{"status":"unknown","message":"build-health probe error"}' > "$ARTIFACTS/logs/build-health.json"; }
369
- df_read() { node -e 'const fs=require("fs");try{const [file,key]=process.argv.slice(-2);const v=JSON.parse(fs.readFileSync(file,"utf8"));process.stdout.write(String(v[key]??""))}catch{process.stdout.write("")}' "$ARTIFACTS/logs/dist-freshness.json" "$1"; }
370
- df_status="$(df_read status)"
371
- echo "dist-freshness: ${df_status:-unknown} — $(df_read message)" >&2
372
- case "$df_status" in
373
- fresh) checks+=("{\"name\":\"dist-freshness\",\"status\":\"pass\"}") ;;
374
- stale)
375
- # A stale dist only fails a LIVE verify (runtime proof would use the wrong
376
- # build). --static-only checks install/idempotency shape, not runtime, so
377
- # there it is a warning — don't regress install-only verification.
378
- if [ "$STATIC_ONLY" = false ]; then
379
- checks+=("{\"name\":\"dist-freshness\",\"status\":\"fail\",\"detail\":\"see logs/dist-freshness.json\"}"); status="fail"
380
- else
381
- checks+=("{\"name\":\"dist-freshness\",\"status\":\"warn\",\"detail\":\"stale (static-only); see logs/dist-freshness.json\"}")
382
- fi
383
- ;;
384
- *) checks+=("{\"name\":\"dist-freshness\",\"status\":\"warn\",\"detail\":\"${df_status:-unknown}; see logs/dist-freshness.json\"}") ;;
385
- esac
386
-
387
- # build-health.json was produced by the runtime-decision derivation above (the
388
- # runner reads the webpack watch log; same status vocab as before).
389
- bh_read() { node -e 'const fs=require("fs");try{const [file,key]=process.argv.slice(-2);process.stdout.write(String(JSON.parse(fs.readFileSync(file,"utf8"))[key]??""))}catch{process.stdout.write("")}' "$ARTIFACTS/logs/build-health.json" "$1"; }
390
- bh_status="$(bh_read status)"
391
- echo "build-health: ${bh_status:-unknown} — $(bh_read message)" >&2
392
- case "$bh_status" in
393
- ok|no-watch) checks+=("{\"name\":\"build-health\",\"status\":\"pass\"}") ;;
394
- errors)
395
- # A broken build only fails a LIVE verify; in --static-only (install-shape only)
396
- # it is a loud warning so install/idempotency checks aren't regressed.
397
- if [ "$STATIC_ONLY" = false ]; then
398
- checks+=("{\"name\":\"build-health\",\"status\":\"fail\",\"detail\":\"see logs/build-health.json\"}"); status="fail"
399
- else
400
- checks+=("{\"name\":\"build-health\",\"status\":\"warn\",\"detail\":\"build errors (static-only); see logs/build-health.json\"}")
401
- fi
402
- ;;
403
- building) checks+=("{\"name\":\"build-health\",\"status\":\"warn\",\"detail\":\"still compiling; see logs/build-health.json\"}") ;;
404
- *) checks+=("{\"name\":\"build-health\",\"status\":\"warn\",\"detail\":\"${bh_status:-unknown}; see logs/build-health.json\"}") ;;
405
- esac
406
-
407
- live_mode="static-only"
408
- if [ "$STATIC_ONLY" = false ]; then
409
- fixture_status_json > "$ARTIFACTS/logs/fixture-status.json"
410
- node -e 'const fs=require("fs"); const file=process.argv[process.argv.length - 1]; const v=JSON.parse(fs.readFileSync(file,"utf8")); console.error(v.message || v.status);' "$ARTIFACTS/logs/fixture-status.json"
411
- checks+=("{\"name\":\"fixture/profile status\",\"status\":\"$(node -e 'const fs=require("fs"); const file=process.argv[process.argv.length - 1]; const v=JSON.parse(fs.readFileSync(file,"utf8")); console.log(v.status === "READY" ? "pass" : "warn");' "$ARTIFACTS/logs/fixture-status.json")\"}")
412
-
413
- if [ -z "$CDP_PORT" ]; then
414
- echo "Live extension verify could not infer a checkout CDP port. Run from a prepared slot checkout or pass --cdp-port." > "$ARTIFACTS/logs/live-missing-cdp.log"
415
- checks+=("{\"name\":\"live runtime CDP port\",\"status\":\"fail\",\"detail\":\"missing CDP port (checkout-scoped inference failed)\"}")
416
- status="fail"
417
- live_mode="missing-cdp"
418
- else
419
- live_mode="live"
420
- cdp_holder_json "$CDP_PORT" > "$ARTIFACTS/logs/cdp-holder.json"
421
- if "$RUNNER_BIN" ensure-ready --adapter extension --target "$TARGET" --cdp-port "$CDP_PORT" --json > "$ARTIFACTS/logs/extension-ensure-ready.json" 2>&1; then
422
- checks+=("{\"name\":\"home-tab convergence\",\"status\":\"pass\",\"detail\":\"see logs/extension-ensure-ready.json\"}")
423
- refresh_extension_id
424
- else
425
- checks+=("{\"name\":\"home-tab convergence\",\"status\":\"fail\",\"detail\":\"see logs/extension-ensure-ready.json\"}")
426
- node - "$ARTIFACTS/logs/extension-ensure-ready.json" <<'NODE' >&2
427
- const fs = require('fs');
428
- const file = process.argv[2];
429
- try {
430
- const report = JSON.parse(fs.readFileSync(file, 'utf8'));
431
- const code = report.health?.errorCode;
432
- const findings = Array.isArray(report.health?.findings) ? report.health.findings : [];
433
- if (code) {
434
- const finding = findings[0] ?? 'Extension UI is not interactive.';
435
- console.error(finding.startsWith(`${code}:`) ? finding : `${code}: ${finding}`);
436
- }
437
- else if (findings[0]) console.error(findings[0]);
438
- if (report.health?.userAction) console.error(`Next: ${report.health.userAction}`);
439
- } catch {
440
- // The summary retains the raw helper output for diagnosis.
441
- }
442
- NODE
443
- status="fail"
444
- fi
445
-
446
- if node "$READINESS_MJS" --target "$TARGET" --cdp-port "$CDP_PORT" --json > "$ARTIFACTS/logs/extension-readiness.json" 2>&1; then
447
- checks+=("{\"name\":\"live extension readiness\",\"status\":\"pass\"}")
448
- # extension-readiness.mjs may repair $RUNTIME_DIR/extension.id when the
449
- # supplied Chrome profile loads a fresh extension ID. Reload it before
450
- # running recipe smoke checks so the recipe bridge targets the live
451
- # extension instead of a stale marker from a previous browser profile.
452
- refresh_extension_id
453
- else
454
- checks+=("{\"name\":\"live extension readiness\",\"status\":\"fail\",\"detail\":\"see logs/extension-readiness.json\"}")
455
- status="fail"
456
- fi
457
-
458
- if (
459
- cd "$TARGET"
460
- "$RUNNER_BIN" actions --raw --adapter extension
461
- ) > "$ARTIFACTS/logs/runner-manifest.json" 2> "$ARTIFACTS/logs/runner-manifest.err"; then
462
- checks+=("{\"name\":\"runner manifest\",\"status\":\"pass\"}")
463
- else
464
- checks+=("{\"name\":\"runner manifest\",\"status\":\"fail\",\"detail\":\"see logs/runner-manifest.err\"}")
465
- status="fail"
466
- fi
467
-
468
- if (
469
- cd "$TARGET"
470
- "$RUNNER_BIN" run "$SMOKE_RECIPE" --adapter extension --target "$TARGET" --cdp-port "$CDP_PORT" --artifacts-dir "$ARTIFACTS/runner-smoke" --json
471
- ) > "$ARTIFACTS/logs/runner-smoke.log" 2>&1; then
472
- checks+=("{\"name\":\"runner v1 smoke\",\"status\":\"pass\"}")
473
- else
474
- checks+=("{\"name\":\"runner v1 smoke\",\"status\":\"fail\",\"detail\":\"see logs/runner-smoke.log\"}")
475
- status="fail"
476
- fi
477
- fi
478
- fi
479
-
480
- if git -C "$TARGET" rev-parse --git-dir >/dev/null 2>&1; then
481
- git -C "$TARGET" status --short -- . ":(exclude)$HARNESS_ROOT" > "$ARTIFACTS/logs/product-diff-excluding-harness.log" 2>&1 || true
482
- fi
483
-
484
- RECIPE_HARNESS_LIVE_MODE="$live_mode" RECIPE_HARNESS_ROOT_EXCLUDE="$HARNESS_ROOT" RECIPE_RUNTIME_DIR="$RUNTIME_DIR" node - "$ARTIFACTS" "$TARGET" "$status" "${checks[@]}" <<'NODE'
485
- const fs = require('fs');
486
- const path = require('path');
487
- const cp = require('child_process');
488
- const [artifacts, target, status, ...checks] = process.argv.slice(2);
489
- const parsedChecks = checks.map((entry) => JSON.parse(entry));
490
- const liveMode = process.env.RECIPE_HARNESS_LIVE_MODE || 'unknown';
491
- let fixtureStatus = null;
492
- let cdpHolder = null;
493
- let readinessReport = null;
494
- let distFreshness = null;
495
- try { distFreshness = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/dist-freshness.json'), 'utf8')); } catch {}
496
- try { fixtureStatus = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/fixture-status.json'), 'utf8')); } catch {}
497
- try { cdpHolder = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/cdp-holder.json'), 'utf8')); } catch {}
498
- try { readinessReport = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/extension-readiness.json'), 'utf8')); } catch {}
499
- let ensureReadyReport = null;
500
- try { ensureReadyReport = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/extension-ensure-ready.json'), 'utf8')); } catch {}
501
- function runGit(args) {
502
- try {
503
- return cp.execFileSync('git', ['-C', target, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
504
- } catch (error) {
505
- // Git metadata is diagnostic-only; non-git targets still produce a usable verify summary.
506
- return null;
507
- }
508
- }
509
- const harnessRootExclude = process.env.RECIPE_HARNESS_ROOT_EXCLUDE;
510
- if (!harnessRootExclude) throw new Error('RECIPE_HARNESS_ROOT_EXCLUDE is required');
511
- const statusShort = runGit(['status', '--short', '--', '.', `:(exclude)${harnessRootExclude}`]);
512
- const gitStatus = {
513
- branch: runGit(['branch', '--show-current']),
514
- head: runGit(['rev-parse', '--short', 'HEAD']),
515
- dirtyCount: statusShort ? statusShort.split('\n').filter(Boolean).length : 0,
516
- dirtyPreview: statusShort ? statusShort.split('\n').filter(Boolean).slice(0, 25) : [],
517
- };
518
- const readiness = parsedChecks.find((check) => check.name === 'live extension readiness');
519
- const extensionIdPath = path.join(target, process.env.RECIPE_RUNTIME_DIR, 'extension.id');
520
- let markerExtensionId = null;
521
- try {
522
- const value = fs.readFileSync(extensionIdPath, 'utf8').trim();
523
- if (/^[a-p]{32}$/.test(value)) markerExtensionId = value;
524
- } catch {}
525
- const cdpTarget = readinessReport?.cdp ? {
526
- selectedExtensionId: readinessReport.cdp.selectedExtensionId || null,
527
- markerExtensionId,
528
- markerMatched: readinessReport.cdp.markerMatched ?? null,
529
- markerRepaired: readinessReport.cdp.markerRepaired ?? null,
530
- extensionIds: readinessReport.cdp.extensionIds || [],
531
- targetCount: readinessReport.cdp.targetCount ?? null,
532
- browser: readinessReport.cdp.browser || cdpHolder?.browser || null,
533
- } : null;
534
- const runtimeOwner = liveMode === 'static-only'
535
- ? 'static-only'
536
- : liveMode === 'missing-cdp'
537
- ? 'none'
538
- : cdpHolder?.cdpReachable
539
- ? (readiness?.status === 'pass' ? 'compatible-external-or-harness' : 'incompatible-external-or-stale')
540
- : 'none';
541
- fs.writeFileSync(path.join(artifacts, 'summary.json'), `${JSON.stringify({
542
- adapter: 'extension',
543
- status,
544
- liveMode,
545
- runtimeClassification: {
546
- runtimeOwner,
547
- recipeControllable: readiness?.status === 'pass',
548
- startedByVerify: false,
549
- },
550
- cleanupOwnership: {
551
- mayStop: false,
552
- reason: 'extension verify inspects the supplied CDP runtime; wrapper/preflight ownership must be recorded by the caller before stopping processes',
553
- },
554
- gitStatus,
555
- distFreshness,
556
- runtimePolicy: {
557
- runtimeReusePolicy: 'reuse a running harness-compatible CDP target when possible; wrapper auto-start must use a cached/watch-only prepare path unless the human explicitly permits a rebuild',
558
- },
559
- fixtureStatus,
560
- cdpHolder,
561
- ensureReady: ensureReadyReport,
562
- cdpTarget,
563
- checks: parsedChecks,
564
- generatedAt: new Date().toISOString(),
565
- }, null, 2)}\n`);
566
- fs.writeFileSync(path.join(artifacts, 'artifact-manifest.json'), `${JSON.stringify({
567
- artifacts: fs.readdirSync(artifacts).map((name) => ({ path: name })),
568
- }, null, 2)}\n`);
569
- NODE
570
-
571
- echo "Extension harness verify $status: $ARTIFACTS/summary.json"
572
- [ "$status" = "pass" ]