@deeeed/metamask-harness 0.17.5 → 0.18.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 (132) hide show
  1. package/CHANGELOG.md +872 -0
  2. package/README.md +94 -140
  3. package/adapters/extension/console-tail.mjs +55 -24
  4. package/adapters/extension/ensure-browser.sh +6 -1
  5. package/adapters/extension/inject.mjs +6 -9
  6. package/adapters/extension/launch-browser.cjs +7 -1
  7. package/adapters/extension/launch-webpack.cjs +83 -0
  8. package/adapters/extension/launch.sh +8 -11
  9. package/adapters/extension/lib/chrome-args.cjs +8 -1
  10. package/adapters/extension/lib/macos-focus.cjs +32 -0
  11. package/adapters/extension/live.sh +12 -21
  12. package/adapters/extension/reattach.sh +1 -0
  13. package/adapters/extension/seed-fixture.sh +4 -12
  14. package/adapters/extension/sidepanel-toggle.sh +4 -1
  15. package/adapters/extension/stamp-runtime-title.cjs +58 -0
  16. package/adapters/extension/start-watch.sh +17 -4
  17. package/adapters/extension/stop-viewers.sh +1 -1
  18. package/adapters/extension/sync-webpack-dist.cjs +107 -0
  19. package/adapters/extension/verify.sh +1 -1
  20. package/adapters/manifest.json +53 -37
  21. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
  22. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  23. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
  24. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
  25. package/adapters/mobile/cleanup.sh +16 -176
  26. package/adapters/mobile/inject.sh +90 -671
  27. package/adapters/mobile/launch-console-forwarder.cjs +39 -0
  28. package/adapters/mobile/open-device.sh +45 -7
  29. package/adapters/mobile/start-console-forwarder.sh +70 -0
  30. package/adapters/mobile/start-metro.sh +0 -41
  31. package/adapters/mobile/verify.sh +19 -64
  32. package/adapters/mobile/wait-for-bridge.sh +22 -8
  33. package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
  34. package/adapters/shared/resolve-slot-ports.sh +17 -5
  35. package/adapters/shared/update-check-worker.mjs +43 -0
  36. package/bin/mm-harness +21 -2
  37. package/dist/adapters/core/surface.js +4 -1
  38. package/dist/adapters/extension/console-capture.js +95 -0
  39. package/dist/adapters/extension/product-config.js +110 -0
  40. package/dist/adapters/extension/runtime-decision.js +20 -2
  41. package/dist/adapters/extension/surface.js +22 -3
  42. package/dist/adapters/mobile/perps-env.js +43 -3
  43. package/dist/adapters/mobile/prepare.js +39 -7
  44. package/dist/adapters/mobile/surface.js +5 -2
  45. package/dist/adapters/resolve-slot-ports.js +2 -2
  46. package/dist/adapters/slot-ports.js +13 -16
  47. package/dist/adapters.js +56 -16
  48. package/dist/checkout-lock.js +27 -2
  49. package/dist/cli-color.js +19 -0
  50. package/dist/cli-commands.js +1 -1
  51. package/dist/cli.js +7 -14
  52. package/dist/command-contract.js +451 -0
  53. package/dist/command-journal.js +225 -0
  54. package/dist/commands/call.js +170 -50
  55. package/dist/commands/check.js +9 -3
  56. package/dist/commands/completion-candidates.js +20 -13
  57. package/dist/commands/device-target.js +27 -12
  58. package/dist/commands/doctor.js +106 -25
  59. package/dist/commands/fixtures.js +92 -34
  60. package/dist/commands/flows.js +39 -10
  61. package/dist/commands/last.js +52 -0
  62. package/dist/commands/launch/extension.js +38 -15
  63. package/dist/commands/launch/index.js +207 -63
  64. package/dist/commands/list-executables.js +151 -29
  65. package/dist/commands/logs.js +8 -6
  66. package/dist/commands/manifest.js +270 -35
  67. package/dist/commands/parse-args.js +13 -1
  68. package/dist/commands/provision.js +10 -3
  69. package/dist/commands/run-engine.js +435 -105
  70. package/dist/commands/run-report.js +12 -3
  71. package/dist/commands/run.js +355 -50
  72. package/dist/commands/shared.js +75 -3
  73. package/dist/commands/status-probe.js +4 -1
  74. package/dist/commands/status.js +2 -1
  75. package/dist/commands/stop.js +7 -2
  76. package/dist/commands/update.js +16 -28
  77. package/dist/completions-cache.js +1 -1
  78. package/dist/doctor.js +57 -10
  79. package/dist/harness.js +47 -137
  80. package/dist/heal-bounds.js +2 -2
  81. package/dist/json-stream.js +57 -0
  82. package/dist/live-adapter-contract.js +138 -32
  83. package/dist/manifest.js +161 -1
  84. package/dist/mm-harness-cli.js +135 -54
  85. package/dist/paths.js +2 -5
  86. package/dist/recipe-security.js +178 -0
  87. package/dist/run-diagnostics.js +261 -0
  88. package/dist/runner.js +117 -8
  89. package/docs/CONTRIBUTING.md +137 -0
  90. package/docs/QA.md +185 -0
  91. package/docs/RECIPES.md +161 -0
  92. package/docs/SECURITY.md +88 -0
  93. package/library/README.md +4 -0
  94. package/library/actions/core/perps/_controller.mjs +10 -55
  95. package/library/actions/core/perps/read_account.mjs +2 -2
  96. package/library/actions/core/perps/read_orders.mjs +2 -1
  97. package/library/actions/core/perps/read_positions.mjs +2 -1
  98. package/library/actions/core/wallet/list_accounts.mjs +95 -0
  99. package/library/actions/extension/platform/cdp.mjs +1 -0
  100. package/library/actions/extension/wallet/list_accounts.mjs +41 -0
  101. package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
  102. package/library/manifests/core.action-manifest.json +68 -7
  103. package/library/manifests/extension.action-manifest.json +53 -0
  104. package/library/manifests/mobile.action-manifest.json +81 -3
  105. package/library/recipes/runner/action-validation.extension.recipe.json +8 -1
  106. package/library/recipes/runner/action-validation.mobile.recipe.json +8 -1
  107. package/library/recipes/runner/smoke.core.recipe.json +27 -0
  108. package/library/recipes/wallet/smoke.extension.recipe.json +42 -0
  109. package/library/recipes/wallet/smoke.mobile.recipe.json +42 -0
  110. package/package.json +12 -5
  111. package/scripts/completions.sh +7 -7
  112. package/scripts/validate-human-outcomes.mjs +169 -0
  113. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
  114. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
  115. package/adapters/mobile/overlay/compat/README.md +0 -28
  116. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
  117. package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
  118. package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
  119. package/adapters/shared/resolve-farmslot-ports.sh +0 -7
  120. package/docs/ADAPTER-SURFACE.md +0 -119
  121. package/docs/CHEATSHEET.md +0 -61
  122. package/docs/CLI-SPEC.md +0 -1098
  123. package/docs/CODE-MAP.md +0 -62
  124. package/docs/DEBUG-HANDOVER.md +0 -36
  125. package/docs/MENTAL-MODEL.md +0 -295
  126. package/docs/UX-PRINCIPLES.md +0 -64
  127. package/docs/architecture.md +0 -398
  128. package/docs/live-adapter-contract.md +0 -188
  129. package/docs/package-boundaries.md +0 -47
  130. package/docs/perps-flow-catalog.md +0 -235
  131. package/docs/recipe-libraries.md +0 -301
  132. package/docs/runtime-file-conventions.md +0 -36
