mobile-automator 0.28.2 → 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 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>` | Validate a scenario JSON against the schema |
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.28.2",
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
@@ -11,6 +11,7 @@ const { DeviceBridge } = require('./device/bridge');
11
11
  const selectionStore = require('./device/selection');
12
12
  const { engineHint } = require('./device/engine-hints');
13
13
  const { ScenarioValidator } = require('./scenario/validator');
14
+ const { findUnsupportedActions } = require('./scenario/replayability');
14
15
  const { evaluate, MECHANICAL_TYPES } = require('./assertion/evaluator');
15
16
  const { ResultStore } = require('./result/store');
16
17
  const { OBSERVATION_TYPES, parseObservation, parseCapture, parseBool } = require('./result/flags');
@@ -210,7 +211,7 @@ async function handleScreenshot({ deviceBridge, cwd = process.cwd() }, destPath)
210
211
  }
211
212
  }
212
213
 
213
- function handleValidate({ validator }, file) {
214
+ function handleValidate({ validator }, file, opts = {}) {
214
215
  let raw;
215
216
  try {
216
217
  raw = fs.readFileSync(file, 'utf8');
@@ -241,7 +242,20 @@ function handleValidate({ validator }, file) {
241
242
 
242
243
  const result = validator.validate(scenario);
243
244
  if (result.valid) {
244
- return { envelope: ok({ valid: true }), exitKind: 'ok' };
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' };
245
259
  }
246
260
 
247
261
  const env = fail(
@@ -1754,8 +1768,9 @@ function buildProgram(deps = {}) {
1754
1768
  program
1755
1769
  .command('validate <file>')
1756
1770
  .description('Validate a scenario JSON file against the scenario schema')
1757
- .action(withEnvelope((file) => {
1758
- const r = handleValidate({ validator }, file);
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) });
1759
1774
  emit(r, humanFlag());
1760
1775
  }));
1761
1776
 
@@ -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:** preconditions like `app_uninstalled` and `fresh_install` are handled by the `execute` pre-flight before this workflow runs.
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 to install |
125
- | `uninstall_app` | `mauto uninstall <appId>` | `appId` from config / scenario metadata |
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" | `clear_app_data` | clear app data before scenario | Set `app_state: "fresh_install"` in preconditions |
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" | `clear_app_data` | clear app data before scenario | Also set `app_state: "fresh_install"` in preconditions |
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 (for clear_app_data and uninstall_app)"
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 (e.g., orientation value)"
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, clear_app_data, long_press, double_tap, scroll_to_element, press_back, dismiss_keyboard, grant_permission, deny_permission."
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",