@deeeed/metamask-harness 0.47.1 → 0.47.3
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 +25 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +1 -0
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +15 -0
- package/dist/commands/call.js +4 -5
- package/dist/mm-harness-cli.js +1 -1
- package/library/actions/mobile/perps/perps.mjs +75 -24
- package/library/actions/mobile/perps/set_market_favorite.mjs +0 -11
- package/library/actions/mobile/platform/bridge.mjs +6 -1
- package/library/actions/mobile/ui/native-navigation.mjs +9 -2
- package/library/actions/mobile/ui/navigate.mjs +13 -4
- package/library/manifests/mobile.action-manifest.json +1 -2
- package/package.json +1 -1
- package/site/assets/help-recipes.json +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.47.3 - 2026-09-05
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Select the requested Mobile Perps mode through the first-entry chooser, verify its detail root, and restore the requested market after mode selection changes the navigation stack.
|
|
10
|
+
- Preserve earlier `call` evidence by assigning each invocation a fresh default artifact directory; approval retries still use the explicit directory in the printed next command.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Teach authors to inspect existing state and limit setup mutations to prerequisites required by their proof.
|
|
15
|
+
|
|
16
|
+
## 0.47.2 - 2026-09-05
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Teach recipe and action discovery, bounded setup probes, source-based diagnosis, and honest blocked-proof reporting through version-matched CLI help.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Flush machine-readable command output before exit so large JSON documents remain complete when piped to another process.
|
|
25
|
+
- Keep `metamask.perps.set_market_favorite` in the current mode while waiting for a temporarily absent favorite control.
|
|
26
|
+
- Preserve the current Mobile Perps mode when `ui.navigate` omits `mode`, including opaque Android navigation.
|
|
27
|
+
- Prefer an exact visible Mobile Perps market row or recently-viewed tile before falling back to search.
|
|
28
|
+
- Preserve Mobile Perps teardown navigation when embedded and fallback bridges use different wording for a missing scrollable target.
|
|
29
|
+
|
|
5
30
|
## 0.47.1 - 2026-09-04
|
|
6
31
|
|
|
7
32
|
### Fixed
|
|
@@ -195,6 +195,7 @@ const NESTED_ROUTE_PARENTS = {
|
|
|
195
195
|
PerpsPnlHeroCard: 'Perps', PerpsHIP3Debug: 'Perps',
|
|
196
196
|
PerpsSelectModifyAction: 'Perps', PerpsSelectAdjustMarginAction: 'Perps',
|
|
197
197
|
PerpsSelectOrderType: 'Perps', PerpsTradingView: 'Perps',
|
|
198
|
+
PerpsModeSelection: 'PerpsModals',
|
|
198
199
|
// Predict
|
|
199
200
|
PredictMarketList: 'Predict', PredictMarketDetails: 'Predict',
|
|
200
201
|
PredictActivityDetail: 'Predict',
|
|
@@ -18,6 +18,10 @@ const BRIDGE_ERROR_CODES = {
|
|
|
18
18
|
METRO_UNREACHABLE: 'METRO_UNREACHABLE',
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
const BRIDGE_RESULT_ERROR_CODES = {
|
|
22
|
+
SCROLLABLE_NOT_FOUND: 'SCROLLABLE_NOT_FOUND',
|
|
23
|
+
};
|
|
24
|
+
|
|
21
25
|
// Process exit code the bridge returns per failure code so a caller that only
|
|
22
26
|
// sees the child's exit status (no stderr) can still recover the code. Kept
|
|
23
27
|
// clear of exit 1 (unknown/uncoded) and 2 (usage).
|
|
@@ -78,6 +82,15 @@ function classifyBridgeErrorMessage(message) {
|
|
|
78
82
|
return null;
|
|
79
83
|
}
|
|
80
84
|
|
|
85
|
+
function classifyBridgeResultError(command, result) {
|
|
86
|
+
if (command !== 'scroll-view' || result?.ok !== false) return null;
|
|
87
|
+
return /^No scrollable(?: found)? near testID=/u.test(
|
|
88
|
+
String(result.error ?? ''),
|
|
89
|
+
)
|
|
90
|
+
? BRIDGE_RESULT_ERROR_CODES.SCROLLABLE_NOT_FOUND
|
|
91
|
+
: null;
|
|
92
|
+
}
|
|
93
|
+
|
|
81
94
|
// Attach a code to an error at its throw site (source classification).
|
|
82
95
|
function coded(error, code) {
|
|
83
96
|
if (error && typeof error === 'object') error.code = code;
|
|
@@ -99,10 +112,12 @@ function parseErrorMarker(text) {
|
|
|
99
112
|
|
|
100
113
|
module.exports = {
|
|
101
114
|
BRIDGE_ERROR_CODES,
|
|
115
|
+
BRIDGE_RESULT_ERROR_CODES,
|
|
102
116
|
EXIT_CODE_BY_ERROR_CODE,
|
|
103
117
|
ERROR_CODE_BY_EXIT_CODE,
|
|
104
118
|
TEACHING_BY_ERROR_CODE,
|
|
105
119
|
classifyBridgeErrorMessage,
|
|
120
|
+
classifyBridgeResultError,
|
|
106
121
|
coded,
|
|
107
122
|
formatErrorMarker,
|
|
108
123
|
parseErrorMarker,
|
package/dist/commands/call.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { resolveActionManifest } from "../manifest.js";
|
|
5
5
|
import { importRecipeProtocol } from "../paths.js";
|
|
@@ -212,7 +212,7 @@ async function handleCall(argv) {
|
|
|
212
212
|
}
|
|
213
213
|
return EXIT.validation;
|
|
214
214
|
}
|
|
215
|
-
const artifactsDir = optionString(options, "artifactsDir") ?? defaultCallArtifactsDir(target, resolvedAction
|
|
215
|
+
const artifactsDir = optionString(options, "artifactsDir") ?? defaultCallArtifactsDir(target, resolvedAction);
|
|
216
216
|
recordCommandEvidence(artifactsDir);
|
|
217
217
|
const requestedRuntimeOptions = runtimeOptionsFromCli(options);
|
|
218
218
|
const inheritedSource = process.env.FARMSLOT_RECIPE_SOURCE_TRUST || process.env.FARMSLOT_RECIPE_SOURCE_KIND || process.env.FARMSLOT_RECIPE_SOURCE_NAME || process.env.FARMSLOT_RECIPE_SOURCE_DIGEST;
|
|
@@ -493,10 +493,9 @@ function renderDefaultsUsed(defaults, stream) {
|
|
|
493
493
|
const values = Object.entries(defaults).map(([name, value]) => `${out("cmd", name)}=${out("accent", JSON.stringify(value) ?? String(value))}`).join(", ");
|
|
494
494
|
return `${out("label", "Defaults used:")} ${values}`;
|
|
495
495
|
}
|
|
496
|
-
function defaultCallArtifactsDir(target, action
|
|
496
|
+
function defaultCallArtifactsDir(target, action) {
|
|
497
497
|
const actionStem = action.replace(/[^a-zA-Z0-9._-]/gu, "_");
|
|
498
|
-
|
|
499
|
-
return path.join(target, "temp", "recipe", "calls", `${actionStem}-${digest}`);
|
|
498
|
+
return path.join(target, "temp", "recipe", "calls", `${actionStem}-${randomUUID()}`);
|
|
500
499
|
}
|
|
501
500
|
function readCallOutput(tracePath) {
|
|
502
501
|
try {
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -830,7 +830,7 @@ for (const command of REAL) {
|
|
|
830
830
|
if (command.name === "tutorial") {
|
|
831
831
|
process.exit(handleTutorial(rawArgv.slice(1), { harnessVersion: pkgVersion }));
|
|
832
832
|
}
|
|
833
|
-
process.
|
|
833
|
+
process.exitCode = await withCommandJournal(command.name, rawArgv, () => delegate(rawArgv));
|
|
834
834
|
});
|
|
835
835
|
}
|
|
836
836
|
const HIDDEN = [
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { pathToFileURL } from 'node:url';
|
|
2
2
|
import {
|
|
3
|
+
MOBILE_BRIDGE_RESULT_ERROR_CODES,
|
|
3
4
|
bridgeCommand,
|
|
4
5
|
evalAsync,
|
|
5
6
|
isTargetTransition,
|
|
@@ -130,7 +131,7 @@ function withRemainingDeadline(input, requestedDeadline, attemptTimeoutMs) {
|
|
|
130
131
|
};
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
async function
|
|
134
|
+
async function queryModeState(input, mode, deadline) {
|
|
134
135
|
const rootTarget = JSON.stringify({
|
|
135
136
|
testId: MODE_ROOT_TEST_IDS[mode],
|
|
136
137
|
visibility: 'viewport',
|
|
@@ -139,6 +140,10 @@ async function isModePresent(input, mode, deadline) {
|
|
|
139
140
|
testId: MODE_TEST_IDS[mode],
|
|
140
141
|
visibility: 'tree',
|
|
141
142
|
});
|
|
143
|
+
const choiceTarget = JSON.stringify({
|
|
144
|
+
testId: `perps-mode-selection-${mode}-option`,
|
|
145
|
+
visibility: 'viewport',
|
|
146
|
+
});
|
|
142
147
|
const result = await evalAsync(
|
|
143
148
|
withRemainingDeadline(input, deadline, READ_ATTEMPT_TIMEOUT_MS),
|
|
144
149
|
`(function(){
|
|
@@ -148,21 +153,25 @@ async function isModePresent(input, mode, deadline) {
|
|
|
148
153
|
}
|
|
149
154
|
return Promise.all([
|
|
150
155
|
api.queryUiTarget(${rootTarget}),
|
|
151
|
-
api.queryUiTarget(${controlTarget})
|
|
156
|
+
api.queryUiTarget(${controlTarget}),
|
|
157
|
+
api.queryUiTarget(${choiceTarget})
|
|
152
158
|
]).then(function(values){
|
|
153
|
-
return JSON.stringify({ root: values[0], control: values[1] });
|
|
159
|
+
return JSON.stringify({ root: values[0], control: values[1], choice: values[2] });
|
|
154
160
|
});
|
|
155
161
|
})()`,
|
|
156
162
|
);
|
|
157
163
|
if (result?.unsupported) {
|
|
158
164
|
throw new Error('metamask.perps.ensure_mode requires UI target queries.');
|
|
159
165
|
}
|
|
160
|
-
return
|
|
166
|
+
return {
|
|
167
|
+
present: result?.root?.visible === true && result?.control?.present === true,
|
|
168
|
+
choiceVisible: result?.choice?.visible === true,
|
|
169
|
+
};
|
|
161
170
|
}
|
|
162
171
|
|
|
163
|
-
async function
|
|
172
|
+
async function readModeState(input, mode, deadline) {
|
|
164
173
|
try {
|
|
165
|
-
return await
|
|
174
|
+
return await queryModeState(input, mode, deadline);
|
|
166
175
|
} catch (error) {
|
|
167
176
|
if (!isTargetTransition(error)) throw error;
|
|
168
177
|
return null;
|
|
@@ -180,14 +189,15 @@ export async function ensureMode(input) {
|
|
|
180
189
|
Number(input.node?.timeout_ms ?? 30_000),
|
|
181
190
|
);
|
|
182
191
|
let switched = false;
|
|
192
|
+
let choiceSelected = false;
|
|
183
193
|
|
|
184
194
|
while (Date.now() < deadline) {
|
|
185
|
-
const
|
|
195
|
+
const requestedState = await readModeState(
|
|
186
196
|
input,
|
|
187
197
|
requestedMode,
|
|
188
198
|
deadline,
|
|
189
199
|
);
|
|
190
|
-
if (
|
|
200
|
+
if (requestedState?.present && !requestedState.choiceVisible) {
|
|
191
201
|
return {
|
|
192
202
|
action: input.action,
|
|
193
203
|
mode: requestedMode,
|
|
@@ -196,13 +206,23 @@ export async function ensureMode(input) {
|
|
|
196
206
|
};
|
|
197
207
|
}
|
|
198
208
|
if (Date.now() >= deadline) break;
|
|
199
|
-
if (
|
|
200
|
-
const
|
|
209
|
+
if (requestedState?.choiceVisible && !choiceSelected) {
|
|
210
|
+
const pressed = await bridgeCommand(withRemainingDeadline(input, deadline), [
|
|
211
|
+
'press-test-id',
|
|
212
|
+
`perps-mode-selection-${requestedMode}-option`,
|
|
213
|
+
]);
|
|
214
|
+
if (pressed?.ok === false) {
|
|
215
|
+
throw new Error(String(pressed.error || 'Perps mode choice press failed.'));
|
|
216
|
+
}
|
|
217
|
+
choiceSelected = true;
|
|
218
|
+
switched = true;
|
|
219
|
+
} else if (requestedState !== null && !switched) {
|
|
220
|
+
const otherState = await readModeState(
|
|
201
221
|
input,
|
|
202
222
|
otherMode,
|
|
203
223
|
deadline,
|
|
204
224
|
);
|
|
205
|
-
if (
|
|
225
|
+
if (otherState?.present && Date.now() < deadline) {
|
|
206
226
|
// The visible control names the mode it will switch to, not the mode
|
|
207
227
|
// currently selected.
|
|
208
228
|
const pressed = await bridgeCommand(withRemainingDeadline(input, deadline), [
|
|
@@ -915,29 +935,50 @@ async function readVisiblePerpsTestIds(input, deadline) {
|
|
|
915
935
|
return new Set(visible.items.map((item) => String(item.test_id ?? '')));
|
|
916
936
|
}
|
|
917
937
|
|
|
918
|
-
async function
|
|
938
|
+
async function waitForPerpsUiTargets(input, testIds, deadline) {
|
|
919
939
|
let lastTestIds = [];
|
|
920
940
|
while (Date.now() < deadline) {
|
|
921
|
-
const
|
|
922
|
-
|
|
923
|
-
|
|
941
|
+
const visibleTestIds = await readVisiblePerpsTestIds(input, deadline);
|
|
942
|
+
const matchedTestId = testIds.find((testId) =>
|
|
943
|
+
visibleTestIds.has(testId),
|
|
944
|
+
);
|
|
945
|
+
if (matchedTestId) return { matchedTestId, visibleTestIds };
|
|
946
|
+
lastTestIds = [...visibleTestIds].filter(Boolean).slice(0, 20);
|
|
924
947
|
await sleepBeforeDeadline(deadline, 250);
|
|
925
948
|
}
|
|
949
|
+
const targetDescription =
|
|
950
|
+
testIds.length === 1
|
|
951
|
+
? `control ${testIds[0]}`
|
|
952
|
+
: `controls ${testIds.join(' or ')}`;
|
|
926
953
|
throw new Error(
|
|
927
|
-
`Perps
|
|
954
|
+
`Perps ${targetDescription} did not become visible before timeout. Observed: ${lastTestIds.join(', ') || 'none'}.`,
|
|
928
955
|
);
|
|
929
956
|
}
|
|
930
957
|
|
|
958
|
+
async function waitForPerpsUiTarget(input, testId, deadline) {
|
|
959
|
+
const { visibleTestIds } = await waitForPerpsUiTargets(
|
|
960
|
+
input,
|
|
961
|
+
[testId],
|
|
962
|
+
deadline,
|
|
963
|
+
);
|
|
964
|
+
return visibleTestIds;
|
|
965
|
+
}
|
|
966
|
+
|
|
931
967
|
async function selectPerpsMarketFromHome(input, symbol, previousRoute) {
|
|
932
968
|
const timeoutMs = Number(input.node?.navigation_timeout_ms ?? 15_000);
|
|
933
969
|
const deadline = deadlineFromTimeout(input, timeoutMs);
|
|
934
970
|
const params = { market: { symbol } };
|
|
935
971
|
const rowTestId = `perps-market-row-item-${symbol}`;
|
|
972
|
+
const recentlyViewedTestId = `perps-recently-viewed-tile-${symbol}`;
|
|
973
|
+
const marketTestIds = [rowTestId, recentlyViewedTestId];
|
|
936
974
|
const searchTestId = 'perps-market-list-search-bar';
|
|
937
975
|
let visibleTestIds = await readVisiblePerpsTestIds(input, deadline);
|
|
976
|
+
let marketTestId = marketTestIds.find((testId) =>
|
|
977
|
+
visibleTestIds.has(testId),
|
|
978
|
+
);
|
|
938
979
|
let usedSearch = false;
|
|
939
980
|
|
|
940
|
-
if (!
|
|
981
|
+
if (!marketTestId) {
|
|
941
982
|
if (!visibleTestIds.has(searchTestId)) {
|
|
942
983
|
if (!visibleTestIds.has('perps-home-search-toggle')) {
|
|
943
984
|
throw new Error(
|
|
@@ -959,17 +1000,19 @@ async function selectPerpsMarketFromHome(input, symbol, previousRoute) {
|
|
|
959
1000
|
searchTestId,
|
|
960
1001
|
symbol,
|
|
961
1002
|
]);
|
|
962
|
-
|
|
1003
|
+
const visibleMarketTarget = await waitForPerpsUiTargets(
|
|
963
1004
|
input,
|
|
964
|
-
|
|
1005
|
+
marketTestIds,
|
|
965
1006
|
deadline,
|
|
966
1007
|
);
|
|
1008
|
+
visibleTestIds = visibleMarketTarget.visibleTestIds;
|
|
1009
|
+
marketTestId = visibleMarketTarget.matchedTestId;
|
|
967
1010
|
usedSearch = true;
|
|
968
1011
|
}
|
|
969
1012
|
|
|
970
1013
|
await bridgeCommand(withRemainingDeadline(input, deadline), [
|
|
971
1014
|
'press-test-id',
|
|
972
|
-
|
|
1015
|
+
marketTestId,
|
|
973
1016
|
]);
|
|
974
1017
|
const currentRoute = await waitForRoute(
|
|
975
1018
|
withRemainingDeadline(input, deadline),
|
|
@@ -983,7 +1026,7 @@ async function selectPerpsMarketFromHome(input, symbol, previousRoute) {
|
|
|
983
1026
|
previousRoute,
|
|
984
1027
|
currentRoute,
|
|
985
1028
|
verifiedRoute: 'PerpsMarketDetails',
|
|
986
|
-
visibleMarketSelection: { usedSearch },
|
|
1029
|
+
visibleMarketSelection: { usedSearch, testId: marketTestId },
|
|
987
1030
|
};
|
|
988
1031
|
}
|
|
989
1032
|
|
|
@@ -1033,7 +1076,12 @@ async function navigateFromMarketDetailsToHome(input, previousRoute) {
|
|
|
1033
1076
|
backTestId = expectedBackTestId;
|
|
1034
1077
|
break;
|
|
1035
1078
|
} catch (error) {
|
|
1036
|
-
if (
|
|
1079
|
+
if (
|
|
1080
|
+
error?.code !==
|
|
1081
|
+
MOBILE_BRIDGE_RESULT_ERROR_CODES.SCROLLABLE_NOT_FOUND
|
|
1082
|
+
) {
|
|
1083
|
+
throw error;
|
|
1084
|
+
}
|
|
1037
1085
|
}
|
|
1038
1086
|
}
|
|
1039
1087
|
}
|
|
@@ -1079,7 +1127,7 @@ async function navigateFromMarketDetailsToHome(input, previousRoute) {
|
|
|
1079
1127
|
};
|
|
1080
1128
|
}
|
|
1081
1129
|
|
|
1082
|
-
async function navigatePerps(input) {
|
|
1130
|
+
export async function navigatePerps(input) {
|
|
1083
1131
|
const selected = String(
|
|
1084
1132
|
input.node?.target ?? input.node?.destination ?? defaultNavigationTarget(input.node),
|
|
1085
1133
|
).toLowerCase();
|
|
@@ -1789,7 +1837,7 @@ export async function startState(input) {
|
|
|
1789
1837
|
const navigation = await applyStateNavigation(stateInput(), config);
|
|
1790
1838
|
const provider = await ensureProvider(stateInput(), config);
|
|
1791
1839
|
const network = await ensureNetwork(stateInput(), config);
|
|
1792
|
-
|
|
1840
|
+
let resolvedNavigation = provider.changed || network.changed
|
|
1793
1841
|
? await applyStateNavigation(stateInput(), config)
|
|
1794
1842
|
: navigation;
|
|
1795
1843
|
const modeInput = stateInput();
|
|
@@ -1804,6 +1852,9 @@ export async function startState(input) {
|
|
|
1804
1852
|
},
|
|
1805
1853
|
})
|
|
1806
1854
|
: { skipped: true };
|
|
1855
|
+
if (marketMode.alreadySelected === false) {
|
|
1856
|
+
resolvedNavigation = await applyStateNavigation(stateInput(), config);
|
|
1857
|
+
}
|
|
1807
1858
|
const tutorial = await applyTutorialState(stateInput(), config);
|
|
1808
1859
|
const readyToTrade = await assertReadyToTrade(stateInput(), config);
|
|
1809
1860
|
const balance = await assertBalance(stateInput(), config);
|
|
@@ -51,17 +51,6 @@ runAdapter(async (input) => {
|
|
|
51
51
|
throw new Error(`Visible Perps market ${JSON.stringify(subtitle)} does not match ${market}.`);
|
|
52
52
|
}
|
|
53
53
|
let control = favoriteControl(snapshot);
|
|
54
|
-
if (!control && nativeNode(snapshot, 'perps-mode-toggle-lite')?.enabled !== false) {
|
|
55
|
-
try {
|
|
56
|
-
await session.execute('ui.press', {
|
|
57
|
-
test_id: 'perps-mode-toggle-lite',
|
|
58
|
-
timeout_ms: timeoutMs,
|
|
59
|
-
settle: false,
|
|
60
|
-
});
|
|
61
|
-
} catch (error) {
|
|
62
|
-
if (!/Selector did not match/iu.test(String(error?.message ?? error))) throw error;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
54
|
const controlDeadline = Date.now() + timeoutMs;
|
|
66
55
|
let lastControls = [];
|
|
67
56
|
while (Date.now() <= controlDeadline && !control) {
|
|
@@ -13,8 +13,10 @@ import { resolveMobileToolPath } from './tool-paths.mjs';
|
|
|
13
13
|
|
|
14
14
|
const {
|
|
15
15
|
BRIDGE_ERROR_CODES,
|
|
16
|
+
BRIDGE_RESULT_ERROR_CODES,
|
|
16
17
|
ERROR_CODE_BY_EXIT_CODE,
|
|
17
18
|
classifyBridgeErrorMessage,
|
|
19
|
+
classifyBridgeResultError,
|
|
18
20
|
coded,
|
|
19
21
|
parseErrorMarker,
|
|
20
22
|
} = bridgeErrors;
|
|
@@ -24,6 +26,7 @@ const { resolvePort } = configModule;
|
|
|
24
26
|
// Re-exported so the TS adapter classifies on the same code constants without a
|
|
25
27
|
// second import path into the cjs bridge-runtime.
|
|
26
28
|
export const MOBILE_BRIDGE_ERROR_CODES = BRIDGE_ERROR_CODES;
|
|
29
|
+
export const MOBILE_BRIDGE_RESULT_ERROR_CODES = BRIDGE_RESULT_ERROR_CODES;
|
|
27
30
|
|
|
28
31
|
const execFileAsync = promisify(execFile);
|
|
29
32
|
const ACTION_DEADLINE_FIELD = '_action_deadline_epoch_ms';
|
|
@@ -536,9 +539,11 @@ export async function bridgeCommand(input, args) {
|
|
|
536
539
|
try {
|
|
537
540
|
const parsed = JSON.parse(result.stdout);
|
|
538
541
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && parsed.ok === false) {
|
|
539
|
-
|
|
542
|
+
const error = new Error(
|
|
540
543
|
`Mobile CDP bridge command reported failure for ${redactBridgeArgs(args).join(' ')}: ${redactBridgeOutput(JSON.stringify(parsed), sensitiveBridgeArgs(args))}`,
|
|
541
544
|
);
|
|
545
|
+
const code = classifyBridgeResultError(command, parsed);
|
|
546
|
+
throw code ? coded(error, code) : error;
|
|
542
547
|
}
|
|
543
548
|
return parsed;
|
|
544
549
|
} catch (error) {
|
|
@@ -73,7 +73,9 @@ export async function navigateOpaqueMobile(input, destination) {
|
|
|
73
73
|
artifactsDir: input.context.artifactsDir,
|
|
74
74
|
env,
|
|
75
75
|
};
|
|
76
|
-
const perpsMode =
|
|
76
|
+
const perpsMode = ['lite', 'pro'].includes(destination.params?.mode)
|
|
77
|
+
? destination.params.mode
|
|
78
|
+
: undefined;
|
|
77
79
|
const press = (testId) => transport.execute('ui.press', {
|
|
78
80
|
test_id: testId,
|
|
79
81
|
timeout_ms: Number(input.node?.timeout_ms ?? 30_000),
|
|
@@ -542,11 +544,16 @@ async function navigatePerpsMarketList({ identifiers, press, wait, back, timeout
|
|
|
542
544
|
throw new Error(`Opaque Mobile navigation reached ${observed}, but no visible market-list control was available.`);
|
|
543
545
|
}
|
|
544
546
|
|
|
545
|
-
async function waitForPerpsDestination(identifiers, timeoutMs, press, perpsMode
|
|
547
|
+
async function waitForPerpsDestination(identifiers, timeoutMs, press, perpsMode) {
|
|
546
548
|
const deadline = Date.now() + timeoutMs;
|
|
547
549
|
while (Date.now() <= deadline) {
|
|
548
550
|
const ids = await identifiers();
|
|
549
551
|
if (ids.has(IDS.modeLite) || ids.has(IDS.modePro)) {
|
|
552
|
+
if (!perpsMode) {
|
|
553
|
+
throw new Error(
|
|
554
|
+
'Opaque Mobile navigation requires mode=lite or mode=pro when the product requests a Perps mode choice.',
|
|
555
|
+
);
|
|
556
|
+
}
|
|
550
557
|
await press(perpsMode === 'pro' ? IDS.modePro : IDS.modeLite);
|
|
551
558
|
continue;
|
|
552
559
|
}
|
|
@@ -23,8 +23,8 @@ function text(value) {
|
|
|
23
23
|
function pageRoute(node) {
|
|
24
24
|
const page = text(node?.page);
|
|
25
25
|
if (!page) return undefined;
|
|
26
|
-
const mode = text(node?.mode)?.toLowerCase()
|
|
27
|
-
if (!['lite', 'pro'].includes(mode)) {
|
|
26
|
+
const mode = text(node?.mode)?.toLowerCase();
|
|
27
|
+
if (mode && !['lite', 'pro'].includes(mode)) {
|
|
28
28
|
throw new Error('mobile ui.navigate mode must be lite or pro.');
|
|
29
29
|
}
|
|
30
30
|
if (PAGE_ROUTES[page]) {
|
|
@@ -32,13 +32,22 @@ function pageRoute(node) {
|
|
|
32
32
|
return {
|
|
33
33
|
page,
|
|
34
34
|
...route,
|
|
35
|
-
params: page.startsWith('perps')
|
|
35
|
+
params: page.startsWith('perps') && mode
|
|
36
|
+
? { ...route.params, mode }
|
|
37
|
+
: route.params,
|
|
36
38
|
};
|
|
37
39
|
}
|
|
38
40
|
if (page === 'perps-market') {
|
|
39
41
|
const market = text(node.market) ?? text(node.symbol) ?? text(node.params?.market?.symbol);
|
|
40
42
|
if (!market) throw new Error('mobile ui.navigate page=perps-market requires market or symbol.');
|
|
41
|
-
return {
|
|
43
|
+
return {
|
|
44
|
+
page,
|
|
45
|
+
route: 'PerpsMarketDetails',
|
|
46
|
+
params: {
|
|
47
|
+
market: { symbol: market },
|
|
48
|
+
...(mode ? { mode } : {}),
|
|
49
|
+
},
|
|
50
|
+
};
|
|
42
51
|
}
|
|
43
52
|
throw new Error('mobile ui.navigate supported page intents: home, perps, perps-market-list, perps-market, swap.');
|
|
44
53
|
}
|
package/package.json
CHANGED
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"instruction": "Search the installed recipe and action catalogs, inspect exact schemas, and select the closest recipe before writing anything new.",
|
|
27
27
|
"details": [
|
|
28
28
|
"A bundled recipe, including a Core recipe, is strong composable vocabulary, not authority for every future product version.",
|
|
29
|
-
"Prefer an existing recipe, then existing actions, then read-only CDP or controller inspection. Add shared vocabulary only after repeated need is proven."
|
|
29
|
+
"Prefer an existing recipe, then existing actions, then read-only CDP or controller inspection. Add shared vocabulary only after repeated need is proven.",
|
|
30
|
+
"Before declaring a capability unsupported, inspect both catalogs and the closest recipe's composition. A missing dedicated action may already be composed from UI actions. Read the returned schema before filtering fields; do not treat guessed field names as empty capabilities."
|
|
30
31
|
],
|
|
31
32
|
"commands": [
|
|
32
33
|
"mm-harness actions --json",
|
|
@@ -51,6 +52,7 @@
|
|
|
51
52
|
"instruction": "Use static call references for unchanged sub-journeys. Keep preparation, action, assertion, evidence, and guaranteed teardown visible as separate nodes with human-facing intent.",
|
|
52
53
|
"details": [
|
|
53
54
|
"Use idempotent ensure actions for required start state and independently assert their postconditions.",
|
|
55
|
+
"Inspect existing state before creating or cleaning fixtures. Establish only prerequisites required by the claim; a history or display check may already have usable records and need no trade or cleanup. Record the selected records and verify they still satisfy the proof boundary.",
|
|
54
56
|
"Actions translate stable UI, CDP, or controller operations. Recipes compose them. Product code owns business rules."
|
|
55
57
|
],
|
|
56
58
|
"commands": []
|
|
@@ -61,6 +63,7 @@
|
|
|
61
63
|
"instruction": "Probe each unfamiliar action with call or a tiny action-to-end recipe. Use read-only CDP inspection only when declared actions cannot reveal the needed authoring detail, then consolidate the final proof from manifest-declared actions.",
|
|
62
64
|
"details": [
|
|
63
65
|
"Plan after every structural change and preserve the first failing trace.",
|
|
66
|
+
"A plan validates structure only. Probe a reused setup boundary against the current build with a short explicit timeout before a full journey; a bundled journey timeout is not a discovery budget.",
|
|
64
67
|
"A CDP observation can help find the current route or selector; it is not final evidence unless the recipe records an allowed action and assertion for that signal."
|
|
65
68
|
],
|
|
66
69
|
"commands": [
|
|
@@ -84,6 +87,7 @@
|
|
|
84
87
|
"instruction": "Run the complete graph once into an explicit artifact directory, then apply the built-in quality bar before assigning coverage or a verdict.",
|
|
85
88
|
"details": [
|
|
86
89
|
"Coverage: every criterion needs an executable path and assertion; manual, untestable, and environment-dependent gaps stay explicit.",
|
|
90
|
+
"A blocked required runtime claim is incomplete proof. Passing static checks or closing a checklist cannot make that claim validated or fixed.",
|
|
87
91
|
"Graph: no unconditional pass, generic intent, hidden start state, or opaque node that collapses preparation, action, assertion, evidence, and teardown.",
|
|
88
92
|
"Evidence: inspect the recipe resolution, summary, trace, manifest, logs, and actual visual evidence. A filename or passing node is not visual proof.",
|
|
89
93
|
"Flake risk: wait on observable state instead of sleeping, keep device and runtime identity explicit, and never overwrite a prior run's artifacts.",
|
|
@@ -101,6 +105,8 @@
|
|
|
101
105
|
"instruction": "When a shared recipe fails, reproduce once and classify product regression, recipe drift, fixture/runtime drift, harness defect, or live dependency before changing anything.",
|
|
102
106
|
"details": [
|
|
103
107
|
"The graph and evidence contract can be deterministic while the product, provider, account, or network outcome is not. Honest external rejection remains a valid result, not a reason to seek a green retry.",
|
|
108
|
+
"Trace the first failure through the exact action source and test the smallest causal explanation before filing or fixing a harness issue. A selector name does not establish its cause. If a proposed fix outgrows the confirmed cause, preserve it separately and return to the minimal fix.",
|
|
109
|
+
"For blocked runtime proof, retain the target identity, build/app state, and first failing command and error. Reconcile later status snapshots with that evidence; they cannot establish that an earlier device was absent.",
|
|
104
110
|
"For product-version drift, preserve the failed trace, copy the composed recipe into the task, keep working nodes, and repair the smallest owning node against the tested build.",
|
|
105
111
|
"Prove the changed node in isolation, re-plan, then rerun the full task-local recipe and affected callers. Never weaken an assertion or change product code merely to turn stale proof green.",
|
|
106
112
|
"Record the source recipe digest, tested product identity, repair, and fresh evidence. If the defect belongs to reusable harness code or bundled vocabulary, prepare a focused experimental-metamask-harness issue and open it when authorized.",
|