@@ -0,0 +1,42 @@
1
+ {
2
+ "schema_version": 1,
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
+ "validate": {
6
+ "workflow": {
7
+ "entry": "status",
8
+ "nodes": {
9
+ "status": {
10
+ "action": "app.status",
11
+ "next": "fixture",
12
+ "intent": "Confirm the Mobile runtime is reachable"
13
+ },
14
+ "fixture": {
15
+ "action": "metamask.wallet.fixture_status",
16
+ "next": "unlock",
17
+ "intent": "Confirm the checkout wallet fixture is ready"
18
+ },
19
+ "unlock": {
20
+ "action": "metamask.wallet.ensure_unlocked",
21
+ "next": "state",
22
+ "intent": "Ensure the fixture-backed wallet is unlocked"
23
+ },
24
+ "state": {
25
+ "action": "metamask.wallet.read_state",
26
+ "next": "screenshot",
27
+ "intent": "Read the current wallet state through the public adapter"
28
+ },
29
+ "screenshot": {
30
+ "action": "ui.screenshot",
31
+ "path": "screenshots/wallet-smoke.png",
32
+ "next": "done",
33
+ "intent": "Capture visual wallet evidence"
34
+ },
35
+ "done": {
36
+ "action": "end",
37
+ "status": "pass"
38
+ }
39
+ }
40
+ }
41
+ }
42
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.17.5",
3
+ "version": "0.18.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.10.0",
24
+ "@farmslot/recipe-harness": "^0.8.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
  #
@@ -41,9 +41,9 @@ _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" ;;
44
+ call) printf '%s' "--list --arg --adapter --target --artifacts-dir --action-manifest --heal --json" ;;
45
45
  flows) printf '%s' "--library --json" ;;
