@deeeed/metamask-harness 0.47.3 → 0.47.4

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.47.4 - 2026-09-05
6
+
7
+ ### Fixed
8
+
9
+ - Verify Mobile reloads through execution-context replacement when React Native retains its Hermes target ID, and advance broker runtime identity on context reset for lifecycle readiness checks.
10
+
5
11
  ## 0.47.3 - 2026-09-05
6
12
 
7
13
  ### Fixed
@@ -862,6 +862,10 @@ function createCdpBroker({
862
862
  }
863
863
  },
864
864
  onCdpEvent(deviceId, method, params) {
865
+ if (method === 'Runtime.executionContextsCleared') {
866
+ const target = knownTargets.get(deviceId);
867
+ if (target) knownTargets.set(deviceId, { ...target, generation: target.generation + 1 });
868
+ }
865
869
  for (const socket of clients) {
866
870
  if (subscriptions.get(socket)?.has(`${deviceId}\u0000${method}`)) {
867
871
  writeMessage(socket, { type: 'event', deviceId, method, params });
@@ -1,12 +1,54 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawnSync } from 'node:child_process';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
4
6
  import { createRequire } from 'node:module';
5
7
  import { resolveMobileToolPath } from '../../library/actions/mobile/platform/tool-paths.mjs';
6
8
 
7
9
  const require = createRequire(import.meta.url);
8
10
  const WebSocket = require('ws');
9
- const { discoverTarget } = require('./bridge-runtime/lib/target-discovery.cjs');
11
+ const { discoverTarget, rankRuntimeCandidates } = require('./bridge-runtime/lib/target-discovery.cjs');
12
+ const { brokerSocketPath, createBrokerClient, deviceIdFromUrl } = require('./bridge-runtime/lib/cdp-broker.cjs');
13
+ const { createWSClient } = require('./bridge-runtime/lib/ws-client.cjs');
14
+
15
+ async function observeRuntimeReset(port, target) {
16
+ const socketPath = brokerSocketPath(
17
+ process.env.RECIPE_RUNTIME_DIR || path.join(process.cwd(), 'temp/recipe/runtime'),
18
+ port,
19
+ );
20
+ const client = fs.existsSync(socketPath)
21
+ ? await createBrokerClient(socketPath, deviceIdFromUrl(target.wsUrl), 5000)
22
+ : await createWSClient(target.wsUrl, 5000);
23
+ let armed = false;
24
+ let cleared = false;
25
+ let recreated = false;
26
+ client.on('Runtime.executionContextsCleared', () => {
27
+ if (armed) cleared = true;
28
+ });
29
+ client.on('Runtime.executionContextCreated', () => {
30
+ if (armed && cleared) recreated = true;
31
+ });
32
+ try {
33
+ await client.send('Runtime.enable');
34
+ // Finish subscription/bootstrap before the reload request can generate events.
35
+ await client.send('Runtime.evaluate', { expression: 'true', returnByValue: true });
36
+ armed = true;
37
+ } catch (error) {
38
+ client.close();
39
+ throw error;
40
+ }
41
+ return {
42
+ get recreated() { return recreated; },
43
+ async responsive(timeoutMs) {
44
+ const result = await client.send('Runtime.evaluate', {
45
+ expression: 'true', returnByValue: true,
46
+ }, timeoutMs);
47
+ return result?.result?.value === true;
48
+ },
49
+ close() { client.close(); },
50
+ };
51
+ }
10
52
 
11
53
  function parseArgs(argv) {
12
54
  const args = {
@@ -46,30 +88,38 @@ async function reload(port) {
46
88
  'The Android Hermes sampling profiler is active, so a runtime reload can abort the app. Use an explicit app.lifecycle restart.',
47
89
  );
48
90
  }
49
- await new Promise((resolve, reject) => {
50
- const socket = new WebSocket(`ws://127.0.0.1:${numericPort}/message`);
51
- const timeout = setTimeout(() => {
52
- socket.close();
53
- reject(new Error(`Metro reload timed out on port ${numericPort}`));
54
- }, 5000);
55
- socket.on('open', () => {
56
- socket.send(JSON.stringify({ method: 'reload', version: 2 }));
57
- setTimeout(() => {
58
- clearTimeout(timeout);
91
+ const observer = await observeRuntimeReset(numericPort, targetBefore);
92
+ let verification;
93
+ try {
94
+ await new Promise((resolve, reject) => {
95
+ const socket = new WebSocket(`ws://127.0.0.1:${numericPort}/message`);
96
+ const timeout = setTimeout(() => {
59
97
  socket.close();
60
- resolve();
61
- }, 50);
62
- });
63
- socket.on('error', () => {
64
- clearTimeout(timeout);
65
- reject(new Error(`Metro is not reachable on port ${numericPort}`));
98
+ reject(new Error(`Metro reload timed out on port ${numericPort}`));
99
+ }, 5000);
100
+ socket.on('open', () => {
101
+ socket.send(JSON.stringify({ method: 'reload', version: 2 }));
102
+ setTimeout(() => {
103
+ clearTimeout(timeout);
104
+ socket.close();
105
+ resolve();
106
+ }, 50);
107
+ });
108
+ socket.on('error', () => {
109
+ clearTimeout(timeout);
110
+ reject(new Error(`Metro is not reachable on port ${numericPort}`));
111
+ });
66
112
  });
67
- });
68
- const targetAfter = await waitForReload(
69
- numericPort,
70
- targetBefore.id,
71
- processBefore,
72
- );
113
+ verification = await waitForReload(
114
+ numericPort,
115
+ targetBefore,
116
+ processBefore,
117
+ observer,
118
+ );
119
+ } finally {
120
+ observer.close();
121
+ }
122
+ const { target: targetAfter, proof } = verification;
73
123
  const processAfter = processBefore ? readAndroidPid() : null;
74
124
  if (processBefore && processAfter !== processBefore) {
75
125
  throw new Error(
@@ -84,6 +134,7 @@ async function reload(port) {
84
134
  port: numericPort,
85
135
  targetBefore: targetBefore.id,
86
136
  targetAfter: targetAfter.id,
137
+ verification: proof,
87
138
  ...(processBefore
88
139
  ? {
89
140
  processBefore,
@@ -94,20 +145,33 @@ async function reload(port) {
94
145
  };
95
146
  }
96
147
 
97
- async function waitForReload(port, targetBefore, processBefore) {
148
+ async function waitForReload(port, targetBefore, processBefore, observer) {
98
149
  const deadline = Date.now() + 30_000;
150
+ let lastError;
99
151
  while (Date.now() < deadline) {
100
152
  if (processBefore && readAndroidPid() !== processBefore) {
101
153
  throw new Error(
102
154
  `Android app process ${processBefore} exited during reload. Inspect the native crash before using an explicit app.lifecycle restart.`,
103
155
  );
104
156
  }
105
- const target = await readHermesTarget(port).catch(() => null);
106
- if (target && target.id !== targetBefore) return target;
157
+ try {
158
+ const response = await fetch(`http://127.0.0.1:${port}/json/list`, {
159
+ signal: AbortSignal.timeout(Math.max(1, Math.min(1000, deadline - Date.now()))),
160
+ });
161
+ const targets = rankRuntimeCandidates(await response.json());
162
+ const target = targets.find((item) => item.deviceName === targetBefore.deviceName);
163
+ if (target && target.id !== targetBefore.id) return { target, proof: 'target-replaced' };
164
+ if (target && observer.recreated && await observer.responsive(Math.max(1, Math.min(1000, deadline - Date.now())))) {
165
+ return { target, proof: 'execution-context-recreated' };
166
+ }
167
+ } catch (error) {
168
+ // A reload temporarily removes the runtime; preserve the last failure if it never returns.
169
+ lastError = error;
170
+ }
107
171
  await new Promise((resolve) => setTimeout(resolve, 250));
108
172
  }
109
173
  throw new Error(
110
- `React Native did not expose a new Hermes target after reload on port ${port}`,
174
+ `React Native reload was not verified on port ${port}: no target replacement or responsive new execution context${lastError ? ` (${lastError.message})` : ''}.\nNext: mm-harness status --json`,
111
175
  );
112
176
  }
113
177
 
@@ -155,7 +219,7 @@ const args = parseArgs(process.argv.slice(2));
155
219
  reload(args.port)
156
220
  .then((result) => {
157
221
  if (args.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
158
- else console.log(`Reload requested through Metro :${result.port}`);
222
+ else console.log(`Reload verified through Metro :${result.port} (${result.verification})`);
159
223
  })
160
224
  .catch((error) => {
161
225
  const result = { ok: false, adapter: 'mobile', error: String(error?.message || error) };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.47.3",
3
+ "version": "0.47.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"