@deeeed/metamask-harness 0.41.1 → 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.
- package/CHANGELOG.md +35 -0
- package/README.md +12 -0
- package/adapters/manifest.json +8 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +108 -10
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +117 -16
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +2 -0
- package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +921 -0
- package/adapters/mobile/bridge-runtime/lib/config.cjs +14 -4
- package/adapters/mobile/bridge-runtime/lib/devtools-proxy.cjs +177 -0
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +13 -3
- package/adapters/mobile/reload-app.mjs +67 -0
- package/adapters/mobile/start-console-forwarder.sh +22 -3
- package/adapters/mobile/start-metro.sh +6 -11
- package/adapters/mobile/stop-metro.sh +10 -1
- package/adapters/shared/open-debug.mjs +172 -2
- package/dist/adapters/extension/browser-cdp.js +174 -0
- package/dist/adapters/extension/network-observer.js +209 -0
- package/dist/adapters/extension/performance-observer.js +75 -0
- package/dist/adapters/mobile/frame-metrics.js +45 -0
- package/dist/adapters/mobile/metro-env.js +0 -5
- package/dist/adapters/mobile/performance-observer.js +43 -0
- package/dist/adapters/mobile/prepare.js +1 -3
- package/dist/adapters/mobile/runtime-decision.js +6 -9
- package/dist/adapters/performance/cdp-trace.js +342 -0
- package/dist/adapters/performance/js-task-metrics.js +35 -0
- package/dist/adapters.js +29 -1
- package/dist/artifact-files.js +92 -0
- package/dist/async.js +19 -0
- package/dist/cli-commands.js +6 -3
- package/dist/cli.js +4 -0
- package/dist/command-contract.js +3 -0
- package/dist/commands/call.js +66 -20
- package/dist/commands/reload.js +80 -0
- package/dist/commands/run-engine.js +18 -0
- package/dist/commands/run.js +68 -22
- package/dist/mm-harness-cli.js +17 -1
- package/dist/network-observation.js +283 -0
- package/dist/performance-observation.js +465 -0
- package/docs/NETWORK-CAPTURE.md +98 -0
- package/docs/PERFORMANCE-CAPTURE.md +33 -0
- package/docs/RECIPES.md +17 -0
- package/library/actions/mobile/app/network_assert.mjs +14 -0
- package/library/actions/mobile/app/network_capture.mjs +72 -0
- package/library/actions/mobile/platform/bridge.mjs +10 -2
- package/library/actions/shared/app/network-artifact.mjs +10 -0
- package/library/actions/shared/app/network-assert.mjs +154 -0
- package/library/manifests/extension.action-manifest.json +173 -0
- package/library/manifests/mobile.action-manifest.json +204 -0
- package/library/recipes/mobile/perps/performance.recipe.json +11 -11
- package/package.json +1 -1
- package/scripts/completions.sh +2 -1
- package/scripts/site-contrast.mjs +43 -27
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import {
|
|
2
|
+
summarizeFrames
|
|
3
|
+
} from "../mobile/frame-metrics.js";
|
|
4
|
+
import { withTimeout } from "../../async.js";
|
|
5
|
+
import {
|
|
6
|
+
summarizeJavaScriptTasks
|
|
7
|
+
} from "./js-task-metrics.js";
|
|
8
|
+
const FRAME_BUDGET_MS = 1e3 / 60;
|
|
9
|
+
const MAX_TRACE_BYTES = 64 * 1024 * 1024;
|
|
10
|
+
const MAX_TRACE_EVENTS = 25e4;
|
|
11
|
+
const MOBILE_TRACE_CATEGORIES = [
|
|
12
|
+
"blink.user_timing",
|
|
13
|
+
"disabled-by-default-devtools.timeline.frame"
|
|
14
|
+
];
|
|
15
|
+
const EXTENSION_TRACE_CATEGORIES = [
|
|
16
|
+
"blink.user_timing",
|
|
17
|
+
"devtools.timeline",
|
|
18
|
+
"disabled-by-default-devtools.timeline",
|
|
19
|
+
"disabled-by-default-devtools.timeline.frame",
|
|
20
|
+
"v8.execute"
|
|
21
|
+
];
|
|
22
|
+
function createCdpTraceCollector(client, platform, marker) {
|
|
23
|
+
let active;
|
|
24
|
+
const offData = client.on("Tracing.dataCollected", (params) => {
|
|
25
|
+
if (!active || !Array.isArray(params.value)) return;
|
|
26
|
+
for (const value of params.value) {
|
|
27
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
28
|
+
const event = value;
|
|
29
|
+
const bytes = Buffer.byteLength(JSON.stringify(event));
|
|
30
|
+
if (active.events.length >= MAX_TRACE_EVENTS || active.bytes + bytes > MAX_TRACE_BYTES) {
|
|
31
|
+
active.overflow = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
active.events.push(event);
|
|
35
|
+
active.bytes += bytes;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
const offComplete = client.on("Tracing.tracingComplete", (params) => {
|
|
39
|
+
if (!active) return;
|
|
40
|
+
active.dataLossOccurred = params.dataLossOccurred === true;
|
|
41
|
+
active.complete?.();
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
async start(id) {
|
|
45
|
+
if (active) throw new Error("Only one CDP performance trace may be active.");
|
|
46
|
+
let complete;
|
|
47
|
+
const completePromise = new Promise((resolve) => {
|
|
48
|
+
complete = resolve;
|
|
49
|
+
});
|
|
50
|
+
active = {
|
|
51
|
+
id,
|
|
52
|
+
events: [],
|
|
53
|
+
bytes: 0,
|
|
54
|
+
overflow: false,
|
|
55
|
+
dataLossOccurred: false,
|
|
56
|
+
complete,
|
|
57
|
+
completePromise
|
|
58
|
+
};
|
|
59
|
+
let tracingStarted = false;
|
|
60
|
+
try {
|
|
61
|
+
await client.send(
|
|
62
|
+
"Tracing.start",
|
|
63
|
+
{
|
|
64
|
+
categories: (platform === "extension" ? EXTENSION_TRACE_CATEGORIES : MOBILE_TRACE_CATEGORIES).join(","),
|
|
65
|
+
transferMode: "ReportEvents"
|
|
66
|
+
},
|
|
67
|
+
15e3
|
|
68
|
+
);
|
|
69
|
+
tracingStarted = true;
|
|
70
|
+
const markerName = `mmh-clock-${id}-${Date.now()}`;
|
|
71
|
+
const sync = await marker(markerName);
|
|
72
|
+
active.markerName = markerName;
|
|
73
|
+
active.markerHostEpochMs = sync.hostEpochMs;
|
|
74
|
+
active.markerUncertaintyMs = sync.uncertaintyMs;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (tracingStarted) {
|
|
77
|
+
await client.send("Tracing.end", {}, 5e3).catch(() => void 0);
|
|
78
|
+
await withTimeout(active.completePromise, {
|
|
79
|
+
message: "CDP tracing cleanup did not report tracingComplete.",
|
|
80
|
+
timeoutMs: 1e3
|
|
81
|
+
}).catch(() => void 0);
|
|
82
|
+
}
|
|
83
|
+
active = void 0;
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
async end(id) {
|
|
88
|
+
const capture = active;
|
|
89
|
+
if (!capture || capture.id !== id) {
|
|
90
|
+
throw new Error(`Performance capture is not active: ${id}`);
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
await client.send("Tracing.end", {}, 15e3);
|
|
94
|
+
await withTimeout(capture.completePromise, {
|
|
95
|
+
message: "CDP tracing did not report tracingComplete.",
|
|
96
|
+
timeoutMs: 3e4
|
|
97
|
+
});
|
|
98
|
+
} finally {
|
|
99
|
+
active = void 0;
|
|
100
|
+
}
|
|
101
|
+
return parseTraceCapture({
|
|
102
|
+
platform,
|
|
103
|
+
events: capture.events,
|
|
104
|
+
markerName: capture.markerName,
|
|
105
|
+
markerHostEpochMs: capture.markerHostEpochMs,
|
|
106
|
+
markerUncertaintyMs: capture.markerUncertaintyMs,
|
|
107
|
+
dataLossOccurred: capture.dataLossOccurred,
|
|
108
|
+
overflow: capture.overflow,
|
|
109
|
+
unavailableReasons: [],
|
|
110
|
+
coverageGapReasons: []
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
async close() {
|
|
114
|
+
if (active) {
|
|
115
|
+
await client.send("Tracing.end", {}, 5e3).catch(() => void 0);
|
|
116
|
+
await withTimeout(active.completePromise, {
|
|
117
|
+
message: "CDP tracing cleanup did not report tracingComplete.",
|
|
118
|
+
timeoutMs: 1e3
|
|
119
|
+
}).catch(() => void 0);
|
|
120
|
+
active = void 0;
|
|
121
|
+
}
|
|
122
|
+
offData();
|
|
123
|
+
offComplete();
|
|
124
|
+
client.close();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function parseTraceCapture(capture) {
|
|
129
|
+
const marker = findClockMarker(capture.events, capture.markerName);
|
|
130
|
+
if (capture.platform === "extension" && !finite(marker?.pid)) {
|
|
131
|
+
const reason = "Extension trace marker did not identify a renderer process.";
|
|
132
|
+
return {
|
|
133
|
+
platform: capture.platform,
|
|
134
|
+
javascript: unavailableJavaScriptSource("cdp-js-runtime-tasks", reason),
|
|
135
|
+
nativeUi: unavailableNativeUiSource("cdp-native-frame-timings", reason),
|
|
136
|
+
trace: traceEvidence(capture, "unresolved")
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const scopedEvents = capture.platform === "extension" && finite(marker?.pid) ? capture.events.filter((event) => event.pid === marker.pid) : capture.events;
|
|
140
|
+
const scope = capture.platform !== "extension" ? "mobile-host" : finite(marker?.pid) ? "extension-renderer" : "unresolved";
|
|
141
|
+
const trace = traceEvidence(
|
|
142
|
+
{ ...capture, events: scopedEvents },
|
|
143
|
+
scope,
|
|
144
|
+
finite(marker?.pid) ? marker.pid : void 0
|
|
145
|
+
);
|
|
146
|
+
if (!finite(marker?.ts) || capture.markerHostEpochMs === void 0 || capture.markerUncertaintyMs === void 0) {
|
|
147
|
+
const reason = "CDP trace clock marker was unavailable.";
|
|
148
|
+
return {
|
|
149
|
+
platform: capture.platform,
|
|
150
|
+
javascript: unavailableJavaScriptSource("cdp-js-runtime-tasks", reason),
|
|
151
|
+
nativeUi: unavailableNativeUiSource(
|
|
152
|
+
"cdp-native-frame-timings",
|
|
153
|
+
reason
|
|
154
|
+
),
|
|
155
|
+
trace
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const traceToEpochMs = (timestampUs) => capture.markerHostEpochMs + (timestampUs - marker.ts) / 1e3;
|
|
159
|
+
const gapReasons = [
|
|
160
|
+
...capture.coverageGapReasons,
|
|
161
|
+
...capture.overflow ? ["CDP trace retention limit was reached."] : [],
|
|
162
|
+
...capture.dataLossOccurred ? ["CDP reported trace data loss."] : []
|
|
163
|
+
];
|
|
164
|
+
const nativeSamples = nativeFrameSamples(
|
|
165
|
+
scopedEvents,
|
|
166
|
+
capture.platform,
|
|
167
|
+
traceToEpochMs
|
|
168
|
+
);
|
|
169
|
+
const javascriptSamples = javascriptTaskSamples(
|
|
170
|
+
scopedEvents,
|
|
171
|
+
traceToEpochMs
|
|
172
|
+
);
|
|
173
|
+
const status = capture.overflow || capture.dataLossOccurred ? "partial" : "complete";
|
|
174
|
+
return {
|
|
175
|
+
platform: capture.platform,
|
|
176
|
+
javascript: javascriptSourceResult(
|
|
177
|
+
"cdp-js-runtime-tasks",
|
|
178
|
+
javascriptSamples,
|
|
179
|
+
status,
|
|
180
|
+
gapReasons,
|
|
181
|
+
traceEventNames(scopedEvents),
|
|
182
|
+
capture.markerUncertaintyMs
|
|
183
|
+
),
|
|
184
|
+
nativeUi: nativeUiSourceResult(
|
|
185
|
+
capture.platform === "extension" ? "chromium-cdp-frame-timings" : "react-native-cdp-frame-timings",
|
|
186
|
+
nativeSamples,
|
|
187
|
+
status,
|
|
188
|
+
gapReasons,
|
|
189
|
+
traceEventNames(scopedEvents),
|
|
190
|
+
capture.markerUncertaintyMs
|
|
191
|
+
),
|
|
192
|
+
trace
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function traceEvidence(capture, scope, rendererProcessId) {
|
|
196
|
+
const counts = /* @__PURE__ */ new Map();
|
|
197
|
+
for (const event of capture.events) {
|
|
198
|
+
const name = String(event.name ?? "");
|
|
199
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
beginFrameCount: counts.get("BeginFrame") ?? 0,
|
|
203
|
+
dataLossOccurred: capture.dataLossOccurred,
|
|
204
|
+
drawFrameCount: counts.get("DrawFrame") ?? 0,
|
|
205
|
+
overflow: capture.overflow,
|
|
206
|
+
profileChunkCount: counts.get("ProfileChunk") ?? 0,
|
|
207
|
+
...rendererProcessId === void 0 ? {} : { rendererProcessId },
|
|
208
|
+
runTaskCount: counts.get("RunTask") ?? 0,
|
|
209
|
+
scope,
|
|
210
|
+
totalEventCount: capture.events.length
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function nativeFrameSamples(events, platform, traceToEpochMs) {
|
|
214
|
+
const begins = /* @__PURE__ */ new Map();
|
|
215
|
+
const frames = [];
|
|
216
|
+
for (const event of events) {
|
|
217
|
+
if (event.name !== "BeginFrame" && event.name !== "DrawFrame") continue;
|
|
218
|
+
const sequence = frameSequence(event);
|
|
219
|
+
if (!sequence || !finite(event.ts)) continue;
|
|
220
|
+
if (event.name === "BeginFrame") {
|
|
221
|
+
begins.set(sequence, event);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const begin = begins.get(sequence);
|
|
225
|
+
if (!begin || !finite(begin.ts)) continue;
|
|
226
|
+
frames.push({ beginUs: begin.ts, endUs: event.ts });
|
|
227
|
+
}
|
|
228
|
+
frames.sort((first, second) => first.beginUs - second.beginUs);
|
|
229
|
+
if (platform === "ios") {
|
|
230
|
+
const samples = [];
|
|
231
|
+
for (let index = 1; index < frames.length; index += 1) {
|
|
232
|
+
const previous = frames[index - 1];
|
|
233
|
+
const current = frames[index];
|
|
234
|
+
if (!previous || !current) continue;
|
|
235
|
+
const durationMs = (current.beginUs - previous.beginUs) / 1e3;
|
|
236
|
+
if (durationMs <= 0) continue;
|
|
237
|
+
samples.push({
|
|
238
|
+
completedAtEpochMs: traceToEpochMs(current.beginUs),
|
|
239
|
+
durationMs,
|
|
240
|
+
janky: durationMs > FRAME_BUDGET_MS * 1.01
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return samples;
|
|
244
|
+
}
|
|
245
|
+
return frames.map((frame) => {
|
|
246
|
+
const durationMs = (frame.endUs - frame.beginUs) / 1e3;
|
|
247
|
+
return {
|
|
248
|
+
completedAtEpochMs: traceToEpochMs(frame.endUs),
|
|
249
|
+
durationMs,
|
|
250
|
+
janky: durationMs > FRAME_BUDGET_MS * 1.01
|
|
251
|
+
};
|
|
252
|
+
}).filter((sample) => sample.durationMs > 0);
|
|
253
|
+
}
|
|
254
|
+
function javascriptTaskSamples(events, traceToEpochMs) {
|
|
255
|
+
return events.filter(
|
|
256
|
+
(event) => event.name === "RunTask" && event.ph === "X" && finite(event.ts) && finite(event.dur) && event.dur > 0
|
|
257
|
+
).map((event) => ({
|
|
258
|
+
completedAtEpochMs: traceToEpochMs(event.ts + event.dur),
|
|
259
|
+
durationMs: event.dur / 1e3
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
function findClockMarker(events, markerName) {
|
|
263
|
+
if (!markerName) return void 0;
|
|
264
|
+
return events.find(
|
|
265
|
+
(candidate) => candidate.name === markerName || asRecord(candidate.args).sync_id === markerName || asRecord(asRecord(candidate.args).data).sync_id === markerName
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
function frameSequence(event) {
|
|
269
|
+
const args = asRecord(event.args);
|
|
270
|
+
const value = args.frameSeqId ?? asRecord(args.data).frameSeqId;
|
|
271
|
+
return value === void 0 ? void 0 : String(value);
|
|
272
|
+
}
|
|
273
|
+
function javascriptSourceResult(kind, samples, status, coverageGapReasons, observedEventNames, clockSyncUncertaintyMs) {
|
|
274
|
+
return {
|
|
275
|
+
status: samples.length > 0 ? status : "partial",
|
|
276
|
+
kind,
|
|
277
|
+
unavailableReasons: [],
|
|
278
|
+
coverageGapReasons: [
|
|
279
|
+
...coverageGapReasons,
|
|
280
|
+
...samples.length > 0 ? [] : [
|
|
281
|
+
`No ${kind} samples were recorded. Observed trace events: ${observedEventNames.join(", ") || "none"}.`
|
|
282
|
+
]
|
|
283
|
+
],
|
|
284
|
+
samples,
|
|
285
|
+
summary: summarizeJavaScriptTasks(samples),
|
|
286
|
+
clockSyncUncertaintyMs
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function nativeUiSourceResult(kind, samples, status, coverageGapReasons, observedEventNames, clockSyncUncertaintyMs) {
|
|
290
|
+
return {
|
|
291
|
+
status: samples.length > 0 ? status : "partial",
|
|
292
|
+
kind,
|
|
293
|
+
unavailableReasons: [],
|
|
294
|
+
coverageGapReasons: [
|
|
295
|
+
...coverageGapReasons,
|
|
296
|
+
...samples.length > 0 ? [] : [
|
|
297
|
+
`No ${kind} samples were recorded. Observed trace events: ${observedEventNames.join(", ") || "none"}.`
|
|
298
|
+
]
|
|
299
|
+
],
|
|
300
|
+
samples,
|
|
301
|
+
summary: summarizeFrames(samples),
|
|
302
|
+
clockSyncUncertaintyMs
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function traceEventNames(events) {
|
|
306
|
+
const counts = /* @__PURE__ */ new Map();
|
|
307
|
+
for (const event of events) {
|
|
308
|
+
const name = String(event.name ?? "unknown");
|
|
309
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
310
|
+
}
|
|
311
|
+
return [...counts.entries()].sort((first, second) => second[1] - first[1]).slice(0, 20).map(([name, count]) => `${name}(${count})`);
|
|
312
|
+
}
|
|
313
|
+
function unavailableJavaScriptSource(kind, reason) {
|
|
314
|
+
return {
|
|
315
|
+
status: "unavailable",
|
|
316
|
+
kind,
|
|
317
|
+
unavailableReasons: [reason],
|
|
318
|
+
coverageGapReasons: [],
|
|
319
|
+
samples: [],
|
|
320
|
+
summary: { taskCount: 0 }
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
function unavailableNativeUiSource(kind, reason) {
|
|
324
|
+
return {
|
|
325
|
+
status: "unavailable",
|
|
326
|
+
kind,
|
|
327
|
+
unavailableReasons: [reason],
|
|
328
|
+
coverageGapReasons: [],
|
|
329
|
+
samples: [],
|
|
330
|
+
summary: { frameCount: 0 }
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function finite(value) {
|
|
334
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
335
|
+
}
|
|
336
|
+
function asRecord(value) {
|
|
337
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
338
|
+
}
|
|
339
|
+
export {
|
|
340
|
+
createCdpTraceCollector,
|
|
341
|
+
parseTraceCapture
|
|
342
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const LONG_TASK_MS = 50;
|
|
2
|
+
function summarizeJavaScriptTasks(samples) {
|
|
3
|
+
const durations = samples.map((sample) => sample.durationMs).filter((duration) => Number.isFinite(duration) && duration > 0).sort((first, second) => first - second);
|
|
4
|
+
if (durations.length === 0) return { taskCount: 0 };
|
|
5
|
+
const longTaskCount = durations.filter(
|
|
6
|
+
(duration) => duration >= LONG_TASK_MS
|
|
7
|
+
).length;
|
|
8
|
+
return {
|
|
9
|
+
longestTaskMs: round(durations.at(-1) ?? 0),
|
|
10
|
+
longTaskCount,
|
|
11
|
+
longTaskPercent: round(longTaskCount / durations.length * 100),
|
|
12
|
+
p50TaskMs: round(percentile(durations, 0.5)),
|
|
13
|
+
p95TaskMs: round(percentile(durations, 0.95)),
|
|
14
|
+
p99TaskMs: round(percentile(durations, 0.99)),
|
|
15
|
+
taskCount: durations.length,
|
|
16
|
+
totalTaskTimeMs: round(
|
|
17
|
+
durations.reduce((total, duration) => total + duration, 0)
|
|
18
|
+
)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function percentile(sorted, quantile) {
|
|
22
|
+
if (sorted.length === 1) return sorted[0] ?? 0;
|
|
23
|
+
const position = (sorted.length - 1) * quantile;
|
|
24
|
+
const lower = Math.floor(position);
|
|
25
|
+
const upper = Math.ceil(position);
|
|
26
|
+
const lowerValue = sorted[lower] ?? 0;
|
|
27
|
+
const upperValue = sorted[upper] ?? lowerValue;
|
|
28
|
+
return lowerValue + (upperValue - lowerValue) * (position - lower);
|
|
29
|
+
}
|
|
30
|
+
function round(value) {
|
|
31
|
+
return Math.round(value * 1e3) / 1e3;
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
summarizeJavaScriptTasks
|
|
35
|
+
};
|
package/dist/adapters.js
CHANGED
|
@@ -12,6 +12,8 @@ import { observeNativeUi } from "../library/actions/mobile/platform/observe-ui.m
|
|
|
12
12
|
import { nativeAgentDeviceStateDir } from "../library/actions/mobile/platform/native-session-name.mjs";
|
|
13
13
|
import { resolveMobileToolPath } from "../library/actions/mobile/platform/tool-paths.mjs";
|
|
14
14
|
import { resolveWalletImportCredentials, validateWalletImportOptions } from "../library/actions/shared/wallet/import-source.mjs";
|
|
15
|
+
import { handleRunNetworkAction } from "./network-observation.js";
|
|
16
|
+
import { handleRunPerformanceAction } from "./performance-observation.js";
|
|
15
17
|
const execFileAsync = promisify(execFile);
|
|
16
18
|
const NATIVE_PROVIDER_UI_ACTIONS = /* @__PURE__ */ new Set([
|
|
17
19
|
"ui.press",
|
|
@@ -103,7 +105,13 @@ const LIVE_ONLY_WALLET_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
103
105
|
"metamask.wallet.list_accounts",
|
|
104
106
|
"metamask.wallet.read_state"
|
|
105
107
|
]);
|
|
106
|
-
const LIVE_ONLY_APP_ACTIONS = /* @__PURE__ */ new Set([
|
|
108
|
+
const LIVE_ONLY_APP_ACTIONS = /* @__PURE__ */ new Set([
|
|
109
|
+
"ui.navigate",
|
|
110
|
+
"app.network_capture",
|
|
111
|
+
"app.network_assert",
|
|
112
|
+
"app.performance_capture",
|
|
113
|
+
"app.performance_assert"
|
|
114
|
+
]);
|
|
107
115
|
function requiresLiveAdapter(platform, action) {
|
|
108
116
|
return LIVE_ONLY_ACTIONS.has(action) || platform === "core" && CORE_ONLY_PERPS_ACTIONS.has(action) || LIVE_ONLY_WALLET_ACTIONS.has(action) || LIVE_ONLY_APP_ACTIONS.has(action);
|
|
109
117
|
}
|
|
@@ -140,6 +148,20 @@ function liveAdapterPathHint(platform, action) {
|
|
|
140
148
|
return `library/actions/${platform}/${action.replaceAll(".", "/")}.mjs`;
|
|
141
149
|
}
|
|
142
150
|
async function semanticResult(platform, action, node, context, forceLive = false, preparedLiveAdapters) {
|
|
151
|
+
const performanceAction = await handleRunPerformanceAction(
|
|
152
|
+
platform,
|
|
153
|
+
action,
|
|
154
|
+
node,
|
|
155
|
+
context
|
|
156
|
+
);
|
|
157
|
+
if (performanceAction) return performanceAction;
|
|
158
|
+
const networkAction = await handleRunNetworkAction(
|
|
159
|
+
platform,
|
|
160
|
+
action,
|
|
161
|
+
node,
|
|
162
|
+
context
|
|
163
|
+
);
|
|
164
|
+
if (networkAction) return networkAction;
|
|
143
165
|
const live = await runLiveFirst(
|
|
144
166
|
platform,
|
|
145
167
|
action,
|
|
@@ -253,6 +275,12 @@ function createMetaMaskSemanticAdapters(platform, declaredCustomActions = [], pr
|
|
|
253
275
|
];
|
|
254
276
|
const bundledActions = [
|
|
255
277
|
...walletActions,
|
|
278
|
+
...platform !== "core" ? [
|
|
279
|
+
"app.network_capture",
|
|
280
|
+
"app.network_assert",
|
|
281
|
+
"app.performance_capture",
|
|
282
|
+
"app.performance_assert"
|
|
283
|
+
] : [],
|
|
256
284
|
"metamask.assets.read_visible_state",
|
|
257
285
|
"metamask.assets.open_details",
|
|
258
286
|
"metamask.assets.read_details",
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { constants as fsConstants } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
lstat,
|
|
4
|
+
mkdir,
|
|
5
|
+
open,
|
|
6
|
+
readFile,
|
|
7
|
+
realpath,
|
|
8
|
+
writeFile
|
|
9
|
+
} from "node:fs/promises";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
async function writeContainedArtifact(artifactsDir, relativePath, value, label) {
|
|
12
|
+
const file = resolveContainedArtifact(artifactsDir, relativePath, label);
|
|
13
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
14
|
+
await assertContainedParent(artifactsDir, file, label);
|
|
15
|
+
await assertNotSymlink(file, label);
|
|
16
|
+
const handle = await open(
|
|
17
|
+
file,
|
|
18
|
+
fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_TRUNC | fsConstants.O_NOFOLLOW,
|
|
19
|
+
384
|
|
20
|
+
);
|
|
21
|
+
try {
|
|
22
|
+
await handle.writeFile(value);
|
|
23
|
+
} finally {
|
|
24
|
+
await handle.close();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function readContainedJsonArtifact(artifactsDir, relativePath, maxBytes, label) {
|
|
28
|
+
const file = resolveContainedArtifact(artifactsDir, relativePath, label);
|
|
29
|
+
await assertContainedParent(artifactsDir, file, label);
|
|
30
|
+
await assertNotSymlink(file, label);
|
|
31
|
+
const handle = await open(file, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
32
|
+
try {
|
|
33
|
+
const info = await handle.stat();
|
|
34
|
+
if (!info.isFile() || info.size > maxBytes) {
|
|
35
|
+
throw new Error(`${label} is not a bounded regular file.`);
|
|
36
|
+
}
|
|
37
|
+
return JSON.parse(await handle.readFile("utf8"));
|
|
38
|
+
} finally {
|
|
39
|
+
await handle.close();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function indexArtifactManifest(manifestPath, entries) {
|
|
43
|
+
const parsed = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
44
|
+
const manifest = asRecord(parsed);
|
|
45
|
+
const replacementPaths = new Set(entries.map((entry) => entry.path));
|
|
46
|
+
const current = Array.isArray(manifest.artifacts) ? manifest.artifacts.filter(
|
|
47
|
+
(artifact) => !replacementPaths.has(String(asRecord(artifact).path ?? ""))
|
|
48
|
+
) : [];
|
|
49
|
+
await writeFile(
|
|
50
|
+
manifestPath,
|
|
51
|
+
`${JSON.stringify(
|
|
52
|
+
{ ...manifest, artifacts: [...current, ...entries] },
|
|
53
|
+
null,
|
|
54
|
+
2
|
|
55
|
+
)}
|
|
56
|
+
`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
function resolveContainedArtifact(artifactsDir, relativePath, label) {
|
|
60
|
+
const root = path.resolve(artifactsDir);
|
|
61
|
+
const file = path.resolve(root, relativePath);
|
|
62
|
+
const relative = path.relative(root, file);
|
|
63
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
64
|
+
throw new Error(`${label} path must stay inside artifactsDir.`);
|
|
65
|
+
}
|
|
66
|
+
return file;
|
|
67
|
+
}
|
|
68
|
+
async function assertContainedParent(artifactsDir, file, label) {
|
|
69
|
+
const root = await realpath(artifactsDir);
|
|
70
|
+
const parent = await realpath(path.dirname(file));
|
|
71
|
+
const relative = path.relative(root, parent);
|
|
72
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
73
|
+
throw new Error(`${label} parent must stay inside artifactsDir.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function assertNotSymlink(file, label) {
|
|
77
|
+
try {
|
|
78
|
+
if ((await lstat(file)).isSymbolicLink()) {
|
|
79
|
+
throw new Error(`${label} must not be a symbolic link.`);
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error.code !== "ENOENT") throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function asRecord(value) {
|
|
86
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
87
|
+
}
|
|
88
|
+
export {
|
|
89
|
+
indexArtifactManifest,
|
|
90
|
+
readContainedJsonArtifact,
|
|
91
|
+
writeContainedArtifact
|
|
92
|
+
};
|
package/dist/async.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
async function withTimeout(promise, options) {
|
|
2
|
+
let timer;
|
|
3
|
+
try {
|
|
4
|
+
return await Promise.race([
|
|
5
|
+
promise,
|
|
6
|
+
new Promise((_resolve, reject) => {
|
|
7
|
+
timer = setTimeout(() => {
|
|
8
|
+
options.onTimeout?.();
|
|
9
|
+
reject(new Error(options.message));
|
|
10
|
+
}, options.timeoutMs);
|
|
11
|
+
})
|
|
12
|
+
]);
|
|
13
|
+
} finally {
|
|
14
|
+
if (timer) clearTimeout(timer);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
withTimeout
|
|
19
|
+
};
|
package/dist/cli-commands.js
CHANGED
|
@@ -15,6 +15,7 @@ const SPEC = {
|
|
|
15
15
|
{ name: "sync", desc: "Refresh harness + canonicalize wallet fixture", flags: ["--json"] },
|
|
16
16
|
{ name: "logs", aliases: ["tail"], desc: "Compact build events or full log", flags: ["--full", "-f", "--window", "--events", "--source", "--json"] },
|
|
17
17
|
{ name: "debug", aliases: ["devtools", "inspect"], desc: "Open DevTools UI", flags: ["--json", "--no-open"] },
|
|
18
|
+
{ name: "reload", desc: "Reload the connected app runtime", flags: ["--json"] },
|
|
18
19
|
{ name: "fixtures", desc: "Manage the canonical wallet fixture (sync/set/reset/generate)", args: ["sync", "set", "reset", "generate"], flags: ["--fixture", "--out", "--adapter", "--target", "--device", "--json"] },
|
|
19
20
|
{ name: "actions", desc: "List runnable recipe actions", flags: ["--json", "--matrix", "--categories", "--category", "--action", "--library"] },
|
|
20
21
|
{ name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir", "--expect-live", "--print-ready", "--cdp-port", "--device"] },
|
|
@@ -31,10 +32,11 @@ const SPEC = {
|
|
|
31
32
|
{ name: "ready", aliases: ["ensure-ready"], desc: "Ensure one healthy extension home tab" },
|
|
32
33
|
{ name: "watch", aliases: ["start-watch", "watcher"], desc: "Start/reuse webpack watcher", flags: ["--full", "-f"] },
|
|
33
34
|
{ name: "stop", aliases: ["stop-watch", "stop-watcher"], desc: "Stop this checkout watcher" },
|
|
34
|
-
{ name: "rebuild", aliases: ["reset", "reopen", "
|
|
35
|
+
{ name: "rebuild", aliases: ["reset", "reopen", "browser", "launch", "full-launch", "runtime-launch", "build", "build:once", "build-once", "refresh-once"], desc: "Clean webpack + browser launch", flags: ["--json", "--dry-run", "--full"] },
|
|
35
36
|
{ name: "sidepanel", desc: "Side panel helper", args: ["cycle", "open", "close", "toggle", "status"] },
|
|
36
37
|
{ name: "prepare", flags: ["--target", "--cdp-port", "--runtime-dir", "--validate"] },
|
|
37
|
-
{ name: "debug", args: ["page", "worker"], flags: ["--json", "--no-open"] }
|
|
38
|
+
{ name: "debug", args: ["page", "worker"], flags: ["--json", "--no-open"] },
|
|
39
|
+
{ name: "reload", desc: "Reload the live Extension UI over CDP", flags: ["--json"] }
|
|
38
40
|
],
|
|
39
41
|
mobile: [
|
|
40
42
|
{ name: "ios", aliases: ["start"], desc: "Start Metro + launch iOS dev client" },
|
|
@@ -53,7 +55,8 @@ const SPEC = {
|
|
|
53
55
|
{ name: "screenshot", desc: "Capture simulator/device screenshot", args: ["path"] },
|
|
54
56
|
{ name: "dev-menu", aliases: ["devmenu"], desc: "Open RN developer menu", flags: ["--json", "--no-open"] },
|
|
55
57
|
{ name: "prepare", flags: ["--target", "--platform", "--preflight-mode", "--port", "--simulator", "--adb-serial", "--runtime-dir", "--wallet-setup", "--wallet-fixture"] },
|
|
56
|
-
{ name: "debug", flags: ["--json", "--no-open", "--action"] }
|
|
58
|
+
{ name: "debug", flags: ["--json", "--no-open", "--action"] },
|
|
59
|
+
{ name: "reload", desc: "Reload the app without restarting Metro", flags: ["--json"] }
|
|
57
60
|
]
|
|
58
61
|
};
|
|
59
62
|
const GLOBAL_FLAGS = ["--json", "--dry-run", "--full", "-f", "--help", "-h", "--no-open", "--no-color"];
|
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ import { handleCompletionCandidates, invalidateCompletionCache } from "./command
|
|
|
15
15
|
import { handleLaunch } from "./commands/launch/index.js";
|
|
16
16
|
import { handleLogs } from "./commands/logs.js";
|
|
17
17
|
import { handleDebug } from "./commands/debug.js";
|
|
18
|
+
import { handleReload } from "./commands/reload.js";
|
|
18
19
|
import { handleFixtures } from "./commands/fixtures.js";
|
|
19
20
|
import { handleRecipeQuality } from "./commands/recipe-quality.js";
|
|
20
21
|
import { handleStatus } from "./commands/status.js";
|
|
@@ -47,6 +48,8 @@ DAILY LOOP \u2014 what a teammate runs many times a day:
|
|
|
47
48
|
mm-harness logs
|
|
48
49
|
debug Open the debug console (extension DevTools / mobile RN).
|
|
49
50
|
mm-harness debug
|
|
51
|
+
reload Reload the connected Mobile or Extension runtime.
|
|
52
|
+
mm-harness reload
|
|
50
53
|
fixtures Sync files, set/reset the wallet, generate fixture-state, or finalize labels over CDP.
|
|
51
54
|
mm-harness fixtures sync # or: set | reset | generate --fixture <f> --out <o> | finalize \u2026
|
|
52
55
|
|
|
@@ -118,6 +121,7 @@ async function main(argv) {
|
|
|
118
121
|
if (command === "stop") return handleStop(argv.slice(1));
|
|
119
122
|
if (command === "logs") return handleLogs(argv.slice(1));
|
|
120
123
|
if (command === "debug") return handleDebug(argv.slice(1));
|
|
124
|
+
if (command === "reload") return handleReload(argv.slice(1));
|
|
121
125
|
if (command === "fixtures") return handleFixtures(argv.slice(1));
|
|
122
126
|
if (command === "recipe-quality") return handleRecipeQuality(argv.slice(1));
|
|
123
127
|
if (command === "check") return handleCheck(argv.slice(1));
|
package/dist/command-contract.js
CHANGED