46
- run) printf '%s' "--plan --adapter --artifacts-dir --target --library --cdp-port --heal --record-video --json" ;;
46
+ run) printf '%s' "--list --describe --plan --adapter --artifacts-dir --target --library --cdp-port --heal --record-video --json" ;;
47
47
  doctor) printf '%s' "--adapter --target --fix --json" ;;
48
48
  install|verify|cleanup) printf '%s' "--adapter --target --json" ;;
49
49
  *) printf '%s' "" ;;
@@ -52,7 +52,7 @@ _mmh_flags_for() {
52
52
 
53
53
  # Dynamic candidates from the DISCOVER layer (silent empty on any failure).
54
54
  _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; }
55
+ _mmh_recipes() { local b; b="$(_mmh_bin)" || return 0; "$b" completion-candidates recipes 2>/dev/null; }
56
56
 
57
57
  # ── Bash ────────────────────────────────────────────────────────────────
58
58
  if [ -n "${BASH_VERSION:-}" ]; then
@@ -82,7 +82,7 @@ if [ -n "${BASH_VERSION:-}" ]; then
82
82
  completions) [ "$cword" -eq 2 ] && COMPREPLY=($(compgen -W "install bash zsh status" -- "$cur")) ;;
83
83
  flows) [ "$cword" -eq 2 ] && COMPREPLY=($(compgen -W "list promote" -- "$cur")) ;;
84
84
  call) [ "$cword" -eq 2 ] && COMPREPLY=($(compgen -W "$(_mmh_actions)" -- "$cur")) ;;
85
- run) COMPREPLY=($(compgen -f -- "$cur"; compgen -W "$(_mmh_flows)" -- "$cur")) ;;
85
+ run) COMPREPLY=($(compgen -f -- "$cur"; compgen -W "$(_mmh_recipes)" -- "$cur")) ;;
86
86
  *) COMPREPLY=($(compgen -W "$(_mmh_flags_for "$cmd")" -- "$cur")) ;;
87
87
  esac
88
88
  }
@@ -117,7 +117,7 @@ if [ -n "${ZSH_VERSION:-}" ]; then
117
117
  completions) (( CURRENT == 3 )) && { candidates=(install bash zsh status); compadd -a candidates; } ;;
118
118
  flows) (( CURRENT == 3 )) && { candidates=(list promote); compadd -a candidates; } ;;
119
119
  call) (( CURRENT == 3 )) && { candidates=("${(@f)$(_mmh_actions)}"); compadd -a candidates; } ;;
120
- run) candidates=("${(@f)$(_mmh_flows)}"); compadd -a candidates; _files ;;
120
+ run) candidates=("${(@f)$(_mmh_recipes)}"); compadd -a candidates; _files ;;
121
121
  *) candidates=(${=$(_mmh_flags_for "$cmd")}); compadd -a candidates ;;
122
122
  esac
123
123
  }
