@deeeed/metamask-harness 0.17.4 → 0.17.5
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 +6 -103
- package/README.md +136 -96
- package/adapters/extension/console-tail.mjs +24 -55
- package/adapters/extension/ensure-browser.sh +7 -1
- package/adapters/extension/inject.mjs +15 -5
- package/adapters/extension/launch-browser.cjs +4 -3
- package/adapters/extension/launch.sh +11 -8
- package/adapters/extension/lib/chrome-args.cjs +24 -0
- package/adapters/extension/live.sh +1 -2
- package/adapters/extension/reattach.sh +0 -1
- package/adapters/extension/seed-fixture.sh +12 -4
- package/adapters/extension/sidepanel-toggle.sh +1 -4
- package/adapters/extension/start-watch.sh +4 -17
- package/adapters/extension/stop-viewers.sh +1 -1
- package/adapters/extension/verify.sh +1 -1
- package/adapters/manifest.json +45 -53
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -16
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
- package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +1 -3
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +3 -2
- package/adapters/mobile/cleanup.sh +176 -16
- package/adapters/mobile/inject.sh +671 -90
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
- package/adapters/mobile/overlay/compat/README.md +28 -0
- package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +42 -0
- package/adapters/mobile/start-metro.sh +41 -0
- package/adapters/mobile/verify.sh +61 -4
- package/adapters/mobile/wait-for-bridge.sh +8 -22
- package/adapters/shared/harness-source-fingerprint.mjs +49 -0
- package/adapters/shared/install-repo-deps.sh +1 -5
- package/adapters/shared/open-debug.mjs +184 -103
- package/adapters/shared/resolve-farmslot-ports-core.mjs +3 -0
- package/adapters/shared/resolve-farmslot-ports.mjs +5 -0
- package/adapters/shared/resolve-farmslot-ports.sh +7 -0
- package/adapters/shared/resolve-slot-ports-core.mjs +6 -0
- package/adapters/shared/resolve-slot-ports.sh +7 -2
- package/bin/mm-harness +0 -7
- package/dist/adapters/extension/harness-freshness.js +39 -0
- package/dist/adapters/extension/runtime-decision.js +4 -73
- package/dist/adapters/extension/surface.js +2 -3
- package/dist/adapters/mobile/perps-env.js +3 -43
- package/dist/adapters/mobile/prepare.js +7 -22
- package/dist/adapters/mobile/source-freshness.js +26 -41
- package/dist/adapters/mobile/surface.js +1 -1
- package/dist/adapters/resolve-slot-ports.js +2 -0
- package/dist/adapters/slot-ports.js +0 -16
- package/dist/adapters.js +5 -12
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +12 -6
- package/dist/commands/call.js +22 -56
- package/dist/commands/check.js +3 -9
- package/dist/commands/checklist.js +4 -1
- package/dist/commands/debug.js +31 -38
- package/dist/commands/device-target.js +12 -27
- package/dist/commands/doctor.js +23 -77
- package/dist/commands/fixtures.js +17 -27
- package/dist/commands/launch/extension.js +2 -0
- package/dist/commands/launch/index.js +59 -167
- package/dist/commands/logs.js +6 -8
- package/dist/commands/manifest.js +25 -151
- package/dist/commands/parse-args.js +0 -2
- package/dist/commands/provision.js +3 -10
- package/dist/commands/run-engine.js +69 -71
- package/dist/commands/run-report.js +3 -12
- package/dist/commands/run.js +35 -245
- package/dist/commands/shared.js +3 -55
- package/dist/commands/status-probe.js +1 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/stop.js +2 -7
- package/dist/commands/update.js +28 -16
- package/dist/doctor.js +4 -1
- package/dist/harness.js +134 -43
- package/dist/heal-bounds.js +1 -1
- package/dist/live-adapter-contract.js +20 -6
- package/dist/mm-harness-cli.js +48 -123
- package/dist/paths.js +5 -2
- package/dist/runner.js +2 -37
- package/dist/runtime-context.js +8 -26
- package/docs/ADAPTER-SURFACE.md +112 -19
- package/docs/CHEATSHEET.md +52 -39
- package/docs/CLI-SPEC.md +1098 -0
- package/docs/CODE-MAP.md +1 -1
- package/docs/DEBUG-HANDOVER.md +36 -0
- package/docs/MENTAL-MODEL.md +295 -0
- package/docs/UX-PRINCIPLES.md +1 -3
- package/docs/architecture.md +375 -65
- package/docs/live-adapter-contract.md +2 -4
- package/docs/recipe-libraries.md +275 -45
- package/library/README.md +0 -4
- package/library/actions/core/perps/_controller.mjs +55 -10
- package/library/actions/mobile/platform/bridge.mjs +1 -5
- package/library/manifests/core.action-manifest.json +7 -7
- package/library/manifests/mobile.action-manifest.json +3 -28
- package/package.json +1 -4
- package/adapters/extension/launch-webpack.cjs +0 -83
- package/adapters/extension/stamp-runtime-title.cjs +0 -58
- package/adapters/extension/sync-webpack-dist.cjs +0 -107
- package/adapters/mobile/launch-console-forwarder.cjs +0 -39
- package/adapters/mobile/start-console-forwarder.sh +0 -70
- package/adapters/shared/update-check-worker.mjs +0 -43
- package/dist/adapters/extension/console-capture.js +0 -93
- package/dist/command-contract.js +0 -441
- package/dist/command-journal.js +0 -225
- package/dist/commands/last.js +0 -52
- package/dist/json-stream.js +0 -57
- package/dist/run-diagnostics.js +0 -261
- package/docs/CLI-ERGONOMICS-AUDIT.md +0 -32
- package/docs/CLI-ERGONOMICS-HUMAN-QA.md +0 -64
- package/docs/VIDEO-DEMO-VALIDATION.md +0 -74
- package/library/recipes/runner/smoke.core.recipe.json +0 -27
- package/library/recipes/wallet/smoke.extension.recipe.json +0 -42
- package/library/recipes/wallet/smoke.mobile.recipe.json +0 -42
- package/scripts/validate-human-outcomes.mjs +0 -169
package/docs/recipe-libraries.md
CHANGED
|
@@ -1,71 +1,301 @@
|
|
|
1
|
-
# Recipe
|
|
1
|
+
# Recipe Libraries
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Per-change recipes are throwaway proofs: generated for one PR, run as evidence,
|
|
4
|
+
discarded. A **recipe library** is the durable keep behind them — the reusable
|
|
5
|
+
flows ("set up a funded wallet", "converge Perps to a clean BTC baseline") a
|
|
6
|
+
per-change recipe is composed FROM. Libraries are owned bottom-up: each
|
|
7
|
+
developer accumulates their own, teams share theirs, and this runner ships only
|
|
8
|
+
a minimal canonical set. Nothing here requires central governance to function.
|
|
4
9
|
|
|
5
|
-
|
|
6
|
-
- a **flow** is a reusable parameterized action sequence;
|
|
7
|
-
- a **recipe** is a complete executable proof and owns task acceptance criteria.
|
|
10
|
+
## The three sources and who owns them
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
| Source | Location | Owned by | Contains |
|
|
13
|
+
| --- | --- | --- | --- |
|
|
14
|
+
| Personal | `<farmslot home>/recipe-library` (`FARMSLOT_HOME`, default `~/.farmslot`) | the developer, entirely | the developer's accumulated flow history |
|
|
15
|
+
| Team | a repo the team owns (any host); checked out locally and configured as a source | the team's developers directly — not a MetaMask-governed artifact | richer domain flows the team maintains |
|
|
16
|
+
| Canonical (repo-native) | this runner's `library/` | this runner | minimal, stable primitives only (see `library/flows/`) |
|
|
11
17
|
|
|
12
|
-
|
|
18
|
+
A library is just a directory:
|
|
13
19
|
|
|
14
20
|
```text
|
|
15
|
-
|
|
16
|
-
library.json
|
|
17
|
-
flows
|
|
18
|
-
wallet.flows.json
|
|
19
|
-
recipes/
|
|
20
|
-
onboarding/
|
|
21
|
-
smoke.extension.recipe.json
|
|
21
|
+
<library-root>/
|
|
22
|
+
library.json { "kind": "recipe-library", "schema_version": 1, "name": "..." }
|
|
23
|
+
flows/*.flows.json standard recipe-flow-catalog documents (Recipe Protocol v1 §8)
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
There is no new format: library flows are the same flow-catalog documents a
|
|
27
|
+
recipe can reference through `uses`, and per-change recipes call them with the
|
|
28
|
+
standard `call` node.
|
|
29
|
+
|
|
30
|
+
## Resolution and precedence
|
|
31
|
+
|
|
32
|
+
`mm-harness run` resolves `call` refs from ordered sources; the first
|
|
33
|
+
source that declares a ref wins:
|
|
34
|
+
|
|
35
|
+
1. recipe-local declarations (inline `flows` and explicit `uses` paths) — always win;
|
|
36
|
+
2. your explicit entries: repeatable/colon-separated `--library name=path` or `RECIPE_LIBRARY_PATH` (order = precedence), or, when neither is set, your personal library;
|
|
37
|
+
3. the canonical `metamask` library, appended last.
|
|
38
|
+
|
|
39
|
+
Personal and team flows therefore shadow canonical ones **by default** — that
|
|
40
|
+
is intentional (your history is the point; the canonical set is fallback
|
|
41
|
+
vocabulary, not authority). Shadowing is never silent: it is printed at run
|
|
42
|
+
start, recorded in `summary.json` under `flowResolution`, and the exact
|
|
43
|
+
definitions a run executed are snapshotted into the `resolved-flows.json`
|
|
44
|
+
artifact so reviewers can inspect the evidence without access to your
|
|
45
|
+
libraries.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
mm-harness flows list # every flow across your sources, with provenance
|
|
49
|
+
RECIPE_LIBRARY_PATH="team=~/dev/team-recipes" mm-harness flows list
|
|
50
|
+
mm-harness run recipe.json --adapter extension --artifacts-dir artifacts \
|
|
51
|
+
--library "team=~/dev/team-recipes"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Promoting a flow (building your history)
|
|
55
|
+
|
|
56
|
+
After a successful run, keep a flow that proved useful:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
mm-harness flows promote --from recipe.json --flow perps.my_baseline \
|
|
60
|
+
--run artifacts # stamps provenance.lastVerified from the passing run
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Promotion is the contract gate: the flow must have a `description`, `ensure_*`
|
|
64
|
+
flows must declare a `postcondition`, and the stored flow is stamped with
|
|
65
|
+
provenance (origin recipe, promotion date, and `lastVerified` only when backed
|
|
66
|
+
by a passing run's artifacts). The personal library is created on first
|
|
67
|
+
promote.
|
|
68
|
+
|
|
69
|
+
## Sharing with your team
|
|
70
|
+
|
|
71
|
+
Sharing is a plain PR: copy or promote the flow into the team's library repo
|
|
72
|
+
and let the team review it like any other change. Their repo, their rules —
|
|
73
|
+
there is no registration step and no org approval involved.
|
|
74
|
+
|
|
75
|
+
## Trust and drift
|
|
76
|
+
|
|
77
|
+
Library flows rot as the app changes. Three guards, cheapest first:
|
|
78
|
+
|
|
79
|
+
1. **Postconditions** — a stale flow fails its postcondition at run time and
|
|
80
|
+
fails the recipe loudly; promotion nudges every flow toward asserting its
|
|
81
|
+
outcome.
|
|
82
|
+
2. **Freshness warnings** — `flows list` and run-start resolution flag flows
|
|
83
|
+
whose `lastVerified` is older than 30 days.
|
|
84
|
+
3. **Provenance in evidence** — `flowResolution` + `resolved-flows.json` make
|
|
85
|
+
it visible exactly which flow, from which source, produced a proof.
|
|
86
|
+
|
|
87
|
+
## Rules for the canonical library
|
|
88
|
+
|
|
89
|
+
Additions to `library/flows/` are held to the flow-catalog maintenance rules
|
|
90
|
+
(`docs/perps-flow-catalog.md`, Recipe Protocol v1 §8.1): parameterized
|
|
91
|
+
primitives over name multiplication, a real reusable domain concept per flow,
|
|
92
|
+
and a small flow budget. If a flow is team- or task-specific, it belongs in a
|
|
93
|
+
team or personal library — that is what the precedence order is for.
|
|
94
|
+
`scripts/check.mjs` validates every committed catalog against the action
|
|
95
|
+
manifests.
|
|
96
|
+
|
|
97
|
+
## Your own measured flow: creating a personal recipe library
|
|
98
|
+
|
|
99
|
+
A **measured flow** is a recipe you run the same way every time to watch how long
|
|
100
|
+
each user-visible step takes. The runner has no benchmark verb: timings are just
|
|
101
|
+
the per-node `duration`s in a passing run's `trace.json`, so a flow is comparable
|
|
102
|
+
across runs only when you pin the run (same device, healing off) and keep the node
|
|
103
|
+
graph stable. The repo ships one canonical example,
|
|
104
|
+
`library/recipes/perps/performance.mobile.recipe.json` — unlock → open the Perps
|
|
105
|
+
market list → read live state → open a market detail. This walkthrough copies it
|
|
106
|
+
into a library of your own and retargets it to your journey. A peer engineer can
|
|
107
|
+
follow it verbatim; the same steps run as the `perps.performance-recipe` contract
|
|
108
|
+
test.
|
|
109
|
+
|
|
110
|
+
### 1. Scaffold a personal library
|
|
111
|
+
|
|
112
|
+
A library is a directory with a `library.json` marker. Keep reusable `flows/`
|
|
113
|
+
(referenced via `call`) beside a domain-grouped `recipes/` folder for the full
|
|
114
|
+
flows you run:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
mkdir -p ~/my-recipes/flows ~/my-recipes/recipes/my-perps
|
|
118
|
+
cat > ~/my-recipes/library.json <<'JSON'
|
|
119
|
+
{ "kind": "recipe-library", "schema_version": 1, "name": "mydev", "owner": "mydev" }
|
|
120
|
+
JSON
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
In real use this is discovered for you: with no `--library` flag and no
|
|
124
|
+
`RECIPE_LIBRARY_PATH`, the runner reads your personal library at
|
|
125
|
+
`$FARMSLOT_HOME/recipe-library` (default `~/.farmslot/recipe-library`). The
|
|
126
|
+
explicit `--library mydev=<dir>` form below is the same mechanism, spelled out so
|
|
127
|
+
it works headlessly (CI, a scratch checkout) and so the path is unambiguous.
|
|
128
|
+
|
|
129
|
+
### 2. Copy the canonical recipe as a starting point
|
|
130
|
+
|
|
131
|
+
Copy it out of your runner checkout's `library/recipes/`:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
cp library/recipes/perps/performance.mobile.recipe.json \
|
|
135
|
+
~/my-recipes/recipes/my-perps/performance.mobile.recipe.json
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 3. Edit the nodes to your journey
|
|
139
|
+
|
|
140
|
+
Open the copy and change what you measure while keeping the measured-flow shape.
|
|
141
|
+
Retarget `open-market-detail` to your market and add one extra measured step —
|
|
142
|
+
here, reading live orders on the detail screen:
|
|
143
|
+
|
|
144
|
+
```jsonc
|
|
145
|
+
"open-market-detail": {
|
|
146
|
+
"action": "ui.navigate",
|
|
147
|
+
"page": "perps-market",
|
|
148
|
+
"market": "ETH", // was BTC
|
|
149
|
+
"intent": "Open the ETH Perps market detail screen",
|
|
150
|
+
"next": "read-orders" // was "end"
|
|
151
|
+
},
|
|
152
|
+
"read-orders": { // your extra measured step
|
|
153
|
+
"action": "metamask.perps.read_orders",
|
|
154
|
+
"market": "ETH",
|
|
155
|
+
"intent": "Read live ETH orders on the market detail screen",
|
|
156
|
+
"next": "end"
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The canonical recipe keeps its nodes inline — a clean measured baseline. For
|
|
161
|
+
bigger journeys you can extract repeated setup steps into a personal `flows/`
|
|
162
|
+
segment and `call` it. For example, define an unlock + open-Perps-list segment
|
|
163
|
+
in `~/my-recipes/flows/mydev.flows.json`:
|
|
25
164
|
|
|
26
165
|
```json
|
|
27
166
|
{
|
|
28
|
-
"kind": "recipe-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
167
|
+
"schema_version": 1, "kind": "recipe-flow-catalog", "owner": "mydev",
|
|
168
|
+
"flows": {
|
|
169
|
+
"mydev.open_perps_setup": {
|
|
170
|
+
"version": 1,
|
|
171
|
+
"description": "Unlock the wallet and open the Perps market list.",
|
|
172
|
+
"workflow": {
|
|
173
|
+
"entry": "ensure-unlocked",
|
|
174
|
+
"nodes": {
|
|
175
|
+
"ensure-unlocked": {
|
|
176
|
+
"action": "metamask.wallet.ensure_unlocked",
|
|
177
|
+
"intent": "Unlock the wallet before the Perps journey",
|
|
178
|
+
"next": "open-perps-list"
|
|
179
|
+
},
|
|
180
|
+
"open-perps-list": {
|
|
181
|
+
"action": "ui.navigate",
|
|
182
|
+
"page": "perps",
|
|
183
|
+
"intent": "Open the Perps market list screen",
|
|
184
|
+
"next": "done"
|
|
185
|
+
},
|
|
186
|
+
"done": { "action": "end", "status": "pass" }
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
32
191
|
}
|
|
33
192
|
```
|
|
34
193
|
|
|
35
|
-
|
|
194
|
+
Then replace the two inline setup nodes in your recipe with a single `call` node:
|
|
36
195
|
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
196
|
+
```jsonc
|
|
197
|
+
"setup": {
|
|
198
|
+
"action": "call",
|
|
199
|
+
"ref": "mydev.open_perps_setup",
|
|
200
|
+
"intent": "Run the personal Perps setup segment (unlock + open list)",
|
|
201
|
+
"next": "read-positions"
|
|
202
|
+
}
|
|
42
203
|
```
|
|
43
204
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
205
|
+
The segment validates via `--plan` — the plan step verifies the `call` ref
|
|
206
|
+
resolves from the library. Keep flow segment actions within the mobile action
|
|
207
|
+
surface: the flow catalog format has no `platform`/`adapter` dimension today,
|
|
208
|
+
so a flow using a core-only action (e.g. `command`) will pass `--plan` on mobile
|
|
209
|
+
but fail at live-run time with "No adapter registered for flow action X". There
|
|
210
|
+
is no plan-time cross-adapter enforcement; that gap would require an `adapters`
|
|
211
|
+
field on flow catalog entries — not yet in the protocol.
|
|
47
212
|
|
|
48
|
-
|
|
49
|
-
The first matching flow wins. Runs print provenance and save resolved definitions
|
|
50
|
-
in `resolved-flows.json`, so shadowing is reviewable.
|
|
213
|
+
### 4. Run it and read the timings
|
|
51
214
|
|
|
52
|
-
|
|
215
|
+
Validate statically **by name** first — `run` probes each library source's
|
|
216
|
+
`recipes/` directory in precedence order (personal → team → canonical), so
|
|
217
|
+
`my-perps.performance` resolves from
|
|
218
|
+
`~/my-recipes/recipes/my-perps/performance.mobile.recipe.json` without you
|
|
219
|
+
spelling out the path:
|
|
53
220
|
|
|
54
221
|
```bash
|
|
55
|
-
|
|
222
|
+
# Static validation by NAME — resolves from the personal library via --library.
|
|
223
|
+
mm-harness run my-perps.performance \
|
|
224
|
+
--library mydev=~/my-recipes --plan --adapter mobile
|
|
225
|
+
|
|
226
|
+
# Zero-flag personal-library: when ~/my-recipes is placed at
|
|
227
|
+
# $FARMSLOT_HOME/recipe-library (default ~/.farmslot/recipe-library), the runner
|
|
228
|
+
# discovers it automatically and run-by-name works without --library:
|
|
229
|
+
mm-harness run my-perps.performance --plan --adapter mobile
|
|
230
|
+
|
|
231
|
+
# Pinned live run — same device, healing OFF, so durations are comparable.
|
|
232
|
+
mm-harness run my-perps.performance \
|
|
233
|
+
--library mydev=~/my-recipes \
|
|
234
|
+
--adapter mobile --device <serial> --heal off --artifacts-dir artifacts
|
|
235
|
+
|
|
236
|
+
# Canonical start-state variants — same installed app, no rebuild.
|
|
237
|
+
mm-harness run app.lifecycle.android-smoke \
|
|
238
|
+
--adapter mobile --device <serial> --heal off --artifacts-dir artifacts/lifecycle-smoke
|
|
239
|
+
mm-harness run perps.performance.warm-start \
|
|
240
|
+
--adapter mobile --device <serial> --heal off --artifacts-dir artifacts/warm
|
|
241
|
+
mm-harness run perps.performance.background-resume \
|
|
242
|
+
--adapter mobile --device <serial> --heal off --artifacts-dir artifacts/background
|
|
243
|
+
mm-harness run perps.performance.cold-start \
|
|
244
|
+
--adapter mobile --device <serial> --heal off --artifacts-dir artifacts/cold
|
|
245
|
+
|
|
246
|
+
# Per-node durations to diff across runs (trace.json is an array of entries, or
|
|
247
|
+
# { metadata, entries: [...] }; each entry carries nodeId + durationMs):
|
|
248
|
+
node -e 'const t=require("./artifacts/trace.json"); \
|
|
249
|
+
for (const e of Array.isArray(t)?t:t.entries) console.log(e.nodeId, e.durationMs)'
|
|
56
250
|
```
|
|
57
251
|
|
|
58
|
-
|
|
59
|
-
|
|
252
|
+
A miss with `--library` names the sources that were searched, so you can tell at a
|
|
253
|
+
glance whether a typo or a missing library entry caused the failure.
|
|
254
|
+
|
|
255
|
+
Run `app.lifecycle.android-smoke` first when validating a new Android slot; it
|
|
256
|
+
isolates lifecycle control from wallet setup and Perps navigation. The start-state
|
|
257
|
+
variants use the standard outer `app.lifecycle` action. Every
|
|
258
|
+
variant begins with `app.status` so `trace.json` records an idempotent start
|
|
259
|
+
marker before lifecycle setup and Perps timing nodes. Android background resume
|
|
260
|
+
sends HOME, then relaunches through the Expo dev-client deep link. Cold start
|
|
261
|
+
force-stops the installed package, then launches the same build through the deep
|
|
262
|
+
link.
|
|
263
|
+
|
|
264
|
+
### The measured-flow pattern
|
|
60
265
|
|
|
61
|
-
|
|
266
|
+
Five rules keep timings meaningful and diffable:
|
|
62
267
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
-
|
|
268
|
+
1. **Pin the run** — `--device <serial> --heal off`. Healing retries hide the
|
|
269
|
+
regressions you are trying to measure.
|
|
270
|
+
2. **One node per user-visible step** — a node's `duration` is only a signal when
|
|
271
|
+
it maps to a single thing the user sees.
|
|
272
|
+
3. **Stable, human-meaningful node keys** — operators diff node keys across runs;
|
|
273
|
+
renaming `open-market-detail` breaks every historical comparison.
|
|
274
|
+
4. **No destructive side effects** — a measured flow should be repeatable. The
|
|
275
|
+
canonical recipe stops at read + navigate; add `place_order`/`close` only in a
|
|
276
|
+
personal copy when you deliberately want to measure the trade path.
|
|
277
|
+
5. **Read timings from `trace.json`, not the console** — the trace is the durable
|
|
278
|
+
per-node record; the console is for humans watching the run.
|
|
279
|
+
|
|
280
|
+
### Shadowing, in practice
|
|
281
|
+
|
|
282
|
+
Because `--library` sources resolve `call` refs before the canonical `metamask`
|
|
283
|
+
library, a personal flow named like a canonical one shadows it — your history is
|
|
284
|
+
the point. You can watch this happen on mobile with the segment recipe from the
|
|
285
|
+
walkthrough above:
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
# A mobile recipe with "action": "call", "ref": "mydev.open_perps_setup"
|
|
289
|
+
# fails without the library source…
|
|
290
|
+
mm-harness run my-perps-with-segment --plan --adapter mobile
|
|
291
|
+
# → workflow.unresolved_call_ref
|
|
292
|
+
|
|
293
|
+
# …and resolves once the personal library is on the path:
|
|
294
|
+
mm-harness run my-perps-with-segment --plan --adapter mobile \
|
|
295
|
+
--library "mydev=~/my-recipes"
|
|
296
|
+
# → plan pass
|
|
297
|
+
```
|
|
69
298
|
|
|
70
|
-
|
|
71
|
-
|
|
299
|
+
That resolution — unresolved without the source, `pass` with it — is what the
|
|
300
|
+
`perps.performance-recipe` contract test asserts, alongside the canonical recipe
|
|
301
|
+
resolving by name and the personal mobile copy planning by path and basename.
|
package/library/README.md
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
# Recipe library
|
|
2
2
|
|
|
3
|
-
- An **action** performs one typed operation.
|
|
4
|
-
- A **flow** packages a reusable parameterized sequence of actions.
|
|
5
|
-
- A **recipe** is a complete proof that may call actions and flows.
|
|
6
|
-
|
|
7
3
|
- `actions/<adapter>/<group>/<name>.mjs` — executable action modules.
|
|
8
4
|
**Importing an action module RUNS it**: each ends with `runAdapter(main)` at top
|
|
9
5
|
level, which reads the live-adapter input path from the environment and executes.
|
|
@@ -90,7 +90,12 @@ function buildInfrastructure(stubbed) {
|
|
|
90
90
|
// The fixture has the standard { accounts: [{ type, value, name }] } shape from
|
|
91
91
|
// wallet-fixture.json.sample. The recipe node selects an account by name via
|
|
92
92
|
// `account_name` (default "dev1"). The viem account derived from the fixture
|
|
93
|
-
// entry is the authoritative source for both the address
|
|
93
|
+
// entry is the authoritative source for both the address AND the signing key —
|
|
94
|
+
// no separate MM_TEST_ACCOUNT_ADDRESS env var needed.
|
|
95
|
+
//
|
|
96
|
+
// Env-var fallback: if no fixture is present (e.g. direct CLI invocation),
|
|
97
|
+
// MM_TEST_ACCOUNT_SRP / MM_TEST_ACCOUNT_PRIVATE_KEY + MM_TEST_ACCOUNT_ADDRESS
|
|
98
|
+
// are still accepted for backward compatibility.
|
|
94
99
|
|
|
95
100
|
/**
|
|
96
101
|
* Load wallet-fixture.json and return the named account entry.
|
|
@@ -150,6 +155,26 @@ function viemAccountFromFixtureEntry(entry) {
|
|
|
150
155
|
return privateKeyToAccount(normalized);
|
|
151
156
|
}
|
|
152
157
|
|
|
158
|
+
// Env-var fallback constants (used only when wallet-fixture.json is absent).
|
|
159
|
+
const SIGNER_PRIVATE_KEY_ENV = 'MM_TEST_ACCOUNT_PRIVATE_KEY';
|
|
160
|
+
const SIGNER_MNEMONIC_ENV = 'MM_TEST_ACCOUNT_SRP';
|
|
161
|
+
|
|
162
|
+
function signerFromEnv() {
|
|
163
|
+
const pk = process.env[SIGNER_PRIVATE_KEY_ENV]?.trim();
|
|
164
|
+
if (pk && pk.length > 0) {
|
|
165
|
+
const normalized = pk.startsWith('0x') ? pk : `0x${pk}`;
|
|
166
|
+
if (!/^0x[0-9a-fA-F]{64}$/u.test(normalized)) {
|
|
167
|
+
throw new Error(`${SIGNER_PRIVATE_KEY_ENV} is not a 32-byte hex private key.`);
|
|
168
|
+
}
|
|
169
|
+
return privateKeyToAccount(normalized);
|
|
170
|
+
}
|
|
171
|
+
const mnemonic = process.env[SIGNER_MNEMONIC_ENV]?.trim();
|
|
172
|
+
if (mnemonic && mnemonic.split(/\s+/u).length >= 12) {
|
|
173
|
+
return mnemonicToAccount(mnemonic, { addressIndex: 0 });
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
153
178
|
/**
|
|
154
179
|
* Resolve the account name to use for signing.
|
|
155
180
|
* Precedence: node.account_name → node.account (if not an address) → "dev1".
|
|
@@ -173,7 +198,9 @@ function resolveAccountName(input) {
|
|
|
173
198
|
|
|
174
199
|
/**
|
|
175
200
|
* Resolve the viem signer and EVM address for writes.
|
|
176
|
-
*
|
|
201
|
+
* Primary: wallet-fixture.json account selected by name.
|
|
202
|
+
* Fallback: MM_TEST_ACCOUNT_PRIVATE_KEY / MM_TEST_ACCOUNT_SRP env vars
|
|
203
|
+
* (requires MM_TEST_ACCOUNT_ADDRESS for address verification).
|
|
177
204
|
*
|
|
178
205
|
* @param input - Adapter input (context.projectRoot, node.account_name).
|
|
179
206
|
* @returns { account: ViemAccount, address: string }
|
|
@@ -182,6 +209,7 @@ async function resolveSignerFromFixture(input) {
|
|
|
182
209
|
const projectRoot = input.context?.projectRoot;
|
|
183
210
|
const accountName = resolveAccountName(input);
|
|
184
211
|
|
|
212
|
+
// Primary: fixture
|
|
185
213
|
if (projectRoot) {
|
|
186
214
|
const entry = await loadFixtureAccount(projectRoot, accountName);
|
|
187
215
|
if (entry) {
|
|
@@ -190,15 +218,32 @@ async function resolveSignerFromFixture(input) {
|
|
|
190
218
|
}
|
|
191
219
|
}
|
|
192
220
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
221
|
+
// Fallback: env vars (no fixture present — direct CLI use)
|
|
222
|
+
const account = signerFromEnv();
|
|
223
|
+
if (!account) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`core perps writes require a wallet-fixture.json with account "${accountName}", ` +
|
|
226
|
+
`or env vars ${SIGNER_PRIVATE_KEY_ENV} / ${SIGNER_MNEMONIC_ENV} + MM_TEST_ACCOUNT_ADDRESS.`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
const envAddress = String(process.env.MM_TEST_ACCOUNT_ADDRESS ?? '').trim();
|
|
230
|
+
if (!/^0x[0-9a-fA-F]{40}$/u.test(envAddress)) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`Env-var fallback requires MM_TEST_ACCOUNT_ADDRESS (a 0x EVM address) to verify the signer.`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
if (account.address.toLowerCase() !== envAddress.toLowerCase()) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`Env-var signer derives ${account.address} but MM_TEST_ACCOUNT_ADDRESS is ${envAddress}; signatures would be invalid.`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
return { account, address: envAddress };
|
|
197
241
|
}
|
|
198
242
|
|
|
199
243
|
/**
|
|
200
244
|
* Resolve the account address for reads (no signing required).
|
|
201
|
-
*
|
|
245
|
+
* Primary: wallet-fixture.json account selected by name (address derived from key).
|
|
246
|
+
* Fallback: node.account / node.address / MM_TEST_ACCOUNT_ADDRESS env var.
|
|
202
247
|
*/
|
|
203
248
|
async function requireAccountAddress(input) {
|
|
204
249
|
const projectRoot = input.context?.projectRoot;
|
|
@@ -216,13 +261,13 @@ async function requireAccountAddress(input) {
|
|
|
216
261
|
}
|
|
217
262
|
}
|
|
218
263
|
|
|
219
|
-
//
|
|
264
|
+
// Fallback: explicit address from node or env
|
|
220
265
|
const fromNode = input.node?.account ?? input.node?.address ?? input.node?.userAddress;
|
|
221
|
-
const address = String(fromNode ?? '').trim();
|
|
266
|
+
const address = String(fromNode ?? process.env.MM_TEST_ACCOUNT_ADDRESS ?? '').trim();
|
|
222
267
|
if (!/^0x[0-9a-fA-F]{40}$/u.test(address)) {
|
|
223
268
|
throw new Error(
|
|
224
269
|
`core perps reads require a wallet-fixture.json with account "${accountName}", ` +
|
|
225
|
-
`or a 0x EVM address via node.account.`,
|
|
270
|
+
`or a 0x EVM address via node.account / MM_TEST_ACCOUNT_ADDRESS.`,
|
|
226
271
|
);
|
|
227
272
|
}
|
|
228
273
|
return address;
|
|
@@ -529,14 +529,10 @@ async function androidScreenshot(input, relPath) {
|
|
|
529
529
|
async function resolveAndroidScreenshotSerial(input) {
|
|
530
530
|
const contextEnv = input.context?.env || {};
|
|
531
531
|
const explicit = input.node?.adb_serial ??
|
|
532
|
-
input.node?.android_device ??
|
|
533
|
-
input.node?.device ??
|
|
534
532
|
contextEnv.ADB_SERIAL ??
|
|
535
533
|
contextEnv.ANDROID_SERIAL ??
|
|
536
|
-
contextEnv.ANDROID_DEVICE ??
|
|
537
534
|
process.env.ADB_SERIAL ??
|
|
538
|
-
process.env.ANDROID_SERIAL
|
|
539
|
-
process.env.ANDROID_DEVICE;
|
|
535
|
+
process.env.ANDROID_SERIAL;
|
|
540
536
|
if (explicit !== undefined && explicit !== null && String(explicit).trim()) return String(explicit).trim();
|
|
541
537
|
|
|
542
538
|
let result;
|
|
@@ -446,7 +446,7 @@
|
|
|
446
446
|
},
|
|
447
447
|
"account": {
|
|
448
448
|
"type": "string",
|
|
449
|
-
"description": "EVM address to read positions for; defaults to
|
|
449
|
+
"description": "EVM address to read positions for; defaults to MM_TEST_ACCOUNT_ADDRESS."
|
|
450
450
|
},
|
|
451
451
|
"timeout_ms": {
|
|
452
452
|
"type": "number"
|
|
@@ -523,7 +523,7 @@
|
|
|
523
523
|
},
|
|
524
524
|
"account": {
|
|
525
525
|
"type": "string",
|
|
526
|
-
"description": "EVM address to read orders for; defaults to
|
|
526
|
+
"description": "EVM address to read orders for; defaults to MM_TEST_ACCOUNT_ADDRESS."
|
|
527
527
|
},
|
|
528
528
|
"timeout_ms": {
|
|
529
529
|
"type": "number"
|
|
@@ -558,7 +558,7 @@
|
|
|
558
558
|
},
|
|
559
559
|
"account": {
|
|
560
560
|
"type": "string",
|
|
561
|
-
"description": "EVM address to read account state for; defaults to
|
|
561
|
+
"description": "EVM address to read account state for; defaults to MM_TEST_ACCOUNT_ADDRESS."
|
|
562
562
|
},
|
|
563
563
|
"timeout_ms": {
|
|
564
564
|
"type": "number"
|
|
@@ -653,7 +653,7 @@
|
|
|
653
653
|
},
|
|
654
654
|
"account": {
|
|
655
655
|
"type": "string",
|
|
656
|
-
"description": "EVM address to trade for; defaults to
|
|
656
|
+
"description": "EVM address to trade for; defaults to MM_TEST_ACCOUNT_ADDRESS."
|
|
657
657
|
},
|
|
658
658
|
"timeout_ms": {
|
|
659
659
|
"type": "number"
|
|
@@ -748,7 +748,7 @@
|
|
|
748
748
|
},
|
|
749
749
|
"account": {
|
|
750
750
|
"type": "string",
|
|
751
|
-
"description": "EVM address to close for; defaults to
|
|
751
|
+
"description": "EVM address to close for; defaults to MM_TEST_ACCOUNT_ADDRESS."
|
|
752
752
|
},
|
|
753
753
|
"timeout_ms": {
|
|
754
754
|
"type": "number"
|
|
@@ -837,7 +837,7 @@
|
|
|
837
837
|
},
|
|
838
838
|
"account": {
|
|
839
839
|
"type": "string",
|
|
840
|
-
"description": "EVM address to read for; defaults to
|
|
840
|
+
"description": "EVM address to read for; defaults to MM_TEST_ACCOUNT_ADDRESS."
|
|
841
841
|
},
|
|
842
842
|
"timeout_ms": {
|
|
843
843
|
"type": "number"
|
|
@@ -937,7 +937,7 @@
|
|
|
937
937
|
},
|
|
938
938
|
"account": {
|
|
939
939
|
"type": "string",
|
|
940
|
-
"description": "EVM address to trade for; defaults to
|
|
940
|
+
"description": "EVM address to trade for; defaults to MM_TEST_ACCOUNT_ADDRESS."
|
|
941
941
|
},
|
|
942
942
|
"timeout_ms": {
|
|
943
943
|
"type": "number"
|
|
@@ -156,38 +156,13 @@
|
|
|
156
156
|
]
|
|
157
157
|
},
|
|
158
158
|
"ui.press": {
|
|
159
|
-
"description": "Press a
|
|
160
|
-
"schema": {
|
|
161
|
-
"type": "object",
|
|
162
|
-
"properties": {
|
|
163
|
-
"action": { "const": "ui.press" },
|
|
164
|
-
"test_id": { "type": "string", "minLength": 1 },
|
|
165
|
-
"testID": { "type": "string", "minLength": 1 },
|
|
166
|
-
"selector": { "type": "string", "minLength": 1 },
|
|
167
|
-
"text": { "type": "string", "minLength": 1 }
|
|
168
|
-
},
|
|
169
|
-
"oneOf": [
|
|
170
|
-
{ "required": ["test_id"] },
|
|
171
|
-
{ "required": ["testID"] },
|
|
172
|
-
{ "required": ["selector"] },
|
|
173
|
-
{ "required": ["text"] }
|
|
174
|
-
]
|
|
175
|
-
},
|
|
159
|
+
"description": "Press/tap/click a UI target through the platform adapter.",
|
|
176
160
|
"examples": [
|
|
177
161
|
{
|
|
178
|
-
"description": "Press by stable React Native testID",
|
|
179
|
-
"node": {
|
|
180
|
-
"action": "ui.press",
|
|
181
|
-
"test_id": "perps-tab",
|
|
182
|
-
"intent": "Open Perps through its stable testID"
|
|
183
|
-
}
|
|
184
|
-
},
|
|
185
|
-
{
|
|
186
|
-
"description": "Press a component that is reachable only by visible text",
|
|
187
162
|
"node": {
|
|
188
163
|
"action": "ui.press",
|
|
189
|
-
"
|
|
190
|
-
"intent": "Press
|
|
164
|
+
"target": "Perps",
|
|
165
|
+
"intent": "Press/tap/click a UI target through the platform adapter"
|
|
191
166
|
}
|
|
192
167
|
}
|
|
193
168
|
]
|
package/package.json
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deeeed/metamask-harness",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mm-harness": "bin/mm-harness"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
9
|
"build": "node scripts/build-dist.mjs",
|
|
10
|
-
"audit:cli-ergonomics": "node scripts/generate-cli-ergonomics-audit.mjs",
|
|
11
10
|
"prepack": "npm run build",
|
|
12
11
|
"dev:link-farmslot": "node scripts/link-local-farmslot.mjs",
|
|
13
12
|
"check": "node scripts/check.mjs",
|
|
14
|
-
"qa:human": "node scripts/validate-human-outcomes.mjs",
|
|
15
13
|
"self-test": "bin/mm-harness self-test",
|
|
16
14
|
"manifest:mobile": "bin/mm-harness actions --raw --adapter mobile --json",
|
|
17
15
|
"manifest:extension": "bin/mm-harness actions --raw --adapter extension --json",
|
|
@@ -58,7 +56,6 @@
|
|
|
58
56
|
"library",
|
|
59
57
|
"scripts/completions.sh",
|
|
60
58
|
"scripts/install-completions.sh",
|
|
61
|
-
"scripts/validate-human-outcomes.mjs",
|
|
62
59
|
"!scripts/README.md",
|
|
63
60
|
"docs",
|
|
64
61
|
"README.md",
|