@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.
Files changed (169) hide show
  1. package/CHANGELOG.md +1059 -0
  2. package/README.md +91 -140
  3. package/adapters/core/inject.sh +1 -6
  4. package/adapters/extension/console-tail.mjs +55 -24
  5. package/adapters/extension/ensure-browser.sh +6 -1
  6. package/adapters/extension/inject.mjs +7 -13
  7. package/adapters/extension/launch-browser.cjs +7 -1
  8. package/adapters/extension/launch-webpack.cjs +83 -0
  9. package/adapters/extension/launch.sh +8 -11
  10. package/adapters/extension/lib/chrome-args.cjs +8 -1
  11. package/adapters/extension/lib/macos-focus.cjs +32 -0
  12. package/adapters/extension/live.sh +12 -21
  13. package/adapters/extension/reattach.sh +1 -0
  14. package/adapters/extension/seed-fixture.sh +4 -12
  15. package/adapters/extension/sidepanel-toggle.sh +4 -1
  16. package/adapters/extension/stamp-runtime-title.cjs +58 -0
  17. package/adapters/extension/start-watch.sh +17 -4
  18. package/adapters/extension/stop-viewers.sh +1 -1
  19. package/adapters/extension/sync-webpack-dist.cjs +107 -0
  20. package/adapters/extension/verify.sh +2 -2
  21. package/adapters/manifest.json +53 -37
  22. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
  23. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  24. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
  25. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
  26. package/adapters/mobile/cleanup.sh +16 -176
  27. package/adapters/mobile/inject.sh +91 -671
  28. package/adapters/mobile/launch-console-forwarder.cjs +39 -0
  29. package/adapters/mobile/open-device.sh +45 -7
  30. package/adapters/mobile/start-console-forwarder.sh +70 -0
  31. package/adapters/mobile/start-metro.sh +0 -41
  32. package/adapters/mobile/verify.sh +34 -83
  33. package/adapters/mobile/wait-for-bridge.sh +22 -8
  34. package/adapters/shared/harness-source-fingerprint.mjs +14 -12
  35. package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
  36. package/adapters/shared/resolve-slot-ports.sh +17 -5
  37. package/adapters/shared/update-check-worker.mjs +43 -0
  38. package/bin/mm-harness +21 -2
  39. package/dist/adapters/core/surface.js +4 -1
  40. package/dist/adapters/extension/console-capture.js +95 -0
  41. package/dist/adapters/extension/product-config.js +110 -0
  42. package/dist/adapters/extension/runtime-decision.js +20 -2
  43. package/dist/adapters/extension/surface.js +22 -3
  44. package/dist/adapters/harness-freshness.js +49 -0
  45. package/dist/adapters/mobile/perps-env.js +43 -3
  46. package/dist/adapters/mobile/prepare.js +39 -7
  47. package/dist/adapters/mobile/surface.js +5 -2
  48. package/dist/adapters/resolve-slot-ports.js +2 -2
  49. package/dist/adapters/slot-ports.js +13 -16
  50. package/dist/adapters.js +56 -16
  51. package/dist/checkout-lock.js +27 -2
  52. package/dist/cli-color.js +19 -0
  53. package/dist/cli-commands.js +1 -1
  54. package/dist/cli.js +7 -18
  55. package/dist/command-contract.js +462 -0
  56. package/dist/command-journal.js +263 -0
  57. package/dist/commands/call.js +252 -57
  58. package/dist/commands/check.js +10 -4
  59. package/dist/commands/completion-candidates.js +17 -22
  60. package/dist/commands/device-target.js +27 -12
  61. package/dist/commands/doctor.js +106 -25
  62. package/dist/commands/fixtures.js +142 -35
  63. package/dist/commands/last.js +60 -0
  64. package/dist/commands/launch/extension.js +40 -17
  65. package/dist/commands/launch/index.js +207 -63
  66. package/dist/commands/launch/mobile.js +2 -0
  67. package/dist/commands/list-executables.js +187 -29
  68. package/dist/commands/logs.js +8 -6
  69. package/dist/commands/manifest.js +286 -35
  70. package/dist/commands/parse-args.js +68 -4
  71. package/dist/commands/provision.js +10 -4
  72. package/dist/commands/run-engine.js +772 -264
  73. package/dist/commands/run-report.js +12 -3
  74. package/dist/commands/run.js +378 -85
  75. package/dist/commands/shared.js +76 -4
  76. package/dist/commands/status-probe.js +4 -1
  77. package/dist/commands/status.js +2 -1
  78. package/dist/commands/stop.js +7 -2
  79. package/dist/commands/update.js +16 -28
  80. package/dist/completions-cache.js +1 -1
  81. package/dist/doctor.js +57 -10
  82. package/dist/harness.js +47 -137
  83. package/dist/heal-bounds.js +7 -2
  84. package/dist/json-stream.js +57 -0
  85. package/dist/live-adapter-contract.js +138 -32
  86. package/dist/manifest.js +161 -1
  87. package/dist/mm-harness-cli.js +146 -73
  88. package/dist/paths.js +2 -5
  89. package/dist/recipe-security.js +178 -0
  90. package/dist/run-diagnostics.js +261 -0
  91. package/dist/run-recording.js +1 -1
  92. package/dist/runner.js +140 -9
  93. package/docs/CONTRIBUTING.md +136 -0
  94. package/docs/QA.md +184 -0
  95. package/docs/RECIPES.md +122 -0
  96. package/docs/SECURITY.md +88 -0
  97. package/library/README.md +7 -3
  98. package/library/actions/core/perps/_controller.mjs +43 -56
  99. package/library/actions/core/perps/assert_orders.mjs +6 -7
  100. package/library/actions/core/perps/assert_positions.mjs +6 -7
  101. package/library/actions/core/perps/close_orders.mjs +2 -0
  102. package/library/actions/core/perps/close_positions.mjs +2 -0
  103. package/library/actions/core/perps/ensure_orders.mjs +4 -2
  104. package/library/actions/core/perps/ensure_positions.mjs +4 -2
  105. package/library/actions/core/perps/place_order.mjs +7 -3
  106. package/library/actions/core/perps/read_account.mjs +2 -2
  107. package/library/actions/core/perps/read_orders.mjs +2 -1
  108. package/library/actions/core/perps/read_positions.mjs +2 -1
  109. package/library/actions/core/wallet/list_accounts.mjs +95 -0
  110. package/library/actions/extension/perps/assert_orders.mjs +2 -1
  111. package/library/actions/extension/perps/assert_positions.mjs +2 -1
  112. package/library/actions/extension/perps/perps.mjs +43 -14
  113. package/library/actions/extension/platform/cdp.mjs +1 -0
  114. package/library/actions/extension/wallet/list_accounts.mjs +41 -0
  115. package/library/actions/mobile/perps/assert_orders.mjs +2 -1
  116. package/library/actions/mobile/perps/assert_positions.mjs +2 -1
  117. package/library/actions/mobile/perps/perps.mjs +40 -12
  118. package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
  119. package/library/library.json +1 -1
  120. package/library/manifests/core.action-manifest.json +1214 -396
  121. package/library/manifests/extension.action-manifest.json +1635 -728
  122. package/library/manifests/mobile.action-manifest.json +1740 -727
  123. package/library/recipes/app/lifecycle.android-smoke.mobile.recipe.json +63 -81
  124. package/library/recipes/perps/clean-market-testnet.core.recipe.json +44 -0
  125. package/library/recipes/perps/clean-market-testnet.recipe.json +49 -0
  126. package/library/recipes/perps/lifecycle.recipe.json +136 -180
  127. package/library/recipes/perps/order-lifecycle.core.recipe.json +71 -67
  128. package/library/recipes/perps/performance.background-resume.mobile.recipe.json +51 -67
  129. package/library/recipes/perps/performance.cold-start.mobile.recipe.json +51 -67
  130. package/library/recipes/perps/performance.mobile.recipe.json +37 -51
  131. package/library/recipes/perps/performance.warm-start.mobile.recipe.json +44 -59
  132. package/library/recipes/perps/read-markets.core.recipe.json +29 -31
  133. package/library/recipes/perps/smoke.core.recipe.json +29 -32
  134. package/library/recipes/perps/smoke.extension.recipe.json +41 -44
  135. package/library/recipes/perps/smoke.mobile.recipe.json +42 -44
  136. package/library/recipes/perps/trading-lifecycle.core.recipe.json +69 -65
  137. package/library/recipes/runner/action-validation.extension.recipe.json +312 -398
  138. package/library/recipes/runner/action-validation.mobile.recipe.json +316 -402
  139. package/library/recipes/runner/smoke.core.recipe.json +25 -0
  140. package/library/recipes/runner/smoke.extension.recipe.json +23 -24
  141. package/library/recipes/runner/smoke.mobile.recipe.json +23 -24
  142. package/library/recipes/wallet/smoke.extension.recipe.json +40 -0
  143. package/library/recipes/wallet/smoke.mobile.recipe.json +40 -0
  144. package/package.json +12 -5
  145. package/scripts/completions.sh +8 -11
  146. package/scripts/validate-human-outcomes.mjs +169 -0
  147. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
  148. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
  149. package/adapters/mobile/overlay/compat/README.md +0 -28
  150. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
  151. package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
  152. package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
  153. package/adapters/shared/resolve-farmslot-ports.sh +0 -7
  154. package/dist/adapters/extension/harness-freshness.js +0 -39
  155. package/dist/commands/flows.js +0 -62
  156. package/docs/ADAPTER-SURFACE.md +0 -119
  157. package/docs/CHEATSHEET.md +0 -61
  158. package/docs/CLI-SPEC.md +0 -1098
  159. package/docs/CODE-MAP.md +0 -62
  160. package/docs/DEBUG-HANDOVER.md +0 -36
  161. package/docs/MENTAL-MODEL.md +0 -295
  162. package/docs/UX-PRINCIPLES.md +0 -64
  163. package/docs/architecture.md +0 -398
  164. package/docs/live-adapter-contract.md +0 -188
  165. package/docs/package-boundaries.md +0 -47
  166. package/docs/perps-flow-catalog.md +0 -235
  167. package/docs/recipe-libraries.md +0 -301
  168. package/docs/runtime-file-conventions.md +0 -36
  169. 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