@@ -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
+ }
@@ -1,185 +0,0 @@
1
- import React, { useEffect, useMemo, useState } from 'react';
2
- import { StyleSheet, Text, View } from 'react-native';
3
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
4
- import { FullWindowOverlay } from 'react-native-screens';
5
-
6
- interface Step {
7
- id: string;
8
- intent: string;
9
- status?: string;
10
- progress?: { current?: number; total?: number };
11
- detail?: string;
12
- error?: string;
13
- nodeId?: string;
14
- debug?: { nodeId?: string; proofTarget?: unknown };
15
- }
16
-
17
- // Step bus. The HUD owns the registry so nothing outside this leaf module needs
18
- // to import AgenticService — keeping the bridge off the static import graph, so
19
- // it is dead-code-eliminated from release builds. AgenticService emits through
20
- // `emitStepHud`; the mounted HUD registers its setState as the sink.
21
- let stepSink: ((step: Step | null) => void) | null = null;
22
-
23
- export function emitStepHud(step: Step | null) {
24
- stepSink?.(step);
25
- }
26
-
27
- function registerStepHudCallback(sink: ((step: Step | null) => void) | null) {
28
- stepSink = sink;
29
- }
30
-
31
- function statusForStep(step: Step) {
32
- const id = typeof step.id === 'string' ? step.id : '';
33
- return String(step.status ?? id.split(/\s+/)[0] ?? '').toLowerCase();
34
- }
35
-
36
- function progressForStep(step: Step) {
37
- if (
38
- typeof step.progress?.current === 'number' &&
39
- typeof step.progress?.total === 'number'
40
- ) {
41
- return `${step.progress.current}/${step.progress.total}`;
42
- }
43
- const progressPattern = /\b\d+\s*\/\s*\d+\b/;
44
- const id = typeof step.id === 'string' ? step.id : '';
45
- const match = progressPattern.exec(id);
46
- return match ? match[0].replace(/\s+/g, '') : null;
47
- }
48
-
49
- function badgeTextForStep(step: Step) {
50
- const rawStatus = statusForStep(step);
51
- const status = rawStatus === 'running' ? 'run' : rawStatus;
52
- const progress = progressForStep(step);
53
- return [status || 'run', progress].filter(Boolean).join(' ').toUpperCase();
54
- }
55
-
56
- function statusToneForStep(step: Step) {
57
- const status = statusForStep(step);
58
- if (status === 'fail' || status === 'failed' || status === 'error') {
59
- return 'fail';
60
- }
61
- if (status === 'pass' || status === 'passed' || status === 'success') {
62
- return 'pass';
63
- }
64
- return 'running';
65
- }
66
-
67
- function displayStateForStep(step: Step) {
68
- const intent = step.intent.trim();
69
- const secondary = [
70
- step.error ? `error: ${step.error}` : null,
71
- step.detail && step.detail !== intent ? step.detail : null,
72
- ].filter((part): part is string => Boolean(part));
73
- return {
74
- intent,
75
- secondary: secondary.filter(
76
- (part, index) => secondary.indexOf(part) === index,
77
- ),
78
- };
79
- }
80
-
81
- // Debug-only overlay — intentionally uses hardcoded colors for guaranteed
82
- // contrast on both light and dark themes. Design tokens would defeat the purpose.
83
- /* eslint-disable react-native/no-color-literals, @metamask/design-tokens/color-no-hex */
84
- const styles = StyleSheet.create({
85
- container: {
86
- position: 'absolute',
87
- left: 0,
88
- right: 0,
89
- zIndex: 9999,
90
- backgroundColor: 'rgba(0, 0, 0, 0.58)',
91
- paddingVertical: 3,
92
- },
93
- line: {
94
- color: '#FFFFFF',
95
- fontSize: 11,
96
- fontWeight: '700',
97
- lineHeight: 14,
98
- },
99
- badgeText: {
100
- fontFamily: 'Courier',
101
- fontSize: 9,
102
- fontWeight: '800',
103
- },
104
- badgeTextRunning: {
105
- color: '#00FF88',
106
- },
107
- badgeTextPass: {
108
- color: '#00FF88',
109
- },
110
- badgeTextFail: {
111
- color: '#FF4D4F',
112
- },
113
- secondary: {
114
- color: '#E6E6E6',
115
- fontSize: 10,
116
- fontWeight: '400',
117
- lineHeight: 12,
118
- },
119
- });
120
- /* eslint-enable react-native/no-color-literals, @metamask/design-tokens/color-no-hex */
121
-
122
- // Inner component — hooks always called unconditionally, per rules of React.
123
- const AgentStepHudInner = () => {
124
- const [step, setStep] = useState<Step | null>(null);
125
- const insets = useSafeAreaInsets();
126
-
127
- const containerStyle = useMemo(
128
- () => [
129
- styles.container,
130
- {
131
- bottom: Math.max(insets.bottom, 0),
132
- paddingLeft: Math.max(insets.left, 10),
133
- paddingRight: Math.max(insets.right, 10),
134
- },
135
- ],
136
- [insets.left, insets.right, insets.bottom],
137
- );
138
-
139
- useEffect(() => {
140
- registerStepHudCallback(setStep);
141
- return () => {
142
- registerStepHudCallback(null);
143
- };
144
- }, []);
145
-
146
- if (!step) return null;
147
-
148
- const { intent, secondary } = displayStateForStep(step);
149
- const badge = badgeTextForStep(step);
150
- const tone = statusToneForStep(step);
151
- const badgeTextStyle =
152
- tone === 'fail'
153
- ? styles.badgeTextFail
154
- : tone === 'pass'
155
- ? styles.badgeTextPass
156
- : styles.badgeTextRunning;
157
-
158
- // FullWindowOverlay (iOS) renders the HUD in a UIWindow above every native
159
- // layer — including native-stack modal screens (perps close-position/TPSL,
160
- // etc.), which a plain absolute/zIndex View in the JS root cannot reach. On
161
- // Android it is a passthrough, so root-level rendering is preserved.
162
- return (
163
- <FullWindowOverlay>
164
- <View style={containerStyle} pointerEvents="none">
165
- <Text style={styles.line}>
166
- <Text style={[styles.badgeText, badgeTextStyle]}>{badge}</Text>
167
- {intent ? ` ${intent}` : ''}
168
- </Text>
169
- {secondary.map((detail) => (
170
- <Text key={detail} style={styles.secondary}>
171
- {detail}
172
- </Text>
173
- ))}
174
- </View>
175
- </FullWindowOverlay>
176
- );
177
- };
178
-
179
- // Outer guard — never calls hooks, so the __DEV__ early return is fine.
180
- const AgentStepHud = () => {
181
- if (!__DEV__) return null;
182
- return <AgentStepHudInner />;
183
- };
184
-
185
- export default AgentStepHud;