@deeeed/metamask-harness 0.2.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 +161 -0
- package/README.md +140 -0
- package/bin/mm-harness +99 -0
- package/docs/CHEATSHEET.md +61 -0
- package/docs/CLI-SPEC.md +915 -0
- package/docs/MENTAL-MODEL.md +295 -0
- package/docs/architecture.md +367 -0
- package/docs/extension-runtime-commands.md +60 -0
- package/docs/harness-cli.md +43 -0
- package/docs/live-adapter-contract.md +188 -0
- package/docs/package-boundaries.md +47 -0
- package/docs/perps-flow-catalog.md +235 -0
- package/docs/recipe-libraries.md +95 -0
- package/docs/runtime-file-conventions.md +36 -0
- package/library/actions/core/perps/_controller.mjs +727 -0
- package/library/actions/core/perps/assert_orders.mjs +53 -0
- package/library/actions/core/perps/assert_positions.mjs +52 -0
- package/library/actions/core/perps/close_orders.mjs +97 -0
- package/library/actions/core/perps/close_positions.mjs +118 -0
- package/library/actions/core/perps/ensure_orders.mjs +40 -0
- package/library/actions/core/perps/ensure_positions.mjs +37 -0
- package/library/actions/core/perps/place_order.mjs +201 -0
- package/library/actions/core/perps/read_account.mjs +30 -0
- package/library/actions/core/perps/read_orders.mjs +27 -0
- package/library/actions/core/perps/read_positions.mjs +27 -0
- package/library/actions/core/perps/start_state.mjs +92 -0
- package/library/actions/core/perps/teardown_state.mjs +86 -0
- package/library/actions/extension/perps/assert_orders.mjs +11 -0
- package/library/actions/extension/perps/assert_positions.mjs +11 -0
- package/library/actions/extension/perps/close_orders.mjs +8 -0
- package/library/actions/extension/perps/close_positions.mjs +8 -0
- package/library/actions/extension/perps/ensure_orders.mjs +4 -0
- package/library/actions/extension/perps/ensure_positions.mjs +4 -0
- package/library/actions/extension/perps/perps.mjs +730 -0
- package/library/actions/extension/perps/place_order.mjs +7 -0
- package/library/actions/extension/perps/read_orders.mjs +4 -0
- package/library/actions/extension/perps/read_positions.mjs +3 -0
- package/library/actions/extension/platform/cdp.mjs +541 -0
- package/library/actions/extension/ui/navigate.mjs +44 -0
- package/library/actions/extension/wallet/ensure_unlocked.mjs +36 -0
- package/library/actions/extension/wallet/read_state.mjs +27 -0
- package/library/actions/extension/wallet/select_account.mjs +48 -0
- package/library/actions/extension/wallet/setup.mjs +35 -0
- package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
- package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
- package/library/actions/mobile/bridge-runtime/cdp-bridge.cjs +686 -0
- package/library/actions/mobile/bridge-runtime/lib/cdp-eval.cjs +110 -0
- package/library/actions/mobile/bridge-runtime/lib/config.cjs +39 -0
- package/library/actions/mobile/bridge-runtime/lib/issue-capture.cjs +446 -0
- package/library/actions/mobile/bridge-runtime/lib/target-discovery.cjs +204 -0
- package/library/actions/mobile/bridge-runtime/lib/ws-client.cjs +108 -0
- package/library/actions/mobile/bridge-runtime/setup-wallet.sh +442 -0
- package/library/actions/mobile/perps/assert_orders.mjs +11 -0
- package/library/actions/mobile/perps/assert_positions.mjs +11 -0
- package/library/actions/mobile/perps/close_orders.mjs +8 -0
- package/library/actions/mobile/perps/close_positions.mjs +8 -0
- package/library/actions/mobile/perps/ensure_orders.mjs +4 -0
- package/library/actions/mobile/perps/ensure_positions.mjs +4 -0
- package/library/actions/mobile/perps/perps.mjs +709 -0
- package/library/actions/mobile/perps/place_order.mjs +7 -0
- package/library/actions/mobile/perps/read_orders.mjs +4 -0
- package/library/actions/mobile/perps/read_positions.mjs +3 -0
- package/library/actions/mobile/platform/bridge.mjs +283 -0
- package/library/actions/mobile/ui/navigate.mjs +38 -0
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +107 -0
- package/library/actions/mobile/wallet/home.mjs +35 -0
- package/library/actions/mobile/wallet/read_state.mjs +40 -0
- package/library/actions/mobile/wallet/select_account.mjs +48 -0
- package/library/actions/mobile/wallet/setup.mjs +220 -0
- package/library/flows/perps.flows.json +64 -0
- package/library/library.json +7 -0
- package/library/manifests/core.action-manifest.json +1282 -0
- package/library/manifests/extension.action-manifest.json +1749 -0
- package/library/manifests/mobile.action-manifest.json +1753 -0
- package/library/recipes/action-validation.extension.recipe.json +417 -0
- package/library/recipes/action-validation.mobile.recipe.json +422 -0
- package/library/recipes/order-lifecycle.core.recipe.json +78 -0
- package/library/recipes/perps-lifecycle.recipe.json +194 -0
- package/library/recipes/read-markets.core.recipe.json +38 -0
- package/library/recipes/smoke.extension.recipe.json +31 -0
- package/library/recipes/smoke.mobile.recipe.json +31 -0
- package/library/recipes/trading-lifecycle.core.recipe.json +76 -0
- package/orchestration/compat-overlays/README.md +19 -0
- package/orchestration/compat-overlays/mobile/README.md +13 -0
- package/orchestration/compat-overlays/mobile/rn81-message-event-source.patch +42 -0
- package/orchestration/core/cleanup.sh +37 -0
- package/orchestration/core/inject.sh +154 -0
- package/orchestration/doctor.mjs +72 -0
- package/orchestration/extension/cleanup.mjs +60 -0
- package/orchestration/extension/console-tail.mjs +228 -0
- package/orchestration/extension/ensure-browser.sh +416 -0
- package/orchestration/extension/ensure-ready.ts +185 -0
- package/orchestration/extension/extension-id.ts +107 -0
- package/orchestration/extension/inject.mjs +266 -0
- package/orchestration/extension/launch-browser.cjs +216 -0
- package/orchestration/extension/launch.sh +175 -0
- package/orchestration/extension/live.sh +320 -0
- package/orchestration/extension/pin-remote-flags.cjs +45 -0
- package/orchestration/extension/readiness.mjs +414 -0
- package/orchestration/extension/refresh-build.sh +190 -0
- package/orchestration/extension/runtime-decision.ts +445 -0
- package/orchestration/extension/runtime.ts +407 -0
- package/orchestration/extension/seed-fixture.sh +177 -0
- package/orchestration/extension/sidepanel-toggle.sh +291 -0
- package/orchestration/extension/snapshot-dist.sh +84 -0
- package/orchestration/extension/start-watch.sh +339 -0
- package/orchestration/extension/wallet-fixture-state.cjs +1086 -0
- package/orchestration/lib/activate-repo-node.sh +144 -0
- package/orchestration/lib/cli-color.mjs +84 -0
- package/orchestration/lib/cli-commands.mjs +243 -0
- package/orchestration/lib/cli-home.mjs +354 -0
- package/orchestration/lib/cli-ux.sh +252 -0
- package/orchestration/lib/cli-version.mjs +123 -0
- package/orchestration/lib/ensure-runner-deps.sh +56 -0
- package/orchestration/lib/harness-path.sh +55 -0
- package/orchestration/lib/hash-helpers.sh +44 -0
- package/orchestration/lib/json-field.sh +23 -0
- package/orchestration/lib/log-tui.mjs +304 -0
- package/orchestration/lib/open-debug.mjs +317 -0
- package/orchestration/lib/path-defaults.json +4 -0
- package/orchestration/lib/progress.mjs +107 -0
- package/orchestration/lib/recipe-paths.mjs +26 -0
- package/orchestration/lib/resolve-farmslot-ports.sh +144 -0
- package/orchestration/manifest.json +358 -0
- package/orchestration/mobile/cleanup.sh +192 -0
- package/orchestration/mobile/deps-markers.ts +21 -0
- package/orchestration/mobile/inject.sh +681 -0
- package/orchestration/mobile/launch.sh +137 -0
- package/orchestration/mobile/live.sh +125 -0
- package/orchestration/mobile/runtime-decision.ts +292 -0
- package/orchestration/porcelain/metamask-recipe +99 -0
- package/orchestration/porcelain/mm-recipe +1591 -0
- package/orchestration/porcelain/mme-recipe +1181 -0
- package/package.json +59 -0
- package/runner/extension/verify.sh +511 -0
- package/runner/mobile/verify.sh +501 -0
- package/runner/src/adapters.ts +601 -0
- package/runner/src/cli.ts +1820 -0
- package/runner/src/commands/debug.ts +44 -0
- package/runner/src/commands/fixtures.ts +99 -0
- package/runner/src/commands/launch.ts +397 -0
- package/runner/src/commands/logs.ts +60 -0
- package/runner/src/commands/shared.ts +138 -0
- package/runner/src/completions-cache.ts +86 -0
- package/runner/src/doctor.ts +203 -0
- package/runner/src/harness.ts +516 -0
- package/runner/src/heal-bounds.ts +179 -0
- package/runner/src/index.ts +6 -0
- package/runner/src/live-adapter-contract.ts +274 -0
- package/runner/src/manifest.ts +47 -0
- package/runner/src/mm-harness-cli.ts +488 -0
- package/runner/src/paths.ts +198 -0
- package/runner/src/recording-target.ts +147 -0
- package/runner/src/run-recording.ts +329 -0
- package/runner/src/runner.ts +108 -0
- package/runner/src/types.ts +57 -0
- package/scripts/completions.sh +125 -0
- package/scripts/install-completions.sh +62 -0
|
@@ -0,0 +1,501 @@
|
|
|
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/../../orchestration/lib/harness-path.sh" "$SCRIPT_DIR/../lib/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 orchestration/lib/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/metamask-recipe"
|
|
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
|
+
let port = process.env.WATCHER_PORT || '8081';
|
|
178
|
+
for (const file of ['.js.env', '.env', '.env.local']) {
|
|
179
|
+
const full = path.join(target, file);
|
|
180
|
+
if (!fs.existsSync(full)) continue;
|
|
181
|
+
const text = fs.readFileSync(full, 'utf8');
|
|
182
|
+
const match = text.match(/^\s*(?:export\s+)?WATCHER_PORT=(["']?)([0-9]+)\1/m);
|
|
183
|
+
if (match) { port = match[2]; break; }
|
|
184
|
+
}
|
|
185
|
+
console.log(port);
|
|
186
|
+
NODE
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# Resolve a runtime env var (e.g. IOS_SIMULATOR, ADB_SERIAL) from the process
|
|
190
|
+
# env or the target repo's .js.env so the runner can bind device-scoped proof
|
|
191
|
+
# (screenshots) to the same device as the bridge commands.
|
|
192
|
+
jsenv_value() {
|
|
193
|
+
TARGET_FOR_JSENV="$TARGET" JSENV_NAME="$1" node <<'NODE'
|
|
194
|
+
const fs = require('fs');
|
|
195
|
+
const path = require('path');
|
|
196
|
+
const target = process.env.TARGET_FOR_JSENV;
|
|
197
|
+
const name = process.env.JSENV_NAME;
|
|
198
|
+
let value = process.env[name] || '';
|
|
199
|
+
if (!value) {
|
|
200
|
+
const re = new RegExp("^\\s*(?:export\\s+)?" + name + "=([\"']?)([^\"'\\n]+)\\1", "m");
|
|
201
|
+
for (const file of ['.js.env', '.env', '.env.local']) {
|
|
202
|
+
const full = path.join(target, file);
|
|
203
|
+
if (!fs.existsSync(full)) continue;
|
|
204
|
+
const match = fs.readFileSync(full, 'utf8').match(re);
|
|
205
|
+
if (match) { value = match[2]; break; }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
process.stdout.write(value);
|
|
209
|
+
NODE
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
check_file() {
|
|
213
|
+
local rel="$1"
|
|
214
|
+
if [ -e "$TARGET/$rel" ]; then
|
|
215
|
+
checks+=("{\"name\":\"$rel\",\"status\":\"pass\"}")
|
|
216
|
+
else
|
|
217
|
+
checks+=("{\"name\":\"$rel\",\"status\":\"fail\"}")
|
|
218
|
+
status="fail"
|
|
219
|
+
fi
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
check_file "$HARNESS_REL/manifest.json"
|
|
223
|
+
check_file "$HARNESS_REL/action-manifest.json"
|
|
224
|
+
check_file "$HARNESS_REL/runner/bin/metamask-recipe"
|
|
225
|
+
check_file "package.json"
|
|
226
|
+
check_file "app/dev-tools/AgenticService/AgenticService.ts"
|
|
227
|
+
|
|
228
|
+
if ! grep -q "AgenticService.install" "$TARGET/app/core/NavigationService/NavigationService.ts" 2>/dev/null; then
|
|
229
|
+
checks+=("{\"name\":\"NavigationService patch\",\"status\":\"fail\"}")
|
|
230
|
+
status="fail"
|
|
231
|
+
else
|
|
232
|
+
checks+=("{\"name\":\"NavigationService patch\",\"status\":\"pass\"}")
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
if ! grep -q "AgentStepHud" "$TARGET/app/components/Nav/App/App.tsx" 2>/dev/null; then
|
|
236
|
+
checks+=("{\"name\":\"App AgentStepHud patch\",\"status\":\"fail\"}")
|
|
237
|
+
status="fail"
|
|
238
|
+
else
|
|
239
|
+
checks+=("{\"name\":\"App AgentStepHud patch\",\"status\":\"pass\"}")
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
# Drift detection (read-only): compare the in-repo AgenticService/HUD against
|
|
243
|
+
# the resolved runner overlay, not a runner-owned copy. The runner is the only
|
|
244
|
+
# MetaMask-aware source of truth for Mobile injection.
|
|
245
|
+
overlay_drift_check="$(
|
|
246
|
+
HARNESS_DIR_FOR_DRIFT="$HARNESS_DIR" TARGET_FOR_DRIFT="$TARGET" node <<'NODE'
|
|
247
|
+
const fs = require('fs');
|
|
248
|
+
const path = require('path');
|
|
249
|
+
const name = 'agentic overlay matches runner (HUD freshness)';
|
|
250
|
+
const harnessDir = process.env.HARNESS_DIR_FOR_DRIFT;
|
|
251
|
+
const targetDir = path.join(process.env.TARGET_FOR_DRIFT, 'app', 'core', 'AgenticService');
|
|
252
|
+
function readTrimmed(file) {
|
|
253
|
+
try { return fs.readFileSync(file, 'utf8').trim(); } catch { return ''; }
|
|
254
|
+
}
|
|
255
|
+
let runnerDir = readTrimmed(path.join(harnessDir, 'runner', '.runner-source'));
|
|
256
|
+
if (!runnerDir) {
|
|
257
|
+
try {
|
|
258
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(harnessDir, 'manifest.json'), 'utf8'));
|
|
259
|
+
runnerDir = manifest && manifest.source && manifest.source.runnerDir || '';
|
|
260
|
+
} catch {}
|
|
261
|
+
}
|
|
262
|
+
if (!runnerDir) {
|
|
263
|
+
process.stdout.write(JSON.stringify({ name, status: 'warn', detail: 'runner source unavailable; overlay freshness compare skipped' }));
|
|
264
|
+
process.exit(0);
|
|
265
|
+
}
|
|
266
|
+
const overlayDir = path.join(runnerDir, 'library', 'actions', 'mobile', 'app-overlay', 'app', 'core', 'AgenticService');
|
|
267
|
+
let checked = 0;
|
|
268
|
+
const drifted = [];
|
|
269
|
+
const missing = [];
|
|
270
|
+
try {
|
|
271
|
+
for (const entry of fs.readdirSync(overlayDir)) {
|
|
272
|
+
if (!entry.endsWith('.patch')) continue;
|
|
273
|
+
if (/\.test\./u.test(entry)) continue;
|
|
274
|
+
const base = entry.slice(0, -'.patch'.length);
|
|
275
|
+
const targetFile = path.join(targetDir, base);
|
|
276
|
+
if (!fs.existsSync(targetFile)) { missing.push(base); continue; }
|
|
277
|
+
checked += 1;
|
|
278
|
+
if (fs.readFileSync(path.join(overlayDir, entry), 'utf8') !== fs.readFileSync(targetFile, 'utf8')) {
|
|
279
|
+
drifted.push(base);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
} catch (error) {
|
|
283
|
+
process.stdout.write(JSON.stringify({ name, status: 'warn', detail: 'runner overlay compare skipped: ' + String((error && error.message) || error) }));
|
|
284
|
+
process.exit(0);
|
|
285
|
+
}
|
|
286
|
+
if (checked === 0) {
|
|
287
|
+
process.stdout.write(JSON.stringify({ name, status: 'pass' }));
|
|
288
|
+
} else if (drifted.length || missing.length) {
|
|
289
|
+
const parts = [];
|
|
290
|
+
if (drifted.length) parts.push('behind runner: ' + drifted.join(', '));
|
|
291
|
+
if (missing.length) parts.push('absent in repo: ' + missing.join(', '));
|
|
292
|
+
process.stdout.write(JSON.stringify({ name, status: 'warn', detail: parts.join('; ') + ' — rerun metamask-recipe install --force-overlay or recipe sync to refresh the in-repo HUD/AgenticService from the runner' }));
|
|
293
|
+
} else {
|
|
294
|
+
process.stdout.write(JSON.stringify({ name, status: 'pass' }));
|
|
295
|
+
}
|
|
296
|
+
NODE
|
|
297
|
+
)"
|
|
298
|
+
checks+=("$overlay_drift_check")
|
|
299
|
+
run_with_timeout() {
|
|
300
|
+
local log_path="$1"
|
|
301
|
+
local timeout_s="$2"
|
|
302
|
+
shift 2
|
|
303
|
+
[[ "$timeout_s" =~ ^[0-9]+$ ]] || {
|
|
304
|
+
echo "Invalid timeout seconds: $timeout_s" >&2
|
|
305
|
+
return 2
|
|
306
|
+
}
|
|
307
|
+
"$@" > "$log_path" 2>&1 &
|
|
308
|
+
local pid=$!
|
|
309
|
+
local waited=0
|
|
310
|
+
while kill -0 "$pid" 2>/dev/null; do
|
|
311
|
+
if [ "$waited" -ge "$timeout_s" ]; then
|
|
312
|
+
echo "Timed out after ${timeout_s}s: $*" >> "$log_path"
|
|
313
|
+
kill "$pid" 2>/dev/null || true
|
|
314
|
+
sleep 1
|
|
315
|
+
kill -9 "$pid" 2>/dev/null || true
|
|
316
|
+
wait "$pid" 2>/dev/null || true
|
|
317
|
+
return 124
|
|
318
|
+
fi
|
|
319
|
+
sleep 1
|
|
320
|
+
waited=$((waited + 1))
|
|
321
|
+
done
|
|
322
|
+
wait "$pid"
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
live_status_ok() {
|
|
326
|
+
local log_path="$1"
|
|
327
|
+
cat > "$ARTIFACTS/mobile-status-smoke.recipe.json" <<'JSON'
|
|
328
|
+
{
|
|
329
|
+
"schema_version": 1,
|
|
330
|
+
"title": "Mobile runner status smoke",
|
|
331
|
+
"description": "Checks that the installed runner can read Mobile app status before live verification.",
|
|
332
|
+
"validate": {
|
|
333
|
+
"workflow": {
|
|
334
|
+
"entry": "status",
|
|
335
|
+
"nodes": {
|
|
336
|
+
"status": { "action": "app.status", "intent": "Read Mobile app status through the runner-owned bridge", "next": "done" },
|
|
337
|
+
"done": { "action": "end", "status": "pass" }
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
JSON
|
|
343
|
+
(
|
|
344
|
+
cd "$TARGET"
|
|
345
|
+
"$RUNNER_BIN" run "$ARTIFACTS/mobile-status-smoke.recipe.json" --adapter mobile --project-root "$TARGET" --artifacts-dir "$ARTIFACTS/runner-status-smoke" --json
|
|
346
|
+
) > "$log_path" 2>&1
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
ensure_live_runtime() {
|
|
350
|
+
if live_status_ok "$ARTIFACTS/logs/app-status-precheck.log"; then
|
|
351
|
+
return 0
|
|
352
|
+
fi
|
|
353
|
+
if [ "$AUTO_START" = true ]; then
|
|
354
|
+
cat >&2 <<'EOF'
|
|
355
|
+
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.
|
|
356
|
+
EOF
|
|
357
|
+
fi
|
|
358
|
+
return 1
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if [ "$STATIC_ONLY" = false ]; then
|
|
362
|
+
if ensure_live_runtime; then
|
|
363
|
+
checks+=('{"name":"mobile runtime controllable precheck","status":"pass"}')
|
|
364
|
+
else
|
|
365
|
+
checks+=('{"name":"mobile runtime controllable precheck","status":"fail","detail":"see logs/app-status-precheck.log"}')
|
|
366
|
+
status="fail"
|
|
367
|
+
fi
|
|
368
|
+
|
|
369
|
+
fixture_json="$(fixture_status_json)"
|
|
370
|
+
printf '%s\n' "$fixture_json" > "$ARTIFACTS/logs/fixture-status.json"
|
|
371
|
+
fixture_check_json="$(fixture_check_json "$ARTIFACTS/logs/fixture-status.json")"
|
|
372
|
+
fixture_message="$(node -e 'const v=JSON.parse(process.argv[1]); console.log(v.message || v.detail);' "$fixture_check_json")"
|
|
373
|
+
echo "$fixture_message" >&2
|
|
374
|
+
add_note "$fixture_message"
|
|
375
|
+
checks+=("$fixture_check_json")
|
|
376
|
+
|
|
377
|
+
port="$(watcher_port)"
|
|
378
|
+
# Reject a non-numeric port before it is interpolated into a shell command
|
|
379
|
+
# (port_holder_json runs `lsof -iTCP:${port}`). The .env path is regex-guarded,
|
|
380
|
+
# but the WATCHER_PORT env path is not, so validate here.
|
|
381
|
+
case "$port" in
|
|
382
|
+
""|*[!0-9]*) echo "Refusing mobile verify: resolved watcher port is not numeric: '$port' (check WATCHER_PORT)" >&2; exit 2 ;;
|
|
383
|
+
esac
|
|
384
|
+
port_holder_json "$port" > "$ARTIFACTS/logs/port-holder.json"
|
|
385
|
+
|
|
386
|
+
cat > "$ARTIFACTS/mobile-v1-live-smoke.recipe.json" <<'JSON'
|
|
387
|
+
{
|
|
388
|
+
"schema_version": 1,
|
|
389
|
+
"title": "Mobile v1 runner live bridge smoke",
|
|
390
|
+
"description": "Verifies the installed MetaMask runner can drive the React Native debug bridge through Recipe v1 actions.",
|
|
391
|
+
"validate": {
|
|
392
|
+
"workflow": {
|
|
393
|
+
"entry": "status",
|
|
394
|
+
"nodes": {
|
|
395
|
+
"status": { "action": "app.status", "intent": "Read Mobile app status through the v1 runner", "next": "cdp-probe" },
|
|
396
|
+
"cdp-probe": { "action": "cdp.target", "intent": "Verify the React Native debug bridge target is reachable", "required": true, "timeout_ms": 15000, "next": "wallet-setup" },
|
|
397
|
+
"wallet-setup": { "action": "metamask.wallet.setup", "intent": "Prepare the wallet fixture for the live bridge smoke", "timeout_ms": 45000, "next": "wallet-unlock" },
|
|
398
|
+
"wallet-unlock": { "action": "metamask.wallet.ensure_unlocked", "intent": "Unlock the wallet through the manifest-declared action", "timeout_ms": 45000, "next": "wallet-read" },
|
|
399
|
+
"wallet-read": { "action": "metamask.wallet.read_state", "intent": "Read wallet state before navigating to the wallet view", "timeout_ms": 45000, "next": "navigate-wallet" },
|
|
400
|
+
"navigate-wallet": { "action": "ui.navigate", "intent": "Open the wallet view through the UI navigation action", "route": "WalletView", "timeout_ms": 45000, "next": "wait-wallet" },
|
|
401
|
+
"wait-wallet": { "action": "ui.wait_for", "intent": "Wait until the wallet screen is present", "test_id": "wallet-screen", "expected": "present", "timeout_ms": 45000, "next": "hud-smoke" },
|
|
402
|
+
"hud-smoke": { "action": "app.hud", "status": "running", "intent": "Show the Mobile live bridge smoke HUD", "progress": { "current": 1, "total": 1 }, "timeout_ms": 45000, "next": "screenshot" },
|
|
403
|
+
"screenshot": { "action": "ui.screenshot", "intent": "Capture proof that the wallet screen is visible", "path": "screenshots/mobile-v1-live-smoke.png", "timeout_ms": 45000, "next": "done" },
|
|
404
|
+
"done": { "action": "end", "status": "pass" }
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
JSON
|
|
410
|
+
|
|
411
|
+
ios_simulator_resolved="$(jsenv_value IOS_SIMULATOR)"
|
|
412
|
+
adb_serial_resolved="$(jsenv_value ADB_SERIAL)"
|
|
413
|
+
if (
|
|
414
|
+
cd "$TARGET"
|
|
415
|
+
METAMASK_RECIPE_AUTO_HUD=0 \
|
|
416
|
+
IOS_SIMULATOR="${IOS_SIMULATOR:-$ios_simulator_resolved}" \
|
|
417
|
+
ADB_SERIAL="${ADB_SERIAL:-$adb_serial_resolved}" \
|
|
418
|
+
ANDROID_SERIAL="${ANDROID_SERIAL:-$adb_serial_resolved}" \
|
|
419
|
+
"$RUNNER_BIN" run "$ARTIFACTS/mobile-v1-live-smoke.recipe.json" --adapter mobile --project-root "$TARGET" --artifacts-dir "$ARTIFACTS/runner-live-smoke" --json
|
|
420
|
+
) > "$ARTIFACTS/logs/runner-live-smoke.log" 2>&1; then
|
|
421
|
+
checks+=("{\"name\":\"runner v1 live bridge smoke\",\"status\":\"pass\"}")
|
|
422
|
+
else
|
|
423
|
+
checks+=("{\"name\":\"runner v1 live bridge smoke\",\"status\":\"fail\",\"detail\":\"see logs/runner-live-smoke.log\"}")
|
|
424
|
+
add_note "Runner v1 live bridge smoke failed; inspect logs/runner-live-smoke.log and runner-live-smoke/trace.json."
|
|
425
|
+
status="fail"
|
|
426
|
+
fi
|
|
427
|
+
fi
|
|
428
|
+
|
|
429
|
+
RECIPE_HARNESS_PREFLIGHT_MODE="$PREFLIGHT_MODE" RECIPE_HARNESS_ROOT_EXCLUDE="$HARNESS_ROOT" node - "$ARTIFACTS" "$TARGET" "$status" "${checks[@]}" <<'NODE'
|
|
430
|
+
const fs = require('fs');
|
|
431
|
+
const path = require('path');
|
|
432
|
+
const cp = require('child_process');
|
|
433
|
+
const [artifacts, target, status, ...checks] = process.argv.slice(2);
|
|
434
|
+
const parsedChecks = checks.map((entry) => JSON.parse(entry));
|
|
435
|
+
let fixtureStatus = null;
|
|
436
|
+
let portHolder = null;
|
|
437
|
+
let runtimeNotes = [];
|
|
438
|
+
const startedRuntime = fs.existsSync(path.join(artifacts, 'logs/harness-started-runtime'));
|
|
439
|
+
try { fixtureStatus = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/fixture-status.json'), 'utf8')); } catch {}
|
|
440
|
+
try { portHolder = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/port-holder.json'), 'utf8')); } catch {}
|
|
441
|
+
try { runtimeNotes = fs.readFileSync(path.join(artifacts, 'logs/runtime-notes.txt'), 'utf8').trim().split('\n').filter(Boolean); } catch {}
|
|
442
|
+
function runGit(args) {
|
|
443
|
+
try {
|
|
444
|
+
return cp.execFileSync('git', ['-C', target, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
445
|
+
} catch (error) {
|
|
446
|
+
// Git metadata is diagnostic-only; non-git targets still produce a usable verify summary.
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const harnessRootExclude = process.env.RECIPE_HARNESS_ROOT_EXCLUDE;
|
|
451
|
+
if (!harnessRootExclude) throw new Error('RECIPE_HARNESS_ROOT_EXCLUDE is required');
|
|
452
|
+
const statusShort = runGit(['status', '--short', '--', '.', `:(exclude)${harnessRootExclude}`]);
|
|
453
|
+
const gitStatus = {
|
|
454
|
+
branch: runGit(['branch', '--show-current']),
|
|
455
|
+
head: runGit(['rev-parse', '--short', 'HEAD']),
|
|
456
|
+
dirtyCount: statusShort ? statusShort.split('\n').filter(Boolean).length : 0,
|
|
457
|
+
dirtyPreview: statusShort ? statusShort.split('\n').filter(Boolean).slice(0, 25) : [],
|
|
458
|
+
};
|
|
459
|
+
const liveRuntimeCheck = parsedChecks.find((check) => check.name === 'runner v1 live bridge smoke');
|
|
460
|
+
const runtimeOwner = !portHolder
|
|
461
|
+
? 'static-only'
|
|
462
|
+
: startedRuntime
|
|
463
|
+
? 'harness-owned'
|
|
464
|
+
: portHolder.listening
|
|
465
|
+
? (liveRuntimeCheck?.status === 'pass' ? 'compatible-external-or-harness' : 'incompatible-external-or-stale')
|
|
466
|
+
: 'none';
|
|
467
|
+
const recipeControllable = liveRuntimeCheck?.status === 'pass';
|
|
468
|
+
fs.writeFileSync(path.join(artifacts, 'summary.json'), `${JSON.stringify({
|
|
469
|
+
adapter: 'mobile',
|
|
470
|
+
status,
|
|
471
|
+
runtimeClassification: {
|
|
472
|
+
runtimeOwner,
|
|
473
|
+
recipeControllable,
|
|
474
|
+
startedByVerify: startedRuntime,
|
|
475
|
+
},
|
|
476
|
+
cleanupOwnership: {
|
|
477
|
+
mayStop: startedRuntime,
|
|
478
|
+
reason: startedRuntime
|
|
479
|
+
? 'verify launched the runtime through harness preflight'
|
|
480
|
+
: 'verify did not launch this runtime; do not stop human-owned or pre-existing processes automatically',
|
|
481
|
+
},
|
|
482
|
+
gitStatus,
|
|
483
|
+
runtimePolicy: {
|
|
484
|
+
preflightMode: process.env.RECIPE_HARNESS_PREFLIGHT_MODE || 'fast',
|
|
485
|
+
nativeBuildPolicy: (process.env.RECIPE_HARNESS_PREFLIGHT_MODE || 'fast') === 'fast'
|
|
486
|
+
? 'verify does not start product-local Mobile scripts; start/reuse a runner-owned or slot-owned runtime before live proof'
|
|
487
|
+
: 'verify did not start product-local Mobile scripts',
|
|
488
|
+
},
|
|
489
|
+
fixtureStatus,
|
|
490
|
+
portHolder,
|
|
491
|
+
runtimeNotes,
|
|
492
|
+
checks: parsedChecks,
|
|
493
|
+
generatedAt: new Date().toISOString(),
|
|
494
|
+
}, null, 2)}\n`);
|
|
495
|
+
fs.writeFileSync(path.join(artifacts, 'artifact-manifest.json'), `${JSON.stringify({
|
|
496
|
+
artifacts: fs.readdirSync(artifacts).map((name) => ({ path: name })),
|
|
497
|
+
}, null, 2)}\n`);
|
|
498
|
+
NODE
|
|
499
|
+
|
|
500
|
+
echo "Mobile harness verify $status: $ARTIFACTS/summary.json"
|
|
501
|
+
[ "$status" = "pass" ]
|