@deeeed/metamask-harness 0.42.0 → 0.43.0

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.
Files changed (30) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +5 -0
  3. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +59 -21
  4. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +3 -2
  5. package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +2 -0
  6. package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +194 -25
  7. package/adapters/mobile/bridge-runtime/lib/config.cjs +14 -4
  8. package/adapters/mobile/start-console-forwarder.sh +5 -1
  9. package/dist/adapters/extension/browser-cdp.js +174 -0
  10. package/dist/adapters/extension/network-observer.js +19 -110
  11. package/dist/adapters/extension/performance-observer.js +75 -0
  12. package/dist/adapters/mobile/frame-metrics.js +45 -0
  13. package/dist/adapters/mobile/performance-observer.js +43 -0
  14. package/dist/adapters/performance/cdp-trace.js +342 -0
  15. package/dist/adapters/performance/js-task-metrics.js +35 -0
  16. package/dist/adapters.js +17 -2
  17. package/dist/artifact-files.js +92 -0
  18. package/dist/async.js +19 -0
  19. package/dist/commands/call.js +23 -2
  20. package/dist/commands/run-engine.js +18 -0
  21. package/dist/commands/run.js +20 -1
  22. package/dist/network-observation.js +59 -47
  23. package/dist/performance-observation.js +465 -0
  24. package/docs/PERFORMANCE-CAPTURE.md +33 -0
  25. package/docs/RECIPES.md +7 -0
  26. package/library/actions/mobile/platform/bridge.mjs +3 -0
  27. package/library/manifests/extension.action-manifest.json +85 -0
  28. package/library/manifests/mobile.action-manifest.json +97 -0
  29. package/package.json +1 -1
  30. package/scripts/site-contrast.mjs +43 -27
@@ -8,6 +8,7 @@ import path from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import bridgeErrors from '../../../../adapters/mobile/bridge-runtime/lib/bridge-errors.cjs';
10
10
  import brokerModule from '../../../../adapters/mobile/bridge-runtime/lib/cdp-broker.cjs';
11
+ import configModule from '../../../../adapters/mobile/bridge-runtime/lib/config.cjs';
11
12
  import { resolveMobileToolPath } from './tool-paths.mjs';
12
13
 
