mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
cd4190b80e
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 ```
245 lines
10 KiB
Bash
245 lines
10 KiB
Bash
#!/usr/bin/env bash
|
||
# Shared library for the full Argent E2E harness.
|
||
#
|
||
# Sourced by run-e2e.sh and every phase module. Provides:
|
||
# - logging (log/warn/err/group)
|
||
# - the argent CLI wrapper (argent_cli) and tool driver (run_tool)
|
||
# - result recording to a JSONL log (pass/fail/skip) + counters
|
||
# - assertion helpers built on the proven contract:
|
||
# `argent run <tool>` exits 0 on success (JSON on stdout),
|
||
# non-zero on validation error / unknown tool / service failure.
|
||
# - a private tool-server lifecycle (own port + own HOME so it never
|
||
# collides with a foreign server on a shared machine)
|
||
#
|
||
# Every phase runs with a sandbox HOME ($E2E_HOME) so ~/.argent, MCP configs,
|
||
# and the npm global prefix land in a throwaway dir, never the real machine.
|
||
#
|
||
# Requires: bash 4+, jq, python3, node, npm. (adb / xvfb / expo are checked
|
||
# per-tier and gate with a recorded skip when absent.)
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Strictness. Phases opt into this by sourcing us; we do NOT set -e globally
|
||
# because assertions must keep running after an individual tool call fails.
|
||
# ---------------------------------------------------------------------------
|
||
set -uo pipefail
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Colors (respect NO_COLOR / non-tty)
|
||
# ---------------------------------------------------------------------------
|
||
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
|
||
C_RED=$'\033[31m'; C_GRN=$'\033[32m'; C_YEL=$'\033[33m'
|
||
C_BLU=$'\033[34m'; C_DIM=$'\033[2m'; C_RST=$'\033[0m'
|
||
else
|
||
C_RED=""; C_GRN=""; C_YEL=""; C_BLU=""; C_DIM=""; C_RST=""
|
||
fi
|
||
|
||
log() { printf '%s\n' "${C_BLU}▸${C_RST} $*" >&2; }
|
||
info() { printf '%s\n' "${C_DIM} $*${C_RST}" >&2; }
|
||
warn() { printf '%s\n' "${C_YEL}! $*${C_RST}" >&2; }
|
||
err() { printf '%s\n' "${C_RED}✗ $*${C_RST}" >&2; }
|
||
group() { printf '\n%s\n' "${C_BLU}==== $* ====${C_RST}" >&2; }
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Configuration derived from the environment exported by run-e2e.sh.
|
||
# ---------------------------------------------------------------------------
|
||
: "${E2E_JSONL:?E2E_JSONL must be set by run-e2e.sh}"
|
||
# ARGENT_BIN (e.g. "/sandbox/bin/argent" or "node /path/dist/cli.js") is set by
|
||
# run-e2e.sh AFTER the install decision — resolved lazily at call time below.
|
||
TOOL_TIMEOUT="${TOOL_TIMEOUT:-120}" # seconds per tool call
|
||
E2E_OS="${E2E_OS:-$(uname -s | tr '[:upper:]' '[:lower:]')}"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# argent_cli — invoke the argent CLI with a timeout. Captures combined output
|
||
# in CLI_OUT and the exit code as the return value. Never aborts the script.
|
||
# The command is rebuilt from ARGENT_BIN each call so it tracks the value
|
||
# run-e2e.sh sets after installing.
|
||
# ---------------------------------------------------------------------------
|
||
argent_cli() {
|
||
local out rc cmd
|
||
read -ra cmd <<< "${ARGENT_BIN:?ARGENT_BIN not set yet}"
|
||
out=$(timeout "$TOOL_TIMEOUT" "${cmd[@]}" "$@" 2>&1); rc=$?
|
||
CLI_OUT="$out"
|
||
return "$rc"
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# run_tool <tool> [json-args] — drive one tool via `argent run`.
|
||
# Sets: RT_RC (exit code), RT_JSON (stdout only), RT_ERR (stderr), RT_OUT (both).
|
||
# ---------------------------------------------------------------------------
|
||
run_tool() {
|
||
local tool="$1" args="${2:-}"
|
||
local errf cmd; errf=$(mktemp)
|
||
read -ra cmd <<< "${ARGENT_BIN:?ARGENT_BIN not set yet}"
|
||
if [ -n "$args" ]; then
|
||
RT_JSON=$(timeout "$TOOL_TIMEOUT" "${cmd[@]}" run "$tool" --args "$args" 2>"$errf"); RT_RC=$?
|
||
else
|
||
RT_JSON=$(timeout "$TOOL_TIMEOUT" "${cmd[@]}" run "$tool" 2>"$errf"); RT_RC=$?
|
||
fi
|
||
RT_ERR=$(cat "$errf"); rm -f "$errf"
|
||
RT_OUT="${RT_JSON}
|
||
${RT_ERR}"
|
||
return "$RT_RC"
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Result recording. One JSON object per test case appended to $E2E_JSONL.
|
||
# Counters live in files so subshells/phases aggregate correctly.
|
||
# ---------------------------------------------------------------------------
|
||
PASS_N=0; FAIL_N=0; SKIP_N=0
|
||
|
||
_record() { # phase tool case status detail
|
||
jq -nc \
|
||
--arg phase "$1" --arg tool "$2" --arg case "$3" \
|
||
--arg status "$4" --arg detail "$5" \
|
||
'{phase:$phase,tool:$tool,case:$case,status:$status,detail:$detail}' \
|
||
>> "$E2E_JSONL"
|
||
}
|
||
|
||
pass() { # phase tool case [detail]
|
||
local phase="${1:-}" tool="${2:-}" case="${3:-}" detail="${4:-}"
|
||
_record "$phase" "$tool" "$case" "pass" "$detail"
|
||
PASS_N=$((PASS_N + 1))
|
||
printf '%s\n' " ${C_GRN}✓${C_RST} ${C_DIM}$tool${C_RST} $case" >&2
|
||
}
|
||
fail() { # phase tool case detail
|
||
local phase="${1:-}" tool="${2:-}" case="${3:-}" detail="${4:-}"
|
||
_record "$phase" "$tool" "$case" "fail" "$detail"
|
||
FAIL_N=$((FAIL_N + 1))
|
||
printf '%s\n' " ${C_RED}✗${C_RST} $tool ${C_DIM}[$case]${C_RST} $detail" >&2
|
||
}
|
||
skip() { # phase tool case reason
|
||
local phase="${1:-}" tool="${2:-}" case="${3:-}" detail="${4:-}"
|
||
_record "$phase" "$tool" "$case" "skip" "$detail"
|
||
SKIP_N=$((SKIP_N + 1))
|
||
printf '%s\n' " ${C_YEL}∼${C_RST} ${C_DIM}$tool ($case): $detail${C_RST}" >&2
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Assertion helpers (all take: phase tool case ...).
|
||
# ---------------------------------------------------------------------------
|
||
|
||
# The tool call must SUCCEED (rc 0).
|
||
assert_ok() { # phase tool case json-args
|
||
local phase="$1" tool="$2" case="$3" args="${4:-}"
|
||
run_tool "$tool" "$args"
|
||
if [ "$RT_RC" -eq 0 ]; then
|
||
pass "$phase" "$tool" "$case"
|
||
else
|
||
fail "$phase" "$tool" "$case" "$(printf '%s' "$RT_OUT" | tr '\n' ' ' | cut -c1-200)"
|
||
fi
|
||
}
|
||
|
||
# The tool call must SUCCEED and a jq filter over stdout must equal `expected`.
|
||
assert_field() { # phase tool case json-args jq-filter expected
|
||
local phase="$1" tool="$2" case="$3" args="$4" filter="$5" expected="$6"
|
||
run_tool "$tool" "$args"
|
||
if [ "$RT_RC" -ne 0 ]; then
|
||
fail "$phase" "$tool" "$case" "rc=$RT_RC: $(printf '%s' "$RT_OUT" | tr '\n' ' ' | cut -c1-160)"
|
||
return
|
||
fi
|
||
local got; got=$(printf '%s' "$RT_JSON" | jq -r "$filter" 2>/dev/null)
|
||
if [ "$got" = "$expected" ]; then
|
||
pass "$phase" "$tool" "$case" "$filter=$got"
|
||
else
|
||
fail "$phase" "$tool" "$case" "expected $filter=$expected got '$got'"
|
||
fi
|
||
}
|
||
|
||
# The tool call must SUCCEED and a jq boolean filter over stdout must be true.
|
||
assert_true() { # phase tool case json-args jq-filter
|
||
local phase="$1" tool="$2" case="$3" args="$4" filter="$5"
|
||
run_tool "$tool" "$args"
|
||
if [ "$RT_RC" -ne 0 ]; then
|
||
fail "$phase" "$tool" "$case" "rc=$RT_RC: $(printf '%s' "$RT_OUT" | tr '\n' ' ' | cut -c1-160)"
|
||
return
|
||
fi
|
||
local got; got=$(printf '%s' "$RT_JSON" | jq -r "$filter" 2>/dev/null)
|
||
if [ "$got" = "true" ]; then
|
||
pass "$phase" "$tool" "$case" "$filter"
|
||
else
|
||
fail "$phase" "$tool" "$case" "expected $filter to be true, got '$got'"
|
||
fi
|
||
}
|
||
|
||
# The tool call must FAIL (rc != 0). If `path` is given, and stdout is a zod
|
||
# issue array, at least one issue must match that path (proves the specific
|
||
# field was rejected, not just an unrelated error).
|
||
assert_reject() { # phase tool case json-args [zod-path] [zod-code]
|
||
local phase="$1" tool="$2" case="$3" args="$4" zpath="${5:-}" zcode="${6:-}"
|
||
run_tool "$tool" "$args"
|
||
if [ "$RT_RC" -eq 0 ]; then
|
||
fail "$phase" "$tool" "$case" "expected rejection but tool SUCCEEDED"
|
||
return
|
||
fi
|
||
if [ -n "$zpath" ]; then
|
||
# Best-effort structured check: if stdout is a zod issue array, assert a
|
||
# matching issue exists. If output isn't JSON (e.g. plain service error),
|
||
# the non-zero exit alone satisfies the case.
|
||
local hit
|
||
hit=$(printf '%s' "$RT_JSON" | jq -e --arg p "$zpath" --arg c "$zcode" '
|
||
if type=="array" then
|
||
any(.[]; (.path[0] == $p) and ($c=="" or .code==$c))
|
||
else false end' 2>/dev/null)
|
||
if [ "$hit" = "true" ]; then
|
||
pass "$phase" "$tool" "$case" "rejected $zpath${zcode:+/$zcode}"
|
||
else
|
||
# Non-structured failure still counts as a rejection, but note it.
|
||
pass "$phase" "$tool" "$case" "rc=$RT_RC (unstructured)"
|
||
fi
|
||
else
|
||
pass "$phase" "$tool" "$case" "rc=$RT_RC"
|
||
fi
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Private tool-server lifecycle. Uses ARGENT_TOOLS_URL so `argent run` targets
|
||
# OUR server (own port), and a sandbox HOME so its state file is isolated.
|
||
# ---------------------------------------------------------------------------
|
||
# Is a healthy tool-server discoverable for this install (via ~/.argent state)?
|
||
server_running() {
|
||
argent_cli server status --json || return 1
|
||
printf '%s' "$CLI_OUT" | jq -e '.running==true and (.healthy==true or .alive==true)' >/dev/null 2>&1
|
||
}
|
||
|
||
# Ensure a tool-server is up. We do NOT pin a port or ARGENT_TOOLS_URL: the CLI
|
||
# auto-spawns on demand and records the port in the sandbox ~/.argent state, so
|
||
# every `argent run`/`tools`/`status` call discovers the same server. On a
|
||
# shared machine this stays isolated because HOME is the sandbox.
|
||
ensure_server() {
|
||
server_running && return 0
|
||
log "starting sandbox tool-server (detached, no-auth, auto-port)"
|
||
argent_cli server start --detach --no-auth --port 0 >/dev/null 2>&1 || true
|
||
local i
|
||
for i in $(seq 1 30); do
|
||
server_running && return 0
|
||
sleep 1
|
||
done
|
||
warn "tool-server did not become ready"
|
||
return 1
|
||
}
|
||
|
||
# PNG "real pixels" floor, shared with drive-device.sh rationale.
|
||
MIN_SHOT_BYTES="${MIN_SHOT_BYTES:-20000}"
|
||
|
||
# Extract an artifact hostPath from a screenshot-style envelope on stdout.
|
||
artifact_path() { # <jq-path-to-artifact-object, default .image>
|
||
local sel="${1:-.image}"
|
||
printf '%s' "$RT_JSON" | jq -r "${sel}.hostPath // empty" 2>/dev/null
|
||
}
|
||
|
||
# Capture a screenshot straight to a file via `argent run screenshot --out`
|
||
# (the CLI renders screenshot artifacts as a "Saved screenshot: <path>" message
|
||
# rather than JSON, so --out is the deterministic path). Returns 0 and sets
|
||
# SHOT_PATH when the file exists and exceeds the real-pixels floor.
|
||
capture_screenshot() { # udid outfile
|
||
local udid="$1" out="$2" cmd
|
||
read -ra cmd <<< "${ARGENT_BIN:?}"
|
||
rm -f "$out"
|
||
timeout "$TOOL_TIMEOUT" "${cmd[@]}" run screenshot --udid "$udid" --out "$out" >/dev/null 2>&1
|
||
SHOT_RC=$?
|
||
[ -f "$out" ] || return 1
|
||
SHOT_SIZE="$(wc -c <"$out" | tr -d ' ')"
|
||
SHOT_PATH="$out"
|
||
[ "$SHOT_SIZE" -gt "$MIN_SHOT_BYTES" ]
|
||
}
|