@deeeed/metamask-harness 0.59.0 → 0.60.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 +10 -0
- package/README.md +48 -0
- package/adapters/extension/build-handoff.cjs +4 -0
- package/adapters/extension/live.sh +15 -11
- package/adapters/extension/pin-remote-flags.cjs +69 -30
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
- package/adapters/mobile/bridge-runtime/setup-wallet.sh +69 -2
- package/dist/adapters/extension/feature-flags.js +41 -0
- package/dist/adapters/extension/runtime-decision.js +1 -0
- package/dist/adapters/mobile/feature-flags.js +127 -0
- package/dist/adapters.js +6 -1
- package/dist/command-contract.js +6 -3
- package/dist/commands/doctor.js +9 -1
- package/dist/commands/fixtures.js +45 -3
- package/dist/commands/launch/extension.js +28 -6
- package/dist/commands/launch/index.js +128 -17
- package/dist/commands/launch/mobile.js +26 -2
- package/dist/commands/mobile-device-view.js +8 -1
- package/dist/commands/prepare.js +20 -3
- package/dist/commands/run-engine.js +10 -1
- package/dist/commands/runtime-launch.js +10 -1
- package/dist/commands/status-probe.js +14 -1
- package/dist/commands/status.js +13 -0
- package/dist/doctor.js +9 -1
- package/dist/feature-flags.js +193 -0
- package/dist/mm-harness-cli.js +15 -2
- package/library/actions/core/feature_flags/_unsupported.mjs +29 -0
- package/library/actions/core/feature_flags/clear.mjs +8 -0
- package/library/actions/core/feature_flags/read.mjs +8 -0
- package/library/actions/core/feature_flags/set.mjs +8 -0
- package/library/actions/extension/feature_flags/clear.mjs +48 -0
- package/library/actions/extension/feature_flags/read.mjs +27 -0
- package/library/actions/extension/feature_flags/set.mjs +36 -0
- package/library/actions/extension/feature_flags/state.mjs +129 -0
- package/library/actions/harness-exports.mjs +14 -0
- package/library/actions/mobile/feature_flags/clear.mjs +20 -0
- package/library/actions/mobile/feature_flags/controller.mjs +120 -0
- package/library/actions/mobile/feature_flags/read.mjs +20 -0
- package/library/actions/mobile/feature_flags/set.mjs +25 -0
- package/library/actions/mobile/wallet/reset.mjs +22 -1
- package/library/manifests/core.action-manifest.json +90 -0
- package/library/manifests/extension.action-manifest.json +70 -0
- package/library/manifests/mobile.action-manifest.json +90 -0
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -2,6 +2,8 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { readExtensionPinnedFlags } from "../adapters/extension/feature-flags.js";
|
|
6
|
+
import { featureFlagReport } from "../feature-flags.js";
|
|
5
7
|
import { color } from "../cli-color.js";
|
|
6
8
|
import { collectCheckoutView, renderCheckoutView } from "../checkout-view.js";
|
|
7
9
|
import { createDoctorReport, fixtureSummary, requiredDoctorCheckSummary } from "../doctor.js";
|
|
@@ -209,12 +211,14 @@ async function handleDoctor({ options }) {
|
|
|
209
211
|
}
|
|
210
212
|
const skills = skillsView(target);
|
|
211
213
|
const update = updateStatus();
|
|
214
|
+
const extensionFlags = adapter === "extension" ? readExtensionPinnedFlags(target) : null;
|
|
215
|
+
const featureFlags = extensionFlags ? featureFlagReport(extensionFlags.overrides, extensionFlags) : featureFlagReport(liveView?.featureFlagOverrides ?? null);
|
|
212
216
|
const doctorUserAction = doctorResult.checks.find(
|
|
213
217
|
(check) => check.required && check.status === "fail" && check.userAction
|
|
214
218
|
)?.userAction ?? `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
|
|
215
219
|
const doctorError = doctorResult.status === "fail" ? { code: "DOCTOR_CHECKS_FAILED", message: "one or more required doctor checks failed", userAction: doctorUserAction } : void 0;
|
|
216
220
|
const next = doctorError?.userAction ?? discovery.deviceDiscoveryErrors?.[0]?.userAction ?? (runtime?.decision === "ready" ? void 0 : runtime?.nextAction);
|
|
217
|
-
if (json) console.log(JSON.stringify({ ...doctorResult, ready: runtime?.decision === "ready", runtime, view, orphanMetros, capture, skills, update, devices, additionalReachableDevices, ...discovery, ...next ? { next } : {}, ...doctorError ? { error: doctorError } : {} }, null, 2));
|
|
221
|
+
if (json) console.log(JSON.stringify({ ...doctorResult, ready: runtime?.decision === "ready", runtime, view, orphanMetros, capture, skills, update, featureFlags, devices, additionalReachableDevices, ...discovery, ...next ? { next } : {}, ...doctorError ? { error: doctorError } : {} }, null, 2));
|
|
218
222
|
else {
|
|
219
223
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
220
224
|
const stateStyle = (value, good) => value === good ? "ok" : "warn";
|
|
@@ -236,6 +240,10 @@ async function handleDoctor({ options }) {
|
|
|
236
240
|
console.log(`${out("err", "runtime probe failed:")} ${out("dim", runtimeProbeError)}`);
|
|
237
241
|
}
|
|
238
242
|
console.log(renderCheckoutView(view));
|
|
243
|
+
console.log(`${out("label", "feature flags:")} ${out(
|
|
244
|
+
featureFlags.error || (featureFlags.overrideCount ?? 0) > 0 ? "warn" : "dim",
|
|
245
|
+
featureFlags.summary.replace(/^feature flags: /u, "")
|
|
246
|
+
)}`);
|
|
239
247
|
if (adapter === "mobile") {
|
|
240
248
|
const perps = mobilePerpsEnvironment(target);
|
|
241
249
|
console.log(
|
|
@@ -8,6 +8,8 @@ import { acquireCheckoutLock } from "../checkout-lock.js";
|
|
|
8
8
|
import { colorHumanMessage } from "../cli-color.js";
|
|
9
9
|
import { applyDeviceTargeting } from "./device-target.js";
|
|
10
10
|
import { ADAPTER_DETECT_NEXT, checkoutBusyOut, EXIT, flag, parseFlags, resolveAdapter, scriptOverride, spawnScript, spawnScriptStreaming, str, targetOf, usageOut } from "./shared.js";
|
|
11
|
+
import { FeatureFlagSpecError, fixtureFeatureFlags, parseRemoteFlagSpec, sameFeatureFlags } from "../feature-flags.js";
|
|
12
|
+
import { readExtensionPinnedFlags } from "../adapters/extension/feature-flags.js";
|
|
11
13
|
const FIXTURES_BOOLEANS = /* @__PURE__ */ new Set(["dev", "force", "json"]);
|
|
12
14
|
const FIXTURE_STREAM_TIMEOUT_MS = 12e4;
|
|
13
15
|
const MOBILE_SETUP_WALLET_TIMEOUT_MS = 36e4;
|
|
@@ -177,8 +179,20 @@ async function handleFixturesLocked(argv) {
|
|
|
177
179
|
);
|
|
178
180
|
}
|
|
179
181
|
}
|
|
182
|
+
const remoteFlagSpec = str(options, "remoteFlag");
|
|
183
|
+
let fixtureFlags;
|
|
184
|
+
try {
|
|
185
|
+
fixtureFlags = {
|
|
186
|
+
...fixtureFeatureFlags(fixturePath),
|
|
187
|
+
...remoteFlagSpec === void 0 ? {} : parseRemoteFlagSpec(remoteFlagSpec)
|
|
188
|
+
};
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (!(error instanceof FeatureFlagSpecError)) throw error;
|
|
191
|
+
return usageOut(json, "fixtures", error.message, error.userAction);
|
|
192
|
+
}
|
|
193
|
+
const fixtureFlagsJson = JSON.stringify(fixtureFlags);
|
|
180
194
|
if (adapter === "extension") {
|
|
181
|
-
return applyExtensionFixture(target, fixturePath, canonicalFixture, json, sub);
|
|
195
|
+
return applyExtensionFixture(target, fixturePath, canonicalFixture, json, sub, fixtureFlags);
|
|
182
196
|
}
|
|
183
197
|
const reset = sub === "reset";
|
|
184
198
|
process.stderr.write(`\u2192 fixtures ${sub} ${adapter} \u2014 ${reset ? "resetting selected app data, relaunching, then applying" : "connecting bridge + applying"} wallet fixture (can take ~30s)\u2026
|
|
@@ -252,7 +266,7 @@ async function handleFixturesLocked(argv) {
|
|
|
252
266
|
if (!resetError) {
|
|
253
267
|
mobileSetupResult = await spawnScriptStreaming(
|
|
254
268
|
setupWalletSh,
|
|
255
|
-
["--fixture", fixturePath],
|
|
269
|
+
["--fixture", fixturePath, "--remote-flags-json", fixtureFlagsJson],
|
|
256
270
|
target,
|
|
257
271
|
{
|
|
258
272
|
env: {
|
|
@@ -303,7 +317,7 @@ async function handleFixturesLocked(argv) {
|
|
|
303
317
|
}
|
|
304
318
|
return status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
305
319
|
}
|
|
306
|
-
async function applyExtensionFixture(target, fixturePath, canonicalFixture, json, action) {
|
|
320
|
+
async function applyExtensionFixture(target, fixturePath, canonicalFixture, json, action, fixtureFlags) {
|
|
307
321
|
const distManifest = path.join(target, "dist/chrome/manifest.json");
|
|
308
322
|
const userAction = `mm-harness launch --build --verify --target ${JSON.stringify(target)}`;
|
|
309
323
|
if (!fs.existsSync(distManifest)) {
|
|
@@ -330,11 +344,39 @@ async function applyExtensionFixture(target, fixturePath, canonicalFixture, json
|
|
|
330
344
|
`);
|
|
331
345
|
const liveScript = path.join(runnerDir, "adapters/extension/live.sh");
|
|
332
346
|
const args = ["--target", target, "--launch-existing-dist", "--launch-only", "--reset-profile"];
|
|
347
|
+
const flagKeys = Object.keys(fixtureFlags);
|
|
348
|
+
if (flagKeys.length > 0) args.push("--remote-flags-json", JSON.stringify(fixtureFlags));
|
|
333
349
|
if (process.env.CDP_PORT) args.push("--cdp-port", process.env.CDP_PORT);
|
|
334
350
|
const result = await spawnScriptStreaming(liveScript, args, target, {
|
|
335
351
|
env: { RECIPE_WALLET_FIXTURE: fixturePath },
|
|
336
352
|
timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
|
|
337
353
|
});
|
|
354
|
+
const pinned = readExtensionPinnedFlags(target);
|
|
355
|
+
const pinnedFlags = pinned.pinned ?? {};
|
|
356
|
+
const flagMismatch = result.status === 0 && !pinned.error && (pinned.pinned === null || !sameFeatureFlags(pinnedFlags, fixtureFlags)) ? [.../* @__PURE__ */ new Set([...flagKeys, ...Object.keys(pinnedFlags)])].filter(
|
|
357
|
+
(key) => JSON.stringify(pinnedFlags[key]) !== JSON.stringify(fixtureFlags[key])
|
|
358
|
+
) : [];
|
|
359
|
+
if (result.status === 0 && (pinned.error || flagMismatch.length > 0)) {
|
|
360
|
+
const detail = pinned.error ? `the launched snapshot manifest could not be read (${pinned.sourcePath ?? "unknown path"}): ${pinned.error}` : `snapshot pin disagrees with the fixture for: ${flagMismatch.join(", ")}`;
|
|
361
|
+
const flagNext = `mm-harness launch --adapter extension --build --target ${JSON.stringify(target)}`;
|
|
362
|
+
if (json) {
|
|
363
|
+
console.log(JSON.stringify({
|
|
364
|
+
schemaVersion: 1,
|
|
365
|
+
command: "fixtures",
|
|
366
|
+
action,
|
|
367
|
+
adapter: "extension",
|
|
368
|
+
fixture: fixturePath,
|
|
369
|
+
canonicalFixture,
|
|
370
|
+
status: "fail",
|
|
371
|
+
exitCode: EXIT.runtime,
|
|
372
|
+
error: { code: "EXTENSION_FIXTURE_FLAGS_NOT_APPLIED", message: detail, userAction: flagNext }
|
|
373
|
+
}, null, 2));
|
|
374
|
+
} else {
|
|
375
|
+
console.error(`\u2717 mm-harness fixtures ${action}: ${detail}
|
|
376
|
+
Next: ${flagNext}`);
|
|
377
|
+
}
|
|
378
|
+
return EXIT.runtime;
|
|
379
|
+
}
|
|
338
380
|
const status = result.status === 0 ? "pass" : "fail";
|
|
339
381
|
const exitCode = status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
340
382
|
const message = status === "pass" ? `Wallet fixture applied and validated. Want different accounts? Edit: ${canonicalFixture}` : "Extension wallet fixture could not be applied and validated.";
|
|
@@ -30,6 +30,8 @@ import { checkExtensionRuntimeHealth } from "../../adapters/extension/runtime.js
|
|
|
30
30
|
import { ensureHarnessFresh } from "../../adapters/harness-freshness.js";
|
|
31
31
|
import { isExtensionWatcherLive, stopExtensionWatcher } from "../../adapters/slot-ports.js";
|
|
32
32
|
import { EXIT, spawnScriptStreaming } from "../shared.js";
|
|
33
|
+
import { readExtensionPinnedFlags } from "../../adapters/extension/feature-flags.js";
|
|
34
|
+
import { sameFeatureFlags } from "../../feature-flags.js";
|
|
33
35
|
const { CdpSession } = await importRecipeHarnessRuntimeCdp();
|
|
34
36
|
const { RUNTIME_IDENTITY_FILENAME, RUNTIME_NONCE_PREFIX } = createRequire(import.meta.url)(
|
|
35
37
|
path.join(runnerDir, "adapters/extension/lib/chrome-args.cjs")
|
|
@@ -55,7 +57,7 @@ async function installExtensionDeps(target) {
|
|
|
55
57
|
if (result.status === 0) recordDepsBaseline(target);
|
|
56
58
|
return result;
|
|
57
59
|
}
|
|
58
|
-
async function launchExtension(target, tier, wantWatch, displayMode = "fullscreen") {
|
|
60
|
+
async function launchExtension(target, tier, wantWatch, displayMode = "fullscreen", remoteFlags) {
|
|
59
61
|
const requestedCdpPort = process.env.CDP_PORT;
|
|
60
62
|
const requestedRecipeCdpPort = process.env.RECIPE_CDP_PORT;
|
|
61
63
|
const restoreRequestedPorts = () => {
|
|
@@ -68,8 +70,19 @@ async function launchExtension(target, tier, wantWatch, displayMode = "fullscree
|
|
|
68
70
|
const artifact = releaseArtifactState(target);
|
|
69
71
|
const artifactSnapshot = runtimeDistCheck(target, artifact);
|
|
70
72
|
const artifactLaunchable = !wantWatch && tier !== "build" && artifact.status === "valid" && artifactSnapshot.status === "fresh";
|
|
71
|
-
const
|
|
73
|
+
const livePin = readExtensionPinnedFlags(target);
|
|
74
|
+
const requestedPin = remoteFlags?.flags ?? {};
|
|
75
|
+
const pinCurrent = !livePin.error && (livePin.overrides === null ? Object.keys(requestedPin).length === 0 : livePin.pinned !== null) && sameFeatureFlags(livePin.pinned ?? {}, requestedPin);
|
|
76
|
+
const reusable = !wantWatch && tier !== "build" && pinCurrent && await extensionRuntimeReusable(target, artifact);
|
|
72
77
|
restoreRequestedPorts();
|
|
78
|
+
if (!pinCurrent && !wantWatch) {
|
|
79
|
+
const have = Object.keys(livePin.pinned ?? {});
|
|
80
|
+
const want = Object.keys(requestedPin);
|
|
81
|
+
const dropping = have.filter((key) => !want.includes(key));
|
|
82
|
+
console.error(colorHumanMessage(
|
|
83
|
+
`\u2192 extension launch \u2014 re-creating the dist snapshot to pin remote feature flags` + (want.length ? ` (${want.join(", ")})` : "") + (dropping.length ? `, dropping the previous run's ${dropping.join(", ")}` : "")
|
|
84
|
+
));
|
|
85
|
+
}
|
|
73
86
|
if (artifact.status !== "none" && !reusable && !artifactLaunchable) {
|
|
74
87
|
const detail = artifact.status === "invalid" ? `identity is invalid: ${artifact.reason ?? "unknown reason"}` : "runtime is not healthy and reusable";
|
|
75
88
|
return {
|
|
@@ -98,7 +111,7 @@ Next: ${block.userAction}`
|
|
|
98
111
|
return spawnScriptStreaming(startWatchSh, watchArgs, target);
|
|
99
112
|
}
|
|
100
113
|
if (tier === "build") {
|
|
101
|
-
return extensionRebuild(target);
|
|
114
|
+
return extensionRebuild(target, remoteFlags);
|
|
102
115
|
}
|
|
103
116
|
if (reusable) {
|
|
104
117
|
if (artifact.status === "valid") {
|
|
@@ -110,9 +123,16 @@ Next: ${block.userAction}`
|
|
|
110
123
|
return extensionReattach(target, displayMode);
|
|
111
124
|
}
|
|
112
125
|
if (artifactLaunchable) {
|
|
126
|
+
if (remoteFlags) {
|
|
127
|
+
return {
|
|
128
|
+
status: EXIT.infra,
|
|
129
|
+
output: `REMOTE_FLAG_NOT_PINNABLE: the remote feature-flag pin '${remoteFlags.spec}' (from --remote-flag and the wallet fixture's remoteFeatureFlags) cannot be applied to a verified release artifact; the artifact's manifest is signed evidence and is never rewritten.
|
|
130
|
+
Next: drop --remote-flag and remove remoteFeatureFlags from the wallet fixture to validate the artifact as shipped, or validate the variant on a source checkout launched without the artifact.`
|
|
131
|
+
};
|
|
132
|
+
}
|
|
113
133
|
return extensionLaunchReleaseArtifact(target, artifact);
|
|
114
134
|
}
|
|
115
|
-
return extensionLaunchDevelopment(target);
|
|
135
|
+
return extensionLaunchDevelopment(target, remoteFlags);
|
|
116
136
|
}
|
|
117
137
|
async function extensionRuntimeReusable(target, artifact = releaseArtifactState(target)) {
|
|
118
138
|
const cdpPort = process.env.CDP_PORT;
|
|
@@ -370,11 +390,12 @@ async function extensionReattach(target, displayMode = "fullscreen") {
|
|
|
370
390
|
console.error(colorHumanMessage(`\u2192 extension quick reattach \u2014 reload in place \xB7 CDP :${process.env.CDP_PORT ?? "default"} (no rebuild, no relaunch)`));
|
|
371
391
|
return spawnScriptStreaming(reattachScript, reattachArgs, target);
|
|
372
392
|
}
|
|
373
|
-
async function extensionLaunchDevelopment(target) {
|
|
393
|
+
async function extensionLaunchDevelopment(target, remoteFlags) {
|
|
374
394
|
const liveScript = recipeHarnessPath(target, "extension", "scripts", "live.sh");
|
|
375
395
|
const liveArgs = ["--target", target];
|
|
376
396
|
const existingBuild = isExtensionWatcherLive(target) && harnessWatcherMatchesProductConfig(target) && extensionCompiledScriptsMatchProductConfig(target, path.join(target, "dist/chrome")) && fs.existsSync(path.join(target, "dist/chrome/manifest.json")) && !isExtensionDistStale(target);
|
|
377
397
|
liveArgs.push(existingBuild ? "--launch-existing-dist" : "--start-watch");
|
|
398
|
+
if (remoteFlags) liveArgs.push("--remote-flags-json", remoteFlags.json);
|
|
378
399
|
if (process.env.CDP_PORT) liveArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
379
400
|
if (process.env.EXTENSION_START_URL) liveArgs.push("--start-url", process.env.EXTENSION_START_URL);
|
|
380
401
|
console.error(colorHumanMessage(
|
|
@@ -425,7 +446,7 @@ function harnessWatcherMatchesProductConfig(target) {
|
|
|
425
446
|
return false;
|
|
426
447
|
}
|
|
427
448
|
}
|
|
428
|
-
async function extensionRebuild(target) {
|
|
449
|
+
async function extensionRebuild(target, remoteFlags) {
|
|
429
450
|
const runtimeDirRel = recipeRuntimeDir();
|
|
430
451
|
const runtimeAbs = path.join(target, runtimeDirRel);
|
|
431
452
|
const rebuildLog = path.join(runtimeAbs, "rebuild.log");
|
|
@@ -434,6 +455,7 @@ async function extensionRebuild(target) {
|
|
|
434
455
|
fs.writeFileSync(rebuildLog, "");
|
|
435
456
|
const liveScript = recipeHarnessPath(target, "extension", "scripts", "live.sh");
|
|
436
457
|
const liveArgs = ["--target", target, "--build-lavamoat"];
|
|
458
|
+
if (remoteFlags) liveArgs.push("--remote-flags-json", remoteFlags.json);
|
|
437
459
|
if (process.env.CDP_PORT) liveArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
438
460
|
if (process.env.EXTENSION_START_URL) liveArgs.push("--start-url", process.env.EXTENSION_START_URL);
|
|
439
461
|
console.error(colorHumanMessage(`\u2192 extension quick relaunch \u2014 production-like LavaMoat build \xB7 CDP :${process.env.CDP_PORT ?? "default"} (output streams below)`));
|
|
@@ -3,7 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { color, colorHumanMessage } from "../../cli-color.js";
|
|
5
5
|
import { handleHarness } from "../../harness.js";
|
|
6
|
-
import { recipeRuntimePath, runnerDir } from "../../paths.js";
|
|
6
|
+
import { recipeRuntimePath, runnerDir, walletFixturePath } from "../../paths.js";
|
|
7
7
|
import { getAdapterSurface } from "../../adapters/surface.js";
|
|
8
8
|
import {
|
|
9
9
|
ADAPTER_DETECT_NEXT,
|
|
@@ -35,6 +35,15 @@ import { launchMobile } from "./mobile.js";
|
|
|
35
35
|
import { applyDeviceTargeting } from "../device-target.js";
|
|
36
36
|
import { acquireCheckoutLock } from "../../checkout-lock.js";
|
|
37
37
|
import { JsonStreamWriter } from "../../json-stream.js";
|
|
38
|
+
import {
|
|
39
|
+
FeatureFlagSpecError,
|
|
40
|
+
fixtureFeatureFlags,
|
|
41
|
+
parseRemoteFlagSpec,
|
|
42
|
+
remoteFlagRequestFor,
|
|
43
|
+
sameFeatureFlags,
|
|
44
|
+
toRemoteFlagSpec
|
|
45
|
+
} from "../../feature-flags.js";
|
|
46
|
+
import { readExtensionPinnedFlags } from "../../adapters/extension/feature-flags.js";
|
|
38
47
|
import { ensureExtensionConsoleCapture } from "../../adapters/extension/console-capture.js";
|
|
39
48
|
import { extensionProductConfigBlock } from "../../adapters/extension/product-config.js";
|
|
40
49
|
import {
|
|
@@ -178,6 +187,25 @@ async function handleLaunchLocked(argv, stream) {
|
|
|
178
187
|
);
|
|
179
188
|
}
|
|
180
189
|
const displayMode = requestedSurface ?? legacySurface ?? "fullscreen";
|
|
190
|
+
const remoteFlagSpec = str(options, "remoteFlag");
|
|
191
|
+
if (remoteFlagSpec !== void 0 && adapter === "extension" && wantWatch) {
|
|
192
|
+
return launchUsage(
|
|
193
|
+
jsonOutput,
|
|
194
|
+
stream,
|
|
195
|
+
"--remote-flag cannot be applied with --watch: the watcher never re-creates the dist snapshot that carries the pin.",
|
|
196
|
+
`mm-harness launch --adapter extension --remote-flag ${remoteFlagSpec} --target ${shellQuote(target)}`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
let remoteFlagRequest;
|
|
200
|
+
try {
|
|
201
|
+
remoteFlagRequest = remoteFlagRequestFor({
|
|
202
|
+
...fixtureFeatureFlags(walletFixturePath(target)),
|
|
203
|
+
...remoteFlagSpec === void 0 ? {} : parseRemoteFlagSpec(remoteFlagSpec)
|
|
204
|
+
});
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (!(error instanceof FeatureFlagSpecError)) throw error;
|
|
207
|
+
return launchUsage(jsonOutput, stream, error.message, error.userAction);
|
|
208
|
+
}
|
|
181
209
|
for (const portFlag of ["cdpPort", "watcherPort"]) {
|
|
182
210
|
const value = str(options, portFlag);
|
|
183
211
|
if (value !== void 0 && !/^\d+$/u.test(value)) {
|
|
@@ -286,10 +314,23 @@ async function handleLaunchLocked(argv, stream) {
|
|
|
286
314
|
machine,
|
|
287
315
|
displayMode,
|
|
288
316
|
false,
|
|
289
|
-
wantClearMetro
|
|
317
|
+
wantClearMetro,
|
|
318
|
+
remoteFlagRequest
|
|
290
319
|
);
|
|
291
320
|
if (attempt.status === 0) {
|
|
292
|
-
return await
|
|
321
|
+
return await settleLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify, remoteFlagRequest, true, attempt.remoteFeatureFlags);
|
|
322
|
+
}
|
|
323
|
+
if (adapter === "extension" && attempt.output.includes("REMOTE_FLAG_NOT_PINNABLE:")) {
|
|
324
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
325
|
+
code: "REMOTE_FLAG_NOT_PINNABLE",
|
|
326
|
+
message: `a verified release artifact cannot carry a remote feature-flag pin (${remoteFlagRequest?.spec ?? ""}); its manifest is signed evidence and is never rewritten. Validate the artifact as shipped (drop --remote-flag and the fixture's remoteFeatureFlags) or validate the variant on a source build.`,
|
|
327
|
+
recoverable: false,
|
|
328
|
+
// The next step must leave the artifact alone: a --build here would replace
|
|
329
|
+
// the artifact under validation with a source build.
|
|
330
|
+
userAction: Object.keys(fixtureFeatureFlags(walletFixturePath(target))).length > 0 ? `remove remoteFeatureFlags from ${shellQuote(walletFixturePath(target))}, then: mm-harness launch --adapter extension --target ${shellQuote(target)}` : `mm-harness launch --adapter extension --target ${shellQuote(target)}`,
|
|
331
|
+
exitCode: EXIT.infra,
|
|
332
|
+
originalError: attempt.output.trim() || void 0
|
|
333
|
+
});
|
|
293
334
|
}
|
|
294
335
|
if (adapter === "extension" && extensionProductConfigMissing(attempt.output)) {
|
|
295
336
|
const block = extensionProductConfigBlock(target);
|
|
@@ -352,10 +393,10 @@ async function handleLaunchLocked(argv, stream) {
|
|
|
352
393
|
const recoveryCode2 = "chrome.relaunched";
|
|
353
394
|
state.attemptedRecoveries.push(recoveryCode2);
|
|
354
395
|
stream.phase("recover");
|
|
355
|
-
const rebuildAttempt = await executeComposition(adapter, mobileTarget, "build", wantWatch, target, machine, displayMode);
|
|
396
|
+
const rebuildAttempt = await executeComposition(adapter, mobileTarget, "build", wantWatch, target, machine, displayMode, false, false, remoteFlagRequest);
|
|
356
397
|
if (rebuildAttempt.status === 0) {
|
|
357
398
|
state.recovered.push(recoveryCode2);
|
|
358
|
-
return await
|
|
399
|
+
return await settleLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify, remoteFlagRequest, true, rebuildAttempt.remoteFeatureFlags);
|
|
359
400
|
}
|
|
360
401
|
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
361
402
|
code: "EXTENSION_RELAUNCH_FAILED",
|
|
@@ -434,11 +475,13 @@ async function handleLaunchLocked(argv, stream) {
|
|
|
434
475
|
target,
|
|
435
476
|
machine,
|
|
436
477
|
displayMode,
|
|
437
|
-
restartMobileApp
|
|
478
|
+
restartMobileApp,
|
|
479
|
+
false,
|
|
480
|
+
remoteFlagRequest
|
|
438
481
|
);
|
|
439
482
|
if (attempt.status === 0) {
|
|
440
483
|
state.recovered.push(recoveryCode);
|
|
441
|
-
return await
|
|
484
|
+
return await settleLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify, remoteFlagRequest, true, attempt.remoteFeatureFlags);
|
|
442
485
|
}
|
|
443
486
|
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
444
487
|
code: "SAME_RECOVERY_TWICE",
|
|
@@ -560,7 +603,39 @@ function nativeInputsChanged(target, adapter) {
|
|
|
560
603
|
return false;
|
|
561
604
|
}
|
|
562
605
|
}
|
|
563
|
-
|
|
606
|
+
function verifyExtensionPin(target, requested) {
|
|
607
|
+
const pinned = readExtensionPinnedFlags(target);
|
|
608
|
+
if (pinned.error) {
|
|
609
|
+
return {
|
|
610
|
+
ok: false,
|
|
611
|
+
flags: {},
|
|
612
|
+
code: "RUNTIME_MANIFEST_UNREADABLE",
|
|
613
|
+
message: `the launched dist snapshot manifest could not be read (${pinned.sourcePath ?? "unknown path"}): ${pinned.error}`
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
if (pinned.overrides !== null && pinned.pinned === null) {
|
|
617
|
+
return {
|
|
618
|
+
ok: false,
|
|
619
|
+
flags: {},
|
|
620
|
+
code: "REMOTE_FLAG_NOT_APPLIED",
|
|
621
|
+
message: `the launched dist snapshot manifest (${pinned.sourcePath ?? "unknown path"}) carries no harness pin record; it was not written by this launch.`
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
const actual = pinned.pinned ?? {};
|
|
625
|
+
if (!sameFeatureFlags(actual, requested)) {
|
|
626
|
+
const mismatched = [.../* @__PURE__ */ new Set([...Object.keys(requested), ...Object.keys(actual)])].filter(
|
|
627
|
+
(key) => JSON.stringify(actual[key]) !== JSON.stringify(requested[key])
|
|
628
|
+
);
|
|
629
|
+
return {
|
|
630
|
+
ok: false,
|
|
631
|
+
flags: {},
|
|
632
|
+
code: "REMOTE_FLAG_NOT_APPLIED",
|
|
633
|
+
message: `the remote feature-flag pin ${toRemoteFlagSpec(requested) || "(none)"} does not match the launched snapshot manifest: ` + mismatched.map((key) => `${key} expected ${JSON.stringify(requested[key])}, got ${JSON.stringify(actual[key])}`).join("; ")
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
return { ok: true, flags: actual, message: "" };
|
|
637
|
+
}
|
|
638
|
+
async function executeComposition(adapter, mobileTarget, tier, wantWatch, target, json, displayMode = "fullscreen", restartMobileApp = false, clearMobileMetro = false, remoteFlagRequest) {
|
|
564
639
|
if (adapter === "mobile") {
|
|
565
640
|
return launchMobile(
|
|
566
641
|
target,
|
|
@@ -568,12 +643,44 @@ async function executeComposition(adapter, mobileTarget, tier, wantWatch, target
|
|
|
568
643
|
tier,
|
|
569
644
|
json,
|
|
570
645
|
restartMobileApp,
|
|
571
|
-
clearMobileMetro
|
|
646
|
+
clearMobileMetro,
|
|
647
|
+
remoteFlagRequest?.flags
|
|
572
648
|
);
|
|
573
649
|
}
|
|
574
|
-
return launchExtension(target, tier, wantWatch, displayMode);
|
|
650
|
+
return launchExtension(target, tier, wantWatch, displayMode, remoteFlagRequest);
|
|
575
651
|
}
|
|
576
|
-
async function
|
|
652
|
+
async function settleLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify, remoteFlagRequest, sidepanelRecoveryAllowed = true, mobileProvenFlags) {
|
|
653
|
+
const requested = remoteFlagRequest?.flags ?? {};
|
|
654
|
+
const pinned = adapter !== "extension" ? { ok: true, flags: mobileProvenFlags === void 0 ? requested : mobileProvenFlags, message: "" } : wantWatch ? { ok: true, flags: readExtensionPinnedFlags(target).pinned, message: "" } : verifyExtensionPin(target, requested);
|
|
655
|
+
if (!pinned.ok) {
|
|
656
|
+
const spec = remoteFlagRequest?.spec;
|
|
657
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
658
|
+
code: pinned.code ?? "REMOTE_FLAG_NOT_APPLIED",
|
|
659
|
+
message: pinned.message,
|
|
660
|
+
recoverable: false,
|
|
661
|
+
// A rebuild re-creates the snapshot; the flag is repeated only when one was asked for.
|
|
662
|
+
userAction: `mm-harness launch --adapter extension --build${spec ? ` --remote-flag ${spec}` : ""} --target ${shellQuote(target)}`,
|
|
663
|
+
exitCode: EXIT.runtime
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
return finishLaunch(
|
|
667
|
+
jsonOutput,
|
|
668
|
+
machine,
|
|
669
|
+
stream,
|
|
670
|
+
adapter,
|
|
671
|
+
mobileTarget,
|
|
672
|
+
tier,
|
|
673
|
+
displayMode,
|
|
674
|
+
target,
|
|
675
|
+
state,
|
|
676
|
+
wantWatch,
|
|
677
|
+
wantVerify,
|
|
678
|
+
sidepanelRecoveryAllowed,
|
|
679
|
+
remoteFlagRequest,
|
|
680
|
+
pinned.flags
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
async function finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify, sidepanelRecoveryAllowed = true, remoteFlagRequest, remoteFlags = {}) {
|
|
577
684
|
if (adapter === "extension" && displayMode === "fullscreen") {
|
|
578
685
|
const sidepanelSh = path.join(runnerDir, "adapters/extension/sidepanel-toggle.sh");
|
|
579
686
|
const sidepanelArgs = ["close"];
|
|
@@ -605,10 +712,10 @@ async function finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget,
|
|
|
605
712
|
if (sidepanelRecoveryAllowed && tier === "quick" && extensionRuntimeBlocked(sidepanel.output) && !state.attemptedRecoveries.includes(recoveryCode)) {
|
|
606
713
|
state.attemptedRecoveries.push(recoveryCode);
|
|
607
714
|
stream.phase("recover");
|
|
608
|
-
const rebuildAttempt = await executeComposition(adapter, mobileTarget, "build", wantWatch, target, machine, displayMode);
|
|
715
|
+
const rebuildAttempt = await executeComposition(adapter, mobileTarget, "build", wantWatch, target, machine, displayMode, false, false, remoteFlagRequest);
|
|
609
716
|
if (rebuildAttempt.status === 0) {
|
|
610
717
|
state.recovered.push(recoveryCode);
|
|
611
|
-
return await
|
|
718
|
+
return await settleLaunch(
|
|
612
719
|
jsonOutput,
|
|
613
720
|
machine,
|
|
614
721
|
stream,
|
|
@@ -620,6 +727,7 @@ async function finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget,
|
|
|
620
727
|
state,
|
|
621
728
|
wantWatch,
|
|
622
729
|
wantVerify,
|
|
730
|
+
remoteFlagRequest,
|
|
623
731
|
false
|
|
624
732
|
);
|
|
625
733
|
}
|
|
@@ -668,14 +776,15 @@ async function finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget,
|
|
|
668
776
|
tier,
|
|
669
777
|
recovered: state.recovered,
|
|
670
778
|
mutations: state.mutations,
|
|
671
|
-
metro: metroLaunchEvidence(adapter, target)
|
|
779
|
+
metro: metroLaunchEvidence(adapter, target),
|
|
780
|
+
remoteFeatureFlags: remoteFlags
|
|
672
781
|
});
|
|
673
782
|
}
|
|
674
783
|
return exitCode;
|
|
675
784
|
}
|
|
676
|
-
return launchPass(jsonOutput, stream, adapter, mobileTarget, tier, displayMode, target, state);
|
|
785
|
+
return launchPass(jsonOutput, stream, adapter, mobileTarget, tier, displayMode, target, state, remoteFlags);
|
|
677
786
|
}
|
|
678
|
-
function launchPass(json, stream, adapter, mobileTarget, tier, displayMode, target, state) {
|
|
787
|
+
function launchPass(json, stream, adapter, mobileTarget, tier, displayMode, target, state, remoteFlags = {}) {
|
|
679
788
|
if (stream.enabled) {
|
|
680
789
|
for (const mutation of state.mutations) stream.mutation(mutation);
|
|
681
790
|
for (const recovery of state.recovered) stream.recovery(recovery);
|
|
@@ -685,7 +794,8 @@ function launchPass(json, stream, adapter, mobileTarget, tier, displayMode, targ
|
|
|
685
794
|
tier,
|
|
686
795
|
recovered: state.recovered,
|
|
687
796
|
mutations: state.mutations,
|
|
688
|
-
metro: metroLaunchEvidence(adapter, target)
|
|
797
|
+
metro: metroLaunchEvidence(adapter, target),
|
|
798
|
+
remoteFeatureFlags: remoteFlags
|
|
689
799
|
});
|
|
690
800
|
} else if (json) {
|
|
691
801
|
console.log(
|
|
@@ -701,6 +811,7 @@ function launchPass(json, stream, adapter, mobileTarget, tier, displayMode, targ
|
|
|
701
811
|
recovered: state.recovered,
|
|
702
812
|
mutations: state.mutations,
|
|
703
813
|
metro: metroLaunchEvidence(adapter, target),
|
|
814
|
+
remoteFeatureFlags: remoteFlags,
|
|
704
815
|
exitCode: EXIT.ok
|
|
705
816
|
},
|
|
706
817
|
null,
|
|
@@ -4,9 +4,11 @@ import {
|
|
|
4
4
|
recordMobileSourceBaseline
|
|
5
5
|
} from "../../adapters/mobile/source-freshness.js";
|
|
6
6
|
import { ensureHarnessFresh } from "../../adapters/harness-freshness.js";
|
|
7
|
+
import { pinMobileFeatureFlags } from "../../adapters/mobile/feature-flags.js";
|
|
8
|
+
import { toRemoteFlagSpec } from "../../feature-flags.js";
|
|
7
9
|
import { clearMobileReleaseArtifactState } from "../../adapters/mobile/release-artifact-state.js";
|
|
8
10
|
import { EXIT } from "../shared.js";
|
|
9
|
-
async function launchMobile(target, mobileTarget, tier, json, restartApp = false, clearMetro = false) {
|
|
11
|
+
async function launchMobile(target, mobileTarget, tier, json, restartApp = false, clearMetro = false, remoteFlags) {
|
|
10
12
|
clearMobileReleaseArtifactState(target);
|
|
11
13
|
await ensureHarnessFresh(target, "mobile");
|
|
12
14
|
const platform = mobileTarget ?? "ios";
|
|
@@ -22,13 +24,35 @@ async function launchMobile(target, mobileTarget, tier, json, restartApp = false
|
|
|
22
24
|
clearMetro
|
|
23
25
|
});
|
|
24
26
|
if (result.status !== 0) return result;
|
|
27
|
+
const flags = remoteFlags ?? {};
|
|
28
|
+
const pin = await pinMobileFeatureFlags(target, flags);
|
|
29
|
+
let provenFlags = flags;
|
|
30
|
+
if (!pin.ok && !remoteFlags && pin.error) {
|
|
31
|
+
provenFlags = null;
|
|
32
|
+
if (!json) process.stderr.write(`WARN: remote feature-flag overrides were not cleared: ${pin.error}
|
|
33
|
+
`);
|
|
34
|
+
} else if (!pin.ok) {
|
|
35
|
+
const detail = pin.error ?? `did not read back: ${pin.mismatched.join(", ")} (got ${JSON.stringify(
|
|
36
|
+
Object.fromEntries(pin.mismatched.map((key) => [key, pin.effective[key] ?? pin.overrides[key]]))
|
|
37
|
+
)})`;
|
|
38
|
+
const message = `REMOTE_FLAG_NOT_APPLIED: remote feature flags ${toRemoteFlagSpec(flags) || "(none: clearing overrides)"} ${detail}
|
|
39
|
+
Next: confirm the app is running with the harness bridge, then re-run mm-harness launch`;
|
|
40
|
+
if (!json) process.stderr.write(`${message}
|
|
41
|
+
`);
|
|
42
|
+
return { status: EXIT.runtime, output: message };
|
|
43
|
+
} else if (!json) {
|
|
44
|
+
process.stderr.write(
|
|
45
|
+
pin.applied.length ? `Remote feature flags pinned: ${pin.applied.join(", ")}
|
|
46
|
+
` : "Remote feature flag overrides cleared.\n"
|
|
47
|
+
);
|
|
48
|
+
}
|
|
25
49
|
if (!recordMobileSourceBaseline(target, source.fingerprint)) {
|
|
26
50
|
const message = "Mobile source changed while the app was launching; the loaded-source baseline was not recorded.\n Next: re-run mm-harness launch.";
|
|
27
51
|
if (!json) process.stderr.write(`${message}
|
|
28
52
|
`);
|
|
29
53
|
return { status: EXIT.runtime, output: message };
|
|
30
54
|
}
|
|
31
|
-
return result;
|
|
55
|
+
return { ...result, remoteFeatureFlags: provenFlags };
|
|
32
56
|
}
|
|
33
57
|
export {
|
|
34
58
|
launchMobile
|
|
@@ -21,9 +21,16 @@ async function mobileDeviceLiveView(target, view) {
|
|
|
21
21
|
liveMap,
|
|
22
22
|
devicesWithLive,
|
|
23
23
|
additionalReachableDevices: reachableDevices,
|
|
24
|
-
androidPortHints: selectedAndroidPortHints(view.devices, liveMap)
|
|
24
|
+
androidPortHints: selectedAndroidPortHints(view.devices, liveMap),
|
|
25
|
+
featureFlagOverrides: slotFeatureFlagOverrides(liveMap)
|
|
25
26
|
};
|
|
26
27
|
}
|
|
28
|
+
function slotFeatureFlagOverrides(liveMap) {
|
|
29
|
+
for (const live of liveMap.values()) {
|
|
30
|
+
if (live.featureFlagOverrides) return live.featureFlagOverrides;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
27
34
|
function scopedLiveMap(devices, allLiveMap) {
|
|
28
35
|
const liveMap = /* @__PURE__ */ new Map();
|
|
29
36
|
for (const device of devices) {
|
package/dist/commands/prepare.js
CHANGED
|
@@ -11,6 +11,7 @@ import { harnessExecutable, manifestPath, recipeRuntimeDir } from "../paths.js";
|
|
|
11
11
|
import { optionFlag, optionString, parseArgs, targetPath, usageError } from "./parse-args.js";
|
|
12
12
|
import { EXIT } from "./shared.js";
|
|
13
13
|
import { formatDuration } from "./status-watch.js";
|
|
14
|
+
import { FeatureFlagSpecError, parseRemoteFlagSpec } from "../feature-flags.js";
|
|
14
15
|
const USAGE = "mm-harness prepare [--target <checkout>] [--platform mobile|extension|core] [--mobile-platform ios|android] [--clear-metro] [--artifacts-dir <dir>] [--json]";
|
|
15
16
|
const REPORT_DIR = "prepare";
|
|
16
17
|
const SKIPPED_AFTER_FAILURE = "previous step failed";
|
|
@@ -318,6 +319,21 @@ async function handlePrepare(argv) {
|
|
|
318
319
|
throw usageError(`--clear-metro is mobile only; this checkout is ${platform}
|
|
319
320
|
Next: ${USAGE}`);
|
|
320
321
|
}
|
|
322
|
+
const remoteFlag = optionString(options, "remoteFlag");
|
|
323
|
+
if (remoteFlag !== void 0) {
|
|
324
|
+
if (platform === "core") {
|
|
325
|
+
throw usageError(`--remote-flag needs an app runtime; this checkout is core
|
|
326
|
+
Next: ${USAGE}`);
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
parseRemoteFlagSpec(remoteFlag);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
if (!(error instanceof FeatureFlagSpecError)) throw error;
|
|
332
|
+
throw usageError(`${error.message}
|
|
333
|
+
Next: ${error.userAction}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const remoteFlagArgs = remoteFlag === void 0 ? [] : ["--remote-flag", remoteFlag];
|
|
321
337
|
let mobilePlatform = mobilePlatformOption(optionString(options, "mobilePlatform"));
|
|
322
338
|
const recordPath = path.join(artifactsDir, READINESS_RECORD);
|
|
323
339
|
const chain = stepChain(platform);
|
|
@@ -353,17 +369,18 @@ async function handlePrepare(argv) {
|
|
|
353
369
|
addStep(skippedStep("fixtures", `${executable} fixtures set`, reason));
|
|
354
370
|
} else if (platform === "mobile") {
|
|
355
371
|
const device = mobilePlatform ? [mobilePlatform] : [];
|
|
356
|
-
await run("launch", ["launch", ...device, ...clearMetro ? ["--clear-metro"] : [], "--verify", "--json", ...targetArgs]);
|
|
372
|
+
await run("launch", ["launch", ...device, ...clearMetro ? ["--clear-metro"] : [], ...remoteFlagArgs, "--verify", "--json", ...targetArgs]);
|
|
357
373
|
await run("fixtures", [
|
|
358
374
|
"fixtures",
|
|
359
375
|
"set",
|
|
360
376
|
...mobilePlatform ? ["--platform", mobilePlatform] : [],
|
|
377
|
+
...remoteFlagArgs,
|
|
361
378
|
"--json",
|
|
362
379
|
...targetArgs
|
|
363
380
|
]);
|
|
364
381
|
} else {
|
|
365
|
-
await run("launch", ["launch", "--verify", "--json", ...targetArgs]);
|
|
366
|
-
await run("fixtures", ["fixtures", "set", "--json", ...targetArgs]);
|
|
382
|
+
await run("launch", ["launch", ...remoteFlagArgs, "--verify", "--json", ...targetArgs]);
|
|
383
|
+
await run("fixtures", ["fixtures", "set", ...remoteFlagArgs, "--json", ...targetArgs]);
|
|
367
384
|
}
|
|
368
385
|
await run("verify", ["verify", "--json", "--adapter", platform, ...targetArgs]);
|
|
369
386
|
} finally {
|
|
@@ -1344,7 +1344,16 @@ async function ensureExtensionProofRuntime(target, heal, state, json, deps = {})
|
|
|
1344
1344
|
);
|
|
1345
1345
|
const launch = deps.launch ?? (async (requestedTier) => {
|
|
1346
1346
|
const { launchExtension } = await import("./launch/extension.js");
|
|
1347
|
-
|
|
1347
|
+
const { readExtensionPinnedFlags } = await import("../adapters/extension/feature-flags.js");
|
|
1348
|
+
const { fixtureFeatureFlags, remoteFlagRequestFor } = await import("../feature-flags.js");
|
|
1349
|
+
const { walletFixturePath } = await import("../paths.js");
|
|
1350
|
+
return launchExtension(
|
|
1351
|
+
target,
|
|
1352
|
+
requestedTier,
|
|
1353
|
+
false,
|
|
1354
|
+
"fullscreen",
|
|
1355
|
+
remoteFlagRequestFor(readExtensionPinnedFlags(target).pinned ?? fixtureFeatureFlags(walletFixturePath(target)))
|
|
1356
|
+
);
|
|
1348
1357
|
});
|
|
1349
1358
|
const result = await launch(tier);
|
|
1350
1359
|
const verified = result.status === 0 ? await decide() : initial;
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
targetPath,
|
|
21
21
|
usageError
|
|
22
22
|
} from "./parse-args.js";
|
|
23
|
+
import { FeatureFlagSpecError, parseRemoteFlagSpec } from "../feature-flags.js";
|
|
23
24
|
async function handleRuntimeLaunch({ options }) {
|
|
24
25
|
if (optionFlag(options, "help")) {
|
|
25
26
|
console.log(`mm-harness runtime-launch --adapter <extension|mobile> --target <checkout> [flags]
|
|
@@ -129,7 +130,15 @@ async function handleRuntimeLaunch({ options }) {
|
|
|
129
130
|
if (chromeUserDataDir) command.push("--chrome-user-data-dir", chromeUserDataDir);
|
|
130
131
|
command.push(optionFlag(options, "resetProfile") ? "--reset-profile" : "--preserve-profile");
|
|
131
132
|
const remoteFlag = optionString(options, "remoteFlag");
|
|
132
|
-
if (remoteFlag)
|
|
133
|
+
if (remoteFlag) {
|
|
134
|
+
try {
|
|
135
|
+
command.push("--remote-flags-json", JSON.stringify(parseRemoteFlagSpec(remoteFlag)));
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (!(error instanceof FeatureFlagSpecError)) throw error;
|
|
138
|
+
throw usageError(`${error.message}
|
|
139
|
+
Next: ${error.userAction}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
133
142
|
if (!fs.existsSync(liveScript)) {
|
|
134
143
|
const report2 = runtimeLaunchReport("fail", {
|
|
135
144
|
adapter,
|