@deeeed/metamask-harness 0.23.1 → 0.25.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 +44 -0
- package/adapters/extension/live.sh +2 -2
- package/adapters/extension/wallet-fixture-state.cjs +57 -19
- package/adapters/shared/ensure-runner-deps.sh +64 -5
- package/bin/mm-harness +26 -8
- package/dist/adapters/extension/runtime.js +1 -10
- package/dist/adapters.js +5 -1
- package/dist/commands/run-engine.js +18 -13
- package/dist/commands/runtime-launch.js +7 -2
- package/dist/live-adapter-contract.js +4 -2
- package/dist/recipe-security.js +10 -2
- package/dist/run-recording.js +156 -39
- package/library/actions/core/perps/_controller.mjs +138 -5
- package/library/actions/core/perps/assert_orders.mjs +174 -10
- package/library/actions/core/perps/assert_positions.mjs +20 -2
- package/library/actions/core/perps/close_orders.mjs +24 -5
- package/library/actions/core/perps/close_positions.mjs +22 -8
- package/library/actions/core/perps/edit_order.mjs +331 -0
- package/library/actions/core/perps/place_order.mjs +192 -43
- package/library/actions/core/perps/update_position_tpsl.mjs +121 -15
- package/library/actions/extension/analytics/set_consent.mjs +165 -0
- package/library/actions/extension/platform/cdp.mjs +112 -14
- package/library/actions/mobile/analytics/set_consent.mjs +90 -0
- package/library/actions/shared/analytics/_adapter.mjs +24 -0
- package/library/actions/shared/analytics/assert_events.mjs +168 -0
- package/library/actions/shared/analytics/collector.mjs +505 -0
- package/library/actions/shared/analytics/consent.mjs +14 -0
- package/library/actions/shared/analytics/read_events.mjs +22 -0
- package/library/actions/shared/analytics/start_capture.mjs +24 -0
- package/library/manifests/core.action-manifest.json +468 -27
- package/library/manifests/extension.action-manifest.json +188 -1
- package/library/manifests/mobile.action-manifest.json +161 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,50 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.25.0 - 2026-07-30
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Extension and Mobile recipes can start a local analytics collector, set MetaMetrics consent through each app's native state boundary, and assert captured event cardinality and properties.
|
|
10
|
+
- Extension UI actions record default screen and visibility observations in recipe traces.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Updated to Recipe Protocol 0.14 and `@farmslot/recipe-harness` 0.10.3 for trustworthy observation, artifact, and hardened-page CDP behavior.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Analytics capture rejects foreign collectors, replaces its own previous listener safely, and reverses the collector port to selected Android devices.
|
|
19
|
+
- Analytics captures honor the selected runtime directory and store telemetry in private, symlink-safe files.
|
|
20
|
+
- Analytics collector startup executes only plan-bound bundled bytes; opt-out consent defaults, typed property assertions, and long settle windows retain their declared semantics.
|
|
21
|
+
- Extension analytics consent setup calls the background controller over a temporary Extension port without navigating through unrelated Settings UI.
|
|
22
|
+
- Recipe evidence screenshots and recordings publish atomically with private permissions and reject symlink destinations.
|
|
23
|
+
- Explicit Extension video recording waits for a captured frame and fails the run when the recorder cannot publish a valid MP4 instead of reporting a proof without its requested evidence.
|
|
24
|
+
- Extension fixture seeding evaluates through raw CDP so LavaMoat scuttling cannot break live launch setup.
|
|
25
|
+
- Extension launch output prints a runnable `runtime-launch` recovery command, including a selected Chrome profile, instead of suggesting unsupported or incomplete `launch` flags.
|
|
26
|
+
- `runtime-launch` distinguishes launch failures from post-launch verification failures and points at the failing evidence.
|
|
27
|
+
- Source-checkout dependency bootstrap verifies every runtime dependency and its declared version before declaring the harness ready, while preserving explicit local protocol package links.
|
|
28
|
+
- Source checkouts run newer TypeScript sources instead of silently executing an older compiled `dist/`.
|
|
29
|
+
|
|
30
|
+
## 0.24.0 - 2026-07-30
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- `metamask.perps.edit_order` drives `PerpsController.editOrder()` on the `core` adapter, verifying that a known resting target was replaced by one new order matching the requested attributes rather than trusting the acknowledgement.
|
|
35
|
+
- `expect_error` asserts that a `core` perps operation is REJECTED with a given typed error instead of succeeding. The comparison is exact and code-only: actions attach the controller's error code as data, and a rejection carrying no code does NOT match, so an unrelated failure whose message happens to mention the code cannot satisfy a refusal proof. Exposed only on the five actions with a real controller-refusal boundary (`place_order`, `edit_order`, `update_position_tpsl`, `close_orders`, `close_positions`); on read, assert and ensure actions every throw is argument validation or a postcondition failure, which can carry no code, so the parameter is not advertised there.
|
|
36
|
+
- `expect_count` on `metamask.perps.assert_orders` and `assert_positions` pins the exact number of matching items, which the default at-least-one check cannot do.
|
|
37
|
+
- Attached TP/SL assertions verify that one parent links to live children of the requested TP/SL family, market, side and trigger price, and record those attributes in evidence.
|
|
38
|
+
- Relative price and size inputs on `place_order` and `update_position_tpsl`: `take_profit_offset_pct` / `stop_loss_offset_pct` derive a price from live mid, and `take_profit_fraction` / `stop_loss_fraction` derive a size from the live position, rounded to the market's size precision. An absolute value still wins when supplied.
|
|
39
|
+
- `notional` on `edit_order` derives a size when the resting order cannot be read.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- Optional params left unset by a recipe template arrive as an empty string; the `core` perps actions treated that as a supplied value, so an order with no TP/SL could be rejected as having an invalid TP/SL size. Empty now means absent and falls through to a supplied alias.
|
|
44
|
+
- Batch order-cancel and position-close failures retain the failed controller result instead of being flattened, including partial failures.
|
|
45
|
+
- `edit_order` accepts only resting `GTC` and `ALO` replacements; `IOC` cannot satisfy its resting-order postcondition.
|
|
46
|
+
- `place_order` no longer discards a `time_in_force` or a stray trigger price on a non-trigger placement. Dropping a field the caller set is wrong on its own terms, and it also hid the controller's refusal of it; both are forwarded and the controller decides.
|
|
47
|
+
- `place_order` and `edit_order` reject a blank or unrecognised `side` instead of treating anything that is not `short` as a long, which let an unset template place a real order on an assumed side.
|
|
48
|
+
|
|
5
49
|
## 0.23.1 - 2026-07-29
|
|
6
50
|
|
|
7
51
|
### Fixed
|
|
@@ -279,10 +279,10 @@ if [ -n "$REMOTE_FLAGS" ] && [ "$REMOTE_FLAGS_APPLIED" != "true" ]; then
|
|
|
279
279
|
fi
|
|
280
280
|
|
|
281
281
|
echo "Extension live validation command:"
|
|
282
|
-
display_args=(mm-harness launch --
|
|
283
|
-
$LAUNCH_EXISTING_DIST && display_args+=(--launch-existing-dist)
|
|
282
|
+
display_args=(mm-harness runtime-launch --adapter extension --target "$TARGET" --cdp-port "$CDP_PORT")
|
|
284
283
|
$START_WATCH && display_args+=(--start-watch)
|
|
285
284
|
[ -n "$REMOTE_FLAGS" ] && display_args+=(--remote-flag "$REMOTE_FLAGS")
|
|
285
|
+
[ -n "$CHROME_USER_DATA_DIR" ] && display_args+=(--chrome-user-data-dir "$CHROME_USER_DATA_DIR")
|
|
286
286
|
printf ' '
|
|
287
287
|
printf '%q ' "${display_args[@]}"
|
|
288
288
|
printf '\n'
|
|
@@ -708,11 +708,10 @@ async function waitForWalletScreen(page) {
|
|
|
708
708
|
const unlockSelector = '[data-testid="unlock-password"]';
|
|
709
709
|
const deadline = Date.now() + 45000;
|
|
710
710
|
while (Date.now() < deadline) {
|
|
711
|
-
const fatalStartup = await page
|
|
712
|
-
.
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
})
|
|
711
|
+
const fatalStartup = await evaluateViaCdp(page, () => {
|
|
712
|
+
const text = document.body?.innerText || '';
|
|
713
|
+
return text.includes('MetaMask had trouble starting') ? text.slice(0, 500) : null;
|
|
714
|
+
})
|
|
716
715
|
.catch(() => null);
|
|
717
716
|
if (fatalStartup) {
|
|
718
717
|
return { state: 'fatal', selector: null, detail: fatalStartup };
|
|
@@ -733,24 +732,51 @@ async function waitForWalletScreen(page) {
|
|
|
733
732
|
return { state: 'unknown', selector: null };
|
|
734
733
|
}
|
|
735
734
|
|
|
735
|
+
async function evaluateViaCdp(page, callback, argument) {
|
|
736
|
+
const session = await page.context().newCDPSession(page);
|
|
737
|
+
const invocation = argument === undefined
|
|
738
|
+
? `(${callback.toString()})()`
|
|
739
|
+
: `(${callback.toString()})(${JSON.stringify(argument)})`;
|
|
740
|
+
try {
|
|
741
|
+
const response = await session.send('Runtime.evaluate', {
|
|
742
|
+
expression: invocation,
|
|
743
|
+
awaitPromise: true,
|
|
744
|
+
returnByValue: true,
|
|
745
|
+
});
|
|
746
|
+
if (response.exceptionDetails) {
|
|
747
|
+
const detail =
|
|
748
|
+
response.exceptionDetails.exception?.description ??
|
|
749
|
+
response.exceptionDetails.text ??
|
|
750
|
+
'unknown evaluation failure';
|
|
751
|
+
throw new Error(`CDP evaluation failed: ${detail}`);
|
|
752
|
+
}
|
|
753
|
+
return response.result?.value;
|
|
754
|
+
} finally {
|
|
755
|
+
await session.detach().catch(() => {});
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
736
759
|
async function attemptUnlock(page, password) {
|
|
737
760
|
// Selector ladder: current testids first, then the generic selectors the CDP
|
|
738
761
|
// unlock action uses (proven against the same build). A freshly seeded vault
|
|
739
762
|
// can also still be initializing, so a single attempt is not conclusive —
|
|
740
763
|
// the caller retries.
|
|
741
|
-
const filled = await page
|
|
764
|
+
const filled = await evaluateViaCdp(page, (pw) => {
|
|
742
765
|
const input =
|
|
743
766
|
document.querySelector('[data-testid="unlock-password"]') ??
|
|
744
767
|
document.querySelector('input[type="password"]');
|
|
745
768
|
if (!input) return false;
|
|
746
|
-
const setter = Object.getOwnPropertyDescriptor(
|
|
769
|
+
const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(input), 'value').set;
|
|
747
770
|
setter.call(input, pw);
|
|
748
|
-
|
|
749
|
-
|
|
771
|
+
for (const type of ['input', 'change']) {
|
|
772
|
+
const event = input.ownerDocument.createEvent('Event');
|
|
773
|
+
event.initEvent(type, true, true);
|
|
774
|
+
input.dispatchEvent(event);
|
|
775
|
+
}
|
|
750
776
|
return true;
|
|
751
777
|
}, password);
|
|
752
778
|
if (!filled) return false;
|
|
753
|
-
return page
|
|
779
|
+
return evaluateViaCdp(page, () => {
|
|
754
780
|
const button =
|
|
755
781
|
document.querySelector('[data-testid="unlock-submit"]') ??
|
|
756
782
|
document.querySelector('button[type="submit"]') ??
|
|
@@ -781,8 +807,8 @@ async function unlockIfNeeded(page, password) {
|
|
|
781
807
|
}
|
|
782
808
|
|
|
783
809
|
async function readLiveAccounts(page) {
|
|
784
|
-
const raw = await page
|
|
785
|
-
const metamask = (window.stateHooks?.
|
|
810
|
+
const raw = await evaluateViaCdp(page, async () => {
|
|
811
|
+
const metamask = (await window.stateHooks?.getCleanAppState?.())?.metamask || {};
|
|
786
812
|
const accts = metamask.internalAccounts || {};
|
|
787
813
|
const byId = accts.accounts || {};
|
|
788
814
|
const selectedId = accts.selectedAccount || null;
|
|
@@ -905,9 +931,10 @@ async function applyAccountNames(page, expectedAccounts) {
|
|
|
905
931
|
if (!account.name || !account.address.startsWith('0x')) {
|
|
906
932
|
continue;
|
|
907
933
|
}
|
|
908
|
-
await
|
|
934
|
+
await evaluateViaCdp(
|
|
935
|
+
page,
|
|
909
936
|
async ({ address, name }) => {
|
|
910
|
-
const metamask = window.stateHooks?.
|
|
937
|
+
const metamask = (await window.stateHooks?.getCleanAppState?.())?.metamask || {};
|
|
911
938
|
const normalizedAddress = String(address || '').toLowerCase();
|
|
912
939
|
const accountsById = metamask.internalAccounts?.accounts || {};
|
|
913
940
|
const accountId = Object.keys(accountsById).find(
|
|
@@ -937,9 +964,11 @@ async function applySelectedAccount(page, expectedSelected) {
|
|
|
937
964
|
if (!expectedSelected?.address) {
|
|
938
965
|
return;
|
|
939
966
|
}
|
|
940
|
-
await
|
|
967
|
+
await evaluateViaCdp(
|
|
968
|
+
page,
|
|
941
969
|
async ({ address }) => {
|
|
942
|
-
const accounts =
|
|
970
|
+
const accounts =
|
|
971
|
+
(await window.stateHooks?.getCleanAppState?.())?.metamask?.internalAccounts?.accounts || {};
|
|
943
972
|
const accountId = Object.keys(accounts).find(
|
|
944
973
|
(id) => String(accounts[id]?.address || '').toLowerCase() === String(address || '').toLowerCase(),
|
|
945
974
|
);
|
|
@@ -1009,9 +1038,18 @@ async function seedCdp(args) {
|
|
|
1009
1038
|
fs.writeFileSync(extensionIdFile, `${extensionId}\n`);
|
|
1010
1039
|
}
|
|
1011
1040
|
|
|
1012
|
-
|
|
1013
|
-
await
|
|
1014
|
-
|
|
1041
|
+
try {
|
|
1042
|
+
await evaluateViaCdp(page, async (state) => {
|
|
1043
|
+
await chrome.storage.local.set(state);
|
|
1044
|
+
}, versionedState);
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
if (!String(error?.message || '').includes('property "chrome" of globalThis is inaccessible under scuttling mode')) {
|
|
1047
|
+
throw error;
|
|
1048
|
+
}
|
|
1049
|
+
console.error(
|
|
1050
|
+
'[fixture] chrome.storage is scuttled in the page realm; validating the prefilled profile state instead.',
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1015
1053
|
await page.goto(`chrome-extension://${extensionId}/home.html`, {
|
|
1016
1054
|
waitUntil: 'load',
|
|
1017
1055
|
timeout: 30000,
|
|
@@ -17,8 +17,65 @@ if [ -z "$RUNNER_DIR" ]; then
|
|
|
17
17
|
fi
|
|
18
18
|
RUNNER_DIR="$(cd "$RUNNER_DIR" && pwd -P)"
|
|
19
19
|
|
|
20
|
+
PROTOCOL_ROOT="${METAMASK_RUNNER_PROTOCOL_ROOT:-${METAMASK_RUNNER_FARMSLOT_ROOT:-${FARMSLOT_ROOT:-}}}"
|
|
21
|
+
if [ -z "$PROTOCOL_ROOT" ] && [ -f "$RUNNER_DIR/.farmslot-root" ]; then
|
|
22
|
+
PROTOCOL_ROOT="$(cat "$RUNNER_DIR/.farmslot-root")"
|
|
23
|
+
fi
|
|
24
|
+
|
|
20
25
|
deps_ready() {
|
|
21
|
-
|
|
26
|
+
node --input-type=module - "$RUNNER_DIR" "$PROTOCOL_ROOT" <<'NODE'
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import { createRequire } from 'node:module';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import { pathToFileURL } from 'node:url';
|
|
31
|
+
|
|
32
|
+
const runnerDir = process.argv[2];
|
|
33
|
+
const overrideRootInput = process.argv[3];
|
|
34
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(runnerDir, 'package.json'), 'utf8'));
|
|
35
|
+
let dependencyVersionSatisfies;
|
|
36
|
+
try {
|
|
37
|
+
const requireFromRunner = createRequire(path.join(runnerDir, 'package.json'));
|
|
38
|
+
const helperPath = requireFromRunner.resolve(
|
|
39
|
+
'@farmslot/recipe-harness/runtime/deps-readiness',
|
|
40
|
+
);
|
|
41
|
+
({ dependencyVersionSatisfies } = await import(pathToFileURL(helperPath).href));
|
|
42
|
+
if (typeof dependencyVersionSatisfies !== 'function') process.exit(1);
|
|
43
|
+
} catch {
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
let overrideRoot = null;
|
|
47
|
+
if (overrideRootInput) {
|
|
48
|
+
try {
|
|
49
|
+
overrideRoot = fs.realpathSync(overrideRootInput);
|
|
50
|
+
} catch {
|
|
51
|
+
overrideRoot = null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function isOverrideLink(packageDir) {
|
|
55
|
+
if (!overrideRoot) return false;
|
|
56
|
+
try {
|
|
57
|
+
if (!fs.lstatSync(packageDir).isSymbolicLink()) return false;
|
|
58
|
+
const resolved = fs.realpathSync(packageDir);
|
|
59
|
+
const relative = path.relative(overrideRoot, resolved);
|
|
60
|
+
return relative === '' || (
|
|
61
|
+
relative !== '..' &&
|
|
62
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
63
|
+
!path.isAbsolute(relative)
|
|
64
|
+
);
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const invalid = Object.entries(pkg.dependencies ?? {}).filter(([name, request]) => {
|
|
70
|
+
const packageDir = path.join(runnerDir, 'node_modules', ...name.split('/'));
|
|
71
|
+
const installedPath = path.join(packageDir, 'package.json');
|
|
72
|
+
if (!fs.existsSync(installedPath)) return true;
|
|
73
|
+
if (isOverrideLink(packageDir)) return false;
|
|
74
|
+
const installed = JSON.parse(fs.readFileSync(installedPath, 'utf8'));
|
|
75
|
+
return !dependencyVersionSatisfies(installed.version, request);
|
|
76
|
+
});
|
|
77
|
+
process.exit(invalid.length === 0 ? 0 : 1);
|
|
78
|
+
NODE
|
|
22
79
|
}
|
|
23
80
|
|
|
24
81
|
# Library actions import @deeeed/metamask-harness by package name; a source checkout
|
|
@@ -55,10 +112,6 @@ if [ ! -f "$RUNNER_DIR/package.json" ]; then
|
|
|
55
112
|
exit 1
|
|
56
113
|
fi
|
|
57
114
|
|
|
58
|
-
PROTOCOL_ROOT="${METAMASK_RUNNER_PROTOCOL_ROOT:-${METAMASK_RUNNER_FARMSLOT_ROOT:-${FARMSLOT_ROOT:-}}}"
|
|
59
|
-
if [ -z "$PROTOCOL_ROOT" ] && [ -f "$RUNNER_DIR/.farmslot-root" ]; then
|
|
60
|
-
PROTOCOL_ROOT="$(cat "$RUNNER_DIR/.farmslot-root")"
|
|
61
|
-
fi
|
|
62
115
|
if [ -n "$PROTOCOL_ROOT" ] && [ -f "$RUNNER_DIR/scripts/link-local-farmslot.mjs" ]; then
|
|
63
116
|
if [ -f "$PROTOCOL_ROOT/packages/recipe-harness/package.json" ]; then
|
|
64
117
|
FARMSLOT_ROOT="$(cd "$PROTOCOL_ROOT" && pwd -P)" \
|
|
@@ -77,6 +130,12 @@ echo "mm-harness: installing runner dependencies via npm in $RUNNER_DIR" >&2
|
|
|
77
130
|
npm install --ignore-scripts >&2
|
|
78
131
|
)
|
|
79
132
|
|
|
133
|
+
# npm may replace the local protocol links with registry packages. Restore the
|
|
134
|
+
# explicit co-development override after the complete dependency tree exists.
|
|
135
|
+
if [ -n "$PROTOCOL_ROOT" ] && [ -f "$RUNNER_DIR/scripts/link-local-farmslot.mjs" ]; then
|
|
136
|
+
FARMSLOT_ROOT="$PROTOCOL_ROOT" node "$RUNNER_DIR/scripts/link-local-farmslot.mjs" >&2
|
|
137
|
+
fi
|
|
138
|
+
|
|
80
139
|
if ! deps_ready; then
|
|
81
140
|
echo "mm-harness: runner dependencies still missing after npm install: $RUNNER_DIR" >&2
|
|
82
141
|
exit 1
|
package/bin/mm-harness
CHANGED
|
@@ -128,20 +128,38 @@ node_can_run_source_typescript() {
|
|
|
128
128
|
rm -rf "$probe_dir"
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
|
|
131
|
+
# Published installs use dist. A source checkout uses dist only while it is at
|
|
132
|
+
# least as new as every TypeScript source it would shadow.
|
|
133
|
+
USE_DIST=0
|
|
134
134
|
if [ -f "$ENTRY_DIST" ] && [ -f "$ENTRY_TS" ]; then
|
|
135
|
+
case "$RUNNER_DIR/" in
|
|
136
|
+
*/node_modules/*)
|
|
137
|
+
USE_DIST=1
|
|
138
|
+
export MM_HARNESS_RUN_MODE="dist"
|
|
139
|
+
;;
|
|
140
|
+
*)
|
|
141
|
+
newer_source=""
|
|
142
|
+
while IFS= read -r newer_source; do
|
|
143
|
+
break
|
|
144
|
+
done < <(find "$RUNNER_DIR/src" -name '*.ts' -newer "$ENTRY_DIST" -print 2>/dev/null)
|
|
145
|
+
if [ -n "$newer_source" ]; then
|
|
146
|
+
export MM_HARNESS_RUN_MODE="src"
|
|
147
|
+
echo "mm-harness: dist/ is older than src/ — running the current source checkout." >&2
|
|
148
|
+
echo " Next: npm run build to refresh dist/" >&2
|
|
149
|
+
else
|
|
150
|
+
USE_DIST=1
|
|
151
|
+
export MM_HARNESS_RUN_MODE="dist"
|
|
152
|
+
fi
|
|
153
|
+
;;
|
|
154
|
+
esac
|
|
155
|
+
elif [ -f "$ENTRY_DIST" ]; then
|
|
156
|
+
USE_DIST=1
|
|
135
157
|
export MM_HARNESS_RUN_MODE="dist"
|
|
136
|
-
if find "$RUNNER_DIR/src" -name '*.ts' -newer "$ENTRY_DIST" 2>/dev/null | head -1 | grep -q .; then
|
|
137
|
-
echo "mm-harness: dist/ is OLDER than src/ — this run uses the stale compiled code." >&2
|
|
138
|
-
echo " Next: npm run build (or: trash dist/ to run straight from src via tsx)" >&2
|
|
139
|
-
fi
|
|
140
158
|
elif [ -f "$ENTRY_TS" ]; then
|
|
141
159
|
export MM_HARNESS_RUN_MODE="src"
|
|
142
160
|
fi
|
|
143
161
|
|
|
144
|
-
if [
|
|
162
|
+
if [ "$USE_DIST" -eq 1 ]; then
|
|
145
163
|
exec node "$ENTRY_DIST" "$@"
|
|
146
164
|
fi
|
|
147
165
|
|
|
@@ -146,16 +146,7 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
|
|
|
146
146
|
if (runtime.backgroundUnresponsive === true) {
|
|
147
147
|
findings.push("Extension UI reports background connection unresponsive.");
|
|
148
148
|
}
|
|
149
|
-
if (runtime.hasSubmitRequest !== true) {
|
|
150
|
-
findings.push("stateHooks.submitRequestToBackground is unavailable.");
|
|
151
|
-
}
|
|
152
|
-
if (runtime.hasStore !== true) {
|
|
153
|
-
findings.push("stateHooks.store is unavailable.");
|
|
154
|
-
}
|
|
155
|
-
if (runtime.hasPerpsStreamManager !== true) {
|
|
156
|
-
findings.push("stateHooks.getPerpsStreamManager is unavailable.");
|
|
157
|
-
}
|
|
158
|
-
if (runtime.backgroundProbeOk !== true) {
|
|
149
|
+
if (runtime.hasSubmitRequest === true && runtime.backgroundProbeOk !== true) {
|
|
159
150
|
warnings.push(`Perps background read probe failed: ${runtime.backgroundProbeError ?? "unknown error"}.`);
|
|
160
151
|
}
|
|
161
152
|
const extensionId = safeExtensionId(target);
|
package/dist/adapters.js
CHANGED
|
@@ -496,7 +496,7 @@ function createMetaMaskUiTransport(platform, harness, preparedLiveAdapters) {
|
|
|
496
496
|
);
|
|
497
497
|
}
|
|
498
498
|
});
|
|
499
|
-
|
|
499
|
+
const transport = {
|
|
500
500
|
async execute(action, node, context) {
|
|
501
501
|
if (action === "ui.navigate") {
|
|
502
502
|
const live = await runLiveFirst(
|
|
@@ -513,6 +513,10 @@ function createMetaMaskUiTransport(platform, harness, preparedLiveAdapters) {
|
|
|
513
513
|
return base.execute(action, action === "ui.wait_for" ? normalizeUiWaitNode(node) : node, context);
|
|
514
514
|
}
|
|
515
515
|
};
|
|
516
|
+
if (base.observe) {
|
|
517
|
+
transport.observe = (refs, node, context) => base.observe(refs, node, context);
|
|
518
|
+
}
|
|
519
|
+
return transport;
|
|
516
520
|
}
|
|
517
521
|
function normalizeUiWaitNode(node) {
|
|
518
522
|
if (node.expected !== void 0 || typeof node.visible !== "boolean") return node;
|
|
@@ -64,24 +64,29 @@ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManif
|
|
|
64
64
|
record: true,
|
|
65
65
|
cdpPort: runtimeOptions.cdpPort
|
|
66
66
|
}) : void 0;
|
|
67
|
+
let result;
|
|
67
68
|
try {
|
|
68
|
-
|
|
69
|
+
result = await runner.run(runRequest);
|
|
70
|
+
} catch (error) {
|
|
69
71
|
try {
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
projectRoot,
|
|
76
|
-
recipeRunEnv(adapter, runtimeOptions)
|
|
77
|
-
);
|
|
78
|
-
}
|
|
72
|
+
await stopRecipeRecording(recording);
|
|
73
|
+
} catch (recordingError) {
|
|
74
|
+
console.error(
|
|
75
|
+
`WARN: recipe and video recording both failed; preserving recipe failure: ${recordingError instanceof Error ? recordingError.message : String(recordingError)}`
|
|
76
|
+
);
|
|
79
77
|
}
|
|
80
|
-
|
|
81
|
-
return finishRunDiagnostics(diagnosticBaseline, result);
|
|
78
|
+
throw error;
|
|
82
79
|
} finally {
|
|
83
|
-
|
|
80
|
+
if (adapter === "mobile") {
|
|
81
|
+
const { hideMobileHudOnTeardown } = await import("../adapters.js");
|
|
82
|
+
await hideMobileHudOnTeardown(
|
|
83
|
+
projectRoot,
|
|
84
|
+
recipeRunEnv(adapter, runtimeOptions)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
84
87
|
}
|
|
88
|
+
await stopRecipeRecording(recording, result);
|
|
89
|
+
return finishRunDiagnostics(diagnosticBaseline, result);
|
|
85
90
|
} finally {
|
|
86
91
|
restoreRuntimeEnvironment();
|
|
87
92
|
}
|
|
@@ -89,6 +89,10 @@ async function handleRuntimeLaunch({ options }) {
|
|
|
89
89
|
return 0;
|
|
90
90
|
}
|
|
91
91
|
const launchLogPath = path.join(artifactsDir, "launch", "logs", "launch.log");
|
|
92
|
+
const verifySummaryPath = path.join(artifactsDir, "verify", "summary.json");
|
|
93
|
+
const launchSucceeded = isRecord(summary) && isRecord(summary.launch) && summary.launch.exitCode === 0;
|
|
94
|
+
const failurePath = launchSucceeded && fs.existsSync(verifySummaryPath) ? verifySummaryPath : fs.existsSync(launchLogPath) ? launchLogPath : summaryPath;
|
|
95
|
+
const reason = launchSucceeded ? "runtime_verify_failed" : "runtime_launch_failed";
|
|
92
96
|
const report = runtimeLaunchReport("fail", {
|
|
93
97
|
adapter,
|
|
94
98
|
target,
|
|
@@ -96,8 +100,9 @@ async function handleRuntimeLaunch({ options }) {
|
|
|
96
100
|
artifactsDir,
|
|
97
101
|
summaryPath: fs.existsSync(summaryPath) ? summaryPath : void 0,
|
|
98
102
|
launchLogPath: fs.existsSync(launchLogPath) ? launchLogPath : void 0,
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
verifySummaryPath: fs.existsSync(verifySummaryPath) ? verifySummaryPath : void 0,
|
|
104
|
+
reason,
|
|
105
|
+
fix: `Read ${failurePath}, fix the first error, then rerun: ${runtimeLaunchCommand(target, cdpPort, chromeUserDataDir, startWatch)}`,
|
|
101
106
|
command,
|
|
102
107
|
exitCode: result.status ?? 1
|
|
103
108
|
});
|
|
@@ -350,11 +350,13 @@ function liveAdapterProcessTimeoutMs(node) {
|
|
|
350
350
|
const timeoutMs = Number(node.live_adapter_timeout_ms);
|
|
351
351
|
return Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 6e4;
|
|
352
352
|
}
|
|
353
|
+
const settleMs = Number(node.settle_ms);
|
|
354
|
+
const settleAllowance = Number.isFinite(settleMs) && settleMs > 0 ? settleMs : 0;
|
|
353
355
|
if (node.timeout_ms != null) {
|
|
354
356
|
const timeoutMs = Number(node.timeout_ms);
|
|
355
|
-
return Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs + 5e3 : 6e4;
|
|
357
|
+
return Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs + settleAllowance + 5e3 : 6e4;
|
|
356
358
|
}
|
|
357
|
-
return 6e4;
|
|
359
|
+
return 6e4 + settleAllowance;
|
|
358
360
|
}
|
|
359
361
|
function commandForPrepared(projectRoot, platform) {
|
|
360
362
|
if (platform !== "core") {
|
package/dist/recipe-security.js
CHANGED
|
@@ -9,7 +9,11 @@ const READ_ONLY_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
9
9
|
"metamask.perps.read_orders",
|
|
10
10
|
"metamask.perps.read_account",
|
|
11
11
|
"metamask.perps.assert_positions",
|
|
12
|
-
"metamask.perps.assert_orders"
|
|
12
|
+
"metamask.perps.assert_orders",
|
|
13
|
+
// Collector reads are host-only; starting capture remains arbitrary-code
|
|
14
|
+
// because it spawns an HTTP listener.
|
|
15
|
+
"metamask.analytics.read_events",
|
|
16
|
+
"metamask.analytics.assert_events"
|
|
13
17
|
]);
|
|
14
18
|
const APP_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
15
19
|
"metamask.wallet.setup",
|
|
@@ -18,16 +22,20 @@ const APP_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
18
22
|
"metamask.perps.close_positions",
|
|
19
23
|
"metamask.perps.close_orders",
|
|
20
24
|
"metamask.perps.place_order",
|
|
25
|
+
"metamask.perps.edit_order",
|
|
21
26
|
"metamask.perps.update_position_tpsl",
|
|
22
27
|
"metamask.perps.ensure_positions",
|
|
23
28
|
"metamask.perps.ensure_orders",
|
|
24
29
|
"metamask.perps.start_state",
|
|
25
|
-
"metamask.perps.teardown_state"
|
|
30
|
+
"metamask.perps.teardown_state",
|
|
31
|
+
// Consent changes wallet state and is therefore an app mutation.
|
|
32
|
+
"metamask.analytics.set_consent"
|
|
26
33
|
]);
|
|
27
34
|
const EXTERNAL_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
28
35
|
"metamask.perps.close_positions",
|
|
29
36
|
"metamask.perps.close_orders",
|
|
30
37
|
"metamask.perps.place_order",
|
|
38
|
+
"metamask.perps.edit_order",
|
|
31
39
|
"metamask.perps.update_position_tpsl",
|
|
32
40
|
"metamask.perps.ensure_positions",
|
|
33
41
|
"metamask.perps.ensure_orders",
|