@deeeed/metamask-harness 0.92.23 → 0.92.25

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,22 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.92.25 - 2026-10-18
6
+
7
+ ### Changed
8
+
9
+ - Pin `@farmslot/handoff` to 0.3.2 exactly, like the other `@farmslot` packages. The `^0.3.1` range let a global install resolve 0.3.2 while CI tested the locked 0.3.1. 0.3.2 no longer treats opaque diagnostic session IDs or underscore-bearing recipe identifiers as secrets when it scrubs a learning package.
10
+
11
+ ### Fixed
12
+
13
+ - Mobile: `provision runway` (and `install --runway`, which uses it) leaves the simulator in the power state it found. A simulator that simctl reported Shutdown is booted for the install and shut down again afterwards, also when the install fails; one that was already booted, booting or in an unknown state is left running. Before, every provisioned simulator stayed booted until something stopped it, which kept idle simulators running on farm machines for days. The installed-app check now reads a Shutdown simulator's installed bundles from its data directory, since simctl cannot look inside a Shutdown device, so the identity-match skip and doctor's `app-installed-deps-pending` keep working on a simulator provisioning left off.
14
+
15
+ ## 0.92.24 - 2026-10-18
16
+
17
+ ### Fixed
18
+
19
+ - Mobile Perps: `teardown_state` and the strategy cleanup in `start_state` no longer fail on a market whose provider doesn't advertise TWAP (Lighter on Mobile 8.17.0, where `getTwapOrders` throws). With a selected market, the cleanup asks `PerpsController.getOrderCapabilities` (for the first selected market) and reads no TWAP orders when `twap` isn't listed. Its result then reports `twap: { status: "unavailable", reason: "provider-does-not-advertise-twap" }`. Callers no longer need `strategies: false` there. `mode: all`, unavailable capabilities and apps without the capability reader read TWAP as before.
20
+
5
21
  ## 0.92.23 - 2026-10-18
6
22
 
7
23
  ### Changed
