mobile-automator 0.25.0 → 0.26.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 +11 -0
- package/bin/mauto-session-daemon.js +85 -16
- package/package.json +1 -1
- package/src/cli.js +724 -59
- package/src/device/bridge.js +30 -0
- package/src/device/connection.js +23 -4
- package/src/device/crash-model.js +110 -0
- package/src/device/device-call.js +145 -0
- package/src/device/failure-probe.js +276 -0
- package/src/device/mobile-mcp-tools.js +64 -0
- package/src/device/resolve-connection.js +8 -10
- package/src/device/session-daemon.js +265 -51
- package/src/device/session-handle.js +53 -0
- package/src/device/session-log.js +5 -16
- package/src/guide/content/execute.agnostic.md +48 -5
- package/src/guide/content/execute.aware.md +42 -1
- package/src/guide/content/execute.invariants.md +1 -0
- package/src/observe/accepts.js +108 -0
- package/src/observe/event.js +241 -0
- package/src/observe/failure-capture.js +166 -0
- package/src/observe/flush.js +177 -0
- package/src/observe/paths.js +125 -0
- package/src/observe/recorder.js +178 -0
- package/src/observe/settings.js +51 -0
- package/src/observe/sinks/file.js +101 -0
- package/src/observe/sinks/stderr.js +20 -0
- package/src/observe/spool.js +199 -0
- package/src/observe/telemetry.js +143 -0
- package/src/observe/trace.js +174 -0
- package/src/observe/transport.js +169 -0
- package/src/observe/vocab.js +96 -0
- package/src/output/envelope.js +6 -0
- package/src/result/capability-catalog.js +14 -0
- package/src/result/store.js +98 -1
- package/src/schemas/config_schema.json +11 -0
- package/src/schemas/result_schema.json +55 -0
- package/src/setup/scaffold.js +27 -0
- package/src/util/log-rotate.js +34 -0
- package/src/workspace/discover.js +69 -0
|
@@ -203,6 +203,61 @@
|
|
|
203
203
|
},
|
|
204
204
|
"description": "Observations from observer traits (regression spotter, flakiness detector, state detective)"
|
|
205
205
|
},
|
|
206
|
+
"measurements": {
|
|
207
|
+
"type": "object",
|
|
208
|
+
"description": "Facts MEASURED from the run's event trace (mobile-automator/.logs/run-<run_id>.ndjson) rather than self-reported by the executing agent. Absent when no trace was available — MAUTO_RUN_ID unset, logging silenced, no workspace, or a run id that could not name a file.",
|
|
209
|
+
"properties": {
|
|
210
|
+
"source": {
|
|
211
|
+
"type": "string",
|
|
212
|
+
"enum": ["trace", "reported", "none"],
|
|
213
|
+
"description": "Where duration_seconds came from: measured from the trace, taken from --duration, or neither."
|
|
214
|
+
},
|
|
215
|
+
"reported_duration_seconds": {
|
|
216
|
+
"type": ["number", "null"],
|
|
217
|
+
"description": "What --duration claimed, retained even when the measured value was written to duration_seconds instead."
|
|
218
|
+
},
|
|
219
|
+
"duration_disagreement": {
|
|
220
|
+
"type": "boolean",
|
|
221
|
+
"description": "True when the reported and measured durations differ by more than the tolerance. The disagreement is itself a signal and is recorded rather than silently resolved."
|
|
222
|
+
},
|
|
223
|
+
"trace_events": {
|
|
224
|
+
"type": "integer",
|
|
225
|
+
"minimum": 0,
|
|
226
|
+
"description": "Number of events in the run trace."
|
|
227
|
+
},
|
|
228
|
+
"device_failures": {
|
|
229
|
+
"type": "integer",
|
|
230
|
+
"minimum": 0,
|
|
231
|
+
"description": "Device-kind and timeout-kind verb failures recorded during the run. Every one is an attempt that did not take."
|
|
232
|
+
},
|
|
233
|
+
"trace_truncated": {
|
|
234
|
+
"type": "boolean",
|
|
235
|
+
"description": "True when the trace reached its size cap, so the measured duration is a lower bound rather than the run's full span."
|
|
236
|
+
},
|
|
237
|
+
"failure_screenshots": {
|
|
238
|
+
"type": "array",
|
|
239
|
+
"items": { "type": "string" },
|
|
240
|
+
"description": "Screenshots captured automatically at device failures during this run."
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
"crashes": {
|
|
245
|
+
"type": "array",
|
|
246
|
+
"description": "App crash reports observed during this run. Present only when at least one was observed, so a clean run's file is unchanged. Populated by `mauto result add-crash`, typically from `mauto crash list`.",
|
|
247
|
+
"items": {
|
|
248
|
+
"type": "object",
|
|
249
|
+
"properties": {
|
|
250
|
+
"crash_id": { "type": "string", "description": "Device-local report id; pass to `mauto crash get` for the full report" },
|
|
251
|
+
"process": { "type": ["string", "null"], "description": "Package/bundle identifier of the process that died" },
|
|
252
|
+
"timestamp": { "type": ["string", "null"], "description": "The device's own time for the report, when it reports one" },
|
|
253
|
+
"detected_at": { "type": "string", "description": "ISO time at which mauto observed the report" },
|
|
254
|
+
"step_id": { "type": ["string", "null"], "description": "Step in flight when the crash was observed" },
|
|
255
|
+
"excerpt": { "type": ["string", "null"], "description": "Bounded head of the crash report (max 2000 chars); the full report lives at report_path" },
|
|
256
|
+
"report_path": { "type": ["string", "null"], "description": "Path to the full report written by `mauto crash get --out`" }
|
|
257
|
+
},
|
|
258
|
+
"required": ["crash_id", "detected_at"]
|
|
259
|
+
}
|
|
260
|
+
},
|
|
206
261
|
"summary": {
|
|
207
262
|
"type": "string",
|
|
208
263
|
"description": "Human-readable overall summary"
|
package/src/setup/scaffold.js
CHANGED
|
@@ -13,6 +13,21 @@ const manager = require('../config/manager');
|
|
|
13
13
|
|
|
14
14
|
const SUBDIRS = ['scenarios', 'screenshots', 'results', 'memory'];
|
|
15
15
|
|
|
16
|
+
// Runtime artifacts that must never reach a user's git history. `.session/`
|
|
17
|
+
// has held a socket, pidfile and handle since the CLI landed; since #163 it
|
|
18
|
+
// also holds daemon.log, which carries device serials, adb/simctl output and
|
|
19
|
+
// stack traces. `.logs/` holds the structured event stream. Neither is
|
|
20
|
+
// reproducible input and both are noise in a diff.
|
|
21
|
+
//
|
|
22
|
+
// Written only when absent: a user who edits this file owns it thereafter.
|
|
23
|
+
const GITIGNORE_BODY = [
|
|
24
|
+
'# Managed by `mauto setup`. Runtime artifacts — not source.',
|
|
25
|
+
'.session/',
|
|
26
|
+
'.logs/',
|
|
27
|
+
'screenshots/',
|
|
28
|
+
'',
|
|
29
|
+
].join('\n');
|
|
30
|
+
|
|
16
31
|
function scaffold(projectRoot, { mode } = {}) {
|
|
17
32
|
const created = [];
|
|
18
33
|
|
|
@@ -22,6 +37,12 @@ function scaffold(projectRoot, { mode } = {}) {
|
|
|
22
37
|
ensureDir(path.join(baseDir, sub), created);
|
|
23
38
|
}
|
|
24
39
|
|
|
40
|
+
const gitignorePath = path.join(baseDir, '.gitignore');
|
|
41
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
42
|
+
fs.writeFileSync(gitignorePath, GITIGNORE_BODY);
|
|
43
|
+
created.push(gitignorePath);
|
|
44
|
+
}
|
|
45
|
+
|
|
25
46
|
const cfgPath = manager.configPath(projectRoot);
|
|
26
47
|
let configWritten = false;
|
|
27
48
|
if (!fs.existsSync(cfgPath)) {
|
|
@@ -31,6 +52,12 @@ function scaffold(projectRoot, { mode } = {}) {
|
|
|
31
52
|
project_name: null,
|
|
32
53
|
environments: [],
|
|
33
54
|
default_environment: null,
|
|
55
|
+
// Written as a LITERAL false rather than omitted. An absent key that
|
|
56
|
+
// happens to default off is not discoverable; a visible false is, and
|
|
57
|
+
// discoverability is part of consent. The `else` branch below never
|
|
58
|
+
// touches this key: re-running setup must not silently revoke — or
|
|
59
|
+
// silently grant — a choice the project owner already made.
|
|
60
|
+
telemetry: { enabled: false },
|
|
34
61
|
};
|
|
35
62
|
fs.writeFileSync(cfgPath, JSON.stringify(skeleton, null, 2) + '\n');
|
|
36
63
|
created.push(cfgPath);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The codebase's ONE log rotation policy, in one place.
|
|
4
|
+
//
|
|
5
|
+
// mauto keeps two log artifacts with different consumers — the daemon's raw
|
|
6
|
+
// stdio at mobile-automator/.session/daemon.log (#176) and the structured
|
|
7
|
+
// NDJSON event stream at mobile-automator/.logs/ — but they want identical
|
|
8
|
+
// disk hygiene, and the design says so explicitly. This module is what makes
|
|
9
|
+
// that true rather than asserted: previously each file implemented rotation
|
|
10
|
+
// itself, and the two copies had drifted in their failure semantics.
|
|
11
|
+
//
|
|
12
|
+
// Policy: at or above the cap, rename to `<log>.1`, clobbering whatever `.1`
|
|
13
|
+
// was there. A single generation is the intended bound — enough to survive a
|
|
14
|
+
// crash loop that overwrites the interesting part of the live log, cheap to
|
|
15
|
+
// reason about, and it cannot grow without limit.
|
|
16
|
+
|
|
17
|
+
const realFs = require('fs');
|
|
18
|
+
|
|
19
|
+
const MAX_LOG_BYTES = 1024 * 1024;
|
|
20
|
+
|
|
21
|
+
// Best effort, and that is the contract, not a shortcut. Every failure here is
|
|
22
|
+
// survivable — no log yet (ENOENT on stat), a concurrent writer that already
|
|
23
|
+
// rotated (ENOENT on rename), an unwritable directory (EACCES) — while the
|
|
24
|
+
// caller's actual job (opening the daemon log, appending an event) is not.
|
|
25
|
+
// An oversized log beats no log, so a rotation that cannot run is swallowed.
|
|
26
|
+
function rotateIfLarge(logPath, { maxBytes = MAX_LOG_BYTES, fs = realFs } = {}) {
|
|
27
|
+
try {
|
|
28
|
+
if (fs.statSync(logPath).size >= maxBytes) fs.renameSync(logPath, `${logPath}.1`);
|
|
29
|
+
} catch (_) {
|
|
30
|
+
/* nothing to rotate, or someone beat us to it */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { MAX_LOG_BYTES, rotateIfLarge };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Upward workspace discovery (#188).
|
|
4
|
+
//
|
|
5
|
+
// Every neighbouring tool a developer runs inside a repo — git, npm, cargo —
|
|
6
|
+
// walks up from cwd to find its root. mauto used to join `<cwd>/mobile-automator`
|
|
7
|
+
// and stop, so from any subdirectory it reported a confident, well-formed answer
|
|
8
|
+
// about an empty workspace (`config get` with the value silently absent, an
|
|
9
|
+
// empty memory store). This module is the walk; src/cli.js decides what each
|
|
10
|
+
// verb does with its answer.
|
|
11
|
+
//
|
|
12
|
+
// Pure apart from `fs.existsSync`, which is injectable so the walk's
|
|
13
|
+
// termination can be tested without a real filesystem root.
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
const { fail } = require('../output/envelope');
|
|
18
|
+
|
|
19
|
+
const WORKSPACE_DIRNAME = 'mobile-automator';
|
|
20
|
+
|
|
21
|
+
// The MARKER is `mobile-automator/config.json`, not a bare `mobile-automator/`
|
|
22
|
+
// directory. This repository is itself usually cloned into a folder named
|
|
23
|
+
// `mobile-automator`, so a bare-directory check would make every checkout's
|
|
24
|
+
// parent look like a workspace. `mauto setup` always writes config.json, so a
|
|
25
|
+
// real workspace always carries it.
|
|
26
|
+
function markerPath(dir) {
|
|
27
|
+
return path.join(dir, WORKSPACE_DIRNAME, 'config.json');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Walk up from `startDir` and return the NEAREST directory holding a workspace.
|
|
31
|
+
// `searched` lists every directory checked, in order, so a caller that finds
|
|
32
|
+
// nothing can tell the user exactly where it looked.
|
|
33
|
+
//
|
|
34
|
+
// The walk stops AFTER checking a directory that contains `.git` (a directory,
|
|
35
|
+
// or a file — worktrees and submodules have a `.git` file). A workspace belongs
|
|
36
|
+
// to a project, and a project does not extend past its repository: without the
|
|
37
|
+
// boundary, a nested checkout with no workspace of its own would silently
|
|
38
|
+
// adopt whichever unrelated workspace happens to sit above it. The check is
|
|
39
|
+
// inclusive — a workspace at the repository root is the common case.
|
|
40
|
+
function findWorkspaceRoot(startDir, { fs = require('fs') } = {}) {
|
|
41
|
+
const searched = [];
|
|
42
|
+
let dir = path.resolve(startDir);
|
|
43
|
+
for (;;) {
|
|
44
|
+
searched.push(dir);
|
|
45
|
+
if (fs.existsSync(markerPath(dir))) return { root: dir, searched };
|
|
46
|
+
if (fs.existsSync(path.join(dir, '.git'))) break;
|
|
47
|
+
const parent = path.dirname(dir);
|
|
48
|
+
if (parent === dir) break; // filesystem root
|
|
49
|
+
dir = parent;
|
|
50
|
+
}
|
|
51
|
+
return { root: null, searched };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The honest answer when discovery found nothing, for a verb that cannot work
|
|
55
|
+
// without a workspace. `environment` (exit 5), matching the CLI's other
|
|
56
|
+
// fixable-by-the-caller faults: an agent reading `ok:true` with a missing value
|
|
57
|
+
// cannot tell "unset" from "wrong directory", so this must not be a success.
|
|
58
|
+
function noWorkspaceFailure(searched) {
|
|
59
|
+
return {
|
|
60
|
+
envelope: fail(
|
|
61
|
+
'environment',
|
|
62
|
+
`no ${WORKSPACE_DIRNAME}/ workspace found (looked for ${WORKSPACE_DIRNAME}/config.json)`,
|
|
63
|
+
`Searched: ${searched.join(', ')}. Run \`mauto setup\` from your project root, or cd into the project that has one.`
|
|
64
|
+
),
|
|
65
|
+
exitKind: 'environment',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { findWorkspaceRoot, noWorkspaceFailure, markerPath, WORKSPACE_DIRNAME };
|