@deeeed/metamask-harness 0.58.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 +21 -0
- package/README.md +48 -0
- package/adapters/extension/build-handoff.cjs +4 -0
- package/adapters/extension/inject.mjs +0 -1
- package/adapters/extension/live.sh +26 -25
- package/adapters/extension/pin-remote-flags.cjs +69 -30
- package/adapters/manifest.json +4 -4
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
- package/adapters/mobile/bridge-runtime/setup-wallet.sh +69 -2
- package/adapters/shared/resolve-slot-ports-core.mjs +47 -19
- package/dist/adapters/extension/feature-flags.js +41 -0
- package/dist/adapters/extension/runtime-decision.js +2 -2
- package/dist/adapters/extension/runtime.js +4 -3
- package/dist/adapters/extension/verify.js +608 -0
- package/dist/adapters/mobile/feature-flags.js +127 -0
- package/dist/adapters/resolve-slot-ports.js +2 -0
- package/dist/adapters.js +6 -1
- package/dist/cli.js +1 -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/harness.js +3 -15
- 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/adapters/extension/verify.sh +0 -572
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { runnerDir } from "../../paths.js";
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
6
|
+
function bridgeScriptPath() {
|
|
7
|
+
return process.env.METAMASK_RECIPE_MOBILE_BRIDGE_SCRIPT ?? path.join(runnerDir, "adapters", "mobile", "bridge-runtime", "cdp-bridge.cjs");
|
|
8
|
+
}
|
|
9
|
+
const CONTROLLER_PREAMBLE = `
|
|
10
|
+
var ctx = globalThis.Engine && globalThis.Engine.context;
|
|
11
|
+
var c = ctx && ctx.RemoteFeatureFlagController;
|
|
12
|
+
if (!c) return JSON.stringify({ ok: false, error: 'Engine.context.RemoteFeatureFlagController is not available on the running app.' });
|
|
13
|
+
var snap = function () {
|
|
14
|
+
var st = c.state || {};
|
|
15
|
+
return { effective: st.remoteFeatureFlags || {}, overrides: st.localOverrides || {} };
|
|
16
|
+
};
|
|
17
|
+
`;
|
|
18
|
+
function evalExpression(body) {
|
|
19
|
+
return `(function(){ try {${CONTROLLER_PREAMBLE}${body} } catch (e) { return JSON.stringify({ ok: false, error: e && (e.message || String(e)) }); } })()`;
|
|
20
|
+
}
|
|
21
|
+
function runBridgeEval(target, expression, timeoutMs) {
|
|
22
|
+
const script = bridgeScriptPath();
|
|
23
|
+
if (!fs.existsSync(script)) return Promise.resolve(null);
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
let settled = false;
|
|
26
|
+
let stdout = "";
|
|
27
|
+
const child = spawn(process.execPath, [script, "eval", expression], {
|
|
28
|
+
cwd: target,
|
|
29
|
+
env: { ...process.env, APP_ROOT: target, CDP_TIMEOUT: String(timeoutMs) },
|
|
30
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
31
|
+
});
|
|
32
|
+
const finish = (value) => {
|
|
33
|
+
if (settled) return;
|
|
34
|
+
settled = true;
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
resolve(value);
|
|
37
|
+
};
|
|
38
|
+
const timer = setTimeout(() => {
|
|
39
|
+
child.kill("SIGTERM");
|
|
40
|
+
const killTimer = setTimeout(() => child.kill("SIGKILL"), 500);
|
|
41
|
+
child.once("close", () => clearTimeout(killTimer));
|
|
42
|
+
finish(null);
|
|
43
|
+
}, timeoutMs);
|
|
44
|
+
child.stdout.on("data", (chunk) => {
|
|
45
|
+
stdout += chunk;
|
|
46
|
+
});
|
|
47
|
+
child.on("close", (code) => {
|
|
48
|
+
if (code !== 0) return finish(null);
|
|
49
|
+
const trimmed = stdout.trim();
|
|
50
|
+
if (!trimmed) return finish(null);
|
|
51
|
+
try {
|
|
52
|
+
const first = JSON.parse(trimmed);
|
|
53
|
+
const parsed = typeof first === "string" ? JSON.parse(first) : first;
|
|
54
|
+
return finish(parsed);
|
|
55
|
+
} catch {
|
|
56
|
+
return finish(null);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
child.on("error", () => finish(null));
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async function readMobileFeatureFlags(target, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
63
|
+
const result = await runBridgeEval(
|
|
64
|
+
target,
|
|
65
|
+
evalExpression("var s = snap(); return JSON.stringify({ ok: true, effective: s.effective, overrides: s.overrides });"),
|
|
66
|
+
timeoutMs
|
|
67
|
+
);
|
|
68
|
+
if (!result || result.ok !== true) return null;
|
|
69
|
+
return { effective: result.effective ?? {}, overrides: result.overrides ?? {} };
|
|
70
|
+
}
|
|
71
|
+
async function pinMobileFeatureFlags(target, flags, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
72
|
+
const payload = JSON.stringify(JSON.stringify(flags));
|
|
73
|
+
const result = await runBridgeEval(
|
|
74
|
+
target,
|
|
75
|
+
evalExpression(`
|
|
76
|
+
if (typeof c.setFlagOverride !== 'function') {
|
|
77
|
+
return JSON.stringify({ ok: false, error: 'RemoteFeatureFlagController.setFlagOverride is unavailable in this build.' });
|
|
78
|
+
}
|
|
79
|
+
var requested = JSON.parse(${payload});
|
|
80
|
+
if (typeof c.clearAllFlagOverrides === 'function') c.clearAllFlagOverrides();
|
|
81
|
+
var applied = [];
|
|
82
|
+
for (var k in requested) {
|
|
83
|
+
if (Object.prototype.hasOwnProperty.call(requested, k)) { c.setFlagOverride(k, requested[k]); applied.push(k); }
|
|
84
|
+
}
|
|
85
|
+
var s = snap();
|
|
86
|
+
return JSON.stringify({ ok: true, applied: applied, effective: s.effective, overrides: s.overrides });
|
|
87
|
+
`),
|
|
88
|
+
timeoutMs
|
|
89
|
+
);
|
|
90
|
+
if (!result) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
error: "the mobile bridge did not answer, so no remote feature flag was applied.",
|
|
94
|
+
applied: [],
|
|
95
|
+
effective: {},
|
|
96
|
+
overrides: {},
|
|
97
|
+
mismatched: Object.keys(flags)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (result.ok !== true) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
error: result.error ?? "the mobile runtime refused the remote feature-flag override.",
|
|
104
|
+
applied: [],
|
|
105
|
+
effective: {},
|
|
106
|
+
overrides: {},
|
|
107
|
+
mismatched: Object.keys(flags)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const effective = result.effective ?? {};
|
|
111
|
+
const overrides = result.overrides ?? {};
|
|
112
|
+
const mismatched = [
|
|
113
|
+
...Object.keys(flags).filter((key) => JSON.stringify(effective[key]) !== JSON.stringify(flags[key])),
|
|
114
|
+
...Object.keys(overrides).filter((key) => !(key in flags))
|
|
115
|
+
];
|
|
116
|
+
return {
|
|
117
|
+
ok: mismatched.length === 0,
|
|
118
|
+
applied: result.applied ?? [],
|
|
119
|
+
effective,
|
|
120
|
+
overrides,
|
|
121
|
+
mismatched
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
export {
|
|
125
|
+
pinMobileFeatureFlags,
|
|
126
|
+
readMobileFeatureFlags
|
|
127
|
+
};
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
formatExtensionIdentityPorts,
|
|
4
4
|
formatExtensionSlotPorts,
|
|
5
5
|
inferSlotSuffix,
|
|
6
|
+
poolFilesForHost,
|
|
6
7
|
realRepoPath,
|
|
7
8
|
resolveDefaultExtensionPorts,
|
|
8
9
|
resolveExtensionRuntimeContext,
|
|
@@ -17,6 +18,7 @@ export {
|
|
|
17
18
|
formatExtensionSlotPorts,
|
|
18
19
|
formatKvLines,
|
|
19
20
|
inferSlotSuffix,
|
|
21
|
+
poolFilesForHost,
|
|
20
22
|
realRepoPath,
|
|
21
23
|
resolveDefaultExtensionPorts,
|
|
22
24
|
resolveExtensionRuntimeContext,
|
package/dist/adapters.js
CHANGED
|
@@ -105,6 +105,11 @@ const LIVE_ONLY_WALLET_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
105
105
|
"metamask.wallet.list_accounts",
|
|
106
106
|
"metamask.wallet.read_state"
|
|
107
107
|
]);
|
|
108
|
+
const LIVE_ONLY_FEATURE_FLAG_ACTIONS = /* @__PURE__ */ new Set([
|
|
109
|
+
"metamask.feature_flags.set",
|
|
110
|
+
"metamask.feature_flags.read",
|
|
111
|
+
"metamask.feature_flags.clear"
|
|
112
|
+
]);
|
|
108
113
|
const LIVE_ONLY_APP_ACTIONS = /* @__PURE__ */ new Set([
|
|
109
114
|
"ui.navigate",
|
|
110
115
|
"app.network_capture",
|
|
@@ -113,7 +118,7 @@ const LIVE_ONLY_APP_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
113
118
|
"app.performance_assert"
|
|
114
119
|
]);
|
|
115
120
|
function requiresLiveAdapter(platform, action) {
|
|
116
|
-
return LIVE_ONLY_ACTIONS.has(action) || platform === "core" && CORE_ONLY_PERPS_ACTIONS.has(action) || LIVE_ONLY_WALLET_ACTIONS.has(action) || LIVE_ONLY_APP_ACTIONS.has(action);
|
|
121
|
+
return LIVE_ONLY_ACTIONS.has(action) || platform === "core" && CORE_ONLY_PERPS_ACTIONS.has(action) || LIVE_ONLY_WALLET_ACTIONS.has(action) || LIVE_ONLY_FEATURE_FLAG_ACTIONS.has(action) || LIVE_ONLY_APP_ACTIONS.has(action);
|
|
117
122
|
}
|
|
118
123
|
function liveRuntimeConfigured(platform, node) {
|
|
119
124
|
if (platform === "mobile" || platform === "core") return true;
|
package/dist/cli.js
CHANGED
|
@@ -103,7 +103,7 @@ async function main(argv) {
|
|
|
103
103
|
if (command === "completion-candidates") return handleCompletionCandidates(argv.slice(1));
|
|
104
104
|
if (command === "provision") return handleProvision(parseArgs(argv.slice(1), command));
|
|
105
105
|
if (OVERLAY_COMMANDS.includes(command)) {
|
|
106
|
-
if (command === "verify") {
|
|
106
|
+
if (command === "verify" && process.env.MM_HARNESS_SKIP_OVERLAY_ENSURE !== "1") {
|
|
107
107
|
try {
|
|
108
108
|
const { options } = parseArgs(argv.slice(1), command);
|
|
109
109
|
const target = targetPath(options);
|
package/dist/command-contract.js
CHANGED
|
@@ -154,7 +154,8 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
154
154
|
"--platform": value(["mobile", "extension", "core"]),
|
|
155
155
|
"--mobile-platform": value(["ios", "android"]),
|
|
156
156
|
"--clear-metro": bool(),
|
|
157
|
-
"--artifacts-dir": value()
|
|
157
|
+
"--artifacts-dir": value(),
|
|
158
|
+
"--remote-flag": value()
|
|
158
159
|
})
|
|
159
160
|
},
|
|
160
161
|
task: {
|
|
@@ -329,7 +330,8 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
329
330
|
"--sidepanel": bool(),
|
|
330
331
|
"--fullscreen": bool(),
|
|
331
332
|
"--url": value(),
|
|
332
|
-
"--heal": value(["off", "infra-only", "auto"])
|
|
333
|
+
"--heal": value(["off", "infra-only", "auto"]),
|
|
334
|
+
"--remote-flag": value()
|
|
333
335
|
}),
|
|
334
336
|
positionals: [{ label: "platform", choices: ["ios", "android"] }]
|
|
335
337
|
},
|
|
@@ -383,7 +385,8 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
383
385
|
"--cdp-port": value(),
|
|
384
386
|
"--extension-dir": value(),
|
|
385
387
|
"--extension-id-file": value(),
|
|
386
|
-
"--action-manifest": value()
|
|
388
|
+
"--action-manifest": value(),
|
|
389
|
+
"--remote-flag": value()
|
|
387
390
|
}),
|
|
388
391
|
positionals: [{ label: "action", choices: ["init", "sync", "set", "reset", "generate", "finalize"] }],
|
|
389
392
|
minimumPositionals: 0
|
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)`));
|