@deeeed/metamask-harness 0.9.1 → 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 +30 -0
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +24 -16
- package/adapters/mobile/start-metro.sh +80 -36
- package/adapters/mobile/wait-for-bridge.sh +15 -1
- package/dist/adapters/extension/runtime.js +2 -3
- 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 +39 -9
- package/dist/commands/fixtures.js +13 -1
- package/dist/commands/launch/index.js +35 -7
- package/dist/commands/run-engine.js +40 -12
- package/dist/commands/run.js +1 -2
- package/dist/live-adapter-contract.js +2 -3
- 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 +203 -0
- package/library/actions/mobile/platform/bridge.mjs +91 -4
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +69 -32
- package/library/actions/mobile/wallet/read_state.mjs +3 -24
- package/library/actions/mobile/wallet/setup.mjs +9 -27
- package/library/manifests/extension.action-manifest.json +13 -0
- package/library/manifests/mobile.action-manifest.json +31 -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 +56 -0
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { bridgeCommand, runAdapter } from '../platform/bridge.mjs';
|
|
2
|
+
import { bridgeCommand, runAdapter, selectBridgeStatusEntry } from '../platform/bridge.mjs';
|
|
3
3
|
import { walletFixturePath } from '../../harness-exports.mjs';
|
|
4
4
|
|
|
5
5
|
async function fixturePassword(projectRoot) {
|
|
@@ -34,58 +34,94 @@ function routeName(status, input) {
|
|
|
34
34
|
return route && typeof route === 'object' ? String(route.name ?? '') : '';
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return status && typeof status === 'object' ? status : null;
|
|
40
|
-
}
|
|
41
|
-
const preferredDevices = [
|
|
42
|
-
input.node?.ios_simulator,
|
|
43
|
-
input.node?.simulator,
|
|
44
|
-
input.node?.android_device,
|
|
45
|
-
input.node?.adb_serial,
|
|
46
|
-
process.env.IOS_SIMULATOR,
|
|
47
|
-
process.env.ANDROID_DEVICE,
|
|
48
|
-
process.env.ADB_SERIAL,
|
|
49
|
-
].filter((value) => typeof value === 'string' && value.length > 0);
|
|
50
|
-
for (const preferredDevice of preferredDevices) {
|
|
51
|
-
const match = status.find((entry) => entry?.deviceName === preferredDevice);
|
|
52
|
-
if (match) return match;
|
|
53
|
-
}
|
|
54
|
-
return (
|
|
55
|
-
status.find((entry) => entry?.account) ??
|
|
56
|
-
status.find((entry) => entry && typeof entry === 'object') ??
|
|
57
|
-
null
|
|
58
|
-
);
|
|
59
|
-
}
|
|
37
|
+
// selectedStatus: shared pin-aware entry selection (see selectBridgeStatusEntry).
|
|
38
|
+
const selectedStatus = selectBridgeStatusEntry;
|
|
60
39
|
|
|
61
40
|
async function status(input) {
|
|
62
41
|
return bridgeCommand(input, ['status']);
|
|
63
42
|
}
|
|
64
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
|
+
|
|
65
63
|
async function waitForUnlocked(input, timeoutMs = 15000) {
|
|
66
64
|
const deadline = Date.now() + timeoutMs;
|
|
67
65
|
let last = null;
|
|
66
|
+
let lastError = null;
|
|
68
67
|
while (Date.now() < deadline) {
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
}
|
|
71
74
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
72
75
|
}
|
|
73
|
-
throw new Error(`Timed out waiting for Mobile wallet unlock; last status was ${JSON.stringify(last)}`);
|
|
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;
|
|
88
|
+
while (Date.now() < deadline) {
|
|
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
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return last;
|
|
74
109
|
}
|
|
75
110
|
|
|
76
111
|
runAdapter(async (input) => {
|
|
77
|
-
const before = await
|
|
112
|
+
const before = await waitForTargetStatus(input, Number(input.node?.target_timeout_ms ?? 20000));
|
|
78
113
|
if (!selectedAccount(before, input) && routeName(before, input) !== 'Login') {
|
|
79
114
|
throw new Error(
|
|
80
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`,
|
|
81
116
|
);
|
|
82
117
|
}
|
|
83
|
-
|
|
118
|
+
const stableBefore = await waitForStableUnlocked(input, before, Number(input.node?.stable_unlocked_ms ?? 750));
|
|
119
|
+
if (stableBefore) {
|
|
84
120
|
return {
|
|
85
121
|
action: input.action,
|
|
86
122
|
unlocked: true,
|
|
87
123
|
alreadyUnlocked: true,
|
|
88
|
-
account: selectedAccount(
|
|
124
|
+
account: selectedAccount(stableBefore, input),
|
|
89
125
|
redacted: true,
|
|
90
126
|
proofPath: 'agentic-wallet-status',
|
|
91
127
|
};
|
|
@@ -95,11 +131,12 @@ runAdapter(async (input) => {
|
|
|
95
131
|
try {
|
|
96
132
|
const result = await bridgeCommand(input, ['unlock', String(password)]);
|
|
97
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;
|
|
98
135
|
return {
|
|
99
136
|
action: input.action,
|
|
100
137
|
unlocked: Boolean(result?.ok ?? result?.unlocked ?? true),
|
|
101
|
-
account: selectedAccount(
|
|
102
|
-
route: selectedStatus(
|
|
138
|
+
account: selectedAccount(stableAfter, input),
|
|
139
|
+
route: selectedStatus(stableAfter, input)?.route ?? null,
|
|
103
140
|
redacted: true,
|
|
104
141
|
proofPath: 'agentic-wallet-unlock',
|
|
105
142
|
};
|
|
@@ -1,28 +1,7 @@
|
|
|
1
|
-
import { bridgeCommand, runAdapter } from '../platform/bridge.mjs';
|
|
1
|
+
import { bridgeCommand, runAdapter, selectBridgeStatusEntry } from '../platform/bridge.mjs';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
return status && typeof status === 'object' ? status : null;
|
|
6
|
-
}
|
|
7
|
-
const preferredDevices = [
|
|
8
|
-
input.node?.ios_simulator,
|
|
9
|
-
input.node?.simulator,
|
|
10
|
-
input.node?.android_device,
|
|
11
|
-
input.node?.adb_serial,
|
|
12
|
-
process.env.IOS_SIMULATOR,
|
|
13
|
-
process.env.ANDROID_DEVICE,
|
|
14
|
-
process.env.ADB_SERIAL,
|
|
15
|
-
].filter((value) => typeof value === 'string' && value.length > 0);
|
|
16
|
-
for (const preferredDevice of preferredDevices) {
|
|
17
|
-
const match = status.find((entry) => entry?.deviceName === preferredDevice);
|
|
18
|
-
if (match) return match;
|
|
19
|
-
}
|
|
20
|
-
return (
|
|
21
|
-
status.find((entry) => entry?.account) ??
|
|
22
|
-
status.find((entry) => entry && typeof entry === 'object') ??
|
|
23
|
-
null
|
|
24
|
-
);
|
|
25
|
-
}
|
|
3
|
+
// selectedStatus: shared pin-aware entry selection (see selectBridgeStatusEntry).
|
|
4
|
+
const selectedStatus = selectBridgeStatusEntry;
|
|
26
5
|
|
|
27
6
|
runAdapter(async (input) => {
|
|
28
7
|
const status = selectedStatus(await bridgeCommand(input, ['status']), input);
|
|
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { bridgeCommand, bridgeEnv, runAdapter } from '../platform/bridge.mjs';
|
|
5
|
+
import { bridgeCommand, bridgeEnv, runAdapter, selectBridgeStatusEntry } from '../platform/bridge.mjs';
|
|
6
6
|
import { walletFixturePath } from '../../harness-exports.mjs';
|
|
7
7
|
|
|
8
8
|
async function fixtureProfile(projectRoot) {
|
|
@@ -77,29 +77,8 @@ function mnemonicAccountCount(account, fixturePath, index) {
|
|
|
77
77
|
return count;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return status && typeof status === 'object' ? status : null;
|
|
83
|
-
}
|
|
84
|
-
const preferredDevices = [
|
|
85
|
-
input.node?.ios_simulator,
|
|
86
|
-
input.node?.simulator,
|
|
87
|
-
input.node?.android_device,
|
|
88
|
-
input.node?.adb_serial,
|
|
89
|
-
process.env.IOS_SIMULATOR,
|
|
90
|
-
process.env.ANDROID_DEVICE,
|
|
91
|
-
process.env.ADB_SERIAL,
|
|
92
|
-
].filter((value) => typeof value === 'string' && value.length > 0);
|
|
93
|
-
for (const preferredDevice of preferredDevices) {
|
|
94
|
-
const match = status.find((entry) => entry?.deviceName === preferredDevice);
|
|
95
|
-
if (match) return match;
|
|
96
|
-
}
|
|
97
|
-
return (
|
|
98
|
-
status.find((entry) => entry?.account) ??
|
|
99
|
-
status.find((entry) => entry && typeof entry === 'object') ??
|
|
100
|
-
null
|
|
101
|
-
);
|
|
102
|
-
}
|
|
80
|
+
// selectedStatus: shared pin-aware entry selection (see selectBridgeStatusEntry).
|
|
81
|
+
const selectedStatus = selectBridgeStatusEntry;
|
|
103
82
|
|
|
104
83
|
function hasSelectedAccount(status, input) {
|
|
105
84
|
return Boolean(selectedStatus(status, input)?.account);
|
|
@@ -138,9 +117,12 @@ async function runSetupWallet(input, fixture) {
|
|
|
138
117
|
if (settled) return;
|
|
139
118
|
settled = true;
|
|
140
119
|
child.kill('SIGTERM');
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
120
|
+
// Always escalate after the grace period: child.killed only records
|
|
121
|
+
// that kill() was CALLED, not that the process exited, so gating
|
|
122
|
+
// SIGKILL on it never fires and a SIGTERM-ignoring child leaks. The
|
|
123
|
+
// close listener cancels the escalation when the child exits in time.
|
|
124
|
+
const killTimer = setTimeout(() => child.kill('SIGKILL'), 1000);
|
|
125
|
+
child.once('close', () => clearTimeout(killTimer));
|
|
144
126
|
reject(
|
|
145
127
|
new Error(
|
|
146
128
|
`Mobile setup-wallet.sh timed out after ${timeoutMs}ms.`,
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"watch_logs",
|
|
12
12
|
"index_artifacts",
|
|
13
13
|
"end",
|
|
14
|
+
"call",
|
|
14
15
|
"ui.navigate",
|
|
15
16
|
"ui.press",
|
|
16
17
|
"ui.key_press",
|
|
@@ -724,6 +725,18 @@
|
|
|
724
725
|
}
|
|
725
726
|
}
|
|
726
727
|
]
|
|
728
|
+
},
|
|
729
|
+
"call": {
|
|
730
|
+
"description": "Invoke a named sub-recipe flow by ref and run its steps inline.",
|
|
731
|
+
"examples": [
|
|
732
|
+
{
|
|
733
|
+
"node": {
|
|
734
|
+
"action": "call",
|
|
735
|
+
"ref": "mydev.open_perps_setup",
|
|
736
|
+
"intent": "Run a personal Perps setup flow segment"
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
]
|
|
727
740
|
}
|
|
728
741
|
},
|
|
729
742
|
"custom_actions": [
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"watch_logs",
|
|
12
12
|
"index_artifacts",
|
|
13
13
|
"end",
|
|
14
|
+
"call",
|
|
14
15
|
"ui.navigate",
|
|
15
16
|
"ui.press",
|
|
16
17
|
"ui.key_press",
|
|
@@ -18,6 +19,7 @@
|
|
|
18
19
|
"ui.scroll",
|
|
19
20
|
"ui.wait_for",
|
|
20
21
|
"ui.screenshot",
|
|
22
|
+
"app.lifecycle",
|
|
21
23
|
"app.status",
|
|
22
24
|
"app.hud",
|
|
23
25
|
"cdp.target"
|
|
@@ -212,6 +214,19 @@
|
|
|
212
214
|
}
|
|
213
215
|
]
|
|
214
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
|
+
},
|
|
215
230
|
"assert_file": {
|
|
216
231
|
"description": "Assert that a project file exists and optionally contains text.",
|
|
217
232
|
"examples": [
|
|
@@ -724,6 +739,18 @@
|
|
|
724
739
|
}
|
|
725
740
|
}
|
|
726
741
|
]
|
|
742
|
+
},
|
|
743
|
+
"call": {
|
|
744
|
+
"description": "Invoke a named sub-recipe flow by ref and run its steps inline.",
|
|
745
|
+
"examples": [
|
|
746
|
+
{
|
|
747
|
+
"node": {
|
|
748
|
+
"action": "call",
|
|
749
|
+
"ref": "mydev.open_perps_setup",
|
|
750
|
+
"intent": "Run a personal Perps setup flow segment"
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
]
|
|
727
754
|
}
|
|
728
755
|
},
|
|
729
756
|
"custom_actions": [
|
|
@@ -1986,6 +2013,10 @@
|
|
|
1986
2013
|
"action": "ui.screenshot",
|
|
1987
2014
|
"implementation": "@farmslot/recipe-harness/runtime/react-native-bridge"
|
|
1988
2015
|
},
|
|
2016
|
+
{
|
|
2017
|
+
"action": "app.lifecycle",
|
|
2018
|
+
"implementation": "@farmslot/recipe-harness/adapters/app-lifecycle"
|
|
2019
|
+
},
|
|
1989
2020
|
{
|
|
1990
2021
|
"action": "app.status",
|
|
1991
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
|
+
}
|