Skip to content
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
8935456
fix(mobile): refresh GitHub repositories after connect
iscekic Jul 31, 2026
9ddbaf9
feat(mobile): add step-based new sessions
iscekic Jul 31, 2026
7141af1
test(mobile): cover new session step contracts
iscekic Jul 31, 2026
2d1ef6a
fix(mobile): recover new session flow from paused queries
iscekic Aug 1, 2026
674ff00
fix(mobile): preserve new session navigation
iscekic Aug 1, 2026
f9d1e01
fix(mobile): resolve Homebrew android-commandlinetools adb in record.sh
iscekic Aug 1, 2026
674ec5f
merge: origin/main into new-session-github-16aa
iscekic Aug 1, 2026
28d8e88
chore: retrigger review
iscekic Aug 1, 2026
0001f68
refactor(mobile-e2e): simplified runbook, scripted stub setup, load-t…
iscekic Aug 1, 2026
50e208c
fix(mobile-e2e): address review findings on retry and stub rollback
iscekic Aug 2, 2026
d5778b7
fix(mobile-e2e): close concurrency races found in adversarial review
iscekic Aug 2, 2026
50be1ed
fix(mobile-e2e): adversarial-review round 2
iscekic Aug 2, 2026
69e21e7
fix(mobile-e2e): seed verifies the endpoint's success flag
iscekic Aug 2, 2026
7146391
fix(mobile-e2e): server stop verifies death before dropping state
iscekic Aug 2, 2026
5c6f274
fix(mobile-e2e): address stub-claim, re-seed, and lsof review findings
iscekic Aug 2, 2026
e26d0d0
fix(mobile-e2e): tighten adoption, signal exits, and legacy-seed hand…
iscekic Aug 2, 2026
48f61f5
fix(mobile-e2e): name both sources of an existing token row
iscekic Aug 2, 2026
e5a0494
fix(mobile-e2e): stub row verification, self-cleaning start, lsof fal…
iscekic Aug 2, 2026
c7835ac
fix(mobile-e2e): honest row probe, race-safe rollback, blind-lsof reuse
iscekic Aug 2, 2026
449c242
fix(mobile-e2e): close the last rollback and adoption windows
iscekic Aug 2, 2026
fec3e1f
fix(mobile-e2e): serialize the stub per worktree instead of guarding …
iscekic Aug 2, 2026
973fdf5
fix(mobile-e2e): row-probe failures skip the missing-user hint
iscekic Aug 2, 2026
4f759f0
fix(mobile-e2e): bound the lock's hold time and the survivor edge
iscekic Aug 2, 2026
616c33b
feat(mobile-e2e): copy a real GitHub integration onto an e2e account
iscekic Aug 2, 2026
1f1eaed
fix(mobile-e2e): copies never donate or collide with the stub
iscekic Aug 2, 2026
1a34adb
docs(mobile-e2e): STE pass over the runbook additions
iscekic Aug 2, 2026
ffd814c
fix(mobile-e2e): copies validate the donor live and can never refresh
iscekic Aug 2, 2026
4ed26ca
fix(mobile-e2e): guard the integration copy's blast radius
iscekic Aug 2, 2026
c398e75
fix(mobile-e2e): env failures in the copy tool name their own fix
iscekic Aug 2, 2026
66b7904
fix(mobile-e2e): make the copy's guards atomic and expiry-safe
iscekic Aug 2, 2026
36f3437
fix(mobile-e2e): state the copy's real lifetime and align the lock bu…
iscekic Aug 2, 2026
f752367
fix(mobile-e2e): re-assert the usable window after the live probes
iscekic Aug 2, 2026
df4aac2
docs(mobile-e2e): bound big outputs, never doctor --json
iscekic Aug 2, 2026
a064887
Merge remote-tracking branch 'origin/mobile-e2e-ste' into new-session…
iscekic Aug 2, 2026
19918ef
refactor(mobile): unify new-session configure form
iscekic Aug 2, 2026
eb74072
refactor(mobile): remove superseded cloud form
iscekic Aug 2, 2026
43f7f52
fix(mobile): harden new-session E2E support
iscekic Aug 2, 2026
d776565
chore: format mobile workflow test
iscekic Aug 2, 2026
450a73a
fix(e2e): preserve cleanup ownership state
iscekic Aug 2, 2026
62cfbd2
fix(e2e): retain cleanup state on conflicts
iscekic Aug 2, 2026
54665c5
fix(e2e): keep appium port bump recovery
iscekic Aug 2, 2026
3946b7b
chore: retrigger review
iscekic Aug 2, 2026
16f56a0
fix(e2e): keep stale appium recovery nonfatal
iscekic Aug 2, 2026
6bb5fa3
fix(e2e): preserve stale appium state
iscekic Aug 2, 2026
231405d
fix(e2e): recover from foreign appium listeners
iscekic Aug 2, 2026
deaafb6
fix(e2e): retain appium conflict diagnostics
iscekic Aug 2, 2026
96200a3
docs(mobile-e2e): doctor is always a full JSON dump; never tail JSON
iscekic Aug 2, 2026
6a1540b
chore(e2e): revert apps/mobile/e2e to base
iscekic Aug 2, 2026
0381b0d
fix(dev): drop test assertions for the reverted e2e harness
iscekic Aug 2, 2026
afd64f0
chore(dev): revert shared e2e harness files
iscekic Aug 2, 2026
396ef83
Merge remote-tracking branch 'origin/mobile-e2e-ste' into new-session…
iscekic Aug 2, 2026
975ff19
chore(mobile-e2e): restore the #4960 harness under the new base
iscekic Aug 2, 2026
16a73af
refactor(mobile): one new-session screen for every entry point
iscekic Aug 2, 2026
88a921c
fix(mobile): name new-session attachment handlers
iscekic Aug 2, 2026
53422e0
chore: retrigger review
iscekic Aug 2, 2026
261e954
chore: retrigger review
iscekic Aug 2, 2026
dc934f0
feat(mobile): add share delivery helpers
iscekic Aug 2, 2026
88d35ff
feat(mobile): carry drafts through remote spawn
iscekic Aug 2, 2026
eeb88f0
feat(mobile): auto-send delivered remote shares
iscekic Aug 2, 2026
a4af3a1
style(mobile): format spawn dispatch test
iscekic Aug 2, 2026
4bee91b
fix(mobile): guard remote share auto-send
iscekic Aug 2, 2026
f5c0451
fix(mobile): require CLI file capability
iscekic Aug 2, 2026
c45f114
Merge main and drop the moved mobile E2E harness
iscekic Aug 3, 2026
39478b2
Drop the github-integration-copy seed tool
iscekic Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
461 changes: 131 additions & 330 deletions apps/mobile/e2e/AGENTS.md

