mobile-automator 0.28.1 → 0.29.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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/cli.js +33 -10
- package/src/device/engine-hints.js +9 -0
- package/src/device/session-spawn.js +5 -0
- package/src/guide/content/execute.agnostic.md +7 -6
- package/src/guide/content/execute.aware.md +5 -4
- package/src/guide/content/generate.agnostic.md +2 -2
- package/src/guide/content/generate.aware.md +3 -3
- package/src/guide/content/generate.invariants.md +1 -1
- package/src/scenario/replayability.js +59 -0
- package/src/schemas/scenario_schema.json +4 -4
package/README.md
CHANGED
|
@@ -140,7 +140,7 @@ Most low-level verbs are called **by the agent**, not by you. The ones you'll ru
|
|
|
140
140
|
| `mauto memory show [--kind <k>] [--scenario <id>]` | Print cross-session memory (run history auto-harvested from results) as raw markdown for the agent to read at run start |
|
|
141
141
|
| `mauto memory add <text> --kind <app-knowledge\|preferences>` | Record a durable agent-learned app fact or user preference (`[asserted]`, de-duped) |
|
|
142
142
|
| `mauto memory forget --kind <k> --match <substr>` | Remove agent-authored memory entries matching a substring (the correction path) |
|
|
143
|
-
| `mauto validate <file
|
|
143
|
+
| `mauto validate <file> [--strict]` | Validate a scenario JSON against the schema; warns on actions that cannot replay (`--strict` fails) |
|
|
144
144
|
| `mauto config get\|set <key> [value]` | Read / update workspace config |
|
|
145
145
|
| **Agent-driven verbs** | `elements`, `tap`, `type`, `swipe`, `press`, `screenshot`, `assert`, `result`, `schema` |
|
|
146
146
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobile-automator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Host-agnostic mauto CLI for AI-driven mobile QA automation — analyzes a mobile project and drives devices through mobile-mcp.",
|
|
5
5
|
"homepage": "https://github.com/sh3lan93/mobile-automator#readme",
|
|
6
6
|
"repository": {
|
package/src/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
4
5
|
const { Command } = require('commander');
|
|
5
6
|
|
|
6
7
|
const { version: PKG_VERSION } = require('../package.json');
|
|
@@ -10,6 +11,7 @@ const { DeviceBridge } = require('./device/bridge');
|
|
|
10
11
|
const selectionStore = require('./device/selection');
|
|
11
12
|
const { engineHint } = require('./device/engine-hints');
|
|
12
13
|
const { ScenarioValidator } = require('./scenario/validator');
|
|
14
|
+
const { findUnsupportedActions } = require('./scenario/replayability');
|
|
13
15
|
const { evaluate, MECHANICAL_TYPES } = require('./assertion/evaluator');
|
|
14
16
|
const { ResultStore } = require('./result/store');
|
|
15
17
|
const { OBSERVATION_TYPES, parseObservation, parseCapture, parseBool } = require('./result/flags');
|
|
@@ -190,9 +192,12 @@ async function handleElements({ deviceBridge }) {
|
|
|
190
192
|
}
|
|
191
193
|
}
|
|
192
194
|
|
|
193
|
-
|
|
195
|
+
// Relative paths resolve against the caller's cwd HERE, not in the daemon: the
|
|
196
|
+
// daemon runs from the project root, which is not where a verb run from a
|
|
197
|
+
// subdirectory meant `./shot.png` (#214).
|
|
198
|
+
async function handleScreenshot({ deviceBridge, cwd = process.cwd() }, destPath) {
|
|
194
199
|
try {
|
|
195
|
-
const savedPath = await deviceBridge.screenshot(destPath);
|
|
200
|
+
const savedPath = await deviceBridge.screenshot(path.resolve(cwd, destPath));
|
|
196
201
|
return { envelope: ok({ path: savedPath }), exitKind: 'ok' };
|
|
197
202
|
} catch (err) {
|
|
198
203
|
return {
|
|
@@ -206,7 +211,7 @@ async function handleScreenshot({ deviceBridge }, destPath) {
|
|
|
206
211
|
}
|
|
207
212
|
}
|
|
208
213
|
|
|
209
|
-
function handleValidate({ validator }, file) {
|
|
214
|
+
function handleValidate({ validator }, file, opts = {}) {
|
|
210
215
|
let raw;
|
|
211
216
|
try {
|
|
212
217
|
raw = fs.readFileSync(file, 'utf8');
|
|
@@ -237,7 +242,20 @@ function handleValidate({ validator }, file) {
|
|
|
237
242
|
|
|
238
243
|
const result = validator.validate(scenario);
|
|
239
244
|
if (result.valid) {
|
|
240
|
-
|
|
245
|
+
// Schema-valid is not the same as replayable: surface actions the catalog
|
|
246
|
+
// marks unsupported. A warning by default so existing scenarios keep
|
|
247
|
+
// validating; `--strict` makes it a failure (#166).
|
|
248
|
+
const warnings = findUnsupportedActions(scenario);
|
|
249
|
+
if (warnings.length === 0) {
|
|
250
|
+
return { envelope: ok({ valid: true }), exitKind: 'ok' };
|
|
251
|
+
}
|
|
252
|
+
const hint = `${warnings.length} action(s) cannot replay unattended — see data.warnings`;
|
|
253
|
+
if (opts.strict) {
|
|
254
|
+
const env = fail('invalid_input', 'scenario uses actions with no execution path', hint);
|
|
255
|
+
env.data = { valid: false, errors: [], warnings };
|
|
256
|
+
return { envelope: env, exitKind: 'invalid_input' };
|
|
257
|
+
}
|
|
258
|
+
return { envelope: ok({ valid: true, warnings }, hint), exitKind: 'ok' };
|
|
241
259
|
}
|
|
242
260
|
|
|
243
261
|
const env = fail(
|
|
@@ -380,7 +398,10 @@ async function handleLaunch({ deviceBridge }, appId) {
|
|
|
380
398
|
}
|
|
381
399
|
}
|
|
382
400
|
|
|
383
|
-
|
|
401
|
+
// Resolved against the caller's cwd for the same reason as handleScreenshot
|
|
402
|
+
// (#214). The empty-path guard runs first: path.resolve(cwd, '') is the cwd
|
|
403
|
+
// itself, which would turn a missing argument into a bogus install attempt.
|
|
404
|
+
async function handleInstall({ deviceBridge, cwd = process.cwd() }, appPath) {
|
|
384
405
|
const p = String(appPath == null ? '' : appPath);
|
|
385
406
|
if (!p) {
|
|
386
407
|
return {
|
|
@@ -388,9 +409,10 @@ async function handleInstall({ deviceBridge }, appPath) {
|
|
|
388
409
|
exitKind: 'invalid_input',
|
|
389
410
|
};
|
|
390
411
|
}
|
|
412
|
+
const abs = path.resolve(cwd, p);
|
|
391
413
|
try {
|
|
392
|
-
await deviceBridge.installApp(
|
|
393
|
-
return { envelope: ok({ installed:
|
|
414
|
+
await deviceBridge.installApp(abs);
|
|
415
|
+
return { envelope: ok({ installed: abs }), exitKind: 'ok' };
|
|
394
416
|
} catch (err) {
|
|
395
417
|
return deviceFail(err);
|
|
396
418
|
}
|
|
@@ -1746,8 +1768,9 @@ function buildProgram(deps = {}) {
|
|
|
1746
1768
|
program
|
|
1747
1769
|
.command('validate <file>')
|
|
1748
1770
|
.description('Validate a scenario JSON file against the scenario schema')
|
|
1749
|
-
.
|
|
1750
|
-
|
|
1771
|
+
.option('--strict', 'fail (instead of warn) when the scenario uses actions that cannot replay unattended')
|
|
1772
|
+
.action(withEnvelope((file, opts) => {
|
|
1773
|
+
const r = handleValidate({ validator }, file, { strict: Boolean(opts && opts.strict) });
|
|
1751
1774
|
emit(r, humanFlag());
|
|
1752
1775
|
}));
|
|
1753
1776
|
|
|
@@ -1807,7 +1830,7 @@ function buildProgram(deps = {}) {
|
|
|
1807
1830
|
.command('install <path>')
|
|
1808
1831
|
.description('Install an app from a local .apk/.app/.ipa path')
|
|
1809
1832
|
.option('--device <id>', 'target device id')
|
|
1810
|
-
.action(withEnvelope((
|
|
1833
|
+
.action(withEnvelope((appPath, opts) => withBridge(opts.device, (bridge) => handleInstall({ deviceBridge: bridge }, appPath))));
|
|
1811
1834
|
|
|
1812
1835
|
program
|
|
1813
1836
|
.command('uninstall <appId>')
|
|
@@ -66,6 +66,14 @@ function deviceNotFoundHint(id) {
|
|
|
66
66
|
);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// B3 (#214): mobile-mcp's validateOutputPath only allows screenshots under the
|
|
70
|
+
// engine's cwd or the temp dir, and its message calls the former "the current
|
|
71
|
+
// directory". The engine runs from the project root (the daemon is spawned
|
|
72
|
+
// there), which is not necessarily the directory the user ran the verb from.
|
|
73
|
+
const OUTSIDE_ALLOWED_DIRS_HINT =
|
|
74
|
+
'Screenshots must be saved under the project root (the directory containing mobile-automator/) ' +
|
|
75
|
+
'or the system temp directory. Choose a path inside one of them and retry.';
|
|
76
|
+
|
|
69
77
|
function engineHint(message, { mobilecliVersion = resolveMobilecliVersion } = {}) {
|
|
70
78
|
try {
|
|
71
79
|
if (typeof message !== 'string' || !message) return null;
|
|
@@ -74,6 +82,7 @@ function engineHint(message, { mobilecliVersion = resolveMobilecliVersion } = {}
|
|
|
74
82
|
}
|
|
75
83
|
const notFound = /Device "([^"]+)" not found/.exec(message);
|
|
76
84
|
if (notFound) return deviceNotFoundHint(notFound[1]);
|
|
85
|
+
if (/not in the list of allowed directories/.test(message)) return OUTSIDE_ALLOWED_DIRS_HINT;
|
|
77
86
|
return null;
|
|
78
87
|
} catch {
|
|
79
88
|
return null;
|
|
@@ -54,7 +54,12 @@ async function spawnDaemon({
|
|
|
54
54
|
try {
|
|
55
55
|
// The root in argv is an identity marker only (see paths.daemonArgv): env
|
|
56
56
|
// stays the daemon's source of truth, but `ps` can only see argv.
|
|
57
|
+
// cwd: the engine inherits the daemon's cwd, and mobile-mcp's
|
|
58
|
+
// validateOutputPath only allows screenshots under its own process.cwd()
|
|
59
|
+
// (or the temp dir). Pin it to the project root so the allow-list never
|
|
60
|
+
// depends on which subdirectory happened to spawn the daemon (#214).
|
|
57
61
|
const child = spawn(process.execPath, [DAEMON_BIN, ...paths.daemonArgv(projectRoot)], {
|
|
62
|
+
cwd: projectRoot,
|
|
58
63
|
detached: true,
|
|
59
64
|
stdio: log.stdio,
|
|
60
65
|
env,
|
|
@@ -58,18 +58,19 @@ Record each observation with `mauto result add-step --observation <type>:<messag
|
|
|
58
58
|
4. **App under test.**
|
|
59
59
|
- If the user pointed you at a prebuilt app artifact, install it with `mauto install <path>`.
|
|
60
60
|
- Otherwise verify the app is already installed; if it is not, halt and ask the user to install it. This guide never builds.
|
|
61
|
+
- **Fresh install.** If the scenario's `preconditions.app_state` is `fresh_install` and the user pointed you at a prebuilt app artifact, uninstall the app first with `mauto uninstall <appId>` (the app identifier from `mobile-automator/config.json` — `android_package` / `ios_bundle_id`) and only then install that artifact, so the run starts with no app data. Without an artifact, do not uninstall — you could not reinstall the app. Leave it in place and report that the app state was not reset, so a failure that depends on existing app data is not mistaken for a regression.
|
|
61
62
|
- If the install fails because a different build is already on the device, uninstall it with `mauto uninstall <appId>` (the app identifier from `mobile-automator/config.json` — `android_package` / `ios_bundle_id`), install again, and report that app data was wiped — a scenario whose preconditions assume existing app state may now fail for that reason.
|
|
62
63
|
- State which path you took before continuing.
|
|
63
64
|
5. **Validate schema version:** read the `$schema_version` field; it must be `"2.1"`. If absent or unrecognized, report an error and halt.
|
|
64
|
-
6. Validate the scenario with `mauto validate <path>` against `mauto schema scenario`.
|
|
65
|
+
6. Validate the scenario with `mauto validate <path>` against `mauto schema scenario`. If the result lists `data.warnings`, each entry (its `path` names the step or precondition) uses an action that is unsupported on this run: report it as unsupported in the result (see "Not mechanically executable" under step execution) and do not substitute a gesture for it.
|
|
65
66
|
|
|
66
67
|
**Precondition `device_actions`** (from the scenario's `preconditions`, run before step replay):
|
|
67
68
|
|
|
68
|
-
| Device action | `mauto` verb |
|
|
69
|
-
|
|
70
|
-
| `install_app` | `mauto install <path>` |
|
|
71
|
-
| `uninstall_app` | `mauto uninstall <appId>` |
|
|
72
|
-
| `set_orientation` | `mauto orientation <portrait\|landscape>` |
|
|
69
|
+
| Device action | `mauto` verb | Notes |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `install_app` | `mauto install <path>` | `path` = the device action's `value` if set and the file exists, else the build artifact used in pre-flight. If neither is available, report this precondition as not executed |
|
|
72
|
+
| `uninstall_app` | `mauto uninstall <appId>` | `appId` = the device action's `target_package` if set, else the app identifier from `mobile-automator/config.json` (`android_package` / `ios_bundle_id`) for the connected device's platform. If a later `install_app` in the same block has no resolvable path, skip this uninstall too and report both as not executed — never leave the device without the app under test |
|
|
73
|
+
| `set_orientation` | `mauto orientation <portrait\|landscape>` | |
|
|
73
74
|
|
|
74
75
|
The remaining precondition actions are not mechanically executable (see step execution): report them honestly or perform them out of band.
|
|
75
76
|
|
|
@@ -56,18 +56,19 @@ Record each observation with `mauto result add-step --observation <type>:<messag
|
|
|
56
56
|
- **App under test.** If the user pointed you at a prebuilt artifact, install it with `mauto install <path>` and do NOT build — reusing a ready artifact is the point.
|
|
57
57
|
- If no artifact was named: check whether the app is already installed. If it is, say so and ask whether to rebuild before running the build command ({{build_command}}) — a previous run may have just built it. If it is not installed, build and install with the build command ({{build_command}}).
|
|
58
58
|
- If that install fails because a different build of the app is already on the device, uninstall it — run `mauto uninstall` with the app package ({{app_package}}) — then install again, and report that app data was wiped — a scenario whose preconditions assume existing app state may now fail for that reason.
|
|
59
|
+
- **Fresh install.** If the scenario's `preconditions.app_state` is `fresh_install` (read it from the scenario file now), uninstall the app first — `mauto uninstall` with the app package ({{app_package}}) — and only then install, so the run starts with no app data.
|
|
59
60
|
- State which path you took — installed a supplied artifact, or built — before continuing.
|
|
60
61
|
|
|
61
62
|
### 2. Load Scenario
|
|
62
63
|
|
|
63
64
|
- Read the JSON scenario file from `mobile-automator/scenarios/`.
|
|
64
65
|
- **Validate schema version:** Read the `$schema_version` field. It must be `"2.1"`. If absent or unrecognized, report an error and halt.
|
|
65
|
-
- Validate the scenario with `mauto validate <path>` against `mauto schema scenario`.
|
|
66
|
+
- Validate the scenario with `mauto validate <path>` against `mauto schema scenario`. If the result lists `data.warnings`, each entry (its `path` names the step or precondition) uses an action that is unsupported on this run: report it as unsupported in the result, as described under "Not mechanically executable" below, and do not substitute a gesture for it.
|
|
66
67
|
- Read the `metadata` to compare the recording environment against the current execution environment. Note any differences (different device, API level, environment).
|
|
67
68
|
|
|
68
69
|
### 3. Verify Preconditions
|
|
69
70
|
|
|
70
|
-
- **Note:**
|
|
71
|
+
- **Note:** an `app_state` of `fresh_install` is handled in pre-flight (uninstall before the install, see above), and precondition `device_actions` run per the table in step 4.
|
|
71
72
|
- Review the scenario's `preconditions` array to understand the expected starting state.
|
|
72
73
|
- If any preconditions are state-based (e.g., `"user_logged_out"`, `"no_network"`, `"dark_mode_enabled"`), verify or configure them now:
|
|
73
74
|
- For app-state preconditions: launch the app and check/configure the state.
|
|
@@ -121,8 +122,8 @@ For each step in the scenario:
|
|
|
121
122
|
|
|
122
123
|
| Device action | `mauto` verb | Notes |
|
|
123
124
|
|---|---|---|
|
|
124
|
-
| `install_app` | `mauto install <path>` | `path` = the build artifact
|
|
125
|
-
| `uninstall_app` | `mauto uninstall <appId>` | `appId`
|
|
125
|
+
| `install_app` | `mauto install <path>` | `path` = the device action's `value` if set and the file exists, else the build artifact used in pre-flight. If neither is available, report this precondition as not executed |
|
|
126
|
+
| `uninstall_app` | `mauto uninstall <appId>` | `appId` = the device action's `target_package` if set, else the app package ({{app_package}}). If a later `install_app` in the same block has no resolvable path, skip this uninstall too and report both as not executed — never leave the device without the app under test |
|
|
126
127
|
| `set_orientation` | `mauto orientation <portrait\|landscape>` | `value` = target orientation |
|
|
127
128
|
|
|
128
129
|
**Not mechanically executable** — these actions have no `mauto` verb and no underlying primitive. Do NOT improvise a gesture for them: report them honestly in the result (as unsupported on this run) and, if the run genuinely depends on them, perform them manually out of band before continuing.
|
|
@@ -100,7 +100,7 @@ Translate user language to action types and `mauto` verbs:
|
|
|
100
100
|
|---|---|---|---|
|
|
101
101
|
| "open the app", "launch" | `launch_app` | `mauto press` / launch the app under test | |
|
|
102
102
|
| "uninstall", "remove the app" | (precondition `device_action`) | uninstall before scenario | |
|
|
103
|
-
| "clear app data", "fresh install" | `
|
|
103
|
+
| "clear app data", "fresh install" | `uninstall_app` + `install_app` | reset app to a clean state before scenario | Set `app_state: "fresh_install"` in preconditions and add `device_actions`: `[{ "action": "uninstall_app" }, { "action": "install_app" }]`. Leave out `target_package` and `value`: a cross-platform scenario must not bake in one platform's app identifier or build path, and the executor fills both per platform from config and the pre-flight build |
|
|
104
104
|
| "tap", "click", "press" | `tap` | `mauto tap --at <x,y>` | Resolve coords from `mauto elements` |
|
|
105
105
|
| "long press", "hold" | `long_press` | `mauto tap --at <x,y>` (long-press variant) | Resolve coords from `mauto elements` |
|
|
106
106
|
| "double tap", "double click" | `double_tap` | `mauto tap --at <x,y>` (double-tap variant) | Resolve coords from `mauto elements` |
|
|
@@ -202,7 +202,7 @@ After all steps are executed and recorded:
|
|
|
202
202
|
2. Obtain the scenario schema by running `mauto schema scenario`, and assemble the JSON scenario to match it. Include the `tags` array.
|
|
203
203
|
3. **Always include `"$schema_version": "2.1"` as the first field** in agnostic-mode scenarios.
|
|
204
204
|
4. **Use named string IDs** for all steps and assertions (snake_case, e.g., `"id": "tap_login"`).
|
|
205
|
-
5. Validate the assembled scenario with `mauto validate <path>` against `mauto schema scenario`.
|
|
205
|
+
5. Validate the assembled scenario with `mauto validate <path>` against `mauto schema scenario`. If it returns `data.warnings`, revise the scenario to drop those steps, or tell the user the step needs manual handling.
|
|
206
206
|
6. Save to `mobile-automator/scenarios/<scenario_id>.json`.
|
|
207
207
|
7. Present summary:
|
|
208
208
|
> "Scenario saved: `mobile-automator/scenarios/<scenario_id>.json`
|
|
@@ -217,7 +217,7 @@ After all steps are executed and recorded:
|
|
|
217
217
|
- `app_version` — from the installed app
|
|
218
218
|
- `environment` — ask the user or use `default_environment` from `mobile-automator/config.json`
|
|
219
219
|
- Do NOT include `device_model`, `api_level`, or `timestamp` in metadata — these belong in the result schema, not the scenario.
|
|
220
|
-
7. Validate the assembled scenario with `mauto validate <path>` against `mauto schema scenario`.
|
|
220
|
+
7. Validate the assembled scenario with `mauto validate <path>` against `mauto schema scenario`. If it returns `data.warnings`, revise the scenario to drop those steps, or tell the user the step needs manual handling.
|
|
221
221
|
8. Save to `mobile-automator/scenarios/<scenario_id>.json`.
|
|
222
222
|
9. Present summary:
|
|
223
223
|
> "Scenario saved: `mobile-automator/scenarios/<scenario_id>.json`
|
|
@@ -232,7 +232,7 @@ Translate user language to `mauto` verbs and schema actions:
|
|
|
232
232
|
|---|---|---|---|
|
|
233
233
|
| "open the app", "launch" | `launch_app` | `mauto press` / launch the app under test | |
|
|
234
234
|
| "uninstall", "remove the app" | (precondition device_action) | uninstall before scenario | Use `device_actions` in `preconditions` block |
|
|
235
|
-
| "clear app data", "fresh install" | `
|
|
235
|
+
| "clear app data", "fresh install" | `uninstall_app` + `install_app` | reset app to a clean state before scenario | Set `app_state: "fresh_install"` in preconditions and add `device_actions`: `uninstall_app` then `install_app`. Both fields are optional: `target_package` defaults to the config app package ({{app_package}}) and `install_app` `value` defaults to the build installed in pre-flight; set them only to override those |
|
|
236
236
|
| "tap", "click", "press" | `tap` | `mauto tap --at <x,y>` | Resolve `<x,y>` from `mauto elements` |
|
|
237
237
|
| "long press", "hold" | `long_press` | `mauto tap --at <x,y>` (long-press variant) | Resolve coords from `mauto elements` |
|
|
238
238
|
| "double tap", "double click" | `double_tap` | `mauto tap --at <x,y>` (double-tap variant) | Resolve coords from `mauto elements` |
|
|
@@ -268,7 +268,7 @@ Translate user language to `mauto` verbs and schema actions:
|
|
|
268
268
|
- Create assertion step using mapped type from Pass 2b
|
|
269
269
|
- Evaluate via `mauto assert <type> ...` and record with appropriate fields
|
|
270
270
|
6. Combine steps and assertions into ordered scenario
|
|
271
|
-
7. Generate scenario JSON, then `mauto validate <path>`
|
|
271
|
+
7. Generate scenario JSON, then `mauto validate <path>` (if it returns `data.warnings`, revise the scenario to drop those steps, or tell the user the step needs manual handling)
|
|
272
272
|
```
|
|
273
273
|
|
|
274
274
|
### Detecting and Encoding Patterns
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
- **You do NOT plan or suggest steps.** The user tells you exactly what to do; you execute, document, and verify — never add extra steps.
|
|
2
2
|
- Capture a screenshot after every single step. No exceptions.
|
|
3
3
|
- Drive the device **only** through `mauto` verbs; never assume resource-ids or OS-specific element IDs.
|
|
4
|
-
- Assemble the scenario JSON to match `mauto schema scenario`: use named string IDs (snake_case, never integers), then validate with `mauto validate <path>` before finishing.
|
|
4
|
+
- Assemble the scenario JSON to match `mauto schema scenario`: use named string IDs (snake_case, never integers), then validate with `mauto validate <path>` before finishing, and resolve any `data.warnings` it reports.
|
|
5
5
|
- If this workspace has memory, read it first with `mauto memory show`; save durable app facts and user corrections with `mauto memory add`.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Replayability check for scenarios (#166). The scenario schema accepts every
|
|
4
|
+
// action in its enum, but the action catalog marks some `unsupported`: mobile-mcp
|
|
5
|
+
// exposes no primitive for them, so no `mauto` verb can run them and the step
|
|
6
|
+
// can never replay unattended. Schema validity alone therefore overstates what
|
|
7
|
+
// a scenario can do; this module reports the gap.
|
|
8
|
+
//
|
|
9
|
+
// The unsupported set is derived from the catalog, never restated here, so it
|
|
10
|
+
// moves when an action gains (or loses) a device primitive.
|
|
11
|
+
|
|
12
|
+
const { actionsByResolution } = require('../device/action-catalog');
|
|
13
|
+
|
|
14
|
+
function finding(action, path, stepId) {
|
|
15
|
+
const f = {
|
|
16
|
+
code: 'unsupported_action',
|
|
17
|
+
action,
|
|
18
|
+
path,
|
|
19
|
+
message: `${action} has no execution path in mauto; this step cannot replay unattended`,
|
|
20
|
+
};
|
|
21
|
+
if (stepId !== undefined) f.step_id = stepId;
|
|
22
|
+
return f;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Walks a step list depth-first in document order, descending into each
|
|
26
|
+
// step's `sub_steps` (the schema types them as steps, so they nest freely).
|
|
27
|
+
function scanSteps(steps, basePath, unsupported, findings) {
|
|
28
|
+
if (!Array.isArray(steps)) return;
|
|
29
|
+
steps.forEach((step, i) => {
|
|
30
|
+
if (!step) return;
|
|
31
|
+
const path = `${basePath}[${i}]`;
|
|
32
|
+
if (unsupported.has(step.action)) {
|
|
33
|
+
findings.push(finding(step.action, path, step.id));
|
|
34
|
+
}
|
|
35
|
+
scanSteps(step.sub_steps, `${path}.sub_steps`, unsupported, findings);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Returns one finding per use of an unsupported action: steps first (including
|
|
40
|
+
// nested sub_steps, depth-first in document order), then precondition device
|
|
41
|
+
// actions in document order.
|
|
42
|
+
function findUnsupportedActions(scenario) {
|
|
43
|
+
const unsupported = new Set(actionsByResolution('unsupported'));
|
|
44
|
+
const findings = [];
|
|
45
|
+
|
|
46
|
+
scanSteps(scenario && scenario.steps, 'steps', unsupported, findings);
|
|
47
|
+
|
|
48
|
+
const pre = scenario && scenario.preconditions;
|
|
49
|
+
const deviceActions = pre && Array.isArray(pre.device_actions) ? pre.device_actions : [];
|
|
50
|
+
deviceActions.forEach((da, i) => {
|
|
51
|
+
if (da && unsupported.has(da.action)) {
|
|
52
|
+
findings.push(finding(da.action, `preconditions.device_actions[${i}]`));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return findings;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { findUnsupportedActions };
|
|
@@ -140,15 +140,15 @@
|
|
|
140
140
|
"disable_wifi",
|
|
141
141
|
"set_orientation"
|
|
142
142
|
],
|
|
143
|
-
"description": "The setup action to perform"
|
|
143
|
+
"description": "The setup action to perform. enable_wifi, disable_wifi and clear_app_data are accepted by the schema but are not mechanically executable (no device primitive), so they cannot replay unattended; mauto validate warns."
|
|
144
144
|
},
|
|
145
145
|
"target_package": {
|
|
146
146
|
"type": "string",
|
|
147
|
-
"description": "App package to target
|
|
147
|
+
"description": "App package to target, for uninstall_app; defaults to the config app id"
|
|
148
148
|
},
|
|
149
149
|
"value": {
|
|
150
150
|
"type": "string",
|
|
151
|
-
"description": "Value parameter for the action
|
|
151
|
+
"description": "Value parameter for the action: the orientation for set_orientation, or the build path for install_app (defaults to the pre-flight build)"
|
|
152
152
|
},
|
|
153
153
|
"description": {
|
|
154
154
|
"type": "string",
|
|
@@ -247,7 +247,7 @@
|
|
|
247
247
|
"grant_permission",
|
|
248
248
|
"deny_permission"
|
|
249
249
|
],
|
|
250
|
-
"description": "The automation action to perform. Supported types: wait_for_element, wait_for_element_gone, wait_for_loading_complete, capture_value,
|
|
250
|
+
"description": "The automation action to perform. Supported types: wait_for_element, wait_for_element_gone, wait_for_loading_complete, capture_value, long_press, double_tap, scroll_to_element, press_back, dismiss_keyboard, grant_permission, deny_permission. clear_app_data is accepted by the schema but is not mechanically executable: mobile-mcp exposes no primitive for it, so it cannot replay unattended (mauto validate warns)."
|
|
251
251
|
},
|
|
252
252
|
"description": {
|
|
253
253
|
"type": "string",
|