@@ -115,10 +115,17 @@ async function provisionRunwayMobile(target, options) {
115
115
  const cacheRoot = options.cacheRoot ?? defaultRunwayCacheRoot();
116
116
  const cache = ensureCachedArtifact(repo, resolved.branch, resolved.runId, cacheRoot, options);
117
117
  log(options, `runway: installing ${cache.artifact.appPath} on ${sim.name}`);
118
- bootSimulator(sim.udid ?? sim.name, options);
119
- execFileSync("xcrun", ["simctl", "install", sim.udid ?? sim.name, cache.artifact.appPath], { stdio: ["ignore", "ignore", "pipe"] });
120
- preapproveDeepLinkScheme(sim.udid ?? sim.name, RUNWAY_IOS_METADATA.bundleId, options);
121
- const baselinePath = writeRunwayBaseline(resolvedTarget, slot, platform, resolved, cache.artifact, sim, false, options.runtimeDir);
118
+ const device = sim.udid ?? sim.name;
119
+ const wasBooted = simulatorBooted(device);
120
+ let baselinePath;
121
+ try {
122
+ bootSimulator(device, options);
123
+ execFileSync("xcrun", ["simctl", "install", device, cache.artifact.appPath], { stdio: ["ignore", "ignore", "pipe"] });
124
+ preapproveDeepLinkScheme(device, RUNWAY_IOS_METADATA.bundleId, options);
125
+ baselinePath = writeRunwayBaseline(resolvedTarget, slot, platform, resolved, cache.artifact, sim, false, options.runtimeDir);
126
+ } finally {
127
+ if (!wasBooted) shutdownSimulator(device, options);
128
+ }
122
129
  return {
123
130
  schemaVersion: 1,
124
131
  command: "provision",
@@ -444,6 +451,21 @@ function bootSimulator(device, options) {
444
451
  log(options, `runway: bootstatus wait for ${device} did not confirm; continuing to install`);
445
452
  }
446
453
  }
454
+ function simulatorBooted(device) {
455
+ try {
456
+ return findSimulator(device)?.state !== "Shutdown";
457
+ } catch {
458
+ return true;
459
+ }
460
+ }
461
+ function shutdownSimulator(device, options) {
462
+ try {
463
+ execFileSync("xcrun", ["simctl", "shutdown", device], { stdio: ["ignore", "ignore", "ignore"], timeout: 3e4 });
464
+ log(options, `runway: shut ${device} down again; it was off before provisioning`);
465
+ } catch (error) {
466
+ log(options, `runway: could not shut ${device} down again (${errorMessage(error)}); it may still be running`);
467
+ }
468
+ }
447
469
  function restartSimulator(device, options) {
448
470
  log(options, `runway: simulator ${device} did not answer the app probe; restarting it once`);
449
471
  execFileSync("xcrun", ["simctl", "shutdown", device], {
@@ -495,12 +517,15 @@ function ensureSimulator(name, runtime, deviceType) {
495
517
  const udid = execFileSync("xcrun", ["simctl", "create", name, resolvedDeviceType, resolvedRuntime], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
496
518
  return { name, udid: udid || name, created: true, runtime: resolvedRuntime, deviceType: resolvedDeviceType };
497
519
  }
498
- function findSimulator(target) {
520
+ function listSimulatorDevices() {
499
521
  const data = execJson(
500
522
  "xcrun",
501
523
  ["simctl", "list", "devices", "--json"]
502
524
  );
503
- const devices = Object.values(data.devices ?? {}).flat().filter((device) => device.isAvailable !== false && Boolean(device.name && device.udid));
525
+ return Object.values(data.devices ?? {}).flat().filter((device) => device.isAvailable !== false && Boolean(device.name && device.udid));
526
+ }
527
+ function findSimulator(target) {
528
+ const devices = listSimulatorDevices();
504
529
  const exact = devices.find((device) => device.udid.toLowerCase() === target.toLowerCase());
505
530
  if (exact) return exact;
506
531
  const matches = target === "booted" ? devices.filter((device) => device.state === "Booted") : devices.filter((device) => device.name === target);
@@ -588,6 +613,8 @@ function readInstalledBaselineArtifact(target, sim, runtimeDir) {
588
613
  return { runId, record };
589
614
  }
590
615
  function appInstalled(device) {
616
+ const shutdown = shutdownSimulatorData(device);
617
+ if (shutdown) return bundleInstalledOnDisk(shutdown, RUNWAY_IOS_METADATA.bundleId);
591
618
  try {
592
619
  execFileSync("xcrun", ["simctl", "get_app_container", device, RUNWAY_IOS_METADATA.bundleId, "app"], {
593
620
  stdio: ["ignore", "ignore", "ignore"],
@@ -601,6 +628,43 @@ function appInstalled(device) {
601
628
  return false;
602
629
  }
603
630
  }
631
+ function shutdownSimulatorData(device) {
632
+ let sim;
633
+ try {
634
+ sim = findSimulator(device);
635
+ } catch {
636
+ return void 0;
637
+ }
638
+ return sim?.state === "Shutdown" ? sim.dataPath : void 0;
639
+ }
640
+ function bundleInstalledOnDisk(dataPath, bundleId) {
641
+ const root = path.join(dataPath, "Containers", "Bundle", "Application");
642
+ if (!fs.existsSync(root)) return false;
643
+ for (const container of fs.readdirSync(root)) {
644
+ const dir = path.join(root, container);
645
+ let entries;
646
+ try {
647
+ entries = fs.readdirSync(dir);
648
+ } catch {
649
+ continue;
650
+ }
651
+ for (const app of entries.filter((entry) => entry.endsWith(".app"))) {
652
+ const plist = path.join(dir, app, "Info.plist");
653
+ if (!fs.existsSync(plist)) continue;
654
+ try {
655
+ const id = execFileSync("plutil", ["-extract", "CFBundleIdentifier", "raw", "-o", "-", plist], {
656
+ encoding: "utf8",
657
+ stdio: ["ignore", "pipe", "ignore"],
658
+ timeout: 5e3
659
+ }).trim();
660
+ if (id === bundleId) return true;
661
+ } catch {
662
+ continue;
663
+ }
664
+ }
665
+ }
666
+ return false;
667
+ }
604
668
  function appInstalledWithRecovery(device, options) {
605
669
  try {
606
670
  return appInstalled(device);
@@ -428,7 +428,16 @@ async function readOpenOrders(input) {
428
428
  );
429
429
  }
430
430
 
431
+ const TWAP_NOT_ADVERTISED = 'provider-does-not-advertise-twap';
432
+
431
433
  async function readStrategyOrders(input) {
434
+ // Where the provider does not advertise TWAP (Lighter on Mobile 8.17),
435
+ // MetaMask cannot place TWAP, so there is none to clean, and getTwapOrders
436
+ // throws. Support is per provider today, so the first selected market speaks
437
+ // for the read.
438
+ const market = configuredSymbols(input, [])[0] ?? null;
439
+ const provider = typeof input.node?.provider === 'string' ? input.node.provider.toLowerCase() : null;
440
+ const capabilityQuery = market ? { symbol: market, ...(provider ? { providerId: provider } : {}) } : null;
432
441
  return retryPerpsClientRead(
433
442
  async () => {
434
443
  const orders = await evalAsync(
@@ -438,14 +447,27 @@ async function readStrategyOrders(input) {
438
447
  if (!controller || typeof controller.getTwapOrders !== 'function' || typeof controller.getChaseOrders !== 'function') {
439
448
  throw new Error('PerpsController strategy readers are unavailable; cannot clean live TWAP and Chase orders.');
440
449
  }
450
+ var capabilityQuery = ${JSON.stringify(capabilityQuery)};
451
+ var notAdvertised = ${JSON.stringify(TWAP_NOT_ADVERTISED)};
452
+ var twap = capabilityQuery && typeof controller.getOrderCapabilities === 'function'
453
+ ? Promise.resolve(controller.getOrderCapabilities(capabilityQuery)).then(function(capabilities){
454
+ var advertised = capabilities && capabilities.status === 'ready' && Array.isArray(capabilities.supportedStrategies)
455
+ ? capabilities.supportedStrategies
456
+ : null;
457
+ return advertised && advertised.indexOf('twap') < 0 ? notAdvertised : controller.getTwapOrders();
458
+ })
459
+ : controller.getTwapOrders();
441
460
  return Promise.all([
442
- controller.getTwapOrders(),
461
+ twap,
443
462
  controller.getChaseOrders()
444
463
  ]).then(function(values){
445
- if (!Array.isArray(values[0]) || !Array.isArray(values[1])) {
464
+ var skipped = values[0] === notAdvertised;
465
+ if (!(skipped || Array.isArray(values[0])) || !Array.isArray(values[1])) {
446
466
  throw new Error('PerpsController strategy readers returned a non-array result.');
447
467
  }
448
- return JSON.stringify({ twap: values[0], chase: values[1] });
468
+ return JSON.stringify(skipped
469
+ ? { twap: [], twapUnavailable: notAdvertised, chase: values[1] }
470
+ : { twap: values[0], chase: values[1] });
449
471
  });
450
472
  })()`,
451
473
  );
@@ -574,6 +596,7 @@ async function closeStrategyOrders(input) {
574
596
  chaseCount: 0,
575
597
  acceptedCount: 0,
576
598
  alreadyAbsentCount: 0,
599
+ ...twapEvidence(orders),
577
600
  proofPath: 'mobile-perps-controller-cancel-strategies',
578
601
  };
579
602
  }
@@ -641,10 +664,17 @@ async function closeStrategyOrders(input) {
641
664
  chaseCount: chaseOrders.length,
642
665
  acceptedCount: result.filter((entry) => entry.success === true).length,
643
666
  alreadyAbsentCount: result.filter(isAlreadyAbsentStrategyResult).length,
667
+ ...twapEvidence(orders),
644
668
  proofPath: 'mobile-perps-controller-cancel-strategies',
645
669
  };
646
670
  }
647
671
 
672
+ // A TWAP count of 0 must not read as "checked and absent" when TWAP was not
673
+ // readable at all.
674
+ function twapEvidence(orders) {
675
+ return orders.twapUnavailable ? { twap: { status: 'unavailable', reason: orders.twapUnavailable } } : {};
676
+ }
677
+
648
678
  export function clearPerformanceCachesExpression() {
649
679
  return `(function(){
650
680
  var metroRequire = globalThis.__r;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.92.23",
3
+ "version": "0.92.25",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -26,7 +26,7 @@
26
26
  "@farmslot/adapter-sdk": "0.18.0",
27
27
  "@farmslot/adapter-web": "0.10.15",
28
28
  "@farmslot/agent-runtime": "0.29.0",
29
- "@farmslot/handoff": "^0.3.1",
29
+ "@farmslot/handoff": "0.3.2",
30
30
  "@farmslot/protocol": "0.54.0",
31
31
  "@farmslot/recipe-cli": "0.23.1",
32
32
  "@farmslot/recipe-runner": "0.39.0",