@deeeed/metamask-harness 0.92.21 → 0.92.22

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,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.92.22 - 2026-10-18
6
+
7
+ ### Fixed
8
+
9
+ - Mobile: the recipe HUD no longer satisfies or blocks a text-only `ui.wait_for` whose own HUD text (badge, intent or detail) contains the asserted text. Each poll hides the HUD (an earlier node's HUD included), then checks every 50 ms, for about 1 s, that the intent has left the app tree before it queries. It hides again if a late HUD update brings the HUD back. That node's HUD comes back with the next update. Such a wait reports `hud: "hidden"`. A poll that can't confirm the HUD left decides nothing. If the app itself shows the whole intent text, the wait times out and says so; reword the intent. Before, `expected: absent` never passed and `expected: visible` could pass on the HUD text alone. Waits with a `test_id` and waits that don't collide with the HUD are unchanged.
10
+
5
11
  ## 0.92.21 - 2026-10-17
6
12
 
7
13
  ### Added
@@ -1,5 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
+ import { hudIntentDisplay } from "@farmslot/protocol/recipe";
3
4
  import { bridgeCommand, evalAsync, evalSync, MOBILE_BRIDGE_ERROR_CODES, selectBridgeStatusEntry, simulatorScreenshot } from "../../../library/actions/mobile/platform/bridge.mjs";
4
5
  import { observeNativeUi } from "../../../library/actions/mobile/platform/observe-ui.mjs";
5
6
  import { nativeAgentDeviceStateDir } from "../../../library/actions/mobile/platform/native-session-name.mjs";
@@ -188,6 +189,16 @@ async function hiddenRouteOnly(input, probeInput, payload, testId, deadline) {
188
189
  const nativeVisible = visible.items.some((item) => isRecord(item) && item.test_id === testId && item.visible !== false);
189
190
  return nativeVisible ? null : { hiddenRoute: true, nativeVisible: false };
190
191
  }
192
+ const HUD_HIDE_SETTLE_MS = 1e3;
193
+ function hudIntentCollidingWith(payload, testId, expectedText) {
194
+ if (testId || !expectedText || typeof payload.intent !== "string") return null;
195
+ const intent = hudIntentDisplay(payload.intent).text;
196
+ if (!intent) return null;
197
+ const detail = typeof payload.detail === "string" ? payload.detail : "";
198
+ const needle = expectedText.toLowerCase();
199
+ const inBadge = ["run", "pass", "fail"].some((word) => `${word} 0/0`.includes(needle.replace(/\d+/gu, "0")));
200
+ return inBadge || [intent, detail].some((text) => text.toLowerCase().includes(needle)) ? intent : null;
201
+ }
191
202
  async function waitForMobileTarget(input, payload) {
192
203
  const timeoutMs = Number(payload.timeout_ms ?? payload.timeoutMs ?? 1e4);
193
204
  const deadline = Date.now() + timeoutMs;
@@ -197,13 +208,43 @@ async function waitForMobileTarget(input, payload) {
197
208
  ...expectedText && textMatch === "contains" ? { textContains: expectedText } : {},
198
209
  visibility
199
210
  };
211
+ const hudIntent = hudIntentCollidingWith(payload, testId, expectedText);
200
212
  const queryExpression = `(function(){
201
213
  const api = globalThis.__AGENTIC__;
202
214
  if (!api || typeof api.queryUiTarget !== 'function') {
203
215
  return Promise.resolve(JSON.stringify({ unsupported: true }));
204
216
  }
205
- return api.queryUiTarget(${JSON.stringify(query)}).then(function(result){
206
- return JSON.stringify(result);
217
+ const query = function(){
218
+ return Promise.resolve(api.queryUiTarget(${JSON.stringify(query)}));
219
+ };
220
+ const hudIntent = ${JSON.stringify(hudIntent)};
221
+ if (hudIntent === null || typeof api.hideStep !== 'function') {
222
+ return query().then(function(result){ return JSON.stringify(result); });
223
+ }
224
+ const hudShown = function(){
225
+ return Promise.resolve(api.queryUiTarget({ textContains: hudIntent, visibility: 'tree' }))
226
+ .then(function(found){ return Boolean(found && found.present); });
227
+ };
228
+ // Hide, let React commit it, then look: an earlier node's HUD can still be
229
+ // up, and the tree only drops it after the commit. Hide again while this
230
+ // node's intent is shown, since a late HUD update can land after a hide.
231
+ const settle = function(waitedMs){
232
+ api.hideStep();
233
+ return new Promise(function(resolve){ setTimeout(resolve, 50); })
234
+ .then(hudShown)
235
+ .then(function(shown){
236
+ if (!shown) return true;
237
+ if (waitedMs + 50 >= ${HUD_HIDE_SETTLE_MS}) return false;
238
+ return settle(waitedMs + 50);
239
+ });
240
+ };
241
+ return settle(0).then(function(hidden){
242
+ if (!hidden) return JSON.stringify({ hudShown: true });
243
+ return query().then(function(result){
244
+ return hudShown().then(function(shown){
245
+ return JSON.stringify(shown ? { hudShown: true } : Object.assign({}, result, { hud: 'hidden' }));
246
+ });
247
+ });
207
248
  });
208
249
  })()`;
209
250
  const textExpression = testId ? `(function(){
@@ -241,6 +282,10 @@ async function waitForMobileTarget(input, payload) {
241
282
  );
242
283
  }
243
284
  lastResult = result;
285
+ if (result.hudShown === true) {
286
+ await sleep(100);
287
+ continue;
288
+ }
244
289
  let present = result.present === true;
245
290
  let visible = result.visible === true;
246
291
  let cachedRoute = null;
@@ -280,7 +325,8 @@ async function waitForMobileTarget(input, payload) {
280
325
  visible,
281
326
  visibility,
282
327
  ...cachedRoute ?? {},
283
- ...isRecord(result.rect) ? { rect: result.rect } : {}
328
+ ...isRecord(result.rect) ? { rect: result.rect } : {},
329
+ ...typeof result.hud === "string" ? { hud: result.hud } : {}
284
330
  };
285
331
  }
286
332
  await sleep(250);
@@ -291,8 +337,9 @@ async function waitForMobileTarget(input, payload) {
291
337
  ].filter(Boolean).join(" with ");
292
338
  const exactReason = textMatch === "exact" ? `; last text=${JSON.stringify(lastText)}` : "";
293
339
  const bridgeReason = lastBridgeError ? `; last bridge error=${traceText(lastBridgeError)}` : "";
340
+ const hudReason = lastResult?.hudShown === true ? `; the recipe HUD showing this node's intent never left the app tree, so the text was not checked without it (if the app itself shows the intent text, reword the intent)` : "";
294
341
  throw new Error(
295
- `Timed out waiting for mobile ${target} to be ${expected}; last result=${JSON.stringify(lastResult)}${exactReason}${bridgeReason}.`
342
+ `Timed out waiting for mobile ${target} to be ${expected}; last result=${JSON.stringify(lastResult)}${exactReason}${bridgeReason}${hudReason}.`
296
343
  );
297
344
  }
298
345
  const MOBILE_BRIDGE_HANDLERS = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.92.21",
3
+ "version": "0.92.22",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"