Files
Ignacy Łątka cd4190b80e Full tgz-only E2E harness (scripts/e2e-full) (#495)
Now verified end-to-end on the real Mac:
install/introspection/validation, the android tier (against a booted
emulator), and the chromium tier all pass from a locally packed tgz; the
rn tier auto-skips without `~/dev/bluesky` as designed.

## What this is

A release-gating end-to-end harness under `scripts/e2e-full/` that
starts from **only a `swmansion-argent-*.tgz` bundle** and exercises the
whole product: the install flow, every CLI command, all 70 tools'
argument validation, and a happy-path run of every tool against real
devices. Intended to be run on the real Linux box and the real Mac
before a release.

Everything runs under a throwaway `HOME` + npm prefix, so it never
touches the machine's real `~/.argent`, editor MCP configs, or global
packages (safe on a shared box). It emits a JSONL log + a markdown
coverage report and exits non-zero if any hard assertion fails.

## Phases

| phase | needs | covers |
|---|---|---|
| `install` | npm + network | `npm i -g <tgz>`, bundled binaries, `init`
(global + `--local`), `update`, `uninstall`, telemetry, MCP-config
generation |
| `introspection` | — | `--version/--help`, `tools describe` for **all
70** tools, feature flags, `server start/status/logs/stop`,
`link/unlink` |
| `validation` | — | per tool: missing-required / bad-enum / bad-type
rejection (schema-driven, no hardware) |
| `android` | Android emulator | happy-path of every
touch/gesture/screenshot/app-lifecycle tool |
| `chromium` | Electron (bundled optional dep) + a display | boots a
generated Electron app; drives CDP tools
(scroll/drag/tabs/cookies/storage) |
| `rn` | `~/dev/bluesky` + Android device | debugger + react/native
profiler + network chain against the real Bluesky app |

Tiers auto-skip with a recorded reason when prerequisites are missing.
iOS / tvOS / Vega are intentionally out of scope for now.

## What I actually verified (real runs, Linux)

- **install** 18✓/0✗, **introspection** 85✓/0✗ (all 70 `tools
describe`), **validation** 81✓/0✗, **chromium** 24✓/0✗ (real Electron
app + CDP).
- Holistic run: **190✓ / 0✗ / 34∼, exit 0.** Failure-gate confirmed
(non-zero on fail), `bash -n` clean on all files, all 72 android/rn JSON
arg literals linted.

## What I did NOT verify live (please scrutinize)

- **`android` and `rn` tiers were never run against a live device.** The
Android emulator would not stay up on my box (memory-constrained shared
machine; QEMU threads hung seconds after boot across ~7 attempts). The
code is complete and uses the same machinery the `chromium` tier
exercised live, but it has **not** been proven end-to-end. This is the
main reason for the "not certain how well it works" caveat.
- The RN tier's exact debugger/profiler arg shapes are derived from
`tools describe`, not confirmed against a running Metro.

## Run it

```bash
bash scripts/e2e-full/run-e2e.sh                                   # everything applicable to this OS
bash scripts/e2e-full/run-e2e.sh --skip-install --phase introspection,validation   # fast offline core
bash scripts/e2e-full/run-e2e.sh --phase android --android-serial emulator-5554     # inject a device
```
2026-07-20 16:43:34 +02:00

111 lines
3.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Aggregate the E2E JSONL log into a markdown report.
Reads one JSON object per line: {phase, tool, case, status, detail}.
Emits: run header, per-phase pass/fail/skip summary, a per-tool coverage
matrix (was each tool validated? happy-path run?), and the full list of
failures with the exact case so a release engineer can reproduce.
"""
import json
import os
import sys
from collections import defaultdict, OrderedDict
DEVICE_PHASES = {"android", "chromium", "rn"}
def main() -> int:
path = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("E2E_JSONL", "")
rows = []
with open(path) as fh:
for line in fh:
line = line.strip()
if line:
try:
rows.append(json.loads(line))
except json.JSONDecodeError:
pass
version = os.environ.get("TGZ_VERSION", "?")
osname = os.environ.get("E2E_OS", "?")
binname = os.environ.get("ARGENT_BIN", "?")
total = defaultdict(int)
per_phase = defaultdict(lambda: defaultdict(int))
tools = OrderedDict() # tool -> {"validation": status, "happy": status}
fails = []
for r in rows:
st = r.get("status", "?")
ph = r.get("phase", "?")
tool = r.get("tool", "?")
total[st] += 1
per_phase[ph][st] += 1
tools.setdefault(tool, {"validated": False, "happy": None})
if ph == "validation":
if st == "pass":
tools[tool]["validated"] = True
if ph in DEVICE_PHASES:
cur = tools[tool]["happy"]
# a single pass anywhere marks the tool happy-path covered
if st == "pass":
tools[tool]["happy"] = "pass"
elif st == "fail" and cur != "pass":
tools[tool]["happy"] = "fail"
elif st == "skip" and cur is None:
tools[tool]["happy"] = "skip"
if st == "fail":
fails.append(r)
out = []
w = out.append
w(f"# Argent full E2E report\n")
w(f"- **package:** `@swmansion/argent` v{version}")
w(f"- **os:** {osname}")
w(f"- **driver:** `{binname}`")
w(f"- **totals:** ✅ {total['pass']} pass · ❌ {total['fail']} fail · ∼ {total['skip']} skip"
f" ({sum(total.values())} cases)\n")
# Per-phase summary
w("## Per-phase\n")
w("| phase | pass | fail | skip |")
w("|---|---:|---:|---:|")
order = ["install", "introspection", "validation", "android", "chromium", "rn", "cleanup"]
seen = [p for p in order if p in per_phase] + [p for p in per_phase if p not in order]
for p in seen:
d = per_phase[p]
w(f"| {p} | {d['pass']} | {d['fail']} | {d['skip']} |")
w("")
# Coverage matrix
w("## Tool coverage\n")
w(f"{len(tools)} tools observed. `validated` = argument-schema rejection tests passed; "
"`happy-path` = a real device/app run (or why it was skipped).\n")
w("| tool | validated | happy-path |")
w("|---|:---:|:---:|")
def mark(v):
return {True: "✅", False: "—", None: "·", "pass": "✅", "fail": "❌", "skip": "∼"}.get(v, "?")
for tool in sorted(tools):
t = tools[tool]
w(f"| `{tool}` | {mark(t['validated'])} | {mark(t['happy'])} |")
w("")
# Failures
w("## Failures\n")
if not fails:
w("None. 🎉\n")
else:
for f in fails:
w(f"- **[{f.get('phase')}] `{f.get('tool')}` — {f.get('case')}**: "
f"{f.get('detail','').strip()}")
w("")
print("\n".join(out))
return 0
if __name__ == "__main__":
sys.exit(main())