zeno-mobile-runner 0.1.3 → 0.2.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 +192 -2
- package/FEATURES.md +50 -7
- package/README.md +168 -120
- package/build.zig.zon +3 -3
- package/clients/README.md +60 -3
- package/clients/go/README.md +12 -0
- package/clients/go/zmr/client.go +142 -0
- package/clients/kotlin/README.md +18 -1
- package/clients/kotlin/build.gradle.kts +1 -1
- package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +76 -1
- package/clients/python/README.md +19 -0
- package/clients/python/pyproject.toml +1 -1
- package/clients/python/zmr_client.py +33 -0
- package/clients/rust/Cargo.lock +1 -1
- package/clients/rust/Cargo.toml +1 -1
- package/clients/rust/README.md +25 -1
- package/clients/rust/src/lib.rs +201 -0
- package/clients/swift/README.md +18 -0
- package/clients/swift/Sources/ZMRClient/ZMRClient.swift +82 -0
- package/clients/typescript/README.md +16 -0
- package/clients/typescript/index.d.ts +12 -0
- package/clients/typescript/index.mjs +16 -0
- package/clients/typescript/package.json +1 -1
- package/docs/agent-discovery.md +151 -22
- package/docs/ai-agents.md +99 -11
- package/docs/benchmarking.md +49 -3
- package/docs/benchmarks/2026-06-09-android-workflow.md +73 -0
- package/docs/benchmarks/2026-06-09-android-workflow.results.jsonl +20 -0
- package/docs/benchmarks/2026-06-09-framework-baseline-status.md +32 -0
- package/docs/benchmarks/2026-06-09-ios-appium-comparison.md +115 -0
- package/docs/benchmarks/2026-06-09-ios-appium-comparison.results.jsonl +40 -0
- package/docs/benchmarks/2026-06-09-ios-demo.md +90 -0
- package/docs/benchmarks/2026-06-09-ios-demo.results.jsonl +20 -0
- package/docs/benchmarks/2026-06-09-ios-maestro-comparison.md +128 -0
- package/docs/benchmarks/2026-06-09-ios-maestro-comparison.results.jsonl +40 -0
- package/docs/benchmarks/2026-06-09-ios-workflow-comparison.md +143 -0
- package/docs/benchmarks/2026-06-09-ios-workflow-comparison.results.jsonl +40 -0
- package/docs/benchmarks/2026-06-09-ios-xctest-floor.md +106 -0
- package/docs/benchmarks/2026-06-09-ios-xctest-floor.results.jsonl +40 -0
- package/docs/benchmarks/README.md +36 -0
- package/docs/benchmarks/benchmark-lab-v1.json +155 -0
- package/docs/benchmarks/benchmark-lab-v1.md +95 -0
- package/docs/clients.md +26 -6
- package/docs/demo.md +40 -1
- package/docs/expo-smoke.md +8 -8
- package/docs/frameworks.md +10 -0
- package/docs/install.md +3 -2
- package/docs/npm.md +100 -4
- package/docs/production-readiness.md +123 -0
- package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
- package/docs/protocol.md +215 -16
- package/docs/scenario-authoring.md +18 -0
- package/docs/trace-privacy.md +9 -0
- package/docs/troubleshooting.md +7 -1
- package/examples/android-workflow.json +79 -0
- package/examples/ios-shim-workflow.json +79 -0
- package/examples/react-native-expo-workflow.json +75 -0
- package/npm/agents.mjs +16 -0
- package/npm/commands.mjs +9 -5
- package/package.json +6 -1
- 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 +4 -0
- package/schemas/discover-output.schema.json +83 -0
- package/schemas/draft-output.schema.json +58 -0
- package/schemas/explore-output.schema.json +94 -0
- package/schemas/inspect-output.schema.json +88 -0
- package/schemas/run-output.schema.json +2 -0
- package/scripts/benchmark-lab.py +253 -0
- package/scripts/create-android-demo-app.sh +324 -29
- package/scripts/create-ios-demo-app.sh +174 -7
- package/scripts/create-react-native-expo-demo-app.sh +727 -0
- package/scripts/demo.sh +3 -0
- package/scripts/install-ios-shim.sh +2 -2
- package/scripts/release-readiness.py +43 -0
- package/scripts/run-android-pilot.sh +35 -9
- package/scripts/run-ios-pilot.sh +11 -4
- package/shims/ios/ZMRShim.swift +10 -0
- package/shims/ios/ZMRShimUITestCase.swift +42 -0
- package/shims/ios/protocol.md +1 -0
- package/skills/zmr-mobile-testing/SKILL.md +28 -3
- package/src/cli_discover.zig +239 -0
- package/src/cli_draft.zig +924 -0
- package/src/cli_explore.zig +136 -0
- package/src/cli_import.zig +31 -15
- package/src/cli_inspect.zig +310 -0
- package/src/cli_output.zig +26 -2
- package/src/cli_run.zig +28 -0
- package/src/cli_trace.zig +45 -15
- package/src/cli_validate.zig +12 -6
- package/src/errors.zig +9 -0
- package/src/ios.zig +49 -12
- package/src/ios_shim.zig +36 -2
- package/src/json_rpc_methods.zig +85 -11
- package/src/json_rpc_params.zig +8 -0
- package/src/json_rpc_protocol.zig +1 -1
- package/src/json_rpc_trace.zig +112 -0
- package/src/main.zig +27 -2
- package/src/mcp.zig +209 -6
- package/src/mcp_protocol.zig +29 -1
- package/src/mcp_trace.zig +126 -4
- package/src/report.zig +186 -0
- package/src/runner.zig +26 -4
- package/src/runner_actions.zig +10 -0
- package/src/runner_diagnostics.zig +31 -1
- package/src/runner_events.zig +70 -7
- package/src/runner_native.zig +17 -1
- package/src/runner_waits.zig +82 -19
- package/src/scaffold.zig +28 -12
- package/src/scenario.zig +32 -4
- package/src/schema_registry.zig +4 -0
- package/src/version.zig +1 -1
- package/viewer/app.js +23 -3
package/scripts/demo.sh
CHANGED
|
@@ -37,9 +37,12 @@ echo "== Validate demo scenarios =="
|
|
|
37
37
|
./zig-out/bin/zmr validate examples/android-app-referral-deep-link.json
|
|
38
38
|
./zig-out/bin/zmr validate examples/android-app-error-state.json
|
|
39
39
|
./zig-out/bin/zmr validate examples/android-shim-smoke.json
|
|
40
|
+
./zig-out/bin/zmr validate examples/android-workflow.json
|
|
41
|
+
./zig-out/bin/zmr validate examples/react-native-expo-workflow.json
|
|
40
42
|
./zig-out/bin/zmr validate examples/ios-smoke.json
|
|
41
43
|
./zig-out/bin/zmr validate examples/ios-dev-client-open-link.json
|
|
42
44
|
./zig-out/bin/zmr validate examples/ios-shim-smoke.json
|
|
45
|
+
./zig-out/bin/zmr validate examples/ios-shim-workflow.json
|
|
43
46
|
|
|
44
47
|
echo
|
|
45
48
|
echo "== Validate diagnostics: field and line location =="
|
|
@@ -342,14 +342,14 @@ is_server_running() {
|
|
|
342
342
|
return 1
|
|
343
343
|
fi
|
|
344
344
|
command="\$(ps -p "\$pid" -o command= 2>/dev/null || true)"
|
|
345
|
-
[[ "\$command" == *xcodebuild* && "\$command" == *
|
|
345
|
+
[[ "\$command" == *xcodebuild* && "\$command" == *"$TEST_TARGET"* ]]
|
|
346
346
|
}
|
|
347
347
|
|
|
348
348
|
run_oneshot() {
|
|
349
349
|
local request_file response_file oneshot_log destination_id
|
|
350
350
|
request_file="\$(mktemp "\$STATE_DIR/request.XXXXXX")"
|
|
351
351
|
response_file="\$(mktemp "\$STATE_DIR/response.XXXXXX")"
|
|
352
|
-
oneshot_log="\$(mktemp "\$STATE_DIR/xcodebuild.oneshot.XXXXXX
|
|
352
|
+
oneshot_log="\$(mktemp "\$STATE_DIR/xcodebuild.oneshot.log.XXXXXX")"
|
|
353
353
|
cp "\$STDIN_FILE" "\$request_file"
|
|
354
354
|
destination_id="\$(destination_spec)"
|
|
355
355
|
|
|
@@ -423,6 +423,38 @@ def benchmark_threshold_reason(row):
|
|
|
423
423
|
return "requires " + ", ".join(reasons)
|
|
424
424
|
|
|
425
425
|
|
|
426
|
+
agent_workflow_fields = (
|
|
427
|
+
"agentWorkflow",
|
|
428
|
+
"mcp",
|
|
429
|
+
"jsonRpc",
|
|
430
|
+
"semanticSnapshot",
|
|
431
|
+
"typedActions",
|
|
432
|
+
"traceEvents",
|
|
433
|
+
"traceExplain",
|
|
434
|
+
"traceExplore",
|
|
435
|
+
"traceDiscover",
|
|
436
|
+
"scenarioValidation",
|
|
437
|
+
"redactedExport",
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def agent_workflow_pass(row):
|
|
442
|
+
command = row.get("command")
|
|
443
|
+
if row.get("name") == "local release gate" and isinstance(command, str) and "release-gate.sh" in command:
|
|
444
|
+
return True
|
|
445
|
+
return all(row.get(field) is True for field in agent_workflow_fields)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def agent_workflow_reason(row):
|
|
449
|
+
command = row.get("command")
|
|
450
|
+
if row.get("name") == "local release gate" and (not isinstance(command, str) or "release-gate.sh" not in command):
|
|
451
|
+
return "requires release-gate.sh or structured agentWorkflow evidence"
|
|
452
|
+
missing = [field for field in agent_workflow_fields if row.get(field) is not True]
|
|
453
|
+
if missing:
|
|
454
|
+
return "requires release-gate.sh or structured agentWorkflow evidence: " + ", ".join(missing)
|
|
455
|
+
return "requires release-gate.sh or structured agentWorkflow evidence"
|
|
456
|
+
|
|
457
|
+
|
|
426
458
|
def row_satisfies(label, row):
|
|
427
459
|
if row.get("status") != "passed":
|
|
428
460
|
return False
|
|
@@ -436,6 +468,8 @@ def row_satisfies(label, row):
|
|
|
436
468
|
return physical_ios_device_value(row) is not None
|
|
437
469
|
if label == "competitive benchmark comparison":
|
|
438
470
|
return benchmark_thresholds_pass(row)
|
|
471
|
+
if label == "agent workflow smoke":
|
|
472
|
+
return agent_workflow_pass(row)
|
|
439
473
|
return True
|
|
440
474
|
|
|
441
475
|
|
|
@@ -485,6 +519,8 @@ def requirement_status(label, names):
|
|
|
485
519
|
reason = "requires concrete physical device identifier evidence"
|
|
486
520
|
elif label == "competitive benchmark comparison":
|
|
487
521
|
reason = benchmark_threshold_reason(row)
|
|
522
|
+
elif label == "agent workflow smoke":
|
|
523
|
+
reason = agent_workflow_reason(row)
|
|
488
524
|
return {
|
|
489
525
|
"name": label,
|
|
490
526
|
"status": "insufficient",
|
|
@@ -508,6 +544,7 @@ requirements = [
|
|
|
508
544
|
|
|
509
545
|
if target in ("production", "market-claim"):
|
|
510
546
|
requirements.extend([
|
|
547
|
+
("agent workflow smoke", ("agent workflow smoke", "local release gate")),
|
|
511
548
|
("physical iOS readiness", "physical iOS readiness"),
|
|
512
549
|
("Android hardware pilot", "Android hardware pilot"),
|
|
513
550
|
("iOS simulator hardware pilot", "iOS simulator hardware pilot"),
|
|
@@ -582,6 +619,7 @@ next_step_commands = {
|
|
|
582
619
|
"local release gate": ["./scripts/release-candidate.sh --mode local"],
|
|
583
620
|
"public Android demo": ["zmr-demo-android --runs 5"],
|
|
584
621
|
"public iOS simulator demo": ["zmr-demo-ios --runs 5"],
|
|
622
|
+
"agent workflow smoke": ["./scripts/release-gate.sh"],
|
|
585
623
|
"physical iOS readiness": [physical_ios_pilot_command(default_pilot_evidence)],
|
|
586
624
|
"Android hardware pilot": [grouped_simulator_pilot_command(default_pilot_evidence)],
|
|
587
625
|
"iOS simulator hardware pilot": [grouped_simulator_pilot_command(default_pilot_evidence)],
|
|
@@ -690,6 +728,11 @@ def append_grouped_next_steps(blocked_items):
|
|
|
690
728
|
append_next_step(next_steps, label, commands, present)
|
|
691
729
|
handled.update(present)
|
|
692
730
|
|
|
731
|
+
maybe_group(
|
|
732
|
+
["local release gate", "agent workflow smoke"],
|
|
733
|
+
"local release gate + agent workflow smoke",
|
|
734
|
+
["./scripts/release-candidate.sh --mode local"],
|
|
735
|
+
)
|
|
693
736
|
maybe_group(
|
|
694
737
|
["Android hardware pilot", "iOS simulator hardware pilot"],
|
|
695
738
|
"Android hardware pilot + iOS simulator hardware pilot",
|
|
@@ -171,6 +171,11 @@ run() {
|
|
|
171
171
|
fi
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
run_zmr_report() {
|
|
175
|
+
local trace_dir="$1"
|
|
176
|
+
run "$ZMR_BIN" report "$trace_dir" --out "$trace_dir/report.html" --junit "$trace_dir/junit.xml"
|
|
177
|
+
}
|
|
178
|
+
|
|
174
179
|
capture() {
|
|
175
180
|
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
176
181
|
echo ""
|
|
@@ -505,7 +510,7 @@ if [[ "$RUNS" -eq 1 ]]; then
|
|
|
505
510
|
SINGLE_TRACE="$TRACE_ROOT/scenario"
|
|
506
511
|
run rm -rf "$SINGLE_TRACE"
|
|
507
512
|
run_zmr_android_scenario "$SCENARIO" --device "$DEVICE" --app-id "$APP_ID" --trace-dir "$SINGLE_TRACE"
|
|
508
|
-
|
|
513
|
+
run_zmr_report "$SINGLE_TRACE"
|
|
509
514
|
run "$ZMR_BIN" export "$SINGLE_TRACE" --out "$TRACE_ROOT/scenario.zmrtrace"
|
|
510
515
|
run "$ZMR_BIN" export "$SINGLE_TRACE" --out "$TRACE_ROOT/scenario-redacted.zmrtrace" --redact
|
|
511
516
|
else
|
|
@@ -513,11 +518,11 @@ if [[ "$RUNS" -eq 1 ]]; then
|
|
|
513
518
|
LOGIN_TRACE="$TRACE_ROOT/login-smoke"
|
|
514
519
|
run rm -rf "$AUTH_TRACE" "$LOGIN_TRACE"
|
|
515
520
|
run_zmr_android_scenario examples/android-app-auth-probe.json --device "$DEVICE" --app-id "$APP_ID" --trace-dir "$AUTH_TRACE"
|
|
516
|
-
|
|
521
|
+
run_zmr_report "$AUTH_TRACE"
|
|
517
522
|
run "$ZMR_BIN" export "$AUTH_TRACE" --out "$TRACE_ROOT/auth.zmrtrace"
|
|
518
523
|
run "$ZMR_BIN" export "$AUTH_TRACE" --out "$TRACE_ROOT/auth-redacted.zmrtrace" --redact
|
|
519
524
|
run_zmr_android_scenario examples/android-app-login-smoke.json --device "$DEVICE" --app-id "$APP_ID" --trace-dir "$LOGIN_TRACE"
|
|
520
|
-
|
|
525
|
+
run_zmr_report "$LOGIN_TRACE"
|
|
521
526
|
run "$ZMR_BIN" export "$LOGIN_TRACE" --out "$TRACE_ROOT/login-smoke.zmrtrace"
|
|
522
527
|
run "$ZMR_BIN" export "$LOGIN_TRACE" --out "$TRACE_ROOT/login-smoke-redacted.zmrtrace" --redact
|
|
523
528
|
fi
|
|
@@ -531,12 +536,12 @@ else
|
|
|
531
536
|
fi
|
|
532
537
|
if [[ -n "$SCENARIO" ]]; then
|
|
533
538
|
run_android_benchmark --zmr "$SCENARIO" --device "$DEVICE" --app-id "$APP_ID" --runs "$RUNS" --trace-root "$TRACE_ROOT/bench-scenario" "${benchmark_gate_args[@]}"
|
|
534
|
-
|
|
539
|
+
run_zmr_report "$TRACE_ROOT/bench-scenario"
|
|
535
540
|
else
|
|
536
541
|
run_android_benchmark --zmr examples/android-app-auth-probe.json --device "$DEVICE" --app-id "$APP_ID" --runs "$RUNS" --trace-root "$TRACE_ROOT/bench-auth" "${benchmark_gate_args[@]}"
|
|
537
|
-
|
|
542
|
+
run_zmr_report "$TRACE_ROOT/bench-auth"
|
|
538
543
|
run_android_benchmark --zmr examples/android-app-login-smoke.json --device "$DEVICE" --app-id "$APP_ID" --runs "$RUNS" --trace-root "$TRACE_ROOT/bench-login-smoke" "${benchmark_gate_args[@]}"
|
|
539
|
-
|
|
544
|
+
run_zmr_report "$TRACE_ROOT/bench-login-smoke"
|
|
540
545
|
fi
|
|
541
546
|
fi
|
|
542
547
|
|
|
@@ -546,9 +551,30 @@ cat <<EOF
|
|
|
546
551
|
|
|
547
552
|
Android pilot complete.
|
|
548
553
|
Output directory: $TRACE_ROOT
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
554
|
+
EOF
|
|
555
|
+
|
|
556
|
+
if [[ "$RUNS" -eq 1 ]]; then
|
|
557
|
+
echo "Shareable bundles:"
|
|
558
|
+
if [[ -n "$SCENARIO" ]]; then
|
|
559
|
+
echo " $TRACE_ROOT/scenario-redacted.zmrtrace"
|
|
560
|
+
else
|
|
561
|
+
echo " $TRACE_ROOT/auth-redacted.zmrtrace"
|
|
562
|
+
echo " $TRACE_ROOT/login-smoke-redacted.zmrtrace"
|
|
563
|
+
fi
|
|
564
|
+
else
|
|
565
|
+
echo "Benchmark reports:"
|
|
566
|
+
if [[ -n "$SCENARIO" ]]; then
|
|
567
|
+
echo " $TRACE_ROOT/bench-scenario/report.html"
|
|
568
|
+
echo " $TRACE_ROOT/bench-scenario/junit.xml"
|
|
569
|
+
else
|
|
570
|
+
echo " $TRACE_ROOT/bench-auth/report.html"
|
|
571
|
+
echo " $TRACE_ROOT/bench-auth/junit.xml"
|
|
572
|
+
echo " $TRACE_ROOT/bench-login-smoke/report.html"
|
|
573
|
+
echo " $TRACE_ROOT/bench-login-smoke/junit.xml"
|
|
574
|
+
fi
|
|
575
|
+
fi
|
|
576
|
+
|
|
577
|
+
cat <<EOF
|
|
552
578
|
Viewer:
|
|
553
579
|
$ROOT/viewer/index.html
|
|
554
580
|
EOF
|
package/scripts/run-ios-pilot.sh
CHANGED
|
@@ -156,6 +156,11 @@ run() {
|
|
|
156
156
|
fi
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
run_zmr_report() {
|
|
160
|
+
local trace_dir="$1"
|
|
161
|
+
run "$ZMR_BIN" report "$trace_dir" --out "$trace_dir/report.html" --junit "$trace_dir/junit.xml"
|
|
162
|
+
}
|
|
163
|
+
|
|
159
164
|
is_retryable_simctl_text() {
|
|
160
165
|
local text="$1"
|
|
161
166
|
[[ "$text" == *"CoreSimulatorService connection became invalid"* ]] ||
|
|
@@ -455,7 +460,7 @@ if [[ "$RUNS" -eq 1 ]]; then
|
|
|
455
460
|
else
|
|
456
461
|
run "$ZMR_BIN" run examples/ios-smoke.json --platform ios --ios-device-type "$IOS_DEVICE_TYPE" --device "$DEVICE" --app-id "$APP_ID" --xcrun "$XCRUN" --trace-dir "$TRACE_DIR"
|
|
457
462
|
fi
|
|
458
|
-
|
|
463
|
+
run_zmr_report "$TRACE_DIR"
|
|
459
464
|
run "$ZMR_BIN" export "$TRACE_DIR" --out "$TRACE_ROOT/ios-smoke.zmrtrace"
|
|
460
465
|
run "$ZMR_BIN" export "$TRACE_DIR" --out "$TRACE_ROOT/ios-smoke-redacted.zmrtrace" --redact
|
|
461
466
|
|
|
@@ -463,7 +468,7 @@ if [[ "$RUNS" -eq 1 ]]; then
|
|
|
463
468
|
SHIM_TRACE_DIR="$TRACE_ROOT/ios-shim-smoke"
|
|
464
469
|
run rm -rf "$SHIM_TRACE_DIR"
|
|
465
470
|
run "$ZMR_BIN" run examples/ios-shim-smoke.json --platform ios --ios-device-type "$IOS_DEVICE_TYPE" --device "$DEVICE" --app-id "$APP_ID" --xcrun "$XCRUN" --ios-shim "$IOS_SHIM" --trace-dir "$SHIM_TRACE_DIR"
|
|
466
|
-
|
|
471
|
+
run_zmr_report "$SHIM_TRACE_DIR"
|
|
467
472
|
run "$ZMR_BIN" export "$SHIM_TRACE_DIR" --out "$TRACE_ROOT/ios-shim-smoke.zmrtrace"
|
|
468
473
|
run "$ZMR_BIN" export "$SHIM_TRACE_DIR" --out "$TRACE_ROOT/ios-shim-smoke-redacted.zmrtrace" --redact
|
|
469
474
|
fi
|
|
@@ -481,11 +486,11 @@ else
|
|
|
481
486
|
else
|
|
482
487
|
ZMR_BIN="$ZMR_BIN" run "$ROOT/scripts/benchmark.sh" --zmr examples/ios-smoke.json --device "$DEVICE" --platform ios --ios-device-type "$IOS_DEVICE_TYPE" --app-id "$APP_ID" --xcrun "$XCRUN" --runs "$RUNS" --trace-root "$TRACE_ROOT/ios-smoke-benchmark" "${benchmark_gate_args[@]}"
|
|
483
488
|
fi
|
|
484
|
-
|
|
489
|
+
run_zmr_report "$TRACE_ROOT/ios-smoke-benchmark"
|
|
485
490
|
|
|
486
491
|
if [[ -n "$IOS_SHIM" ]]; then
|
|
487
492
|
ZMR_BIN="$ZMR_BIN" run "$ROOT/scripts/benchmark.sh" --zmr examples/ios-shim-smoke.json --device "$DEVICE" --platform ios --ios-device-type "$IOS_DEVICE_TYPE" --app-id "$APP_ID" --xcrun "$XCRUN" --ios-shim "$IOS_SHIM" --runs "$RUNS" --trace-root "$TRACE_ROOT/ios-shim-smoke-benchmark" "${benchmark_gate_args[@]}"
|
|
488
|
-
|
|
493
|
+
run_zmr_report "$TRACE_ROOT/ios-shim-smoke-benchmark"
|
|
489
494
|
fi
|
|
490
495
|
fi
|
|
491
496
|
|
|
@@ -501,8 +506,10 @@ if [[ "$RUNS" -eq 1 ]]; then
|
|
|
501
506
|
else
|
|
502
507
|
echo "Benchmark reports:"
|
|
503
508
|
echo " $TRACE_ROOT/ios-smoke-benchmark/report.html"
|
|
509
|
+
echo " $TRACE_ROOT/ios-smoke-benchmark/junit.xml"
|
|
504
510
|
if [[ -n "$IOS_SHIM" ]]; then
|
|
505
511
|
echo " $TRACE_ROOT/ios-shim-smoke-benchmark/report.html"
|
|
512
|
+
echo " $TRACE_ROOT/ios-shim-smoke-benchmark/junit.xml"
|
|
506
513
|
fi
|
|
507
514
|
fi
|
|
508
515
|
echo "Viewer:"
|
package/shims/ios/ZMRShim.swift
CHANGED
|
@@ -22,6 +22,11 @@ struct ZMRShimBounds: Encodable {
|
|
|
22
22
|
let height: Int
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
struct ZMRShimViewport: Encodable {
|
|
26
|
+
let width: Int
|
|
27
|
+
let height: Int
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
struct ZMRShimNode: Encodable {
|
|
26
31
|
let id: String
|
|
27
32
|
let type: String
|
|
@@ -35,6 +40,11 @@ struct ZMRShimNode: Encodable {
|
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
enum ZMRShim {
|
|
43
|
+
static func viewport(app: XCUIApplication) -> ZMRShimViewport {
|
|
44
|
+
let frame = app.frame
|
|
45
|
+
return ZMRShimViewport(width: Int(frame.size.width), height: Int(frame.size.height))
|
|
46
|
+
}
|
|
47
|
+
|
|
38
48
|
static func snapshot(app: XCUIApplication) -> [ZMRShimNode] {
|
|
39
49
|
let queries: [(XCUIElement.ElementType, XCUIElementQuery)] = [
|
|
40
50
|
(.button, app.buttons),
|
|
@@ -105,10 +105,15 @@ final class ZMRShimUITestCase: XCTestCase {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
private func run(command: ZMRShimCommand, app: XCUIApplication) -> [String: Any] {
|
|
108
|
+
if commandRequiresForeground(command), let foregroundError = ensureAppForeground(app: app) {
|
|
109
|
+
return foregroundError
|
|
110
|
+
}
|
|
111
|
+
|
|
108
112
|
switch command.cmd {
|
|
109
113
|
case "snapshot":
|
|
110
114
|
return [
|
|
111
115
|
"status": "ok",
|
|
116
|
+
"viewport": ZMRShim.viewport(app: app).json,
|
|
112
117
|
"nodes": ZMRShim.snapshot(app: app).map { $0.json }
|
|
113
118
|
]
|
|
114
119
|
case "screenshot":
|
|
@@ -188,6 +193,34 @@ final class ZMRShimUITestCase: XCTestCase {
|
|
|
188
193
|
}
|
|
189
194
|
}
|
|
190
195
|
|
|
196
|
+
private func commandRequiresForeground(_ command: ZMRShimCommand) -> Bool {
|
|
197
|
+
switch command.cmd {
|
|
198
|
+
case "snapshot", "query", "tap", "type", "eraseText", "hideKeyboard", "swipe", "settle":
|
|
199
|
+
return true
|
|
200
|
+
default:
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private func ensureAppForeground(app: XCUIApplication) -> [String: Any]? {
|
|
206
|
+
if app.state != .runningForeground {
|
|
207
|
+
app.activate()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let deadline = Date().addingTimeInterval(5)
|
|
211
|
+
while Date() < deadline {
|
|
212
|
+
if app.state == .runningForeground {
|
|
213
|
+
return nil
|
|
214
|
+
}
|
|
215
|
+
Thread.sleep(forTimeInterval: 0.1)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return error(
|
|
219
|
+
"app.not_foreground",
|
|
220
|
+
"target app did not become foreground; state=\(app.state.rawValue)"
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
191
224
|
private func ok() -> [String: Any] {
|
|
192
225
|
["status": "ok"]
|
|
193
226
|
}
|
|
@@ -532,6 +565,15 @@ private extension ZMRShimBounds {
|
|
|
532
565
|
}
|
|
533
566
|
}
|
|
534
567
|
|
|
568
|
+
private extension ZMRShimViewport {
|
|
569
|
+
var json: [String: Any] {
|
|
570
|
+
[
|
|
571
|
+
"width": width,
|
|
572
|
+
"height": height
|
|
573
|
+
]
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
535
577
|
private extension ZMRShimNode {
|
|
536
578
|
var json: [String: Any] {
|
|
537
579
|
[
|
package/shims/ios/protocol.md
CHANGED
|
@@ -57,9 +57,34 @@ zmr mcp --config .zmr/config.json --trace-dir traces/zmr-agent
|
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
Use the `semantic_snapshot`, `tap`, `type`, `wait_visible`, `trace_events`, and
|
|
60
|
-
`trace_export` tools. Prefer
|
|
61
|
-
|
|
62
|
-
actions.
|
|
60
|
+
`trace_explore`, `trace_discover`, and `trace_export` tools. Prefer
|
|
61
|
+
`semantic_snapshot` because it normalizes Android and iOS hierarchy classes
|
|
62
|
+
into roles, selectors, bounds, and recommended actions.
|
|
63
|
+
|
|
64
|
+
After a session has produced trace artifacts, prefer the review-first
|
|
65
|
+
exploration handoff when a goal should travel with the generated scenario
|
|
66
|
+
candidate:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{"method":"trace.explore","params":{"out":".zmr/discovered/login-smoke.json","goal":"find a stable login smoke","includeActions":true,"validate":true,"force":true}}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
For MCP agents, call `trace_explore` with the same `out`, `goal`,
|
|
73
|
+
`includeActions`, `validate`, and `force` arguments. The offline CLI equivalent
|
|
74
|
+
is:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
zmr explore --from-trace traces/zmr-agent \
|
|
78
|
+
--out .zmr/discovered/login-smoke.json \
|
|
79
|
+
--goal "find a stable login smoke" \
|
|
80
|
+
--include-actions \
|
|
81
|
+
--validate \
|
|
82
|
+
--json
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Treat the output as a starting point. Its JSON includes `autonomous:false`,
|
|
86
|
+
`reviewRequired:true`, `guardrails`, replay coverage, validation, and next
|
|
87
|
+
commands; it does not crawl, discover credentials, or commit tests.
|
|
63
88
|
|
|
64
89
|
## Scenario Pattern
|
|
65
90
|
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
|
|
3
|
+
const cli_draft = @import("cli_draft.zig");
|
|
4
|
+
const cli_output = @import("cli_output.zig");
|
|
5
|
+
const trace = @import("trace.zig");
|
|
6
|
+
const validation = @import("validation.zig");
|
|
7
|
+
const version = @import("version.zig");
|
|
8
|
+
|
|
9
|
+
pub const ParsedArgs = struct {
|
|
10
|
+
from_trace: ?[]const u8 = null,
|
|
11
|
+
out_path: ?[]const u8 = null,
|
|
12
|
+
name: ?[]const u8 = null,
|
|
13
|
+
app_id: ?[]const u8 = null,
|
|
14
|
+
include_actions: bool = false,
|
|
15
|
+
validate: bool = false,
|
|
16
|
+
force: bool = false,
|
|
17
|
+
json: bool = false,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
pub const DiscoverSummary = struct {
|
|
21
|
+
ok: bool,
|
|
22
|
+
draft: cli_draft.DraftSummary,
|
|
23
|
+
validated: bool,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
pub const JsonOptions = struct {
|
|
27
|
+
mode: []const u8 = "discover",
|
|
28
|
+
goal: ?[]const u8 = null,
|
|
29
|
+
autonomous: ?bool = null,
|
|
30
|
+
review_required: ?bool = null,
|
|
31
|
+
guardrails: []const []const u8 = &.{},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
pub const OwnedDiscover = struct {
|
|
35
|
+
draft: cli_draft.OwnedDraft,
|
|
36
|
+
validation: ?validation.Result = null,
|
|
37
|
+
summary: DiscoverSummary,
|
|
38
|
+
|
|
39
|
+
pub fn deinit(self: *OwnedDiscover, allocator: std.mem.Allocator) void {
|
|
40
|
+
if (self.validation) |result| result.deinit(allocator);
|
|
41
|
+
self.draft.deinit(allocator);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
pub fn parseArgs(args: []const []const u8) !ParsedArgs {
|
|
46
|
+
var parsed = ParsedArgs{};
|
|
47
|
+
var index: usize = 0;
|
|
48
|
+
while (index < args.len) : (index += 1) {
|
|
49
|
+
const arg = args[index];
|
|
50
|
+
if (std.mem.eql(u8, arg, "--from-trace")) {
|
|
51
|
+
index += 1;
|
|
52
|
+
parsed.from_trace = if (index < args.len) args[index] else return error.MissingTraceDir;
|
|
53
|
+
} else if (std.mem.eql(u8, arg, "--out")) {
|
|
54
|
+
index += 1;
|
|
55
|
+
parsed.out_path = if (index < args.len) args[index] else return error.MissingDraftOut;
|
|
56
|
+
} else if (std.mem.eql(u8, arg, "--name")) {
|
|
57
|
+
index += 1;
|
|
58
|
+
parsed.name = if (index < args.len) args[index] else return error.MissingParam;
|
|
59
|
+
} else if (std.mem.eql(u8, arg, "--app-id")) {
|
|
60
|
+
index += 1;
|
|
61
|
+
parsed.app_id = if (index < args.len) args[index] else return error.MissingAppId;
|
|
62
|
+
} else if (std.mem.eql(u8, arg, "--include-actions")) {
|
|
63
|
+
parsed.include_actions = true;
|
|
64
|
+
} else if (std.mem.eql(u8, arg, "--validate")) {
|
|
65
|
+
parsed.validate = true;
|
|
66
|
+
} else if (std.mem.eql(u8, arg, "--force")) {
|
|
67
|
+
parsed.force = true;
|
|
68
|
+
} else if (std.mem.eql(u8, arg, "--json")) {
|
|
69
|
+
parsed.json = true;
|
|
70
|
+
} else {
|
|
71
|
+
return error.UnknownFlag;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (parsed.from_trace == null) return error.MissingTraceDir;
|
|
76
|
+
if (parsed.out_path == null) return error.MissingDraftOut;
|
|
77
|
+
return parsed;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
pub fn run(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
|
|
81
|
+
var raw_args = std.ArrayList([]const u8).empty;
|
|
82
|
+
defer raw_args.deinit(allocator);
|
|
83
|
+
while (args.next()) |arg| try raw_args.append(allocator, arg);
|
|
84
|
+
|
|
85
|
+
const parsed = try parseArgs(raw_args.items);
|
|
86
|
+
var discovered = try discoverFromTrace(allocator, parsed);
|
|
87
|
+
defer discovered.deinit(allocator);
|
|
88
|
+
|
|
89
|
+
const stdout = std.fs.File.stdout().deprecatedWriter();
|
|
90
|
+
if (parsed.json) {
|
|
91
|
+
try writeJson(stdout, discovered.summary, discovered.validation);
|
|
92
|
+
} else {
|
|
93
|
+
try stdout.print("wrote {s}\n", .{discovered.summary.draft.out_path});
|
|
94
|
+
if (discovered.validation) |result| {
|
|
95
|
+
if (result.ok) {
|
|
96
|
+
try stdout.print("validated {s}\n", .{discovered.summary.draft.out_path});
|
|
97
|
+
} else {
|
|
98
|
+
try stdout.print("validation failed {s}\n", .{discovered.summary.draft.out_path});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
try stdout.writeAll("next: zmr validate --json ");
|
|
102
|
+
try cli_output.writeShellArg(stdout, discovered.summary.draft.out_path);
|
|
103
|
+
try stdout.writeAll("\n");
|
|
104
|
+
}
|
|
105
|
+
if (!discovered.summary.ok) std.process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
pub fn discoverFromTrace(allocator: std.mem.Allocator, parsed: ParsedArgs) !OwnedDiscover {
|
|
109
|
+
var draft = try cli_draft.draftFromTrace(allocator, .{
|
|
110
|
+
.from_trace = parsed.from_trace,
|
|
111
|
+
.out_path = parsed.out_path,
|
|
112
|
+
.name = parsed.name,
|
|
113
|
+
.app_id = parsed.app_id,
|
|
114
|
+
.include_actions = parsed.include_actions,
|
|
115
|
+
.force = parsed.force,
|
|
116
|
+
.json = parsed.json,
|
|
117
|
+
});
|
|
118
|
+
errdefer draft.deinit(allocator);
|
|
119
|
+
|
|
120
|
+
var validation_result: ?validation.Result = null;
|
|
121
|
+
errdefer if (validation_result) |result| result.deinit(allocator);
|
|
122
|
+
if (parsed.validate) {
|
|
123
|
+
validation_result = try validation.validateFile(allocator, draft.summary.out_path);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const ok = draft.summary.ok and (validation_result == null or validation_result.?.ok);
|
|
127
|
+
return .{
|
|
128
|
+
.draft = draft,
|
|
129
|
+
.validation = validation_result,
|
|
130
|
+
.summary = .{
|
|
131
|
+
.ok = ok,
|
|
132
|
+
.draft = draft.summary,
|
|
133
|
+
.validated = parsed.validate,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
pub fn writeJson(writer: anytype, summary: DiscoverSummary, validation_result: ?validation.Result) !void {
|
|
139
|
+
try writeJsonWithOptions(writer, summary, validation_result, .{});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
pub fn writeJsonWithOptions(writer: anytype, summary: DiscoverSummary, validation_result: ?validation.Result, options: JsonOptions) !void {
|
|
143
|
+
const draft = summary.draft;
|
|
144
|
+
try writer.writeAll("{\"ok\":");
|
|
145
|
+
try writer.writeAll(if (summary.ok) "true" else "false");
|
|
146
|
+
try writer.writeAll(",\"mode\":");
|
|
147
|
+
try trace.writeJsonString(writer, options.mode);
|
|
148
|
+
try writer.writeAll(",\"schemaVersion\":1");
|
|
149
|
+
try writer.writeAll(",\"runnerVersion\":");
|
|
150
|
+
try trace.writeJsonString(writer, version.runner_version);
|
|
151
|
+
try writer.writeAll(",\"protocolVersion\":");
|
|
152
|
+
try trace.writeJsonString(writer, version.protocol_version);
|
|
153
|
+
if (options.goal) |goal| {
|
|
154
|
+
try writer.writeAll(",\"goal\":");
|
|
155
|
+
try trace.writeJsonString(writer, goal);
|
|
156
|
+
}
|
|
157
|
+
if (options.autonomous) |autonomous| {
|
|
158
|
+
try writer.writeAll(",\"autonomous\":");
|
|
159
|
+
try writer.writeAll(if (autonomous) "true" else "false");
|
|
160
|
+
}
|
|
161
|
+
if (options.review_required) |review_required| {
|
|
162
|
+
try writer.writeAll(",\"reviewRequired\":");
|
|
163
|
+
try writer.writeAll(if (review_required) "true" else "false");
|
|
164
|
+
}
|
|
165
|
+
if (options.guardrails.len > 0) {
|
|
166
|
+
try writer.writeAll(",\"guardrails\":[");
|
|
167
|
+
for (options.guardrails, 0..) |guardrail, index| {
|
|
168
|
+
if (index > 0) try writer.writeAll(",");
|
|
169
|
+
try trace.writeJsonString(writer, guardrail);
|
|
170
|
+
}
|
|
171
|
+
try writer.writeAll("]");
|
|
172
|
+
}
|
|
173
|
+
try writer.writeAll(",\"out\":");
|
|
174
|
+
try trace.writeJsonString(writer, draft.out_path);
|
|
175
|
+
try writer.writeAll(",\"traceDir\":");
|
|
176
|
+
try trace.writeJsonString(writer, draft.trace_dir);
|
|
177
|
+
try writer.writeAll(",\"sourceSnapshot\":");
|
|
178
|
+
try trace.writeJsonString(writer, draft.source_snapshot);
|
|
179
|
+
try writer.writeAll(",\"name\":");
|
|
180
|
+
try trace.writeJsonString(writer, draft.name);
|
|
181
|
+
try writer.writeAll(",\"appId\":");
|
|
182
|
+
if (draft.app_id) |actual| {
|
|
183
|
+
try trace.writeJsonString(writer, actual);
|
|
184
|
+
} else {
|
|
185
|
+
try writer.writeAll("null");
|
|
186
|
+
}
|
|
187
|
+
try writer.print(",\"selectorCount\":{d},\"stepCount\":{d}", .{ draft.selector_count, draft.step_count });
|
|
188
|
+
try cli_draft.writeReplayJson(writer, draft.replay);
|
|
189
|
+
try writer.writeAll(",\"warnings\":[");
|
|
190
|
+
for (draft.warnings, 0..) |warning, index| {
|
|
191
|
+
if (index > 0) try writer.writeAll(",");
|
|
192
|
+
try trace.writeJsonString(writer, warning);
|
|
193
|
+
}
|
|
194
|
+
try writer.writeAll("],\"validated\":");
|
|
195
|
+
try writer.writeAll(if (summary.validated) "true" else "false");
|
|
196
|
+
try writer.writeAll(",\"validation\":");
|
|
197
|
+
if (validation_result) |result| {
|
|
198
|
+
try writeValidationObject(writer, draft.out_path, result);
|
|
199
|
+
} else {
|
|
200
|
+
try writer.writeAll("null");
|
|
201
|
+
}
|
|
202
|
+
try writer.writeAll(",\"nextCommands\":[\"zmr validate --json ");
|
|
203
|
+
try cli_output.writeShellArgJsonContent(writer, draft.out_path);
|
|
204
|
+
try writer.writeAll("\",\"zmr run ");
|
|
205
|
+
try cli_output.writeShellArgJsonContent(writer, draft.out_path);
|
|
206
|
+
try writer.writeAll(" --json --trace-dir ");
|
|
207
|
+
try cli_output.writeShellArgJsonContent(writer, draft.trace_dir);
|
|
208
|
+
try writer.writeAll("\"]}\n");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
fn writeValidationObject(writer: anytype, path: []const u8, result: validation.Result) !void {
|
|
212
|
+
try writer.writeAll("{\"ok\":");
|
|
213
|
+
try writer.writeAll(if (result.ok) "true" else "false");
|
|
214
|
+
try writer.writeAll(",\"path\":");
|
|
215
|
+
try trace.writeJsonString(writer, path);
|
|
216
|
+
if (result.ok) {
|
|
217
|
+
try writer.writeAll(",\"name\":");
|
|
218
|
+
try trace.writeJsonString(writer, result.name.?);
|
|
219
|
+
try writer.writeAll(",\"appId\":");
|
|
220
|
+
if (result.app_id) |app_id| {
|
|
221
|
+
try trace.writeJsonString(writer, app_id);
|
|
222
|
+
} else {
|
|
223
|
+
try writer.writeAll("null");
|
|
224
|
+
}
|
|
225
|
+
try writer.print(",\"stepCount\":{d}", .{result.step_count});
|
|
226
|
+
} else {
|
|
227
|
+
try writer.writeAll(",\"errorCode\":");
|
|
228
|
+
try trace.writeJsonString(writer, result.error_code.?);
|
|
229
|
+
try writer.writeAll(",\"message\":");
|
|
230
|
+
try trace.writeJsonString(writer, result.message.?);
|
|
231
|
+
if (result.path) |field_path| {
|
|
232
|
+
try writer.writeAll(",\"fieldPath\":");
|
|
233
|
+
try trace.writeJsonString(writer, field_path);
|
|
234
|
+
}
|
|
235
|
+
if (result.line) |line| try writer.print(",\"line\":{d}", .{line});
|
|
236
|
+
if (result.column) |column| try writer.print(",\"column\":{d}", .{column});
|
|
237
|
+
}
|
|
238
|
+
try writer.writeAll("}");
|
|
239
|
+
}
|