agent-desktop/tests/e2e/electron_metrics_report.py
Lahfir 3f322728b4
feat!: implement Playwright-grade foundation contract
Settle the Playwright-grade reliability contract in agent-desktop-core
before the Windows/Linux adapters are built, so they inherit it instead
of redesigning it. Every command now observes, waits, verifies, and
reports honestly instead of firing blindly.

Highlights: capability-supertrait split of PlatformAdapter with
not_supported() defaults; canonical role/state vocabulary with live
`is --property visible`; display enumeration (`list-displays`) and honest
`--screen` with scale factor; truthful Automation permission; `native_id`
identity spine; window-id-first resolution; serializable `LocatorQuery`
with live `find`; default-on auto-wait before every ref action; three-way
`hit_test` occlusion gate; `scroll_into_view` in core; core accessible-name
precedence; typed `ActionStep` delivery tier; `ProcessState` and
`APP_UNRESPONSIVE`; `LaunchOptions`; baseline-diff desktop signals
(`wait --event`); typed clipboard (`Text`/`Image`/`FileUrls`); mouse
modifier chords and `mouse-wheel`. Hardened through a 35-reviewer pass with
independent validation and a green live e2e gate (109/0), plus a
head-vs-main performance comparison harness.

BREAKING CHANGE: default-on auto-wait changes the timing of every
previously-untouched ref-action call (bounded 5000 ms default; `--timeout-ms 0`
restores single-shot). `ENVELOPE_VERSION` is now `2.1` (adds the
`APP_UNRESPONSIVE` code and process state in error details). FFI ABI major
is `3` (append-only struct evolution; `wait --event` is intentionally not
exposed over FFI). The legacy string clipboard API is removed in favor of
typed content. `key-down`/`key-up` fail closed until daemon-owned held input
exists. `close-app` verifies termination and the osascript fallback path is
removed. `--text` matching is subtree containment: `find --text X --first`
returns the outermost matching container.
2026-07-20 00:21:38 -07:00

204 lines
7.6 KiB
Python

