@deeeed/metamask-harness 0.79.0 → 0.81.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 +73 -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 +16 -64
- 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/harness-source-fingerprint.mjs +13 -8
- package/adapters/shared/open-debug.mjs +2 -2
- 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/launch.mjs +17 -13
- package/adapters/terminal/lib/mainnet-guard.mjs +9 -0
- package/adapters/terminal/lib/readiness.mjs +20 -12
- package/adapters/terminal/policy.mjs +35 -0
- package/adapters/terminal/verify.mjs +10 -3
- package/adapters/terminal/wallet-host.mjs +43 -225
- 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 +8 -4
- package/dist/adapters/core/overlay.js +1 -1
- package/dist/adapters/core/readiness.js +28 -1
- package/dist/adapters/core/run.js +2 -2
- package/dist/adapters/core/surface.js +16 -1
- package/dist/adapters/extension/actions.js +6 -2
- 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 +14 -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 +135 -14
- package/dist/adapters/extension/runtime-decision.js +2 -1
- package/dist/adapters/extension/runtime.js +9 -5
- package/dist/adapters/extension/surface.js +15 -2
- package/dist/adapters/extension/verify.js +2 -2
- package/dist/adapters/harness-freshness.js +2 -1
- package/dist/adapters/mobile/actions.js +4 -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/network-observer.js +68 -0
- package/dist/adapters/mobile/performance-observer.js +2 -4
- package/dist/adapters/mobile/prepare-target.js +1 -1
- package/dist/adapters/mobile/prepare.js +3 -4
- 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 +39 -15
- package/dist/adapters/mobile/runtime-decision.js +2 -4
- package/dist/adapters/mobile/source-freshness.js +22 -126
- package/dist/adapters/mobile/surface.js +26 -3
- 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/perps-order-inputs.js +36 -0
- package/dist/adapters/proof-failure.js +11 -0
- package/dist/adapters/slot-ports.js +1 -1
- package/dist/adapters/surface.js +1 -0
- package/dist/adapters/terminal/actions.js +4 -1
- package/dist/adapters/terminal/console-capture.js +1 -1
- package/dist/adapters/terminal/doctor-checks.js +2 -0
- package/dist/adapters/terminal/launch.js +4 -1
- package/dist/adapters/terminal/overlay.js +8 -1
- package/dist/adapters/terminal/run.js +33 -5
- package/dist/adapters/terminal/surface.js +9 -1
- package/dist/adapters.js +27 -7
- package/dist/advisor-policy.js +1 -1
- package/dist/checkout-view.js +1 -2
- package/dist/cli.js +59 -21
- package/dist/command-contract.js +1 -26
- package/dist/commands/check.js +33 -10
- package/dist/commands/checklist.js +1 -1
- package/dist/commands/completion-candidates.js +6 -14
- 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 +2 -8
- package/dist/commands/launch/mobile.js +1 -1
- package/dist/commands/logs.js +3 -3
- 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 +4 -9
- 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/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 +5 -8
- 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/doctor.js +2 -3
- package/dist/engine.js +57 -0
- package/dist/funding-execution-context.js +96 -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 +4 -6
- package/dist/paths.js +34 -64
- package/dist/performance-observation.js +3 -5
- package/dist/progress.js +1 -1
- package/dist/recipe-assistance-context.js +6 -9
- package/dist/recipe-security.js +2 -59
- package/dist/review/knowledge.js +2 -2
- package/dist/runner.js +7 -4
- package/dist/runtime-context.js +1 -1
- package/dist/task-view.js +2 -3
- package/docs/CONTRIBUTING.md +5 -3
- package/docs/RECIPES.md +31 -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 -134
- package/library/actions/core/perps/_rewards.mjs +186 -0
- package/library/actions/core/perps/close_positions.mjs +1 -1
- package/library/actions/core/perps/place_order.mjs +167 -45
- package/library/actions/core/perps/quote_fees.mjs +61 -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/perps/close_visible_position.mjs +5 -0
- package/library/actions/extension/perps/edit_margin.mjs +4 -0
- package/library/actions/extension/perps/mutation-receipt.mjs +95 -3
- package/library/actions/extension/perps/perps.mjs +320 -3
- package/library/actions/extension/perps/place_order.mjs +3 -1
- package/library/actions/extension/perps/update_position_tpsl.mjs +4 -0
- package/library/actions/extension/platform/cdp.mjs +73 -11
- package/library/actions/harness-exports.mjs +1 -6
- package/library/actions/mobile/app/network-control.mjs +6 -8
- package/library/actions/mobile/perps/perps.mjs +216 -11
- package/library/actions/mobile/perps/place_order.mjs +3 -1
- 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/shared/perps/order-policy.mjs +177 -0
- package/library/actions/shared/wallet/fixture-accounts.mjs +84 -0
- package/library/actions/terminal/app/launch.mjs +3 -2
- package/library/actions/terminal/platform/hud.mjs +2 -1
- package/library/actions/terminal/platform/page.mjs +2 -1
- package/library/actions/terminal/platform/runtime.mjs +2 -1
- package/library/actions/terminal/wallet/_signature-log.mjs +40 -161
- package/library/actions/terminal/wallet/assert_signatures.mjs +6 -12
- package/library/actions/terminal/wallet/read_signatures.mjs +10 -9
- package/library/manifests/core.action-manifest.json +87 -13
- package/library/manifests/extension.action-manifest.json +4 -0
- package/library/manifests/mobile.action-manifest.json +12 -0
- package/package.json +8 -9
- package/site/assets/help-recipes.json +1 -0
- 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/adapters/terminal/lib/origin.mjs +0 -30
- package/adapters/terminal/lib/page-script.mjs +0 -246
- package/adapters/terminal/lib/strict-wallet.mjs +0 -138
- 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/call.js +0 -685
- package/dist/commands/last.js +0 -60
- package/dist/commands/launch/index.js +0 -109
- package/dist/commands/list-executables.js +0 -247
- package/dist/commands/manifest.js +0 -488
- package/dist/commands/parse-args.js +0 -279
- package/dist/commands/reload.js +0 -41
- package/dist/commands/run-engine.js +0 -1340
- package/dist/commands/run-report.js +0 -241
- package/dist/commands/run.js +0 -751
- package/dist/commands/shared.js +0 -323
- package/dist/commands/stop.js +0 -60
- package/dist/devices.js +0 -100
- package/dist/discovery.js +0 -15
- 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/metamask-action-validation.js +0 -68
- package/dist/network-observation.js +0 -290
- 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
|
@@ -14,6 +14,14 @@ import {
|
|
|
14
14
|
import { ensureUnlocked } from '../wallet/ensure_unlocked.mjs';
|
|
15
15
|
import { selectAccount } from '../wallet/select_account.mjs';
|
|
16
16
|
import { observeNativeUi } from '../platform/observe-ui.mjs';
|
|
17
|
+
import {
|
|
18
|
+
assertFlatBeforeRoundTrip,
|
|
19
|
+
assertHouseLimitPosition,
|
|
20
|
+
closeImmediatelyRequested,
|
|
21
|
+
orderNetwork,
|
|
22
|
+
requireHouseNotional,
|
|
23
|
+
requirePerpsOrderAllowed,
|
|
24
|
+
} from '../../shared/perps/order-policy.mjs';
|
|
17
25
|
|
|
18
26
|
function sleep(ms) {
|
|
19
27
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -1281,7 +1289,7 @@ function closePositionParams(input, position) {
|
|
|
1281
1289
|
return params;
|
|
1282
1290
|
}
|
|
1283
1291
|
|
|
1284
|
-
async function closePositionItem(input, position) {
|
|
1292
|
+
async function closePositionItem(input, position, bound = null) {
|
|
1285
1293
|
const maxAttempts = Number(input.node?.close_attempts ?? 3);
|
|
1286
1294
|
const retryDelayMs = Number(input.node?.close_retry_delay_ms ?? 1000);
|
|
1287
1295
|
const deadline = deadlineFromTimeout(
|
|
@@ -1293,7 +1301,7 @@ async function closePositionItem(input, position) {
|
|
|
1293
1301
|
let lastResult = null;
|
|
1294
1302
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
1295
1303
|
if (Date.now() >= deadline) break;
|
|
1296
|
-
const currentPositions = await readPositions(input);
|
|
1304
|
+
const currentPositions = bound ? await readBoundPositions(input, bound) : await readPositions(input);
|
|
1297
1305
|
const currentPosition = currentPositions.find((candidate) => symbolForItem(candidate) === baseParams.symbol);
|
|
1298
1306
|
if (!currentPosition) return { symbol: baseParams.symbol, result: { success: true, alreadyClosed: true }, attempts };
|
|
1299
1307
|
|
|
@@ -1301,8 +1309,12 @@ async function closePositionItem(input, position) {
|
|
|
1301
1309
|
const params = { ...baseParams, position: currentPosition, priceAtCalculation };
|
|
1302
1310
|
const result = await evalAsync(
|
|
1303
1311
|
input,
|
|
1304
|
-
`
|
|
1312
|
+
`(function(){ ${boundContextGuard(bound)}
|
|
1313
|
+
return Engine.context.PerpsController.closePosition(${sourceLiteral(params)}).then(function(r){return JSON.stringify(r)}); })()`,
|
|
1305
1314
|
);
|
|
1315
|
+
if (result?.contextChanged) {
|
|
1316
|
+
throw new Error(`Close of ${baseParams.symbol} stopped: the selected account or network changed; nothing more was sent.`);
|
|
1317
|
+
}
|
|
1306
1318
|
attempts.push({ attempt, priceAtCalculation, result });
|
|
1307
1319
|
lastResult = result;
|
|
1308
1320
|
if (result?.success === false) {
|
|
@@ -1394,7 +1406,37 @@ export async function placeOrder(input) {
|
|
|
1394
1406
|
const maxSlippageBps = Number(input.node?.max_slippage_bps ?? input.node?.maxSlippageBps ?? 300);
|
|
1395
1407
|
const isBuy = orderSideIsBuy(side);
|
|
1396
1408
|
const orderType = resolveOrderType(input);
|
|
1409
|
+
const closeImmediately = closeImmediatelyRequested(input.node);
|
|
1410
|
+
const runtime = await readPerpsRuntimeState(input);
|
|
1411
|
+
const network = orderNetwork({ declared: input.node?.network, liveIsTestnet: runtime?.isTestnet });
|
|
1412
|
+
// The account and network the rule allows, rechecked at dispatch and before any close.
|
|
1413
|
+
const bound = { network, address: String((await readSelectedAddress(input)) ?? '').toLowerCase() };
|
|
1414
|
+
const policy = await requirePerpsOrderAllowed({
|
|
1415
|
+
network,
|
|
1416
|
+
amount,
|
|
1417
|
+
leverage,
|
|
1418
|
+
orderType,
|
|
1419
|
+
closeImmediately,
|
|
1420
|
+
projectRoot: input.context?.projectRoot,
|
|
1421
|
+
selectedAddress: bound.address,
|
|
1422
|
+
});
|
|
1423
|
+
if ((closeImmediately || network === 'mainnet') && input.node?.size != null) {
|
|
1424
|
+
throw new Error('close_immediately and mainnet orders are sized from amount; drop size.');
|
|
1425
|
+
}
|
|
1426
|
+
if (closeImmediately) {
|
|
1427
|
+
const book = await readBoundBook(input, bound);
|
|
1428
|
+
assertFlatBeforeRoundTrip(book.positions, symbol, symbolForItem, book.orders);
|
|
1429
|
+
}
|
|
1397
1430
|
const currentPrice = await currentPriceForOrder(input, symbol);
|
|
1431
|
+
const houseNotional = requireHouseNotional({
|
|
1432
|
+
network,
|
|
1433
|
+
symbol,
|
|
1434
|
+
amount,
|
|
1435
|
+
leverage,
|
|
1436
|
+
price: currentPrice,
|
|
1437
|
+
szDecimals: network === 'mainnet' ? await readSizeDecimals(input, symbol) : null,
|
|
1438
|
+
maxSlippageBps,
|
|
1439
|
+
});
|
|
1398
1440
|
const limitPrice = orderType === 'limit'
|
|
1399
1441
|
? resolveLimitPrice(input, isBuy, currentPrice)
|
|
1400
1442
|
: null;
|
|
@@ -1426,16 +1468,179 @@ export async function placeOrder(input) {
|
|
|
1426
1468
|
priceAtCalculation: currentPrice,
|
|
1427
1469
|
maxSlippageBps,
|
|
1428
1470
|
};
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1471
|
+
if (await tradingContextChanged(input, bound)) {
|
|
1472
|
+
throw new Error('Perps order not dispatched: the selected account or network changed after the order rule allowed it.');
|
|
1473
|
+
}
|
|
1474
|
+
let result;
|
|
1475
|
+
try {
|
|
1476
|
+
result = await evalAsync(input, `(function(){ ${boundContextGuard(bound)}
|
|
1477
|
+
return Engine.context.PerpsController.placeOrder(${sourceLiteral(orderParams)}).then(function(r){return JSON.stringify(r)}); })()`);
|
|
1478
|
+
if (result?.contextChanged) {
|
|
1479
|
+
throw Object.assign(
|
|
1480
|
+
new Error('Perps order not dispatched: the selected account or network changed after the order rule allowed it.'),
|
|
1481
|
+
{ knownNotDispatched: true },
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
if (result?.success === false || result == null) {
|
|
1485
|
+
throw new Error(`Failed to place ${symbol} ${side}: ${result?.error || JSON.stringify(result)}`);
|
|
1486
|
+
}
|
|
1487
|
+
if (orderType === 'limit') {
|
|
1488
|
+
await waitForSelectedState(input, readOpenOrders, true, Number(input.node?.timeout_ms ?? 30000));
|
|
1489
|
+
} else {
|
|
1490
|
+
await waitForPositionPresent(input, symbol, Number(input.node?.timeout_ms ?? 30000));
|
|
1491
|
+
}
|
|
1492
|
+
} catch (error) {
|
|
1493
|
+
if (!closeImmediately || error?.knownNotDispatched) throw error;
|
|
1494
|
+
throw await closeAfterFailedPlacement(input, symbol, error, bound);
|
|
1495
|
+
}
|
|
1496
|
+
const placed = { action: input.action, market: symbol, side, orderType, amount, size, leverage, limitPrice, submitted: true, result, network, policy: policy.lane, ...(houseNotional ? { houseNotional } : {}), proofPath: 'mobile-perps-controller-place-order' };
|
|
1497
|
+
if (!closeImmediately) return placed;
|
|
1498
|
+
return { ...placed, roundTrip: await closeOpenedPosition(input, symbol, bound) };
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// close_immediately started from a flat market (checked before dispatch), so a
|
|
1502
|
+
// position in it after a failed placement is this order's: close it rather
|
|
1503
|
+
// than leave it open, and say what happened.
|
|
1504
|
+
async function closeAfterFailedPlacement(input, symbol, error, bound) {
|
|
1505
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1506
|
+
let outcome;
|
|
1507
|
+
try {
|
|
1508
|
+
if (await tradingContextChanged(input, bound)) {
|
|
1509
|
+
return new Error(`${message} close_immediately: the selected account or network changed, so nothing was closed; check ${symbol} on the original account now.`);
|
|
1510
|
+
}
|
|
1511
|
+
const position = (await readBoundPositions(input, bound)).find((candidate) => symbolForItem(candidate) === symbol);
|
|
1512
|
+
if (!position) {
|
|
1513
|
+
outcome = `no ${symbol} position is open`;
|
|
1514
|
+
} else {
|
|
1515
|
+
await closePositionItem(fullCloseInput(input), position, bound);
|
|
1516
|
+
outcome = await waitForBoundPositionAbsent(input, bound, symbol, Number(input.node?.timeout_ms ?? 30000))
|
|
1517
|
+
? `the ${symbol} position it opened was closed`
|
|
1518
|
+
: `the ${symbol} position is STILL OPEN after a close; close it now`;
|
|
1519
|
+
}
|
|
1520
|
+
} catch (closeError) {
|
|
1521
|
+
outcome = `the ${symbol} position could not be checked or closed (${closeError instanceof Error ? closeError.message : String(closeError)}); check it now`;
|
|
1522
|
+
}
|
|
1523
|
+
return new Error(`${message} close_immediately: ${outcome}.`);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
async function readSizeDecimals(input, symbol) {
|
|
1527
|
+
const markets = await evalAsync(
|
|
1528
|
+
input,
|
|
1529
|
+
`Engine.context.PerpsController.getMarkets({ symbols: [${sourceLiteral(symbol)}] }).then(function(r){return JSON.stringify((r||[]).map(function(m){return { name: m.name, szDecimals: m.szDecimals };}))})`,
|
|
1530
|
+
);
|
|
1531
|
+
return (Array.isArray(markets) ? markets : []).find((market) => normalizeMarketSymbol(market?.name ?? '') === symbol)?.szDecimals ?? null;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// The close takes the whole position: the order's own size or amount never
|
|
1535
|
+
// becomes a partial close size.
|
|
1536
|
+
function fullCloseInput(input) {
|
|
1537
|
+
const { size: _size, amount: _amount, notional: _notional, ...node } = input.node ?? {};
|
|
1538
|
+
return { ...input, node };
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// Bridge sources embed values as JSON literals. JSON.stringify leaves `<`, `>`,
|
|
1542
|
+
// `/` and U+2028/U+2029 raw; escape them so no value can end a script context
|
|
1543
|
+
// or a statement in the evaluated source.
|
|
1544
|
+
const UNSAFE_SOURCE_CHARS = { '<': '\\u003C', '>': '\\u003E', '/': '\\u002F', '\u2028': '\\u2028', '\u2029': '\\u2029' };
|
|
1545
|
+
export function sourceLiteral(value) {
|
|
1546
|
+
return JSON.stringify(value).replace(/[<>/\u2028\u2029]/gu, (char) => UNSAFE_SOURCE_CHARS[char]);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// Bridge source that resolves to { contextChanged: true } unless the selected
|
|
1550
|
+
// account and Perps network are still the bound ones. It runs in the same
|
|
1551
|
+
// evaluation as the controller call it guards, so a switch can't slip between.
|
|
1552
|
+
function boundContextGuard(bound) {
|
|
1553
|
+
if (!bound) return '';
|
|
1554
|
+
return `var __account = Engine.context.AccountsController.getSelectedAccount();
|
|
1555
|
+
var __address = String((__account && __account.address) || '').toLowerCase();
|
|
1556
|
+
var __network = Engine.context.PerpsController.state.isTestnet ? 'testnet' : 'mainnet';
|
|
1557
|
+
if (__address !== ${sourceLiteral(bound.address)} || __network !== ${sourceLiteral(bound.network)}) {
|
|
1558
|
+
return Promise.resolve(JSON.stringify({ success: false, contextChanged: true }));
|
|
1559
|
+
}`;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// The positions of the bound account on the bound network, read in one guarded
|
|
1563
|
+
// evaluation; a changed context throws instead of reading another account.
|
|
1564
|
+
async function readBoundPositions(input, bound) {
|
|
1565
|
+
const positions = await evalAsync(input, `(function(){ ${boundContextGuard(bound)}
|
|
1566
|
+
return Engine.context.PerpsController.getPositions().then(function(r){return JSON.stringify(r)}); })()`);
|
|
1567
|
+
if (positions?.contextChanged) {
|
|
1568
|
+
throw new Error('Perps positions not read: the selected account or network changed after the order rule allowed it. Check the order\'s market on the original account now.');
|
|
1569
|
+
}
|
|
1570
|
+
if (!Array.isArray(positions)) throw new Error('PerpsController.getPositions returned a non-array result.');
|
|
1571
|
+
return positions;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// The flat-market baseline: the bound account's positions and resting orders,
|
|
1575
|
+
// read in one evaluation with the context guard before, between and after the
|
|
1576
|
+
// reads, so a switch to another account's book and back can't pass as flat.
|
|
1577
|
+
async function readBoundBook(input, bound) {
|
|
1578
|
+
const guard = `(function(){ ${boundContextGuard(bound)} return null; })()`;
|
|
1579
|
+
const book = await evalAsync(input, `(function(){
|
|
1580
|
+
var controller = Engine.context.PerpsController;
|
|
1581
|
+
var positions;
|
|
1582
|
+
return ${guard} || controller.getPositions().then(function(r){
|
|
1583
|
+
positions = r;
|
|
1584
|
+
return ${guard} || controller.getOpenOrders().then(function(orders){
|
|
1585
|
+
return ${guard} || JSON.stringify({ positions: positions, orders: orders });
|
|
1586
|
+
});
|
|
1587
|
+
}); })()`);
|
|
1588
|
+
if (book?.contextChanged) {
|
|
1589
|
+
throw new Error('Perps order not dispatched: the selected account or network changed while the market was checked flat.');
|
|
1590
|
+
}
|
|
1591
|
+
if (!Array.isArray(book?.positions) || !Array.isArray(book?.orders)) {
|
|
1592
|
+
throw new Error('PerpsController.getPositions or getOpenOrders returned a non-array result.');
|
|
1593
|
+
}
|
|
1594
|
+
return book;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
async function waitForBoundPositionAbsent(input, bound, symbol, timeoutMs) {
|
|
1598
|
+
const deadline = Date.now() + timeoutMs;
|
|
1599
|
+
for (;;) {
|
|
1600
|
+
const positions = await readBoundPositions(input, bound);
|
|
1601
|
+
if (!positions.some((position) => symbolForItem(position) === symbol)) return true;
|
|
1602
|
+
if (Date.now() >= deadline) return false;
|
|
1603
|
+
await sleep(1000);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
async function tradingContextChanged(input, bound) {
|
|
1608
|
+
const runtime = await readPerpsRuntimeState(input);
|
|
1609
|
+
const network = runtime?.isTestnet === true ? 'testnet' : runtime?.isTestnet === false ? 'mainnet' : null;
|
|
1610
|
+
const address = String((await readSelectedAddress(input)) ?? '').toLowerCase();
|
|
1611
|
+
return network !== bound.network || address !== bound.address;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
async function readSelectedAddress(input) {
|
|
1615
|
+
return evalAsync(input, 'JSON.stringify(Engine.context.AccountsController.getSelectedAccount()?.address ?? null)');
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// close_immediately: close the position this order opened and prove the market
|
|
1619
|
+
// is flat. On mainnet the opened position must be isolated 1x; it is closed
|
|
1620
|
+
// either way before that check fails the action.
|
|
1621
|
+
async function closeOpenedPosition(input, symbol, bound) {
|
|
1622
|
+
const { network } = bound;
|
|
1623
|
+
if (await tradingContextChanged(input, bound)) {
|
|
1624
|
+
throw new Error(`close_immediately did not close ${symbol}: the selected account or network changed after the order. The order's position is still open on the original account; close it now.`);
|
|
1625
|
+
}
|
|
1626
|
+
const position = (await readBoundPositions(input, bound)).find((candidate) => symbolForItem(candidate) === symbol);
|
|
1627
|
+
if (!position) {
|
|
1628
|
+
throw new Error(`close_immediately could not find the ${symbol} position the order opened; it may still fill. Check ${symbol} now and close it if it opened.`);
|
|
1629
|
+
}
|
|
1630
|
+
let limitError = null;
|
|
1631
|
+
if (network === 'mainnet') {
|
|
1632
|
+
try {
|
|
1633
|
+
assertHouseLimitPosition(position, symbol);
|
|
1634
|
+
} catch (error) {
|
|
1635
|
+
limitError = error;
|
|
1636
|
+
}
|
|
1432
1637
|
}
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
await waitForPositionPresent(input, symbol, Number(input.node?.timeout_ms ?? 30000));
|
|
1638
|
+
const closed = await closePositionItem(fullCloseInput(input), position, bound);
|
|
1639
|
+
if (!(await waitForBoundPositionAbsent(input, bound, symbol, Number(input.node?.timeout_ms ?? 30000)))) {
|
|
1640
|
+
throw new Error(`close_immediately left the ${symbol} position open: ${JSON.stringify(redactPosition(position))}. Close it now.`);
|
|
1437
1641
|
}
|
|
1438
|
-
|
|
1642
|
+
if (limitError) throw limitError;
|
|
1643
|
+
return { opened: redactPosition(position), closed: true, result: closed.result };
|
|
1439
1644
|
}
|
|
1440
1645
|
|
|
1441
1646
|
export async function ensurePositions(input) {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Submit a Perps order through the Mobile controller and assert a matching open position.
|
|
2
|
+
// With close_immediately the order's position is already closed again, so the market must be flat.
|
|
2
3
|
import { runAdapter } from '../platform/bridge.mjs';
|
|
4
|
+
import { closeImmediatelyRequested } from '../../shared/perps/order-policy.mjs';
|
|
3
5
|
import { assertPositions, placeOrder } from './perps.mjs';
|
|
4
6
|
runAdapter(async (input) => {
|
|
5
7
|
const order = await placeOrder(input);
|
|
6
|
-
const assertion = await assertPositions(input,
|
|
8
|
+
const assertion = await assertPositions(input, !closeImmediatelyRequested(input.node));
|
|
7
9
|
return { ...assertion, submitted: order.submitted === true, order };
|
|
8
10
|
});
|
|
@@ -4,7 +4,7 @@ import { execFile } from 'node:child_process';
|
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
|
|
6
6
|
import { runAdapter } from '../platform/bridge.mjs';
|
|
7
|
-
import { resolveMobileToolPath } from '
|
|
7
|
+
import { resolveMobileToolPath } from '@farmslot/adapter-rn';
|
|
8
8
|
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
10
|
|
|
@@ -8,10 +8,10 @@ import { promisify } from 'node:util';
|
|
|
8
8
|
import os from 'node:os';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
|
-
import bridgeErrors from '
|
|
12
|
-
import brokerModule from '
|
|
13
|
-
import configModule from '
|
|
14
|
-
import { resolveMobileToolPath } from '
|
|
11
|
+
import bridgeErrors from '@farmslot/adapter-rn/bridge-runtime/lib/bridge-errors.cjs';
|
|
12
|
+
import brokerModule from '@farmslot/recipe-runner/cdp-broker';
|
|
13
|
+
import configModule from '@farmslot/adapter-rn/bridge-runtime/lib/config.cjs';
|
|
14
|
+
import { resolveMobileToolPath } from '@farmslot/adapter-rn';
|
|
15
15
|
|
|
16
16
|
const {
|
|
17
17
|
BRIDGE_ERROR_CODES,
|
|
@@ -155,8 +155,8 @@ export async function bridgeEnv(input) {
|
|
|
155
155
|
...(input.context?.env || {}),
|
|
156
156
|
CDP_TIMEOUT: String(input.node?.cdp_timeout_ms ?? process.env.CDP_TIMEOUT ?? '10000'),
|
|
157
157
|
};
|
|
158
|
-
if (adbPath) env.
|
|
159
|
-
if (idbPath) env.
|
|
158
|
+
if (adbPath) env.RECIPE_RN_ADB_PATH = adbPath;
|
|
159
|
+
if (idbPath) env.RECIPE_RN_IDB_PATH = idbPath;
|
|
160
160
|
const target = resolveMobileTarget(input);
|
|
161
161
|
const requestedPlatform = target.requestedPlatform;
|
|
162
162
|
const watcherPort = target.watcherPort;
|
|
@@ -238,8 +238,8 @@ export function resolveMobileTarget(input) {
|
|
|
238
238
|
input.node?.sim_udid ?? contextEnv.SIM_UDID ?? process.env.SIM_UDID;
|
|
239
239
|
const configuredPlatform = String(
|
|
240
240
|
input.node?.platform ??
|
|
241
|
-
contextEnv.
|
|
242
|
-
process.env.
|
|
241
|
+
contextEnv.RECIPE_RN_EXPLICIT_PLATFORM ??
|
|
242
|
+
process.env.RECIPE_RN_EXPLICIT_PLATFORM ??
|
|
243
243
|
contextEnv.PLATFORM ??
|
|
244
244
|
process.env.PLATFORM ??
|
|
245
245
|
'',
|
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
// Derive native session names and device-specific state directories, honoring configured overrides.
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// The host adapter supplies the app under test in the action env
|
|
5
|
+
// (ANDROID_PACKAGE_ID / IOS_BUNDLE_ID, display name in
|
|
6
|
+
// METAMASK_RECIPE_MOBILE_APP_LABEL); the library has no default app.
|
|
7
|
+
export function nativeAppId(env, platform) {
|
|
8
|
+
const key = platform === 'android' ? 'ANDROID_PACKAGE_ID' : 'IOS_BUNDLE_ID';
|
|
9
|
+
const app = String(env[key] ?? '').trim();
|
|
10
|
+
if (!app) {
|
|
11
|
+
throw new Error(`Mobile native UI needs the app identity in ${key}. Next: run through mm-harness run or call, or set ${key}.`);
|
|
12
|
+
}
|
|
13
|
+
return app;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function nativeAppLabel(env) {
|
|
17
|
+
const label = String(env.METAMASK_RECIPE_MOBILE_APP_LABEL ?? '').trim();
|
|
18
|
+
if (!label) {
|
|
19
|
+
throw new Error('Mobile native UI needs the app display name in METAMASK_RECIPE_MOBILE_APP_LABEL. Next: run through mm-harness run or call, or set METAMASK_RECIPE_MOBILE_APP_LABEL.');
|
|
20
|
+
}
|
|
21
|
+
return label;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function nativeSessionName(env, device, app) {
|
|
5
25
|
const configured = String(env.METAMASK_RECIPE_MOBILE_SESSION ?? '').trim();
|
|
6
26
|
const source = configured || `mm-harness-${app}-${device}`;
|
|
7
27
|
return source.replace(/[^A-Za-z0-9._-]/gu, '-').slice(0, 120);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Create device-targeted native UI sessions for actions and accessibility snapshots.
|
|
2
2
|
// Opening an instrumented iOS session hides the accessibility-masking HUD.
|
|
3
|
-
import { createAgentDeviceUiTransport } from '@farmslot/
|
|
3
|
+
import { createAgentDeviceUiTransport } from '@farmslot/adapter-rn';
|
|
4
4
|
import { createAgentDeviceClient } from 'agent-device';
|
|
5
5
|
|
|
6
6
|
import { resolveMobileTarget } from './bridge.mjs';
|
|
@@ -8,6 +8,7 @@ import { hideAccessibilityMaskingHud } from './observe-ui.mjs';
|
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
nativeAgentDeviceStateDir,
|
|
11
|
+
nativeAppId,
|
|
11
12
|
nativeSessionName,
|
|
12
13
|
} from './native-session-name.mjs';
|
|
13
14
|
|
|
@@ -19,9 +20,7 @@ export function createNativeSession(input, domain, sessionSuffix = '') {
|
|
|
19
20
|
? target.adbSerial ?? ''
|
|
20
21
|
: input.node?.sim_udid ?? env.SIM_UDID ?? target.iosSimulator ?? '').trim();
|
|
21
22
|
if (!device) throw new Error(`Mobile ${domain} native actions require one explicit ${platform} device. Next: pass --device <device>.`);
|
|
22
|
-
const app =
|
|
23
|
-
? env.ANDROID_PACKAGE_ID ?? 'io.metamask'
|
|
24
|
-
: env.IOS_BUNDLE_ID ?? 'io.metamask.MetaMask');
|
|
23
|
+
const app = nativeAppId(env, platform);
|
|
25
24
|
const baseSession = nativeSessionName(env, device, app);
|
|
26
25
|
const session = sessionSuffix
|
|
27
26
|
? `${baseSession}-${String(sessionSuffix).replace(/[^A-Za-z0-9._-]/gu, '-')}`.slice(0, 120)
|
|
@@ -7,9 +7,11 @@ import { createAgentDeviceClient } from 'agent-device';
|
|
|
7
7
|
import { bridgeCommand } from './bridge.mjs';
|
|
8
8
|
import {
|
|
9
9
|
nativeAgentDeviceStateDir,
|
|
10
|
+
nativeAppId,
|
|
11
|
+
nativeAppLabel,
|
|
10
12
|
nativeSessionName,
|
|
11
13
|
} from './native-session-name.mjs';
|
|
12
|
-
import { mobileToolRecovery, resolveMobileToolPath } from '
|
|
14
|
+
import { mobileToolRecovery, resolveMobileToolPath } from '@farmslot/adapter-rn';
|
|
13
15
|
|
|
14
16
|
const execFileAsync = promisify(execFile);
|
|
15
17
|
const VISIBLE_LIMIT = 200;
|
|
@@ -93,8 +95,13 @@ async function readNativeHierarchy(payload, context) {
|
|
|
93
95
|
env.METAMASK_RECIPE_PROJECT_ROOT = String(context.projectRoot);
|
|
94
96
|
}
|
|
95
97
|
const platform = resolvePlatform(node, env);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
const appLabel = nativeAppLabel(env);
|
|
99
|
+
const hierarchy = platform === 'android'
|
|
100
|
+
? await readAndroidHierarchy(node, env)
|
|
101
|
+
: await readIosHierarchy(node, env);
|
|
102
|
+
// No screen name and no labelled item: name the screen after the app.
|
|
103
|
+
hierarchy.screen.name ??= appLabel;
|
|
104
|
+
return hierarchy;
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
async function readIosHierarchy(node, env) {
|
|
@@ -166,7 +173,7 @@ async function readAndroidHierarchy(node, env) {
|
|
|
166
173
|
}
|
|
167
174
|
|
|
168
175
|
async function readAndroidAgentDeviceHierarchy(serial, env) {
|
|
169
|
-
const app =
|
|
176
|
+
const app = nativeAppId(env, 'android');
|
|
170
177
|
const session = nativeSessionName(env, serial, app);
|
|
171
178
|
const stateDir = nativeAgentDeviceStateDir(env, env.METAMASK_RECIPE_PROJECT_ROOT ?? process.cwd(), serial);
|
|
172
179
|
const client = createAgentDeviceClient({
|
|
@@ -257,8 +264,7 @@ function normalizedHierarchy(
|
|
|
257
264
|
const items = nodes.slice(0, VISIBLE_LIMIT);
|
|
258
265
|
const hiddenOrOffscreen = hiddenNodes.slice(0, HIDDEN_LIMIT);
|
|
259
266
|
const name = metadata.screenName ??
|
|
260
|
-
items.find((item) => item.label)?.label
|
|
261
|
-
'MetaMask';
|
|
267
|
+
items.find((item) => item.label)?.label;
|
|
262
268
|
return {
|
|
263
269
|
screen: {
|
|
264
270
|
provider,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Navigate Android Home, Swap, and Perps through native controls and verify visible destinations.
|
|
2
2
|
// Switch Perps Lite or Pro mode through visible controls on Android and iOS.
|
|
3
|
-
import { createAgentDeviceUiTransport } from '@farmslot/
|
|
3
|
+
import { createAgentDeviceUiTransport } from '@farmslot/adapter-rn';
|
|
4
4
|
import { createAgentDeviceClient } from 'agent-device';
|
|
5
5
|
import {
|
|
6
6
|
nativeAgentDeviceStateDir,
|
|
7
|
+
nativeAppId,
|
|
7
8
|
nativeSessionName,
|
|
8
9
|
} from '../platform/native-session-name.mjs';
|
|
9
10
|
import { bridgeCommand } from '../platform/bridge.mjs';
|
|
@@ -51,7 +52,7 @@ export async function navigateOpaqueMobile(input, destination) {
|
|
|
51
52
|
if (!device) {
|
|
52
53
|
throw new Error('Opaque Mobile navigation requires one explicit Android device serial.');
|
|
53
54
|
}
|
|
54
|
-
const app =
|
|
55
|
+
const app = nativeAppId(env, 'android');
|
|
55
56
|
const session = nativeSessionName(env, device, app);
|
|
56
57
|
const stateDir = nativeAgentDeviceStateDir(env, input.context.projectRoot, device);
|
|
57
58
|
const client = createAgentDeviceClient({
|
|
@@ -184,7 +185,7 @@ export async function ensureVisibleAndroidPerpsMode(input, perpsMode) {
|
|
|
184
185
|
'Android Perps mode requires one explicit device serial.',
|
|
185
186
|
);
|
|
186
187
|
}
|
|
187
|
-
const app =
|
|
188
|
+
const app = nativeAppId(env, 'android');
|
|
188
189
|
const session = nativeSessionName(env, device, app);
|
|
189
190
|
const stateDir = nativeAgentDeviceStateDir(env, input.context.projectRoot, device);
|
|
190
191
|
const client = createAgentDeviceClient({
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Drive Android wallet onboarding, lock, unlock, and reset through native accessibility.
|
|
2
2
|
// Wallet observations can navigate home and open the address list to verify the selected account.
|
|
3
|
-
import { createAgentDeviceUiTransport } from '@farmslot/
|
|
3
|
+
import { createAgentDeviceUiTransport } from '@farmslot/adapter-rn';
|
|
4
4
|
import { createAgentDeviceClient } from 'agent-device';
|
|
5
5
|
import {
|
|
6
6
|
nativeAgentDeviceStateDir,
|
|
7
|
+
nativeAppId,
|
|
7
8
|
nativeSessionName,
|
|
8
9
|
} from '../platform/native-session-name.mjs';
|
|
9
10
|
|
|
@@ -249,7 +250,7 @@ function nativeSession(input) {
|
|
|
249
250
|
const env = { ...process.env, ...(input.context?.env ?? {}) };
|
|
250
251
|
const device = String(env.ADB_SERIAL ?? env.ANDROID_SERIAL ?? '').trim();
|
|
251
252
|
if (!device) throw new Error('Opaque Mobile UI execution requires one explicit Android device serial.');
|
|
252
|
-
const app =
|
|
253
|
+
const app = nativeAppId(env, 'android');
|
|
253
254
|
const session = nativeSessionName(env, device, app);
|
|
254
255
|
const stateDir = nativeAgentDeviceStateDir(env, input.context.projectRoot, device);
|
|
255
256
|
const client = createAgentDeviceClient({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Minimal adapter IO for the shared analytics actions.
|
|
2
2
|
//
|
|
3
|
-
// Mirrors the live-adapter contract in
|
|
3
|
+
// Mirrors the live-adapter contract in @farmslot/recipe-cli/harness. Neither
|
|
4
4
|
// existing runAdapter fits: extension's lives under actions/extension/platform
|
|
5
5
|
// and core's carries PerpsController teardown and expect_error handling. A
|
|
6
6
|
// `shared/` action importing either would couple every platform to one of them,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The one console matcher: classify application console events and apply a
|
|
2
2
|
// recipe-library allowlist of known noise. Used by run diagnostics
|
|
3
|
-
// (src/
|
|
3
|
+
// (src/console-classifier.ts), the assert_no_console_errors action, the
|
|
4
4
|
// allowlist generator, and the recipe library's checks/console-guard.mjs
|
|
5
5
|
// (which imports this file), so every caller judges an event the same way.
|
|
6
6
|
//
|