@deeeed/metamask-harness 0.78.0 → 0.80.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 +55 -0
- package/README.md +1 -1
- package/adapters/extension/ensure-browser.sh +2 -2
- package/adapters/extension/inject.mjs +0 -1
- package/adapters/extension/launch-browser.cjs +39 -613
- package/adapters/extension/readiness.mjs +2 -5
- package/adapters/extension/reattach.sh +2 -2
- package/adapters/extension/stamp-runtime-title.cjs +1 -1
- package/adapters/manifest.json +2 -34
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +57 -1358
- package/adapters/mobile/bridge-runtime/teaching.cjs +20 -0
- package/adapters/mobile/inject.sh +16 -0
- package/adapters/mobile/launch-console-forwarder.cjs +1 -2
- package/adapters/mobile/open-device.sh +2 -2
- package/adapters/mobile/reload-app.mjs +4 -4
- package/adapters/mobile/reset-app-data.sh +1 -1
- package/adapters/mobile/start-console-forwarder.sh +3 -2
- package/adapters/mobile/start-metro.sh +17 -12
- package/adapters/mobile/wait-for-bridge.cjs +2 -2
- package/adapters/shared/ensure-runner-deps.sh +2 -2
- package/adapters/shared/harness-source-fingerprint.mjs +13 -8
- package/adapters/shared/open-debug.mjs +2 -2
- package/adapters/shared/runner-deps-ready.mjs +1 -1
- package/adapters/shared/sync-wallet-fixture.sh +8 -5
- package/adapters/terminal/cleanup.mjs +2 -1
- package/adapters/terminal/inject.mjs +2 -1
- package/adapters/terminal/lib/cdp-client.mjs +1 -1
- package/bin/mm-harness +9 -28
- package/dist/adapters/checkout-files.js +16 -0
- package/dist/adapters/console-capture.js +14 -1
- package/dist/adapters/core/actions.js +4 -3
- package/dist/adapters/core/overlay.js +1 -1
- package/dist/adapters/core/readiness.js +28 -1
- package/dist/adapters/core/run.js +3 -3
- package/dist/adapters/core/surface.js +22 -5
- package/dist/adapters/extension/browser-pid.js +56 -0
- package/dist/adapters/extension/console-capture.js +1 -1
- package/dist/adapters/extension/doctor-checks.js +1 -1
- package/dist/adapters/extension/ensure-ready.js +5 -4
- package/dist/adapters/extension/extension-id.js +1 -1
- package/dist/adapters/extension/feature-flags.js +1 -1
- package/dist/adapters/extension/fixtures.js +1 -2
- package/dist/adapters/extension/launch.js +4 -5
- package/dist/adapters/extension/launched-browser.js +1 -2
- package/dist/adapters/extension/network-observer.js +1 -1
- package/dist/adapters/extension/overlay.js +1 -1
- package/dist/adapters/extension/product-config.js +1 -1
- package/dist/adapters/extension/readiness.js +1 -1
- package/dist/adapters/extension/reload.js +1 -1
- package/dist/adapters/extension/run.js +24 -10
- package/dist/adapters/extension/runtime-decision.js +3 -2
- package/dist/adapters/extension/runtime.js +9 -5
- package/dist/adapters/extension/surface.js +20 -5
- package/dist/adapters/extension/verify.js +2 -2
- package/dist/adapters/harness-freshness.js +2 -1
- package/dist/adapters/mobile/android-gfxinfo-performance.js +1 -3
- package/dist/adapters/mobile/app-identity.js +10 -0
- package/dist/adapters/mobile/doctor-checks.js +1 -1
- package/dist/adapters/mobile/fixtures.js +1 -1
- package/dist/adapters/mobile/launch.js +27 -28
- package/dist/adapters/mobile/lifecycle.js +1 -1
- package/dist/adapters/mobile/metro-env.js +9 -59
- package/dist/adapters/mobile/performance-observer.js +1 -1
- package/dist/adapters/mobile/prepare-target.js +1 -1
- package/dist/adapters/mobile/prepare.js +4 -5
- package/dist/adapters/mobile/provision.js +1 -1
- package/dist/adapters/mobile/readiness.js +1 -1
- package/dist/adapters/mobile/recording-target.js +15 -0
- package/dist/adapters/mobile/release-artifact-state.js +1 -1
- package/dist/adapters/mobile/reload.js +1 -1
- package/dist/adapters/mobile/run.js +9 -8
- package/dist/adapters/mobile/runtime-decision.js +5 -7
- package/dist/adapters/mobile/source-freshness.js +22 -126
- package/dist/adapters/mobile/surface.js +31 -6
- package/dist/adapters/mobile/ui.js +16 -5
- package/dist/adapters/mobile/verify.js +2 -3
- package/dist/adapters/performance/cdp-trace.js +1 -1
- package/dist/adapters/performance/device-platform.js +1 -1
- package/dist/adapters/registry.js +2 -14
- package/dist/adapters/slot-ports.js +1 -1
- package/dist/adapters/surface.js +1 -0
- package/dist/adapters/terminal/console-capture.js +1 -1
- package/dist/adapters/terminal/launch.js +1 -1
- package/dist/adapters/terminal/run.js +25 -2
- package/dist/adapters/terminal/surface.js +12 -4
- package/dist/adapters.js +8 -4
- package/dist/advisor-policy.js +1 -1
- package/dist/checkout-view.js +1 -2
- package/dist/cli.js +28 -15
- package/dist/command-contract.js +1 -26
- package/dist/commands/call.js +10 -24
- package/dist/commands/check.js +3 -4
- package/dist/commands/checklist.js +1 -1
- package/dist/commands/completion-candidates.js +2 -10
- package/dist/commands/config.js +2 -2
- package/dist/commands/core-readiness.js +12 -7
- package/dist/commands/debug.js +3 -3
- package/dist/commands/device-target.js +5 -5
- package/dist/commands/doctor.js +2 -14
- package/dist/commands/domain.js +2 -2
- package/dist/commands/ensure-ready.js +1 -7
- package/dist/commands/fixtures.js +5 -6
- package/dist/commands/help.js +3 -4
- package/dist/commands/launch/device.js +10 -17
- package/dist/commands/launch/extension.js +3 -9
- package/dist/commands/launch/mobile.js +1 -1
- package/dist/commands/list-executables.js +1 -3
- package/dist/commands/logs.js +3 -3
- package/dist/commands/manifest.js +1 -12
- package/dist/commands/mobile-device-view.js +4 -2
- package/dist/commands/overlay.js +110 -0
- package/dist/commands/pin.js +1 -1
- package/dist/commands/pr-body.js +2 -2
- package/dist/commands/prepare.js +6 -11
- package/dist/commands/provision.js +3 -12
- package/dist/commands/recipe-advice.js +1 -2
- package/dist/commands/recipe-quality.js +2 -3
- package/dist/commands/resolve-extension.js +1 -7
- package/dist/commands/review.js +4 -4
- package/dist/commands/run-engine.js +12 -51
- package/dist/commands/run.js +4 -25
- package/dist/commands/runtime-decision.js +1 -6
- package/dist/commands/runtime-health.js +1 -7
- package/dist/commands/runtime-launch.js +1 -13
- package/dist/commands/self-test.js +2 -6
- package/dist/commands/status-watch.js +1 -2
- package/dist/commands/status.js +2 -5
- package/dist/commands/task-init.js +2 -3
- package/dist/commands/tutorial.js +2 -2
- package/dist/commands/update.js +1 -1
- package/dist/completions-cache.js +1 -1
- package/dist/console-classifier.js +19 -0
- package/dist/discovery.js +1 -1
- package/dist/doctor.js +3 -4
- package/dist/funding-execution-context.js +15 -1
- package/dist/host.js +32 -0
- package/dist/index.js +1 -6
- package/dist/live-tsx.js +49 -0
- package/dist/mm-harness-cli.js +2 -4
- package/dist/network-observation.js +4 -4
- package/dist/paths.js +48 -78
- package/dist/performance-observation.js +2 -2
- package/dist/progress.js +1 -1
- package/dist/recipe-assistance-context.js +2 -4
- package/dist/recipe-security.js +2 -59
- package/dist/review/knowledge.js +3 -3
- package/dist/runner.js +5 -3
- package/dist/runtime-context.js +1 -1
- package/dist/task-view.js +3 -4
- package/docs/CONTRIBUTING.md +3 -3
- package/docs/cli-contract.md +2 -2
- package/docs/feature-guard.md +90 -0
- package/library/README.md +3 -1
- package/library/actions/core/perps/_controller.mjs +153 -130
- package/library/actions/core/perps/quote_fees.mjs +58 -0
- package/library/actions/core/wallet/_fixture-accounts.mjs +69 -0
- package/library/actions/core/wallet/list_accounts.mjs +14 -57
- package/library/actions/extension/feature_flags/state.mjs +1 -1
- package/library/actions/extension/platform/cdp.mjs +2 -8
- package/library/actions/harness-exports.mjs +1 -6
- package/library/actions/mobile/app/network-control.mjs +6 -8
- package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +1 -1
- package/library/actions/mobile/platform/bridge.mjs +8 -8
- package/library/actions/mobile/platform/native-session-name.mjs +21 -1
- package/library/actions/mobile/platform/native-session.mjs +3 -4
- package/library/actions/mobile/platform/observe-ui.mjs +12 -6
- package/library/actions/mobile/ui/native-navigation.mjs +4 -3
- package/library/actions/mobile/wallet/native-ui.mjs +3 -2
- package/library/actions/shared/analytics/_adapter.mjs +1 -1
- package/library/actions/shared/console-findings.mjs +1 -1
- package/library/actions/terminal/platform/runtime.mjs +2 -1
- package/library/manifests/core.action-manifest.json +83 -13
- package/package.json +8 -8
- package/adapters/extension/lib/slot-title.cjs +0 -292
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +0 -472
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +0 -125
- package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +0 -1055
- package/adapters/mobile/bridge-runtime/lib/cdp-eval.cjs +0 -110
- package/adapters/mobile/bridge-runtime/lib/config.cjs +0 -61
- package/adapters/mobile/bridge-runtime/lib/console-format.cjs +0 -53
- package/adapters/mobile/bridge-runtime/lib/devtools-proxy.cjs +0 -177
- package/adapters/mobile/bridge-runtime/lib/issue-capture.cjs +0 -446
- package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +0 -84
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +0 -403
- package/adapters/mobile/bridge-runtime/lib/ws-client.cjs +0 -116
- package/adapters/mobile/coalesce-metro-log.cjs +0 -24
- package/adapters/mobile/launch-metro.cjs +0 -77
- package/adapters/mobile/metro-config.cjs +0 -98
- package/adapters/mobile/metro-log-generation.cjs +0 -106
- package/dist/adapters/mobile/frame-metrics.js +0 -118
- package/dist/adapters/mobile/video-recorder.js +0 -289
- package/dist/artifact-files.js +0 -92
- package/dist/checkout-lock.js +0 -149
- package/dist/cli-color.js +0 -98
- package/dist/command-journal.js +0 -308
- package/dist/commands/last.js +0 -60
- package/dist/commands/launch/index.js +0 -109
- package/dist/commands/parse-args.js +0 -279
- package/dist/commands/reload.js +0 -41
- package/dist/commands/run-report.js +0 -241
- package/dist/commands/shared.js +0 -323
- package/dist/commands/stop.js +0 -60
- package/dist/devices.js +0 -100
- package/dist/execution-provenance.js +0 -332
- package/dist/harness.js +0 -451
- package/dist/heal-bounds.js +0 -137
- package/dist/json-stream.js +0 -60
- package/dist/leaf-invoke.js +0 -19
- package/dist/library-provenance.js +0 -33
- package/dist/live-adapter-contract.js +0 -380
- package/dist/recording-target.js +0 -120
- package/dist/run-diagnostics.js +0 -364
- package/dist/run-recording.js +0 -481
- package/dist/runtime-proof.js +0 -90
- package/library/actions/mobile/platform/tool-paths.mjs +0 -123
package/dist/run-recording.js
DELETED
|
@@ -1,481 +0,0 @@
|
|
|
1
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { optionalVideoTiming, readCaptureHelperTiming, writeRecordingTimeline } from "@farmslot/recipe-harness";
|
|
5
|
-
import { getAdapterSurface } from "./adapters/surface.js";
|
|
6
|
-
import {
|
|
7
|
-
captureHelperSupportsRecordSessionSnapshots,
|
|
8
|
-
captureHelperSupportsCapability
|
|
9
|
-
} from "./recording-target.js";
|
|
10
|
-
const activeRecordingsByPid = /* @__PURE__ */ new Map();
|
|
11
|
-
const ACTIVE_EXTENSION_RECORDING_PID = "METAMASK_RECIPE_EXTENSION_ACTIVE_RECORDING_PID";
|
|
12
|
-
async function startRecipeRecording(adapter, projectRoot, artifactsDir, options) {
|
|
13
|
-
if (!options.record) return void 0;
|
|
14
|
-
if (process.platform !== "darwin") {
|
|
15
|
-
console.error(
|
|
16
|
-
"WARN: --record-video uses capture-helper and is currently supported only on macOS; continuing without video."
|
|
17
|
-
);
|
|
18
|
-
return void 0;
|
|
19
|
-
}
|
|
20
|
-
const framed = getAdapterSurface(adapter).recording?.framed;
|
|
21
|
-
if (!framed) return void 0;
|
|
22
|
-
const cdpPort = options.cdpPort ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT;
|
|
23
|
-
if (!captureHelperSupportsRecordSessionSnapshots(projectRoot)) {
|
|
24
|
-
console.error(
|
|
25
|
-
"WARN: framed extension recording requires capture-helper capability record_session_snapshot; falling back to harness --record-video."
|
|
26
|
-
);
|
|
27
|
-
return void 0;
|
|
28
|
-
}
|
|
29
|
-
const pid = framed.browserPid(projectRoot, artifactsDir, cdpPort);
|
|
30
|
-
if (!pid) {
|
|
31
|
-
console.error(
|
|
32
|
-
`WARN: framed extension recording could not resolve browser PID from CDP port ${cdpPort ?? "<unset>"}; falling back to harness --record-video.`
|
|
33
|
-
);
|
|
34
|
-
return void 0;
|
|
35
|
-
}
|
|
36
|
-
const recordArgs = ["record", "--framed", "--pid", String(pid)];
|
|
37
|
-
const relativePath = "videos/full-run.mp4";
|
|
38
|
-
const outputPath = path.join(artifactsDir, relativePath);
|
|
39
|
-
const { stagingDir, stagedPath } = preparePrivateRecordingDestination(
|
|
40
|
-
artifactsDir,
|
|
41
|
-
outputPath
|
|
42
|
-
);
|
|
43
|
-
let child;
|
|
44
|
-
const startedAtUnixMs = Date.now();
|
|
45
|
-
try {
|
|
46
|
-
child = spawn(captureHelperPath(), [...recordArgs, "--output", stagedPath], {
|
|
47
|
-
cwd: projectRoot,
|
|
48
|
-
env: process.env,
|
|
49
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
50
|
-
});
|
|
51
|
-
} catch (error) {
|
|
52
|
-
cleanupRecordingStaging(stagingDir);
|
|
53
|
-
throw error;
|
|
54
|
-
}
|
|
55
|
-
const recording = {
|
|
56
|
-
child,
|
|
57
|
-
startedAtUnixMs,
|
|
58
|
-
nativeTiming: captureHelperSupportsCapability(projectRoot, "record_session_timing_v1"),
|
|
59
|
-
outputPath,
|
|
60
|
-
stagingDir,
|
|
61
|
-
stagedPath,
|
|
62
|
-
relativePath,
|
|
63
|
-
pid,
|
|
64
|
-
stdout: "",
|
|
65
|
-
stderr: "",
|
|
66
|
-
exited: false,
|
|
67
|
-
exitCode: null,
|
|
68
|
-
frameReady: false,
|
|
69
|
-
finalized: false,
|
|
70
|
-
previousActiveRecordingPid: process.env[ACTIVE_EXTENSION_RECORDING_PID],
|
|
71
|
-
stderrBuffer: "",
|
|
72
|
-
pendingSnapshots: /* @__PURE__ */ new Map()
|
|
73
|
-
};
|
|
74
|
-
child.stdout.on("data", (chunk) => {
|
|
75
|
-
recording.stdout += String(chunk);
|
|
76
|
-
});
|
|
77
|
-
child.stderr.on("data", (chunk) => {
|
|
78
|
-
recording.stderr += String(chunk);
|
|
79
|
-
handleRecordingStderr(recording, String(chunk));
|
|
80
|
-
});
|
|
81
|
-
child.on("error", (error) => {
|
|
82
|
-
recording.error = error;
|
|
83
|
-
recording.stderr += error.message;
|
|
84
|
-
recording.exited = true;
|
|
85
|
-
});
|
|
86
|
-
child.on("close", (exitCode) => {
|
|
87
|
-
recording.stoppedAtUnixMs = Date.now();
|
|
88
|
-
recording.exited = true;
|
|
89
|
-
recording.exitCode = exitCode;
|
|
90
|
-
activeRecordingsByPid.delete(recording.pid);
|
|
91
|
-
restoreActiveRecordingEnvironment(recording);
|
|
92
|
-
rejectPendingSnapshots(recording, new Error(`capture-helper recording exited before snapshot completed (code=${exitCode ?? "unknown"})`));
|
|
93
|
-
});
|
|
94
|
-
await waitForRecordingReady(recording, 15e3);
|
|
95
|
-
if (recording.exited) {
|
|
96
|
-
cleanupRecordingStaging(recording.stagingDir);
|
|
97
|
-
throw new Error(
|
|
98
|
-
`capture-helper exited before recording its first frame (code=${recording.exitCode ?? "unknown"}): ${recording.stderr || recording.stdout}`
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
if (!recording.frameReady) {
|
|
102
|
-
try {
|
|
103
|
-
await stopRecordingProcess(recording);
|
|
104
|
-
} finally {
|
|
105
|
-
cleanupRecordingStaging(recording.stagingDir);
|
|
106
|
-
}
|
|
107
|
-
throw new Error(
|
|
108
|
-
`capture-helper did not record a frame within 15000ms: ${recording.stderr || recording.stdout || "no recorder output"}`
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
activeRecordingsByPid.set(pid, recording);
|
|
112
|
-
process.env[ACTIVE_EXTENSION_RECORDING_PID] = String(pid);
|
|
113
|
-
console.error(`INFO: recording recipe video with capture-helper pid=${pid} output=${outputPath}`);
|
|
114
|
-
return recording;
|
|
115
|
-
}
|
|
116
|
-
async function captureActiveRecipeRecordingSnapshot(pid, outputPath, timeoutMs = 3e4) {
|
|
117
|
-
const recording = activeRecordingsByPid.get(pid);
|
|
118
|
-
if (!recording || recording.exited) return void 0;
|
|
119
|
-
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
120
|
-
fs.rmSync(outputPath, { force: true });
|
|
121
|
-
return new Promise((resolve, reject) => {
|
|
122
|
-
const timer = setTimeout(() => {
|
|
123
|
-
recording.pendingSnapshots.delete(outputPath);
|
|
124
|
-
reject(new Error(`capture-helper record session snapshot timed out after ${timeoutMs}ms: ${outputPath}`));
|
|
125
|
-
}, timeoutMs);
|
|
126
|
-
recording.pendingSnapshots.set(outputPath, { outputPath, timer, resolve, reject });
|
|
127
|
-
recording.child.stdin.write(`snapshot ${outputPath}
|
|
128
|
-
`, (error) => {
|
|
129
|
-
if (!error) return;
|
|
130
|
-
clearTimeout(timer);
|
|
131
|
-
recording.pendingSnapshots.delete(outputPath);
|
|
132
|
-
reject(error);
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
async function stopRecipeRecording(recording, result) {
|
|
137
|
-
if (!recording || recording.finalized) return;
|
|
138
|
-
recording.finalized = true;
|
|
139
|
-
try {
|
|
140
|
-
await stopRecordingProcess(recording);
|
|
141
|
-
const validation = await validateRecordingArtifact(recording);
|
|
142
|
-
if (validation.ok === false) {
|
|
143
|
-
throw new Error(
|
|
144
|
-
`capture-helper recording did not produce a usable video artifact: ${validation.reason}`
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
publishRecordingArtifact(recording);
|
|
148
|
-
if (result) await addRecordingArtifactToManifest(result, recording);
|
|
149
|
-
} catch (error) {
|
|
150
|
-
if (result) removeRecordingArtifactFromManifest(result, recording.relativePath);
|
|
151
|
-
throw error;
|
|
152
|
-
} finally {
|
|
153
|
-
activeRecordingsByPid.delete(recording.pid);
|
|
154
|
-
restoreActiveRecordingEnvironment(recording);
|
|
155
|
-
cleanupRecordingStaging(recording.stagingDir);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
async function stopRecordingProcess(recording) {
|
|
159
|
-
if (!recording.exited && !recording.child.stdin.destroyed) {
|
|
160
|
-
recording.child.stdin.end("stop\n");
|
|
161
|
-
await waitForRecordingExit(recording, 15e3);
|
|
162
|
-
}
|
|
163
|
-
if (!recording.exited) {
|
|
164
|
-
recording.child.kill("SIGINT");
|
|
165
|
-
await waitForRecordingExit(recording, 5e3);
|
|
166
|
-
}
|
|
167
|
-
if (!recording.exited) {
|
|
168
|
-
recording.child.kill("SIGTERM");
|
|
169
|
-
await waitForRecordingExit(recording, 3e3);
|
|
170
|
-
}
|
|
171
|
-
if (!recording.exited) {
|
|
172
|
-
recording.child.kill("SIGKILL");
|
|
173
|
-
await waitForRecordingExit(recording, 2e3);
|
|
174
|
-
}
|
|
175
|
-
if (!recording.exited) {
|
|
176
|
-
throw new Error(`capture-helper recording process did not exit: pid=${recording.child.pid ?? "unknown"}`);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
async function waitForRecordingExit(recording, timeoutMs) {
|
|
180
|
-
if (recording.exited) return;
|
|
181
|
-
await new Promise((resolve) => {
|
|
182
|
-
const finish = () => {
|
|
183
|
-
clearTimeout(timer);
|
|
184
|
-
recording.child.removeListener("close", finish);
|
|
185
|
-
resolve();
|
|
186
|
-
};
|
|
187
|
-
const timer = setTimeout(finish, timeoutMs);
|
|
188
|
-
recording.child.once("close", finish);
|
|
189
|
-
if (recording.exited) finish();
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
async function waitForRecordingReady(recording, timeoutMs) {
|
|
193
|
-
const deadline = Date.now() + timeoutMs;
|
|
194
|
-
while (!recording.frameReady && !recording.exited && Date.now() < deadline) {
|
|
195
|
-
await sleep(50);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
function handleRecordingStderr(recording, chunk) {
|
|
199
|
-
recording.stderrBuffer += chunk;
|
|
200
|
-
let newlineIndex = recording.stderrBuffer.indexOf("\n");
|
|
201
|
-
while (newlineIndex !== -1) {
|
|
202
|
-
const line = recording.stderrBuffer.slice(0, newlineIndex).trim();
|
|
203
|
-
recording.stderrBuffer = recording.stderrBuffer.slice(newlineIndex + 1);
|
|
204
|
-
if (line) handleRecordingEventLine(recording, line);
|
|
205
|
-
newlineIndex = recording.stderrBuffer.indexOf("\n");
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
function handleRecordingEventLine(recording, line) {
|
|
209
|
-
let event;
|
|
210
|
-
try {
|
|
211
|
-
const parsed = JSON.parse(line);
|
|
212
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
|
|
213
|
-
event = parsed;
|
|
214
|
-
} catch {
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
if (event.type === "info" && typeof event.msg === "string" && /^record frames=[1-9]\d*$/.test(event.msg)) {
|
|
218
|
-
recording.frameReady = true;
|
|
219
|
-
}
|
|
220
|
-
if (event.type === "record_complete" && event.output === recording.stagedPath) {
|
|
221
|
-
recording.completedVideo = typeof event.frames === "number" && event.frames > 0;
|
|
222
|
-
if (typeof event.recording_id === "string") recording.completedRecordingId = event.recording_id;
|
|
223
|
-
}
|
|
224
|
-
const output = typeof event.output === "string" ? event.output : void 0;
|
|
225
|
-
if (!output) return;
|
|
226
|
-
const pending = recording.pendingSnapshots.get(output);
|
|
227
|
-
if (!pending) return;
|
|
228
|
-
if (event.type === "snapshot") {
|
|
229
|
-
clearTimeout(pending.timer);
|
|
230
|
-
recording.pendingSnapshots.delete(output);
|
|
231
|
-
pending.resolve(event);
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
if (event.type === "error") {
|
|
235
|
-
clearTimeout(pending.timer);
|
|
236
|
-
recording.pendingSnapshots.delete(output);
|
|
237
|
-
pending.reject(new Error(String(event.message ?? `capture-helper snapshot failed: ${output}`)));
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
function rejectPendingSnapshots(recording, error) {
|
|
241
|
-
for (const pending of recording.pendingSnapshots.values()) {
|
|
242
|
-
clearTimeout(pending.timer);
|
|
243
|
-
pending.reject(error);
|
|
244
|
-
}
|
|
245
|
-
recording.pendingSnapshots.clear();
|
|
246
|
-
}
|
|
247
|
-
async function validateRecordingArtifact(recording) {
|
|
248
|
-
if (!fs.existsSync(recording.stagedPath)) {
|
|
249
|
-
return { ok: false, reason: `missing output ${recording.stagedPath}` };
|
|
250
|
-
}
|
|
251
|
-
const stagedStat = fs.lstatSync(recording.stagedPath);
|
|
252
|
-
if (stagedStat.isSymbolicLink() || !stagedStat.isFile()) {
|
|
253
|
-
return { ok: false, reason: `unsafe non-regular output ${recording.stagedPath}` };
|
|
254
|
-
}
|
|
255
|
-
const size = stagedStat.size;
|
|
256
|
-
if (size === 0) {
|
|
257
|
-
return { ok: false, reason: `empty output ${recording.stagedPath}` };
|
|
258
|
-
}
|
|
259
|
-
const recorderOutput = `${recording.stdout}
|
|
260
|
-
${recording.stderr}`;
|
|
261
|
-
if (recording.nativeTiming) {
|
|
262
|
-
if (!recording.completedVideo) return { ok: false, reason: "Missing finalized video completion" };
|
|
263
|
-
recording.nativeTimingEvidence = await readNativeRecordingTiming(recording);
|
|
264
|
-
return { ok: true };
|
|
265
|
-
}
|
|
266
|
-
if (!recorderOutput.includes("record_complete")) {
|
|
267
|
-
return {
|
|
268
|
-
ok: false,
|
|
269
|
-
reason: `capture-helper did not report record_complete for ${recording.stagedPath}: ${recorderOutput.trim() || "no recorder output"}`
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
const ffprobe = spawnSync(
|
|
273
|
-
"ffprobe",
|
|
274
|
-
["-hide_banner", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", recording.stagedPath],
|
|
275
|
-
{ encoding: "utf8" }
|
|
276
|
-
);
|
|
277
|
-
if (ffprobe.error && ffprobe.error.code === "ENOENT") {
|
|
278
|
-
return { ok: true };
|
|
279
|
-
}
|
|
280
|
-
if (ffprobe.error) {
|
|
281
|
-
return { ok: false, reason: `ffprobe failed for ${recording.stagedPath}: ${ffprobe.error.message}` };
|
|
282
|
-
}
|
|
283
|
-
if (ffprobe.status !== 0) {
|
|
284
|
-
return {
|
|
285
|
-
ok: false,
|
|
286
|
-
reason: `invalid MP4 ${recording.stagedPath}: ${ffprobe.stderr.trim() || ffprobe.stdout.trim() || `ffprobe exited ${ffprobe.status}`}`
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
const durationSeconds = Number(ffprobe.stdout.trim());
|
|
290
|
-
if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) {
|
|
291
|
-
return { ok: false, reason: `MP4 has no positive duration: ${recording.stagedPath}` };
|
|
292
|
-
}
|
|
293
|
-
return { ok: true };
|
|
294
|
-
}
|
|
295
|
-
async function readNativeRecordingTiming(recording) {
|
|
296
|
-
try {
|
|
297
|
-
if (!recording.completedRecordingId) throw new Error("Missing native recording completion identity");
|
|
298
|
-
const sidecar = recording.stagedPath + ".timing.json";
|
|
299
|
-
const entry = fs.lstatSync(sidecar);
|
|
300
|
-
if (!entry.isFile() || entry.isSymbolicLink()) throw new Error("Native timing must be a regular file");
|
|
301
|
-
const native = JSON.parse(fs.readFileSync(sidecar, "utf8"));
|
|
302
|
-
if (native.recording_id !== recording.completedRecordingId) throw new Error("Native recording identity mismatch");
|
|
303
|
-
return await readCaptureHelperTiming(recording.stagedPath);
|
|
304
|
-
} catch (error) {
|
|
305
|
-
return { timingUnavailableReason: error instanceof Error ? error.message : String(error) };
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
function preparePrivateRecordingDestination(artifactsDir, outputPath) {
|
|
309
|
-
const outputDir = path.dirname(outputPath);
|
|
310
|
-
ensureRecordingDirectory(path.resolve(artifactsDir), outputDir);
|
|
311
|
-
refuseRecordingDestinationSymlink(outputPath);
|
|
312
|
-
const stagingDir = fs.mkdtempSync(path.join(outputDir, ".mm-harness-recording-"));
|
|
313
|
-
fs.chmodSync(stagingDir, 448);
|
|
314
|
-
return {
|
|
315
|
-
stagingDir,
|
|
316
|
-
stagedPath: path.join(stagingDir, "full-run.mp4")
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
function ensureRecordingDirectory(artifactsRoot, outputDir) {
|
|
320
|
-
fs.mkdirSync(artifactsRoot, { recursive: true });
|
|
321
|
-
requireRecordingDirectory(artifactsRoot);
|
|
322
|
-
const relative = path.relative(artifactsRoot, outputDir);
|
|
323
|
-
let current = artifactsRoot;
|
|
324
|
-
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
325
|
-
current = path.join(current, segment);
|
|
326
|
-
try {
|
|
327
|
-
fs.mkdirSync(current);
|
|
328
|
-
} catch (error) {
|
|
329
|
-
if (error.code !== "EEXIST") throw error;
|
|
330
|
-
}
|
|
331
|
-
requireRecordingDirectory(current);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
function requireRecordingDirectory(directory) {
|
|
335
|
-
const entry = fs.lstatSync(directory);
|
|
336
|
-
if (entry.isDirectory() && !entry.isSymbolicLink()) return;
|
|
337
|
-
throw new Error(
|
|
338
|
-
`Refusing unsafe recording artifact directory: ${directory}
|
|
339
|
-
Next: replace ${JSON.stringify(directory)} with a regular directory and retry the recipe.`
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
function publishRecordingArtifact(recording) {
|
|
343
|
-
publishPrivateRecordingFile(recording.stagedPath, recording.outputPath);
|
|
344
|
-
if (recording.nativeTimingEvidence?.timing) {
|
|
345
|
-
try {
|
|
346
|
-
publishPrivateRecordingFile(recording.stagedPath + ".timing.json", recording.outputPath + ".timing.json");
|
|
347
|
-
} catch (error) {
|
|
348
|
-
recording.nativeTimingEvidence = { timingUnavailableReason: error instanceof Error ? error.message : String(error) };
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
function publishPrivateRecordingFile(sourcePath, outputPath) {
|
|
353
|
-
const noFollow = fs.constants.O_NOFOLLOW ?? 0;
|
|
354
|
-
let descriptor;
|
|
355
|
-
try {
|
|
356
|
-
descriptor = fs.openSync(sourcePath, fs.constants.O_RDONLY | noFollow);
|
|
357
|
-
const stagedStat = fs.fstatSync(descriptor);
|
|
358
|
-
if (!stagedStat.isFile()) {
|
|
359
|
-
throw unsafeRecordingArtifactError(sourcePath);
|
|
360
|
-
}
|
|
361
|
-
fs.fchmodSync(descriptor, 384);
|
|
362
|
-
} catch (error) {
|
|
363
|
-
if (error instanceof Error && error.message.includes("Next:")) throw error;
|
|
364
|
-
throw unsafeRecordingArtifactError(
|
|
365
|
-
sourcePath,
|
|
366
|
-
error instanceof Error ? error.message : String(error)
|
|
367
|
-
);
|
|
368
|
-
} finally {
|
|
369
|
-
if (descriptor !== void 0) fs.closeSync(descriptor);
|
|
370
|
-
}
|
|
371
|
-
refuseRecordingDestinationSymlink(outputPath);
|
|
372
|
-
fs.renameSync(sourcePath, outputPath);
|
|
373
|
-
}
|
|
374
|
-
function refuseRecordingDestinationSymlink(outputPath) {
|
|
375
|
-
try {
|
|
376
|
-
const destination = fs.lstatSync(outputPath);
|
|
377
|
-
if (destination.isSymbolicLink()) {
|
|
378
|
-
throw new Error(
|
|
379
|
-
`Refusing recording artifact destination symlink: ${outputPath}
|
|
380
|
-
Next: rm -- ${JSON.stringify(outputPath)} and retry the recipe.`
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
if (!destination.isFile()) {
|
|
384
|
-
throw new Error(
|
|
385
|
-
`Refusing non-file recording artifact destination: ${outputPath}
|
|
386
|
-
Next: remove ${JSON.stringify(outputPath)} and retry the recipe.`
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
} catch (error) {
|
|
390
|
-
if (error.code === "ENOENT") return;
|
|
391
|
-
throw error;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
function unsafeRecordingArtifactError(stagedPath, detail) {
|
|
395
|
-
return new Error(
|
|
396
|
-
`Capture-helper recording output is not a safe regular file: ${stagedPath}${detail ? ` (${detail})` : ""}.
|
|
397
|
-
Next: capture-helper doctor --json`
|
|
398
|
-
);
|
|
399
|
-
}
|
|
400
|
-
function cleanupRecordingStaging(stagingDir) {
|
|
401
|
-
try {
|
|
402
|
-
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
403
|
-
} catch (error) {
|
|
404
|
-
console.error(
|
|
405
|
-
`WARN: could not remove capture-helper staging directory ${stagingDir}: ${error instanceof Error ? error.message : String(error)}`
|
|
406
|
-
);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
function restoreActiveRecordingEnvironment(recording) {
|
|
410
|
-
if (process.env[ACTIVE_EXTENSION_RECORDING_PID] !== String(recording.pid)) return;
|
|
411
|
-
if (recording.previousActiveRecordingPid === void 0) {
|
|
412
|
-
delete process.env[ACTIVE_EXTENSION_RECORDING_PID];
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
process.env[ACTIVE_EXTENSION_RECORDING_PID] = recording.previousActiveRecordingPid;
|
|
416
|
-
}
|
|
417
|
-
async function addRecordingArtifactToManifest(result, recording) {
|
|
418
|
-
const manifestPath = result.artifactManifestPath;
|
|
419
|
-
if (!manifestPath || !fs.existsSync(manifestPath)) {
|
|
420
|
-
console.error(`WARN: cannot add video artifact to missing artifact manifest: ${manifestPath ?? "<unset>"}`);
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
424
|
-
if (!Array.isArray(manifest.artifacts)) manifest.artifacts = [];
|
|
425
|
-
manifest.artifacts = manifest.artifacts.filter((artifact) => artifact.path !== recording.relativePath);
|
|
426
|
-
const timing = recording.nativeTiming ? recording.nativeTimingEvidence ?? { timingUnavailableReason: "Native recording timing unavailable" } : await optionalVideoTiming(recording.outputPath, {
|
|
427
|
-
startedAtUnixMs: recording.startedAtUnixMs,
|
|
428
|
-
stoppedAtUnixMs: recording.stoppedAtUnixMs ?? Date.now()
|
|
429
|
-
});
|
|
430
|
-
if (recording.nativeTimingEvidence?.timing) manifest.artifacts.push({ path: recording.relativePath + ".timing.json", type: "json", category: "system", label: "Native recording and screenshot timing" });
|
|
431
|
-
let timelinePath;
|
|
432
|
-
let timelineUnavailableReason = timing.timingUnavailableReason;
|
|
433
|
-
if (timing.timing) {
|
|
434
|
-
try {
|
|
435
|
-
const raw = JSON.parse(fs.readFileSync(result.tracePath, "utf8"));
|
|
436
|
-
const trace = Array.isArray(raw) ? raw : raw.entries;
|
|
437
|
-
timelinePath = await writeRecordingTimeline(path.dirname(manifestPath), recording.relativePath, timing.timing, trace);
|
|
438
|
-
manifest.artifacts.push({ path: timelinePath, type: "json", label: "Recording frames and action markers", category: "system" });
|
|
439
|
-
} catch (error) {
|
|
440
|
-
timelineUnavailableReason = error instanceof Error ? error.message : String(error);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
manifest.artifacts.push({
|
|
444
|
-
path: recording.relativePath,
|
|
445
|
-
type: "video",
|
|
446
|
-
label: "Full recipe replay video",
|
|
447
|
-
category: "evidence",
|
|
448
|
-
mimeType: "video/mp4",
|
|
449
|
-
record: "full_run",
|
|
450
|
-
...timelinePath ? { timelinePath } : { timelineUnavailableReason },
|
|
451
|
-
metadata: {
|
|
452
|
-
provider: "capture-helper",
|
|
453
|
-
mode: "full_run",
|
|
454
|
-
pid: recording.pid
|
|
455
|
-
}
|
|
456
|
-
});
|
|
457
|
-
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
458
|
-
`);
|
|
459
|
-
}
|
|
460
|
-
function removeRecordingArtifactFromManifest(result, relativePath) {
|
|
461
|
-
const manifestPath = result.artifactManifestPath;
|
|
462
|
-
if (!manifestPath || !fs.existsSync(manifestPath)) return;
|
|
463
|
-
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
464
|
-
if (!Array.isArray(manifest.artifacts)) return;
|
|
465
|
-
const nextArtifacts = manifest.artifacts.filter((artifact) => artifact.path !== relativePath);
|
|
466
|
-
if (nextArtifacts.length === manifest.artifacts.length) return;
|
|
467
|
-
manifest.artifacts = nextArtifacts;
|
|
468
|
-
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
469
|
-
`);
|
|
470
|
-
}
|
|
471
|
-
function captureHelperPath() {
|
|
472
|
-
return process.env.CAPTURE_HELPER_PATH || "capture-helper";
|
|
473
|
-
}
|
|
474
|
-
function sleep(ms) {
|
|
475
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
476
|
-
}
|
|
477
|
-
export {
|
|
478
|
-
captureActiveRecipeRecordingSnapshot,
|
|
479
|
-
startRecipeRecording,
|
|
480
|
-
stopRecipeRecording
|
|
481
|
-
};
|
package/dist/runtime-proof.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { normalizeRecipeRef, validateRecipeArtifactPackage } from "@farmslot/protocol";
|
|
3
|
-
function validateRuntimeProof(bundle, smokeTarget) {
|
|
4
|
-
const checked = validateRecipeArtifactPackage(bundle);
|
|
5
|
-
assert.equal(checked.status, "valid", JSON.stringify(checked.findings));
|
|
6
|
-
const { recipe, summary, trace, manifest, recipeResolution, resolvedRecipes } = bundle;
|
|
7
|
-
assert.equal(summary.status, "pass", "Runtime recipe did not pass");
|
|
8
|
-
const entries = Array.isArray(trace) ? trace : trace.entries;
|
|
9
|
-
assert(entries.length && entries.every((entry) => entry.ok === true), "Runtime trace contains failed or unknown actions");
|
|
10
|
-
assert(entries.some((entry) => entry.action === "end" && entry.status === "pass" && !String(entry.nodeId ?? entry.id).includes("/")), "Runtime recipe did not reach its passing end");
|
|
11
|
-
const documents = new Map(recipeResolution.dependencies.map((dependency) => [normalizeRecipeRef(dependency.ref), resolvedRecipes[dependency.digest]]));
|
|
12
|
-
const nodes = collectProofNodes(recipe, documents);
|
|
13
|
-
const executed = new Map(entries.map((entry, index) => [entry.nodeId ?? entry.id, { entry, index }]));
|
|
14
|
-
function proves(id) {
|
|
15
|
-
const node = nodes.get(id);
|
|
16
|
-
const current = executed.get(id);
|
|
17
|
-
if (!current || current.entry.action !== node.action) return false;
|
|
18
|
-
if (node.action === "call") return [...nodes.keys()].some((child) => child.startsWith(`${id}/`) && proves(child));
|
|
19
|
-
if (!assertions.has(node.action) || !current.entry.output || typeof current.entry.output !== "object") return false;
|
|
20
|
-
if (node.action === "assert_output") {
|
|
21
|
-
const source = assertionSource(id, node);
|
|
22
|
-
const prior = executed.get(source);
|
|
23
|
-
const action = nodes.get(source)?.action;
|
|
24
|
-
return Boolean(prior && prior.index < current.index && action && action === prior.entry.action && isObservation(action));
|
|
25
|
-
}
|
|
26
|
-
if (node.action === "ui.wait_for") return entries.slice(0, current.index).some((entry) => inputs.has(entry.action));
|
|
27
|
-
return typeof node.path === "string" && manifest.artifacts.some((artifact) => artifact.path === node.path && artifact.category === "proof");
|
|
28
|
-
}
|
|
29
|
-
assert(recipe.proofTargets?.length, "Runtime recipe has no behavioral proof targets");
|
|
30
|
-
for (const target of recipe.proofTargets) {
|
|
31
|
-
const linked = [...nodes].filter(([, node]) => node.proves?.includes(target.id));
|
|
32
|
-
assert(linked.some(([id]) => proves(id)), `Proof target ${target.id} has no executed runtime assertion`);
|
|
33
|
-
}
|
|
34
|
-
if (smokeTarget) assert(recipe.proofTargets.some((target) => target.id === smokeTarget), "Smoke target is absent from recipe");
|
|
35
|
-
}
|
|
36
|
-
const assertions = /* @__PURE__ */ new Set(["assert_output", "assert_json", "ui.wait_for"]);
|
|
37
|
-
const controls = /* @__PURE__ */ new Set(["end", "wait", "manual", "switch", "call", "index_artifacts", "assert_exit_code", "assert_file", "branch", "repeat", "set", "log"]);
|
|
38
|
-
const inputs = /* @__PURE__ */ new Set(["ui.press", "ui.key_press", "ui.set_input", "ui.swipe", "ui.drag", "ui.long_press"]);
|
|
39
|
-
function isObservation(action) {
|
|
40
|
-
return !controls.has(action) && !assertions.has(action);
|
|
41
|
-
}
|
|
42
|
-
function assertionSource(id, node) {
|
|
43
|
-
const prefix = id.includes("/") ? id.slice(0, id.lastIndexOf("/") + 1) : "";
|
|
44
|
-
return prefix + String(node.source ?? node.node ?? "");
|
|
45
|
-
}
|
|
46
|
-
function collectProofNodes(recipe, documents) {
|
|
47
|
-
const nodes = /* @__PURE__ */ new Map();
|
|
48
|
-
function collect(document, prefix = "", stack = /* @__PURE__ */ new Set()) {
|
|
49
|
-
for (const [id, node] of Object.entries(document.workflow.nodes)) {
|
|
50
|
-
nodes.set(prefix + id, node);
|
|
51
|
-
if (node.action === "call" && typeof node.ref === "string") {
|
|
52
|
-
const ref = normalizeRecipeRef(node.ref);
|
|
53
|
-
assert(!stack.has(ref), "Recursive recipe proof");
|
|
54
|
-
const nested = documents.get(ref);
|
|
55
|
-
assert(nested, `Missing resolved recipe ${ref}`);
|
|
56
|
-
collect(nested, `${prefix}${id}/`, new Set(stack).add(ref));
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
collect(recipe);
|
|
61
|
-
return nodes;
|
|
62
|
-
}
|
|
63
|
-
function validateRuntimeProofPlan(recipe, documents = /* @__PURE__ */ new Map()) {
|
|
64
|
-
const finding = (code, path, message) => ({ severity: "error", code, path, message });
|
|
65
|
-
if (!recipe.proofTargets?.length) return [finding("proof.targets_missing", "proofTargets", "Behavioral proof requires at least one proof target.")];
|
|
66
|
-
const nodes = collectProofNodes(recipe, documents);
|
|
67
|
-
const hasInput = [...nodes.values()].some((node) => inputs.has(node.action));
|
|
68
|
-
function eligible(id) {
|
|
69
|
-
const node = nodes.get(id);
|
|
70
|
-
if (node.action === "call") return [...nodes.keys()].some((child) => child.startsWith(`${id}/`) && eligible(child));
|
|
71
|
-
if (node.action === "assert_output") {
|
|
72
|
-
const source = nodes.get(assertionSource(id, node));
|
|
73
|
-
return Boolean(source && isObservation(source.action));
|
|
74
|
-
}
|
|
75
|
-
if (node.action === "ui.wait_for") return hasInput;
|
|
76
|
-
return node.action === "assert_json" && typeof node.path === "string";
|
|
77
|
-
}
|
|
78
|
-
return recipe.proofTargets.flatMap((target, index) => {
|
|
79
|
-
const linked = [...nodes].filter(([, node]) => node.proves?.includes(target.id));
|
|
80
|
-
return linked.some(([id]) => eligible(id)) ? [] : [finding(
|
|
81
|
-
"proof.no_runtime_assertion",
|
|
82
|
-
`proofTargets[${index}]`,
|
|
83
|
-
`Proof target ${target.id} has no potentially eligible runtime assertion. Bind proves to assert_output over an observation, ui.wait_for with an input action, assert_json over a proof artifact, or a call containing one. Runtime order, outcomes and artifact indexing remain unverified.`
|
|
84
|
-
)];
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
export {
|
|
88
|
-
validateRuntimeProof,
|
|
89
|
-
validateRuntimeProofPlan
|
|
90
|
-
};
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
// Locate executable adb and idb clients, cache their paths, and export tool-path environment overrides.
|
|
2
|
-
import { constants as fsConstants, accessSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
|
|
6
|
-
const TOOL_ENV = {
|
|
7
|
-
adb: 'MM_HARNESS_ADB_PATH',
|
|
8
|
-
idb: 'MM_HARNESS_IDB_PATH',
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const TOOL_RECOVERY = {
|
|
12
|
-
adb: 'brew install android-platform-tools',
|
|
13
|
-
idb: 'brew install idb-companion pipx python@3.13 && pipx install fb-idb --python python3.13',
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const resolvedPaths = new Map();
|
|
17
|
-
|
|
18
|
-
export function mobileToolRecovery(tool) {
|
|
19
|
-
assertTool(tool);
|
|
20
|
-
return TOOL_RECOVERY[tool];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function mobileToolEnvName(tool) {
|
|
24
|
-
assertTool(tool);
|
|
25
|
-
return TOOL_ENV[tool];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function resolveMobileToolPath(tool, { required = false } = {}) {
|
|
29
|
-
assertTool(tool);
|
|
30
|
-
const cached = resolvedPaths.get(tool);
|
|
31
|
-
if (cached && executablePath(cached)) return cached;
|
|
32
|
-
|
|
33
|
-
for (const candidate of candidatesFor(tool)) {
|
|
34
|
-
const resolved = executablePath(candidate);
|
|
35
|
-
if (!resolved) continue;
|
|
36
|
-
resolvedPaths.set(tool, resolved);
|
|
37
|
-
process.env[TOOL_ENV[tool]] = resolved;
|
|
38
|
-
return resolved;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
resolvedPaths.delete(tool);
|
|
42
|
-
if (!required) return null;
|
|
43
|
-
const label = tool === 'adb' ? 'Android SDK Platform-Tools (adb)' : 'Facebook idb client';
|
|
44
|
-
throw new Error(
|
|
45
|
-
`${label} is required but no executable was found.\n` +
|
|
46
|
-
` Next: ${TOOL_RECOVERY[tool]}`,
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function resolveAndExportMobileToolPaths(platform) {
|
|
51
|
-
if (platform === 'android') {
|
|
52
|
-
return { adb: resolveMobileToolPath('adb', { required: true }), idb: null };
|
|
53
|
-
}
|
|
54
|
-
if (platform === 'ios') {
|
|
55
|
-
return { adb: null, idb: resolveMobileToolPath('idb') };
|
|
56
|
-
}
|
|
57
|
-
return {
|
|
58
|
-
adb: resolveMobileToolPath('adb'),
|
|
59
|
-
idb: resolveMobileToolPath('idb'),
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function clearMobileToolPathCache() {
|
|
64
|
-
resolvedPaths.clear();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function candidatesFor(tool) {
|
|
68
|
-
const override = process.env[TOOL_ENV[tool]];
|
|
69
|
-
const pathCandidates = String(process.env.PATH ?? '')
|
|
70
|
-
.split(path.delimiter)
|
|
71
|
-
.filter(Boolean)
|
|
72
|
-
.map((directory) => path.join(directory, tool));
|
|
73
|
-
const candidates = override ? [override] : [];
|
|
74
|
-
candidates.push(...pathCandidates);
|
|
75
|
-
if (tool === 'adb') {
|
|
76
|
-
for (const root of [
|
|
77
|
-
process.env.ANDROID_HOME,
|
|
78
|
-
process.env.ANDROID_SDK_ROOT,
|
|
79
|
-
path.join(os.homedir(), 'Library/Android/sdk'),
|
|
80
|
-
path.join(os.homedir(), 'Android/Sdk'),
|
|
81
|
-
]) {
|
|
82
|
-
if (root) candidates.push(path.join(root, 'platform-tools', 'adb'));
|
|
83
|
-
}
|
|
84
|
-
} else {
|
|
85
|
-
if (process.env.IDB_PATH) candidates.push(process.env.IDB_PATH);
|
|
86
|
-
candidates.push(
|
|
87
|
-
'/opt/homebrew/bin/idb',
|
|
88
|
-
'/usr/local/bin/idb',
|
|
89
|
-
path.join(os.homedir(), '.local/bin/idb'),
|
|
90
|
-
);
|
|
91
|
-
candidates.push(...pythonUserCandidates());
|
|
92
|
-
}
|
|
93
|
-
return [...new Set(candidates.map((candidate) => path.resolve(candidate)))];
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function pythonUserCandidates() {
|
|
97
|
-
const root = path.join(os.homedir(), 'Library/Python');
|
|
98
|
-
try {
|
|
99
|
-
return readdirSync(root, { withFileTypes: true })
|
|
100
|
-
.filter((entry) => entry.isDirectory())
|
|
101
|
-
.map((entry) => path.join(root, entry.name, 'bin/idb'));
|
|
102
|
-
} catch {
|
|
103
|
-
return [];
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function executablePath(candidate) {
|
|
108
|
-
try {
|
|
109
|
-
const resolved = realpathSync(candidate);
|
|
110
|
-
const stat = statSync(resolved);
|
|
111
|
-
if (!stat.isFile()) return null;
|
|
112
|
-
accessSync(resolved, fsConstants.X_OK);
|
|
113
|
-
return path.resolve(resolved);
|
|
114
|
-
} catch {
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function assertTool(tool) {
|
|
120
|
-
if (tool !== 'adb' && tool !== 'idb') {
|
|
121
|
-
throw new Error(`Unsupported mobile device tool: ${String(tool)}.`);
|
|
122
|
-
}
|
|
123
|
-
}
|