@deeeed/metamask-harness 0.92.21 → 0.92.23
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 +43 -0
- package/dist/adapters/core/surface.js +2 -3
- package/dist/adapters/extension/surface.js +1 -2
- package/dist/adapters/mobile/devices.js +0 -1
- package/dist/adapters/mobile/overlay.js +3 -2
- package/dist/adapters/mobile/prepare-target.js +13 -0
- package/dist/adapters/mobile/surface.js +1 -2
- package/dist/adapters/mobile/ui.js +51 -4
- package/dist/commands/doctor.js +3 -423
- package/dist/commands/review.js +5 -66
- package/dist/doctor.js +0 -11
- package/dist/mm-harness-cli.js +50 -20
- package/dist/paths.js +0 -2
- package/dist/provider-doctor.js +3 -1
- package/dist/provider-helpers.js +143 -1
- package/dist/provider-lifecycle.js +43 -1
- package/dist/review/knowledge.js +0 -119
- package/docs/CONTRIBUTING.md +3 -2
- package/package.json +3 -3
- package/dist/commands/checklist.js +0 -242
- package/dist/commands/domain.js +0 -57
- package/dist/commands/execution-template.js +0 -28
- package/dist/commands/logs.js +0 -99
- package/dist/commands/prepare.js +0 -433
- package/dist/commands/recipe-quality.js +0 -103
- package/dist/commands/status-watch.js +0 -317
- package/dist/commands/status.js +0 -146
- package/dist/commands/task-init.js +0 -49
- package/dist/task-view.js +0 -332
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.92.23 - 2026-10-18
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Pin `@farmslot/recipe-cli` 0.23.1 and `@farmslot/adapter-web` 0.10.15. A browser-extension launch no longer fails with `spawnSync ps ENOBUFS` on a host whose process list is over 1 MB.
|
|
10
|
+
- `task init` and `execution-template` now run through the shared Farmslot handlers. Their output is unchanged.
|
|
11
|
+
- `logs` now runs through the shared Farmslot handler:
|
|
12
|
+
- `--events` reaches the compact viewer as its argument; `logs` no longer sets `RECIPE_LOG_EVENTS` in its own environment.
|
|
13
|
+
- `--window` needs the dev-server log itself: when it is missing, `logs --window` exits 2 with `The primary log window is unavailable.` instead of opening a window on the missing file.
|
|
14
|
+
- `--full` runs `tail` as its own child, so stopping `mm-harness logs` also stops `tail`.
|
|
15
|
+
- A headless platform that declares logs is tailed; only one without logs is refused, naming that platform instead of always `core`.
|
|
16
|
+
- The "nothing running" error names the requested log only (on Extension it listed every log file).
|
|
17
|
+
- `--json` results include `target`.
|
|
18
|
+
- `recipe-quality` now runs through the shared Farmslot handler: `plan` and `advise` use the detected platform when `--adapter` is absent, instead of exiting 2.
|
|
19
|
+
- `checklist` now runs through the shared Farmslot handler:
|
|
20
|
+
- `mark` with `--checklist` or `--signal` checks the terminal signal and the check-diff gate against those files, not the defaults.
|
|
21
|
+
- `mark <task-dir> start` resumes a task whose signal is `blocked`.
|
|
22
|
+
- An explicit `--checklist` is used even when `checklist-target.json` is invalid.
|
|
23
|
+
- `status` now runs through the shared Farmslot handler. The dashboard is unchanged: it still saves an explicit `--device` choice and shows no command journal.
|
|
24
|
+
- `status --task` and `status --watch` JSON, and each line under `--watch --json`, end with a `context` field (target and slot).
|
|
25
|
+
- The task view no longer reports another checkout's runtime context (an inherited `RECIPE_RUNTIME_CONTEXT`, or one whose `repoRoot` is elsewhere) as this checkout's slot, device and ports.
|
|
26
|
+
- `doctor` now runs through the shared Farmslot handler. It still saves an explicit `--device` choice, and `--print-ready` still prints the Farmslot ready indicator.
|
|
27
|
+
- `--fix` prints one stderr progress line per repair stage, such as `[1/3] runtime-context: done, created, 0s` or `[3/3] host repair: failed, wallet-fixture, 0s`, with a heartbeat on a long stage. Stdout and the JSON are unchanged.
|
|
28
|
+
- A failing recorder without a next step no longer prints `Next: undefined`.
|
|
29
|
+
- Under `--expect-live --json`, a `RECORDER_UNAVAILABLE` error whose recorder gives no next step takes the runtime's next step as its `userAction`.
|
|
30
|
+
- `--fix` waits for a platform repair that returns a promise, and reports a rejected repair as failed instead of fixed.
|
|
31
|
+
- On a platform with both a recorder and live checks, the recorder check comes before the live checks in `checks` and `requiredChecks.failed`.
|
|
32
|
+
- `review` and `domain` now run through the shared Farmslot handler. They keep every library this machine resolves (`RECIPE_LIBRARY_PATH`, `mm-harness config`, sibling checkouts, the fetch cache), `MM_HARNESS_DOMAIN`, and the checklist header, steps and verdict. `help review` is unchanged.
|
|
33
|
+
- `domain` names where a declared domain came from: `domain: declared via --domain` or `domain: declared via MM_HARNESS_DOMAIN` (it printed `declared via --domain or MM_HARNESS_DOMAIN`).
|
|
34
|
+
- `domain --domain <name> --json` outside a review checkout reports `"adapter": "unscoped"`, as `review --json` does (it reported `null`).
|
|
35
|
+
- When `domain` classifies against an `owned-paths.json` with an empty `domain` or invalid globs for the reviewed platform, it exits 2 with `Invalid owned-paths.json: <file>` or `Invalid owned paths for <platform>: <file>` (it exited 1 with a different message). `review checklist --domain` no longer reads the library's `owned-paths.json`.
|
|
36
|
+
- Setup step 2 names `review/antipatterns.<platform>.md` when the library has only that file (it named `review/antipatterns.md`).
|
|
37
|
+
- `prepare` now runs through the shared Farmslot handler. Its steps, readiness record, refusals, Mobile device choice and exit codes are unchanged.
|
|
38
|
+
- Under `--json`, or when stdout or stderr is not a terminal, stderr gets a stage line per step (`[1/3] doctor --fix: started`, then `done` or `failed, <reason>`, with a heartbeat on a long step) and each step's own stage lines, prefixed with the step (`[1/3] doctor --fix › [1/3] runtime-context: done, created`). Stdout is unchanged.
|
|
39
|
+
- In a terminal, the running step's row shows its latest stage line instead of its hint.
|
|
40
|
+
- A platform plugin that declares `readiness.prepare` can be prepared.
|
|
41
|
+
|
|
42
|
+
## 0.92.22 - 2026-10-18
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- Mobile: the recipe HUD no longer satisfies or blocks a text-only `ui.wait_for` whose own HUD text (badge, intent or detail) contains the asserted text. Each poll hides the HUD (an earlier node's HUD included), then checks every 50 ms, for about 1 s, that the intent has left the app tree before it queries. It hides again if a late HUD update brings the HUD back. That node's HUD comes back with the next update. Such a wait reports `hud: "hidden"`. A poll that can't confirm the HUD left decides nothing. If the app itself shows the whole intent text, the wait times out and says so; reword the intent. Before, `expected: absent` never passed and `expected: visible` could pass on the HUD text alone. Waits with a `test_id` and waits that don't collide with the HUD are unchanged.
|
|
47
|
+
|
|
5
48
|
## 0.92.21 - 2026-10-17
|
|
6
49
|
|
|
7
50
|
### Added
|
|
@@ -93,9 +93,8 @@ const coreSurface = defineAdapter({
|
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
95
|
},
|
|
96
|
-
readiness: coreReadiness,
|
|
97
|
-
fixtures: { operations: ["init", "sync", "set"] }
|
|
98
|
-
prepare: corePrepare
|
|
96
|
+
readiness: { ...coreReadiness, prepare: corePrepare },
|
|
97
|
+
fixtures: { operations: ["init", "sync", "set"] }
|
|
99
98
|
});
|
|
100
99
|
adapterRegistry.register(coreSurface);
|
|
101
100
|
export {
|
|
@@ -148,12 +148,11 @@ const extensionSurface = defineAdapter({
|
|
|
148
148
|
actions: extensionActions,
|
|
149
149
|
harness: extensionHarness,
|
|
150
150
|
runtimeContext: extensionRuntimeContext,
|
|
151
|
-
readiness: extensionReadiness,
|
|
151
|
+
readiness: { ...extensionReadiness, prepare: extensionPrepare },
|
|
152
152
|
fixtures: {
|
|
153
153
|
operations: ["init", "sync", "set", "reset", "generate", "finalize"],
|
|
154
154
|
apply: async (context) => (await import("./fixtures.js")).applyExtensionFixture(context)
|
|
155
155
|
},
|
|
156
|
-
prepare: extensionPrepare,
|
|
157
156
|
debug: {
|
|
158
157
|
refuse: ({ devMenu }) => devMenu ? { message: "--dev-menu is mobile-only (RN developer menu). Use --worker on the extension.", userAction: "mm-harness debug --worker" } : void 0,
|
|
159
158
|
args: ({ worker }) => [
|
|
@@ -17,7 +17,6 @@ const mobileDevices = {
|
|
|
17
17
|
renderList: renderMobileDeviceList,
|
|
18
18
|
renderAdditional: renderAdditionalReachableDevices,
|
|
19
19
|
renderLive: renderMobileLiveBlock,
|
|
20
|
-
renderPortHints: renderAndroidPortHints,
|
|
21
20
|
renderHints: (live, paint) => renderAndroidPortHints(live.androidPortHints, paint),
|
|
22
21
|
nextForLive
|
|
23
22
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mobilePlatformFromConfiguredTarget, mobilePlatformFromDevices, readRuntimeContextTarget } from "./prepare-target.js";
|
|
1
|
+
import { mobilePlatformFromConfiguredTarget, mobilePlatformFromDevices, mobileTargetAmbiguityError, readRuntimeContextTarget } from "./prepare-target.js";
|
|
2
2
|
const mobileHarness = {
|
|
3
3
|
install: { entry: "adapters/mobile/inject.sh", fallback: "scripts/inject-mobile-harness.sh" },
|
|
4
4
|
cleanup: { entry: "adapters/mobile/cleanup.sh", fallback: "scripts/cleanup-mobile-harness.sh", fromInstalledRunner: true },
|
|
@@ -18,7 +18,8 @@ const mobileRuntimeContext = {
|
|
|
18
18
|
};
|
|
19
19
|
const mobilePrepare = {
|
|
20
20
|
clearMetro: true,
|
|
21
|
-
devicePlatform: (target, devices) => mobilePlatformFromConfiguredTarget(process.env, readRuntimeContextTarget(target)) ?? mobilePlatformFromDevices(devices)
|
|
21
|
+
devicePlatform: (target, devices) => mobilePlatformFromConfiguredTarget(process.env, readRuntimeContextTarget(target)) ?? mobilePlatformFromDevices(devices),
|
|
22
|
+
ambiguousTarget: mobileTargetAmbiguityError
|
|
22
23
|
};
|
|
23
24
|
export {
|
|
24
25
|
mobileHarness,
|
|
@@ -31,8 +31,21 @@ function mobilePlatformFromDevices(devices) {
|
|
|
31
31
|
];
|
|
32
32
|
return platforms.length === 1 ? platforms[0] : void 0;
|
|
33
33
|
}
|
|
34
|
+
function mobileTargetAmbiguityError() {
|
|
35
|
+
const choices = [
|
|
36
|
+
"1. iOS mm-harness prepare --platform mobile --mobile-platform ios",
|
|
37
|
+
"2. Android mm-harness prepare --platform mobile --mobile-platform android"
|
|
38
|
+
];
|
|
39
|
+
return {
|
|
40
|
+
code: "PREPARE_MOBILE_TARGET_AMBIGUOUS",
|
|
41
|
+
message: "could not infer the mobile target from the connected devices",
|
|
42
|
+
userAction: `choose the mobile target, then rerun this command:
|
|
43
|
+
${choices.join("\n")}`
|
|
44
|
+
};
|
|
45
|
+
}
|
|
34
46
|
export {
|
|
35
47
|
mobilePlatformFromConfiguredTarget,
|
|
36
48
|
mobilePlatformFromDevices,
|
|
49
|
+
mobileTargetAmbiguityError,
|
|
37
50
|
readRuntimeContextTarget
|
|
38
51
|
};
|
|
@@ -107,12 +107,11 @@ const mobileSurface = defineAdapter({
|
|
|
107
107
|
actions: mobileActions,
|
|
108
108
|
harness: mobileHarness,
|
|
109
109
|
runtimeContext: mobileRuntimeContext,
|
|
110
|
-
readiness: mobileReadiness,
|
|
110
|
+
readiness: { ...mobileReadiness, prepare: mobilePrepare },
|
|
111
111
|
fixtures: {
|
|
112
112
|
operations: ["init", "sync", "set", "reset"],
|
|
113
113
|
apply: async (context) => (await import("./fixtures.js")).applyMobileFixture(context)
|
|
114
114
|
},
|
|
115
|
-
prepare: mobilePrepare,
|
|
116
115
|
debug: {
|
|
117
116
|
refuse: ({ worker }) => worker ? { message: "--worker is extension-only (service-worker DevTools). Use --dev-menu on mobile.", userAction: "mm-harness debug --dev-menu" } : void 0,
|
|
118
117
|
args: ({ devMenu }) => [
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
+
import { hudIntentDisplay } from "@farmslot/protocol/recipe";
|
|
3
4
|
import { bridgeCommand, evalAsync, evalSync, MOBILE_BRIDGE_ERROR_CODES, selectBridgeStatusEntry, simulatorScreenshot } from "../../../library/actions/mobile/platform/bridge.mjs";
|
|
4
5
|
import { observeNativeUi } from "../../../library/actions/mobile/platform/observe-ui.mjs";
|
|
5
6
|
import { nativeAgentDeviceStateDir } from "../../../library/actions/mobile/platform/native-session-name.mjs";
|
|
@@ -188,6 +189,16 @@ async function hiddenRouteOnly(input, probeInput, payload, testId, deadline) {
|
|
|
188
189
|
const nativeVisible = visible.items.some((item) => isRecord(item) && item.test_id === testId && item.visible !== false);
|
|
189
190
|
return nativeVisible ? null : { hiddenRoute: true, nativeVisible: false };
|
|
190
191
|
}
|
|
192
|
+
const HUD_HIDE_SETTLE_MS = 1e3;
|
|
193
|
+
function hudIntentCollidingWith(payload, testId, expectedText) {
|
|
194
|
+
if (testId || !expectedText || typeof payload.intent !== "string") return null;
|
|
195
|
+
const intent = hudIntentDisplay(payload.intent).text;
|
|
196
|
+
if (!intent) return null;
|
|
197
|
+
const detail = typeof payload.detail === "string" ? payload.detail : "";
|
|
198
|
+
const needle = expectedText.toLowerCase();
|
|
199
|
+
const inBadge = ["run", "pass", "fail"].some((word) => `${word} 0/0`.includes(needle.replace(/\d+/gu, "0")));
|
|
200
|
+
return inBadge || [intent, detail].some((text) => text.toLowerCase().includes(needle)) ? intent : null;
|
|
201
|
+
}
|
|
191
202
|
async function waitForMobileTarget(input, payload) {
|
|
192
203
|
const timeoutMs = Number(payload.timeout_ms ?? payload.timeoutMs ?? 1e4);
|
|
193
204
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -197,13 +208,43 @@ async function waitForMobileTarget(input, payload) {
|
|
|
197
208
|
...expectedText && textMatch === "contains" ? { textContains: expectedText } : {},
|
|
198
209
|
visibility
|
|
199
210
|
};
|
|
211
|
+
const hudIntent = hudIntentCollidingWith(payload, testId, expectedText);
|
|
200
212
|
const queryExpression = `(function(){
|
|
201
213
|
const api = globalThis.__AGENTIC__;
|
|
202
214
|
if (!api || typeof api.queryUiTarget !== 'function') {
|
|
203
215
|
return Promise.resolve(JSON.stringify({ unsupported: true }));
|
|
204
216
|
}
|
|
205
|
-
|
|
206
|
-
return JSON.stringify(
|
|
217
|
+
const query = function(){
|
|
218
|
+
return Promise.resolve(api.queryUiTarget(${JSON.stringify(query)}));
|
|
219
|
+
};
|
|
220
|
+
const hudIntent = ${JSON.stringify(hudIntent)};
|
|
221
|
+
if (hudIntent === null || typeof api.hideStep !== 'function') {
|
|
222
|
+
return query().then(function(result){ return JSON.stringify(result); });
|
|
223
|
+
}
|
|
224
|
+
const hudShown = function(){
|
|
225
|
+
return Promise.resolve(api.queryUiTarget({ textContains: hudIntent, visibility: 'tree' }))
|
|
226
|
+
.then(function(found){ return Boolean(found && found.present); });
|
|
227
|
+
};
|
|
228
|
+
// Hide, let React commit it, then look: an earlier node's HUD can still be
|
|
229
|
+
// up, and the tree only drops it after the commit. Hide again while this
|
|
230
|
+
// node's intent is shown, since a late HUD update can land after a hide.
|
|
231
|
+
const settle = function(waitedMs){
|
|
232
|
+
api.hideStep();
|
|
233
|
+
return new Promise(function(resolve){ setTimeout(resolve, 50); })
|
|
234
|
+
.then(hudShown)
|
|
235
|
+
.then(function(shown){
|
|
236
|
+
if (!shown) return true;
|
|
237
|
+
if (waitedMs + 50 >= ${HUD_HIDE_SETTLE_MS}) return false;
|
|
238
|
+
return settle(waitedMs + 50);
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
return settle(0).then(function(hidden){
|
|
242
|
+
if (!hidden) return JSON.stringify({ hudShown: true });
|
|
243
|
+
return query().then(function(result){
|
|
244
|
+
return hudShown().then(function(shown){
|
|
245
|
+
return JSON.stringify(shown ? { hudShown: true } : Object.assign({}, result, { hud: 'hidden' }));
|
|
246
|
+
});
|
|
247
|
+
});
|
|
207
248
|
});
|
|
208
249
|
})()`;
|
|
209
250
|
const textExpression = testId ? `(function(){
|
|
@@ -241,6 +282,10 @@ async function waitForMobileTarget(input, payload) {
|
|
|
241
282
|
);
|
|
242
283
|
}
|
|
243
284
|
lastResult = result;
|
|
285
|
+
if (result.hudShown === true) {
|
|
286
|
+
await sleep(100);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
244
289
|
let present = result.present === true;
|
|
245
290
|
let visible = result.visible === true;
|
|
246
291
|
let cachedRoute = null;
|
|
@@ -280,7 +325,8 @@ async function waitForMobileTarget(input, payload) {
|
|
|
280
325
|
visible,
|
|
281
326
|
visibility,
|
|
282
327
|
...cachedRoute ?? {},
|
|
283
|
-
...isRecord(result.rect) ? { rect: result.rect } : {}
|
|
328
|
+
...isRecord(result.rect) ? { rect: result.rect } : {},
|
|
329
|
+
...typeof result.hud === "string" ? { hud: result.hud } : {}
|
|
284
330
|
};
|
|
285
331
|
}
|
|
286
332
|
await sleep(250);
|
|
@@ -291,8 +337,9 @@ async function waitForMobileTarget(input, payload) {
|
|
|
291
337
|
].filter(Boolean).join(" with ");
|
|
292
338
|
const exactReason = textMatch === "exact" ? `; last text=${JSON.stringify(lastText)}` : "";
|
|
293
339
|
const bridgeReason = lastBridgeError ? `; last bridge error=${traceText(lastBridgeError)}` : "";
|
|
340
|
+
const hudReason = lastResult?.hudShown === true ? `; the recipe HUD showing this node's intent never left the app tree, so the text was not checked without it (if the app itself shows the intent text, reword the intent)` : "";
|
|
294
341
|
throw new Error(
|
|
295
|
-
`Timed out waiting for mobile ${target} to be ${expected}; last result=${JSON.stringify(lastResult)}${exactReason}${bridgeReason}.`
|
|
342
|
+
`Timed out waiting for mobile ${target} to be ${expected}; last result=${JSON.stringify(lastResult)}${exactReason}${bridgeReason}${hudReason}.`
|
|
296
343
|
);
|
|
297
344
|
}
|
|
298
345
|
const MOBILE_BRIDGE_HANDLERS = {
|