@deeeed/metamask-harness 0.82.0 → 0.83.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 +26 -0
- package/adapters/core/cleanup.sh +5 -29
- package/adapters/core/controller-import-probe.mjs +26 -0
- package/adapters/manifest.json +16 -0
- package/adapters/web-dapp/lib/policy-fence.mjs +119 -0
- package/adapters/web-dapp/lib/policy.mjs +7 -2
- package/dist/adapters/core/actions.js +0 -3
- package/dist/adapters/core/overlay.js +1 -5
- package/dist/adapters/core/readiness.js +133 -2
- package/dist/adapters/core/surface.js +53 -41
- package/dist/adapters/extension/extension-id.js +1 -0
- package/dist/adapters/extension/fixtures.js +60 -1
- package/dist/adapters/extension/network-observer.js +19 -206
- package/dist/adapters/extension/run.js +1 -1
- package/dist/adapters/extension/runtime.js +42 -3
- package/dist/adapters/mobile/android-gfxinfo-performance.js +3 -1
- package/dist/adapters/mobile/performance-observer.js +9 -6
- package/dist/adapters/performance/trace-kinds.js +36 -0
- package/dist/adapters/plugin-surface.js +29 -1
- package/dist/adapters/web-dapp/run.js +6 -1
- package/dist/command-contract.js +34 -0
- package/dist/commands/check.js +3 -2
- package/dist/commands/core-readiness.js +1 -9
- package/dist/performance-observation.js +28 -5
- package/library/actions/core/perps/_controller.mjs +10 -0
- package/library/actions/extension/wallet/import.mjs +21 -4
- package/library/actions/extension/wallet/visible-session.mjs +32 -12
- package/package.json +7 -6
- package/dist/adapters/core/live-script-env.js +0 -36
- package/dist/adapters/extension/browser-cdp.js +0 -174
- package/dist/adapters/extension/performance-observer.js +0 -75
- package/dist/adapters/performance/cdp-trace.js +0 -406
- package/dist/adapters/performance/js-task-metrics.js +0 -35
- package/dist/async.js +0 -19
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { colorHumanMessage, EXIT, spawnScriptStreaming } from "@farmslot/recipe-cli/harness";
|
|
3
|
+
import { colorHumanMessage, EXIT, shellQuote, spawnScriptStreaming } from "@farmslot/recipe-cli/harness";
|
|
4
4
|
import { FIXTURE_STREAM_TIMEOUT_MS } from "../../commands/fixtures.js";
|
|
5
5
|
import { sameFeatureFlags } from "../../feature-flags.js";
|
|
6
6
|
import { runnerDir } from "../../paths.js";
|
|
7
7
|
import { readExtensionPinnedFlags } from "./feature-flags.js";
|
|
8
|
+
import { releaseArtifactState } from "./runtime-decision.js";
|
|
8
9
|
async function applyExtensionFixture({
|
|
9
10
|
target,
|
|
10
11
|
fixturePath,
|
|
@@ -13,6 +14,64 @@ async function applyExtensionFixture({
|
|
|
13
14
|
sub: action,
|
|
14
15
|
fixtureFlags
|
|
15
16
|
}) {
|
|
17
|
+
const artifact = releaseArtifactState(target);
|
|
18
|
+
if (artifact.status !== "none") {
|
|
19
|
+
const targetArg = `--target ${shellQuote(target)}`;
|
|
20
|
+
const cdpArg = process.env.CDP_PORT ? ` --cdp-port ${shellQuote(process.env.CDP_PORT)}` : "";
|
|
21
|
+
const canonical = path.resolve(fixturePath) === path.resolve(canonicalFixture);
|
|
22
|
+
const fixtureArg = canonical ? "" : ` --fixture ${shellQuote(fixturePath)}`;
|
|
23
|
+
const checkoutAction = `mm-harness runtime-launch --adapter extension ${targetArg}${cdpArg} && mm-harness fixtures ${action} --adapter extension ${targetArg}${fixtureArg}`;
|
|
24
|
+
let message2;
|
|
25
|
+
let artifactAction;
|
|
26
|
+
let alternatives = [];
|
|
27
|
+
if (artifact.status === "invalid") {
|
|
28
|
+
message2 = `The selected release artifact is no longer valid (${artifact.reason ?? "unknown reason"}) and fixtures will not replace it with the checkout build; re-select it with runtime-launch and the same artifact source, or seed the checkout build instead.`;
|
|
29
|
+
artifactAction = checkoutAction;
|
|
30
|
+
} else {
|
|
31
|
+
let archive;
|
|
32
|
+
try {
|
|
33
|
+
const provenance = JSON.parse(fs.readFileSync(artifact.provenancePath, "utf8"));
|
|
34
|
+
if (typeof provenance.source?.path === "string") archive = provenance.source.path;
|
|
35
|
+
} catch {
|
|
36
|
+
archive = void 0;
|
|
37
|
+
}
|
|
38
|
+
const archivePresent = archive !== void 0 && fs.existsSync(archive);
|
|
39
|
+
const reselect = [
|
|
40
|
+
`mm-harness runtime-launch --adapter extension ${targetArg}${cdpArg}`,
|
|
41
|
+
`--artifact-file ${archivePresent ? shellQuote(archive) : "<path-to-the-artifact-zip>"}`,
|
|
42
|
+
`--artifact-version ${shellQuote(artifact.expectedVersion)}`,
|
|
43
|
+
`--artifact-sha256 ${shellQuote(artifact.sha256)}`,
|
|
44
|
+
`--artifact-cache ${shellQuote(path.dirname(path.dirname(path.dirname(artifact.provenancePath))))}`,
|
|
45
|
+
`--chrome-user-data-dir ${shellQuote(artifact.chromeUserDataDir)}`,
|
|
46
|
+
"--reset-profile"
|
|
47
|
+
].join(" ");
|
|
48
|
+
const publish = canonical ? "" : `mm-harness fixtures init --from ${shellQuote(fixturePath)} --force ${targetArg} && `;
|
|
49
|
+
const importWallet = `mm-harness run wallet.import method=ui credential_source=fixture --adapter extension ${targetArg}`;
|
|
50
|
+
artifactAction = `${publish}${reselect} && ${importWallet}`;
|
|
51
|
+
alternatives = [checkoutAction];
|
|
52
|
+
const archiveNote = archivePresent ? "" : `, supplying its archive with --artifact-file (${archive ? `${archive} no longer exists` : "its provenance names no archive"})`;
|
|
53
|
+
const fixtureNote = canonical ? "" : `, after the selected fixture replaces this slot's canonical fixture ${canonicalFixture}, the only one wallet.import reads`;
|
|
54
|
+
message2 = `Release artifact ${artifact.manifestVersion} is selected and fixtures can only seed the checkout build; re-select it with a clean profile and import the fixture wallet${archiveNote}${fixtureNote}, or seed the checkout build instead.`;
|
|
55
|
+
}
|
|
56
|
+
if (json) {
|
|
57
|
+
console.log(JSON.stringify({
|
|
58
|
+
schemaVersion: 1,
|
|
59
|
+
command: "fixtures",
|
|
60
|
+
action,
|
|
61
|
+
adapter: "extension",
|
|
62
|
+
fixture: fixturePath,
|
|
63
|
+
canonicalFixture,
|
|
64
|
+
status: "fail",
|
|
65
|
+
exitCode: EXIT.runtime,
|
|
66
|
+
error: { code: "FIXTURES_ARTIFACT_ACTIVE", message: message2, userAction: artifactAction, alternatives }
|
|
67
|
+
}, null, 2));
|
|
68
|
+
} else {
|
|
69
|
+
console.error(`\u2717 mm-harness fixtures ${action}: ${message2}
|
|
70
|
+
Next: ${artifactAction}${alternatives.map((alternative) => `
|
|
71
|
+
Or: ${alternative}`).join("")}`);
|
|
72
|
+
}
|
|
73
|
+
return EXIT.runtime;
|
|
74
|
+
}
|
|
16
75
|
const distManifest = path.join(target, "dist/chrome/manifest.json");
|
|
17
76
|
const userAction = `mm-harness launch --build --verify --target ${JSON.stringify(target)}`;
|
|
18
77
|
if (!fs.existsSync(distManifest)) {
|
|
@@ -1,211 +1,24 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import path from "node:path";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
fetchCdpJson
|
|
8
|
-
} from "./browser-cdp.js";
|
|
9
|
-
const DEVICE_ID = "extension";
|
|
10
|
-
const COMMAND_TIMEOUT_MS = 1e4;
|
|
11
|
-
const EXTENSION_TARGET_TYPES = /* @__PURE__ */ new Set([
|
|
12
|
-
"background_page",
|
|
13
|
-
"other",
|
|
14
|
-
"page",
|
|
15
|
-
"service_worker"
|
|
16
|
-
]);
|
|
17
|
-
const { brokerSocketPath, createBrokerClient, createCdpBroker } = brokerModule;
|
|
18
|
-
async function createExtensionNetworkObserver(cdpPort, runtimeDir) {
|
|
19
|
-
if (!Number.isInteger(cdpPort) || cdpPort <= 0) {
|
|
20
|
-
throw new Error("Extension network observation requires a valid CDP port.");
|
|
21
|
-
}
|
|
22
|
-
const targets = await fetchCdpJson(cdpPort);
|
|
23
|
-
const extensionId = extensionIdFromCdpTargets(targets);
|
|
24
|
-
if (!extensionId) {
|
|
25
|
-
throw new Error(
|
|
26
|
-
"Extension CDP browser or loaded extension target is unavailable."
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
const connection = await createBrowserCdpClient(cdpPort);
|
|
30
|
-
let closed = false;
|
|
31
|
-
const brokerRef = {};
|
|
32
|
-
const targetSessions = /* @__PURE__ */ new Map();
|
|
33
|
-
const attaching = /* @__PURE__ */ new Set();
|
|
34
|
-
const send = (method, params = {}, sessionId, timeoutMs = COMMAND_TIMEOUT_MS) => connection.send(method, params, sessionId, timeoutMs);
|
|
35
|
-
const relevantTarget = (target) => {
|
|
36
|
-
if (!EXTENSION_TARGET_TYPES.has(String(target.type))) return false;
|
|
37
|
-
try {
|
|
38
|
-
return new URL(String(target.url)).hostname === extensionId;
|
|
39
|
-
} catch {
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
const refreshBrokerSession = () => {
|
|
44
|
-
if (!brokerRef.current) return;
|
|
45
|
-
brokerRef.current.onSessionClose(DEVICE_ID);
|
|
46
|
-
brokerRef.current.onSessionOpen(DEVICE_ID);
|
|
47
|
-
};
|
|
48
|
-
const attachTarget = async (target) => {
|
|
49
|
-
if (closed || !relevantTarget(target) || targetSessions.has(target.targetId) || attaching.has(target.targetId)) {
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
attaching.add(target.targetId);
|
|
53
|
-
try {
|
|
54
|
-
const result = asRecord(
|
|
55
|
-
await send("Target.attachToTarget", {
|
|
56
|
-
targetId: target.targetId,
|
|
57
|
-
flatten: true
|
|
58
|
-
})
|
|
59
|
-
);
|
|
60
|
-
const sessionId = String(result.sessionId ?? "");
|
|
61
|
-
if (sessionId) {
|
|
62
|
-
targetSessions.set(target.targetId, sessionId);
|
|
63
|
-
refreshBrokerSession();
|
|
64
|
-
}
|
|
65
|
-
} catch (error) {
|
|
66
|
-
if (!/already attached/iu.test(String(error))) {
|
|
67
|
-
brokerRef.current?.onSessionClose(DEVICE_ID);
|
|
68
|
-
}
|
|
69
|
-
} finally {
|
|
70
|
-
attaching.delete(target.targetId);
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
connection.onMessage((message) => {
|
|
74
|
-
if (message.method === "Target.targetCreated") {
|
|
75
|
-
const target = asBrowserCdpTarget(message.params?.targetInfo);
|
|
76
|
-
if (target) void attachTarget(target);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
if (message.method === "Target.attachedToTarget") {
|
|
80
|
-
const target = asBrowserCdpTarget(message.params?.targetInfo);
|
|
81
|
-
const sessionId = String(message.params?.sessionId ?? "");
|
|
82
|
-
if (target && sessionId && relevantTarget(target)) {
|
|
83
|
-
targetSessions.set(target.targetId, sessionId);
|
|
84
|
-
refreshBrokerSession();
|
|
85
|
-
}
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
if (message.method === "Target.detachedFromTarget") {
|
|
89
|
-
const sessionId = String(message.params?.sessionId ?? "");
|
|
90
|
-
for (const [targetId, activeSessionId] of targetSessions) {
|
|
91
|
-
if (activeSessionId !== sessionId) continue;
|
|
92
|
-
targetSessions.delete(targetId);
|
|
93
|
-
refreshBrokerSession();
|
|
94
|
-
}
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
if (message.sessionId && message.method?.startsWith("Network.")) {
|
|
98
|
-
brokerRef.current?.onCdpEvent(
|
|
99
|
-
DEVICE_ID,
|
|
100
|
-
message.method,
|
|
101
|
-
message.params ?? {}
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
connection.onClose(() => {
|
|
106
|
-
if (!closed) brokerRef.current?.onSessionClose(DEVICE_ID);
|
|
107
|
-
});
|
|
108
|
-
await send("Target.setDiscoverTargets", { discover: true });
|
|
109
|
-
const targetResult = asRecord(await send("Target.getTargets"));
|
|
110
|
-
const targetInfos = Array.isArray(targetResult.targetInfos) ? targetResult.targetInfos : [];
|
|
111
|
-
await Promise.all(
|
|
112
|
-
targetInfos.map(asBrowserCdpTarget).filter((target) => Boolean(target)).map(attachTarget)
|
|
113
|
-
);
|
|
114
|
-
if (targetSessions.size === 0) {
|
|
115
|
-
connection.close();
|
|
116
|
-
throw new Error("Extension CDP exposes no attachable extension targets.");
|
|
117
|
-
}
|
|
118
|
-
const sessions = /* @__PURE__ */ new Map([[DEVICE_ID, { brokerReady: true }]]);
|
|
119
|
-
const socketPath = brokerSocketPath(
|
|
120
|
-
path.join(runtimeDir, "extension-network")
|
|
121
|
-
);
|
|
122
|
-
brokerRef.current = createCdpBroker({
|
|
123
|
-
socketPath,
|
|
124
|
-
sessions,
|
|
125
|
-
async sendCommand(_session, method, params, timeoutMs) {
|
|
126
|
-
const sessionIds = [...new Set(targetSessions.values())];
|
|
127
|
-
if (sessionIds.length === 0) {
|
|
128
|
-
throw new Error(
|
|
129
|
-
"Extension CDP has no active extension target sessions."
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
const results = await Promise.allSettled(
|
|
133
|
-
sessionIds.map(
|
|
134
|
-
(sessionId) => send(method, params, sessionId, timeoutMs)
|
|
135
|
-
)
|
|
136
|
-
);
|
|
137
|
-
const successes = results.filter(
|
|
138
|
-
(result) => result.status === "fulfilled"
|
|
139
|
-
);
|
|
140
|
-
if (successes.length === 0) {
|
|
141
|
-
const firstFailure = results.find(
|
|
142
|
-
(result) => result.status === "rejected"
|
|
143
|
-
);
|
|
144
|
-
throw new Error(
|
|
145
|
-
`Extension CDP command failed on every target: ${String(firstFailure?.reason ?? method)}`
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
if (successes.length !== results.length) {
|
|
149
|
-
results.forEach((result, index) => {
|
|
150
|
-
if (result.status === "fulfilled") return;
|
|
151
|
-
const failedSession = sessionIds[index];
|
|
152
|
-
for (const [targetId, sessionId] of targetSessions) {
|
|
153
|
-
if (sessionId === failedSession) targetSessions.delete(targetId);
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
brokerRef.current?.onSessionClose(DEVICE_ID);
|
|
157
|
-
}
|
|
158
|
-
return successes[0].value;
|
|
159
|
-
},
|
|
160
|
-
requestDiscovery() {
|
|
161
|
-
void send("Target.getTargets").then((result) => {
|
|
162
|
-
const infos = asRecord(result).targetInfos;
|
|
163
|
-
if (!Array.isArray(infos)) return;
|
|
164
|
-
for (const value of infos) {
|
|
165
|
-
const target = asBrowserCdpTarget(value);
|
|
166
|
-
if (target) void attachTarget(target);
|
|
167
|
-
}
|
|
168
|
-
}).catch(() => brokerRef.current?.onSessionClose(DEVICE_ID));
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
brokerRef.current.onSessionOpen(DEVICE_ID);
|
|
172
|
-
await waitForSocket(socketPath);
|
|
173
|
-
const client = await createBrokerClient(
|
|
174
|
-
socketPath,
|
|
175
|
-
DEVICE_ID,
|
|
176
|
-
COMMAND_TIMEOUT_MS
|
|
177
|
-
);
|
|
178
|
-
return {
|
|
179
|
-
start(params) {
|
|
180
|
-
return client.control("capture-start", params, COMMAND_TIMEOUT_MS);
|
|
181
|
-
},
|
|
182
|
-
async end(id) {
|
|
183
|
-
return asRecord(
|
|
184
|
-
await client.control("capture-end", { id }, COMMAND_TIMEOUT_MS)
|
|
185
|
-
);
|
|
186
|
-
},
|
|
187
|
-
async close() {
|
|
188
|
-
closed = true;
|
|
189
|
-
client.close();
|
|
190
|
-
brokerRef.current?.close();
|
|
191
|
-
connection.close();
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
function asRecord(value) {
|
|
196
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
3
|
+
import { idFromManifest, resolveExtensionId } from "./extension-id.js";
|
|
4
|
+
async function createExtensionNetworkBackend(target, env, artifactsDir) {
|
|
5
|
+
const cdpPort = extensionCdpPort(env);
|
|
6
|
+
const { extensionId } = await resolveExtensionId(target, { cdpPort });
|
|
7
|
+
return observeExtensionNetwork(cdpPort, artifactsDir, extensionId);
|
|
197
8
|
}
|
|
198
|
-
async function
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
while (Date.now() < deadline) {
|
|
202
|
-
if (existsSync(socketPath)) return;
|
|
203
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
204
|
-
}
|
|
205
|
-
throw new Error("Extension network observer broker did not start.");
|
|
9
|
+
async function createLoadedExtensionNetworkBackend(extensionDir, env, artifactsDir) {
|
|
10
|
+
const extensionId = idFromManifest(path.join(extensionDir, "manifest.json"));
|
|
11
|
+
return observeExtensionNetwork(extensionCdpPort(env), artifactsDir, extensionId);
|
|
206
12
|
}
|
|
207
|
-
|
|
208
|
-
|
|
13
|
+
function observeExtensionNetwork(cdpPort, artifactsDir, extensionId) {
|
|
14
|
+
const { createExtensionNetworkObserver } = createRequire(import.meta.url)(
|
|
15
|
+
"@farmslot/adapter-web/network-observer"
|
|
16
|
+
);
|
|
17
|
+
return createExtensionNetworkObserver({
|
|
18
|
+
cdpPort,
|
|
19
|
+
runtimeDir: artifactsDir,
|
|
20
|
+
...extensionId ? { extensionId } : {}
|
|
21
|
+
});
|
|
209
22
|
}
|
|
210
23
|
function extensionCdpPort(env) {
|
|
211
24
|
const port = Number(
|
|
@@ -218,5 +31,5 @@ function extensionCdpPort(env) {
|
|
|
218
31
|
}
|
|
219
32
|
export {
|
|
220
33
|
createExtensionNetworkBackend,
|
|
221
|
-
|
|
34
|
+
createLoadedExtensionNetworkBackend
|
|
222
35
|
};
|
|
@@ -181,7 +181,7 @@ const extensionDiagnostics = {
|
|
|
181
181
|
};
|
|
182
182
|
const extensionObservation = {
|
|
183
183
|
network: {
|
|
184
|
-
backend: async (
|
|
184
|
+
backend: async (target, env, artifactsDir) => (await import("./network-observer.js")).createExtensionNetworkBackend(target, env, artifactsDir),
|
|
185
185
|
actions: true
|
|
186
186
|
},
|
|
187
187
|
performance: {
|
|
@@ -40,22 +40,39 @@ async function prepareExtensionRuntime(options) {
|
|
|
40
40
|
projectRoot,
|
|
41
41
|
cdpPort,
|
|
42
42
|
timeoutMs: options.healthTimeoutMs,
|
|
43
|
-
pageMode: options.pageMode
|
|
43
|
+
pageMode: options.pageMode,
|
|
44
|
+
// The validation launch seeds the copied wallet fixture into a fresh
|
|
45
|
+
// profile; attaching to an existing runtime may legitimately be onboarding.
|
|
46
|
+
seededLaunchDir: seededLaunchDir(projectRoot, launch)
|
|
44
47
|
});
|
|
45
48
|
return { launch, health };
|
|
46
49
|
}
|
|
50
|
+
function seededLaunchDir(projectRoot, launch) {
|
|
51
|
+
return launch?.runtimeDir ? path.join(projectRoot, launch.runtimeDir, "live") : void 0;
|
|
52
|
+
}
|
|
53
|
+
const FIXTURE_SEED_RECHECK_MS = 5e3;
|
|
47
54
|
async function assertHealthyExtensionRuntime(options) {
|
|
48
55
|
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
49
56
|
const deadline = Date.now() + timeoutMs;
|
|
50
57
|
let lastReport = null;
|
|
58
|
+
let onboardingSince;
|
|
51
59
|
while (Date.now() <= deadline) {
|
|
52
60
|
lastReport = await checkExtensionRuntimeHealth(options.projectRoot, options.cdpPort, {
|
|
53
|
-
pageMode: options.pageMode
|
|
61
|
+
pageMode: options.pageMode,
|
|
62
|
+
seededLaunchDir: options.seededLaunchDir
|
|
54
63
|
});
|
|
55
64
|
if (lastReport.status === "PASS") return lastReport;
|
|
56
65
|
if (lastReport.errorCode === "UI_COMPOSITOR_SUSPENDED" || lastReport.errorCode === "EXTENSION_BACKGROUND_UNRESPONSIVE") {
|
|
57
66
|
throw new Error(formatHealthFailure(lastReport, options.projectRoot));
|
|
58
67
|
}
|
|
68
|
+
if (lastReport.errorCode === "FIXTURE_SEED_NOT_APPLIED") {
|
|
69
|
+
onboardingSince ??= Date.now();
|
|
70
|
+
if (Date.now() - onboardingSince >= FIXTURE_SEED_RECHECK_MS) {
|
|
71
|
+
throw new Error(formatHealthFailure(lastReport, options.projectRoot));
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
onboardingSince = void 0;
|
|
75
|
+
}
|
|
59
76
|
await sleep(500);
|
|
60
77
|
}
|
|
61
78
|
const report = lastReport ?? failReport(options.cdpPort, ["CDP runtime was not probed."], {});
|
|
@@ -102,6 +119,24 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
|
|
|
102
119
|
if (String(target.url ?? "").startsWith("chrome-error://")) {
|
|
103
120
|
findings.push(`Extension target resolved to chrome-error page: ${target.url}`);
|
|
104
121
|
}
|
|
122
|
+
if (options.seededLaunchDir && String(target.url ?? "").includes("/onboarding/")) {
|
|
123
|
+
return {
|
|
124
|
+
status: "FAIL",
|
|
125
|
+
errorCode: "FIXTURE_SEED_NOT_APPLIED",
|
|
126
|
+
userAction: `Read logs/fixture-source.json and logs/fixture-account-parity.json in the newest run under ${shellQuote(options.seededLaunchDir)}, fix the slot's wallet fixture (${shellQuote(path.join(projectRoot, recipeRuntimeDir(), "wallet-fixture.json"))}), then rerun with --launch-existing-dist.`,
|
|
127
|
+
cdpPort,
|
|
128
|
+
targetUrl: target.url,
|
|
129
|
+
extensionId: safeExtensionId(target),
|
|
130
|
+
extensionPageTargets: extensionTargets.length,
|
|
131
|
+
findings: [
|
|
132
|
+
"FIXTURE_SEED_NOT_APPLIED: MetaMask is still on onboarding after launch: the wallet fixture was not applied."
|
|
133
|
+
],
|
|
134
|
+
details: {
|
|
135
|
+
projectRoot,
|
|
136
|
+
targetUrls: targets.map((entry) => entry.url).filter(Boolean)
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
105
140
|
let session;
|
|
106
141
|
try {
|
|
107
142
|
session = await CdpSession.connect(target.webSocketDebuggerUrl, {
|
|
@@ -653,6 +688,9 @@ async function launchLockedValidationBrowser(options) {
|
|
|
653
688
|
"--cdp-port",
|
|
654
689
|
String(options.cdpPort),
|
|
655
690
|
"--launch-existing-dist",
|
|
691
|
+
// The profile is a fresh mkdtemp dir; live.sh resolves and seeds the copied
|
|
692
|
+
// wallet fixture only on a reset launch.
|
|
693
|
+
"--reset-profile",
|
|
656
694
|
"--chrome-user-data-dir",
|
|
657
695
|
validationProfilePath,
|
|
658
696
|
"--artifacts-dir",
|
|
@@ -1001,5 +1039,6 @@ export {
|
|
|
1001
1039
|
evmRpcRecoveryAction,
|
|
1002
1040
|
extensionBackgroundProbeTimeoutMs,
|
|
1003
1041
|
formatHealthFailure,
|
|
1004
|
-
prepareExtensionRuntime
|
|
1042
|
+
prepareExtensionRuntime,
|
|
1043
|
+
seededLaunchDir
|
|
1005
1044
|
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
summarizeFrames
|
|
5
|
+
} from "@farmslot/recipe-runner/runtime/cdp-trace";
|
|
4
6
|
const execFileAsync = promisify(execFile);
|
|
5
7
|
const POLL_INTERVAL_MS = 750;
|
|
6
8
|
const MAX_FRAMES = 1e5;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createCdpTraceCollector
|
|
3
|
-
} from "
|
|
3
|
+
} from "@farmslot/recipe-runner/runtime/cdp-trace";
|
|
4
|
+
import { TRACE_KINDS, TRACE_MARKER_PREFIX } from "../performance/trace-kinds.js";
|
|
4
5
|
import { connectMobileBroker } from "./network-observer.js";
|
|
5
6
|
import { createAndroidGfxinfoPerformanceBackend } from "./android-gfxinfo-performance.js";
|
|
6
7
|
async function createMobilePerformanceBackend(target, env) {
|
|
@@ -8,10 +9,12 @@ async function createMobilePerformanceBackend(target, env) {
|
|
|
8
9
|
return createAndroidGfxinfoPerformanceBackend(env);
|
|
9
10
|
}
|
|
10
11
|
const { client } = await connectMobileBroker(target, env);
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const platform = resolvePlatform(env);
|
|
13
|
+
return createCdpTraceCollector(client, {
|
|
14
|
+
kind: TRACE_KINDS[platform],
|
|
15
|
+
platform,
|
|
16
|
+
markerPrefix: TRACE_MARKER_PREFIX,
|
|
17
|
+
async marker(name) {
|
|
15
18
|
const before = Date.now();
|
|
16
19
|
await markRuntime(client, name);
|
|
17
20
|
const after = Date.now();
|
|
@@ -20,7 +23,7 @@ async function createMobilePerformanceBackend(target, env) {
|
|
|
20
23
|
uncertaintyMs: (after - before) / 2
|
|
21
24
|
};
|
|
22
25
|
}
|
|
23
|
-
);
|
|
26
|
+
});
|
|
24
27
|
}
|
|
25
28
|
async function markRuntime(client, name) {
|
|
26
29
|
await client.send(
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const TRACE_MARKER_PREFIX = "mmh-clock-";
|
|
2
|
+
const MOBILE_TRACE_CATEGORIES = [
|
|
3
|
+
"blink.user_timing",
|
|
4
|
+
"disabled-by-default-devtools.timeline.frame"
|
|
5
|
+
];
|
|
6
|
+
const EXTENSION_TRACE_CATEGORIES = [
|
|
7
|
+
"blink.user_timing",
|
|
8
|
+
"devtools.timeline",
|
|
9
|
+
"disabled-by-default-devtools.timeline",
|
|
10
|
+
"disabled-by-default-devtools.timeline.frame",
|
|
11
|
+
"v8.execute"
|
|
12
|
+
];
|
|
13
|
+
const ANDROID_TRACE_KIND = {
|
|
14
|
+
categories: MOBILE_TRACE_CATEGORIES,
|
|
15
|
+
rendererScoped: false,
|
|
16
|
+
scope: "mobile-host",
|
|
17
|
+
nativeSource: "react-native-cdp-frame-timings",
|
|
18
|
+
javascriptTasks: false,
|
|
19
|
+
frameTiming: "draw"
|
|
20
|
+
};
|
|
21
|
+
const TRACE_KINDS = {
|
|
22
|
+
extension: {
|
|
23
|
+
categories: EXTENSION_TRACE_CATEGORIES,
|
|
24
|
+
rendererScoped: true,
|
|
25
|
+
scope: "extension-renderer",
|
|
26
|
+
nativeSource: "chromium-cdp-frame-timings",
|
|
27
|
+
javascriptTasks: true,
|
|
28
|
+
frameTiming: "draw"
|
|
29
|
+
},
|
|
30
|
+
ios: { ...ANDROID_TRACE_KIND, frameTiming: "cadence" },
|
|
31
|
+
android: ANDROID_TRACE_KIND
|
|
32
|
+
};
|
|
33
|
+
export {
|
|
34
|
+
TRACE_KINDS,
|
|
35
|
+
TRACE_MARKER_PREFIX
|
|
36
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import {
|
|
3
4
|
adapterPlugin,
|
|
@@ -5,6 +6,9 @@ import {
|
|
|
5
6
|
ensureAdapterLoaded,
|
|
6
7
|
selectedAdapterId
|
|
7
8
|
} from "@farmslot/recipe-cli/harness";
|
|
9
|
+
import { libraryAdapterFiles } from "@farmslot/recipe-runner";
|
|
10
|
+
import { POLICY_ENV } from "../../adapters/web-dapp/lib/policy.mjs";
|
|
11
|
+
import { POLICY_DIGEST_ENV, PolicyFenceError, fencePolicy } from "../../adapters/web-dapp/lib/policy-fence.mjs";
|
|
8
12
|
import { configPath, declaredLibraryPathEnv, readConfig } from "../review/knowledge.js";
|
|
9
13
|
import { adapterRegistry } from "./registry.js";
|
|
10
14
|
import { bindWebDappPolicy } from "./web-dapp/surface.js";
|
|
@@ -34,6 +38,7 @@ async function loadSelectedAdapter(command, args) {
|
|
|
34
38
|
else if (arg.startsWith("--library=")) libraries.push(arg.slice("--library=".length));
|
|
35
39
|
});
|
|
36
40
|
const selected = selectedAdapterId(args);
|
|
41
|
+
const builtin = selected !== void 0 && adapterRegistry.has(selected);
|
|
37
42
|
try {
|
|
38
43
|
await ensureAdapterLoaded(selected, {
|
|
39
44
|
libraries,
|
|
@@ -41,11 +46,12 @@ async function loadSelectedAdapter(command, args) {
|
|
|
41
46
|
env: operatorLibraryEnv(),
|
|
42
47
|
adopt: adoptPluginAdapter
|
|
43
48
|
});
|
|
49
|
+
await fenceBoundPolicy();
|
|
44
50
|
return void 0;
|
|
45
51
|
} catch (error) {
|
|
46
52
|
const refusal = error;
|
|
47
53
|
if (typeof refusal.code !== "string" || typeof refusal.userAction !== "string") throw error;
|
|
48
|
-
if (
|
|
54
|
+
if (builtin && refusal.code !== "ADAPTER_ID_CONFLICT") {
|
|
49
55
|
return void 0;
|
|
50
56
|
}
|
|
51
57
|
return adapterSelectionFailureOut(own.includes("--json"), command, {
|
|
@@ -56,8 +62,30 @@ async function loadSelectedAdapter(command, args) {
|
|
|
56
62
|
}
|
|
57
63
|
}
|
|
58
64
|
const ADAPTER_PLUGIN_ENV = "METAMASK_RECIPE_ADAPTER_PLUGIN";
|
|
65
|
+
let boundPolicy;
|
|
66
|
+
async function fenceBoundPolicy() {
|
|
67
|
+
const bound = boundPolicy;
|
|
68
|
+
boundPolicy = void 0;
|
|
69
|
+
if (!bound || process.env[POLICY_ENV] !== bound.file) return;
|
|
70
|
+
const plugin = adapterPlugin(bound.adapterId);
|
|
71
|
+
try {
|
|
72
|
+
if (!plugin) throw new PolicyFenceError(`no plugin record for adapter '${bound.adapterId}'`);
|
|
73
|
+
const files = await libraryAdapterFiles(plugin.root, { module: plugin.module });
|
|
74
|
+
const covered = new Set(files.map((file) => fs.realpathSync(path.join(plugin.root, file))));
|
|
75
|
+
const scope = `${path.posix.dirname(plugin.module.replace(/^\.\//u, ""))}/ and actions/ in ${plugin.root}`;
|
|
76
|
+
process.env[POLICY_DIGEST_ENV] = fencePolicy({ file: bound.file, covered, scope });
|
|
77
|
+
} catch (error) {
|
|
78
|
+
delete process.env[POLICY_ENV];
|
|
79
|
+
delete process.env[POLICY_DIGEST_ENV];
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
59
83
|
function adoptPluginAdapter(adapter) {
|
|
60
84
|
const surface = bindWebDappPolicy(adapter);
|
|
85
|
+
const policy = adapter;
|
|
86
|
+
if (policy.extends === "web-dapp" && typeof policy.policy?.module === "string") {
|
|
87
|
+
boundPolicy = { adapterId: adapter.id, file: policy.policy.module };
|
|
88
|
+
}
|
|
61
89
|
return {
|
|
62
90
|
...surface,
|
|
63
91
|
run: {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { isRecord } from "@farmslot/recipe-cli/harness";
|
|
2
3
|
import { verifyOwnedCollectorControl } from "../console-capture.js";
|
|
3
4
|
import { devicePlatformFromEnv } from "../performance/device-platform.js";
|
|
@@ -40,7 +41,11 @@ function walletRequestFindings(lines) {
|
|
|
40
41
|
}
|
|
41
42
|
const webDappObservation = {
|
|
42
43
|
network: {
|
|
43
|
-
backend: async (
|
|
44
|
+
backend: async (target, env, artifactsDir) => (await import("../extension/network-observer.js")).createLoadedExtensionNetworkBackend(
|
|
45
|
+
path.resolve(env.TERMINAL_EXTENSION_DIR || path.join(target, "temp/metamask-extension/chrome")),
|
|
46
|
+
env,
|
|
47
|
+
artifactsDir
|
|
48
|
+
)
|
|
44
49
|
},
|
|
45
50
|
performance: {
|
|
46
51
|
start: async (context) => {
|
package/dist/command-contract.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { adapterChoices, closest } from "@farmslot/recipe-cli/harness";
|
|
2
4
|
import { configuredLibrarySources, operatorLibraryEnv } from "./adapters/plugin-surface.js";
|
|
3
5
|
const bool = () => ({ kind: "boolean" });
|
|
@@ -440,6 +442,36 @@ function optionValues(args, name) {
|
|
|
440
442
|
}
|
|
441
443
|
return values;
|
|
442
444
|
}
|
|
445
|
+
const LIBRARY_ADAPTERS = {
|
|
446
|
+
terminal: { title: "MetaMask Web Terminal", repository: "MetaMask/experimental-metamask-recipe-terminal" }
|
|
447
|
+
};
|
|
448
|
+
const quoteArg = (value2) => /^[A-Za-z0-9_./:=@%+-]+$/u.test(value2) ? value2 : `'${value2.replaceAll("'", `'\\''`)}'`;
|
|
449
|
+
function missingAdapterLibrary(command, optionName, id, tokens) {
|
|
450
|
+
const library = LIBRARY_ADAPTERS[id];
|
|
451
|
+
if (!library || optionName !== "--adapter" && optionName !== "--platform") return void 0;
|
|
452
|
+
const current = operatorLibraryEnv().RECIPE_LIBRARY_PATH;
|
|
453
|
+
const rerun = (libraryPath) => `RECIPE_LIBRARY_PATH=${quoteArg(libraryPath)} mm-harness ${[command, ...tokens].map(quoteArg).join(" ")}`;
|
|
454
|
+
const revision = `a revision whose recipe-library.json declares adapters.${id}`;
|
|
455
|
+
const older = (current ?? "").split(":").filter(Boolean).map((entry) => {
|
|
456
|
+
const separator = entry.indexOf("=");
|
|
457
|
+
return separator > 0 && !entry.slice(0, separator).includes("/") ? { name: entry.slice(0, separator), root: entry.slice(separator + 1) } : { name: void 0, root: entry };
|
|
458
|
+
}).find((entry) => entry.name === id || fs.existsSync(path.join(entry.root, "recipes", id)));
|
|
459
|
+
if (older) {
|
|
460
|
+
return {
|
|
461
|
+
code: "CLI_ADAPTER_LIBRARY_MISSING",
|
|
462
|
+
command,
|
|
463
|
+
message: `${optionName} ${id} is the ${library.title} adapter, a plugin of the ${library.repository} recipe library. The library on RECIPE_LIBRARY_PATH at ${older.root} does not declare it: an older checkout, or a recipe-library.json that does not parse.`,
|
|
464
|
+
userAction: `Update that checkout to ${revision} (e.g. git -C ${quoteArg(older.root)} pull), then rerun: ${rerun(current)}`
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
const checkout = `<${library.repository.split("/")[1]} checkout>`;
|
|
468
|
+
return {
|
|
469
|
+
code: "CLI_ADAPTER_LIBRARY_MISSING",
|
|
470
|
+
command,
|
|
471
|
+
message: `${optionName} ${id} is the ${library.title} adapter, a plugin of the ${library.repository} recipe library, and no library on RECIPE_LIBRARY_PATH, --library or the config declares it.`,
|
|
472
|
+
userAction: `Keep ${optionName} ${id} and put that library, at ${revision}, on RECIPE_LIBRARY_PATH (git clone https://github.com/${library.repository}.git ${checkout}): ${rerun(current ? `${id}=${checkout}:${current}` : `${id}=${checkout}`)}`
|
|
473
|
+
};
|
|
474
|
+
}
|
|
443
475
|
function validatePublicInvocation(argv, examples = {}) {
|
|
444
476
|
const token = argv[0];
|
|
445
477
|
if (!token || token === "--help" || token === "-h" || token === "--version" || token === "-v") return null;
|
|
@@ -509,6 +541,8 @@ function validatePublicInvocation(argv, examples = {}) {
|
|
|
509
541
|
if (inlineValue === void 0) index += 1;
|
|
510
542
|
const choices = typeof spec.choices === "function" ? spec.choices(tokens) : spec.choices;
|
|
511
543
|
if (choices && !choices.includes(optionValue)) {
|
|
544
|
+
const missing = missingAdapterLibrary(name, optionName, optionValue, tokens);
|
|
545
|
+
if (missing) return missing;
|
|
512
546
|
const suggestion = closest(optionValue, choices);
|
|
513
547
|
return {
|
|
514
548
|
code: "CLI_INVALID_OPTION_VALUE",
|
package/dist/commands/check.js
CHANGED
|
@@ -7,6 +7,7 @@ const USAGE = "mm-harness check diff [--fix] [--target <repo>] [--adapter <adapt
|
|
|
7
7
|
const JS_TS_EXT_RE = /\.(?:c|m)?jsx?$|\.tsx?$/u;
|
|
8
8
|
const FORMAT_EXT_RE = /\.(?:c|m)?jsx?$|\.tsx?$|\.json$|\.md$|\.css$|\.scss$|\.ya?ml$/u;
|
|
9
9
|
const TEST_FILE_RE = /(?:^|[./_-])(?:test|spec)\.(?:c|m)?jsx?$|(?:^|[./_-])(?:test|spec)\.tsx?$/u;
|
|
10
|
+
const OXFMT_TARGET_ARGS = ["--no-error-on-unmatched-pattern"];
|
|
10
11
|
const ESLINT_SUPPRESSION_RE = /^\s*eslint-disable(?:-line|-next-line)?\b/u;
|
|
11
12
|
const EXTENSION_LOCALE_RE = /^app\/_locales\//u;
|
|
12
13
|
async function handleCheck(argv) {
|
|
@@ -427,7 +428,7 @@ function runFormatterCheck(input) {
|
|
|
427
428
|
label: "Changed-file oxfmt",
|
|
428
429
|
...input,
|
|
429
430
|
command: oxfmt,
|
|
430
|
-
args: ["--config", oxfmtConfig, "--check"]
|
|
431
|
+
args: ["--config", oxfmtConfig, "--check", ...OXFMT_TARGET_ARGS]
|
|
431
432
|
});
|
|
432
433
|
}
|
|
433
434
|
return runToolCheck({
|
|
@@ -447,7 +448,7 @@ function runFormatterFix(input) {
|
|
|
447
448
|
label: "Changed-file oxfmt fix",
|
|
448
449
|
...input,
|
|
449
450
|
command: oxfmt,
|
|
450
|
-
args: ["--config", oxfmtConfig, "--write"]
|
|
451
|
+
args: ["--config", oxfmtConfig, "--write", ...OXFMT_TARGET_ARGS]
|
|
451
452
|
});
|
|
452
453
|
}
|
|
453
454
|
return runToolCheck({
|