- "schema_version": 1,
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
- "validate": {
6
- "workflow": {
7
- "entry": "status",
8
- "nodes": {
9
- "status": {
10
- "action": "app.status",
11
- "next": "fixture",
12
- "intent": "Perform status for the recipe"
13
- },
14
- "fixture": {
15
- "action": "metamask.wallet.fixture_status",
16
- "next": "cdp",
17
- "intent": "Perform fixture for the recipe"
18
- },
19
- "cdp": {
20
- "action": "cdp.target",
21
- "next": "done",
22
- "intent": "Perform cdp for the recipe"
23
- },
24
- "done": {
25
- "action": "end",
26
- "status": "pass"
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
- "schema_version": 1,
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
- "validate": {
6
- "workflow": {
7
- "entry": "status",
8
- "nodes": {
9
- "status": {
10
- "action": "app.status",
11
- "next": "fixture",
12
- "intent": "Perform status for the recipe"
13
- },
14
- "fixture": {
15
- "action": "metamask.wallet.fixture_status",
16
- "next": "cdp",
17
- "intent": "Perform fixture for the recipe"
18
- },
19
- "cdp": {
20
- "action": "cdp.target",
21
- "next": "done",
22
- "intent": "Perform cdp for the recipe"
23
- },
24
- "done": {
25
- "action": "end",
26
- "status": "pass"
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.17.5",
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.8.0",
21
- "@farmslot/recipe-harness": "^0.5.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
- "esbuild": "0.28.1",
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",
@@ -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|flows>` command (per-checkout cached):
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 + flow refs from the recipe library
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 flows run doctor check checklist recipe-quality install verify cleanup completions"
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
- flows) printf '%s' "--library --json" ;;
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
- _mmh_flows() { local b; b="$(_mmh_bin)" || return 0; "$b" completion-candidates flows 2>/dev/null; }
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 "$(_mmh_flows)" -- "$cur")) ;;
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)$(_mmh_flows)}"); compadd -a candidates; _files ;;
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
+ }