import datetime
import math
import platform
from collections import Counter
def percentile(values, quantile):
ordered = sorted(values)
index = max(0, math.ceil(quantile * len(ordered)) - 1)
return ordered[index]
def metric_summary(values, unit):
return {
"unit": unit,
"samples": len(values),
"min": round(min(values), 3),
"p50": round(percentile(values, 0.50), 3),
"p95": round(percentile(values, 0.95), 3),
"max": round(max(values), 3),
}
def rate(count, total):
return round(count / total, 6) if total else 0.0
def nested_number(sample, *path):
value = sample.get("stats")
for part in path:
if not isinstance(value, dict):
return None
value = value.get(part)
if isinstance(value, bool) or not isinstance(value, (int, float)):
return None
return float(value)
def optional_metric(samples, unit, *path):
values = [nested_number(sample, *path) for sample in samples]
available = [value for value in values if value is not None]
result = {
"available": bool(available),
"available_samples": len(available),
"total_samples": len(samples),
}
if available:
result["summary"] = metric_summary(available, unit)
return result
def summarize_run(identity, samples):
attempts = len(samples)
command_successes = sum(sample["command_success"] for sample in samples)
addressable = sum(sample["addressable"] for sample in samples)
exact = sum(sample["exact_reresolution"] for sample in samples)
correct = sum(sample["correct"] for sample in samples)
failures = Counter(
sample["failure_kind"] for sample in samples if sample.get("failure_kind")
)
activation = [
sample["stats"]["activation"]
for sample in samples
if isinstance(sample.get("stats"), dict)
and isinstance(sample["stats"].get("activation"), dict)
]
return {
"binary": identity,
"metrics": {
"end_to_end_wall_all_attempts": metric_summary(
[sample["wall_ms"] for sample in samples], "ms"
),
"process_cpu_all_attempts": metric_summary(
[sample["cpu_ms"] for sample in samples], "ms"
),
"locator_internal": optional_metric(samples, "us", "elapsed_us"),
"nodes_visited": optional_metric(
samples, "nodes", "traversal", "nodes_visited"
),
"peak_native_handles": optional_metric(
samples, "handles", "traversal", "peak_handles_owned"
),
"attribute_batches": optional_metric(
samples, "batches", "reads", "attribute_batches"
),
"attributes_requested": optional_metric(
samples, "attributes", "reads", "attributes_requested"
),
"cannot_complete": optional_metric(
samples, "events", "reads", "cannot_complete"
),
"deadline_exhausted": optional_metric(
samples, "events", "reads", "deadline_exhausted"
),
},
"reliability": {
"attempts": attempts,
"command_successes": command_successes,
"command_success_rate": rate(command_successes, attempts),
"addressable_results": addressable,
"addressable_result_rate": rate(addressable, attempts),
"exact_reresolutions": exact,
"exact_reresolution_rate": rate(exact, attempts),
"correct_results": correct,
"correct_result_rate": rate(correct, attempts),
"failure_kinds": dict(sorted(failures.items())),
},
"activation": {
"available_samples": len(activation),
"attempted_runs": sum(bool(item.get("attempted")) for item in activation),
"succeeded_runs": sum(bool(item.get("succeeded")) for item in activation),
"ready_runs": sum(bool(item.get("ready")) for item in activation),
},
"samples": samples,
}
def paired_summary(pairs):
comparable = [
pair
for pair in pairs
if pair["current"]["command_success"]
and pair["baseline"]["command_success"]
]
wall = [
pair["current"]["wall_ms"] - pair["baseline"]["wall_ms"]
for pair in comparable
]
cpu = [
pair["current"]["cpu_ms"] - pair["baseline"]["cpu_ms"]
for pair in comparable
]
current_only = sum(
pair["current"]["correct"] and not pair["baseline"]["correct"]
for pair in pairs
)
baseline_only = sum(
pair["baseline"]["correct"] and not pair["current"]["correct"]
for pair in pairs
)
result = {
"paired_attempts": len(pairs),
"comparable_successful_pairs": len(comparable),
"orders": dict(sorted(Counter(pair["order"] for pair in pairs).items())),
"correctness_discordance": {
"current_only_correct": current_only,
"baseline_only_correct": baseline_only,
},
}
if comparable:
result["current_minus_baseline"] = {
"wall": metric_summary(wall, "ms"),
"cpu": metric_summary(cpu, "ms"),
"current_faster_wall_pairs": sum(value < 0 for value in wall),
"current_faster_wall_rate": rate(sum(value < 0 for value in wall), len(wall)),
}
return result
def build_report(args, runs, pairs, state_reference):
baseline = "baseline" in runs
report = {
"schema": "agent-desktop-electron-live-v3",
"ok": True,
"generated_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
"app": args.app,
"environment": {
"platform": platform.platform(),
"architecture": platform.machine(),
},
"method": {
"operation": "release CLI find --app APP --window-id EXACT --role button --first",
"warmup_pairs": args.warmups,
"measured_pairs": args.samples,
"order": "deterministic balanced AB/BA interleaving",
"order_labels": "A=baseline, B=current",
"percentile": "nearest-rank",
"subprocess_timeout_seconds": args.timeout_seconds,
"subprocess_capture_limit_bytes": args.capture_limit_bytes,
"trace_limit_bytes": 16 * 1024 * 1024,
"state_gate": "exact app process generation and window inventory before and after every pair",
"exclusivity": "caller acknowledgement plus harness-only lock; unrelated automation is not mechanically excluded",
"network_used": False,
"app_mutations": "none; observation and renderer accessibility activation only",
"cpu_scope": "per-command RUSAGE_CHILDREN user+system delta in a serial harness",
"rss_reported": False,
},
"state_reference": state_reference,
"comparison": {
"baseline_provided": baseline,
"commands_identical": True,
"separate_homes": True,
"immutable_binary_hashes": True,
},
"runs": {label: summarize_run(run["identity"], run["samples"]) for label, run in runs.items()},
"limitations": [
"Results describe this exact app state, machine, and TCC state; they are not a cross-machine benchmark.",
"The harness measures locator reliability and exact ref re-resolution without click or text mutation.",
"Locator-internal metrics are reported only for samples whose binary emitted valid query_stats.",
],
}
if baseline:
report["paired_comparison"] = paired_summary(pairs)
return report