@deeeed/metamask-harness 0.56.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.
- package/CHANGELOG.md +20 -0
- package/adapters/manifest.json +3 -3
- package/adapters/mobile/inject.sh +4 -7
- package/dist/adapters/mobile/verify.js +549 -0
- package/dist/commands/checklist.js +6 -16
- package/dist/commands/run-engine.js +2 -2
- package/dist/commands/status-watch.js +13 -2
- package/dist/execution-provenance.js +16 -28
- package/dist/harness.js +36 -8
- package/dist/task-view.js +79 -76
- package/package.json +5 -5
- package/adapters/mobile/verify.sh +0 -557
|
@@ -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" ]
|