fix(ci): make activation-e2e harness actually run its flows (#91)

Port the harness fixes from test/e2e-new-features to main so the
Activation E2E workflow stops failing before any flow executes:

- Run everything through scripts/run-e2e-flows.sh as a single script
  invocation: android-emulator-runner executes each 'script:' line in
  its own shell, so the previous 'cd artifacts/screenshots' never
  persisted and maestro failed with 'Flow path does not exist' on
  every run (19/19 red since the workflow landed).
- adb reverse + 127.0.0.1 instead of 10.0.2.2 (unreliable headless),
  emulator->mock reachability probe, logcat + maestro debug capture.
- Trim the flow list to the two flows that exist on main; the three
  newer flows land with the test/e2e-new-features PR.
- continue-on-error until the suite's first green: the positive flow
  still fails its final reply assertion (mode B in #90), and a
  never-green suite should not block unrelated PRs or pollute the
  product-intelligence failure metrics (#89).

Refs #90. Refs #89.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Den
2026-07-16 23:31:01 -07:00
committed by GitHub
parent 0bedad366b
commit 6a9bb6d2d5
4 changed files with 100 additions and 22 deletions

View File

@@ -37,6 +37,10 @@ on:
jobs:
activation-e2e:
runs-on: ubuntu-latest
# Non-blocking until the suite has its first green run: the positive flow
# still fails its final reply assertion (mode B in issue #90). Remove once
# #90 is closed so regressions block PRs again.
continue-on-error: true
# Same npm install + expo prebuild + assembleRelease + emulator pipeline as
# cua-smoke.yml, which budgets 60 min (emulator-boot-timeout alone is 10 min).
# Typical runs finish well under this; the ceiling just avoids flaky kills
@@ -106,10 +110,16 @@ jobs:
set -x
mkdir -p artifacts/screenshots
# Normal mode on 4096 (positive flow) and --fail-auth on 4097 (negative flow).
# Both bind 0.0.0.0 so the emulator can reach them via 10.0.2.2.
# 4098 is normal mode + --seed-sessions (two pre-populated sessions in two
# different directories, for all-sessions.yaml). 4099 is a fresh normal-mode
# instance shared by directory-picker.yaml and variant-picker.yaml (its fake
# file tree / provider variants / project list don't affect each other).
# All bind 0.0.0.0 so the emulator can reach them via 10.0.2.2.
nohup node tests/fixtures/mock-opencode-server.ts --port 4096 > /tmp/mock-4096.log 2>&1 &
nohup node tests/fixtures/mock-opencode-server.ts --port 4097 --fail-auth > /tmp/mock-4097.log 2>&1 &
for port in 4096 4097; do
nohup node tests/fixtures/mock-opencode-server.ts --port 4098 --seed-sessions > /tmp/mock-4098.log 2>&1 &
nohup node tests/fixtures/mock-opencode-server.ts --port 4099 > /tmp/mock-4099.log 2>&1 &
for port in 4096 4097 4098 4099; do
for i in $(seq 1 30); do
if curl -sf --connect-timeout 1 -m 3 "http://127.0.0.1:${port}/global/health" > /dev/null 2>&1; then
echo "mock server on ${port} responded (may be 401, that's expected on 4097)"
@@ -138,19 +148,19 @@ jobs:
disable-animations: true
emulator-boot-timeout: 600
emulator-options: -no-window -no-audio -no-boot-anim -gpu swiftshader_indirect -no-snapshot
script: |
adb install android/app/build/outputs/apk/release/app-release.apk
cd artifacts/screenshots
echo "--- positive activation flow (connect -> send -> reply) ---"
maestro test ../../.maestro/flows/activation-positive.yaml
echo "--- negative activation flow (connect-time 401 must show a visible error) ---"
maestro test ../../.maestro/flows/activation-negative-401.yaml
# One script file, not one `sh -c` per line — run-e2e-flows.sh does the
# adb reverse port-forwarding, runs every flow, and captures logcat on
# exit. (Running maestro line-by-line here broke `cd` persistence and
# left no diagnostics.)
script: bash scripts/run-e2e-flows.sh
- name: Mock server logs
if: always()
run: |
echo "--- 4096 (normal) ---"; cat /tmp/mock-4096.log || true
echo "--- 4097 (fail-auth) ---"; cat /tmp/mock-4097.log || true
echo "--- 4098 (seed-sessions) ---"; cat /tmp/mock-4098.log || true
echo "--- 4099 (normal, directory-picker + variant-picker) ---"; cat /tmp/mock-4099.log || true
- name: Upload screenshots
if: always()
@@ -159,3 +169,12 @@ jobs:
name: activation-e2e-screenshots-${{ github.run_number }}
path: artifacts/screenshots/*.png
if-no-files-found: warn
- name: Upload maestro debug + logcat
if: always()
uses: actions/upload-artifact@v7
with:
name: activation-e2e-debug-${{ github.run_number }}
path: |
artifacts/diag/**
if-no-files-found: warn

View File

@@ -39,20 +39,19 @@ name: Activation - negative path (connect-time 401 must surface a visible error)
- tapOn:
id: "connect-ip-input"
- inputText: "10.0.2.2"
- tapOn:
id: "connect-port-input"
- eraseText
- inputText: "4097"
- inputText: "127.0.0.1:4097"
- hideKeyboard
- takeScreenshot: negative-S3_401_server_url_entered
- tapOn:
id: "connect-submit-button"
# Matches the 40s margin used for the positive flow's connection-status-dot
# wait (src/lib/sdk.ts REQUEST_TIMEOUT_MS = 30_000) for consistency, even
# though --fail-auth responds with 401 immediately in practice.
- extendedWaitUntil:
visible:
text: "Connection Failed"
timeout: 20000
timeout: 40000
- assertVisible:
text: "Connection Failed"
- assertVisible:

View File

@@ -8,7 +8,7 @@ name: Activation - positive path (consent -> connect -> send -> reply)
#
# The CI job starts `node tests/fixtures/mock-opencode-server.ts --port 4096`
# on the runner host BEFORE this flow runs. The Android emulator reaches the
# runner host via the standard emulator alias 10.0.2.2.
# runner host via the standard emulator alias 127.0.0.1.
- launchApp:
clearState: true
@@ -29,20 +29,24 @@ name: Activation - positive path (consent -> connect -> send -> reply)
- takeScreenshot: positive-S3_add_connection_form
- tapOn:
id: "connect-ip-input"
- inputText: "10.0.2.2"
- tapOn:
id: "connect-port-input"
- eraseText
- inputText: "4096"
- inputText: "127.0.0.1:4096"
- hideKeyboard
- takeScreenshot: positive-S4_server_url_entered
- tapOn:
id: "connect-submit-button"
# Quick Connect fires up to 3 sequential/parallel network calls before the
# dot renders (testConnection's health(), then addConnection's parallel
# project.current() + path.get()), and each individual fetch is capped by
# src/lib/sdk.ts REQUEST_TIMEOUT_MS = 30_000. A 20s wait here raced that
# 30s client-side timeout and flaked in CI (first real run, #82) even
# though the mock server was up and reachable. 40s gives the app's own
# timeout room to complete; this still asserts the real thing, just with a
# wait that isn't shorter than the code path it's waiting on.
- extendedWaitUntil:
visible:
id: "connection-status-dot"
timeout: 20000
timeout: 40000
- assertVisible:
id: "connection-status-dot"
- takeScreenshot: positive-S5_connected

56
scripts/run-e2e-flows.sh Executable file
View File

@@ -0,0 +1,56 @@
#!/usr/bin/env bash
#
# Runs the Maestro activation E2E flows against the mock opencode servers the
# workflow started on the host (ports 4096-4099). Invoked as a single line from
# activation-e2e.yml's emulator-runner `script:` so cd/trap/loop actually work.
#
# Networking: `adb reverse` maps each mock port so the emulator's own
# localhost:PORT forwards to the host's localhost:PORT; flows connect to
# 127.0.0.1:<port>. (10.0.2.2 was unreliable headless.)
set -uo pipefail
ROOT="$(pwd)" # capture BEFORE any cd, so diag paths are absolute
APK="android/app/build/outputs/apk/release/app-release.apk"
# Only the flows that exist on main. directory-picker/all-sessions/variant-picker
# land with the test/e2e-new-features PR.
FLOWS=(activation-positive activation-negative-401)
mkdir -p "$ROOT/artifacts/screenshots" "$ROOT/artifacts/diag"
echo "== installing APK =="
adb install "$APK"
echo "== forwarding mock ports into the emulator (adb reverse) =="
for p in 4096 4097 4098 4099; do adb reverse "tcp:$p" "tcp:$p"; done
adb reverse --list
# Decisive probe: can the EMULATOR actually reach the host mock via 127.0.0.1?
# If either method prints the health JSON, the network path is good and any flow
# failure is app-side; if both fail/hang, it's the transport (adb reverse). Both
# are best-effort — API 28's toybox may or may not ship wget, so nc is a fallback.
echo "== emulator -> mock reachability probe (127.0.0.1:4096/global/health) ==" | tee "$ROOT/artifacts/diag/probe.txt"
echo "[wget]" | tee -a "$ROOT/artifacts/diag/probe.txt"
timeout 15 adb shell 'toybox wget -qO - http://127.0.0.1:4096/global/health' 2>&1 | tee -a "$ROOT/artifacts/diag/probe.txt" || true
echo "[nc]" | tee -a "$ROOT/artifacts/diag/probe.txt"
timeout 15 adb shell 'printf "GET /global/health HTTP/1.0\r\n\r\n" | toybox nc 127.0.0.1 4096' 2>&1 | tee -a "$ROOT/artifacts/diag/probe.txt" || true
adb logcat -c || true # clear, so the captured log is just this run
dump_diag() {
echo "== capturing diagnostics =="
adb logcat -d > "$ROOT/artifacts/diag/logcat.txt" 2>&1 || true
# Maestro writes per-run debug (UI hierarchy + commands) under ~/.maestro/tests
cp -r "$HOME/.maestro/tests" "$ROOT/artifacts/diag/maestro-tests" 2>/dev/null || true
}
trap dump_diag EXIT
cd "$ROOT/artifacts/screenshots"
rc=0
for f in "${FLOWS[@]}"; do
echo "--- flow: $f ---"
if ! maestro test --debug-output "$ROOT/artifacts/diag/maestro-$f" "$ROOT/.maestro/flows/$f.yaml"; then
echo "::error::Maestro flow failed: $f"
rc=1
break
fi
done
exit $rc