13
14
  const {
@@ -18,6 +19,7 @@ const {
18
19
  parseErrorMarker,
19
20
  } = bridgeErrors;
20
21
  const { brokerSocketPath } = brokerModule;
22
+ const { resolvePort } = configModule;
21
23
 
22
24
  // Re-exported so the TS adapter classifies on the same code constants without a
23
25
  // second import path into the cjs bridge-runtime.
@@ -434,6 +436,7 @@ export async function bridgeCommand(input, args) {
434
436
  const env = await bridgeEnv(input);
435
437
  const brokerSocket = brokerSocketPath(
436
438
  path.dirname(resolveBridgeLockPath(input, env)),
439
+ resolvePort(env, input.context.projectRoot),
437
440
  );
438
441
  if (adapterActionActive && !existsSync(brokerSocket)) {
439
442
  env.CDP_BRIDGE_LOCK_OWNER_PID = await acquireActionBridgeLock(input, env);
@@ -1207,6 +1207,91 @@
1207
1207
  },
1208
1208
  "execution_capabilities": []
1209
1209
  },
1210
+ "app.performance_capture": {
1211
+ "description": "Capture bounded CDP renderer frame metrics and separately labelled JavaScript work across Recipe Protocol v1 node boundaries.",
1212
+ "examples": [
1213
+ {
1214
+ "action": "app.performance_capture",
1215
+ "phase": "start",
1216
+ "id": "homepage-scroll",
1217
+ "max_duration_ms": 300000,
1218
+ "intent": "Start the bounded UI smoothness window",
1219
+ "next": "exercise-flow"
1220
+ },
1221
+ {
1222
+ "action": "app.performance_capture",
1223
+ "phase": "end",
1224
+ "id": "homepage-scroll",
1225
+ "artifact_path": "performance/homepage-scroll.json",
1226
+ "html_path": "performance/homepage-scroll.html",
1227
+ "intent": "End and index the UI smoothness window",
1228
+ "next": "assert-performance"
1229
+ }
1230
+ ],
1231
+ "schema": {
1232
+ "type": "object",
1233
+ "properties": {
1234
+ "phase": {
1235
+ "type": "string",
1236
+ "enum": ["start", "end"]
1237
+ },
1238
+ "id": { "type": "string" },
1239
+ "max_duration_ms": {
1240
+ "type": "integer",
1241
+ "minimum": 1,
1242
+ "description": "Maximum accepted at runtime: 3600000 ms."
1243
+ },
1244
+ "artifact_path": { "type": "string" },
1245
+ "html_path": { "type": "string" }
1246
+ },
1247
+ "required": ["phase", "id"],
1248
+ "additionalProperties": false
1249
+ },
1250
+ "execution_capabilities": ["host-read-export"]
1251
+ },
1252
+ "app.performance_assert": {
1253
+ "description": "Assert a previously indexed app.performance_capture summary without hiding unavailable or partial source coverage.",
1254
+ "examples": [
1255
+ {
1256
+ "action": "app.performance_assert",
1257
+ "id": "homepage-scroll",
1258
+ "artifact_path": "performance/homepage-scroll.json",
1259
+ "required_status": ["complete", "partial"],
1260
+ "minimum_frame_count": 1,
1261
+ "require_native_ui_when_supported": true,
1262
+ "required_node_ids": ["scroll-homepage"],
1263
+ "intent": "Assert the indexed UI smoothness summary",
1264
+ "next": "done"
1265
+ }
1266
+ ],
1267
+ "schema": {
1268
+ "type": "object",
1269
+ "properties": {
1270
+ "id": { "type": "string" },
1271
+ "artifact_path": { "type": "string" },
1272
+ "required_status": {
1273
+ "type": "array",
1274
+ "items": {
1275
+ "type": "string",
1276
+ "enum": ["complete", "partial", "unavailable"]
1277
+ }
1278
+ },
1279
+ "minimum_frame_count": {
1280
+ "type": "integer",
1281
+ "minimum": 0,
1282
+ "description": "Minimum MetaMask renderer frame count. JavaScript task count cannot satisfy this assertion."
1283
+ },
1284
+ "require_native_ui_when_supported": { "type": "boolean" },
1285
+ "required_node_ids": {
1286
+ "type": "array",
1287
+ "items": { "type": "string" }
1288
+ }
1289
+ },
1290
+ "required": ["id"],
1291
+ "additionalProperties": false
1292
+ },
1293
+ "execution_capabilities": ["host-read-export"]
1294
+ },
1210
1295
  "app.status": {
1211
1296
  "description": "Report the adapter's static status — platform, project root, resolved checkout shape, and headless compatibility mode (no live route or account).",
1212
1297
  "examples": [
@@ -1219,6 +1219,103 @@
1219
1219
  },
1220
1220
  "execution_capabilities": []
1221
1221
  },
1222
+ "app.performance_capture": {
1223
+ "description": "Capture bounded CDP native UI frame metrics and separately labelled JavaScript work across Recipe Protocol v1 node boundaries.",
1224
+ "examples": [
1225
+ {
1226
+ "action": "app.performance_capture",
1227
+ "phase": "start",
1228
+ "id": "homepage-scroll",
1229
+ "max_duration_ms": 300000,
1230
+ "intent": "Start the bounded UI smoothness window",
1231
+ "next": "exercise-flow"
1232
+ },
1233
+ {
1234
+ "action": "app.performance_capture",
1235
+ "phase": "end",
1236
+ "id": "homepage-scroll",
1237
+ "artifact_path": "performance/homepage-scroll.json",
1238
+ "html_path": "performance/homepage-scroll.html",
1239
+ "intent": "End and index the UI smoothness window",
1240
+ "next": "assert-performance"
1241
+ }
1242
+ ],
1243
+ "schema": {
1244
+ "type": "object",
1245
+ "properties": {
1246
+ "phase": {
1247
+ "type": "string",
1248
+ "enum": ["start", "end"]
1249
+ },
1250
+ "id": {
1251
+ "type": "string"
1252
+ },
1253
+ "max_duration_ms": {
1254
+ "type": "integer",
1255
+ "minimum": 1,
1256
+ "description": "Maximum accepted at runtime: 3600000 ms."
1257
+ },
1258
+ "artifact_path": {
1259
+ "type": "string"
1260
+ },
1261
+ "html_path": {
1262
+ "type": "string"
1263
+ }
1264
+ },
1265
+ "required": ["phase", "id"],
1266
+ "additionalProperties": false
1267
+ },
1268
+ "execution_capabilities": ["host-read-export"]
1269
+ },
1270
+ "app.performance_assert": {
1271
+ "description": "Assert a previously indexed app.performance_capture summary without hiding unavailable or partial source coverage.",
1272
+ "examples": [
1273
+ {
1274
+ "action": "app.performance_assert",
1275
+ "id": "homepage-scroll",
1276
+ "artifact_path": "performance/homepage-scroll.json",
1277
+ "required_status": ["complete", "partial"],
1278
+ "minimum_frame_count": 1,
1279
+ "require_native_ui_when_supported": true,
1280
+ "required_node_ids": ["scroll-homepage"],
1281
+ "intent": "Assert the indexed UI smoothness summary",
1282
+ "next": "done"
1283
+ }
1284
+ ],
1285
+ "schema": {
1286
+ "type": "object",
1287
+ "properties": {
1288
+ "id": {
1289
+ "type": "string"
1290
+ },
1291
+ "artifact_path": {
1292
+ "type": "string"
1293
+ },
1294
+ "required_status": {
1295
+ "type": "array",
1296
+ "items": {
1297
+ "type": "string",
1298
+ "enum": ["complete", "partial", "unavailable"]
1299
+ }
1300
+ },
1301
+ "minimum_frame_count": {
1302
+ "type": "integer",
1303
+ "minimum": 0,
1304
+ "description": "Minimum native UI frame count. JavaScript task count cannot satisfy this assertion."
1305
+ },
1306
+ "require_native_ui_when_supported": {
1307
+ "type": "boolean"
1308
+ },
1309
+ "required_node_ids": {
1310
+ "type": "array",
1311
+ "items": { "type": "string" }
1312
+ }
1313
+ },
1314
+ "required": ["id"],
1315
+ "additionalProperties": false
1316
+ },
1317
+ "execution_capabilities": ["host-read-export"]
1318
+ },
1222
1319
  "app.status": {
1223
1320
  "description": "Report the adapter's static status — platform, project root, resolved checkout shape, and headless compatibility mode (no live route or account).",
1224
1321
  "examples": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.42.0",
3
+ "version": "0.43.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -272,6 +272,18 @@ function findChrome() {
272
272
  }
273
273
 
274
274
  async function startChrome() {
275
+ let lastError;
276
+ for (let attempt = 0; attempt < 2; attempt += 1) {
277
+ try {
278
+ return await startChromeOnce();
279
+ } catch (error) {
280
+ lastError = error;
281
+ }
282
+ }
283
+ throw lastError;
284
+ }
285
+
286
+ async function startChromeOnce() {
275
287
  const binary = findChrome();
276
288
  const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'site-contrast-'));