Large diffs are not rendered by default.

160 changes: 144 additions & 16 deletions apps/mobile/e2e/appium.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
# lock around a multi-command helper.
#
# appium.sh <device> test [-e KEY=VALUE]... <flow.js> [more-flows.js]
# appium.sh <device> hierarchy
# appium.sh <device> hierarchy [out.xml] # writes the XML to a file and
# # prints its path; never stdout
# appium.sh <device> server start|stop|status
# appium.sh <device> --exec <command...>
#
Expand All @@ -30,6 +31,10 @@ if [ -z "${ANDROID_HOME:-}" ] && [ -z "${ANDROID_SDK_ROOT:-}" ]; then
fi
APPIUM_BIN="$REPO_ROOT/node_modules/.bin/appium"
LOCK="${TMPDIR:-/tmp}/kilo-appium-locks/$DEVICE"
# Port-ownership checks need lsof; without it, fall back to pid+status
# adoption instead of killing healthy servers for 50 port blocks.
LSOF_OK=1
command -v lsof >/dev/null 2>&1 || LSOF_OK=0

if [ "${KILO_APPIUM_LOCKED:-}" != "1" ]; then
exec "$REPO_ROOT/node_modules/.bin/tsx" "$REPO_ROOT/dev/local/process-lock.ts" \
Expand All @@ -51,9 +56,12 @@ ensure_drivers() {
installed="$("$APPIUM_BIN" driver list --installed 2>&1 || true)"
if ! grep -qw "$want" <<<"$installed"; then
# Machine-global install: serialize so parallel first runs cannot race it.
# Re-check INSIDE the lock — the loser of the race would otherwise run a
# second install and fail on "already installed".
"$REPO_ROOT/node_modules/.bin/tsx" "$REPO_ROOT/dev/local/process-lock.ts" \
--wait 1800 "${TMPDIR:-/tmp}/kilo-appium-locks/driver-install" -- \
"$APPIUM_BIN" driver install "$want"
bash -c '"$1" driver list --installed 2>&1 | grep -qw "$2" || exec "$1" driver install "$2"' \
_ "$APPIUM_BIN" "$want"
fi
}

