@deeeed/metamask-harness 0.2.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 +161 -0
- package/README.md +140 -0
- package/bin/mm-harness +99 -0
- package/docs/CHEATSHEET.md +61 -0
- package/docs/CLI-SPEC.md +915 -0
- package/docs/MENTAL-MODEL.md +295 -0
- package/docs/architecture.md +367 -0
- package/docs/extension-runtime-commands.md +60 -0
- package/docs/harness-cli.md +43 -0
- package/docs/live-adapter-contract.md +188 -0
- package/docs/package-boundaries.md +47 -0
- package/docs/perps-flow-catalog.md +235 -0
- package/docs/recipe-libraries.md +95 -0
- package/docs/runtime-file-conventions.md +36 -0
- package/library/actions/core/perps/_controller.mjs +727 -0
- package/library/actions/core/perps/assert_orders.mjs +53 -0
- package/library/actions/core/perps/assert_positions.mjs +52 -0
- package/library/actions/core/perps/close_orders.mjs +97 -0
- package/library/actions/core/perps/close_positions.mjs +118 -0
- package/library/actions/core/perps/ensure_orders.mjs +40 -0
- package/library/actions/core/perps/ensure_positions.mjs +37 -0
- package/library/actions/core/perps/place_order.mjs +201 -0
- package/library/actions/core/perps/read_account.mjs +30 -0
- package/library/actions/core/perps/read_orders.mjs +27 -0
- package/library/actions/core/perps/read_positions.mjs +27 -0
- package/library/actions/core/perps/start_state.mjs +92 -0
- package/library/actions/core/perps/teardown_state.mjs +86 -0
- package/library/actions/extension/perps/assert_orders.mjs +11 -0
- package/library/actions/extension/perps/assert_positions.mjs +11 -0
- package/library/actions/extension/perps/close_orders.mjs +8 -0
- package/library/actions/extension/perps/close_positions.mjs +8 -0
- package/library/actions/extension/perps/ensure_orders.mjs +4 -0
- package/library/actions/extension/perps/ensure_positions.mjs +4 -0
- package/library/actions/extension/perps/perps.mjs +730 -0
- package/library/actions/extension/perps/place_order.mjs +7 -0
- package/library/actions/extension/perps/read_orders.mjs +4 -0
- package/library/actions/extension/perps/read_positions.mjs +3 -0
- package/library/actions/extension/platform/cdp.mjs +541 -0
- package/library/actions/extension/ui/navigate.mjs +44 -0
- package/library/actions/extension/wallet/ensure_unlocked.mjs +36 -0
- package/library/actions/extension/wallet/read_state.mjs +27 -0
- package/library/actions/extension/wallet/select_account.mjs +48 -0
- package/library/actions/extension/wallet/setup.mjs +35 -0
- package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
- package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
- package/library/actions/mobile/bridge-runtime/cdp-bridge.cjs +686 -0
- package/library/actions/mobile/bridge-runtime/lib/cdp-eval.cjs +110 -0
- package/library/actions/mobile/bridge-runtime/lib/config.cjs +39 -0
- package/library/actions/mobile/bridge-runtime/lib/issue-capture.cjs +446 -0
- package/library/actions/mobile/bridge-runtime/lib/target-discovery.cjs +204 -0
- package/library/actions/mobile/bridge-runtime/lib/ws-client.cjs +108 -0
- package/library/actions/mobile/bridge-runtime/setup-wallet.sh +442 -0
- package/library/actions/mobile/perps/assert_orders.mjs +11 -0
- package/library/actions/mobile/perps/assert_positions.mjs +11 -0
- package/library/actions/mobile/perps/close_orders.mjs +8 -0
- package/library/actions/mobile/perps/close_positions.mjs +8 -0
- package/library/actions/mobile/perps/ensure_orders.mjs +4 -0
- package/library/actions/mobile/perps/ensure_positions.mjs +4 -0
- package/library/actions/mobile/perps/perps.mjs +709 -0
- package/library/actions/mobile/perps/place_order.mjs +7 -0
- package/library/actions/mobile/perps/read_orders.mjs +4 -0
- package/library/actions/mobile/perps/read_positions.mjs +3 -0
- package/library/actions/mobile/platform/bridge.mjs +283 -0
- package/library/actions/mobile/ui/navigate.mjs +38 -0
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +107 -0
- package/library/actions/mobile/wallet/home.mjs +35 -0
- package/library/actions/mobile/wallet/read_state.mjs +40 -0
- package/library/actions/mobile/wallet/select_account.mjs +48 -0
- package/library/actions/mobile/wallet/setup.mjs +220 -0
- package/library/flows/perps.flows.json +64 -0
- package/library/library.json +7 -0
- package/library/manifests/core.action-manifest.json +1282 -0
- package/library/manifests/extension.action-manifest.json +1749 -0
- package/library/manifests/mobile.action-manifest.json +1753 -0
- package/library/recipes/action-validation.extension.recipe.json +417 -0
- package/library/recipes/action-validation.mobile.recipe.json +422 -0
- package/library/recipes/order-lifecycle.core.recipe.json +78 -0
- package/library/recipes/perps-lifecycle.recipe.json +194 -0
- package/library/recipes/read-markets.core.recipe.json +38 -0
- package/library/recipes/smoke.extension.recipe.json +31 -0
- package/library/recipes/smoke.mobile.recipe.json +31 -0
- package/library/recipes/trading-lifecycle.core.recipe.json +76 -0
- package/orchestration/compat-overlays/README.md +19 -0
- package/orchestration/compat-overlays/mobile/README.md +13 -0
- package/orchestration/compat-overlays/mobile/rn81-message-event-source.patch +42 -0
- package/orchestration/core/cleanup.sh +37 -0
- package/orchestration/core/inject.sh +154 -0
- package/orchestration/doctor.mjs +72 -0
- package/orchestration/extension/cleanup.mjs +60 -0
- package/orchestration/extension/console-tail.mjs +228 -0
- package/orchestration/extension/ensure-browser.sh +416 -0
- package/orchestration/extension/ensure-ready.ts +185 -0
- package/orchestration/extension/extension-id.ts +107 -0
- package/orchestration/extension/inject.mjs +266 -0
- package/orchestration/extension/launch-browser.cjs +216 -0
- package/orchestration/extension/launch.sh +175 -0
- package/orchestration/extension/live.sh +320 -0
- package/orchestration/extension/pin-remote-flags.cjs +45 -0
- package/orchestration/extension/readiness.mjs +414 -0
- package/orchestration/extension/refresh-build.sh +190 -0
- package/orchestration/extension/runtime-decision.ts +445 -0
- package/orchestration/extension/runtime.ts +407 -0
- package/orchestration/extension/seed-fixture.sh +177 -0
- package/orchestration/extension/sidepanel-toggle.sh +291 -0
- package/orchestration/extension/snapshot-dist.sh +84 -0
- package/orchestration/extension/start-watch.sh +339 -0
- package/orchestration/extension/wallet-fixture-state.cjs +1086 -0
- package/orchestration/lib/activate-repo-node.sh +144 -0
- package/orchestration/lib/cli-color.mjs +84 -0
- package/orchestration/lib/cli-commands.mjs +243 -0
- package/orchestration/lib/cli-home.mjs +354 -0
- package/orchestration/lib/cli-ux.sh +252 -0
- package/orchestration/lib/cli-version.mjs +123 -0
- package/orchestration/lib/ensure-runner-deps.sh +56 -0
- package/orchestration/lib/harness-path.sh +55 -0
- package/orchestration/lib/hash-helpers.sh +44 -0
- package/orchestration/lib/json-field.sh +23 -0
- package/orchestration/lib/log-tui.mjs +304 -0
- package/orchestration/lib/open-debug.mjs +317 -0
- package/orchestration/lib/path-defaults.json +4 -0
- package/orchestration/lib/progress.mjs +107 -0
- package/orchestration/lib/recipe-paths.mjs +26 -0
- package/orchestration/lib/resolve-farmslot-ports.sh +144 -0
- package/orchestration/manifest.json +358 -0
- package/orchestration/mobile/cleanup.sh +192 -0
- package/orchestration/mobile/deps-markers.ts +21 -0
- package/orchestration/mobile/inject.sh +681 -0
- package/orchestration/mobile/launch.sh +137 -0
- package/orchestration/mobile/live.sh +125 -0
- package/orchestration/mobile/runtime-decision.ts +292 -0
- package/orchestration/porcelain/metamask-recipe +99 -0
- package/orchestration/porcelain/mm-recipe +1591 -0
- package/orchestration/porcelain/mme-recipe +1181 -0
- package/package.json +59 -0
- package/runner/extension/verify.sh +511 -0
- package/runner/mobile/verify.sh +501 -0
- package/runner/src/adapters.ts +601 -0
- package/runner/src/cli.ts +1820 -0
- package/runner/src/commands/debug.ts +44 -0
- package/runner/src/commands/fixtures.ts +99 -0
- package/runner/src/commands/launch.ts +397 -0
- package/runner/src/commands/logs.ts +60 -0
- package/runner/src/commands/shared.ts +138 -0
- package/runner/src/completions-cache.ts +86 -0
- package/runner/src/doctor.ts +203 -0
- package/runner/src/harness.ts +516 -0
- package/runner/src/heal-bounds.ts +179 -0
- package/runner/src/index.ts +6 -0
- package/runner/src/live-adapter-contract.ts +274 -0
- package/runner/src/manifest.ts +47 -0
- package/runner/src/mm-harness-cli.ts +488 -0
- package/runner/src/paths.ts +198 -0
- package/runner/src/recording-target.ts +147 -0
- package/runner/src/run-recording.ts +329 -0
- package/runner/src/runner.ts +108 -0
- package/runner/src/types.ts +57 -0
- package/scripts/completions.sh +125 -0
- package/scripts/install-completions.sh +62 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
// runtime.ts — extension runtime prepare/health over CDP.
|
|
2
|
+
// App/instance control.
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { importRecipeHarnessRuntimeBrowserExtension, importRecipeHarnessRuntimeCdp, recipeRuntimeDir, resolveLocalProtocolRoot, resolveRequiredLocalProtocolRoot, runnerDir } from '../../runner/src/paths.ts';
|
|
8
|
+
|
|
9
|
+
// Runtime health uses package dependencies. Launching a host-managed validation
|
|
10
|
+
// browser remains a dev-only path because it needs pool/project scripts from a
|
|
11
|
+
// checkout.
|
|
12
|
+
const { CdpSession, jsonGet, sleep } = await importRecipeHarnessRuntimeCdp();
|
|
13
|
+
const { extensionIdFromTarget } = await importRecipeHarnessRuntimeBrowserExtension();
|
|
14
|
+
|
|
15
|
+
export interface ExtensionRuntimeOptions {
|
|
16
|
+
projectRoot: string;
|
|
17
|
+
cdpPort?: string | number;
|
|
18
|
+
slot?: string;
|
|
19
|
+
launchExistingDist?: boolean;
|
|
20
|
+
validationRuntimeDir?: string;
|
|
21
|
+
healthTimeoutMs?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ExtensionRuntimeLaunchResult {
|
|
25
|
+
launched: boolean;
|
|
26
|
+
slotId?: string;
|
|
27
|
+
cdpPort: number;
|
|
28
|
+
runtimeDir?: string;
|
|
29
|
+
stdout?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ExtensionRuntimeHealthReport {
|
|
33
|
+
status: 'PASS' | 'FAIL';
|
|
34
|
+
cdpPort: number;
|
|
35
|
+
targetUrl?: string;
|
|
36
|
+
extensionId?: string;
|
|
37
|
+
extensionPageTargets: number;
|
|
38
|
+
findings: string[];
|
|
39
|
+
details: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface CdpTarget {
|
|
43
|
+
id?: string;
|
|
44
|
+
type?: string;
|
|
45
|
+
title?: string;
|
|
46
|
+
url?: string;
|
|
47
|
+
webSocketDebuggerUrl?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function prepareExtensionRuntime(
|
|
51
|
+
options: ExtensionRuntimeOptions,
|
|
52
|
+
): Promise<{ launch: ExtensionRuntimeLaunchResult | null; health: ExtensionRuntimeHealthReport }> {
|
|
53
|
+
const projectRoot = path.resolve(options.projectRoot);
|
|
54
|
+
const slot = resolveExtensionSlot(projectRoot, options.slot);
|
|
55
|
+
const cdpPort = resolveCdpPort(options.cdpPort, slot);
|
|
56
|
+
let launch: ExtensionRuntimeLaunchResult | null = null;
|
|
57
|
+
if (options.launchExistingDist) {
|
|
58
|
+
if (!slot) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Cannot launch host-managed Extension runtime for ${projectRoot}: no configured slot maps to this repo. ` +
|
|
61
|
+
'Pass --slot <slot-id> or add the checkout to pool/*.json.',
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
launch = await launchHostValidationBrowser({
|
|
65
|
+
projectRoot,
|
|
66
|
+
cdpPort,
|
|
67
|
+
slotId: slot.id,
|
|
68
|
+
validationRuntimeDir:
|
|
69
|
+
options.validationRuntimeDir ?? `${recipeRuntimeDir()}/validation-${cdpPort}`,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const health = await assertHealthyExtensionRuntime({
|
|
73
|
+
projectRoot,
|
|
74
|
+
cdpPort,
|
|
75
|
+
timeoutMs: options.healthTimeoutMs,
|
|
76
|
+
});
|
|
77
|
+
return { launch, health };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function assertHealthyExtensionRuntime(options: {
|
|
81
|
+
projectRoot: string;
|
|
82
|
+
cdpPort: number;
|
|
83
|
+
timeoutMs?: number;
|
|
84
|
+
}): Promise<ExtensionRuntimeHealthReport> {
|
|
85
|
+
const timeoutMs = options.timeoutMs ?? 30_000;
|
|
86
|
+
const deadline = Date.now() + timeoutMs;
|
|
87
|
+
let lastReport: ExtensionRuntimeHealthReport | null = null;
|
|
88
|
+
while (Date.now() <= deadline) {
|
|
89
|
+
lastReport = await checkExtensionRuntimeHealth(options.projectRoot, options.cdpPort);
|
|
90
|
+
if (lastReport.status === 'PASS') return lastReport;
|
|
91
|
+
await sleep(500);
|
|
92
|
+
}
|
|
93
|
+
const report = lastReport ?? failReport(options.cdpPort, ['CDP runtime was not probed.'], {});
|
|
94
|
+
throw new Error(formatHealthFailure(report, options.projectRoot));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function checkExtensionRuntimeHealth(
|
|
98
|
+
projectRoot: string,
|
|
99
|
+
cdpPort: number,
|
|
100
|
+
): Promise<ExtensionRuntimeHealthReport> {
|
|
101
|
+
const findings: string[] = [];
|
|
102
|
+
let targets: CdpTarget[] = [];
|
|
103
|
+
try {
|
|
104
|
+
await jsonGet(`http://127.0.0.1:${cdpPort}/json/version`);
|
|
105
|
+
const rawTargets = await jsonGet(`http://127.0.0.1:${cdpPort}/json/list`);
|
|
106
|
+
targets = Array.isArray(rawTargets) ? rawTargets as CdpTarget[] : [];
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return failReport(cdpPort, [`CDP is not reachable on port ${cdpPort}: ${messageOf(error)}`], {});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const extensionTargets = targets.filter((target) =>
|
|
112
|
+
target.type === 'page' &&
|
|
113
|
+
String(target.url ?? '').startsWith('chrome-extension://') &&
|
|
114
|
+
String(target.url ?? '').includes('/home.html') &&
|
|
115
|
+
Boolean(target.webSocketDebuggerUrl),
|
|
116
|
+
);
|
|
117
|
+
if (extensionTargets.length !== 1) {
|
|
118
|
+
findings.push(`Expected exactly one MetaMask extension home page target, found ${extensionTargets.length}.`);
|
|
119
|
+
}
|
|
120
|
+
const target = extensionTargets[0];
|
|
121
|
+
if (!target?.webSocketDebuggerUrl) {
|
|
122
|
+
return failReport(cdpPort, findings, {
|
|
123
|
+
targetUrls: targets.map((entry) => entry.url).filter(Boolean),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (String(target.url ?? '').startsWith('chrome-error://')) {
|
|
127
|
+
findings.push(`Extension target resolved to chrome-error page: ${target.url}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const session = await CdpSession.connect(target.webSocketDebuggerUrl);
|
|
131
|
+
try {
|
|
132
|
+
await session.call('Runtime.enable');
|
|
133
|
+
await session.call('Page.enable');
|
|
134
|
+
const runtime = await evaluateHealth(session);
|
|
135
|
+
if (runtime.href && !String(runtime.href).startsWith('chrome-extension://')) {
|
|
136
|
+
findings.push(`Extension page href is not an extension URL: ${runtime.href}`);
|
|
137
|
+
}
|
|
138
|
+
if (runtime.backgroundUnresponsive === true) {
|
|
139
|
+
findings.push('Extension UI reports background connection unresponsive.');
|
|
140
|
+
}
|
|
141
|
+
if (runtime.hasSubmitRequest !== true) {
|
|
142
|
+
findings.push('stateHooks.submitRequestToBackground is unavailable.');
|
|
143
|
+
}
|
|
144
|
+
if (runtime.hasStore !== true) {
|
|
145
|
+
findings.push('stateHooks.store is unavailable.');
|
|
146
|
+
}
|
|
147
|
+
if (runtime.hasPerpsStreamManager !== true) {
|
|
148
|
+
findings.push('stateHooks.getPerpsStreamManager is unavailable.');
|
|
149
|
+
}
|
|
150
|
+
if (runtime.backgroundProbeOk !== true) {
|
|
151
|
+
findings.push(`Perps background read probe failed: ${runtime.backgroundProbeError ?? 'unknown error'}.`);
|
|
152
|
+
}
|
|
153
|
+
const extensionId = safeExtensionId(target);
|
|
154
|
+
return {
|
|
155
|
+
status: findings.length === 0 ? 'PASS' : 'FAIL',
|
|
156
|
+
cdpPort,
|
|
157
|
+
targetUrl: target.url,
|
|
158
|
+
extensionId,
|
|
159
|
+
extensionPageTargets: extensionTargets.length,
|
|
160
|
+
findings,
|
|
161
|
+
details: {
|
|
162
|
+
projectRoot,
|
|
163
|
+
targetUrls: targets.map((entry) => entry.url).filter(Boolean),
|
|
164
|
+
runtime,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
} finally {
|
|
168
|
+
session.close();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function failReport(
|
|
173
|
+
cdpPort: number,
|
|
174
|
+
findings: string[],
|
|
175
|
+
details: Record<string, unknown>,
|
|
176
|
+
): ExtensionRuntimeHealthReport {
|
|
177
|
+
return {
|
|
178
|
+
status: 'FAIL',
|
|
179
|
+
cdpPort,
|
|
180
|
+
extensionPageTargets: 0,
|
|
181
|
+
findings,
|
|
182
|
+
details,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function evaluateHealth(session: any): Promise<Record<string, unknown>> {
|
|
187
|
+
const result = await session.call('Runtime.evaluate', {
|
|
188
|
+
expression: `(() => {
|
|
189
|
+
const hooks = globalThis.stateHooks || {};
|
|
190
|
+
const bodyText = document.body?.innerText || '';
|
|
191
|
+
const storeState = hooks.store?.getState?.() || {};
|
|
192
|
+
const manager = hooks.getPerpsStreamManager?.();
|
|
193
|
+
const accountCache = manager?.account?.cache;
|
|
194
|
+
return Promise.race([
|
|
195
|
+
(async () => {
|
|
196
|
+
let backgroundProbeOk = false;
|
|
197
|
+
let backgroundProbeError = null;
|
|
198
|
+
if (typeof hooks.submitRequestToBackground === 'function') {
|
|
199
|
+
try {
|
|
200
|
+
const accountState = await hooks.submitRequestToBackground('perpsGetAccountState', []);
|
|
201
|
+
backgroundProbeOk = Boolean(accountState && typeof accountState === 'object');
|
|
202
|
+
if (!backgroundProbeOk) backgroundProbeError = 'perpsGetAccountState returned an empty result';
|
|
203
|
+
} catch (error) {
|
|
204
|
+
backgroundProbeError = String(error?.message || error);
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
backgroundProbeError = 'submitRequestToBackground is not a function';
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
href: location.href,
|
|
211
|
+
title: document.title,
|
|
212
|
+
hookKeys: Object.keys(hooks),
|
|
213
|
+
hasSubmitRequest: typeof hooks.submitRequestToBackground === 'function',
|
|
214
|
+
hasStore: Boolean(hooks.store),
|
|
215
|
+
hasPerpsStreamManager: typeof hooks.getPerpsStreamManager === 'function',
|
|
216
|
+
backgroundUnresponsive: bodyText.includes('Background connection unresponsive') || bodyText.includes('MetaMask had trouble starting'),
|
|
217
|
+
activeProvider: storeState.metamask?.activeProvider || null,
|
|
218
|
+
isTestnet: Boolean(storeState.metamask?.isTestnet),
|
|
219
|
+
perpsManagerInitialized: Boolean(manager?.isInitialized?.()),
|
|
220
|
+
positionsCacheIsArray: Array.isArray(manager?.positions?.cache),
|
|
221
|
+
ordersCacheIsArray: Array.isArray(manager?.orders?.cache),
|
|
222
|
+
accountCachePresent: Boolean(accountCache && typeof accountCache === 'object'),
|
|
223
|
+
backgroundProbeOk,
|
|
224
|
+
backgroundProbeError,
|
|
225
|
+
};
|
|
226
|
+
})(),
|
|
227
|
+
new Promise((resolve) => setTimeout(() => resolve({
|
|
228
|
+
href: location.href,
|
|
229
|
+
title: document.title,
|
|
230
|
+
hookKeys: Object.keys(hooks),
|
|
231
|
+
hasSubmitRequest: typeof hooks.submitRequestToBackground === 'function',
|
|
232
|
+
hasStore: Boolean(hooks.store),
|
|
233
|
+
hasPerpsStreamManager: typeof hooks.getPerpsStreamManager === 'function',
|
|
234
|
+
backgroundUnresponsive: bodyText.includes('Background connection unresponsive') || bodyText.includes('MetaMask had trouble starting'),
|
|
235
|
+
backgroundProbeOk: false,
|
|
236
|
+
backgroundProbeError: 'perpsGetAccountState timed out after 5000ms',
|
|
237
|
+
}), 5000)),
|
|
238
|
+
]);
|
|
239
|
+
})()`,
|
|
240
|
+
awaitPromise: true,
|
|
241
|
+
returnByValue: true,
|
|
242
|
+
});
|
|
243
|
+
if (result.exceptionDetails) {
|
|
244
|
+
throw new Error(
|
|
245
|
+
result.exceptionDetails.exception?.description ??
|
|
246
|
+
result.exceptionDetails.text ??
|
|
247
|
+
'Extension runtime health evaluation failed.',
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return result.result?.value ?? {};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function formatHealthFailure(report: ExtensionRuntimeHealthReport, projectRoot: string): string {
|
|
254
|
+
return [
|
|
255
|
+
`Extension runtime health check failed for ${projectRoot} on CDP port ${report.cdpPort}.`,
|
|
256
|
+
...report.findings.map((finding) => `- ${finding}`),
|
|
257
|
+
'Use the host-managed validation launcher, for example:',
|
|
258
|
+
` mm-harness run <recipe.json> --adapter extension --target ${projectRoot} --slot <slot-id> --cdp-port ${report.cdpPort} --launch-existing-dist --artifacts-dir <artifacts-dir>`,
|
|
259
|
+
`Health details: ${JSON.stringify(report.details)}`,
|
|
260
|
+
].join('\n');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function resolveCdpPort(rawPort: string | number | undefined, slot: ResolvedSlot | null): number {
|
|
264
|
+
const raw = rawPort ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT ?? slot?.cdpPort;
|
|
265
|
+
const port = Number(raw);
|
|
266
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
267
|
+
throw new Error('Extension runtime requires --cdp-port, CDP_PORT, RECIPE_CDP_PORT, or a configured slot with resources.browser.cdp_port.');
|
|
268
|
+
}
|
|
269
|
+
return port;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
interface ResolvedSlot {
|
|
273
|
+
id: string;
|
|
274
|
+
repo: string;
|
|
275
|
+
cdpPort?: number;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function resolveExtensionSlot(projectRoot: string, requestedSlot?: string): ResolvedSlot | null {
|
|
279
|
+
const protocolRoot = resolveLocalProtocolRoot();
|
|
280
|
+
if (!protocolRoot) {
|
|
281
|
+
if (requestedSlot) throw new Error('Extension slot lookup requires a local host checkout when --slot is provided.');
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
const poolsDir = path.join(protocolRoot, 'pool');
|
|
285
|
+
if (!fs.existsSync(poolsDir)) return null;
|
|
286
|
+
for (const file of fs.readdirSync(poolsDir)) {
|
|
287
|
+
// Only canonical pool files: skip backups (`*.json.bak.*`) and any non-.json,
|
|
288
|
+
// so a stale backup can't resolve a slot the live pool no longer defines.
|
|
289
|
+
if (!file.endsWith('.json') || file.includes('.bak')) continue;
|
|
290
|
+
const pool = JSON.parse(fs.readFileSync(path.join(poolsDir, file), 'utf8')) as { slots?: unknown[] };
|
|
291
|
+
for (const rawSlot of pool.slots ?? []) {
|
|
292
|
+
if (!rawSlot || typeof rawSlot !== 'object') continue;
|
|
293
|
+
const slot = rawSlot as Record<string, any>;
|
|
294
|
+
if (requestedSlot && slot.id !== requestedSlot) continue;
|
|
295
|
+
if (!requestedSlot) {
|
|
296
|
+
// Auto-match by repo requires an ABSOLUTE repo path. A relative entry —
|
|
297
|
+
// e.g. demo pools' "repo": "." — resolves to the runner's CWD and would
|
|
298
|
+
// spuriously shadow the real slot for whatever repo it runs in.
|
|
299
|
+
const repo = String(slot.repo ?? '');
|
|
300
|
+
if (!path.isAbsolute(repo) || path.resolve(repo) !== projectRoot) continue;
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
id: String(slot.id),
|
|
304
|
+
repo: path.resolve(String(slot.repo)),
|
|
305
|
+
cdpPort: Number(slot.resources?.browser?.cdp_port || slot.resources?.browser?.cdpPort || undefined) || undefined,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function launchHostValidationBrowser(options: {
|
|
313
|
+
projectRoot: string;
|
|
314
|
+
cdpPort: number;
|
|
315
|
+
slotId: string;
|
|
316
|
+
validationRuntimeDir: string;
|
|
317
|
+
}): Promise<ExtensionRuntimeLaunchResult> {
|
|
318
|
+
const protocolRoot = resolveRequiredLocalProtocolRoot('Extension validation browser launch');
|
|
319
|
+
const script = path.join(protocolRoot, 'projects/metamask-extension-farm/setup/launch-validation-browser.sh');
|
|
320
|
+
if (!fs.existsSync(script)) {
|
|
321
|
+
throw new Error(`Extension validation launcher not found: ${script}`);
|
|
322
|
+
}
|
|
323
|
+
await killCdpPort(options.cdpPort);
|
|
324
|
+
const result = await runProcess('bash', [
|
|
325
|
+
script,
|
|
326
|
+
options.slotId,
|
|
327
|
+
'--cdp-port',
|
|
328
|
+
String(options.cdpPort),
|
|
329
|
+
'--runtime-dir',
|
|
330
|
+
options.validationRuntimeDir,
|
|
331
|
+
'--no-landing',
|
|
332
|
+
], {
|
|
333
|
+
cwd: protocolRoot,
|
|
334
|
+
env: process.env,
|
|
335
|
+
timeoutMs: 180_000,
|
|
336
|
+
});
|
|
337
|
+
if (result.timedOut) {
|
|
338
|
+
throw new Error(`Extension validation launcher timed out after 180000ms.\n${result.stdout}\n${result.stderr}`);
|
|
339
|
+
}
|
|
340
|
+
if (result.exitCode !== 0) {
|
|
341
|
+
throw new Error(`Extension validation launcher failed with exit ${result.exitCode}.\n${result.stdout}\n${result.stderr}`);
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
launched: true,
|
|
345
|
+
slotId: options.slotId,
|
|
346
|
+
cdpPort: options.cdpPort,
|
|
347
|
+
runtimeDir: options.validationRuntimeDir,
|
|
348
|
+
stdout: result.stdout,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function runProcess(command: string, args: string[], options: { cwd: string; env: NodeJS.ProcessEnv; timeoutMs?: number }): Promise<{ exitCode: number | null; stdout: string; stderr: string; timedOut: boolean }> {
|
|
353
|
+
return new Promise((resolve, reject) => {
|
|
354
|
+
const child = spawn(command, args, { cwd: options.cwd, env: options.env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
355
|
+
let stdout = '';
|
|
356
|
+
let stderr = '';
|
|
357
|
+
let settled = false;
|
|
358
|
+
const timeout = options.timeoutMs
|
|
359
|
+
? setTimeout(() => {
|
|
360
|
+
if (settled) return;
|
|
361
|
+
settled = true;
|
|
362
|
+
child.kill('SIGTERM');
|
|
363
|
+
setTimeout(() => {
|
|
364
|
+
if (!child.killed) child.kill('SIGKILL');
|
|
365
|
+
}, 1000);
|
|
366
|
+
resolve({ exitCode: null, stdout, stderr, timedOut: true });
|
|
367
|
+
}, options.timeoutMs)
|
|
368
|
+
: undefined;
|
|
369
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
370
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
371
|
+
child.on('error', (error) => {
|
|
372
|
+
if (settled) return;
|
|
373
|
+
settled = true;
|
|
374
|
+
if (timeout) clearTimeout(timeout);
|
|
375
|
+
reject(error);
|
|
376
|
+
});
|
|
377
|
+
child.on('close', (exitCode) => {
|
|
378
|
+
if (settled) return;
|
|
379
|
+
settled = true;
|
|
380
|
+
if (timeout) clearTimeout(timeout);
|
|
381
|
+
resolve({ exitCode, stdout, stderr, timedOut: false });
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function killCdpPort(cdpPort: number): Promise<void> {
|
|
387
|
+
if (process.platform !== 'darwin' && process.platform !== 'linux') return;
|
|
388
|
+
await runProcess('bash', ['-lc', `lsof -ti tcp:${cdpPort} -sTCP:LISTEN | xargs -r kill -TERM || true; sleep 1; lsof -ti tcp:${cdpPort} -sTCP:LISTEN | xargs -r kill -KILL || true`], {
|
|
389
|
+
cwd: runnerDir,
|
|
390
|
+
env: process.env,
|
|
391
|
+
timeoutMs: 10_000,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function safeExtensionId(target: CdpTarget): string | undefined {
|
|
396
|
+
try {
|
|
397
|
+
return extensionIdFromTarget(target);
|
|
398
|
+
} catch (_error) {
|
|
399
|
+
// Non-extension CDP targets are expected while scanning all browser tabs.
|
|
400
|
+
// Treat them as unmatched targets instead of failing runtime discovery.
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function messageOf(error: unknown): string {
|
|
406
|
+
return error instanceof Error ? error.message : String(error);
|
|
407
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# seed-fixture.sh — wallet fixture resolution chain + generate/prefill/seed
|
|
3
|
+
# prepare_parts mega-string in scripts/extension/live.sh)
|
|
4
|
+
#
|
|
5
|
+
# Purpose:
|
|
6
|
+
# Resolves which wallet fixture a slot run should use (env override then
|
|
7
|
+
# runtime-dir/legacy/port-scoped candidates) and drives the recipe-tree
|
|
8
|
+
# wallet-fixture-state.cjs phases around the browser launch.
|
|
9
|
+
#
|
|
10
|
+
# Subcommands (flags):
|
|
11
|
+
# resolve --target <repo> --cdp-port <port> [--source-out <file>]
|
|
12
|
+
# Prints the fixture path (empty if none). Chain:
|
|
13
|
+
# RECIPE_WALLET_FIXTURE env > <runtime-dir>/wallet-fixture.json >
|
|
14
|
+
# temp/runtime/ > temp/.recipe-validation-<port>/ >
|
|
15
|
+
# temp/.agent-validation/.
|
|
16
|
+
# prefill --fixture <json> --target <repo> --state <file> --profile <dir>
|
|
17
|
+
# --extension-dir <dist> --extension-id-file <file>
|
|
18
|
+
# Runs fixture-state generate then prefill-profile (pre-launch).
|
|
19
|
+
# seed-cdp --fixture <json> --target <repo> --state <file> --cdp-port <p>
|
|
20
|
+
# --extension-dir <dist> --extension-id-file <file> --out <file>
|
|
21
|
+
# Runs fixture-state seed-cdp (post-launch).
|
|
22
|
+
# Common: [--fixture-script <path>] [--summary <file>]
|
|
23
|
+
#
|
|
24
|
+
# Outputs:
|
|
25
|
+
# resolve: fixture path on stdout, optional --source-out provenance JSON;
|
|
26
|
+
# prefill/seed-cdp: whatever wallet-fixture-state.cjs writes (--state/--out).
|
|
27
|
+
# Optional --summary file {feature,status,inputs,outputs,generatedAt}.
|
|
28
|
+
# Exit 0 — ok (resolve: also when no fixture found); 1 — env fixture not a
|
|
29
|
+
# file or fixture-state phase failed; 2 — bad args.
|
|
30
|
+
#
|
|
31
|
+
# Never touches: product source files; the fixture file (read-only).
|
|
32
|
+
set -euo pipefail
|
|
33
|
+
|
|
34
|
+
SUBCOMMAND="${1:-}"
|
|
35
|
+
case "$SUBCOMMAND" in
|
|
36
|
+
resolve|prefill|seed-cdp) shift ;;
|
|
37
|
+
-h|--help)
|
|
38
|
+
echo "Usage: seed-fixture.sh <resolve|prefill|seed-cdp> [flags] (see header)"
|
|
39
|
+
exit 0
|
|
40
|
+
;;
|
|
41
|
+
*) echo "Unknown subcommand: ${SUBCOMMAND:-<none>} (want resolve|prefill|seed-cdp)" >&2; exit 2 ;;
|
|
42
|
+
esac
|
|
43
|
+
|
|
44
|
+
TARGET="$PWD"
|
|
45
|
+
CDP_PORT=""
|
|
46
|
+
FIXTURE=""
|
|
47
|
+
STATE=""
|
|
48
|
+
PROFILE=""
|
|
49
|
+
EXTENSION_DIR=""
|
|
50
|
+
EXTENSION_ID_FILE=""
|
|
51
|
+
OUT=""
|
|
52
|
+
SOURCE_OUT=""
|
|
53
|
+
FIXTURE_SCRIPT=""
|
|
54
|
+
SUMMARY=""
|
|
55
|
+
require_value() { [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; }
|
|
56
|
+
while [ "$#" -gt 0 ]; do
|
|
57
|
+
case "$1" in
|
|
58
|
+
--target) require_value "$@"; TARGET="$2"; shift 2 ;;
|
|
59
|
+
--cdp-port) require_value "$@"; CDP_PORT="$2"; shift 2 ;;
|
|
60
|
+
--fixture) require_value "$@"; FIXTURE="$2"; shift 2 ;;
|
|
61
|
+
--state) require_value "$@"; STATE="$2"; shift 2 ;;
|
|
62
|
+
--profile) require_value "$@"; PROFILE="$2"; shift 2 ;;
|
|
63
|
+
--extension-dir) require_value "$@"; EXTENSION_DIR="$2"; shift 2 ;;
|
|
64
|
+
--extension-id-file) require_value "$@"; EXTENSION_ID_FILE="$2"; shift 2 ;;
|
|
65
|
+
--out) require_value "$@"; OUT="$2"; shift 2 ;;
|
|
66
|
+
--source-out) require_value "$@"; SOURCE_OUT="$2"; shift 2 ;;
|
|
67
|
+
--fixture-script) require_value "$@"; FIXTURE_SCRIPT="$2"; shift 2 ;;
|
|
68
|
+
--summary) require_value "$@"; SUMMARY="$2"; shift 2 ;;
|
|
69
|
+
-h|--help)
|
|
70
|
+
echo "Usage: seed-fixture.sh <resolve|prefill|seed-cdp> [flags] (see header)"
|
|
71
|
+
exit 0
|
|
72
|
+
;;
|
|
73
|
+
*) echo "Unknown arg: $1" >&2; exit 2 ;;
|
|
74
|
+
esac
|
|
75
|
+
done
|
|
76
|
+
|
|
77
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
78
|
+
|
|
79
|
+
status=fail
|
|
80
|
+
finish() {
|
|
81
|
+
if [ -n "$SUMMARY" ]; then
|
|
82
|
+
mkdir -p "$(dirname "$SUMMARY")"
|
|
83
|
+
STATUS_FOR_SUMMARY="$status" SUB_FOR_SUMMARY="$SUBCOMMAND" TARGET_FOR_SUMMARY="$TARGET" FIXTURE_FOR_SUMMARY="$FIXTURE" STATE_FOR_SUMMARY="$STATE" OUT_FOR_SUMMARY="$OUT" SUMMARY_PATH="$SUMMARY" node <<'NODE' || true
|
|
84
|
+
const fs = require('fs');
|
|
85
|
+
fs.writeFileSync(process.env.SUMMARY_PATH, `${JSON.stringify({
|
|
86
|
+
feature: 'extension/seed-fixture',
|
|
87
|
+
status: process.env.STATUS_FOR_SUMMARY,
|
|
88
|
+
inputs: {
|
|
89
|
+
subcommand: process.env.SUB_FOR_SUMMARY,
|
|
90
|
+
target: process.env.TARGET_FOR_SUMMARY,
|
|
91
|
+
fixture: process.env.FIXTURE_FOR_SUMMARY || null,
|
|
92
|
+
},
|
|
93
|
+
outputs: { state: process.env.STATE_FOR_SUMMARY || null, out: process.env.OUT_FOR_SUMMARY || null },
|
|
94
|
+
generatedAt: new Date().toISOString(),
|
|
95
|
+
}, null, 2)}\n`);
|
|
96
|
+
NODE
|
|
97
|
+
fi
|
|
98
|
+
}
|
|
99
|
+
trap finish EXIT
|
|
100
|
+
|
|
101
|
+
resolve_fixture_script() {
|
|
102
|
+
if [ -n "$FIXTURE_SCRIPT" ]; then
|
|
103
|
+
[ -f "$FIXTURE_SCRIPT" ] || { echo "seed-fixture: --fixture-script not found: $FIXTURE_SCRIPT" >&2; exit 2; }
|
|
104
|
+
printf '%s' "$FIXTURE_SCRIPT"
|
|
105
|
+
return 0
|
|
106
|
+
fi
|
|
107
|
+
if [ -f "$SCRIPT_DIR/wallet-fixture-state.cjs" ]; then
|
|
108
|
+
printf '%s' "$SCRIPT_DIR/wallet-fixture-state.cjs"
|
|
109
|
+
return 0
|
|
110
|
+
fi
|
|
111
|
+
echo "seed-fixture: wallet-fixture-state.cjs not found next to $SCRIPT_DIR; reinstall the runner." >&2
|
|
112
|
+
exit 1
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if [ "$SUBCOMMAND" = resolve ]; then
|
|
116
|
+
RUNTIME_DIR=""
|
|
117
|
+
# shellcheck disable=SC1091
|
|
118
|
+
for _hp in "$SCRIPT_DIR/lib/harness-path.sh" "$SCRIPT_DIR/../lib/harness-path.sh"; do
|
|
119
|
+
[ -f "$_hp" ] && { . "$_hp"; break; }
|
|
120
|
+
done
|
|
121
|
+
unset _hp
|
|
122
|
+
if command -v recipe_runtime_dir >/dev/null 2>&1; then
|
|
123
|
+
RUNTIME_DIR="$(recipe_runtime_dir)"
|
|
124
|
+
fi
|
|
125
|
+
resolved=""
|
|
126
|
+
resolve_status=found
|
|
127
|
+
if [ -n "${RECIPE_WALLET_FIXTURE:-}" ]; then
|
|
128
|
+
if [ ! -f "$RECIPE_WALLET_FIXTURE" ]; then
|
|
129
|
+
echo "[recipe-harness] RECIPE_WALLET_FIXTURE is not a file: $RECIPE_WALLET_FIXTURE" >&2
|
|
130
|
+
exit 1
|
|
131
|
+
fi
|
|
132
|
+
resolved="$RECIPE_WALLET_FIXTURE"
|
|
133
|
+
else
|
|
134
|
+
for candidate in \
|
|
135
|
+
${RUNTIME_DIR:+"$TARGET/$RUNTIME_DIR/wallet-fixture.json"} \
|
|
136
|
+
"$TARGET/temp/runtime/wallet-fixture.json" \
|
|
137
|
+
${CDP_PORT:+"$TARGET/temp/.recipe-validation-${CDP_PORT}/wallet-fixture.json"} \
|
|
138
|
+
"$TARGET/temp/.agent-validation/wallet-fixture.json"
|
|
139
|
+
do
|
|
140
|
+
[ -f "$candidate" ] && { resolved="$candidate"; break; }
|
|
141
|
+
done
|
|
142
|
+
fi
|
|
143
|
+
[ -n "$resolved" ] || resolve_status=missing
|
|
144
|
+
if [ -n "$SOURCE_OUT" ]; then
|
|
145
|
+
node - "$SOURCE_OUT" "$resolved" <<'NODE'
|
|
146
|
+
const fs = require('fs');
|
|
147
|
+
const [out, fixture] = process.argv.slice(2);
|
|
148
|
+
fs.mkdirSync(require('path').dirname(out), { recursive: true });
|
|
149
|
+
fs.writeFileSync(out, `${JSON.stringify({
|
|
150
|
+
status: fixture ? 'present' : 'missing',
|
|
151
|
+
fixturePath: fixture || null,
|
|
152
|
+
generatedAt: new Date().toISOString(),
|
|
153
|
+
}, null, 2)}\n`);
|
|
154
|
+
NODE
|
|
155
|
+
fi
|
|
156
|
+
[ -n "$resolved" ] && printf '%s\n' "$resolved"
|
|
157
|
+
status=pass
|
|
158
|
+
exit 0
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
# prefill / seed-cdp phases
|
|
162
|
+
[ -n "$FIXTURE" ] || { echo "Missing --fixture" >&2; exit 2; }
|
|
163
|
+
[ -n "$STATE" ] || { echo "Missing --state" >&2; exit 2; }
|
|
164
|
+
[ -n "$EXTENSION_DIR" ] || { echo "Missing --extension-dir" >&2; exit 2; }
|
|
165
|
+
[ -n "$EXTENSION_ID_FILE" ] || { echo "Missing --extension-id-file" >&2; exit 2; }
|
|
166
|
+
FS_SCRIPT="$(resolve_fixture_script)"
|
|
167
|
+
|
|
168
|
+
if [ "$SUBCOMMAND" = prefill ]; then
|
|
169
|
+
[ -n "$PROFILE" ] || { echo "Missing --profile" >&2; exit 2; }
|
|
170
|
+
node "$FS_SCRIPT" generate --target "$TARGET" --fixture "$FIXTURE" --out "$STATE" || exit 1
|
|
171
|
+
node "$FS_SCRIPT" prefill-profile --target "$TARGET" --state "$STATE" --profile "$PROFILE" --extension-dir "$EXTENSION_DIR" --extension-id-file "$EXTENSION_ID_FILE" || exit 1
|
|
172
|
+
else
|
|
173
|
+
[ -n "$CDP_PORT" ] || { echo "Missing --cdp-port" >&2; exit 2; }
|
|
174
|
+
[ -n "$OUT" ] || { echo "Missing --out" >&2; exit 2; }
|
|
175
|
+
node "$FS_SCRIPT" seed-cdp --target "$TARGET" --fixture "$FIXTURE" --state "$STATE" --cdp-port "$CDP_PORT" --extension-dir "$EXTENSION_DIR" --extension-id-file "$EXTENSION_ID_FILE" --out "$OUT" || exit 1
|
|
176
|
+
fi
|
|
177
|
+
status=pass
|