277
289
  const child = spawn(
@@ -279,6 +291,7 @@ async function startChrome() {
279
291
  [
280
292
  '--headless=new',
281
293
  '--disable-gpu',
294
+ '--disable-dev-shm-usage',
282
295
  '--no-first-run',
283
296
  '--no-default-browser-check',
284
297
  '--remote-debugging-port=0',
@@ -290,35 +303,38 @@ async function startChrome() {
290
303
 
291
304
  // Chrome writes the chosen port to DevToolsActivePort once it is listening.
292
305
  const portFile = path.join(userDataDir, 'DevToolsActivePort');
293
- const port = await waitFor(() => {
294
- if (!fs.existsSync(portFile)) return null;
295
- const line = fs.readFileSync(portFile, 'utf8').split('\n')[0].trim();
296
- return line ? Number(line) : null;
297
- }, 15000, 'Chrome did not report a debugging port');
298
-
299
- const version = await getJson(`http://127.0.0.1:${port}/json/version`);
300
- return {
301
- endpoint: version.webSocketDebuggerUrl,
302
- port,
303
- close: async () => {
304
- child.kill();
305
- // Chrome holds the profile dir open until it exits; removing it early
306
- // fails with ENOTEMPTY. Wait for exit, and never fail the gate on cleanup.
307
- await new Promise((done) => {
308
- if (child.exitCode !== null || child.signalCode !== null) return done();
309
- const timer = setTimeout(done, 5000);
310
- child.once('exit', () => {
311
- clearTimeout(timer);
312
- done();
313
- });
306
+ const close = async () => {
307
+ child.kill();
308
+ // Chrome holds the profile dir open until it exits; removing it early
309
+ // fails with ENOTEMPTY. Wait for exit, and never fail the gate on cleanup.
310
+ await new Promise((done) => {
311
+ if (child.exitCode !== null || child.signalCode !== null) return done();
312
+ const timer = setTimeout(done, 5000);
313
+ child.once('exit', () => {
314
+ clearTimeout(timer);
315
+ done();
314
316
  });
315
- try {
316
- fs.rmSync(userDataDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
317
- } catch {
318
- /* a leftover temp profile is not worth failing a contrast run over */
319
- }
320
- },
317
+ });
318
+ try {
319
+ fs.rmSync(userDataDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
320
+ } catch {
321
+ /* a leftover temp profile is not worth failing a contrast run over */
322
+ }
321
323
  };
324
+
325
+ try {
326
+ const port = await waitFor(() => {
327
+ if (!fs.existsSync(portFile)) return null;
328
+ const line = fs.readFileSync(portFile, 'utf8').split('\n')[0].trim();
329
+ return line ? Number(line) : null;
330
+ }, 15000, 'Chrome did not report a debugging port');
331
+
332
+ const version = await getJson(`http://127.0.0.1:${port}/json/version`);
333
+ return { endpoint: version.webSocketDebuggerUrl, port, close };
334
+ } catch (error) {
335
+ await close();
336
+ throw error;
337
+ }
322
338
  }
323
339
 
324
340
  async function auditPage(browserEndpoint, url) {