@deeeed/metamask-harness 0.47.2 → 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 CHANGED
@@ -2,6 +2,17 @@
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
+
5
16
  ## 0.47.2 - 2026-09-05
6
17
 
7
18
  ### Changed
@@ -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',
@@ -1,5 +1,5 @@
1
1
  import fs from "node:fs";
2
- import { createHash } from "node:crypto";
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, args);
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, args) {
496
+ function defaultCallArtifactsDir(target, action) {
497
497
  const actionStem = action.replace(/[^a-zA-Z0-9._-]/gu, "_");
498
- const digest = createHash("sha256").update(JSON.stringify({ action, args })).digest("hex").slice(0, 12);
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 {
@@ -131,7 +131,7 @@ function withRemainingDeadline(input, requestedDeadline, attemptTimeoutMs) {
131
131
  };
132
132
  }
133
133
 
134
- async function isModePresent(input, mode, deadline) {
134
+ async function queryModeState(input, mode, deadline) {
135
135
  const rootTarget = JSON.stringify({
136
136
  testId: MODE_ROOT_TEST_IDS[mode],
137
137
  visibility: 'viewport',
@@ -140,6 +140,10 @@ async function isModePresent(input, mode, deadline) {
140
140
  testId: MODE_TEST_IDS[mode],
141
141
  visibility: 'tree',
142
142
  });
143
+ const choiceTarget = JSON.stringify({
144
+ testId: `perps-mode-selection-${mode}-option`,
145
+ visibility: 'viewport',
146
+ });
143
147
  const result = await evalAsync(
144
148
  withRemainingDeadline(input, deadline, READ_ATTEMPT_TIMEOUT_MS),
145
149
  `(function(){
@@ -149,21 +153,25 @@ async function isModePresent(input, mode, deadline) {
149
153
  }
150
154
  return Promise.all([
151
155
  api.queryUiTarget(${rootTarget}),
152
- api.queryUiTarget(${controlTarget})
156
+ api.queryUiTarget(${controlTarget}),
157
+ api.queryUiTarget(${choiceTarget})
153
158
  ]).then(function(values){
154
- return JSON.stringify({ root: values[0], control: values[1] });
159
+ return JSON.stringify({ root: values[0], control: values[1], choice: values[2] });
155
160
  });
156
161
  })()`,
157
162
  );
158
163
  if (result?.unsupported) {
159
164
  throw new Error('metamask.perps.ensure_mode requires UI target queries.');
160
165
  }
161
- return result?.root?.visible === true && result?.control?.present === true;
166
+ return {
167
+ present: result?.root?.visible === true && result?.control?.present === true,
168
+ choiceVisible: result?.choice?.visible === true,
169
+ };
162
170
  }
163
171
 
164
- async function readModePresence(input, mode, deadline) {
172
+ async function readModeState(input, mode, deadline) {
165
173
  try {
166
- return await isModePresent(input, mode, deadline);
174
+ return await queryModeState(input, mode, deadline);
167
175
  } catch (error) {
168
176
  if (!isTargetTransition(error)) throw error;
169
177
  return null;
@@ -181,14 +189,15 @@ export async function ensureMode(input) {
181
189
  Number(input.node?.timeout_ms ?? 30_000),
182
190
  );
183
191
  let switched = false;
192
+ let choiceSelected = false;
184
193
 
185
194
  while (Date.now() < deadline) {
186
- const requestedModePresent = await readModePresence(
195
+ const requestedState = await readModeState(
187
196
  input,
188
197
  requestedMode,
189
198
  deadline,
190
199
  );
191
- if (requestedModePresent) {
200
+ if (requestedState?.present && !requestedState.choiceVisible) {
192
201
  return {
193
202
  action: input.action,
194
203
  mode: requestedMode,
@@ -197,13 +206,23 @@ export async function ensureMode(input) {
197
206
  };
198
207
  }
199
208
  if (Date.now() >= deadline) break;
200
- if (requestedModePresent !== null && !switched) {
201
- const otherModePresent = await readModePresence(
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(
202
221
  input,
203
222
  otherMode,
204
223
  deadline,
205
224
  );
206
- if (otherModePresent && Date.now() < deadline) {
225
+ if (otherState?.present && Date.now() < deadline) {
207
226
  // The visible control names the mode it will switch to, not the mode
208
227
  // currently selected.
209
228
  const pressed = await bridgeCommand(withRemainingDeadline(input, deadline), [
@@ -1818,7 +1837,7 @@ export async function startState(input) {
1818
1837
  const navigation = await applyStateNavigation(stateInput(), config);
1819
1838
  const provider = await ensureProvider(stateInput(), config);
1820
1839
  const network = await ensureNetwork(stateInput(), config);
1821
- const resolvedNavigation = provider.changed || network.changed
1840
+ let resolvedNavigation = provider.changed || network.changed
1822
1841
  ? await applyStateNavigation(stateInput(), config)
1823
1842
  : navigation;
1824
1843
  const modeInput = stateInput();
@@ -1833,6 +1852,9 @@ export async function startState(input) {
1833
1852
  },
1834
1853
  })
1835
1854
  : { skipped: true };
1855
+ if (marketMode.alreadySelected === false) {
1856
+ resolvedNavigation = await applyStateNavigation(stateInput(), config);
1857
+ }
1836
1858
  const tutorial = await applyTutorialState(stateInput(), config);
1837
1859
  const readyToTrade = await assertReadyToTrade(stateInput(), config);
1838
1860
  const balance = await assertBalance(stateInput(), config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.47.2",
3
+ "version": "0.47.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -52,6 +52,7 @@
52
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.",
53
53
  "details": [
54
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.",
55
56
  "Actions translate stable UI, CDP, or controller operations. Recipes compose them. Product code owns business rules."
56
57
  ],
57
58
  "commands": []