@deeeed/metamask-harness 0.17.5 → 0.19.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 +1059 -0
- package/README.md +91 -140
- package/adapters/core/inject.sh +1 -6
- package/adapters/extension/console-tail.mjs +55 -24
- package/adapters/extension/ensure-browser.sh +6 -1
- package/adapters/extension/inject.mjs +7 -13
- package/adapters/extension/launch-browser.cjs +7 -1
- package/adapters/extension/launch-webpack.cjs +83 -0
- package/adapters/extension/launch.sh +8 -11
- package/adapters/extension/lib/chrome-args.cjs +8 -1
- package/adapters/extension/lib/macos-focus.cjs +32 -0
- package/adapters/extension/live.sh +12 -21
- package/adapters/extension/reattach.sh +1 -0
- package/adapters/extension/seed-fixture.sh +4 -12
- package/adapters/extension/sidepanel-toggle.sh +4 -1
- package/adapters/extension/stamp-runtime-title.cjs +58 -0
- package/adapters/extension/start-watch.sh +17 -4
- package/adapters/extension/stop-viewers.sh +1 -1
- package/adapters/extension/sync-webpack-dist.cjs +107 -0
- package/adapters/extension/verify.sh +2 -2
- package/adapters/manifest.json +53 -37
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
- package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
- package/adapters/mobile/cleanup.sh +16 -176
- package/adapters/mobile/inject.sh +91 -671
- package/adapters/mobile/launch-console-forwarder.cjs +39 -0
- package/adapters/mobile/open-device.sh +45 -7
- package/adapters/mobile/start-console-forwarder.sh +70 -0
- package/adapters/mobile/start-metro.sh +0 -41
- package/adapters/mobile/verify.sh +34 -83
- package/adapters/mobile/wait-for-bridge.sh +22 -8
- package/adapters/shared/harness-source-fingerprint.mjs +14 -12
- package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
- package/adapters/shared/resolve-slot-ports.sh +17 -5
- package/adapters/shared/update-check-worker.mjs +43 -0
- package/bin/mm-harness +21 -2
- package/dist/adapters/core/surface.js +4 -1
- package/dist/adapters/extension/console-capture.js +95 -0
- package/dist/adapters/extension/product-config.js +110 -0
- package/dist/adapters/extension/runtime-decision.js +20 -2
- package/dist/adapters/extension/surface.js +22 -3
- package/dist/adapters/harness-freshness.js +49 -0
- package/dist/adapters/mobile/perps-env.js +43 -3
- package/dist/adapters/mobile/prepare.js +39 -7
- package/dist/adapters/mobile/surface.js +5 -2
- package/dist/adapters/resolve-slot-ports.js +2 -2
- package/dist/adapters/slot-ports.js +13 -16
- package/dist/adapters.js +56 -16
- package/dist/checkout-lock.js +27 -2
- package/dist/cli-color.js +19 -0
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +7 -18
- package/dist/command-contract.js +462 -0
- package/dist/command-journal.js +263 -0
- package/dist/commands/call.js +252 -57
- package/dist/commands/check.js +10 -4
- package/dist/commands/completion-candidates.js +17 -22
- package/dist/commands/device-target.js +27 -12
- package/dist/commands/doctor.js +106 -25
- package/dist/commands/fixtures.js +142 -35
- package/dist/commands/last.js +60 -0
- package/dist/commands/launch/extension.js +40 -17
- package/dist/commands/launch/index.js +207 -63
- package/dist/commands/launch/mobile.js +2 -0
- package/dist/commands/list-executables.js +187 -29
- package/dist/commands/logs.js +8 -6
- package/dist/commands/manifest.js +286 -35
- package/dist/commands/parse-args.js +68 -4
- package/dist/commands/provision.js +10 -4
- package/dist/commands/run-engine.js +772 -264
- package/dist/commands/run-report.js +12 -3
- package/dist/commands/run.js +378 -85
- package/dist/commands/shared.js +76 -4
- package/dist/commands/status-probe.js +4 -1
- package/dist/commands/status.js +2 -1
- package/dist/commands/stop.js +7 -2
- package/dist/commands/update.js +16 -28
- package/dist/completions-cache.js +1 -1
- package/dist/doctor.js +57 -10
- package/dist/harness.js +47 -137
- package/dist/heal-bounds.js +7 -2
- package/dist/json-stream.js +57 -0
- package/dist/live-adapter-contract.js +138 -32
- package/dist/manifest.js +161 -1
- package/dist/mm-harness-cli.js +146 -73
- package/dist/paths.js +2 -5
- package/dist/recipe-security.js +178 -0
- package/dist/run-diagnostics.js +261 -0
- package/dist/run-recording.js +1 -1
- package/dist/runner.js +140 -9
- package/docs/CONTRIBUTING.md +136 -0
- package/docs/QA.md +184 -0
- package/docs/RECIPES.md +122 -0
- package/docs/SECURITY.md +88 -0
- package/library/README.md +7 -3
- package/library/actions/core/perps/_controller.mjs +43 -56
- package/library/actions/core/perps/assert_orders.mjs +6 -7
- package/library/actions/core/perps/assert_positions.mjs +6 -7
- package/library/actions/core/perps/close_orders.mjs +2 -0
- package/library/actions/core/perps/close_positions.mjs +2 -0
- package/library/actions/core/perps/ensure_orders.mjs +4 -2
- package/library/actions/core/perps/ensure_positions.mjs +4 -2
- package/library/actions/core/perps/place_order.mjs +7 -3
- package/library/actions/core/perps/read_account.mjs +2 -2
- package/library/actions/core/perps/read_orders.mjs +2 -1
- package/library/actions/core/perps/read_positions.mjs +2 -1
- package/library/actions/core/wallet/list_accounts.mjs +95 -0
- package/library/actions/extension/perps/assert_orders.mjs +2 -1
- package/library/actions/extension/perps/assert_positions.mjs +2 -1
- package/library/actions/extension/perps/perps.mjs +43 -14
- package/library/actions/extension/platform/cdp.mjs +1 -0
- package/library/actions/extension/wallet/list_accounts.mjs +41 -0
- package/library/actions/mobile/perps/assert_orders.mjs +2 -1
- package/library/actions/mobile/perps/assert_positions.mjs +2 -1
- package/library/actions/mobile/perps/perps.mjs +40 -12
- package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
- package/library/library.json +1 -1
- package/library/manifests/core.action-manifest.json +1214 -396
- package/library/manifests/extension.action-manifest.json +1635 -728
- package/library/manifests/mobile.action-manifest.json +1740 -727
- package/library/recipes/app/lifecycle.android-smoke.mobile.recipe.json +63 -81
- package/library/recipes/perps/clean-market-testnet.core.recipe.json +44 -0
- package/library/recipes/perps/clean-market-testnet.recipe.json +49 -0
- package/library/recipes/perps/lifecycle.recipe.json +136 -180
- package/library/recipes/perps/order-lifecycle.core.recipe.json +71 -67
- package/library/recipes/perps/performance.background-resume.mobile.recipe.json +51 -67
- package/library/recipes/perps/performance.cold-start.mobile.recipe.json +51 -67
- package/library/recipes/perps/performance.mobile.recipe.json +37 -51
- package/library/recipes/perps/performance.warm-start.mobile.recipe.json +44 -59
- package/library/recipes/perps/read-markets.core.recipe.json +29 -31
- package/library/recipes/perps/smoke.core.recipe.json +29 -32
- package/library/recipes/perps/smoke.extension.recipe.json +41 -44
- package/library/recipes/perps/smoke.mobile.recipe.json +42 -44
- package/library/recipes/perps/trading-lifecycle.core.recipe.json +69 -65
- package/library/recipes/runner/action-validation.extension.recipe.json +312 -398
- package/library/recipes/runner/action-validation.mobile.recipe.json +316 -402
- package/library/recipes/runner/smoke.core.recipe.json +25 -0
- package/library/recipes/runner/smoke.extension.recipe.json +23 -24
- package/library/recipes/runner/smoke.mobile.recipe.json +23 -24
- package/library/recipes/wallet/smoke.extension.recipe.json +40 -0
- package/library/recipes/wallet/smoke.mobile.recipe.json +40 -0
- package/package.json +12 -5
- package/scripts/completions.sh +8 -11
- package/scripts/validate-human-outcomes.mjs +169 -0
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
- package/adapters/mobile/overlay/compat/README.md +0 -28
- package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
- package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
- package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
- package/adapters/shared/resolve-farmslot-ports.sh +0 -7
- package/dist/adapters/extension/harness-freshness.js +0 -39
- package/dist/commands/flows.js +0 -62
- package/docs/ADAPTER-SURFACE.md +0 -119
- package/docs/CHEATSHEET.md +0 -61
- package/docs/CLI-SPEC.md +0 -1098
- package/docs/CODE-MAP.md +0 -62
- package/docs/DEBUG-HANDOVER.md +0 -36
- package/docs/MENTAL-MODEL.md +0 -295
- package/docs/UX-PRINCIPLES.md +0 -64
- package/docs/architecture.md +0 -398
- package/docs/live-adapter-contract.md +0 -188
- package/docs/package-boundaries.md +0 -47
- package/docs/perps-flow-catalog.md +0 -235
- package/docs/recipe-libraries.md +0 -301
- package/docs/runtime-file-conventions.md +0 -36
- package/library/flows/perps.flows.json +0 -64
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
|
|
3
|
+
"title": "MetaMask Core runner smoke",
|
|
4
|
+
"description": "Proves the headless Core runner resolves the checkout, executes generic actions, and writes a valid artifact package.",
|
|
5
|
+
"workflow": {
|
|
6
|
+
"entry": "status",
|
|
7
|
+
"nodes": {
|
|
8
|
+
"status": {
|
|
9
|
+
"action": "app.status",
|
|
10
|
+
"next": "package",
|
|
11
|
+
"intent": "Resolve the Core checkout and report headless readiness"
|
|
12
|
+
},
|
|
13
|
+
"package": {
|
|
14
|
+
"action": "assert_file",
|
|
15
|
+
"path": "package.json",
|
|
16
|
+
"next": "done",
|
|
17
|
+
"intent": "Prove generic project-file assertions work in Core"
|
|
18
|
+
},
|
|
19
|
+
"done": {
|
|
20
|
+
"action": "end",
|
|
21
|
+
"status": "pass"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -1,30 +1,29 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
2
|
+
"$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
|
|
3
3
|
"title": "MetaMask Extension v1 runner smoke",
|
|
4
4
|
"description": "Proves the MetaMask Extension runner executes manifest-declared actions and writes a valid artifact package.",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
5
|
+
"workflow": {
|
|
6
|
+
"entry": "status",
|
|
7
|
+
"nodes": {
|
|
8
|
+
"status": {
|
|
9
|
+
"action": "app.status",
|
|
10
|
+
"next": "fixture",
|
|
11
|
+
"intent": "Confirm the Extension checkout is ready"
|
|
12
|
+
},
|
|
13
|
+
"fixture": {
|
|
14
|
+
"action": "metamask.wallet.fixture_status",
|
|
15
|
+
"next": "cdp",
|
|
16
|
+
"intent": "Confirm the wallet fixture is available"
|
|
17
|
+
},
|
|
18
|
+
"cdp": {
|
|
19
|
+
"action": "cdp.target",
|
|
20
|
+
"required": true,
|
|
21
|
+
"next": "done",
|
|
22
|
+
"intent": "Confirm the Extension runtime is reachable"
|
|
23
|
+
},
|
|
24
|
+
"done": {
|
|
25
|
+
"action": "end",
|
|
26
|
+
"status": "pass"
|
|
28
27
|
}
|
|
29
28
|
}
|
|
30
29
|
}
|
|
@@ -1,30 +1,29 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
2
|
+
"$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
|
|
3
3
|
"title": "MetaMask Mobile v1 runner smoke",
|
|
4
4
|
"description": "Proves the MetaMask Mobile runner executes manifest-declared actions and writes a valid artifact package.",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
5
|
+
"workflow": {
|
|
6
|
+
"entry": "status",
|
|
7
|
+
"nodes": {
|
|
8
|
+
"status": {
|
|
9
|
+
"action": "app.status",
|
|
10
|
+
"next": "fixture",
|
|
11
|
+
"intent": "Confirm the Mobile checkout is ready"
|
|
12
|
+
},
|
|
13
|
+
"fixture": {
|
|
14
|
+
"action": "metamask.wallet.fixture_status",
|
|
15
|
+
"next": "cdp",
|
|
16
|
+
"intent": "Confirm the wallet fixture is available"
|
|
17
|
+
},
|
|
18
|
+
"cdp": {
|
|
19
|
+
"action": "cdp.target",
|
|
20
|
+
"require_reachable": true,
|
|
21
|
+
"next": "done",
|
|
22
|
+
"intent": "Confirm the Mobile runtime is reachable"
|
|
23
|
+
},
|
|
24
|
+
"done": {
|
|
25
|
+
"action": "end",
|
|
26
|
+
"status": "pass"
|
|
28
27
|
}
|
|
29
28
|
}
|
|
30
29
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
|
|
3
|
+
"title": "MetaMask Extension wallet smoke",
|
|
4
|
+
"description": "Fixture-backed proof that the Extension wallet is reachable, unlocked, readable, and visually capturable without depending on a product feature domain.",
|
|
5
|
+
"workflow": {
|
|
6
|
+
"entry": "status",
|
|
7
|
+
"nodes": {
|
|
8
|
+
"status": {
|
|
9
|
+
"action": "app.status",
|
|
10
|
+
"next": "fixture",
|
|
11
|
+
"intent": "Confirm the Extension runtime is reachable"
|
|
12
|
+
},
|
|
13
|
+
"fixture": {
|
|
14
|
+
"action": "metamask.wallet.fixture_status",
|
|
15
|
+
"next": "unlock",
|
|
16
|
+
"intent": "Confirm the checkout wallet fixture is ready"
|
|
17
|
+
},
|
|
18
|
+
"unlock": {
|
|
19
|
+
"action": "metamask.wallet.ensure_unlocked",
|
|
20
|
+
"next": "state",
|
|
21
|
+
"intent": "Ensure the fixture-backed wallet is unlocked"
|
|
22
|
+
},
|
|
23
|
+
"state": {
|
|
24
|
+
"action": "metamask.wallet.read_state",
|
|
25
|
+
"next": "screenshot",
|
|
26
|
+
"intent": "Read the current wallet state through the public adapter"
|
|
27
|
+
},
|
|
28
|
+
"screenshot": {
|
|
29
|
+
"action": "ui.screenshot",
|
|
30
|
+
"path": "screenshots/wallet-smoke.png",
|
|
31
|
+
"next": "done",
|
|
32
|
+
"intent": "Record the visible wallet state as evidence"
|
|
33
|
+
},
|
|
34
|
+
"done": {
|
|
35
|
+
"action": "end",
|
|
36
|
+
"status": "pass"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
|
|
3
|
+
"title": "MetaMask Mobile wallet smoke",
|
|
4
|
+
"description": "Fixture-backed proof that the Mobile wallet is reachable, unlocked, readable, and visually capturable without depending on a product feature domain.",
|
|
5
|
+
"workflow": {
|
|
6
|
+
"entry": "status",
|
|
7
|
+
"nodes": {
|
|
8
|
+
"status": {
|
|
9
|
+
"action": "app.status",
|
|
10
|
+
"next": "fixture",
|
|
11
|
+
"intent": "Confirm the Mobile runtime is reachable"
|
|
12
|
+
},
|
|
13
|
+
"fixture": {
|
|
14
|
+
"action": "metamask.wallet.fixture_status",
|
|
15
|
+
"next": "unlock",
|
|
16
|
+
"intent": "Confirm the checkout wallet fixture is ready"
|
|
17
|
+
},
|
|
18
|
+
"unlock": {
|
|
19
|
+
"action": "metamask.wallet.ensure_unlocked",
|
|
20
|
+
"next": "state",
|
|
21
|
+
"intent": "Ensure the fixture-backed wallet is unlocked"
|
|
22
|
+
},
|
|
23
|
+
"state": {
|
|
24
|
+
"action": "metamask.wallet.read_state",
|
|
25
|
+
"next": "screenshot",
|
|
26
|
+
"intent": "Read the current wallet state through the public adapter"
|
|
27
|
+
},
|
|
28
|
+
"screenshot": {
|
|
29
|
+
"action": "ui.screenshot",
|
|
30
|
+
"path": "screenshots/wallet-smoke.png",
|
|
31
|
+
"next": "done",
|
|
32
|
+
"intent": "Record the visible wallet state as evidence"
|
|
33
|
+
},
|
|
34
|
+
"done": {
|
|
35
|
+
"action": "end",
|
|
36
|
+
"status": "pass"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deeeed/metamask-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mm-harness": "bin/mm-harness"
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
"prepack": "npm run build",
|
|
11
11
|
"dev:link-farmslot": "node scripts/link-local-farmslot.mjs",
|
|
12
12
|
"check": "node scripts/check.mjs",
|
|
13
|
+
"test:unit": "vitest run --config scripts/vitest.config.mjs",
|
|
14
|
+
"test:coverage": "vitest run --coverage --config scripts/vitest.config.mjs",
|
|
15
|
+
"qa:human": "node scripts/validate-human-outcomes.mjs",
|
|
13
16
|
"self-test": "bin/mm-harness self-test",
|
|
14
17
|
"manifest:mobile": "bin/mm-harness actions --raw --adapter mobile --json",
|
|
15
18
|
"manifest:extension": "bin/mm-harness actions --raw --adapter extension --json",
|
|
@@ -17,9 +20,11 @@
|
|
|
17
20
|
},
|
|
18
21
|
"dependencies": {
|
|
19
22
|
"@farmslot/agent-runtime": "^0.2.0",
|
|
20
|
-
"@farmslot/protocol": "^0.
|
|
21
|
-
"@farmslot/recipe-harness": "^0.
|
|
23
|
+
"@farmslot/protocol": "^0.11.0",
|
|
24
|
+
"@farmslot/recipe-harness": "^0.9.0",
|
|
22
25
|
"commander": "^12.0.0",
|
|
26
|
+
"es-module-lexer": "2.3.1",
|
|
27
|
+
"esbuild": "0.28.1",
|
|
23
28
|
"viem": "^2.54.3",
|
|
24
29
|
"ws": "8.21.0"
|
|
25
30
|
},
|
|
@@ -31,12 +36,13 @@
|
|
|
31
36
|
"devDependencies": {
|
|
32
37
|
"@eslint/js": "^9",
|
|
33
38
|
"@types/node": "^22.0.0",
|
|
34
|
-
"
|
|
39
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
35
40
|
"eslint": "^9",
|
|
36
41
|
"globals": "^17.7.0",
|
|
37
42
|
"tsx": "^4.23.0",
|
|
38
43
|
"typescript": "^5.6.0",
|
|
39
|
-
"typescript-eslint": "^8"
|
|
44
|
+
"typescript-eslint": "^8",
|
|
45
|
+
"vitest": "^4.1.10"
|
|
40
46
|
},
|
|
41
47
|
"main": "./dist/index.js",
|
|
42
48
|
"exports": {
|
|
@@ -56,6 +62,7 @@
|
|
|
56
62
|
"library",
|
|
57
63
|
"scripts/completions.sh",
|
|
58
64
|
"scripts/install-completions.sh",
|
|
65
|
+
"scripts/validate-human-outcomes.mjs",
|
|
59
66
|
"!scripts/README.md",
|
|
60
67
|
"docs",
|
|
61
68
|
"README.md",
|
package/scripts/completions.sh
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
#
|
|
4
4
|
# Static parts (commands, positional targets, per-command flags) mirror the
|
|
5
5
|
# mm-harness surface. Dynamic parts are sourced live from the hidden
|
|
6
|
-
# `mm-harness completion-candidates <actions|
|
|
6
|
+
# `mm-harness completion-candidates <actions|recipes>` command (per-checkout cached):
|
|
7
7
|
# call <TAB> → action names from the adapter manifest
|
|
8
|
-
# run <TAB> → recipe files +
|
|
8
|
+
# run <TAB> → recipe files + named recipes from the recipe library
|
|
9
9
|
# so completion reflects the installed overlay's real vocabulary. A candidate read
|
|
10
10
|
# that fails is silently empty — completion never blocks the shell.
|
|
11
11
|
#
|
|
@@ -29,7 +29,7 @@ _mmh_bin() {
|
|
|
29
29
|
return 1
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
_mmh_commands="launch stop logs debug fixtures actions call
|
|
32
|
+
_mmh_commands="launch stop logs debug fixtures actions call run doctor check checklist recipe-quality install verify cleanup completions"
|
|
33
33
|
|
|
34
34
|
# Per-command flags (static, from the mm-harness surface).
|
|
35
35
|
_mmh_flags_for() {
|
|
@@ -41,9 +41,8 @@ _mmh_flags_for() {
|
|
|
41
41
|
fixtures) printf '%s' "--from --dev --force --fixture --adapter --target --json" ;;
|
|
42
42
|
checklist) printf '%s' "" ;;
|
|
43
43
|
actions) printf '%s' "--adapter --action --kind --raw --action-manifest --target --json" ;;
|
|
44
|
-
call) printf '%s' "--arg --adapter --target --artifacts-dir --action-manifest --heal --json" ;;
|
|
45
|
-
|
|
46
|
-
run) printf '%s' "--plan --adapter --artifacts-dir --target --library --cdp-port --heal --record-video --json" ;;
|
|
44
|
+
call) printf '%s' "--list --arg --adapter --target --artifacts-dir --action-manifest --heal --json" ;;
|
|
45
|
+
run) printf '%s' "--list --describe --plan --adapter --artifacts-dir --target --library --cdp-port --heal --record-video --json" ;;
|
|
47
46
|
doctor) printf '%s' "--adapter --target --fix --json" ;;
|
|
48
47
|
install|verify|cleanup) printf '%s' "--adapter --target --json" ;;
|
|
49
48
|
*) printf '%s' "" ;;
|
|
@@ -52,7 +51,7 @@ _mmh_flags_for() {
|
|
|
52
51
|
|
|
53
52
|
# Dynamic candidates from the DISCOVER layer (silent empty on any failure).
|
|
54
53
|
_mmh_actions() { local b; b="$(_mmh_bin)" || return 0; "$b" completion-candidates actions 2>/dev/null; }
|
|
55
|
-
|
|
54
|
+
_mmh_recipes() { local b; b="$(_mmh_bin)" || return 0; "$b" completion-candidates recipes 2>/dev/null; }
|
|
56
55
|
|
|
57
56
|
# ── Bash ────────────────────────────────────────────────────────────────
|
|
58
57
|
if [ -n "${BASH_VERSION:-}" ]; then
|
|
@@ -80,9 +79,8 @@ if [ -n "${BASH_VERSION:-}" ]; then
|
|
|
80
79
|
fixtures) [ "$cword" -eq 2 ] && COMPREPLY=($(compgen -W "init sync set generate finalize" -- "$cur")) ;;
|
|
81
80
|
checklist) [ "$cword" -eq 2 ] && COMPREPLY=($(compgen -W "mark" -- "$cur")) ;;
|
|
82
81
|
completions) [ "$cword" -eq 2 ] && COMPREPLY=($(compgen -W "install bash zsh status" -- "$cur")) ;;
|
|
83
|
-
flows) [ "$cword" -eq 2 ] && COMPREPLY=($(compgen -W "list promote" -- "$cur")) ;;
|
|
84
82
|
call) [ "$cword" -eq 2 ] && COMPREPLY=($(compgen -W "$(_mmh_actions)" -- "$cur")) ;;
|
|
85
|
-
run) COMPREPLY=($(compgen -f -- "$cur"; compgen -W "$(
|
|
83
|
+
run) COMPREPLY=($(compgen -f -- "$cur"; compgen -W "$(_mmh_recipes)" -- "$cur")) ;;
|
|
86
84
|
*) COMPREPLY=($(compgen -W "$(_mmh_flags_for "$cmd")" -- "$cur")) ;;
|
|
87
85
|
esac
|
|
88
86
|
}
|
|
@@ -115,9 +113,8 @@ if [ -n "${ZSH_VERSION:-}" ]; then
|
|
|
115
113
|
fixtures) (( CURRENT == 3 )) && { candidates=(init sync set generate finalize); compadd -a candidates; } ;;
|
|
116
114
|
checklist) (( CURRENT == 3 )) && { candidates=(mark); compadd -a candidates; } ;;
|
|
117
115
|
completions) (( CURRENT == 3 )) && { candidates=(install bash zsh status); compadd -a candidates; } ;;
|
|
118
|
-
flows) (( CURRENT == 3 )) && { candidates=(list promote); compadd -a candidates; } ;;
|
|
119
116
|
call) (( CURRENT == 3 )) && { candidates=("${(@f)$(_mmh_actions)}"); compadd -a candidates; } ;;
|
|
120
|
-
run) candidates=("${(@f)$(
|
|
117
|
+
run) candidates=("${(@f)$(_mmh_recipes)}"); compadd -a candidates; _files ;;
|
|
121
118
|
*) candidates=(${=$(_mmh_flags_for "$cmd")}); compadd -a candidates ;;
|
|
122
119
|
esac
|
|
123
120
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
8
|
+
const options = parseArgs(process.argv.slice(2));
|
|
9
|
+
const harness = path.resolve(options.bin ?? path.join(repoRoot, 'bin', 'mm-harness'));
|
|
10
|
+
const artifactsRoot = path.resolve(options.artifactsDir ?? path.join(repoRoot, 'temp', 'human-qa'));
|
|
11
|
+
const targets = ['extension', 'mobile', 'core']
|
|
12
|
+
.filter((adapter) => options[adapter])
|
|
13
|
+
.map((adapter) => ({ adapter, target: path.resolve(options[adapter]) }));
|
|
14
|
+
|
|
15
|
+
if (targets.length === 0) usage('pass at least one of --extension, --mobile, or --core');
|
|
16
|
+
if (!fs.existsSync(harness)) usage(`mm-harness executable not found: ${harness}`);
|
|
17
|
+
fs.mkdirSync(artifactsRoot, { recursive: true });
|
|
18
|
+
|
|
19
|
+
const platforms = targets.map(runPlatform);
|
|
20
|
+
const report = {
|
|
21
|
+
schemaVersion: 1,
|
|
22
|
+
command: 'validate-human-outcomes',
|
|
23
|
+
status: platforms.every((platform) => platform.status === 'pass') ? 'pass' : 'fail',
|
|
24
|
+
harness,
|
|
25
|
+
artifactsRoot,
|
|
26
|
+
platforms,
|
|
27
|
+
createdAt: new Date().toISOString(),
|
|
28
|
+
};
|
|
29
|
+
const reportPath = path.join(artifactsRoot, 'human-qa.json');
|
|
30
|
+
const markdownPath = path.join(artifactsRoot, 'human-qa.md');
|
|
31
|
+
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
|
|
32
|
+
fs.writeFileSync(markdownPath, renderMarkdown(report));
|
|
33
|
+
|
|
34
|
+
if (options.json) process.stdout.write(`${JSON.stringify({ ...report, reportPath, markdownPath }, null, 2)}\n`);
|
|
35
|
+
else {
|
|
36
|
+
console.log(`${report.status.toUpperCase()} human outcome QA`);
|
|
37
|
+
for (const platform of platforms) {
|
|
38
|
+
console.log(` ${platform.status.toUpperCase()} ${platform.adapter}: ${platform.steps.filter((step) => step.status === 'pass').length}/${platform.steps.length}`);
|
|
39
|
+
for (const step of platform.steps.filter((candidate) => candidate.status !== 'pass')) {
|
|
40
|
+
console.log(` ${step.name}: ${step.detail}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
console.log(`report: ${reportPath}`);
|
|
44
|
+
}
|
|
45
|
+
process.exit(report.status === 'pass' ? 0 : 1);
|
|
46
|
+
|
|
47
|
+
function runPlatform({ adapter, target }) {
|
|
48
|
+
const platformArtifacts = path.join(artifactsRoot, adapter);
|
|
49
|
+
fs.mkdirSync(platformArtifacts, { recursive: true });
|
|
50
|
+
const before = gitState(target);
|
|
51
|
+
const steps = [];
|
|
52
|
+
|
|
53
|
+
steps.push(runJsonStep('doctor-live', ['doctor', '--adapter', adapter, '--target', target, '--expect-live', '--json']));
|
|
54
|
+
steps.push(runJsonStep('verify', ['verify', '--adapter', adapter, '--target', target, '--json']));
|
|
55
|
+
if (adapter !== 'core') {
|
|
56
|
+
steps.push(runJsonStep('debug-target', ['debug', '--adapter', adapter, '--target', target, '--no-open', '--json'], (result) => {
|
|
57
|
+
if (adapter === 'extension' && !String(result.target?.url ?? '').startsWith('chrome-extension://')) {
|
|
58
|
+
throw new Error('debug did not resolve an Extension page');
|
|
59
|
+
}
|
|
60
|
+
if (adapter === 'mobile' && !result.endpoint) throw new Error('debug did not resolve a Mobile endpoint');
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const smokeArtifacts = path.join(platformArtifacts, 'runner-smoke');
|
|
65
|
+
steps.push(runJsonStep(
|
|
66
|
+
'runner-smoke',
|
|
67
|
+
['run', 'runner.smoke', '--adapter', adapter, '--target', target, '--artifacts-dir', smokeArtifacts, '--json'],
|
|
68
|
+
(result) => assertRunEvidence(result, smokeArtifacts),
|
|
69
|
+
));
|
|
70
|
+
|
|
71
|
+
if (adapter !== 'core') {
|
|
72
|
+
const walletArtifacts = path.join(platformArtifacts, 'wallet-smoke');
|
|
73
|
+
steps.push(runJsonStep(
|
|
74
|
+
'wallet-smoke',
|
|
75
|
+
['run', 'wallet.smoke', '--adapter', adapter, '--target', target, '--artifacts-dir', walletArtifacts, '--json'],
|
|
76
|
+
(result) => assertRunEvidence(result, walletArtifacts),
|
|
77
|
+
));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const after = gitState(target);
|
|
81
|
+
steps.push({
|
|
82
|
+
name: 'product-source-unchanged',
|
|
83
|
+
status: before === after ? 'pass' : 'fail',
|
|
84
|
+
detail: before === after ? 'tracked checkout state unchanged' : 'tracked checkout state changed during QA',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
adapter,
|
|
89
|
+
target,
|
|
90
|
+
status: steps.every((step) => step.status === 'pass') ? 'pass' : 'fail',
|
|
91
|
+
steps,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function runJsonStep(name, args, assertResult) {
|
|
96
|
+
const result = spawnSync(harness, args, {
|
|
97
|
+
encoding: 'utf8',
|
|
98
|
+
timeout: 10 * 60 * 1000,
|
|
99
|
+
// Installed platform delegates must exercise this exact candidate. Package
|
|
100
|
+
// managers prepend their own bin shims to PATH during scripts, which can
|
|
101
|
+
// otherwise send nested verify calls through a different/global version.
|
|
102
|
+
env: { ...process.env, MM_HARNESS_BIN: harness, MM_HARNESS_NO_UPDATE_CHECK: '1' },
|
|
103
|
+
});
|
|
104
|
+
let payload;
|
|
105
|
+
try {
|
|
106
|
+
payload = JSON.parse(result.stdout);
|
|
107
|
+
} catch {
|
|
108
|
+
return { name, status: 'fail', detail: `stdout was not one JSON document: ${compact(result.stdout || result.stderr)}` };
|
|
109
|
+
}
|
|
110
|
+
if (result.status !== 0 || payload.status === 'fail' || payload.exitCode > 0) {
|
|
111
|
+
return { name, status: 'fail', detail: compact(payload.error?.message || result.stderr || result.stdout) };
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
assertResult?.(payload);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return { name, status: 'fail', detail: error instanceof Error ? error.message : String(error) };
|
|
117
|
+
}
|
|
118
|
+
return { name, status: 'pass', detail: 'human outcome verified' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function assertRunEvidence(result, artifactsDir) {
|
|
122
|
+
if (result.status !== 'pass') throw new Error('recipe did not pass');
|
|
123
|
+
const required = ['report.md', 'summary.json', 'trace.json', 'recipe.json', 'artifact-manifest.json'];
|
|
124
|
+
for (const relative of required) {
|
|
125
|
+
if (!fs.existsSync(path.join(artifactsDir, relative))) throw new Error(`missing run evidence: ${relative}`);
|
|
126
|
+
}
|
|
127
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(artifactsDir, 'artifact-manifest.json'), 'utf8'));
|
|
128
|
+
if (!Array.isArray(manifest.artifacts) || manifest.artifacts.length === 0) throw new Error('artifact manifest is empty');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function gitState(target) {
|
|
132
|
+
const result = spawnSync('git', ['status', '--porcelain', '--untracked-files=no'], { cwd: target, encoding: 'utf8' });
|
|
133
|
+
return result.status === 0 ? result.stdout : '';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function compact(value) {
|
|
137
|
+
return String(value ?? '').trim().replace(/\s+/gu, ' ').slice(0, 500) || 'command failed without detail';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function renderMarkdown(report) {
|
|
141
|
+
const lines = ['# mm-harness human outcome QA', '', `Status: **${report.status.toUpperCase()}**`, ''];
|
|
142
|
+
for (const platform of report.platforms) {
|
|
143
|
+
lines.push(`## ${platform.adapter}`, '', `Target: \`${platform.target}\``, '');
|
|
144
|
+
for (const step of platform.steps) lines.push(`- [${step.status === 'pass' ? 'x' : ' '}] ${step.name} — ${step.detail}`);
|
|
145
|
+
lines.push('');
|
|
146
|
+
}
|
|
147
|
+
return `${lines.join('\n')}\n`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseArgs(argv) {
|
|
151
|
+
const parsed = {};
|
|
152
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
153
|
+
const arg = argv[index];
|
|
154
|
+
if (arg === '--json') parsed.json = true;
|
|
155
|
+
else if (['--extension', '--mobile', '--core', '--bin', '--artifacts-dir'].includes(arg)) {
|
|
156
|
+
const value = argv[index + 1];
|
|
157
|
+
if (!value || value.startsWith('--')) usage(`missing value for ${arg}`);
|
|
158
|
+
parsed[arg.slice(2).replace(/-([a-z])/gu, (_, letter) => letter.toUpperCase())] = value;
|
|
159
|
+
index += 1;
|
|
160
|
+
} else usage(`unknown option: ${arg}`);
|
|
161
|
+
}
|
|
162
|
+
return parsed;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function usage(message) {
|
|
166
|
+
if (message) console.error(`validate-human-outcomes: ${message}`);
|
|
167
|
+
console.error('Usage: yarn qa:human [--extension <path>] [--mobile <path>] [--core <path>] [--artifacts-dir <path>] [--json]');
|
|
168
|
+
process.exit(2);
|
|
169
|
+
}
|