@deeeed/metamask-harness 0.40.0 → 0.41.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 +22 -0
- package/adapters/manifest.json +8 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +5 -4
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +24 -5
- package/adapters/mobile/metro-config.cjs +94 -0
- package/adapters/mobile/start-metro.sh +13 -1
- package/dist/commands/device-target.js +3 -0
- package/dist/commands/run-engine.js +4 -14
- package/dist/funding-execution-context.js +35 -28
- package/dist/heal-bounds.js +1 -0
- package/dist/live-adapter-contract.js +2 -1
- package/dist/mm-harness-cli.js +1 -1
- package/docs/RECIPES.md +25 -19
- package/library/actions/extension/perps/mutation-receipt.mjs +12 -8
- package/library/actions/extension/perps/select_activity_filter.mjs +1 -1
- package/library/actions/extension/perps/select_market_filter.mjs +1 -1
- package/library/actions/mobile/perps/capture_performance.mjs +2 -2
- package/library/actions/mobile/perps/performance-capture.mjs +375 -17
- package/library/actions/mobile/perps/perps.mjs +56 -10
- package/library/actions/mobile/platform/bridge.mjs +59 -1
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +60 -26
- package/library/actions/mobile/wallet/import.mjs +2 -0
- package/library/actions/mobile/wallet/select_account.mjs +21 -2
- package/library/manifests/mobile.action-manifest.json +47 -6
- package/library/recipes/mobile/perps/performance.recipe.json +680 -26
- package/package.json +1 -1
- package/library/recipes/mobile/perps/performance.background-resume.recipe.json +0 -56
- package/library/recipes/mobile/perps/performance.cold-start.recipe.json +0 -56
- package/library/recipes/mobile/perps/performance.homepage.android-background-reconnect.recipe.json +0 -159
- package/library/recipes/mobile/perps/performance.homepage.android-background-short.recipe.json +0 -157
- package/library/recipes/mobile/perps/performance.homepage.android-cold-disk-cache.recipe.json +0 -165
- package/library/recipes/mobile/perps/performance.homepage.android-cold-no-cache.recipe.json +0 -139
- package/library/recipes/mobile/perps/performance.homepage.android-network-recovery.recipe.json +0 -149
- package/library/recipes/mobile/perps/performance.homepage.cold-position-sample.recipe.json +0 -120
- package/library/recipes/mobile/perps/performance.homepage.ios-background-reconnect.recipe.json +0 -111
- package/library/recipes/mobile/perps/performance.homepage.ios-background-short.recipe.json +0 -108
- package/library/recipes/mobile/perps/performance.homepage.ios-cold-disk-cache.recipe.json +0 -122
- package/library/recipes/mobile/perps/performance.homepage.ios-cold-no-cache.recipe.json +0 -95
- package/library/recipes/mobile/perps/performance.warm-start.recipe.json +0 -49
|
@@ -145,6 +145,7 @@ export async function bridgeEnv(input) {
|
|
|
145
145
|
if (adbPath) env.MM_HARNESS_ADB_PATH = adbPath;
|
|
146
146
|
if (idbPath) env.MM_HARNESS_IDB_PATH = idbPath;
|
|
147
147
|
const target = resolveMobileTarget(input);
|
|
148
|
+
const requestedPlatform = target.requestedPlatform;
|
|
148
149
|
const watcherPort = target.watcherPort;
|
|
149
150
|
const simulator = target.iosSimulator;
|
|
150
151
|
const androidDevice = target.androidDevice;
|
|
@@ -160,6 +161,12 @@ export async function bridgeEnv(input) {
|
|
|
160
161
|
env.ADB_SERIAL = String(adbSerial);
|
|
161
162
|
env.ANDROID_SERIAL = String(adbSerial);
|
|
162
163
|
}
|
|
164
|
+
if (requestedPlatform === 'ios') {
|
|
165
|
+
env.ADB_SERIAL = '';
|
|
166
|
+
env.ANDROID_SERIAL = '';
|
|
167
|
+
env.ANDROID_DEVICE = '';
|
|
168
|
+
env.ANDROID_TARGET_DEVICE_NAME = '';
|
|
169
|
+
}
|
|
163
170
|
// When --device passes an adb serial, device-target.ts sets both ANDROID_DEVICE and
|
|
164
171
|
// ADB_SERIAL to the same value. ANDROID_DEVICE must be the Metro deviceName for
|
|
165
172
|
// target-discovery to select the right CDP target; it cannot be a raw adb serial.
|
|
@@ -210,12 +217,55 @@ export function shouldResolveAndroidTargetModel(
|
|
|
210
217
|
|
|
211
218
|
function resolveMobileTarget(input) {
|
|
212
219
|
const contextEnv = input.context?.env || {};
|
|
220
|
+
const explicitSimulatorUdid =
|
|
221
|
+
input.node?.sim_udid ?? contextEnv.SIM_UDID ?? process.env.SIM_UDID;
|
|
222
|
+
const configuredPlatform = String(
|
|
223
|
+
input.node?.platform ??
|
|
224
|
+
contextEnv.MM_HARNESS_EXPLICIT_PLATFORM ??
|
|
225
|
+
process.env.MM_HARNESS_EXPLICIT_PLATFORM ??
|
|
226
|
+
contextEnv.PLATFORM ??
|
|
227
|
+
process.env.PLATFORM ??
|
|
228
|
+
'',
|
|
229
|
+
).toLowerCase();
|
|
230
|
+
const requestedPlatform =
|
|
231
|
+
configuredPlatform === 'ios' || configuredPlatform === 'android'
|
|
232
|
+
? configuredPlatform
|
|
233
|
+
: explicitSimulatorUdid
|
|
234
|
+
? 'ios'
|
|
235
|
+
: configuredPlatform;
|
|
213
236
|
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;
|
|
214
237
|
const iosSimulator = input.node?.simulator ?? input.node?.ios_simulator ?? contextEnv.IOS_SIMULATOR ?? process.env.IOS_SIMULATOR;
|
|
215
238
|
const androidDevice = input.node?.android_device ?? contextEnv.ANDROID_DEVICE ?? process.env.ANDROID_DEVICE;
|
|
216
239
|
const androidTargetDeviceName = input.node?.android_target_device_name ?? input.node?.androidTargetDeviceName ?? contextEnv.ANDROID_TARGET_DEVICE_NAME ?? process.env.ANDROID_TARGET_DEVICE_NAME;
|
|
217
240
|
const adbSerial = input.node?.adb_serial ?? contextEnv.ADB_SERIAL ?? contextEnv.ANDROID_SERIAL ?? process.env.ADB_SERIAL ?? process.env.ANDROID_SERIAL ?? androidDevice;
|
|
218
|
-
|
|
241
|
+
if (requestedPlatform === 'ios') {
|
|
242
|
+
return {
|
|
243
|
+
watcherPort,
|
|
244
|
+
iosSimulator,
|
|
245
|
+
androidDevice: undefined,
|
|
246
|
+
androidTargetDeviceName: undefined,
|
|
247
|
+
adbSerial: undefined,
|
|
248
|
+
requestedPlatform,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
if (requestedPlatform === 'android') {
|
|
252
|
+
return {
|
|
253
|
+
watcherPort,
|
|
254
|
+
iosSimulator: undefined,
|
|
255
|
+
androidDevice,
|
|
256
|
+
androidTargetDeviceName,
|
|
257
|
+
adbSerial,
|
|
258
|
+
requestedPlatform,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
watcherPort,
|
|
263
|
+
iosSimulator,
|
|
264
|
+
androidDevice,
|
|
265
|
+
androidTargetDeviceName,
|
|
266
|
+
adbSerial,
|
|
267
|
+
requestedPlatform,
|
|
268
|
+
};
|
|
219
269
|
}
|
|
220
270
|
|
|
221
271
|
export async function openExternalDeeplink(input, url, timeout = 15000) {
|
|
@@ -353,6 +403,14 @@ export function selectBridgeStatusEntry(status, input) {
|
|
|
353
403
|
if (iosSimulator) {
|
|
354
404
|
const sim = entries.find((entry) => entry.deviceName === iosSimulator);
|
|
355
405
|
if (sim) return sim;
|
|
406
|
+
if (target.requestedPlatform === 'ios') {
|
|
407
|
+
throw coded(new Error(
|
|
408
|
+
`Pinned iOS simulator ${iosSimulator} has no responding bridge status entry.\n` +
|
|
409
|
+
` Bridge status entries:\n` +
|
|
410
|
+
entries.map((entry) => ` - ${entry.deviceName ?? '?'} [${entry.platform || 'unknown'}]`).join('\n') +
|
|
411
|
+
`\n Next: mm-harness launch ios # bring the pinned app back to the foreground`,
|
|
412
|
+
), BRIDGE_ERROR_CODES.NO_TARGET);
|
|
413
|
+
}
|
|
356
414
|
}
|
|
357
415
|
if (androidDevice) {
|
|
358
416
|
const byName = entries.find(
|
|
@@ -56,7 +56,8 @@ async function statusBeforeDeadline(input, deadline) {
|
|
|
56
56
|
30000,
|
|
57
57
|
);
|
|
58
58
|
const probeTimeoutMs = Math.min(
|
|
59
|
-
|
|
59
|
+
15000,
|
|
60
|
+
Math.max(5000, Math.floor(remainingMs / 3)),
|
|
60
61
|
remainingMs,
|
|
61
62
|
Number.isFinite(configuredMs) && configuredMs > 0 ? configuredMs : 30000,
|
|
62
63
|
);
|
|
@@ -153,7 +154,7 @@ async function waitForStableUnlocked(input, initialStatus, stableMs = 750) {
|
|
|
153
154
|
if (transientDrops > 2) return null;
|
|
154
155
|
}
|
|
155
156
|
}
|
|
156
|
-
return last;
|
|
157
|
+
return isUnlockedStatus(last, input) ? last : null;
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
export async function ensureUnlocked(input) {
|
|
@@ -188,37 +189,70 @@ export async function ensureUnlocked(input) {
|
|
|
188
189
|
unlocked: true,
|
|
189
190
|
alreadyUnlocked: true,
|
|
190
191
|
account: selectedAccount(stableBefore, input),
|
|
192
|
+
deviceName: selectedStatus(stableBefore, input)?.deviceName ?? null,
|
|
193
|
+
platform: selectedStatus(stableBefore, input)?.platform ?? null,
|
|
191
194
|
redacted: true,
|
|
192
195
|
proofPath: 'agentic-wallet-status',
|
|
193
196
|
};
|
|
194
197
|
}
|
|
195
198
|
|
|
196
199
|
const password = input.node?.password ?? await fixturePassword(input.context.projectRoot);
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
200
|
+
const unlockTimeoutMs = Number(input.node?.unlock_timeout_ms ?? 30000);
|
|
201
|
+
const unlockDeadline = Date.now() + unlockTimeoutMs;
|
|
202
|
+
let result;
|
|
203
|
+
while (result === undefined && Date.now() < unlockDeadline) {
|
|
204
|
+
try {
|
|
205
|
+
result = await bridgeCommand(input, ['unlock', String(password)]);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (!String(error?.message ?? error).includes('login-password-input not found')) {
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
const current = await statusBeforeDeadline(input, unlockDeadline);
|
|
212
|
+
if (selectedAccount(current, input) && routeName(current, input) !== 'Login') {
|
|
213
|
+
return {
|
|
214
|
+
action: input.action,
|
|
215
|
+
unlocked: true,
|
|
216
|
+
alreadyUnlocked: true,
|
|
217
|
+
account: selectedAccount(current, input),
|
|
218
|
+
deviceName: selectedStatus(current, input)?.deviceName ?? null,
|
|
219
|
+
platform: selectedStatus(current, input)?.platform ?? null,
|
|
220
|
+
redacted: true,
|
|
221
|
+
proofPath: 'agentic-wallet-status-after-missing-login-input',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
// The bridge target can rotate while the login input mounts. Keep the
|
|
226
|
+
// retry bounded by the same unlock deadline.
|
|
227
|
+
}
|
|
228
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (result === undefined) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Timed out waiting ${unlockTimeoutMs}ms for the Mobile login password input.`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
const remainingUnlockMs = Math.max(1, unlockDeadline - Date.now());
|
|
237
|
+
const after = await waitForUnlocked(input, remainingUnlockMs);
|
|
238
|
+
const stableAfter = await waitForStableUnlocked(
|
|
239
|
+
input,
|
|
240
|
+
after,
|
|
241
|
+
Number(input.node?.stable_unlocked_ms ?? 750),
|
|
242
|
+
);
|
|
243
|
+
if (!stableAfter) {
|
|
244
|
+
throw new Error('Mobile wallet did not remain unlocked for the required stability window.');
|
|
221
245
|
}
|
|
246
|
+
return {
|
|
247
|
+
action: input.action,
|
|
248
|
+
unlocked: Boolean(result?.ok ?? result?.unlocked ?? true),
|
|
249
|
+
account: selectedAccount(stableAfter, input),
|
|
250
|
+
route: selectedStatus(stableAfter, input)?.route ?? null,
|
|
251
|
+
deviceName: selectedStatus(stableAfter, input)?.deviceName ?? null,
|
|
252
|
+
platform: selectedStatus(stableAfter, input)?.platform ?? null,
|
|
253
|
+
redacted: true,
|
|
254
|
+
proofPath: 'agentic-wallet-unlock',
|
|
255
|
+
};
|
|
222
256
|
}
|
|
223
257
|
|
|
224
258
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
@@ -319,6 +319,8 @@ runAdapter(async (input) => {
|
|
|
319
319
|
...input,
|
|
320
320
|
node: {
|
|
321
321
|
...input.node,
|
|
322
|
+
target_timeout_ms: input.node?.target_timeout_ms ?? timeoutMs,
|
|
323
|
+
unlock_timeout_ms: input.node?.unlock_timeout_ms ?? timeoutMs,
|
|
322
324
|
...(route === 'Login' ? { password: credentials.password } : {}),
|
|
323
325
|
},
|
|
324
326
|
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { pathToFileURL } from 'node:url';
|
|
1
2
|
import { bridgeCommand, runAdapter } from '../platform/bridge.mjs';
|
|
2
3
|
|
|
3
4
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
export async function selectAccount(input) {
|
|
6
7
|
const requestedAddress = input.node?.address ? String(input.node.address).toLowerCase() : null;
|
|
7
8
|
const requestedId = input.node?.id ? String(input.node.id) : null;
|
|
8
9
|
const requestedName = input.node?.name ? String(input.node.name) : null;
|
|
@@ -24,6 +25,20 @@ runAdapter(async (input) => {
|
|
|
24
25
|
if (!match?.address) {
|
|
25
26
|
throw new Error(`Requested mobile account was not found in list-accounts: ${JSON.stringify({ address: requestedAddress, id: requestedId, name: requestedName })}`);
|
|
26
27
|
}
|
|
28
|
+
const current = await bridgeCommand(input, ['get-selected-account']);
|
|
29
|
+
const matchAddress = String(match.address).toLowerCase();
|
|
30
|
+
if (String(current?.address ?? current ?? '').toLowerCase() === matchAddress) {
|
|
31
|
+
return {
|
|
32
|
+
action: input.action,
|
|
33
|
+
selected: {
|
|
34
|
+
address: current?.address ?? match.address,
|
|
35
|
+
id: current?.id ?? match.id,
|
|
36
|
+
name: current?.name ?? match.name,
|
|
37
|
+
},
|
|
38
|
+
alreadySelected: true,
|
|
39
|
+
proofPath: 'agentic-account-selection',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
27
42
|
const result = await bridgeCommand(input, ['switch-account', String(match.address)]);
|
|
28
43
|
const timeoutMs = Number(input.node?.timeout_ms ?? 5000);
|
|
29
44
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -49,4 +64,8 @@ runAdapter(async (input) => {
|
|
|
49
64
|
},
|
|
50
65
|
proofPath: 'agentic-account-selection',
|
|
51
66
|
};
|
|
52
|
-
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
70
|
+
runAdapter(selectAccount);
|
|
71
|
+
}
|
|
@@ -1395,6 +1395,11 @@
|
|
|
1395
1395
|
},
|
|
1396
1396
|
"unlock_timeout_ms": {
|
|
1397
1397
|
"type": "number"
|
|
1398
|
+
},
|
|
1399
|
+
"stable_unlocked_ms": {
|
|
1400
|
+
"type": "number",
|
|
1401
|
+
"minimum": 0,
|
|
1402
|
+
"description": "How long the unlocked route must remain stable before the action succeeds; zero disables the extra stability window."
|
|
1398
1403
|
}
|
|
1399
1404
|
},
|
|
1400
1405
|
"additionalProperties": false
|
|
@@ -1445,6 +1450,11 @@
|
|
|
1445
1450
|
"type": "number",
|
|
1446
1451
|
"minimum": 1,
|
|
1447
1452
|
"description": "Maximum time in milliseconds for the mobile CDP command."
|
|
1453
|
+
},
|
|
1454
|
+
"timeout_ms": {
|
|
1455
|
+
"type": "number",
|
|
1456
|
+
"minimum": 1,
|
|
1457
|
+
"description": "Maximum time in milliseconds to observe the requested account as selected."
|
|
1448
1458
|
}
|
|
1449
1459
|
},
|
|
1450
1460
|
"additionalProperties": false
|
|
@@ -2877,7 +2887,7 @@
|
|
|
2877
2887
|
"execution_capabilities": ["app-mutation", "external-mutation"]
|
|
2878
2888
|
},
|
|
2879
2889
|
"metamask.perps.capture_performance": {
|
|
2880
|
-
"description": "Capture
|
|
2890
|
+
"description": "Capture a bounded app-console window, summarize production [PerpsLoadProof]/WebSocket milestones, and optionally parse detailed [PerpsPerf]/[StartupPerf] records when a build emits them.",
|
|
2881
2891
|
"schema": {
|
|
2882
2892
|
"type": "object",
|
|
2883
2893
|
"properties": {
|
|
@@ -2891,6 +2901,24 @@
|
|
|
2891
2901
|
"artifact_prefix": {
|
|
2892
2902
|
"type": "string"
|
|
2893
2903
|
},
|
|
2904
|
+
"wait_for_stage": {
|
|
2905
|
+
"type": "string",
|
|
2906
|
+
"description": "For phase=end, optionally poll until this detailed PerpsPerf stage exists before writing artifacts."
|
|
2907
|
+
},
|
|
2908
|
+
"wait_for_source": {
|
|
2909
|
+
"type": "string"
|
|
2910
|
+
},
|
|
2911
|
+
"wait_for_lifecycle": {
|
|
2912
|
+
"type": "string"
|
|
2913
|
+
},
|
|
2914
|
+
"wait_timeout_ms": {
|
|
2915
|
+
"type": "number",
|
|
2916
|
+
"minimum": 1
|
|
2917
|
+
},
|
|
2918
|
+
"wait_poll_interval_ms": {
|
|
2919
|
+
"type": "number",
|
|
2920
|
+
"minimum": 10
|
|
2921
|
+
},
|
|
2894
2922
|
"require_records": {
|
|
2895
2923
|
"type": "boolean"
|
|
2896
2924
|
},
|
|
@@ -2906,6 +2934,18 @@
|
|
|
2906
2934
|
"type": "string"
|
|
2907
2935
|
}
|
|
2908
2936
|
},
|
|
2937
|
+
"require_perps_bootstrap_start_before_demand": {
|
|
2938
|
+
"type": "boolean",
|
|
2939
|
+
"description": "Require the first Perps surface demand to occur after a captured PerpsLoadProof perps_bootstrap_start boundary."
|
|
2940
|
+
},
|
|
2941
|
+
"required_live_streams": {
|
|
2942
|
+
"type": "array",
|
|
2943
|
+
"description": "Require values_ready records sourced from fresh_socket for each named Perps stream.",
|
|
2944
|
+
"items": {
|
|
2945
|
+
"type": "string",
|
|
2946
|
+
"enum": ["prices", "positions", "orders", "account"]
|
|
2947
|
+
}
|
|
2948
|
+
},
|
|
2909
2949
|
"required_lifecycles": {
|
|
2910
2950
|
"type": "array",
|
|
2911
2951
|
"items": {
|
|
@@ -2969,8 +3009,9 @@
|
|
|
2969
3009
|
{
|
|
2970
3010
|
"action": "metamask.perps.capture_performance",
|
|
2971
3011
|
"phase": "end",
|
|
2972
|
-
"
|
|
2973
|
-
"
|
|
3012
|
+
"require_records": false,
|
|
3013
|
+
"required_live_streams": ["positions", "orders", "account"],
|
|
3014
|
+
"intent": "Preserve production Perps loading and live-stream evidence emitted during this run",
|
|
2974
3015
|
"next": "done"
|
|
2975
3016
|
}
|
|
2976
3017
|
],
|
|
@@ -3094,9 +3135,9 @@
|
|
|
3094
3135
|
}
|
|
3095
3136
|
},
|
|
3096
3137
|
"page": {
|
|
3097
|
-
"description": "Initial Perps page target; omit it to use the profile or market default.",
|
|
3098
|
-
"type": "string",
|
|
3099
|
-
"enum": ["home", "perps", "perps_home", "market", "market_details"]
|
|
3138
|
+
"description": "Initial Perps page target; omit it to use the profile or market default, or set false without market/symbol to preserve the current route.",
|
|
3139
|
+
"type": ["string", "boolean"],
|
|
3140
|
+
"enum": [false, "home", "perps", "perps_home", "market", "market_details"]
|
|
3100
3141
|
},
|
|
3101
3142
|
"positions": {
|
|
3102
3143
|
"type": ["object", "boolean"],
|