@deeeed/metamask-harness 0.17.2 → 0.17.4

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 (81) hide show
  1. package/CHANGELOG.md +35 -8
  2. package/README.md +12 -6
  3. package/adapters/core/cleanup.sh +0 -0
  4. package/adapters/core/inject.sh +0 -0
  5. package/adapters/extension/cleanup.mjs +0 -0
  6. package/adapters/extension/console-tail.mjs +5 -1
  7. package/adapters/extension/ensure-browser.sh +0 -0
  8. package/adapters/extension/inject.mjs +0 -0
  9. package/adapters/extension/launch-browser.cjs +0 -0
  10. package/adapters/extension/launch.sh +0 -0
  11. package/adapters/extension/live.sh +0 -0
  12. package/adapters/extension/readiness.mjs +0 -0
  13. package/adapters/extension/reattach.sh +0 -0
  14. package/adapters/extension/refresh-build.sh +0 -0
  15. package/adapters/extension/seed-fixture.sh +0 -0
  16. package/adapters/extension/sidepanel-toggle.sh +0 -0
  17. package/adapters/extension/snapshot-dist.sh +0 -0
  18. package/adapters/extension/stamp-runtime-title.cjs +18 -9
  19. package/adapters/extension/start-watch.sh +1 -0
  20. package/adapters/extension/stop-viewers.sh +0 -0
  21. package/adapters/extension/sync-webpack-dist.cjs +9 -7
  22. package/adapters/extension/verify.sh +0 -0
  23. package/adapters/extension/wallet-fixture-state.cjs +0 -0
  24. package/adapters/manifest.json +9 -1
  25. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
  26. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
  27. package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
  28. package/adapters/mobile/cleanup.sh +0 -0
  29. package/adapters/mobile/inject.sh +13 -11
  30. package/adapters/mobile/launch-console-forwarder.cjs +0 -0
  31. package/adapters/mobile/lib/metro-listener.sh +0 -0
  32. package/adapters/mobile/lib/tmux-viewer.sh +0 -0
  33. package/adapters/mobile/open-device.sh +0 -0
  34. package/adapters/mobile/prewarm-bundle.sh +0 -0
  35. package/adapters/mobile/start-console-forwarder.sh +3 -3
  36. package/adapters/mobile/start-metro.sh +0 -0
  37. package/adapters/mobile/stop-metro.sh +0 -0
  38. package/adapters/mobile/verify.sh +0 -0
  39. package/adapters/mobile/wait-for-bridge.sh +22 -8
  40. package/adapters/mobile/yarn-setup.sh +0 -0
  41. package/adapters/shared/activate-repo-node.sh +0 -0
  42. package/adapters/shared/activate-repo-ruby.sh +0 -0
  43. package/adapters/shared/cli-ux.sh +0 -0
  44. package/adapters/shared/ensure-runner-deps.sh +0 -0
  45. package/adapters/shared/harness-path.sh +0 -0
  46. package/adapters/shared/hash-helpers.sh +0 -0
  47. package/adapters/shared/install-repo-deps.sh +0 -0
  48. package/adapters/shared/json-field.sh +0 -0
  49. package/adapters/shared/open-debug.mjs +7 -1
  50. package/adapters/shared/open-log-window.sh +0 -0
  51. package/adapters/shared/reap-checkout-metros.sh +0 -0
  52. package/adapters/shared/resolve-slot-ports.mjs +0 -0
  53. package/adapters/shared/resolve-slot-ports.sh +0 -0
  54. package/adapters/shared/sync-wallet-fixture.sh +0 -0
  55. package/adapters/shared/tmux-session.sh +0 -0
  56. package/adapters/shared/tmux-viewer.sh +0 -0
  57. package/adapters/shared/update-check-worker.mjs +11 -5
  58. package/dist/adapters/extension/console-capture.js +10 -1
  59. package/dist/adapters/mobile/prepare.js +5 -5
  60. package/dist/adapters/slot-ports.js +8 -0
  61. package/dist/cli.js +1 -1
  62. package/dist/commands/doctor.js +33 -8
  63. package/dist/commands/run-engine.js +2 -4
  64. package/dist/commands/update.js +2 -1
  65. package/dist/harness.js +24 -9
  66. package/dist/mm-harness-cli.js +7 -6
  67. package/dist/run-diagnostics.js +1 -6
  68. package/docs/ADAPTER-SURFACE.md +19 -112
  69. package/docs/CLI-ERGONOMICS-HUMAN-QA.md +16 -0
  70. package/docs/UX-PRINCIPLES.md +1 -1
  71. package/docs/VIDEO-DEMO-VALIDATION.md +56 -44
  72. package/docs/recipe-libraries.md +45 -275
  73. package/library/README.md +4 -0
  74. package/library/manifests/core.action-manifest.json +7 -7
  75. package/library/recipes/runner/smoke.core.recipe.json +27 -0
  76. package/library/recipes/wallet/smoke.extension.recipe.json +42 -0
  77. package/library/recipes/wallet/smoke.mobile.recipe.json +42 -0
  78. package/package.json +4 -2
  79. package/scripts/completions.sh +0 -0
  80. package/scripts/install-completions.sh +0 -0
  81. package/scripts/validate-human-outcomes.mjs +169 -0
@@ -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
+ }