Expand All @@ -72,35 +80,100 @@ ensure_server() {
# adopting it would interleave taps across devices.
if [ -f "$STATE_DIR/server.port" ] && [ -f "$STATE_DIR/appium.pid" ]; then
APPIUM_PORT=$(cat "$STATE_DIR/server.port")
if kill -0 "$(cat "$STATE_DIR/appium.pid")" 2>/dev/null; then
if server_status; then return 0; fi
stop_server
RECORDED_PID=$(cat "$STATE_DIR/appium.pid")
if kill -0 "$RECORDED_PID" 2>/dev/null; then
if server_status; then
# Adopt only when the recorded pid actually owns the listener — a
# recycled pid plus a sibling's server on this port answers /status
# while belonging to another device. Without lsof, or with an lsof
# that stays blind while our pid lives and /status answers, adopt on
# pid+status (same evidence rule as the start loop) — dropping the
# state here would orphan a healthy server on every invocation.
# Every probe rechecks liveness and /status, so a server dying
# mid-probe is never adopted. lsof exits 1 on no match; without
# || true, pipefail + set -e would kill the script here.
if [ "$LSOF_OK" -eq 0 ]; then return 0; fi
ADOPT=0 FOREIGN=0 BLIND=0
while kill -0 "$RECORDED_PID" 2>/dev/null && server_status; do
LISTENER=$(lsof -ti "tcp:$APPIUM_PORT" -sTCP:LISTEN 2>/dev/null | head -1 || true)
if [ -n "$LISTENER" ] && [ "$LISTENER" = "$RECORDED_PID" ]; then
ADOPT=1; break
fi
[ -z "$LISTENER" ] || { FOREIGN=1; break; }
BLIND=$((BLIND + 1))
if [ "$BLIND" -ge 3 ]; then
echo "appium.sh: lsof cannot attribute port $APPIUM_PORT while our recorded pid is alive and /status answers; adopting on pid+status" >&2
ADOPT=1; break
fi
sleep 1
done
[ "$ADOPT" -eq 0 ] || return 0
if [ "$FOREIGN" -eq 1 ]; then
# Not ours to kill: a recycled pid may be another device's appium.
rm -f "$STATE_DIR/appium.pid" "$STATE_DIR/server.port"
else
# Died or stopped answering mid-probe: clean up our own remains.
stop_server || true
if [ -f "$STATE_DIR/appium.pid" ]; then
echo "appium.sh: cannot reap pid $(cat "$STATE_DIR/appium.pid") — conflict state preserved; refusing to overwrite. Manage it manually or wait for it to exit, then remove $(printf '%q\n' "$STATE_DIR")/appium.pid." >&2
return 1
Comment thread
iscekic marked this conversation as resolved.
Outdated
fi
fi
else
stop_server || true
Comment thread
iscekic marked this conversation as resolved.
Outdated
if [ -f "$STATE_DIR/appium.pid" ]; then
echo "appium.sh: cannot reap pid $(cat "$STATE_DIR/appium.pid") — conflict state preserved; refusing to overwrite. Manage it manually or wait for it to exit, then remove $(printf '%q\n' "$STATE_DIR")/appium.pid." >&2
return 1
fi
fi
fi
fi
ensure_drivers
local base_port=$APPIUM_PORT attempt
local base_port=$APPIUM_PORT attempt BLIND
# Hash collisions and foreign listeners both resolve by bumping one block.
for attempt in $(seq 0 49); do
APPIUM_PORT=$((base_port + attempt * 10))
BLIND=0
port_free "$APPIUM_PORT" || continue
if [ "$attempt" -gt 0 ]; then
echo "appium.sh: bumping to port block $APPIUM_PORT" >&2
fi
echo "appium.sh: starting appium server for $DEVICE on port $APPIUM_PORT" >&2
echo "$APPIUM_PORT" >"$STATE_DIR/server.port"
nohup "$APPIUM_BIN" --port "$APPIUM_PORT" --log-level warn \
>"$STATE_DIR/appium.log" 2>&1 &
echo $! >"$STATE_DIR/appium.pid"
for _ in $(seq 1 60); do
# Liveness first, then /status, then port OWNERSHIP: a sibling device's
# server on this port also answers /status, and adopting it would let
# one device's cleanup kill the other's server mid-flow.
kill -0 "$(cat "$STATE_DIR/appium.pid")" 2>/dev/null || break
if server_status; then
echo "$APPIUM_PORT" >"$STATE_DIR/server.port"
return 0
fi
if ! kill -0 "$(cat "$STATE_DIR/appium.pid")" 2>/dev/null; then
break
# Adopt only on proven ownership: a foreign pid means bump. An empty
# lsof (listener vanished or raced) means keep looping — but an lsof
# that stays blind while our pid lives and /status answers means the
# environment cannot attribute sockets; adopt on pid+status rather
# than killing a healthy server for 50 port blocks.
if [ "$LSOF_OK" -eq 0 ]; then
return 0
fi
LISTENER=$(lsof -ti "tcp:$APPIUM_PORT" -sTCP:LISTEN 2>/dev/null | head -1 || true)
if [ "$LISTENER" = "$(cat "$STATE_DIR/appium.pid")" ] && [ -n "$LISTENER" ]; then
return 0
fi
if [ -n "$LISTENER" ]; then
echo "appium.sh: port $APPIUM_PORT is owned by pid $LISTENER, not ours; bumping" >&2
break
fi
BLIND=$((BLIND + 1))
if [ "$BLIND" -ge 3 ]; then
echo "appium.sh: lsof cannot attribute port $APPIUM_PORT while our pid is alive and /status answers; adopting on pid+status" >&2
return 0
fi
fi
sleep 1
done
stop_server
APPIUM_CLEANUP_OWNED=1 stop_server
echo "appium.sh: server attempt on port $APPIUM_PORT failed; last log lines:" >&2
tail -n 10 "$STATE_DIR/appium.log" >&2
done
Expand All @@ -111,9 +184,58 @@ ensure_server() {
stop_server() {
if [ -f "$STATE_DIR/appium.pid" ]; then
PID=$(cat "$STATE_DIR/appium.pid")
# Pids get recycled; only signal a process that is actually our appium.
if kill -0 "$PID" 2>/dev/null && ps -o command= -p "$PID" 2>/dev/null | grep -q appium; then
kill "$PID" || true
STOP_PORT=""
if [ -f "$STATE_DIR/server.port" ]; then
STOP_PORT=$(cat "$STATE_DIR/server.port")
fi
# Pids get recycled; only signal a process that is actually our Appium
# server for the recorded port. "--port" in the process command is the
# primary evidence — no longer require the literal binary path, because
# a pnpm shim (node …/node_modules/.bin/appium) or a shebang wrapper
# may not contain $APPIUM_BIN verbatim. When lsof can attribute the
# listener, the listener PID must also match our recorded PID.
if kill -0 "$PID" 2>/dev/null; then
PROCESS_CMD=$(ps -ww -o command= -p "$PID" 2>/dev/null || true)
if [ -n "$STOP_PORT" ] && echo "$PROCESS_CMD" | grep -qF -- "--port $STOP_PORT"; then
SHOULD_KILL=1
if [ "$LSOF_OK" -eq 1 ]; then
LISTENER=$(lsof -ti "tcp:$STOP_PORT" -sTCP:LISTEN 2>/dev/null | head -1 || true)
if [ -n "$LISTENER" ] && [ "$LISTENER" != "$PID" ]; then
if [ "${APPIUM_CLEANUP_OWNED:-0}" = "1" ]; then
# Start-loop cleanup: PID is freshly spawned by us. Kill it
# (alive but not the listener — it failed to bind) and clean
# up state so the loop continues to the next port block.
echo "appium.sh: port $STOP_PORT is owned by pid $LISTENER, not our just-spawned pid $PID; cleaning up" >&2
SHOULD_KILL=1
else
echo "appium.sh: port $STOP_PORT is owned by pid $LISTENER, not our recorded pid $PID; skipping kill" >&2
SHOULD_KILL=0
fi
fi
fi
if [ "$SHOULD_KILL" -eq 1 ]; then
kill "$PID" 2>/dev/null || true
# Dropping the state while the process lives would leave an untracked
# listener squatting on the port; escalate before forgetting the pid,
# and keep the state (fail) if even SIGKILL does not take.
for _ in $(seq 1 10); do
kill -0 "$PID" 2>/dev/null || break
sleep 1
done
kill -0 "$PID" 2>/dev/null && kill -9 "$PID" 2>/dev/null || true
for _ in 1 2 3; do
kill -0 "$PID" 2>/dev/null || break
sleep 1
done
if kill -0 "$PID" 2>/dev/null; then
echo "appium.sh: pid $PID survived SIGKILL; keeping server state" >&2
return 1
fi
else
echo "appium.sh: port $STOP_PORT has a foreign listener; keeping server state" >&2
return 1
Comment thread
iscekic marked this conversation as resolved.
Outdated
fi
fi
fi
fi
rm -f "$STATE_DIR/appium.pid" "$STATE_DIR/server.port"
Expand Down Expand Up @@ -154,8 +276,14 @@ case "$cmd" in
node "$SCRIPT_DIR/wdio/run-flow.js" "${FLOWS[@]}"
;;
hierarchy)
# Always a file, never stdout: a raw XML dump into an agent session is
# large enough to kill the session silently. Grep the file for selectors.
# Xs must end the template: BSD mktemp leaves embedded Xs literal, so a
# suffixed template gives one fixed path that fails on the second use.
OUT="${2:-$(mktemp "${TMPDIR:-/tmp}/kilo-hierarchy.XXXXXX")}"
ensure_server
env DEVICE="$DEVICE" APPIUM_PORT="$APPIUM_PORT" node "$SCRIPT_DIR/wdio/hierarchy.js"
env DEVICE="$DEVICE" APPIUM_PORT="$APPIUM_PORT" node "$SCRIPT_DIR/wdio/hierarchy.js" > "$OUT"
echo "hierarchy: $OUT ($(grep -c '<' "$OUT") elements)"
;;
*)
echo "usage: appium.sh <device> test|hierarchy|server|--exec ..." >&2
Expand Down
24 changes: 14 additions & 10 deletions apps/mobile/e2e/flows/open-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,23 @@ module.exports = async function openApp(ctx) {
await h.launchApp(BUNDLE_ID);

// Cold launch and bundling are slow; wait for any known state before
// settling. Android under load gets the long budget; it returns as soon as
// a state renders, so healthy runs never pay it.
const launchTimeout = ctx.platform === 'android' ? 420000 : 30000;
for (let anr = 0; ; anr++) {
// settling. Both platforms get a long budget under parallel-workflow host
// load; the wait returns as soon as a state renders, so healthy runs never
// pay it. The wait runs in short slices with an ANR check between them —
// an ANR dialog over a blank splash would otherwise sit unanswered for the
// whole budget before the first check.
const launchBudget = ctx.platform === 'android' ? 420000 : 120000;
const slice = 30000;
const deadline = Date.now() + launchBudget;
for (;;) {
try {
await h.waitVisible(S.ANY_STATE, { timeout: launchTimeout });
await h.waitVisible(S.ANY_STATE, {
timeout: Math.min(slice, Math.max(deadline - Date.now(), 1)),
});
break;
} catch (err) {
if (anr < 3 && (await ctx.h.visible(ANR_DIALOG))) {
await h.tapOn('Wait');
continue;
}
throw err;
if (Date.now() >= deadline) throw err;
if (await ctx.h.visible(ANR_DIALOG)) await h.tapOn('Wait');
}
}
await when(ctx, ANR_DIALOG, () => h.tapOn('Wait'));
Expand Down
8 changes: 4 additions & 4 deletions apps/mobile/e2e/flows/settle-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ module.exports = async function settleApp(ctx) {
const { h, when } = ctx;

// Something known must render before we settle. A preflight reconnect
// refetches the JS bundle on Android (a minute or more under load); the
// long budget returns as soon as anything renders, so healthy runs and
// iOS never pay it.
await h.waitVisible(S.ANY_STATE, { timeout: ctx.platform === 'android' ? 300000 : 15000 });
// refetches the JS bundle (a minute or more under parallel-workflow host
// load); the long budget returns as soon as anything renders, so healthy
// runs never pay it.
await h.waitVisible(S.ANY_STATE, { timeout: ctx.platform === 'android' ? 300000 : 120000 });

// A deep-link reconnect may raise the external-app confirmation; one
// bounded optional look, then move on (the only optional wait here).
Expand Down
Loading
Loading