@deeeed/metamask-harness 0.10.0 → 0.11.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 +19 -0
- package/adapters/mobile/start-metro.sh +80 -36
- package/adapters/mobile/wait-for-bridge.sh +15 -1
- package/dist/adapters.js +41 -18
- package/dist/app-lifecycle.js +72 -0
- package/dist/cli-commands.js +1 -1
- package/dist/commands/device-target.js +35 -9
- package/dist/commands/fixtures.js +13 -1
- package/dist/commands/launch/index.js +35 -7
- package/dist/commands/run-engine.js +1 -0
- package/dist/mm-harness-cli.js +1 -0
- package/dist/paths.js +8 -1
- package/dist/runner.js +10 -4
- package/docs/CLI-SPEC.md +2 -1
- package/docs/recipe-libraries.md +19 -0
- package/library/actions/mobile/platform/bridge.mjs +29 -7
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +66 -8
- package/library/manifests/mobile.action-manifest.json +18 -0
- package/library/recipes/app-lifecycle-android-smoke.mobile.recipe.json +87 -0
- package/library/recipes/perps-performance-background-resume.mobile.recipe.json +72 -0
- package/library/recipes/perps-performance-cold-start.mobile.recipe.json +72 -0
- package/library/recipes/perps-performance-warm-start.mobile.recipe.json +64 -0
- package/library/recipes/perps-performance.mobile.recipe.json +15 -9
- package/package.json +2 -2
package/dist/runner.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
2
|
import { createMetaMaskAdapters, createMetaMaskUiTransport } from "./adapters.js";
|
|
3
|
+
import { resolveMetaMaskMobileLifecycleTarget } from "./app-lifecycle.js";
|
|
3
4
|
import { loadMetaMaskExtensionActionManifest, loadMetaMaskMobileActionManifest } from "./manifest.js";
|
|
4
5
|
import { createMetaMaskRecordingTargetProvider } from "./recording-target.js";
|
|
5
|
-
import { importRecipeHarness, importRecipeHarnessRuntimeCdp, importRecipeHarnessRuntimeReactNativeBridge, runnerDir } from "./paths.js";
|
|
6
|
+
import { importRecipeHarness, importRecipeHarnessAppLifecycle, importRecipeHarnessRuntimeCdp, importRecipeHarnessRuntimeReactNativeBridge, runnerDir } from "./paths.js";
|
|
6
7
|
async function createMetaMaskMobileRunner(options = {}) {
|
|
7
8
|
return createMetaMaskRunner(
|
|
8
9
|
"mobile",
|
|
@@ -23,13 +24,14 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
23
24
|
} = await importRecipeHarness();
|
|
24
25
|
const { createCdpWebUiTransport } = await importRecipeHarnessRuntimeCdp();
|
|
25
26
|
const { createReactNativeBridgeUiTransport } = await importRecipeHarnessRuntimeReactNativeBridge();
|
|
27
|
+
const { createAppLifecycleAdapters } = await importRecipeHarnessAppLifecycle();
|
|
26
28
|
const actions = [
|
|
27
29
|
...actionManifest.supported_official_actions,
|
|
28
30
|
...(actionManifest.custom_actions ?? []).map((entry) => entry.name)
|
|
29
31
|
];
|
|
30
32
|
const declaredActions = new Set(actions);
|
|
31
33
|
const core = createStandardCoreAdapters({ actions });
|
|
32
|
-
const projectOwnedOfficialActions = /* @__PURE__ */ new Set(["app.status"]);
|
|
34
|
+
const projectOwnedOfficialActions = /* @__PURE__ */ new Set(["app.status", "app.lifecycle"]);
|
|
33
35
|
const ui = createStandardUiAdapters({
|
|
34
36
|
actions: actions.filter((action) => !projectOwnedOfficialActions.has(action)),
|
|
35
37
|
transport: createMetaMaskUiTransport(adapter, {
|
|
@@ -38,14 +40,18 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
38
40
|
})
|
|
39
41
|
});
|
|
40
42
|
const existing = new Set([...core, ...ui].map((entry) => entry.action));
|
|
43
|
+
const lifecycle = adapter === "mobile" ? createAppLifecycleAdapters({
|
|
44
|
+
actions,
|
|
45
|
+
targetProvider: { resolveTarget: resolveMetaMaskMobileLifecycleTarget }
|
|
46
|
+
}) : [];
|
|
41
47
|
const custom = createMetaMaskAdapters(adapter).filter(
|
|
42
|
-
(entry) => declaredActions.has(entry.action) && !existing.has(entry.action)
|
|
48
|
+
(entry) => declaredActions.has(entry.action) && !existing.has(entry.action) && !lifecycle.some((lifecycleEntry) => lifecycleEntry.action === entry.action)
|
|
43
49
|
);
|
|
44
50
|
const autoHudDisabled = process.env.METAMASK_RECIPE_AUTO_HUD === "0" || process.env.METAMASK_RECIPE_AUTO_HUD === "false";
|
|
45
51
|
const logger = options.quietStdout ? new console.Console(process.stderr) : console;
|
|
46
52
|
return createRecipeRunner({
|
|
47
53
|
actionManifest,
|
|
48
|
-
adapters: [...core, ...ui, ...custom],
|
|
54
|
+
adapters: [...core, ...ui, ...lifecycle, ...custom],
|
|
49
55
|
logger,
|
|
50
56
|
recording: {
|
|
51
57
|
targetProvider: createMetaMaskRecordingTargetProvider(adapter)
|
package/docs/CLI-SPEC.md
CHANGED
|
@@ -439,7 +439,7 @@ Readiness check for a checkout without launching the app. Doctor is the single p
|
|
|
439
439
|
|
|
440
440
|
## `--device <id>` — first-class mobile device targeting (REAL)
|
|
441
441
|
|
|
442
|
-
`--device` is the uniform mobile device selector on `run`, `call`, and `
|
|
442
|
+
`--device` is the uniform mobile device selector on `run`, `call`, `doctor`, and `fixtures`. Pass the **adb serial** for Android (from `adb devices`) or the **UDID / simulator name** for iOS. The harness resolves the adb serial to the Metro CDP target identity internally — users never need to know or set `ANDROID_DEVICE='Pixel 6 - 16 - API 36'`.
|
|
443
443
|
|
|
444
444
|
**Internal Android identity mapping**: `ADB_SERIAL` / `ANDROID_SERIAL` carry the raw adb serial. The bridge resolves the device model via `adb -s <serial> shell getprop ro.product.model` and propagates it as `ANDROID_TARGET_DEVICE_NAME`. Target-discovery uses `ANDROID_TARGET_DEVICE_NAME` for Metro `deviceName` prefix matching (e.g. `"Pixel 6"` matches `"Pixel 6 - 16 - API 36"`). When the pinned model cannot be matched to any Metro `/json/list` candidate and multiple candidates exist, the bridge fails fast with a diagnostic listing every candidate's `deviceName` — it never silently selects the wrong device. iOS UDID/simulator name → `IOS_SIMULATOR` (unchanged).
|
|
445
445
|
|
|
@@ -447,6 +447,7 @@ Readiness check for a checkout without launching the app. Doctor is the single p
|
|
|
447
447
|
|---|---|---|
|
|
448
448
|
| `run` / `call` | resolve id → set serial/simulator env; proceed | **ambiguity gate:** >1 connected mobile device (across both android and ios, counting only targetable ones: android state `device`, iOS state `Booted`) → fail fast (exit 2), listing connected devices and `--device <id>` hints. Exactly one targetable (or zero — existing engine errors speak) → unchanged behavior. |
|
|
449
449
|
| `doctor` | resolve id → set env | no gate — doctor is diagnostic and **reports** the device list (`devices[]`) instead. |
|
|
450
|
+
| `fixtures` | resolve id → set serial/simulator env before sync/set and recovery relaunch | no gate — preserves existing single-device and ambient slot behavior. |
|
|
450
451
|
|
|
451
452
|
`--device` on the **extension/core** adapter is a teaching usage error (those adapters have no device to target). `run --plan` is static (touches no device) and is exempt from the gate.
|
|
452
453
|
|
package/docs/recipe-libraries.md
CHANGED
|
@@ -230,6 +230,16 @@ mm-harness run my-perps-performance \
|
|
|
230
230
|
--library mydev=~/my-recipes \
|
|
231
231
|
--adapter mobile --device <serial> --heal off --artifacts-dir artifacts
|
|
232
232
|
|
|
233
|
+
# Canonical start-state variants — same installed app, no rebuild.
|
|
234
|
+
mm-harness run app-lifecycle-android-smoke \
|
|
235
|
+
--adapter mobile --device <serial> --heal off --artifacts-dir artifacts/lifecycle-smoke
|
|
236
|
+
mm-harness run perps-performance-warm-start \
|
|
237
|
+
--adapter mobile --device <serial> --heal off --artifacts-dir artifacts/warm
|
|
238
|
+
mm-harness run perps-performance-background-resume \
|
|
239
|
+
--adapter mobile --device <serial> --heal off --artifacts-dir artifacts/background
|
|
240
|
+
mm-harness run perps-performance-cold-start \
|
|
241
|
+
--adapter mobile --device <serial> --heal off --artifacts-dir artifacts/cold
|
|
242
|
+
|
|
233
243
|
# Per-node durations to diff across runs (trace.json is an array of entries, or
|
|
234
244
|
# { metadata, entries: [...] }; each entry carries nodeId + durationMs):
|
|
235
245
|
node -e 'const t=require("./artifacts/trace.json"); \
|
|
@@ -239,6 +249,15 @@ node -e 'const t=require("./artifacts/trace.json"); \
|
|
|
239
249
|
A miss with `--library` names the sources that were searched, so you can tell at a
|
|
240
250
|
glance whether a typo or a missing library entry caused the failure.
|
|
241
251
|
|
|
252
|
+
Run `app-lifecycle-android-smoke` first when validating a new Android slot; it
|
|
253
|
+
isolates lifecycle control from wallet setup and Perps navigation. The start-state
|
|
254
|
+
variants use the standard outer `app.lifecycle` action. Every
|
|
255
|
+
variant begins with `app.status` so `trace.json` records an idempotent start
|
|
256
|
+
marker before lifecycle setup and Perps timing nodes. Android background resume
|
|
257
|
+
sends HOME, then relaunches through the Expo dev-client deep link. Cold start
|
|
258
|
+
force-stops the installed package, then launches the same build through the deep
|
|
259
|
+
link.
|
|
260
|
+
|
|
242
261
|
### The measured-flow pattern
|
|
243
262
|
|
|
244
263
|
Five rules keep timings meaningful and diffable:
|
|
@@ -71,10 +71,14 @@ export async function bridgeEnv(input) {
|
|
|
71
71
|
const watcherPort = target.watcherPort;
|
|
72
72
|
const simulator = target.iosSimulator;
|
|
73
73
|
const androidDevice = target.androidDevice;
|
|
74
|
+
const androidTargetDeviceName = target.androidTargetDeviceName;
|
|
74
75
|
const adbSerial = target.adbSerial;
|
|
75
76
|
if (watcherPort !== undefined && watcherPort !== null && String(watcherPort) !== '') env.WATCHER_PORT = String(watcherPort);
|
|
76
77
|
if (simulator !== undefined && simulator !== null && String(simulator) !== '') env.IOS_SIMULATOR = String(simulator);
|
|
77
78
|
if (androidDevice !== undefined && androidDevice !== null && String(androidDevice) !== '') env.ANDROID_DEVICE = String(androidDevice);
|
|
79
|
+
if (androidTargetDeviceName !== undefined && androidTargetDeviceName !== null && String(androidTargetDeviceName) !== '') {
|
|
80
|
+
env.ANDROID_TARGET_DEVICE_NAME = String(androidTargetDeviceName);
|
|
81
|
+
}
|
|
78
82
|
if (adbSerial !== undefined && adbSerial !== null && String(adbSerial) !== '') {
|
|
79
83
|
env.ADB_SERIAL = String(adbSerial);
|
|
80
84
|
env.ANDROID_SERIAL = String(adbSerial);
|
|
@@ -92,6 +96,8 @@ export async function bridgeEnv(input) {
|
|
|
92
96
|
if (model) {
|
|
93
97
|
env.ANDROID_TARGET_DEVICE_NAME = model;
|
|
94
98
|
}
|
|
99
|
+
}
|
|
100
|
+
if (serialStr) {
|
|
95
101
|
// ANDROID_DEVICE === ADB_SERIAL is the explicit --device android pin shape
|
|
96
102
|
// (device-target.ts / launch --device). Slots hosting BOTH platforms carry an
|
|
97
103
|
// ambient IOS_SIMULATOR in their context env, which spreads over process.env
|
|
@@ -113,8 +119,14 @@ function resolveMobileTarget(input) {
|
|
|
113
119
|
const watcherPort = input.node?.watcher_port ?? input.node?.metro_port ?? input.node?.cdp_port ?? contextEnv.WATCHER_PORT ?? contextEnv.CDP_PORT ?? contextEnv.RECIPE_CDP_PORT ?? process.env.WATCHER_PORT ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT;
|
|
114
120
|
const iosSimulator = input.node?.simulator ?? input.node?.ios_simulator ?? contextEnv.IOS_SIMULATOR ?? process.env.IOS_SIMULATOR;
|
|
115
121
|
const androidDevice = input.node?.android_device ?? contextEnv.ANDROID_DEVICE ?? process.env.ANDROID_DEVICE;
|
|
122
|
+
const androidTargetDeviceName = input.node?.android_target_device_name ?? input.node?.androidTargetDeviceName ?? contextEnv.ANDROID_TARGET_DEVICE_NAME ?? process.env.ANDROID_TARGET_DEVICE_NAME;
|
|
116
123
|
const adbSerial = input.node?.adb_serial ?? contextEnv.ADB_SERIAL ?? contextEnv.ANDROID_SERIAL ?? process.env.ADB_SERIAL ?? process.env.ANDROID_SERIAL ?? androidDevice;
|
|
117
|
-
return { watcherPort, iosSimulator, androidDevice, adbSerial };
|
|
124
|
+
return { watcherPort, iosSimulator, androidDevice, androidTargetDeviceName, adbSerial };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function androidDeviceNameMatches(deviceName, targetName) {
|
|
128
|
+
if (!targetName) return false;
|
|
129
|
+
return deviceName === targetName || deviceName.startsWith(`${targetName} -`);
|
|
118
130
|
}
|
|
119
131
|
|
|
120
132
|
// Select THIS action's entry from a multi-target bridge `status` reply (the bridge
|
|
@@ -124,21 +136,31 @@ function resolveMobileTarget(input) {
|
|
|
124
136
|
// against Metro deviceNames (which are model descriptors, never serials), so a
|
|
125
137
|
// pinned android run selected the iOS entry on dual-platform slots.
|
|
126
138
|
export function selectBridgeStatusEntry(status, input) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const entries = status.filter((entry) => entry && typeof entry === 'object');
|
|
139
|
+
const entries = Array.isArray(status)
|
|
140
|
+
? status.filter((entry) => entry && typeof entry === 'object')
|
|
141
|
+
: (status && typeof status === 'object' ? [status] : []);
|
|
131
142
|
if (entries.length === 0) return null;
|
|
132
143
|
const target = resolveMobileTarget(input);
|
|
133
144
|
const adbSerial = target.adbSerial != null ? String(target.adbSerial) : '';
|
|
134
145
|
const androidDevice = target.androidDevice != null ? String(target.androidDevice) : '';
|
|
146
|
+
const androidTargetDeviceName = target.androidTargetDeviceName != null ? String(target.androidTargetDeviceName) : '';
|
|
135
147
|
const iosSimulator = target.iosSimulator != null ? String(target.iosSimulator) : '';
|
|
136
148
|
|
|
137
149
|
// Explicit android pin shape (--device <serial> sets ANDROID_DEVICE === ADB_SERIAL).
|
|
138
150
|
// Checked BEFORE the simulator identity: dual-platform slots inject an ambient
|
|
139
151
|
// IOS_SIMULATOR that must not capture a pinned android action.
|
|
140
|
-
if (adbSerial && androidDevice === adbSerial) {
|
|
152
|
+
if (adbSerial && (androidTargetDeviceName || androidDevice === adbSerial || !androidDevice)) {
|
|
141
153
|
const android = entries.filter((entry) => entry.platform === 'android');
|
|
154
|
+
if (androidTargetDeviceName) {
|
|
155
|
+
const matchesAndroidTargetNameEntry = (entry) => {
|
|
156
|
+
const deviceName = String(entry.deviceName ?? '');
|
|
157
|
+
return androidDeviceNameMatches(deviceName, androidTargetDeviceName);
|
|
158
|
+
};
|
|
159
|
+
const byName = android.filter(matchesAndroidTargetNameEntry);
|
|
160
|
+
if (byName.length === 1) return byName[0];
|
|
161
|
+
const byModelName = entries.filter(matchesAndroidTargetNameEntry);
|
|
162
|
+
if (byModelName.length === 1) return byModelName[0];
|
|
163
|
+
}
|
|
142
164
|
if (android.length === 1) return android[0];
|
|
143
165
|
if (android.length > 1) {
|
|
144
166
|
// Two android targets can't be told apart by serial here (Metro deviceNames
|
|
@@ -160,7 +182,7 @@ export function selectBridgeStatusEntry(status, input) {
|
|
|
160
182
|
}
|
|
161
183
|
if (androidDevice) {
|
|
162
184
|
const byName = entries.find(
|
|
163
|
-
(entry) =>
|
|
185
|
+
(entry) => androidDeviceNameMatches(String(entry.deviceName ?? ''), androidDevice),
|
|
164
186
|
);
|
|
165
187
|
if (byName) return byName;
|
|
166
188
|
}
|
|
@@ -41,30 +41,87 @@ async function status(input) {
|
|
|
41
41
|
return bridgeCommand(input, ['status']);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
async function waitForTargetStatus(input, timeoutMs = 20000) {
|
|
45
|
+
const deadline = Date.now() + timeoutMs;
|
|
46
|
+
let last = null;
|
|
47
|
+
let lastError = null;
|
|
48
|
+
while (Date.now() < deadline) {
|
|
49
|
+
try {
|
|
50
|
+
last = await status(input);
|
|
51
|
+
const selected = selectedStatus(last, input);
|
|
52
|
+
if (selected?.agenticPresent === true) return last;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
lastError = error;
|
|
55
|
+
}
|
|
56
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
57
|
+
}
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Timed out waiting for pinned Mobile agentic bridge target; last status was ${JSON.stringify(last)}; last error was ${String(lastError?.message ?? lastError)}`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
44
63
|
async function waitForUnlocked(input, timeoutMs = 15000) {
|
|
45
64
|
const deadline = Date.now() + timeoutMs;
|
|
46
65
|
let last = null;
|
|
66
|
+
let lastError = null;
|
|
67
|
+
while (Date.now() < deadline) {
|
|
68
|
+
try {
|
|
69
|
+
last = await status(input);
|
|
70
|
+
if (selectedAccount(last, input) && routeName(last, input) !== 'Login') return last;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
lastError = error;
|
|
73
|
+
}
|
|
74
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`Timed out waiting for Mobile wallet unlock; last status was ${JSON.stringify(last)}; last error was ${String(lastError?.message ?? lastError)}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isUnlockedStatus(status, input) {
|
|
80
|
+
return Boolean(selectedAccount(status, input)) && routeName(status, input) !== 'Login';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function waitForStableUnlocked(input, initialStatus, stableMs = 750) {
|
|
84
|
+
if (!isUnlockedStatus(initialStatus, input)) return null;
|
|
85
|
+
const deadline = Date.now() + stableMs;
|
|
86
|
+
let last = initialStatus;
|
|
87
|
+
let transientDrops = 0;
|
|
47
88
|
while (Date.now() < deadline) {
|
|
48
|
-
last = await status(input);
|
|
49
|
-
if (selectedAccount(last, input) && routeName(last, input) !== 'Login') return last;
|
|
50
89
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
90
|
+
try {
|
|
91
|
+
last = await status(input);
|
|
92
|
+
if (!isUnlockedStatus(last, input)) {
|
|
93
|
+
if (routeName(last, input) === 'Login') return null;
|
|
94
|
+
transientDrops += 1;
|
|
95
|
+
if (transientDrops > 2) return null;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
transientDrops = 0;
|
|
99
|
+
} catch {
|
|
100
|
+
// Recovery is correct: a bridge error during the stability window is the
|
|
101
|
+
// same signal as an unlocked-state drop — count it toward the transient
|
|
102
|
+
// threshold; exceeding it returns null so the caller runs the real
|
|
103
|
+
// password unlock instead of reporting a false alreadyUnlocked.
|
|
104
|
+
transientDrops += 1;
|
|
105
|
+
if (transientDrops > 2) return null;
|
|
106
|
+
}
|
|
51
107
|
}
|
|
52
|
-
|
|
108
|
+
return last;
|
|
53
109
|
}
|
|
54
110
|
|
|
55
111
|
runAdapter(async (input) => {
|
|
56
|
-
const before = await
|
|
112
|
+
const before = await waitForTargetStatus(input, Number(input.node?.target_timeout_ms ?? 20000));
|
|
57
113
|
if (!selectedAccount(before, input) && routeName(before, input) !== 'Login') {
|
|
58
114
|
throw new Error(
|
|
59
115
|
`No wallet onboarded on this device (route ${routeName(before, input) || 'unknown'}, empty wallet status) — unlock has nothing to unlock.\n Next: mm-harness fixtures set # applies the fixture wallet, then re-run: mm-harness call ensure_unlocked`,
|
|
60
116
|
);
|
|
61
117
|
}
|
|
62
|
-
|
|
118
|
+
const stableBefore = await waitForStableUnlocked(input, before, Number(input.node?.stable_unlocked_ms ?? 750));
|
|
119
|
+
if (stableBefore) {
|
|
63
120
|
return {
|
|
64
121
|
action: input.action,
|
|
65
122
|
unlocked: true,
|
|
66
123
|
alreadyUnlocked: true,
|
|
67
|
-
account: selectedAccount(
|
|
124
|
+
account: selectedAccount(stableBefore, input),
|
|
68
125
|
redacted: true,
|
|
69
126
|
proofPath: 'agentic-wallet-status',
|
|
70
127
|
};
|
|
@@ -74,11 +131,12 @@ runAdapter(async (input) => {
|
|
|
74
131
|
try {
|
|
75
132
|
const result = await bridgeCommand(input, ['unlock', String(password)]);
|
|
76
133
|
const after = await waitForUnlocked(input, Number(input.node?.unlock_timeout_ms ?? 15000));
|
|
134
|
+
const stableAfter = await waitForStableUnlocked(input, after, Number(input.node?.stable_unlocked_ms ?? 750)) ?? after;
|
|
77
135
|
return {
|
|
78
136
|
action: input.action,
|
|
79
137
|
unlocked: Boolean(result?.ok ?? result?.unlocked ?? true),
|
|
80
|
-
account: selectedAccount(
|
|
81
|
-
route: selectedStatus(
|
|
138
|
+
account: selectedAccount(stableAfter, input),
|
|
139
|
+
route: selectedStatus(stableAfter, input)?.route ?? null,
|
|
82
140
|
redacted: true,
|
|
83
141
|
proofPath: 'agentic-wallet-unlock',
|
|
84
142
|
};
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"ui.scroll",
|
|
20
20
|
"ui.wait_for",
|
|
21
21
|
"ui.screenshot",
|
|
22
|
+
"app.lifecycle",
|
|
22
23
|
"app.status",
|
|
23
24
|
"app.hud",
|
|
24
25
|
"cdp.target"
|
|
@@ -213,6 +214,19 @@
|
|
|
213
214
|
}
|
|
214
215
|
]
|
|
215
216
|
},
|
|
217
|
+
"app.lifecycle": {
|
|
218
|
+
"description": "Drive outer mobile app lifecycle for deterministic performance start states without rebuilding: foreground/launch, background, terminate, or restart the installed app.",
|
|
219
|
+
"examples": [
|
|
220
|
+
{
|
|
221
|
+
"node": {
|
|
222
|
+
"action": "app.lifecycle",
|
|
223
|
+
"command": "background",
|
|
224
|
+
"settle_ms": 1000,
|
|
225
|
+
"intent": "Send the selected Android device to the launcher before a warm-resume measurement"
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
},
|
|
216
230
|
"assert_file": {
|
|
217
231
|
"description": "Assert that a project file exists and optionally contains text.",
|
|
218
232
|
"examples": [
|
|
@@ -1999,6 +2013,10 @@
|
|
|
1999
2013
|
"action": "ui.screenshot",
|
|
2000
2014
|
"implementation": "@farmslot/recipe-harness/runtime/react-native-bridge"
|
|
2001
2015
|
},
|
|
2016
|
+
{
|
|
2017
|
+
"action": "app.lifecycle",
|
|
2018
|
+
"implementation": "@farmslot/recipe-harness/adapters/app-lifecycle"
|
|
2019
|
+
},
|
|
2002
2020
|
{
|
|
2003
2021
|
"action": "app.status",
|
|
2004
2022
|
"implementation": "@metamask/recipe-runner/platform-status"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"title": "MetaMask Mobile Android lifecycle smoke",
|
|
4
|
+
"description": "Isolated on-device proof for the standard outer app.lifecycle action on Android. Setup cold-relaunches the installed app through the Expo dev-client deep link without rebuilding and proves the React Native bridge is reachable. The main workflow then backgrounds the installed app, foregrounds it again, proves the bridge is reachable after resume, and captures a screenshot.",
|
|
5
|
+
"startState": {
|
|
6
|
+
"action": "app.status",
|
|
7
|
+
"intent": "Record the stable run start status after setup relaunch",
|
|
8
|
+
"detail": "Read-only start-state marker after setup has made the Android bridge reachable."
|
|
9
|
+
},
|
|
10
|
+
"validate": {
|
|
11
|
+
"workflow": {
|
|
12
|
+
"setup": [
|
|
13
|
+
{
|
|
14
|
+
"id": "terminate-before",
|
|
15
|
+
"action": "app.lifecycle",
|
|
16
|
+
"platform": "android",
|
|
17
|
+
"command": "terminate",
|
|
18
|
+
"settle_ms": 3000,
|
|
19
|
+
"intent": "Terminate the installed Android app before lifecycle smoke",
|
|
20
|
+
"detail": "Force-stops stale foreground instances that may have no React Native debug target."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "launch-before",
|
|
24
|
+
"action": "app.lifecycle",
|
|
25
|
+
"platform": "android",
|
|
26
|
+
"command": "launch",
|
|
27
|
+
"settle_ms": 15000,
|
|
28
|
+
"intent": "Launch the installed Android app through the Expo dev-client deep link from a cold process",
|
|
29
|
+
"detail": "Uses the selected adb serial, Metro port, and installed package; it does not rebuild."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "bridge-before",
|
|
33
|
+
"action": "cdp.target",
|
|
34
|
+
"require_reachable": true,
|
|
35
|
+
"intent": "Confirm the mobile React Native debug bridge is reachable after setup relaunch",
|
|
36
|
+
"detail": "This proves setup can recover a missing Android bridge target before the background-resume proof."
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"entry": "background-app",
|
|
40
|
+
"nodes": {
|
|
41
|
+
"background-app": {
|
|
42
|
+
"action": "app.lifecycle",
|
|
43
|
+
"platform": "android",
|
|
44
|
+
"command": "background",
|
|
45
|
+
"settle_ms": 1000,
|
|
46
|
+
"intent": "Put the selected Android device on the launcher",
|
|
47
|
+
"detail": "Uses adb HOME through the shared Farmslot lifecycle adapter.",
|
|
48
|
+
"flow": "lifecycle",
|
|
49
|
+
"next": "foreground-app"
|
|
50
|
+
},
|
|
51
|
+
"foreground-app": {
|
|
52
|
+
"action": "app.lifecycle",
|
|
53
|
+
"platform": "android",
|
|
54
|
+
"command": "foreground",
|
|
55
|
+
"settle_ms": 15000,
|
|
56
|
+
"intent": "Foreground the installed Android app through the Expo dev-client deep link",
|
|
57
|
+
"detail": "Uses the selected adb serial, Metro port, and installed package; it does not rebuild.",
|
|
58
|
+
"flow": "lifecycle",
|
|
59
|
+
"next": "bridge-after"
|
|
60
|
+
},
|
|
61
|
+
"bridge-after": {
|
|
62
|
+
"action": "cdp.target",
|
|
63
|
+
"require_reachable": true,
|
|
64
|
+
"intent": "Confirm the mobile React Native debug bridge recovered after foregrounding",
|
|
65
|
+
"detail": "This is the capability gate before running Perps recipes that depend on CDP/bridge control.",
|
|
66
|
+
"flow": "lifecycle",
|
|
67
|
+
"next": "screenshot-after"
|
|
68
|
+
},
|
|
69
|
+
"screenshot-after": {
|
|
70
|
+
"action": "ui.screenshot",
|
|
71
|
+
"path": "screenshots/app-lifecycle-android-smoke.png",
|
|
72
|
+
"intent": "Capture the app after Android lifecycle foreground",
|
|
73
|
+
"detail": "Visual proof that the selected device is displaying the relaunched app.",
|
|
74
|
+
"flow": "evidence",
|
|
75
|
+
"next": "end"
|
|
76
|
+
},
|
|
77
|
+
"end": {
|
|
78
|
+
"action": "end",
|
|
79
|
+
"status": "pass",
|
|
80
|
+
"intent": "Finish the isolated Android lifecycle smoke",
|
|
81
|
+
"detail": "If this passes, lifecycle is proven independently of wallet and Perps actions.",
|
|
82
|
+
"flow": "complete"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"title": "MetaMask Mobile Perps performance flow - background resume",
|
|
4
|
+
"description": "Measured Perps flow from a deterministic background-resume start state. Setup sends the installed app to the background, foregrounds it through the Expo dev-client deep link without rebuilding, and unlocks if needed. startState records the stable status. The measured workflow is only Perps list -> state -> BTC detail.",
|
|
5
|
+
"startState": {
|
|
6
|
+
"action": "app.status",
|
|
7
|
+
"intent": "Record the stable run start status after background-resume setup",
|
|
8
|
+
"detail": "Read-only start-state marker after lifecycle and wallet setup have completed."
|
|
9
|
+
},
|
|
10
|
+
"validate": {
|
|
11
|
+
"workflow": {
|
|
12
|
+
"setup": [
|
|
13
|
+
{
|
|
14
|
+
"id": "background-app",
|
|
15
|
+
"action": "app.lifecycle",
|
|
16
|
+
"command": "background",
|
|
17
|
+
"settle_ms": 3000,
|
|
18
|
+
"intent": "Put the installed app in the background before measuring resume",
|
|
19
|
+
"detail": "On Android this sends HOME to the selected device. On iOS simulator this foregrounds Settings through the shared outer adapter."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "foreground-app",
|
|
23
|
+
"action": "app.lifecycle",
|
|
24
|
+
"command": "foreground",
|
|
25
|
+
"settle_ms": 15000,
|
|
26
|
+
"intent": "Foreground the installed app through the Expo dev-client deep link",
|
|
27
|
+
"detail": "Uses the selected device, Metro port, and installed package; it does not rebuild."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "ensure-unlocked",
|
|
31
|
+
"action": "metamask.wallet.ensure_unlocked",
|
|
32
|
+
"intent": "Unlock the wallet before timing the Perps journey",
|
|
33
|
+
"detail": "Unlock only if the app is currently locked so the first measured node starts from a stable, signed-in state."
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"entry": "open-perps-list",
|
|
37
|
+
"nodes": {
|
|
38
|
+
"open-perps-list": {
|
|
39
|
+
"action": "ui.navigate",
|
|
40
|
+
"page": "perps",
|
|
41
|
+
"intent": "Open the Perps market list screen",
|
|
42
|
+
"detail": "Navigate to the Perps markets list via the stable page alias; its node duration measures market-list render time.",
|
|
43
|
+
"flow": "perps",
|
|
44
|
+
"next": "read-positions"
|
|
45
|
+
},
|
|
46
|
+
"read-positions": {
|
|
47
|
+
"action": "metamask.perps.read_positions",
|
|
48
|
+
"intent": "Read the live Perps positions shown on the market list",
|
|
49
|
+
"detail": "Read live Perps positions through the controller; its node duration measures how long live account state takes to resolve.",
|
|
50
|
+
"flow": "perps",
|
|
51
|
+
"next": "open-market-detail"
|
|
52
|
+
},
|
|
53
|
+
"open-market-detail": {
|
|
54
|
+
"action": "ui.navigate",
|
|
55
|
+
"page": "perps-market",
|
|
56
|
+
"market": "BTC",
|
|
57
|
+
"intent": "Open the BTC Perps market detail screen",
|
|
58
|
+
"detail": "Navigate to the BTC market detail via the stable page alias; its node duration measures market-detail render time.",
|
|
59
|
+
"flow": "perps",
|
|
60
|
+
"next": "end"
|
|
61
|
+
},
|
|
62
|
+
"end": {
|
|
63
|
+
"action": "end",
|
|
64
|
+
"status": "pass",
|
|
65
|
+
"intent": "Finish the background-resume Perps performance flow",
|
|
66
|
+
"detail": "Terminal node; trace.json holds setup plus measured Perps node durations.",
|
|
67
|
+
"flow": "complete"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"title": "MetaMask Mobile Perps performance flow - cold process start",
|
|
4
|
+
"description": "Measured Perps flow from a deterministic cold-process start state. Setup force-stops/terminates the installed app, relaunches it through the Expo dev-client deep link without rebuilding, and unlocks if needed. startState records the stable status. The measured workflow is only Perps list -> state -> BTC detail.",
|
|
5
|
+
"startState": {
|
|
6
|
+
"action": "app.status",
|
|
7
|
+
"intent": "Record the stable run start status after cold-start setup",
|
|
8
|
+
"detail": "Read-only start-state marker after lifecycle and wallet setup have completed."
|
|
9
|
+
},
|
|
10
|
+
"validate": {
|
|
11
|
+
"workflow": {
|
|
12
|
+
"setup": [
|
|
13
|
+
{
|
|
14
|
+
"id": "terminate-app",
|
|
15
|
+
"action": "app.lifecycle",
|
|
16
|
+
"command": "terminate",
|
|
17
|
+
"settle_ms": 3000,
|
|
18
|
+
"intent": "Terminate the installed app process before measuring cold start",
|
|
19
|
+
"detail": "Uses adb force-stop on Android or simctl terminate on iOS simulator; it does not uninstall or rebuild."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "launch-app",
|
|
23
|
+
"action": "app.lifecycle",
|
|
24
|
+
"command": "launch",
|
|
25
|
+
"settle_ms": 15000,
|
|
26
|
+
"intent": "Launch the installed app through the Expo dev-client deep link",
|
|
27
|
+
"detail": "Uses the selected device, Metro port, and installed package; it does not rebuild."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "ensure-unlocked",
|
|
31
|
+
"action": "metamask.wallet.ensure_unlocked",
|
|
32
|
+
"intent": "Unlock the wallet before timing the Perps journey",
|
|
33
|
+
"detail": "Unlock only if the app is currently locked so the first measured node starts from a stable, signed-in state."
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"entry": "open-perps-list",
|
|
37
|
+
"nodes": {
|
|
38
|
+
"open-perps-list": {
|
|
39
|
+
"action": "ui.navigate",
|
|
40
|
+
"page": "perps",
|
|
41
|
+
"intent": "Open the Perps market list screen",
|
|
42
|
+
"detail": "Navigate to the Perps markets list via the stable page alias; its node duration measures market-list render time.",
|
|
43
|
+
"flow": "perps",
|
|
44
|
+
"next": "read-positions"
|
|
45
|
+
},
|
|
46
|
+
"read-positions": {
|
|
47
|
+
"action": "metamask.perps.read_positions",
|
|
48
|
+
"intent": "Read the live Perps positions shown on the market list",
|
|
49
|
+
"detail": "Read live Perps positions through the controller; its node duration measures how long live account state takes to resolve.",
|
|
50
|
+
"flow": "perps",
|
|
51
|
+
"next": "open-market-detail"
|
|
52
|
+
},
|
|
53
|
+
"open-market-detail": {
|
|
54
|
+
"action": "ui.navigate",
|
|
55
|
+
"page": "perps-market",
|
|
56
|
+
"market": "BTC",
|
|
57
|
+
"intent": "Open the BTC Perps market detail screen",
|
|
58
|
+
"detail": "Navigate to the BTC market detail via the stable page alias; its node duration measures market-detail render time.",
|
|
59
|
+
"flow": "perps",
|
|
60
|
+
"next": "end"
|
|
61
|
+
},
|
|
62
|
+
"end": {
|
|
63
|
+
"action": "end",
|
|
64
|
+
"status": "pass",
|
|
65
|
+
"intent": "Finish the cold-start Perps performance flow",
|
|
66
|
+
"detail": "Terminal node; trace.json holds setup plus measured Perps node durations.",
|
|
67
|
+
"flow": "complete"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"title": "MetaMask Mobile Perps performance flow - warm foreground start",
|
|
4
|
+
"description": "Measured Perps flow from a deterministic warm-foreground start state. Setup foregrounds the already installed app through the Expo dev-client deep link without rebuilding and unlocks if needed. startState records the stable status. The measured workflow is only Perps list -> state -> BTC detail.",
|
|
5
|
+
"startState": {
|
|
6
|
+
"action": "app.status",
|
|
7
|
+
"intent": "Record the stable run start status after warm foreground setup",
|
|
8
|
+
"detail": "Read-only start-state marker after lifecycle and wallet setup have completed."
|
|
9
|
+
},
|
|
10
|
+
"validate": {
|
|
11
|
+
"workflow": {
|
|
12
|
+
"setup": [
|
|
13
|
+
{
|
|
14
|
+
"id": "foreground-app",
|
|
15
|
+
"action": "app.lifecycle",
|
|
16
|
+
"command": "foreground",
|
|
17
|
+
"settle_ms": 10000,
|
|
18
|
+
"intent": "Foreground the installed app through the Expo dev-client deep link",
|
|
19
|
+
"detail": "Uses the selected device, Metro port, and installed package; it does not rebuild."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "ensure-unlocked",
|
|
23
|
+
"action": "metamask.wallet.ensure_unlocked",
|
|
24
|
+
"intent": "Unlock the wallet before timing the Perps journey",
|
|
25
|
+
"detail": "Unlock only if the app is currently locked so the first measured node starts from a stable, signed-in state."
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"entry": "open-perps-list",
|
|
29
|
+
"nodes": {
|
|
30
|
+
"open-perps-list": {
|
|
31
|
+
"action": "ui.navigate",
|
|
32
|
+
"page": "perps",
|
|
33
|
+
"intent": "Open the Perps market list screen",
|
|
34
|
+
"detail": "Navigate to the Perps markets list via the stable page alias; its node duration measures market-list render time.",
|
|
35
|
+
"flow": "perps",
|
|
36
|
+
"next": "read-positions"
|
|
37
|
+
},
|
|
38
|
+
"read-positions": {
|
|
39
|
+
"action": "metamask.perps.read_positions",
|
|
40
|
+
"intent": "Read the live Perps positions shown on the market list",
|
|
41
|
+
"detail": "Read live Perps positions through the controller; its node duration measures how long live account state takes to resolve.",
|
|
42
|
+
"flow": "perps",
|
|
43
|
+
"next": "open-market-detail"
|
|
44
|
+
},
|
|
45
|
+
"open-market-detail": {
|
|
46
|
+
"action": "ui.navigate",
|
|
47
|
+
"page": "perps-market",
|
|
48
|
+
"market": "BTC",
|
|
49
|
+
"intent": "Open the BTC Perps market detail screen",
|
|
50
|
+
"detail": "Navigate to the BTC market detail via the stable page alias; its node duration measures market-detail render time.",
|
|
51
|
+
"flow": "perps",
|
|
52
|
+
"next": "end"
|
|
53
|
+
},
|
|
54
|
+
"end": {
|
|
55
|
+
"action": "end",
|
|
56
|
+
"status": "pass",
|
|
57
|
+
"intent": "Finish the warm-start Perps performance flow",
|
|
58
|
+
"detail": "Terminal node; trace.json holds setup plus measured Perps node durations.",
|
|
59
|
+
"flow": "complete"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|