zig-mobile-runner 0.1.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/CHANGELOG.md +484 -0
- package/CONTRIBUTING.md +42 -0
- package/FEATURES.md +112 -0
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/SECURITY.md +34 -0
- package/build.zig +38 -0
- package/build.zig.zon +7 -0
- package/clients/README.md +144 -0
- package/clients/go/README.md +24 -0
- package/clients/go/examples/fake-session/main.go +93 -0
- package/clients/go/go.mod +3 -0
- package/clients/go/zmr/client.go +432 -0
- package/clients/kotlin/README.md +35 -0
- package/clients/kotlin/build.gradle.kts +35 -0
- package/clients/kotlin/settings.gradle.kts +15 -0
- package/clients/kotlin/src/main/kotlin/dev/zmr/FakeSession.kt +86 -0
- package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +67 -0
- package/clients/python/README.md +29 -0
- package/clients/python/examples/fake_session.py +48 -0
- package/clients/python/pyproject.toml +13 -0
- package/clients/python/zmr_client.py +202 -0
- package/clients/rust/Cargo.lock +107 -0
- package/clients/rust/Cargo.toml +10 -0
- package/clients/rust/README.md +19 -0
- package/clients/rust/examples/fake_session.rs +70 -0
- package/clients/rust/src/lib.rs +461 -0
- package/clients/swift/Package.swift +16 -0
- package/clients/swift/README.md +36 -0
- package/clients/swift/Sources/ZMRClient/ZMRClient.swift +114 -0
- package/clients/swift/Sources/ZMRFakeSession/main.swift +86 -0
- package/clients/typescript/README.md +34 -0
- package/clients/typescript/examples/fake-session.mjs +36 -0
- package/clients/typescript/index.d.ts +144 -0
- package/clients/typescript/index.mjs +192 -0
- package/clients/typescript/package.json +8 -0
- package/docs/adr/0001-agent-native-runner-boundary.md +31 -0
- package/docs/adr/0002-app-local-zmr-contract.md +39 -0
- package/docs/adr/0003-ios-simulator-xctest-shim.md +41 -0
- package/docs/adr/0004-benchmark-claims-and-baseline-collection.md +37 -0
- package/docs/adr/README.md +12 -0
- package/docs/ai-agents.md +156 -0
- package/docs/app-integration.md +316 -0
- package/docs/benchmarking.md +275 -0
- package/docs/client-installation.md +141 -0
- package/docs/clients.md +98 -0
- package/docs/config.md +175 -0
- package/docs/demo.md +259 -0
- package/docs/dsl.md +57 -0
- package/docs/install.md +233 -0
- package/docs/market-positioning.md +70 -0
- package/docs/npm.md +359 -0
- package/docs/protocol-fixtures/README.md +8 -0
- package/docs/protocol-fixtures/core-session.requests.jsonl +8 -0
- package/docs/protocol-fixtures/core-session.responses.jsonl +8 -0
- package/docs/protocol-versioning.md +65 -0
- package/docs/protocol.md +560 -0
- package/docs/publication.md +77 -0
- package/docs/release-audit.md +99 -0
- package/docs/release-candidate.md +111 -0
- package/docs/release-evidence.md +188 -0
- package/docs/release-notes-template.md +58 -0
- package/docs/roadmap.md +334 -0
- package/docs/scenario-authoring.md +88 -0
- package/docs/shipping.md +170 -0
- package/docs/trace-privacy.md +88 -0
- package/docs/troubleshooting.md +256 -0
- package/examples/android-app-auth-probe.json +89 -0
- package/examples/android-app-error-state.json +13 -0
- package/examples/android-app-login-smoke.json +192 -0
- package/examples/android-app-onboarding.json +12 -0
- package/examples/android-app-referral-deep-link.json +12 -0
- package/examples/android-shim-smoke.json +19 -0
- package/examples/demo-failure.json +12 -0
- package/examples/demo-fake.json +14 -0
- package/examples/ios-dev-client-open-link.json +26 -0
- package/examples/ios-dev-client-route-snapshot.json +24 -0
- package/examples/ios-shim-smoke.json +23 -0
- package/examples/ios-smoke.json +9 -0
- package/go.work +3 -0
- package/npm/agents.mjs +183 -0
- package/npm/app-config.mjs +95 -0
- package/npm/build-zmr.mjs +21 -0
- package/npm/commands.mjs +104 -0
- package/npm/generated-files.mjs +50 -0
- package/npm/index.mjs +75 -0
- package/npm/init-app.mjs +80 -0
- package/npm/package-scripts.mjs +72 -0
- package/npm/postinstall.mjs +21 -0
- package/npm/scaffold.mjs +179 -0
- package/npm/scenarios.mjs +93 -0
- package/npm/setup.mjs +69 -0
- package/npm/wizard.mjs +117 -0
- package/npm/zmr.mjs +23 -0
- package/package.json +114 -0
- package/prebuilds/darwin-arm64/zmr +0 -0
- package/prebuilds/darwin-x64/zmr +0 -0
- package/prebuilds/linux-arm64/zmr +0 -0
- package/prebuilds/linux-x64/zmr +0 -0
- package/schemas/README.md +26 -0
- package/schemas/action-result.schema.json +27 -0
- package/schemas/capabilities-output.schema.json +98 -0
- package/schemas/devices-output.schema.json +25 -0
- package/schemas/doctor-output.schema.json +51 -0
- package/schemas/explain-output.schema.json +51 -0
- package/schemas/import-output.schema.json +23 -0
- package/schemas/init-output.schema.json +71 -0
- package/schemas/json-rpc.schema.json +55 -0
- package/schemas/release-manifest.schema.json +43 -0
- package/schemas/release-readiness-output.schema.json +127 -0
- package/schemas/run-output.schema.json +43 -0
- package/schemas/scenario.schema.json +128 -0
- package/schemas/schemas-output.schema.json +26 -0
- package/schemas/semantic-snapshot.schema.json +116 -0
- package/schemas/snapshot.schema.json +60 -0
- package/schemas/trace-event.schema.json +14 -0
- package/schemas/trace-manifest.schema.json +59 -0
- package/schemas/validate-output.schema.json +42 -0
- package/schemas/version-output.schema.json +23 -0
- package/schemas/zmr-config.schema.json +75 -0
- package/scripts/android-emulator.sh +126 -0
- package/scripts/assert-ios-physical-ready.sh +213 -0
- package/scripts/benchmark-command.sh +307 -0
- package/scripts/benchmark.sh +359 -0
- package/scripts/benchmark_gate.py +117 -0
- package/scripts/benchmark_result_row.py +88 -0
- package/scripts/compare-benchmarks.py +288 -0
- package/scripts/create-android-demo-app.sh +342 -0
- package/scripts/create-ios-demo-app.sh +261 -0
- package/scripts/demo-android-real.sh +232 -0
- package/scripts/demo-ios-real.sh +270 -0
- package/scripts/demo.sh +464 -0
- package/scripts/device-matrix.sh +338 -0
- package/scripts/ensure-ios-shim-target.rb +237 -0
- package/scripts/install-android-shim.sh +281 -0
- package/scripts/install-ios-shim.sh +589 -0
- package/scripts/pilot-gate.sh +560 -0
- package/scripts/release-readiness.py +838 -0
- package/scripts/release-readiness.sh +91 -0
- package/scripts/run-android-pilot.sh +561 -0
- package/scripts/run-ios-pilot.sh +509 -0
- package/shims/android/README.md +21 -0
- package/shims/android/ZMRShimInstrumentedTest.java +152 -0
- package/shims/android/protocol.md +18 -0
- package/shims/ios/README.md +50 -0
- package/shims/ios/ZMRShim.swift +110 -0
- package/shims/ios/ZMRShimUITestCase.swift +475 -0
- package/shims/ios/protocol.md +74 -0
- package/skills/zmr-mobile-testing/SKILL.md +127 -0
- package/src/android.zig +344 -0
- package/src/android_device_info.zig +99 -0
- package/src/android_emulator.zig +154 -0
- package/src/android_screen_recording.zig +112 -0
- package/src/android_shell.zig +112 -0
- package/src/bundle.zig +124 -0
- package/src/bundle_redaction.zig +272 -0
- package/src/bundle_tar.zig +123 -0
- package/src/cli_devices.zig +97 -0
- package/src/cli_doctor.zig +114 -0
- package/src/cli_import.zig +70 -0
- package/src/cli_info.zig +39 -0
- package/src/cli_init.zig +72 -0
- package/src/cli_output.zig +467 -0
- package/src/cli_run.zig +259 -0
- package/src/cli_serve.zig +287 -0
- package/src/cli_trace.zig +111 -0
- package/src/cli_validate.zig +41 -0
- package/src/command.zig +211 -0
- package/src/config.zig +305 -0
- package/src/config_diagnostics.zig +212 -0
- package/src/config_paths.zig +49 -0
- package/src/device_registry.zig +37 -0
- package/src/doctor.zig +412 -0
- package/src/doctor_hints.zig +52 -0
- package/src/errors.zig +55 -0
- package/src/fake_device.zig +163 -0
- package/src/health.zig +28 -0
- package/src/importer.zig +343 -0
- package/src/importer_json.zig +100 -0
- package/src/importer_model.zig +103 -0
- package/src/ios.zig +399 -0
- package/src/ios_devices.zig +219 -0
- package/src/ios_lifecycle.zig +72 -0
- package/src/ios_shim.zig +242 -0
- package/src/ios_snapshot.zig +20 -0
- package/src/json_fields.zig +80 -0
- package/src/json_rpc.zig +150 -0
- package/src/json_rpc_methods.zig +318 -0
- package/src/json_rpc_observation.zig +31 -0
- package/src/json_rpc_params.zig +52 -0
- package/src/json_rpc_protocol.zig +110 -0
- package/src/json_rpc_trace.zig +73 -0
- package/src/main.zig +135 -0
- package/src/mcp.zig +234 -0
- package/src/mcp_protocol.zig +64 -0
- package/src/mcp_trace.zig +83 -0
- package/src/report.zig +346 -0
- package/src/report_html.zig +63 -0
- package/src/report_values.zig +27 -0
- package/src/run_options.zig +152 -0
- package/src/runner.zig +280 -0
- package/src/runner_actions.zig +109 -0
- package/src/runner_config.zig +6 -0
- package/src/runner_diagnostics.zig +268 -0
- package/src/runner_events.zig +170 -0
- package/src/runner_native.zig +88 -0
- package/src/runner_waits.zig +300 -0
- package/src/scaffold.zig +472 -0
- package/src/scenario.zig +346 -0
- package/src/scenario_fields.zig +50 -0
- package/src/schema_registry.zig +53 -0
- package/src/selector.zig +84 -0
- package/src/semantic.zig +171 -0
- package/src/trace.zig +315 -0
- package/src/trace_json.zig +340 -0
- package/src/trace_summary.zig +218 -0
- package/src/trace_summary_diagnostic.zig +202 -0
- package/src/types.zig +120 -0
- package/src/uiautomator.zig +164 -0
- package/src/validation.zig +187 -0
- package/src/version.zig +22 -0
- package/viewer/app.js +373 -0
- package/viewer/index.html +126 -0
- package/viewer/parser.js +233 -0
- package/viewer/styles.css +585 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import ZMRClient
|
|
3
|
+
|
|
4
|
+
struct Options {
|
|
5
|
+
var zmr = "zig-out/bin/zmr"
|
|
6
|
+
var adb = "tests/fake-adb.sh"
|
|
7
|
+
var device = "fake-android-1"
|
|
8
|
+
var appID = "com.example.mobiletest"
|
|
9
|
+
var traceDir = "traces/demo-swift-client"
|
|
10
|
+
var traceOut = "traces/demo-swift-client-redacted.zmrtrace"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
func parseOptions(_ arguments: [String]) throws -> Options {
|
|
14
|
+
var options = Options()
|
|
15
|
+
var index = 0
|
|
16
|
+
while index < arguments.count {
|
|
17
|
+
let argument = arguments[index]
|
|
18
|
+
guard index + 1 < arguments.count else {
|
|
19
|
+
throw NSError(domain: "ZMRFakeSession", code: 2, userInfo: [NSLocalizedDescriptionKey: "\(argument) requires a value"])
|
|
20
|
+
}
|
|
21
|
+
let value = arguments[index + 1]
|
|
22
|
+
switch argument {
|
|
23
|
+
case "--zmr": options.zmr = value
|
|
24
|
+
case "--adb": options.adb = value
|
|
25
|
+
case "--device": options.device = value
|
|
26
|
+
case "--app-id": options.appID = value
|
|
27
|
+
case "--trace-dir": options.traceDir = value
|
|
28
|
+
case "--trace-out": options.traceOut = value
|
|
29
|
+
default:
|
|
30
|
+
throw NSError(domain: "ZMRFakeSession", code: 2, userInfo: [NSLocalizedDescriptionKey: "unknown argument: \(argument)"])
|
|
31
|
+
}
|
|
32
|
+
index += 2
|
|
33
|
+
}
|
|
34
|
+
return options
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func object(_ value: Any) throws -> [String: Any] {
|
|
38
|
+
guard let object = value as? [String: Any] else {
|
|
39
|
+
throw ZMRError.invalidResponse
|
|
40
|
+
}
|
|
41
|
+
return object
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
do {
|
|
45
|
+
let options = try parseOptions(Array(CommandLine.arguments.dropFirst()))
|
|
46
|
+
let client = ZMRClient(
|
|
47
|
+
executable: options.zmr,
|
|
48
|
+
arguments: [
|
|
49
|
+
"serve",
|
|
50
|
+
"--transport", "stdio",
|
|
51
|
+
"--device", options.device,
|
|
52
|
+
"--app-id", options.appID,
|
|
53
|
+
"--adb", options.adb,
|
|
54
|
+
"--trace-dir", options.traceDir
|
|
55
|
+
]
|
|
56
|
+
)
|
|
57
|
+
try client.start()
|
|
58
|
+
defer { client.close() }
|
|
59
|
+
|
|
60
|
+
let capabilities = try object(try client.call("runner.capabilities"))
|
|
61
|
+
try client.createSession()
|
|
62
|
+
_ = try client.call("app.openLink", params: ["url": "exampleapp://swift-client"])
|
|
63
|
+
_ = try client.call("wait.until", params: ["visible": ["text": "Dashboard"], "timeoutMs": 1000])
|
|
64
|
+
_ = try client.call("ui.tap", params: ["selector": ["text": "Sign in"]])
|
|
65
|
+
_ = try client.call("ui.type", params: ["text": "agent@example.com", "selector": ["resourceId": "email-login-email-input"]])
|
|
66
|
+
_ = try client.call("assert.notVisible", params: ["selector": ["text": "Application has crashed"], "timeoutMs": 100])
|
|
67
|
+
_ = try client.assertHealthy(timeoutMs: 100)
|
|
68
|
+
let snapshot = try client.snapshot()
|
|
69
|
+
let exported = try object(try client.call("trace.export", params: ["out": options.traceOut, "redact": true, "includeScreenshots": true]))
|
|
70
|
+
let events = try object(try client.call("trace.events", params: ["afterSeq": 0, "limit": 10]))
|
|
71
|
+
|
|
72
|
+
let nodes = snapshot["nodes"] as? [Any] ?? []
|
|
73
|
+
let summary: [String: Any] = [
|
|
74
|
+
"protocolVersion": capabilities["protocolVersion"] ?? "",
|
|
75
|
+
"activePackage": snapshot["activePackage"] ?? "",
|
|
76
|
+
"nodes": nodes.count,
|
|
77
|
+
"events": events["nextSeq"] ?? 0,
|
|
78
|
+
"traceDir": options.traceDir,
|
|
79
|
+
"traceOut": exported["out"] ?? options.traceOut
|
|
80
|
+
]
|
|
81
|
+
let data = try JSONSerialization.data(withJSONObject: summary, options: [.sortedKeys])
|
|
82
|
+
print(String(data: data, encoding: .utf8) ?? "{}")
|
|
83
|
+
} catch {
|
|
84
|
+
fputs("\(error)\n", stderr)
|
|
85
|
+
exit(1)
|
|
86
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# ZMR TypeScript Reference Client
|
|
2
|
+
|
|
3
|
+
Zero-dependency ESM client for ZMR's newline-delimited JSON-RPC protocol.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import { createZmrClient } from "./index.mjs";
|
|
7
|
+
|
|
8
|
+
const zmr = createZmrClient({
|
|
9
|
+
command: "zmr",
|
|
10
|
+
args: [
|
|
11
|
+
"serve",
|
|
12
|
+
"--transport", "stdio",
|
|
13
|
+
"--device", "emulator-5554",
|
|
14
|
+
"--app-id", "com.example.mobiletest",
|
|
15
|
+
"--trace-dir", "traces/agent-session",
|
|
16
|
+
],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
await zmr.createSession();
|
|
21
|
+
await zmr.openLink("exampleapp://e2e-auth?probe=1");
|
|
22
|
+
await zmr.waitUntil({ text: "E2E auth probe" }, { timeoutMs: 30000 });
|
|
23
|
+
const snapshot = await zmr.snapshot();
|
|
24
|
+
const events = await zmr.traceEvents(0, { limit: 100 });
|
|
25
|
+
console.log(snapshot.nodes);
|
|
26
|
+
console.log(events.events.length);
|
|
27
|
+
await zmr.exportTrace("traces/agent-session-redacted.zmrtrace", { redact: true, omitScreenshots: true });
|
|
28
|
+
} finally {
|
|
29
|
+
await zmr.close();
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The runtime is plain JavaScript (`index.mjs`) with TypeScript declarations
|
|
34
|
+
(`index.d.ts`) so consumers can use it without a build step.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import url from "node:url";
|
|
4
|
+
import { createZmrClient } from "../index.mjs";
|
|
5
|
+
|
|
6
|
+
const root = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), "../../..");
|
|
7
|
+
|
|
8
|
+
const zmr = createZmrClient({
|
|
9
|
+
command: path.join(root, "zig-out", "bin", "zmr"),
|
|
10
|
+
args: [
|
|
11
|
+
"serve",
|
|
12
|
+
"--transport", "stdio",
|
|
13
|
+
"--device", "fake-android-1",
|
|
14
|
+
"--app-id", "com.example.mobiletest",
|
|
15
|
+
"--adb", path.join(root, "tests", "fake-adb.sh"),
|
|
16
|
+
"--trace-dir", "traces/demo-typescript-client",
|
|
17
|
+
],
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const capabilities = await zmr.capabilities();
|
|
22
|
+
await zmr.createSession();
|
|
23
|
+
await zmr.openLink("exampleapp://e2e-auth?probe=1");
|
|
24
|
+
await zmr.assertHealthy({ timeoutMs: 100 });
|
|
25
|
+
const snapshot = await zmr.snapshot();
|
|
26
|
+
const events = await zmr.traceEvents(0, { limit: 20 });
|
|
27
|
+
await zmr.exportTrace("traces/demo-typescript-client-redacted.zmrtrace", { redact: true, omitScreenshots: true });
|
|
28
|
+
console.log(JSON.stringify({
|
|
29
|
+
protocolVersion: capabilities.protocolVersion,
|
|
30
|
+
activePackage: snapshot.activePackage,
|
|
31
|
+
nodes: snapshot.nodes.length,
|
|
32
|
+
events: events.events.length,
|
|
33
|
+
}));
|
|
34
|
+
} finally {
|
|
35
|
+
await zmr.close();
|
|
36
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
export interface ZmrClientOptions {
|
|
2
|
+
command: string;
|
|
3
|
+
args?: string[];
|
|
4
|
+
cwd?: string;
|
|
5
|
+
env?: Record<string, string | undefined>;
|
|
6
|
+
stderr?: "inherit" | "ignore" | "pipe";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Selector {
|
|
10
|
+
id?: string;
|
|
11
|
+
resourceId?: string;
|
|
12
|
+
text?: string;
|
|
13
|
+
textContains?: string;
|
|
14
|
+
contentDesc?: string;
|
|
15
|
+
contentDescContains?: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Viewport {
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface UiNode {
|
|
25
|
+
stableId: string;
|
|
26
|
+
className: string;
|
|
27
|
+
resourceId?: string | null;
|
|
28
|
+
text?: string | null;
|
|
29
|
+
contentDesc?: string | null;
|
|
30
|
+
bounds: { x: number; y: number; width: number; height: number };
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
visible: boolean;
|
|
33
|
+
selected: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ObservationSnapshot {
|
|
37
|
+
id: string;
|
|
38
|
+
timestampMs: number;
|
|
39
|
+
viewport: Viewport;
|
|
40
|
+
displayDensityDpi?: number | null;
|
|
41
|
+
activePackage?: string | null;
|
|
42
|
+
activeActivity?: string | null;
|
|
43
|
+
screenshotArtifact?: string | null;
|
|
44
|
+
treeArtifact?: string | null;
|
|
45
|
+
focusedNodeId?: string | null;
|
|
46
|
+
logDelta?: string | null;
|
|
47
|
+
nodes: UiNode[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SemanticNode {
|
|
51
|
+
id: string;
|
|
52
|
+
role: "button" | "textbox" | "switch" | "checkbox" | "radio" | "image" | "text" | "node" | string;
|
|
53
|
+
name: string;
|
|
54
|
+
selector: Record<string, string>;
|
|
55
|
+
source: {
|
|
56
|
+
className: string;
|
|
57
|
+
resourceId?: string | null;
|
|
58
|
+
text?: string | null;
|
|
59
|
+
contentDesc?: string | null;
|
|
60
|
+
};
|
|
61
|
+
bounds: { x: number; y: number; width: number; height: number; centerX: number; centerY: number };
|
|
62
|
+
enabled: boolean;
|
|
63
|
+
visible: boolean;
|
|
64
|
+
selected: boolean;
|
|
65
|
+
interactive: boolean;
|
|
66
|
+
recommendedAction?: "tap" | "type" | null | string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface SemanticSnapshot {
|
|
70
|
+
id: string;
|
|
71
|
+
timestampMs: number;
|
|
72
|
+
viewport: Viewport;
|
|
73
|
+
activePackage?: string | null;
|
|
74
|
+
activeActivity?: string | null;
|
|
75
|
+
focusedNodeId?: string | null;
|
|
76
|
+
nodes: SemanticNode[];
|
|
77
|
+
summary: {
|
|
78
|
+
nodeCount: number;
|
|
79
|
+
interactiveCount: number;
|
|
80
|
+
visibleText: string[];
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface PlatformSupport {
|
|
85
|
+
status: "supported" | "preview" | "unsupported" | string;
|
|
86
|
+
deviceTypes: string[];
|
|
87
|
+
automation: string[];
|
|
88
|
+
physicalDevices?: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface Capabilities {
|
|
92
|
+
name: string;
|
|
93
|
+
version: string;
|
|
94
|
+
protocolVersion: string;
|
|
95
|
+
platforms: string[];
|
|
96
|
+
platformSupport?: Record<string, PlatformSupport>;
|
|
97
|
+
iosPreview?: boolean;
|
|
98
|
+
transports: string[];
|
|
99
|
+
methods: string[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface DeviceInfo {
|
|
103
|
+
serial: string;
|
|
104
|
+
state: string;
|
|
105
|
+
ready: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface ZmrClient {
|
|
109
|
+
request<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
|
|
110
|
+
capabilities(): Promise<Capabilities>;
|
|
111
|
+
createSession(): Promise<{ sessionId: string }>;
|
|
112
|
+
closeSession(): Promise<boolean>;
|
|
113
|
+
devices(): Promise<DeviceInfo[]>;
|
|
114
|
+
launch(): Promise<boolean>;
|
|
115
|
+
stop(): Promise<boolean>;
|
|
116
|
+
clearState(): Promise<boolean>;
|
|
117
|
+
openLink(url: string): Promise<boolean>;
|
|
118
|
+
snapshot(): Promise<ObservationSnapshot>;
|
|
119
|
+
semanticSnapshot(): Promise<SemanticSnapshot>;
|
|
120
|
+
tap(selector: Selector): Promise<boolean>;
|
|
121
|
+
typeText(text: string, options?: { selector?: Selector }): Promise<boolean>;
|
|
122
|
+
eraseText(options?: { selector?: Selector; maxChars?: number }): Promise<boolean>;
|
|
123
|
+
hideKeyboard(): Promise<boolean>;
|
|
124
|
+
swipe(input: { x1: number; y1: number; x2: number; y2: number; durationMs?: number }): Promise<boolean>;
|
|
125
|
+
pressBack(): Promise<boolean>;
|
|
126
|
+
scrollUntilVisible(selector: Selector, options?: { direction?: "up" | "down"; timeoutMs?: number }): Promise<boolean>;
|
|
127
|
+
waitUntil(selector: Selector, options?: { timeoutMs?: number }): Promise<boolean>;
|
|
128
|
+
waitAny(selectors: Selector[], options?: { timeoutMs?: number }): Promise<{ matchedIndex: number } | false>;
|
|
129
|
+
waitGone(selector: Selector, options?: { timeoutMs?: number }): Promise<boolean>;
|
|
130
|
+
assertVisible(selector: Selector, options?: { timeoutMs?: number }): Promise<boolean>;
|
|
131
|
+
assertNotVisible(selector: Selector, options?: { timeoutMs?: number }): Promise<boolean>;
|
|
132
|
+
assertHealthy(options?: { timeoutMs?: number }): Promise<boolean>;
|
|
133
|
+
exportTrace(out: string, options?: { redact?: boolean; omitScreenshots?: boolean }): Promise<Record<string, unknown>>;
|
|
134
|
+
traceEvents(afterSeq?: number, options?: { limit?: number }): Promise<Record<string, unknown>>;
|
|
135
|
+
close(): Promise<void>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export class ZmrRpcError extends Error {
|
|
139
|
+
code?: number;
|
|
140
|
+
publicCode?: string;
|
|
141
|
+
data?: unknown;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function createZmrClient(options: ZmrClientOptions): ZmrClient;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import readline from "node:readline";
|
|
3
|
+
|
|
4
|
+
export class ZmrRpcError extends Error {
|
|
5
|
+
constructor(error) {
|
|
6
|
+
super(error?.message || "ZMR JSON-RPC error");
|
|
7
|
+
this.name = "ZmrRpcError";
|
|
8
|
+
this.code = error?.code;
|
|
9
|
+
this.publicCode = error?.publicCode;
|
|
10
|
+
this.data = error?.data;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createZmrClient(options) {
|
|
15
|
+
return new ZmrClient(options);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ZmrClient {
|
|
19
|
+
#child;
|
|
20
|
+
#nextId = 1;
|
|
21
|
+
#pending = new Map();
|
|
22
|
+
#closed = false;
|
|
23
|
+
|
|
24
|
+
constructor(options) {
|
|
25
|
+
if (!options?.command) throw new Error("createZmrClient requires command");
|
|
26
|
+
this.#child = spawn(options.command, options.args ?? [], {
|
|
27
|
+
cwd: options.cwd,
|
|
28
|
+
env: options.env,
|
|
29
|
+
stdio: ["pipe", "pipe", options.stderr ?? "inherit"],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const lines = readline.createInterface({ input: this.#child.stdout });
|
|
33
|
+
lines.on("line", (line) => this.#handleLine(line));
|
|
34
|
+
this.#child.on("error", (error) => this.#rejectAll(error));
|
|
35
|
+
this.#child.on("exit", (code, signal) => {
|
|
36
|
+
this.#closed = true;
|
|
37
|
+
if (this.#pending.size > 0) {
|
|
38
|
+
this.#rejectAll(new Error(`zmr process exited with ${signal ?? code}`));
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
request(method, params = {}) {
|
|
44
|
+
if (this.#closed) return Promise.reject(new Error("zmr client is closed"));
|
|
45
|
+
const id = this.#nextId++;
|
|
46
|
+
const message = { jsonrpc: "2.0", id, method, params };
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
this.#pending.set(id, { resolve, reject });
|
|
49
|
+
this.#child.stdin.write(`${JSON.stringify(message)}\n`, (error) => {
|
|
50
|
+
if (!error) return;
|
|
51
|
+
this.#pending.delete(id);
|
|
52
|
+
reject(error);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
capabilities() {
|
|
58
|
+
return this.request("runner.capabilities", {});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
createSession() {
|
|
62
|
+
return this.request("session.create", {});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
closeSession() {
|
|
66
|
+
return this.request("session.close", {});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
devices() {
|
|
70
|
+
return this.request("device.list", {});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
launch() {
|
|
74
|
+
return this.request("app.launch", {});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
stop() {
|
|
78
|
+
return this.request("app.stop", {});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
clearState() {
|
|
82
|
+
return this.request("app.clearState", {});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
openLink(url) {
|
|
86
|
+
return this.request("app.openLink", { url });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
snapshot() {
|
|
90
|
+
return this.request("observe.snapshot", {});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
semanticSnapshot() {
|
|
94
|
+
return this.request("observe.semanticSnapshot", {});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
tap(selector) {
|
|
98
|
+
return this.request("ui.tap", { selector });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
typeText(text, options = {}) {
|
|
102
|
+
return this.request("ui.type", { ...options, text });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
eraseText(options = {}) {
|
|
106
|
+
return this.request("ui.eraseText", options);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
hideKeyboard() {
|
|
110
|
+
return this.request("ui.hideKeyboard", {});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
swipe(input) {
|
|
114
|
+
return this.request("ui.swipe", input);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
pressBack() {
|
|
118
|
+
return this.request("ui.pressBack", {});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
scrollUntilVisible(selector, options = {}) {
|
|
122
|
+
return this.request("ui.scrollUntilVisible", { selector, ...options });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
waitUntil(selector, options = {}) {
|
|
126
|
+
return this.request("wait.until", { visible: selector, ...options });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
waitAny(selectors, options = {}) {
|
|
130
|
+
return this.request("wait.any", { selectors, ...options });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
waitGone(selector, options = {}) {
|
|
134
|
+
return this.request("wait.gone", { selector, ...options });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
assertVisible(selector, options = {}) {
|
|
138
|
+
return this.request("assert.visible", { selector, ...options });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
assertNotVisible(selector, options = {}) {
|
|
142
|
+
return this.request("assert.notVisible", { selector, ...options });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
assertHealthy(options = {}) {
|
|
146
|
+
return this.request("assert.healthy", options);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
exportTrace(out, options = {}) {
|
|
150
|
+
return this.request("trace.export", { out, ...options });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
traceEvents(afterSeq = 0, options = {}) {
|
|
154
|
+
return this.request("trace.events", { afterSeq, ...options });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async close() {
|
|
158
|
+
if (this.#closed) return;
|
|
159
|
+
this.#closed = true;
|
|
160
|
+
this.#child.stdin.end();
|
|
161
|
+
if (this.#child.exitCode == null && this.#child.signalCode == null) {
|
|
162
|
+
this.#child.kill();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
#handleLine(line) {
|
|
167
|
+
if (!line.trim()) return;
|
|
168
|
+
let message;
|
|
169
|
+
try {
|
|
170
|
+
message = JSON.parse(line);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
this.#rejectAll(error);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const pending = this.#pending.get(message.id);
|
|
177
|
+
if (!pending) return;
|
|
178
|
+
this.#pending.delete(message.id);
|
|
179
|
+
if (message.error) {
|
|
180
|
+
pending.reject(new ZmrRpcError(message.error));
|
|
181
|
+
} else {
|
|
182
|
+
pending.resolve(message.result);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#rejectAll(error) {
|
|
187
|
+
for (const pending of this.#pending.values()) {
|
|
188
|
+
pending.reject(error);
|
|
189
|
+
}
|
|
190
|
+
this.#pending.clear();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# 0001: Agent-Native Runner Boundary
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted.
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
ZMR is intended for AI agents and deterministic automation, but embedding an
|
|
10
|
+
LLM inside the runner would make device control harder to test, version, and
|
|
11
|
+
secure. The runner needs a small, stable control surface that external agents,
|
|
12
|
+
scripts, and SDKs can consume.
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
ZMR does not embed an LLM. Zig owns orchestration, device control,
|
|
17
|
+
JSON-RPC/session handling, scenario execution, wait/assertion logic, and trace
|
|
18
|
+
generation. External agents decide what to do next by reading structured
|
|
19
|
+
observations and calling typed actions.
|
|
20
|
+
|
|
21
|
+
The public agent contract is JSON-RPC over stdio or localhost TCP, plus
|
|
22
|
+
machine-readable CLI JSON and public schemas.
|
|
23
|
+
|
|
24
|
+
## Consequences
|
|
25
|
+
|
|
26
|
+
- The core runner remains deterministic and testable without model calls.
|
|
27
|
+
- Agents can be swapped without changing device adapters.
|
|
28
|
+
- Trace output records what happened at the protocol/action layer, which makes
|
|
29
|
+
failures explainable without replaying model reasoning.
|
|
30
|
+
- Higher-level planning, natural-language test generation, and app-specific
|
|
31
|
+
heuristics belong in external clients or app-local `.zmr/` assets.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# 0002: App-Local `.zmr/` Contract
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted.
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Mobile app repositories need a predictable place for runner configuration,
|
|
10
|
+
smoke scenarios, shim commands, and setup scripts. ZMR should be installable as
|
|
11
|
+
an npm dev dependency, but it should also work from source checkouts and
|
|
12
|
+
release archives.
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
`.zmr/` is the app-local contract. The default config file is
|
|
17
|
+
`.zmr/config.json`, validated by `schemas/config.schema.json`. ZMR commands
|
|
18
|
+
auto-discover the config from the app checkout, and explicit CLI flags always
|
|
19
|
+
override config defaults.
|
|
20
|
+
|
|
21
|
+
`zmr-wizard` and `zmr init --app` scaffold the same shape:
|
|
22
|
+
|
|
23
|
+
- `.zmr/config.json`
|
|
24
|
+
- `.zmr/android-smoke.json`
|
|
25
|
+
- `.zmr/ios-smoke.json`
|
|
26
|
+
- `.zmr/AGENTS.md`
|
|
27
|
+
- optional shim commands and source files
|
|
28
|
+
- app package script suggestions
|
|
29
|
+
- `traces/` ignored by default
|
|
30
|
+
|
|
31
|
+
## Consequences
|
|
32
|
+
|
|
33
|
+
- App-specific scenarios and private traces stay in the app repository, not in
|
|
34
|
+
the public ZMR repo.
|
|
35
|
+
- npm, source, and release-archive installs share one integration model.
|
|
36
|
+
- Agents can discover the same setup state humans use through `zmr doctor`,
|
|
37
|
+
`zmr schemas`, `.zmr/config.json`, and `.zmr/AGENTS.md`.
|
|
38
|
+
- Generated files need backwards-compatible schema handling and clear
|
|
39
|
+
diagnostics because app repositories may pin older versions.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# 0003: iOS XCTest Shim
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted.
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
`xcrun simctl` is good for simulator lifecycle, screenshots, logs, deep links,
|
|
10
|
+
and app install/launch, but it is not enough for robust selector-driven UI
|
|
11
|
+
automation. `xcrun devicectl` provides physical-device lifecycle operations,
|
|
12
|
+
but it still does not provide the selector-grade UI semantics ZMR needs. ZMR
|
|
13
|
+
needs iOS behavior that matches Android scenario semantics where the platforms
|
|
14
|
+
overlap.
|
|
15
|
+
|
|
16
|
+
## Decision
|
|
17
|
+
|
|
18
|
+
iOS support uses three layers:
|
|
19
|
+
|
|
20
|
+
- `xcrun simctl` for lifecycle, install, launch, stop, open link, clear state,
|
|
21
|
+
screenshots, logs, and device discovery.
|
|
22
|
+
- `xcrun devicectl` for physical-device discovery, install, launch, deep-link
|
|
23
|
+
launch, clear-state uninstall, and best-effort stop.
|
|
24
|
+
- An app-local XCTest/XCUIAutomation shim for hierarchy snapshots, element
|
|
25
|
+
queries, screenshots, tap, type, erase text, keyboard control, swipe, and app
|
|
26
|
+
state on simulators and physical devices.
|
|
27
|
+
|
|
28
|
+
V1 iOS clear-state semantics are best-effort app uninstall by bundle id.
|
|
29
|
+
Physical-device log capture remains limited until a supported capture channel is
|
|
30
|
+
available.
|
|
31
|
+
|
|
32
|
+
## Consequences
|
|
33
|
+
|
|
34
|
+
- iOS selector actions use platform automation APIs instead of coordinate-only
|
|
35
|
+
shelling.
|
|
36
|
+
- App repositories must wire a UI test target when they need selector-grade iOS
|
|
37
|
+
runs.
|
|
38
|
+
- The internal shim protocol can evolve during the dev preview, while public
|
|
39
|
+
behavior remains the ZMR CLI, scenario format, JSON-RPC methods, and schemas.
|
|
40
|
+
- Physical iOS support depends on local signing, provisioning, Developer Mode,
|
|
41
|
+
pairing, and `devicectl` transport state.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# 0004: Benchmark Claims And Baseline Collection
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted.
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
ZMR should eventually demonstrate speed, reliability, and diagnostics against
|
|
10
|
+
existing app-local automation, but public fixtures cannot assume a private app,
|
|
11
|
+
private credentials, or a specific third-party runner.
|
|
12
|
+
|
|
13
|
+
## Decision
|
|
14
|
+
|
|
15
|
+
Public ZMR benchmarking remains tool-agnostic:
|
|
16
|
+
|
|
17
|
+
- `zmr-benchmark` records repeated ZMR scenario runs and can append rows to a
|
|
18
|
+
shared comparison JSONL file.
|
|
19
|
+
- `zmr-benchmark-command` records repeated app-local baseline commands.
|
|
20
|
+
- `zmr-compare-benchmarks` compares normalized candidate and baseline rows and
|
|
21
|
+
can enforce candidate pass-rate, failure-count, mean-speedup, and p95-speedup
|
|
22
|
+
gates.
|
|
23
|
+
|
|
24
|
+
Public docs describe the measurement method and output shape. They do not make
|
|
25
|
+
real-app speed claims unless equivalent candidate and baseline flows were run
|
|
26
|
+
under the same local conditions and the report can be shared safely.
|
|
27
|
+
|
|
28
|
+
## Consequences
|
|
29
|
+
|
|
30
|
+
- Benchmark infrastructure is reusable without hardcoded private app details.
|
|
31
|
+
- App teams can compare against whatever runner they already use by wrapping a
|
|
32
|
+
command.
|
|
33
|
+
- CI can fail when a speed or reliability claim is not supported by the latest
|
|
34
|
+
repeated-run data.
|
|
35
|
+
- Public performance statements require fair inputs: same app build, same
|
|
36
|
+
device/simulator state, same user path, repeated runs, and trace-backed
|
|
37
|
+
failure diagnostics.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Architecture Decisions
|
|
2
|
+
|
|
3
|
+
Accepted architecture decisions for ZMR are recorded here so product docs,
|
|
4
|
+
protocol docs, and implementation choices stay aligned.
|
|
5
|
+
|
|
6
|
+
- [0001: Agent-Native Runner Boundary](0001-agent-native-runner-boundary.md)
|
|
7
|
+
- [0002: App-Local `.zmr/` Contract](0002-app-local-zmr-contract.md)
|
|
8
|
+
- [0003: iOS XCTest Shim](0003-ios-simulator-xctest-shim.md)
|
|
9
|
+
- [0004: Benchmark Claims And Baseline Collection](0004-benchmark-claims-and-baseline-collection.md)
|
|
10
|
+
|
|
11
|
+
ADRs describe current decisions, not permanent constraints. A later ADR can
|
|
12
|
+
supersede an earlier decision when the product or support matrix changes.
|