@deeeed/metamask-harness 0.43.0 → 0.44.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 +25 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +304 -34
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +20 -8
- package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +102 -0
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +4 -2
- package/adapters/mobile/reload-app.mjs +99 -1
- package/dist/adapters/mobile/prepare.js +12 -0
- package/dist/adapters.js +22 -3
- package/dist/commands/call.js +40 -2
- package/dist/commands/run-engine.js +247 -139
- package/dist/commands/run-report.js +68 -0
- package/dist/commands/run.js +47 -2
- package/dist/execution-provenance.js +342 -0
- package/dist/run-diagnostics.js +36 -11
- package/dist/runner.js +44 -13
- package/library/actions/mobile/perps/performance-capture.mjs +570 -189
- package/library/actions/mobile/perps/perps.mjs +122 -0
- package/library/actions/mobile/platform/bridge.mjs +40 -8
- package/library/actions/mobile/platform/native-session.mjs +1 -1
- package/library/actions/mobile/wallet/lock.mjs +1 -4
- package/library/actions/mobile/wallet/select_account.mjs +129 -17
- package/library/manifests/mobile.action-manifest.json +28 -3
- package/library/recipes/mobile/perps/performance.recipe.json +73 -47
- package/package.json +1 -1
|
@@ -28,6 +28,12 @@ import {
|
|
|
28
28
|
validateMetaMaskActionInputs
|
|
29
29
|
} from "../metamask-action-validation.js";
|
|
30
30
|
import { gitLibraryProvenance } from "../library-provenance.js";
|
|
31
|
+
import {
|
|
32
|
+
captureExecutionProvenance,
|
|
33
|
+
executionProvenanceDrift,
|
|
34
|
+
ProvenanceDriftError,
|
|
35
|
+
writeExecutionProvenance
|
|
36
|
+
} from "../execution-provenance.js";
|
|
31
37
|
import {
|
|
32
38
|
authorizeTrustedMutationPlan,
|
|
33
39
|
loadTrustedMutationBase
|
|
@@ -36,7 +42,7 @@ import {
|
|
|
36
42
|
beginRunDiagnostics,
|
|
37
43
|
finishRunDiagnostics
|
|
38
44
|
} from "../run-diagnostics.js";
|
|
39
|
-
import { EXIT
|
|
45
|
+
import { EXIT } from "./shared.js";
|
|
40
46
|
import {
|
|
41
47
|
actionManifestPathOption,
|
|
42
48
|
optionString,
|
|
@@ -46,13 +52,14 @@ import {
|
|
|
46
52
|
targetPath,
|
|
47
53
|
usageError
|
|
48
54
|
} from "./parse-args.js";
|
|
49
|
-
async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions = {}, preflightedExecution) {
|
|
55
|
+
async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions = {}, preflightedExecution, effectsState = newHealState()) {
|
|
50
56
|
const restoreRuntimeEnvironment = activateRecipeRuntimeEnvironment(
|
|
51
57
|
adapter,
|
|
52
58
|
projectRoot,
|
|
53
59
|
runtimeOptions
|
|
54
60
|
);
|
|
55
61
|
try {
|
|
62
|
+
const wasPreflighted = preflightedExecution !== void 0;
|
|
56
63
|
const execution = preflightedExecution ?? await resolveRecipeExecution(
|
|
57
64
|
adapter,
|
|
58
65
|
recipe,
|
|
@@ -65,27 +72,48 @@ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManif
|
|
|
65
72
|
runner,
|
|
66
73
|
runRequest,
|
|
67
74
|
absoluteArtifactsDir,
|
|
68
|
-
useFramedExtensionRecording
|
|
75
|
+
useFramedExtensionRecording,
|
|
76
|
+
provenanceInput,
|
|
77
|
+
provenanceSnapshots
|
|
69
78
|
} = execution;
|
|
70
|
-
await runner.preflight(runRequest);
|
|
79
|
+
if (!wasPreflighted) await runner.preflight(runRequest);
|
|
71
80
|
await prepareRuntimeIfNeeded(adapter, projectRoot, runtimeOptions);
|
|
81
|
+
await runner.preflight(runRequest);
|
|
82
|
+
const preExecute = await captureExecutionProvenance(
|
|
83
|
+
provenanceInput,
|
|
84
|
+
"pre-execute"
|
|
85
|
+
);
|
|
86
|
+
provenanceSnapshots.push(preExecute);
|
|
87
|
+
const preExecuteDrift = executionProvenanceDrift(
|
|
88
|
+
provenanceSnapshots[0],
|
|
89
|
+
preExecute
|
|
90
|
+
);
|
|
91
|
+
if (preExecuteDrift.length > 0) {
|
|
92
|
+
const provenancePath2 = writeExecutionProvenance(
|
|
93
|
+
absoluteArtifactsDir,
|
|
94
|
+
provenanceSnapshots,
|
|
95
|
+
preExecuteDrift
|
|
96
|
+
);
|
|
97
|
+
throw new ProvenanceDriftError(provenancePath2, preExecuteDrift);
|
|
98
|
+
}
|
|
72
99
|
const diagnosticBaseline = await beginRunDiagnostics(adapter, projectRoot);
|
|
73
100
|
const recording = useFramedExtensionRecording ? await startRecipeRecording(adapter, projectRoot, absoluteArtifactsDir, {
|
|
74
101
|
record: true,
|
|
75
102
|
cdpPort: runtimeOptions.cdpPort
|
|
76
103
|
}) : void 0;
|
|
77
104
|
let result;
|
|
105
|
+
let executionError;
|
|
78
106
|
try {
|
|
79
107
|
result = await runner.run(runRequest);
|
|
80
108
|
} catch (error) {
|
|
109
|
+
executionError = error;
|
|
81
110
|
try {
|
|
82
111
|
await stopRecipeRecording(recording);
|
|
83
|
-
} catch (
|
|
112
|
+
} catch (recordingError2) {
|
|
84
113
|
console.error(
|
|
85
|
-
`WARN: recipe and video recording both failed; preserving recipe failure: ${
|
|
114
|
+
`WARN: recipe and video recording both failed; preserving recipe failure: ${recordingError2 instanceof Error ? recordingError2.message : String(recordingError2)}`
|
|
86
115
|
);
|
|
87
116
|
}
|
|
88
|
-
throw error;
|
|
89
117
|
} finally {
|
|
90
118
|
if (adapter === "mobile") {
|
|
91
119
|
const { hideMobileHudOnTeardown } = await import("../adapters.js");
|
|
@@ -95,8 +123,36 @@ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManif
|
|
|
95
123
|
);
|
|
96
124
|
}
|
|
97
125
|
}
|
|
98
|
-
|
|
99
|
-
|
|
126
|
+
let recordingError;
|
|
127
|
+
if (executionError === void 0 && result) {
|
|
128
|
+
try {
|
|
129
|
+
await stopRecipeRecording(recording, result);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
recordingError = error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const end = await captureExecutionProvenance(provenanceInput, "end");
|
|
135
|
+
provenanceSnapshots.push(end);
|
|
136
|
+
const endDrift = executionProvenanceDrift(provenanceSnapshots[0], end);
|
|
137
|
+
const provenancePath = writeExecutionProvenance(
|
|
138
|
+
absoluteArtifactsDir,
|
|
139
|
+
provenanceSnapshots,
|
|
140
|
+
endDrift,
|
|
141
|
+
result?.artifactManifestPath
|
|
142
|
+
);
|
|
143
|
+
if (endDrift.length > 0) {
|
|
144
|
+
throw new ProvenanceDriftError(provenancePath, endDrift, executionError);
|
|
145
|
+
}
|
|
146
|
+
if (executionError !== void 0) throw executionError;
|
|
147
|
+
if (recordingError !== void 0) throw recordingError;
|
|
148
|
+
if (!result) throw new Error("Recipe execution returned no result.");
|
|
149
|
+
const finalized = finishRunDiagnostics(diagnosticBaseline, result);
|
|
150
|
+
persistRunEffects(
|
|
151
|
+
finalized.summaryPath,
|
|
152
|
+
finalized.artifactManifestPath,
|
|
153
|
+
effectsState
|
|
154
|
+
);
|
|
155
|
+
return finalized;
|
|
100
156
|
} finally {
|
|
101
157
|
restoreRuntimeEnvironment();
|
|
102
158
|
}
|
|
@@ -122,6 +178,23 @@ async function preflightRecipe(adapter, recipe, artifactsDir, projectRoot, actio
|
|
|
122
178
|
restoreRuntimeEnvironment();
|
|
123
179
|
}
|
|
124
180
|
}
|
|
181
|
+
function persistRunEffects(summaryPath, artifactManifestPath, state) {
|
|
182
|
+
for (const filePath of [summaryPath, artifactManifestPath]) {
|
|
183
|
+
let document;
|
|
184
|
+
try {
|
|
185
|
+
document = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
186
|
+
} catch {
|
|
187
|
+
document = null;
|
|
188
|
+
}
|
|
189
|
+
if (!isRecord(document)) {
|
|
190
|
+
throw new Error(`Recipe evidence file is not valid JSON: ${filePath}`);
|
|
191
|
+
}
|
|
192
|
+
document.recovered = [...state.recovered];
|
|
193
|
+
document.mutations = state.mutations.map((mutation) => ({ ...mutation }));
|
|
194
|
+
fs.writeFileSync(filePath, `${JSON.stringify(document, null, 2)}
|
|
195
|
+
`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
125
198
|
async function resolveRecipeExecution(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions) {
|
|
126
199
|
const absoluteArtifactsDir = path.resolve(artifactsDir);
|
|
127
200
|
const absoluteRecipePath = typeof recipe === "string" ? path.resolve(recipe) : void 0;
|
|
@@ -173,7 +246,7 @@ async function resolveRecipeExecution(adapter, recipe, artifactsDir, projectRoot
|
|
|
173
246
|
});
|
|
174
247
|
const useFramedExtensionRecording = adapter === "extension" && recordVideo === "full-run" && trust.source?.trust !== "untrusted" && trust.source?.trust !== "unknown" && captureHelperSupportsRecordSessionSnapshots(projectRoot);
|
|
175
248
|
const runRequest = {
|
|
176
|
-
...absoluteRecipePath ? { recipePath: absoluteRecipePath } : { recipeDocument: recipe },
|
|
249
|
+
...recipeDocument !== void 0 ? { recipeDocument } : absoluteRecipePath ? { recipePath: absoluteRecipePath } : { recipeDocument: recipe },
|
|
177
250
|
artifactsDir: absoluteArtifactsDir,
|
|
178
251
|
projectRoot,
|
|
179
252
|
inheritProcessEnv: false,
|
|
@@ -196,12 +269,138 @@ async function resolveRecipeExecution(adapter, recipe, artifactsDir, projectRoot
|
|
|
196
269
|
trustedMutation: authorizedMutation
|
|
197
270
|
});
|
|
198
271
|
}
|
|
272
|
+
const provenanceInput = {
|
|
273
|
+
adapter,
|
|
274
|
+
projectRoot,
|
|
275
|
+
recipeDocument: recipeDocument ?? recipe,
|
|
276
|
+
...absoluteRecipePath ? { recipePath: absoluteRecipePath } : {},
|
|
277
|
+
...librarySources ? { librarySources } : {},
|
|
278
|
+
excludedProductRoots: [absoluteArtifactsDir],
|
|
279
|
+
helperPaths: commandHelperPaths(recipeDocument ?? recipe, projectRoot)
|
|
280
|
+
};
|
|
281
|
+
const startProvenance = await captureExecutionProvenance(
|
|
282
|
+
provenanceInput,
|
|
283
|
+
"start"
|
|
284
|
+
);
|
|
199
285
|
return {
|
|
200
286
|
runner,
|
|
201
287
|
absoluteArtifactsDir,
|
|
202
288
|
useFramedExtensionRecording,
|
|
203
|
-
runRequest
|
|
289
|
+
runRequest,
|
|
290
|
+
provenanceInput,
|
|
291
|
+
provenanceSnapshots: [startProvenance]
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function commandHelperPaths(recipe, projectRoot) {
|
|
295
|
+
const helpers = /* @__PURE__ */ new Set();
|
|
296
|
+
visitRecipeValues(recipe, (value) => {
|
|
297
|
+
if (!isRecord(value) || value.action !== "command" || typeof value.cmd !== "string") return;
|
|
298
|
+
const candidates = commandHelperTokens(value.cmd);
|
|
299
|
+
for (const token of candidates) {
|
|
300
|
+
if (/[$`{}]/u.test(token)) {
|
|
301
|
+
throw new Error(`Command helper path cannot be bound before execution: ${token}`);
|
|
302
|
+
}
|
|
303
|
+
if (!isHelperToken(token)) continue;
|
|
304
|
+
const absolute = path.isAbsolute(token) ? path.resolve(token) : path.resolve(projectRoot, token);
|
|
305
|
+
let stat;
|
|
306
|
+
try {
|
|
307
|
+
stat = fs.lstatSync(absolute);
|
|
308
|
+
} catch {
|
|
309
|
+
throw new Error(`Command helper source does not exist: ${token}`);
|
|
310
|
+
}
|
|
311
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
312
|
+
throw new Error(`Command helper source must be a regular file: ${token}`);
|
|
313
|
+
}
|
|
314
|
+
helpers.add(absolute);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
return [...helpers].sort();
|
|
318
|
+
}
|
|
319
|
+
function commandHelperTokens(command, depth = 0) {
|
|
320
|
+
if (depth > 4) {
|
|
321
|
+
throw new Error("Command helper sources exceed the supported shell nesting depth.");
|
|
322
|
+
}
|
|
323
|
+
const tokens = shellTokens(command);
|
|
324
|
+
const candidates = new Set(tokens.filter(isHelperToken));
|
|
325
|
+
for (const token of tokens) {
|
|
326
|
+
const assignment = /^(?:--)?(?:config|configuration|manifest|project|recipe|schema|tsconfig)=([^=]+)$/iu.exec(
|
|
327
|
+
token
|
|
328
|
+
);
|
|
329
|
+
const value = assignment?.[1];
|
|
330
|
+
if (value && (isHelperToken(value) || isConfigInputToken(value))) {
|
|
331
|
+
candidates.add(value);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
for (let index = 0; index < tokens.length - 1; index += 1) {
|
|
335
|
+
if (!isScriptInterpreter(tokens[index])) continue;
|
|
336
|
+
const option = tokens[index + 1];
|
|
337
|
+
if (isShellInterpreter(tokens[index]) && /^-[a-z]*c[a-z]*$/iu.test(option) && tokens[index + 2]) {
|
|
338
|
+
for (const nested of commandHelperTokens(tokens[index + 2], depth + 1)) {
|
|
339
|
+
candidates.add(nested);
|
|
340
|
+
}
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (!option.startsWith("-")) candidates.add(option);
|
|
344
|
+
}
|
|
345
|
+
return candidates;
|
|
346
|
+
}
|
|
347
|
+
function visitRecipeValues(value, visit) {
|
|
348
|
+
visit(value);
|
|
349
|
+
if (Array.isArray(value)) {
|
|
350
|
+
for (const child of value) visitRecipeValues(child, visit);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (!isRecord(value)) return;
|
|
354
|
+
for (const child of Object.values(value)) visitRecipeValues(child, visit);
|
|
355
|
+
}
|
|
356
|
+
function isHelperToken(token) {
|
|
357
|
+
return !token.startsWith("-") && !token.includes("=") && !/^[a-z][a-z0-9+.-]*:\/\//iu.test(token) && /^\S+\.(?:cjs|mjs|js|jsx|ts|tsx|sh|py)$/u.test(token);
|
|
358
|
+
}
|
|
359
|
+
function isConfigInputToken(token) {
|
|
360
|
+
return !token.startsWith("-") && !token.includes("=") && /^\S+\.(?:json|json5|ya?ml|toml)$/u.test(token);
|
|
361
|
+
}
|
|
362
|
+
function isScriptInterpreter(token) {
|
|
363
|
+
return /^(?:node|bash|sh|python|python3)$/u.test(path.basename(token));
|
|
364
|
+
}
|
|
365
|
+
function isShellInterpreter(token) {
|
|
366
|
+
return /^(?:bash|sh)$/u.test(path.basename(token));
|
|
367
|
+
}
|
|
368
|
+
function shellTokens(command) {
|
|
369
|
+
const tokens = [];
|
|
370
|
+
let token = "";
|
|
371
|
+
let quote = null;
|
|
372
|
+
let escaped = false;
|
|
373
|
+
const push = () => {
|
|
374
|
+
if (token) tokens.push(token);
|
|
375
|
+
token = "";
|
|
204
376
|
};
|
|
377
|
+
for (const character of command) {
|
|
378
|
+
if (escaped) {
|
|
379
|
+
token += character;
|
|
380
|
+
escaped = false;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (character === "\\" && quote !== "'") {
|
|
384
|
+
escaped = true;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (quote) {
|
|
388
|
+
if (character === quote) quote = null;
|
|
389
|
+
else token += character;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (character === "'" || character === '"') {
|
|
393
|
+
quote = character;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (/\s|[;&|<>]/u.test(character)) push();
|
|
397
|
+
else token += character;
|
|
398
|
+
}
|
|
399
|
+
if (quote || escaped) {
|
|
400
|
+
throw new Error("Command helper sources cannot be bound from an unterminated shell token.");
|
|
401
|
+
}
|
|
402
|
+
push();
|
|
403
|
+
return tokens;
|
|
205
404
|
}
|
|
206
405
|
function runtimeRecipeDocument(recipe, absoluteRecipePath) {
|
|
207
406
|
if (typeof recipe !== "string") return recipe;
|
|
@@ -938,19 +1137,15 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
|
|
|
938
1137
|
target,
|
|
939
1138
|
heal,
|
|
940
1139
|
state,
|
|
941
|
-
json
|
|
942
|
-
{
|
|
943
|
-
onRecovery: (code) => prepareOptions.onPhase?.("recover", { code })
|
|
944
|
-
}
|
|
1140
|
+
json
|
|
945
1141
|
);
|
|
946
1142
|
if (current !== null) return current;
|
|
947
1143
|
}
|
|
948
1144
|
return { state, heal };
|
|
949
1145
|
}
|
|
950
|
-
async function ensureMobileProofRuntime(target,
|
|
1146
|
+
async function ensureMobileProofRuntime(target, _heal, state, json, deps = {}) {
|
|
951
1147
|
const source = await import("../adapters/mobile/source-freshness.js");
|
|
952
1148
|
const check = deps.check ?? (() => source.mobileSourceCheck(target));
|
|
953
|
-
const record = deps.record ?? ((fingerprint) => source.recordMobileSourceBaseline(target, fingerprint));
|
|
954
1149
|
const initial = check();
|
|
955
1150
|
if (initial.status === "unavailable") {
|
|
956
1151
|
if (json) {
|
|
@@ -975,99 +1170,26 @@ async function ensureMobileProofRuntime(target, heal, state, json, deps = {}) {
|
|
|
975
1170
|
if (initial.status === "current") return null;
|
|
976
1171
|
const targetArg = JSON.stringify(path.resolve(target));
|
|
977
1172
|
const userAction = `mm-harness call app.lifecycle --arg command=restart --adapter mobile --target ${targetArg}`;
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
)
|
|
993
|
-
);
|
|
994
|
-
} else {
|
|
995
|
-
console.error(`\u2717 ${message}`);
|
|
996
|
-
console.error(` Next: ${userAction}`);
|
|
997
|
-
}
|
|
998
|
-
return EXIT.infra;
|
|
999
|
-
}
|
|
1000
|
-
const recoveryCode = "mobile.source_reloaded";
|
|
1001
|
-
state.attemptedRecoveries.push(recoveryCode);
|
|
1002
|
-
deps.onRecovery?.(recoveryCode);
|
|
1003
|
-
if (!json)
|
|
1004
|
-
console.error(
|
|
1005
|
-
`\u2192 Mobile proof preflight: source ${initial.status}; restarting the app before execution`
|
|
1006
|
-
);
|
|
1007
|
-
const restart = deps.restart ?? (async () => {
|
|
1008
|
-
const result2 = await runOneNode(
|
|
1009
|
-
"mobile",
|
|
1010
|
-
"app.lifecycle",
|
|
1011
|
-
{ command: "restart" },
|
|
1012
|
-
target,
|
|
1013
|
-
void 0
|
|
1173
|
+
const message = `Mobile loaded source is not current (${initial.status}).`;
|
|
1174
|
+
if (json) {
|
|
1175
|
+
console.log(
|
|
1176
|
+
JSON.stringify(
|
|
1177
|
+
{
|
|
1178
|
+
schemaVersion: 1,
|
|
1179
|
+
status: "fail",
|
|
1180
|
+
recoverable: true,
|
|
1181
|
+
mutations: state.mutations,
|
|
1182
|
+
error: { code: "MOBILE_SOURCE_NOT_LOADED", message, userAction }
|
|
1183
|
+
},
|
|
1184
|
+
null,
|
|
1185
|
+
2
|
|
1186
|
+
)
|
|
1014
1187
|
);
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
};
|
|
1019
|
-
});
|
|
1020
|
-
const result = await restart();
|
|
1021
|
-
const platform = resolveRunMobilePlatform();
|
|
1022
|
-
const bridge = result.status === 0 ? await (deps.waitForBridge ? deps.waitForBridge() : spawnScriptStreaming(
|
|
1023
|
-
path.join(runnerDir, "adapters/mobile/wait-for-bridge.sh"),
|
|
1024
|
-
["--target", target, "--platform", platform],
|
|
1025
|
-
target
|
|
1026
|
-
)) : { status: result.status, output: result.output };
|
|
1027
|
-
const recorded = result.status === 0 && bridge.status === 0 && record(initial.fingerprint);
|
|
1028
|
-
const verified = recorded ? check() : initial;
|
|
1029
|
-
if (result.status !== 0 || bridge.status !== 0 || !recorded || verified.status !== "current") {
|
|
1030
|
-
const message = bridge.status !== 0 ? "Mobile source restart completed but the platform-matched bridge did not become ready." : "Mobile source restart did not produce a current loaded-source baseline.";
|
|
1031
|
-
if (json) {
|
|
1032
|
-
console.log(
|
|
1033
|
-
JSON.stringify(
|
|
1034
|
-
{
|
|
1035
|
-
schemaVersion: 1,
|
|
1036
|
-
status: "fail",
|
|
1037
|
-
recoverable: false,
|
|
1038
|
-
attemptedRecoveries: state.attemptedRecoveries,
|
|
1039
|
-
error: {
|
|
1040
|
-
code: bridge.status !== 0 ? "MOBILE_BRIDGE_NOT_READY" : "MOBILE_SOURCE_RELOAD_FAILED",
|
|
1041
|
-
message,
|
|
1042
|
-
userAction,
|
|
1043
|
-
originalError: bridge.output || result.output
|
|
1044
|
-
}
|
|
1045
|
-
},
|
|
1046
|
-
null,
|
|
1047
|
-
2
|
|
1048
|
-
)
|
|
1049
|
-
);
|
|
1050
|
-
} else {
|
|
1051
|
-
console.error(`\u2717 ${message}`);
|
|
1052
|
-
console.error(
|
|
1053
|
-
` Next: ${bridge.status !== 0 ? "inspect the bridge-status log and rerun mm-harness launch <platform>" : userAction}`
|
|
1054
|
-
);
|
|
1055
|
-
}
|
|
1056
|
-
return EXIT.infra;
|
|
1057
|
-
}
|
|
1058
|
-
state.recovered.push(recoveryCode);
|
|
1059
|
-
state.mutations.push({
|
|
1060
|
-
type: "runtime",
|
|
1061
|
-
action: "restarted",
|
|
1062
|
-
reasonCode: `source-${initial.status}`
|
|
1063
|
-
});
|
|
1064
|
-
return null;
|
|
1065
|
-
}
|
|
1066
|
-
function resolveRunMobilePlatform() {
|
|
1067
|
-
if (process.env.PLATFORM === "android" || process.env.PLATFORM === "ios") {
|
|
1068
|
-
return process.env.PLATFORM;
|
|
1188
|
+
} else {
|
|
1189
|
+
console.error(`\u2717 ${message}`);
|
|
1190
|
+
console.error(` Next: ${userAction}`);
|
|
1069
1191
|
}
|
|
1070
|
-
return
|
|
1192
|
+
return EXIT.infra;
|
|
1071
1193
|
}
|
|
1072
1194
|
async function ensureExtensionProofRuntime(target, heal, state, json, deps = {}) {
|
|
1073
1195
|
const decide = deps.decide ?? (async () => {
|
|
@@ -1263,26 +1385,10 @@ async function recoverRunInfra(adapter, target, json, runtimeOptions = {}) {
|
|
|
1263
1385
|
return launchExtension(target, "quick", false);
|
|
1264
1386
|
}
|
|
1265
1387
|
if (adapter === "mobile") {
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
await relaunchAndroidReleaseArtifactRuntime(artifact);
|
|
1271
|
-
return {
|
|
1272
|
-
status: 0,
|
|
1273
|
-
output: `Relaunched ${artifact.packageId} ${artifact.versionName} (${artifact.versionCode}) on ${artifact.deviceSerial}.`
|
|
1274
|
-
};
|
|
1275
|
-
}
|
|
1276
|
-
} catch (error) {
|
|
1277
|
-
return {
|
|
1278
|
-
status: 1,
|
|
1279
|
-
output: `Release artifact recovery could not revalidate and relaunch the pinned Android package: ${error instanceof Error ? error.message : String(error)}
|
|
1280
|
-
Next: rerun runtime-launch with the same artifact source.`
|
|
1281
|
-
};
|
|
1282
|
-
}
|
|
1283
|
-
const { launchMobile } = await import("./launch/mobile.js");
|
|
1284
|
-
const platform = resolveMobileRecoveryPlatform();
|
|
1285
|
-
return launchMobile(target, platform, "quick", json);
|
|
1388
|
+
return {
|
|
1389
|
+
status: 1,
|
|
1390
|
+
output: "Automatic Mobile runtime recovery is disabled.\nNext: use an explicit app.lifecycle action when the recipe intends to relaunch the app."
|
|
1391
|
+
};
|
|
1286
1392
|
}
|
|
1287
1393
|
return { status: 0, output: "headless run retry" };
|
|
1288
1394
|
} finally {
|
|
@@ -1290,15 +1396,6 @@ Next: rerun runtime-launch with the same artifact source.`
|
|
|
1290
1396
|
restoreEnv("RECIPE_CDP_PORT", previousRecipeCdpPort);
|
|
1291
1397
|
}
|
|
1292
1398
|
}
|
|
1293
|
-
function resolveMobileRecoveryPlatform(env = process.env) {
|
|
1294
|
-
if (env.PLATFORM === "android" || env.PLATFORM === "ios") return env.PLATFORM;
|
|
1295
|
-
const androidPinned = Boolean(
|
|
1296
|
-
env.ADB_SERIAL || env.ANDROID_SERIAL || env.ANDROID_TARGET_DEVICE_NAME || env.ANDROID_DEVICE
|
|
1297
|
-
);
|
|
1298
|
-
const iosPinned = Boolean(env.IOS_SIMULATOR || env.SIM_UDID);
|
|
1299
|
-
if (androidPinned && !iosPinned) return "android";
|
|
1300
|
-
return "ios";
|
|
1301
|
-
}
|
|
1302
1399
|
function readRunFailureText(result) {
|
|
1303
1400
|
try {
|
|
1304
1401
|
const trace = JSON.parse(fs.readFileSync(result.tracePath, "utf8"));
|
|
@@ -1320,11 +1417,22 @@ async function executeWithHealBounds(exec, adapter, target, heal, state, recover
|
|
|
1320
1417
|
state
|
|
1321
1418
|
);
|
|
1322
1419
|
if (violation !== null) return { result, violation };
|
|
1420
|
+
if (adapter === "mobile") return { result, violation: null };
|
|
1323
1421
|
if (heal === "off") return { result, violation: null };
|
|
1324
1422
|
const recoveryCode = RUN_RECOVERY_CODE[adapter];
|
|
1325
1423
|
state.attemptedRecoveries.push(recoveryCode);
|
|
1326
1424
|
onRecovery(recoveryCode);
|
|
1327
|
-
await recover();
|
|
1425
|
+
const recovery = await recover();
|
|
1426
|
+
if (recovery.status !== 0) {
|
|
1427
|
+
return { result, violation: null };
|
|
1428
|
+
}
|
|
1429
|
+
if (adapter === "extension") {
|
|
1430
|
+
state.mutations.push({
|
|
1431
|
+
type: "runtime",
|
|
1432
|
+
action: "reloaded",
|
|
1433
|
+
reasonCode: recoveryCode
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1328
1436
|
result = await exec();
|
|
1329
1437
|
if (result.status === "pass") {
|
|
1330
1438
|
state.recovered.push(recoveryCode);
|
|
@@ -1387,12 +1495,12 @@ export {
|
|
|
1387
1495
|
ensureMobileProofRuntime,
|
|
1388
1496
|
executeWithHealBounds,
|
|
1389
1497
|
listRunnableRecipes,
|
|
1498
|
+
persistRunEffects,
|
|
1390
1499
|
preflightRecipe,
|
|
1391
1500
|
prepareHeal,
|
|
1392
1501
|
prepareRuntimeIfNeeded,
|
|
1393
1502
|
recoverRunInfra,
|
|
1394
1503
|
resolveMetaMaskLibrarySources,
|
|
1395
|
-
resolveMobileRecoveryPlatform,
|
|
1396
1504
|
resolveRunRecipeArg,
|
|
1397
1505
|
runOneNode,
|
|
1398
1506
|
runRecipe,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
3
4
|
import { isRecord } from "./parse-args.js";
|
|
4
5
|
function writeRunReport(result) {
|
|
5
6
|
const summaryPath = String(result.summaryPath);
|
|
@@ -19,6 +20,72 @@ function writeRunReport(result) {
|
|
|
19
20
|
preview: lines.filter((line) => line.startsWith("- ")).slice(0, 6).map((line) => line.slice(2))
|
|
20
21
|
};
|
|
21
22
|
}
|
|
23
|
+
function indexProductProvenanceArtifact(artifactManifestPath, target, adapter) {
|
|
24
|
+
const artifactsDir = path.dirname(artifactManifestPath);
|
|
25
|
+
let gitRef = "unknown";
|
|
26
|
+
let branch = "unknown";
|
|
27
|
+
let dirty = true;
|
|
28
|
+
try {
|
|
29
|
+
gitRef = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
30
|
+
cwd: target,
|
|
31
|
+
encoding: "utf8",
|
|
32
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
33
|
+
}).trim();
|
|
34
|
+
branch = execFileSync("git", ["branch", "--show-current"], {
|
|
35
|
+
cwd: target,
|
|
36
|
+
encoding: "utf8",
|
|
37
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
38
|
+
}).trim() || "detached";
|
|
39
|
+
dirty = execFileSync(
|
|
40
|
+
"git",
|
|
41
|
+
["status", "--porcelain", "--untracked-files=normal"],
|
|
42
|
+
{
|
|
43
|
+
cwd: target,
|
|
44
|
+
encoding: "utf8",
|
|
45
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
46
|
+
}
|
|
47
|
+
).trim().length > 0;
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
const provenance = {
|
|
51
|
+
schemaVersion: 1,
|
|
52
|
+
adapter,
|
|
53
|
+
repository: path.basename(path.resolve(target)),
|
|
54
|
+
git_ref: gitRef,
|
|
55
|
+
branch,
|
|
56
|
+
dirty
|
|
57
|
+
};
|
|
58
|
+
fs.writeFileSync(
|
|
59
|
+
path.join(artifactsDir, "product-provenance.json"),
|
|
60
|
+
`${JSON.stringify(provenance, null, 2)}
|
|
61
|
+
`
|
|
62
|
+
);
|
|
63
|
+
let manifest;
|
|
64
|
+
try {
|
|
65
|
+
manifest = JSON.parse(fs.readFileSync(artifactManifestPath, "utf8"));
|
|
66
|
+
} catch {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (!isRecord(manifest)) return;
|
|
70
|
+
const artifacts = Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
|
|
71
|
+
manifest.artifacts = [
|
|
72
|
+
...artifacts.filter(
|
|
73
|
+
(artifact) => !isRecord(artifact) || artifact.path !== "product-provenance.json"
|
|
74
|
+
),
|
|
75
|
+
{
|
|
76
|
+
path: "product-provenance.json",
|
|
77
|
+
type: "json",
|
|
78
|
+
label: "Product checkout provenance",
|
|
79
|
+
category: "system",
|
|
80
|
+
metadata: { adapter, git_ref: gitRef, branch, dirty }
|
|
81
|
+
}
|
|
82
|
+
];
|
|
83
|
+
fs.writeFileSync(
|
|
84
|
+
artifactManifestPath,
|
|
85
|
+
`${JSON.stringify(manifest, null, 2)}
|
|
86
|
+
`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
22
89
|
function renderRunReport(summary, entries) {
|
|
23
90
|
const lines = [
|
|
24
91
|
"# MetaMask Recipe Run",
|
|
@@ -120,5 +187,6 @@ function indexRunReportArtifact(artifactManifestPath) {
|
|
|
120
187
|
`);
|
|
121
188
|
}
|
|
122
189
|
export {
|
|
190
|
+
indexProductProvenanceArtifact,
|
|
123
191
|
writeRunReport
|
|
124
192
|
};
|