@deeeed/metamask-harness 0.57.0 → 0.58.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,557 +0,0 @@
1
- #!/usr/bin/env bash
2
- # verify.sh — mobile slot verification (readiness/proof layer)
3
- #
4
- # Purpose:
5
- # Recipe-tree verification for ONE mobile instance: static checks plus
6
- # runtime/bridge verification, with the auto-start gate deciding whether
7
- # verify may start the app or only attach.
8
- #
9
- # Inputs (flags / env):
10
- # --target <metamask-mobile> (default $PWD), --platform ios|android,
11
- # --preflight-mode fast|auto|default|rebuild-native|clean,
12
- # --auto-start/--no-auto-start (env RECIPE_HARNESS_MOBILE_AUTO_START,
13
- # default false), --static-only, --artifacts-dir <dir>
14
- # env RECIPE_HARNESS_ROOT, RECIPE_RUNTIME_DIR, RECIPE_HARNESS_PLATFORM
15
- #
16
- # Outputs:
17
- # <artifacts>/summary.json + logs/.
18
- # Exit 0 — verify pass; 1 — checks failed; 2 — bad args.
19
- #
20
- # Never touches: product source files; app startup unless auto-start is
21
- # enabled (launch owns startup in live runs).
22
- set -euo pipefail
23
-
24
- TARGET="$PWD"
25
- ARTIFACTS=""
26
- STATIC_ONLY=false
27
- AUTO_START="${RECIPE_HARNESS_MOBILE_AUTO_START:-false}"
28
- PLATFORM="${RECIPE_HARNESS_PLATFORM:-${PLATFORM:-ios}}"
29
- PREFLIGHT_MODE="${RECIPE_HARNESS_MOBILE_PREFLIGHT_MODE:-fast}"
30
- require_value() { [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; }
31
- while [ "$#" -gt 0 ]; do
32
- case "$1" in
33
- --target) require_value "$@"; TARGET="$2"; shift 2 ;;
34
- --artifacts-dir) require_value "$@"; ARTIFACTS="$2"; shift 2 ;;
35
- --static-only) STATIC_ONLY=true; shift ;;
36
- --platform) require_value "$@"; PLATFORM="$2"; shift 2 ;;
37
- --preflight-mode) require_value "$@"; PREFLIGHT_MODE="$2"; shift 2 ;;
38
- --auto-start) AUTO_START=true; shift ;;
39
- --no-auto-start) AUTO_START=false; shift ;;
40
- -h|--help) echo "Usage: verify.sh [--target <metamask-mobile>] [--artifacts-dir <dir>] [--static-only] [--platform ios|android] [--preflight-mode fast|auto|default|rebuild-native|clean] [--no-auto-start]"; exit 0 ;;
41
- *) echo "Unknown arg: $1" >&2; exit 2 ;;
42
- esac
43
- done
44
- case "$PREFLIGHT_MODE" in
45
- fast|auto|default|rebuild-native|clean) ;;
46
- *) echo "Unknown --preflight-mode: $PREFLIGHT_MODE" >&2; exit 2 ;;
47
- esac
48
- case "$PLATFORM" in
49
- ios|android) ;;
50
- *) echo "Unknown --platform: $PLATFORM (expected ios or android)" >&2; exit 2 ;;
51
- esac
52
-
53
- case "$AUTO_START" in
54
- 1|true|TRUE|True|yes|YES|Yes|on|ON|On) AUTO_START=true ;;
55
- 0|false|FALSE|False|no|NO|No|off|OFF|Off|"") AUTO_START=false ;;
56
- *) echo "Unknown RECIPE_HARNESS_MOBILE_AUTO_START value: $AUTO_START" >&2; exit 2 ;;
57
- esac
58
-
59
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
60
- # shellcheck disable=SC1091
61
- for _hp in "$SCRIPT_DIR/lib/harness-path.sh" "$SCRIPT_DIR/../../adapters/shared/harness-path.sh" "$SCRIPT_DIR/../shared/harness-path.sh"; do
62
- [ -f "$_hp" ] && { . "$_hp"; break; }
63
- done
64
- unset _hp
65
- if ! command -v harness_root >/dev/null 2>&1; then
66
- echo "mobile verify: shared lib adapters/shared/harness-path.sh not found; reinstall the runner." >&2
67
- exit 1
68
- fi
69
- TARGET="$(cd "$TARGET" && pwd)"
70
- RUNTIME_DIR="$(recipe_runtime_dir)"
71
- HARNESS_ROOT="$(harness_root)"
72
- HARNESS_REL="$HARNESS_ROOT/mobile"
73
- HARNESS_DIR="$(harness_dir "$TARGET" mobile)"
74
- RUNNER_BIN="$HARNESS_DIR/runner/bin/mm-harness"
75
- ARTIFACTS="${ARTIFACTS:-$HARNESS_DIR/verify/$(date -u +%Y%m%dT%H%M%SZ)}"
76
- mkdir -p "$ARTIFACTS/logs"
77
-
78
- status="pass"
79
- checks=()
80
-
81
- add_note() {
82
- printf '%s\n' "$1" >> "$ARTIFACTS/logs/runtime-notes.txt"
83
- }
84
-
85
- fixture_status_json() {
86
- TARGET_FOR_FIXTURE="$TARGET" RECIPE_RUNTIME_DIR="$RUNTIME_DIR" node <<'NODE'
87
- const fs = require('fs');
88
- const crypto = require('crypto');
89
- const path = require('path');
90
- const target = process.env.TARGET_FOR_FIXTURE;
91
- const runtimeDir = process.env.RECIPE_RUNTIME_DIR;
92
- if (!runtimeDir) throw new Error('RECIPE_RUNTIME_DIR is required');
93
- const candidates = [
94
- runtimeDir + '/wallet-fixture.json',
95
- ].map((rel) => path.join(target, rel));
96
- const fixture = candidates.find((file) => fs.existsSync(file));
97
- if (!fixture) {
98
- console.log(JSON.stringify({
99
- status: 'MISSING_FIXTURES',
100
- message: `No wallet fixture found. This run may spend time repairing wallet/perps state manually. For a clean isolated sandbox, create ${runtimeDir}/wallet-fixture.json.`,
101
- setupCommand: `mkdir -p ${runtimeDir} && edit ${runtimeDir}/wallet-fixture.json with password + accounts`,
102
- }));
103
- process.exit(0);
104
- }
105
- const fixtureRaw = fs.readFileSync(fixture);
106
- let parsed = null;
107
- let valid = false;
108
- let accountCount = null;
109
- let hasPassword = false;
110
- try {
111
- parsed = JSON.parse(fixtureRaw.toString('utf8'));
112
- valid = true;
113
- accountCount = Array.isArray(parsed.accounts) ? parsed.accounts.length : 0;
114
- hasPassword = typeof parsed.password === 'string' && parsed.password.length > 0;
115
- } catch {
116
- valid = false;
117
- }
118
- const stat = fs.statSync(fixture);
119
- console.log(JSON.stringify({
120
- status: valid && hasPassword && accountCount > 0 ? 'READY' : 'STALE_OR_INVALID',
121
- path: path.relative(target, fixture),
122
- sha256: crypto.createHash('sha256').update(fixtureRaw).digest('hex'),
123
- modifiedAt: stat.mtime.toISOString(),
124
- accountCount,
125
- hasPassword,
126
- message: valid && hasPassword && accountCount > 0
127
- ? `Fixture status: READY (${path.relative(target, fixture)}, accounts=${accountCount}).`
128
- : `Fixture status: STALE_OR_INVALID (${path.relative(target, fixture)}). Validate password/accounts before relying on a clean sandbox.`,
129
- }));
130
- NODE
131
- }
132
-
133
- port_holder_json() {
134
- local port="$1"
135
- PORT_FOR_STATUS="$port" node <<'NODE'
136
- const cp = require('child_process');
137
- const port = process.env.PORT_FOR_STATUS;
138
- function run(cmd) {
139
- try { return cp.execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); }
140
- catch { return ''; }
141
- }
142
- const pid = run(`lsof -iTCP:${port} -sTCP:LISTEN -t | head -1`);
143
- let command = '';
144
- // Validate pid is numeric before interpolating it into the `ps -p` shell string.
145
- if (/^[0-9]+$/.test(pid)) command = run(`ps -p ${pid} -o command=`);
146
- console.log(JSON.stringify({
147
- port,
148
- listening: Boolean(pid),
149
- pid: pid || null,
150
- command: command || null,
151
- metroStatusReachable: null,
152
- metroHttpProbeSkipped: true,
153
- note: 'HTTP /status probing is skipped during live verify because the React Native bridge is the authoritative readiness path for Mobile.',
154
- }));
155
- NODE
156
- }
157
-
158
- fixture_check_json() {
159
- local fixture_status_path="$1"
160
- node - "$fixture_status_path" <<'NODE'
161
- const fs = require('fs');
162
- const v = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
163
- console.log(JSON.stringify({
164
- name: 'fixture status',
165
- status: v.status === 'READY' ? 'pass' : 'warn',
166
- detail: v.path || v.status || '',
167
- message: v.message || v.status,
168
- }));
169
- NODE
170
- }
171
-
172
- watcher_port() {
173
- TARGET_FOR_WATCHER_PORT="$TARGET" node <<'NODE'
174
- const fs = require('fs');
175
- const path = require('path');
176
- const target = process.env.TARGET_FOR_WATCHER_PORT;
177
- // Precedence: an explicit WATCHER_PORT in the process env (which carries the
178
- // --watcher-port flag applied by launch) wins over the slot's .js.env, which wins
179
- // over the 8081 default. This lets a run be pointed at an alternate Metro without
180
- // editing a shared slot .js.env.
181
- let port = process.env.WATCHER_PORT || '';
182
- if (!port) {
183
- for (const file of ['.js.env', '.env', '.env.local']) {
184
- const full = path.join(target, file);
185
- if (!fs.existsSync(full)) continue;
186
- const text = fs.readFileSync(full, 'utf8');
187
- const match = text.match(/^\s*(?:export\s+)?WATCHER_PORT=(["']?)([0-9]+)\1/m);
188
- if (match) { port = match[2]; break; }
189
- }
190
- }
191
- if (!port) port = '8081';
192
- console.log(port);
193
- NODE
194
- }
195
-
196
- # Resolve a runtime env var (e.g. IOS_SIMULATOR, ADB_SERIAL) from the process
197
- # env or the target repo's .js.env so the runner can bind device-scoped proof
198
- # (screenshots) to the same device as the bridge commands.
199
- jsenv_value() {
200
- TARGET_FOR_JSENV="$TARGET" JSENV_NAME="$1" node <<'NODE'
201
- const fs = require('fs');
202
- const path = require('path');
203
- const target = process.env.TARGET_FOR_JSENV;
204
- const name = process.env.JSENV_NAME;
205
- let value = process.env[name] || '';
206
- if (!value) {
207
- const re = new RegExp("^\\s*(?:export\\s+)?" + name + "=([\"']?)([^\"'\\n]+)\\1", "m");
208
- for (const file of ['.js.env', '.env', '.env.local']) {
209
- const full = path.join(target, file);
210
- if (!fs.existsSync(full)) continue;
211
- const match = fs.readFileSync(full, 'utf8').match(re);
212
- if (match) { value = match[2]; break; }
213
- }
214
- }
215
- process.stdout.write(value);
216
- NODE
217
- }
218
-
219
- runtime_context_value() {
220
- TARGET_FOR_RUNTIME_CONTEXT="$TARGET" RUNTIME_DIR_FOR_RUNTIME_CONTEXT="$RUNTIME_DIR" RUNTIME_CONTEXT_NAME="$1" node <<'NODE'
221
- const fs = require('fs');
222
- const path = require('path');
223
- const target = process.env.TARGET_FOR_RUNTIME_CONTEXT;
224
- const runtimeDir = process.env.RUNTIME_DIR_FOR_RUNTIME_CONTEXT;
225
- const names = process.env.RUNTIME_CONTEXT_NAME.split(',');
226
- const file = path.join(target, runtimeDir, 'agentic-runtime.json');
227
- let data = {};
228
- try { data = JSON.parse(fs.readFileSync(file, 'utf8')); } catch {}
229
- for (const name of names) {
230
- const value = data && data[name];
231
- if (value !== undefined && value !== null && value !== '') {
232
- process.stdout.write(String(value));
233
- process.exit(0);
234
- }
235
- }
236
- NODE
237
- }
238
-
239
- mobile_device_target() {
240
- if [ "$PLATFORM" = "android" ]; then
241
- printf '%s' "${ADB_SERIAL:-${ANDROID_SERIAL:-${ANDROID_DEVICE:-$(runtime_context_value adbSerial,androidDevice,device)}}}"
242
- return
243
- fi
244
- printf '%s' "${SIM_UDID:-${IOS_SIMULATOR:-$(runtime_context_value simulator,iosSimulator,device)}}"
245
- }
246
-
247
- check_file() {
248
- local rel="$1"
249
- if [ -e "$TARGET/$rel" ]; then
250
- checks+=("{\"name\":\"$rel\",\"status\":\"pass\"}")
251
- else
252
- checks+=("{\"name\":\"$rel\",\"status\":\"fail\"}")
253
- status="fail"
254
- fi
255
- }
256
-
257
- check_file "$HARNESS_REL/manifest.json"
258
- check_file "$HARNESS_REL/action-manifest.json"
259
- check_file "$HARNESS_REL/runner/bin/mm-harness"
260
- check_file "package.json"
261
- check_file "app/dev-tools/AgenticService/AgenticService.ts"
262
-
263
- if ! grep -q "AgenticService.install" "$TARGET/app/core/NavigationService/NavigationService.ts" 2>/dev/null; then
264
- checks+=("{\"name\":\"NavigationService agentic bridge\",\"status\":\"fail\"}")
265
- status="fail"
266
- else
267
- checks+=("{\"name\":\"NavigationService agentic bridge\",\"status\":\"pass\"}")
268
- fi
269
-
270
- if ! grep -q "AgentStepHud" "$TARGET/app/components/Nav/App/App.tsx" 2>/dev/null; then
271
- checks+=("{\"name\":\"App AgentStepHud\",\"status\":\"fail\"}")
272
- status="fail"
273
- else
274
- checks+=("{\"name\":\"App AgentStepHud\",\"status\":\"pass\"}")
275
- fi
276
-
277
- run_with_timeout() {
278
- local log_path="$1"
279
- local timeout_s="$2"
280
- shift 2
281
- [[ "$timeout_s" =~ ^[0-9]+$ ]] || {
282
- echo "Invalid timeout seconds: $timeout_s" >&2
283
- return 2
284
- }
285
- "$@" > "$log_path" 2>&1 &
286
- local pid=$!
287
- local waited=0
288
- while kill -0 "$pid" 2>/dev/null; do
289
- if [ "$waited" -ge "$timeout_s" ]; then
290
- echo "Timed out after ${timeout_s}s: $*" >> "$log_path"
291
- kill "$pid" 2>/dev/null || true
292
- sleep 1
293
- kill -9 "$pid" 2>/dev/null || true
294
- wait "$pid" 2>/dev/null || true
295
- return 124
296
- fi
297
- sleep 1
298
- waited=$((waited + 1))
299
- done
300
- wait "$pid"
301
- }
302
-
303
- live_status_ok() {
304
- local log_path="$1"
305
- local device_target="$2"
306
- cat > "$ARTIFACTS/mobile-status-smoke.recipe.json" <<'JSON'
307
- {
308
- "$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
309
- "title": "Mobile runner status smoke",
310
- "description": "Checks that the installed runner can read Mobile app status before live verification.",
311
- "workflow": {
312
- "entry": "status",
313
- "nodes": {
314
- "status": { "action": "app.status", "intent": "Read Mobile app status through the runner-owned bridge", "next": "done" },
315
- "done": { "action": "end", "status": "pass" }
316
- }
317
- }
318
- }
319
- JSON
320
- (
321
- cd "$TARGET"
322
- device_args=()
323
- [ -n "$device_target" ] && device_args=(--device "$device_target")
324
- "$RUNNER_BIN" run "$ARTIFACTS/mobile-status-smoke.recipe.json" --adapter mobile --target "$TARGET" "${device_args[@]}" --watcher-port "$port" --artifacts-dir "$ARTIFACTS/runner-status-smoke" --json
325
- ) > "$log_path" 2>&1
326
- }
327
-
328
- evm_rpc_ready() {
329
- local log_path="$1"
330
- local bridge="$HARNESS_DIR/bridge-runtime/cdp-bridge.cjs"
331
- local expression
332
- expression="(async function(){try{var engine=globalThis.Engine;var controller=engine&&engine.context&&engine.context.NetworkController;if(!controller||typeof controller.getSelectedNetworkClient!=='function')throw new Error('NetworkController selected client is unavailable');var client=controller.getSelectedNetworkClient();if(!client||!client.provider||typeof client.provider.request!=='function')throw new Error('selected EVM provider is unavailable');var result=await client.provider.request({method:'eth_blockNumber'});if(typeof result!=='string'||!/^0x[0-9a-f]+$/i.test(result))throw new Error('eth_blockNumber returned an invalid result');return {ok:true,result:result};}catch(error){return {ok:false,error:error&&(error.message||String(error))};}})()"
333
- [ -f "$bridge" ] || {
334
- printf 'Mobile bridge is missing at %s\n' "$bridge" > "$log_path"
335
- return 1
336
- }
337
- if ! run_with_timeout "$log_path" 25 env \
338
- APP_ROOT="$TARGET" \
339
- WATCHER_PORT="$port" \
340
- IOS_SIMULATOR="${IOS_SIMULATOR:-$ios_simulator_resolved}" \
341
- ADB_SERIAL="${ADB_SERIAL:-$adb_serial_resolved}" \
342
- ANDROID_SERIAL="${ANDROID_SERIAL:-$adb_serial_resolved}" \
343
- node "$bridge" eval-async "$expression"; then
344
- return 1
345
- fi
346
- node - "$log_path" <<'NODE'
347
- const fs = require('fs');
348
- try {
349
- const result = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
350
- process.exit(result.ok === true ? 0 : 1);
351
- } catch {
352
- process.exit(1);
353
- }
354
- NODE
355
- }
356
-
357
- ensure_live_runtime() {
358
- local device_target="$1"
359
- local attempt
360
- for attempt in 1 2 3 4 5; do
361
- if live_status_ok "$ARTIFACTS/logs/app-status-precheck.log" "$device_target"; then
362
- return 0
363
- fi
364
- # A freshly booted simulator can expose its Metro bridge just before
365
- # `simctl list devices booted` publishes the device. Bound that launch→verify
366
- # race instead of reporting a false DEVICE_NOT_FOUND immediately.
367
- if [ "$attempt" -lt 5 ] && grep -q '"code"[[:space:]]*:[[:space:]]*"DEVICE_NOT_FOUND"' "$ARTIFACTS/logs/app-status-precheck.log"; then
368
- echo "Waiting for Mobile device discovery (${attempt}/5): ${device_target:-configured target}" >&2
369
- sleep 1
370
- continue
371
- fi
372
- break
373
- done
374
- if [ "$AUTO_START" = true ]; then
375
- cat >&2 <<'EOF'
376
- Mobile auto-start is not allowed from product-local scripts. Start or prepare the app through the runner/slot runtime, then rerun verify with --no-auto-start.
377
- EOF
378
- fi
379
- return 1
380
- }
381
-
382
- if [ "$STATIC_ONLY" = false ]; then
383
- port="$(watcher_port)"
384
- # Reject a non-numeric port before it reaches runner arguments or the
385
- # port-holder shell probe.
386
- case "$port" in
387
- ""|*[!0-9]*) echo "Refusing mobile verify: resolved watcher port is not numeric: '$port' (check WATCHER_PORT)" >&2; exit 2 ;;
388
- esac
389
-
390
- device_target="$(mobile_device_target)"
391
- if [ -z "$device_target" ]; then
392
- if [ "$PLATFORM" = "android" ]; then
393
- device_target="$(jsenv_value ADB_SERIAL)"
394
- [ -n "$device_target" ] || device_target="$(jsenv_value ANDROID_DEVICE)"
395
- else
396
- device_target="$(jsenv_value IOS_SIMULATOR)"
397
- fi
398
- fi
399
-
400
- if ensure_live_runtime "$device_target"; then
401
- checks+=('{"name":"mobile runtime controllable precheck","status":"pass"}')
402
- else
403
- checks+=('{"name":"mobile runtime controllable precheck","status":"fail","detail":"see logs/app-status-precheck.log"}')
404
- status="fail"
405
- fi
406
-
407
- fixture_json="$(fixture_status_json)"
408
- printf '%s\n' "$fixture_json" > "$ARTIFACTS/logs/fixture-status.json"
409
- fixture_check_json="$(fixture_check_json "$ARTIFACTS/logs/fixture-status.json")"
410
- fixture_message="$(node -e 'const v=JSON.parse(process.argv[1]); console.log(v.message || v.detail);' "$fixture_check_json")"
411
- echo "$fixture_message" >&2
412
- add_note "$fixture_message"
413
- checks+=("$fixture_check_json")
414
-
415
- port_holder_json "$port" > "$ARTIFACTS/logs/port-holder.json"
416
-
417
- # Right after a launch or a fixture unlock the app's JS thread can stay busy
418
- # for a while (account hydration); a single CDP probe then times out although
419
- # the runtime is healthy. When Metro holds the port, let wait-for-bridge
420
- # absorb that settle before the smoke. Best effort and bounded: the smoke
421
- # below is the check that counts. The installed overlay carries only this
422
- # script, so the waiter is resolved from the runner source it was installed
423
- # from.
424
- if [ "$status" != "fail" ] && node -e 'process.exit(JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")).listening ? 0 : 1)' "$ARTIFACTS/logs/port-holder.json" 2>/dev/null; then
425
- wait_sh=""
426
- for candidate in "$SCRIPT_DIR/wait-for-bridge.sh" "$(cat "$HARNESS_DIR/runner/.runner-source" 2>/dev/null || true)/adapters/mobile/wait-for-bridge.sh"; do
427
- [ -f "$candidate" ] && { wait_sh="$candidate"; break; }
428
- done
429
- if [ -z "$wait_sh" ]; then
430
- add_note "Mobile bridge settle skipped: wait-for-bridge.sh not found beside verify.sh or under runner/.runner-source; reinstall the runner."
431
- elif MOBILE_BRIDGE_READY_TIMEOUT_MS="${MOBILE_BRIDGE_READY_TIMEOUT_MS:-90000}" \
432
- MOBILE_BRIDGE_UNRESPONSIVE_GRACE_MS="${MOBILE_BRIDGE_UNRESPONSIVE_GRACE_MS:-90000}" \
433
- bash "$wait_sh" --target "$TARGET" --port "$port" --platform "$PLATFORM" \
434
- > "$ARTIFACTS/logs/wait-for-bridge.log" 2>&1; then
435
- add_note "Mobile bridge settled before the live smoke (logs/wait-for-bridge.log)."
436
- else
437
- add_note "Mobile bridge settle did not complete (waiter exit $?); running the live smoke anyway (logs/wait-for-bridge.log)."
438
- fi
439
- fi
440
-
441
- cat > "$ARTIFACTS/mobile-v1-live-smoke.recipe.json" <<'JSON'
442
- {
443
- "$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
444
- "title": "Mobile v1 runner live bridge smoke",
445
- "description": "Verifies the installed MetaMask runner can read the React Native debug bridge without mutating wallet state.",
446
- "workflow": {
447
- "entry": "status",
448
- "nodes": {
449
- "status": { "action": "app.status", "intent": "Read Mobile app status through the v1 runner", "next": "cdp-probe" },
450
- "cdp-probe": { "action": "cdp.target", "intent": "Verify the React Native debug bridge target is reachable", "require_reachable": true, "timeout_ms": 60000, "cdp_timeout_ms": 60000, "next": "done" },
451
- "done": { "action": "end", "status": "pass" }
452
- }
453
- }
454
- }
455
- JSON
456
-
457
- ios_simulator_resolved="$(jsenv_value IOS_SIMULATOR)"
458
- adb_serial_resolved="$(jsenv_value ADB_SERIAL)"
459
- device_args=()
460
- [ -n "$device_target" ] && device_args=(--device "$device_target")
461
- if (
462
- cd "$TARGET"
463
- METAMASK_RECIPE_AUTO_HUD=0 \
464
- IOS_SIMULATOR="${IOS_SIMULATOR:-$ios_simulator_resolved}" \
465
- ADB_SERIAL="${ADB_SERIAL:-$adb_serial_resolved}" \
466
- ANDROID_SERIAL="${ANDROID_SERIAL:-$adb_serial_resolved}" \
467
- "$RUNNER_BIN" run "$ARTIFACTS/mobile-v1-live-smoke.recipe.json" --adapter mobile --target "$TARGET" "${device_args[@]}" --watcher-port "$port" --artifacts-dir "$ARTIFACTS/runner-live-smoke" --json
468
- ) > "$ARTIFACTS/logs/runner-live-smoke.log" 2>&1; then
469
- checks+=("{\"name\":\"runner v1 live bridge smoke\",\"status\":\"pass\"}")
470
- else
471
- checks+=("{\"name\":\"runner v1 live bridge smoke\",\"status\":\"fail\",\"detail\":\"see logs/runner-live-smoke.log\"}")
472
- add_note "Runner v1 live bridge smoke failed; inspect logs/runner-live-smoke.log and runner-live-smoke/trace.json."
473
- status="fail"
474
- fi
475
-
476
- if evm_rpc_ready "$ARTIFACTS/logs/evm-rpc-readiness.log"; then
477
- checks+=("{\"name\":\"live EVM RPC readiness\",\"status\":\"pass\"}")
478
- else
479
- checks+=("{\"name\":\"live EVM RPC readiness\",\"status\":\"fail\",\"detail\":\"see logs/evm-rpc-readiness.log\"}")
480
- add_note "EVM RPC readiness probe failed. Infura configured ≠ RPC reachable. Next: confirm $TARGET/.js.env has a real MM_INFURA_PROJECT_ID (not 00000000000), then rerun mm-harness launch $PLATFORM --verify --target '$TARGET'."
481
- status="fail"
482
- fi
483
- fi
484
-
485
- RECIPE_HARNESS_PREFLIGHT_MODE="$PREFLIGHT_MODE" RECIPE_HARNESS_ROOT_EXCLUDE="$HARNESS_ROOT" node - "$ARTIFACTS" "$TARGET" "$status" "${checks[@]}" <<'NODE'
486
- const fs = require('fs');
487
- const path = require('path');
488
- const cp = require('child_process');
489
- const [artifacts, target, status, ...checks] = process.argv.slice(2);
490
- const parsedChecks = checks.map((entry) => JSON.parse(entry));
491
- let fixtureStatus = null;
492
- let portHolder = null;
493
- let runtimeNotes = [];
494
- const startedRuntime = fs.existsSync(path.join(artifacts, 'logs/harness-started-runtime'));
495
- try { fixtureStatus = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/fixture-status.json'), 'utf8')); } catch {}
496
- try { portHolder = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/port-holder.json'), 'utf8')); } catch {}
497
- try { runtimeNotes = fs.readFileSync(path.join(artifacts, 'logs/runtime-notes.txt'), 'utf8').trim().split('\n').filter(Boolean); } catch {}
498
- function runGit(args) {
499
- try {
500
- return cp.execFileSync('git', ['-C', target, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
501
- } catch (error) {
502
- // Git metadata is diagnostic-only; non-git targets still produce a usable verify summary.
503
- return null;
504
- }
505
- }
506
- const harnessRootExclude = process.env.RECIPE_HARNESS_ROOT_EXCLUDE;
507
- if (!harnessRootExclude) throw new Error('RECIPE_HARNESS_ROOT_EXCLUDE is required');
508
- const statusShort = runGit(['status', '--short', '--', '.', `:(exclude)${harnessRootExclude}`]);
509
- const gitStatus = {
510
- branch: runGit(['branch', '--show-current']),
511
- head: runGit(['rev-parse', '--short', 'HEAD']),
512
- dirtyCount: statusShort ? statusShort.split('\n').filter(Boolean).length : 0,
513
- dirtyPreview: statusShort ? statusShort.split('\n').filter(Boolean).slice(0, 25) : [],
514
- };
515
- const liveRuntimeCheck = parsedChecks.find((check) => check.name === 'runner v1 live bridge smoke');
516
- const runtimeOwner = !portHolder
517
- ? 'static-only'
518
- : startedRuntime
519
- ? 'harness-owned'
520
- : portHolder.listening
521
- ? (liveRuntimeCheck?.status === 'pass' ? 'compatible-external-or-harness' : 'incompatible-external-or-stale')
522
- : 'none';
523
- const recipeControllable = liveRuntimeCheck?.status === 'pass';
524
- fs.writeFileSync(path.join(artifacts, 'summary.json'), `${JSON.stringify({
525
- adapter: 'mobile',
526
- status,
527
- runtimeClassification: {
528
- runtimeOwner,
529
- recipeControllable,
530
- startedByVerify: startedRuntime,
531
- },
532
- cleanupOwnership: {
533
- mayStop: startedRuntime,
534
- reason: startedRuntime
535
- ? 'verify launched the runtime through harness preflight'
536
- : 'verify did not launch this runtime; do not stop human-owned or pre-existing processes automatically',
537
- },
538
- gitStatus,
539
- runtimePolicy: {
540
- preflightMode: process.env.RECIPE_HARNESS_PREFLIGHT_MODE || 'fast',
541
- nativeBuildPolicy: (process.env.RECIPE_HARNESS_PREFLIGHT_MODE || 'fast') === 'fast'
542
- ? 'verify does not start product-local Mobile scripts; start/reuse a runner-owned or slot-owned runtime before live proof'
543
- : 'verify did not start product-local Mobile scripts',
544
- },
545
- fixtureStatus,
546
- portHolder,
547
- runtimeNotes,
548
- checks: parsedChecks,
549
- generatedAt: new Date().toISOString(),
550
- }, null, 2)}\n`);
551
- fs.writeFileSync(path.join(artifacts, 'artifact-manifest.json'), `${JSON.stringify({
552
- artifacts: fs.readdirSync(artifacts).map((name) => ({ path: name })),
553
- }, null, 2)}\n`);
554
- NODE
555
-
556
- echo "Mobile harness verify $status: $ARTIFACTS/summary.json"
557
- [ "$status" = "pass" ]