Skip to content
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
56aecf7
feat(attachments): raise upload cap to 20 MB
iscekic Jul 31, 2026
325349d
fix(mobile): separate attachment notices in bubbles
iscekic Jul 31, 2026
5a4af15
feat(mobile): receive files from remote tools
iscekic Jul 31, 2026
a1e08c2
style(wrapper): format attachment cap error
iscekic Jul 31, 2026
f40abe9
fix(mobile): satisfy attachment lint contracts
iscekic Jul 31, 2026
4f7cb84
style(mobile): format attachment tests
iscekic Jul 31, 2026
9a5df70
fix(mobile): sanitize MIME cache filenames
iscekic Aug 1, 2026
dff9c21
fix(mobile): preserve safe image cache names
iscekic Aug 1, 2026
3fd44d4
fix(mobile): preserve cache marker suffix
iscekic Aug 1, 2026
66bcb6c
fix(mobile): derive cache marker pattern
iscekic Aug 1, 2026
d148d67
merge: origin/main into attachments-cli-f31a
iscekic Aug 1, 2026
94249e2
fix(mobile): satisfy cache filename lint
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
96200a3
docs(mobile-e2e): doctor is always a full JSON dump; never tail JSON
iscekic Aug 2, 2026
c776b71
Merge remote-tracking branch 'origin/mobile-e2e-ste' into attachments…
iscekic Aug 2, 2026
acd4504
fix(mobile): preserve picker filename in CLI attachments
iscekic Aug 2, 2026
f97b75d
fix(mobile): sanitize remote attachment filenames
iscekic Aug 2, 2026
dbec3e0
fix(mobile): bound remote filenames by UTF-8 bytes
iscekic Aug 2, 2026
1d1966d
refactor(mobile): share UTF-8 filename helpers
iscekic Aug 2, 2026
27f961b
fix(mobile): include shared UTF-8 filename utility
iscekic Aug 2, 2026
a62b0e3
fix(mobile): keep UTF-8 width helper internal
iscekic Aug 2, 2026
38b171e
merge: origin/main into attachments-cli-f31a
iscekic Aug 3, 2026
c2f83da
chore(seed): drop the GitHub integration copy command
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
459 changes: 132 additions & 327 deletions apps/mobile/e2e/AGENTS.md

Large diffs are not rendered by default.

115 changes: 102 additions & 13 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,16 +80,52 @@ 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
fi
else
stop_server
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
Expand All @@ -91,12 +135,35 @@ ensure_server() {
>"$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
echo "$APPIUM_PORT" >"$STATE_DIR/server.port"
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
echo "$APPIUM_PORT" >"$STATE_DIR/server.port"
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
echo "$APPIUM_PORT" >"$STATE_DIR/server.port"
return 0
fi
fi
sleep 1
done
Expand All @@ -113,7 +180,23 @@ stop_server() {
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
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
fi
fi
rm -f "$STATE_DIR/appium.pid" "$STATE_DIR/server.port"
Expand Down Expand Up @@ -154,8 +237,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