Skip to content

refactor(mobile-e2e): simplified runbook, scripted stub setup, load-tolerant flows - #4960

Open
iscekic wants to merge 26 commits into
mainfrom
mobile-e2e-ste
Open

refactor(mobile-e2e): simplified runbook, scripted stub setup, load-tolerant flows#4960
iscekic wants to merge 26 commits into
mainfrom
mobile-e2e-ste

Conversation

@iscekic

@iscekic iscekic commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Refactors the mobile E2E harness so weak agent models can run it without misreading anything, and removes the flakiness found in a full-load test (two parallel dual-platform bundles: 2 dev stacks, 2 iOS simulators, 2 Android emulators).

  • e2e/AGENTS.md rewritten in strict simplified technical English: task-structured, one instruction per sentence, 410 → ~215 lines. Everything mechanical now defers to scripts; the runbook keeps only rules and judgment.
  • New e2e/github-stub.sh start|status|stop: the PR-review GitHub stub setup was a three-step manual ritual (env-file edit, next-auth fake-login curl dance to seed the token row, hand-started tmux server). It is now one command each way, with the env line tagged and removed on stop.
  • emulator-start --wait now applies the launch/GPU retry policy itself (one automatic relaunch: boot-envelope timeout keeps the GPU, every other failure switches to software rendering). The runbook's 40-line decision table is gone; callers just read the error after the second failure.
  • Load-tolerance fixes from the full-load test: iOS cold-launch and settle budgets were 30s/15s (Android already had 420s/300s) and timed out under parallel load; both are now 120s. The waits return as soon as a state renders, so healthy runs pay nothing.
  • Simulator claims record originalDeviceName in the initial claim write, so a claim that dies between rename and the full record write can still restore the device's real name on release (found a simulator carrying a stale Kilo E2E - … label from exactly this window).

Verification

  • Full-load campaign, five rounds: two sections in parallel, each bundle <avd> (stack + iOS simulator + Android emulator — both machine slots full), real e2e-verifier sessions executing launch scenarios on all four devices. Final round with these fixes: both sections VERIFICATION PASSED, teardown clean (no leaked slots, devices, emulators, or stacks) in all five rounds. The campaign is what surfaced the iOS launch budgets, the ANR misjudgment, the hierarchy context-dump, and the claim-flag loss this PR fixes.
  • github-stub.sh exercised end to end against a live worktree stack: start (server up, env line written, token row seeded through fake-login), fixture served over HTTP, status, stop (env line removed, session gone), plus the failure path (missing user) cleaning up after itself.
  • Runbook comprehension: 10 scenario drills against fresh weak-model (kilo-auto/efficient) sessions, 10/10 correct (emulator double-failure handling, selector discipline, stub setup, keyboard dead zones, cleanup ordering, port rules, recording segmentation, foreign-data triage).
  • pnpm typecheck, pnpm lint, pnpm format green. pnpm test: web suite green (8814 passed / 3 skipped); the two dev/local/tmux.test.ts failures reproduce identically on origin/main on this machine (pre-existing, unrelated).

Visual Changes

N/A

Reviewer Notes

  • The emulator retry lives in dev/local/mobile-android.ts (emulator-start handler); stopAndroidEmulator is already safe to call with no record, which is what makes the blind stop-then-retry correct.
  • github-stub.sh refuses to overwrite a GITHUB_API_BASE_URL line it did not write (marker comment).

…olerant flows

- rewrite e2e/AGENTS.md in simplified technical English: task-structured,
  scripts own the mechanics, 410 -> ~215 lines
- new e2e/github-stub.sh: PR-review stub server + env line + token seed
  as one reversible command pair; failed starts clean up after themselves
- emulator-start --wait retries a failed launch once itself (boot-envelope
  timeout keeps the GPU, other failures switch to software rendering);
  the runbook's manual GPU decision table is gone
- appium.sh hierarchy writes to a file and prints the path; a raw XML dump
  into an agent session is large enough to kill it silently
- raise iOS cold-launch/settle budgets to 120s (timed out under parallel
  dual-bundle load); document ANR-under-load handling
- simulator claims record originalDeviceName and bootedByClaim intent in
  the initial write, so a lost post-boot rewrite can no longer leak a
  renamed or still-booted device on release
@iscekic iscekic self-assigned this Aug 1, 2026
Comment thread dev/local/mobile-android.ts Outdated
Comment thread apps/mobile/e2e/github-stub.sh Outdated
Comment thread apps/mobile/e2e/github-stub.sh
@kilo-code-bot

kilo-code-bot Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

The increment (df4aac264..96200a3e0) rewrites the two apps/mobile/e2e/AGENTS.md lines flagged in the previous review; both new jq examples were verified against the actual command output shapes and no new issues were found.

Previous findings resolved in this increment

File Line Previous finding Resolution
apps/mobile/e2e/AGENTS.md 26 tail -c 2000 was prescribed for --json diagnostics, yielding unparseable JSON Bounding rule is now scoped: tail -c for log-like output only; JSON is field-selected with jq
apps/mobile/e2e/AGENTS.md 91 Warned against a non-existent doctor --json flag and implied bare doctor was bounded Now states doctor takes no flags and always dumps the full environment; the example bounds it with jq

Verification performed

  • pnpm -s dev:status --json | jq -r '.services[] | "\(.name) \(.status)"' — checked against cmdStatus in dev/local/cli.ts:898-952. The payload is { session, portOffset, services: [{ name, port, status, group }] }, so both selected fields exist on every element, including the empty-services cases at :906 and :921.
  • pnpm -s dev:mobile:android doctor | jq -r '.sdkRoot, .avds[]' — checked against dev/local/mobile-android.ts:621-629. doctor prints { ...env, avds, worktree }; sdkRoot comes from AndroidEnvironment (:18-25, populated at :129-136) and avds is a string array, so both filters resolve. The "takes no flags" claim matches if (command === 'doctor' || command === undefined) with no args validation, and the PATH-inclusion claim matches path: process.env.PATH at :139-141.
  • jq was already an established dependency of this runbook (AGENTS.md:98), so the new examples add no new tooling assumption.
  • No contradictions remain between line 26 and the other dev:status --json / tail references (lines 22, 31, 57, 67, 70, 215).

Notes and assumptions

  • Static review only; nothing was executed, per read-only mode.
  • Incremental scope: one file, 6 changed lines. Findings on files outside this diff were not re-verified or carried forward; several earlier inline threads on apps/mobile/e2e/github-stub.sh, apps/mobile/e2e/appium.sh, and dev/seed/app/github-integration-copy.ts remain open on GitHub and are unaffected by this increment.
Files Reviewed (1 file)
  • apps/mobile/e2e/AGENTS.md
Previous Review Summaries (6 snapshots, latest commit df4aac2)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit df4aac2)

Status: 2 Issues Found | Recommendation: Address before merge

Executive Summary

The new doctor guidance in apps/mobile/e2e/AGENTS.md warns against a --json flag that does not exist and mislabels bare doctor as bounded, so the runbook now teaches the opposite of what the code does.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/e2e/AGENTS.md 90 doctor ignores all arguments and always prints the full env including PATH, so doctor --json is byte-identical to doctor; the line marks the unbounded command as the safe one

SUGGESTION

File Line Issue
apps/mobile/e2e/AGENTS.md 26 tail -c 2000 applied to --json diagnostics yields unparseable JSON, contradicting line 22 and the stack-start steps that require parsing pnpm dev:status --json

Verified in this increment

  • Line 26 (new bullet): the tail -c 2000 pattern itself is fine for logs and Appium output, and it is consistent with the existing bounded-tail example on lines 29-32. Only the --json scope is problematic.
  • Line 90 (doctor sentence): checked against dev/local/mobile-android.ts. main() destructures const [command, ...args] = process.argv.slice(2); the doctor branch (:621) has no args.length guard and prints JSON.stringify({ ...env, avds, worktree }, null, 2) (:628). AndroidEnvironment (:18-25) includes path, populated from process.env.PATH in environment() (:139-141). No global flag preprocessing exists.
  • The rest of the sentence ("the wrappers resolve the SDK themselves", "prints resolved paths and AVDs") is accurate.

Notes and assumptions

  • Static reading only; nothing was executed, so the actual byte size of doctor output was not measured. The finding does not depend on the size claim, only on --json being a no-op.
  • Incremental scope: f752367e2..df4aac264 touches one file with 2 changed lines. Findings from files outside this diff were not carried forward.
  • No pre-existing inline comments exist on apps/mobile/e2e/AGENTS.md, so neither finding duplicates prior review state.
Files Reviewed (1 file)
  • apps/mobile/e2e/AGENTS.md - 2 issues

Fix these issues in Kilo Cloud

Previous review (commit f752367)

Status: No Issues Found | Recommendation: Merge

Executive Summary

Incremental review of f752367e2 (1 file, 19 changed lines in dev/seed/app/github-integration-copy.ts): the extracted usableUntilMs helper and the new post-probe usable-window re-assertion are behavior-preserving and correct; high confidence.

Verified in this increment

  • usableUntilMs helper (lines 49-54): the 5-minute subtraction matches EXPIRY_BUFFER_MS = 5 * 60 * 1000 in services/git-token-service/src/github-user-authorization-service.ts:48, which gates both the refresh decision (:220) and selectUserAuthorization (:111), so "usable until the refresh buffer opens" remains accurate.
  • New donor re-check (lines 331-336): usableUntilMs(row.access_token_expires_at) - Date.now() < 30 * 60 * 1000 is exactly consistent with the SQL margin access_token_expires_at > now() + interval '35 minutes' (:281) — 35 minutes of expiry margin is 30 usable minutes — so the two guards cannot drift or contradict each other. The continue keeps accessToken/source unassigned, so a fully skipped candidate list still falls through to the BLOCKED throw at :341 rather than copying a stale token.
  • usableUntil return (line 421): new Date(usableUntilMs(source.access_token_expires_at)).toISOString() is semantically identical to the removed inline computation; the value stays a flat string, satisfying the SeedResult contract in dev/seed/AGENTS.md.
  • Column nullability: access_token_expires_at is timestamp({ withTimezone: true, mode: 'string' }).notNull() (packages/db/src/schema.ts:4090), so the helper's string parameter cannot receive null and cannot produce NaN from a missing value.

Notes and assumptions

  • Static reading only; nothing was executed. new Date(<pg timestamptz string>) was not run here — the identical new Date(authorization.access_token_expires_at).getTime() pattern on this same column already ships in github-user-authorization-service.ts:220/483, so it is treated as established.
  • The new re-check runs after the up-to-15s GitHub probe, so with at most 10 candidates it can only ever reclaim a couple of minutes of drift; it is defensive rather than load-bearing, which is what the comment claims.
  • Under --json the per-candidate skip message is suppressed by the runner, so an all-skipped run prints only the generic BLOCKED error. That behavior predates this commit (the probe.ok skip has the same shape) and was not treated as a new finding.
  • Findings from files outside this incremental diff were not carried forward.
Files Reviewed (1 file)
  • dev/seed/app/github-integration-copy.ts

Previous review (commit 36f3437)

Status: No Issues Found | Recommendation: Merge

Executive Summary

Incremental review of 36f34377f (3 files, ~66 changed lines): every previous finding is resolved, and the new copy-lifetime documentation now matches what git-token-service actually does.

Verified in this increment

  • Copy lifetime wording (dev/seed/app/github-integration-copy.ts 344-350, 58-63, AGENTS.md 45-47): the docs no longer claim the expired dummy refresh token prevents a refresh. getUserAccessToken refreshes once access_token_expires_at - now < EXPIRY_BUFFER_MS (github-user-authorization-service.ts:220), GitHub's bad_refresh_token body matches RefreshErrorResponseSchema so refreshAuthorization returns terminal_rejection (:522), and revokeCurrentGeneration sets revoked_at/github_token_rejected on the copy row only (:697). "Reads as revoked, not expired" is accurate, and the row keeps its synthetic id, so the upsert's setWhere re-claims it on the next run as the comment says.
  • 35-minute donor margin (:274) and usableUntil (:406-408): EXPIRY_BUFFER_MS is 5 minutes for both the fetch path and selectUserAuthorization, and no GitHub path uses a larger minimum validity (only Bitbucket has a 55-minute cloud-agent bound), so expiry - 5min is the correct usable boundary and the 35-minute filter really does leave >= 30 usable minutes.
  • --remove reporting (:199-216): the delete now uses .returning() and reports removedRows from the result; a row replaced between the SELECT and the DELETE reports 0 and says so. Previous SUGGESTION resolved.
  • Unused or import (:9): removed. Previous SUGGESTION resolved.
  • --wait 900 (apps/mobile/e2e/github-stub.sh 39-46): the longest lock-held curl chain is csrf + fake-login + trpc-seed + getUserAuthorization at --max-time 120 each = 480s, so the ~8 minute claim holds and 900s clears it. process-lock.ts maps --wait 900 to 900 one-second retries, and the holder refreshes the lockfile every 10s against a 30s stale window, so a queued stop/seed waits instead of failing.
  • Removed SeedResult keys: nothing in the repo consumes accessTokenExpiresAt or refreshable from this topic, so dropping them breaks no caller.
  • AGENTS.md: prose only; no images, HTML tags, or links introduced.
Files Reviewed (3 files)
  • dev/seed/app/github-integration-copy.ts
  • apps/mobile/e2e/github-stub.sh
  • apps/mobile/e2e/AGENTS.md

Notes and assumptions

  • Static reading only; nothing was executed. node is unavailable in this environment, so new Date(<pg timestamptz string>) was not run — the same new Date(access_token_expires_at).getTime() pattern is already used on this column in github-user-authorization-service.ts:220/251 and bitbucket-authorization-service.ts:245, so it is treated as established.
  • nextjs_port() runs pnpm -s dev:status --json inside the lock without an explicit timeout, so the "about 8 minutes" bound covers the curl calls rather than literally every command; with --wait 900 this is not a practical regression and the framing predates this commit.
  • Findings from files outside this incremental diff were not carried forward.

Previous review (commit 66b7904)

Status: 4 Issues Found | Recommendation: Address before merge

Executive Summary

The copy's "already-expired dummy refresh token" does not actually prevent a refresh: git-token-service gates refreshes on the access token's expiry alone, so the bogus token is POSTed to GitHub and the copy row ends up revoked instead of merely expired.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 3
Issue Details (click to expand)

WARNING

File Line Issue
dev/seed/app/github-integration-copy.ts 360 The already-expired dummy refresh token does not stop a refresh attempt: refreshAuthorization never reads refresh_token_expires_at, so 'e2e-copy-never-refreshes' reaches GitHub once the access token is inside the 5-minute buffer and the row is marked revoked_at/github_token_rejected (or hard-fails with temporarily_unavailable) — not the "expired, run the copy again" state the comment, printUsage(), and AGENTS.md describe

SUGGESTION

File Line Issue
dev/seed/app/github-integration-copy.ts 208 --remove reports removedRows: 1 without .returning(), so a row replaced between the SELECT and the DELETE is reported as removed when nothing was
dev/seed/app/github-integration-copy.ts 9 or is imported but unused after the lookup was split into two queries; dev/seed/ is outside both lint and typecheck scope, so nothing catches it
apps/mobile/e2e/github-stub.sh 110 The raised --max-time 120 budgets sum to 360-480s inside the lock, above the wrapper's --wait 300, so a queued stop/seed hard-fails on the lock instead of waiting

Verified in this increment

  • assertLocalDatabaseTarget: matches the established dev/seed/cost-insights/spend-evidence.ts guard exactly (USE_PRODUCTION_DB plus loopback-only hostname set, including [::1]), and runs before any query. Previous production-target finding is resolved.
  • --remove blast radius: the row is read first and non-synthetic ids are refused; the DELETE is additionally bound to the observed github_user_id, so it cannot destroy a real connection. Previous finding resolved.
  • Copy overwrite guard: the pre-check plus setWhere: github_user_id ~ '^[89][0-9]{12}$' and the empty-returning() throw is the same shape as apps/web/src/lib/github-pr-review/dev-seed.ts and user-authorization.ts; a real OAuth row landing mid-run leaves the row untouched and fails loudly. setWhere is supported by the pinned drizzle-orm 0.45.2.
  • Donor safety of the dummy refresh token: revokeCurrentGeneration is a local UPDATE and lockAuthorizationGrant keys on the copy's synthetic id, so the failed refresh cannot touch the donor's grant. The old rotation hazard is genuinely gone.
  • Live donor probe: GET /user with AbortSignal.timeout correctly separates "token rejected" (skip candidate) from "no network" (single explicit error).
  • status states: down / starting / up now derive from session plus state-file presence, and the state file is still written last; nothing machine-parses the string. Previous finding resolved.
  • tmux new-session failure branch: re-checks for the session, so only a real survivor is reported as concurrent; CREATED_SESSION=0 still protects the survivor's session and STATE_DIR. Previous finding resolved.
  • AGENTS.md: prose only; no images, links, or commands that contradict the scripts.
Files Reviewed (3 files)
  • dev/seed/app/github-integration-copy.ts - 3 issues
  • apps/mobile/e2e/github-stub.sh - 1 issue
  • apps/mobile/e2e/AGENTS.md - 0 issues (its "shows as expired" wording is cited in the line-360 finding)

Notes and assumptions

  • Incremental review of 1a34adb48..66b7904ec: 4ed26ca80 (blast-radius guards), c398e757b (env-failure messages), 66b7904ec (atomic guards, expiry margin, live probe).
  • Static reading only; nothing was executed. Dependencies are not installed in this environment, so neither pnpm dev:seed app:github-integration-copy nor github-stub.sh was run, and GitHub's exact error body for a malformed refresh token was inferred from RefreshErrorResponseSchema.
  • Not flagged: the donor-selection comment at lines 246-250 still claims the refresh-expiry bound "excludes every synthetic row", which is why the length(github_user_id) < 13 filter exists.
  • Not flagged: the synthetic id derives from the normalized email while the target lookup now prefers google_user_email, so two accounts normalizing to the same address would collide on UQ_user_github_app_tokens_github_user_app; the id derivation predates this increment.

Fix these issues in Kilo Cloud

Previous review (commit 1a34adb)

Status: 9 Issues Found | Recommendation: Address before merge

Executive Summary

The new --remove path deletes whatever user_github_app_tokens row the target account holds without proving it is a copy, so the documented "take an account back for the stub" step destroys a real GitHub authorization in the shared dev database on a mistyped email.

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 6
Issue Details (click to expand)

WARNING

File Line Issue
dev/seed/app/github-integration-copy.ts 106 New: --remove deletes by kilo_user_id + github_app_type only, so it cannot tell a copy (8 prefix) or a stub seed (9 prefix) from a real authorization; removedRows does not report what was destroyed
dev/seed/app/github-integration-copy.ts 37 Caveats still cover only refresh-token rotation; the copy shares the whole grant, so a disconnect on the copy revokes the donor's real GitHub authorization and the grant advisory lock no longer serializes donor and copy refreshes
dev/seed/app/github-integration-copy.ts 89 No environment guard: getSeedDb() honours USE_PRODUCTION_DB=true, so both the copy and the new --remove delete can run against production tokens; assertLocalDatabaseTarget() is the existing pattern

SUGGESTION

File Line Issue
apps/mobile/e2e/github-stub.sh 201 New: every tmux new-session failure (missing tmux, dead server, rejected -c) is reported as "session appeared concurrently", hiding the real cause
dev/seed/app/github-integration-copy.ts 94 Target lookup matches only normalized_email (nullable), so an existing account gets the misleading "no user — create one" error; app:user-id matches google_user_email OR normalized_email
dev/seed/app/github-integration-copy.ts 189 The synthetic id feeds git-token-service's commit author email, so pushes from the copy are attributed to no GitHub account
dev/seed/app/github-integration-copy.ts 221 Upsert drops the setWhere/.returning() guard both other writers use, so a pre-existing real integration on the target is overwritten irreversibly with no report
apps/mobile/e2e/github-stub.sh 40 Unlocked status answers from tmux has-session alone: up with an empty port before the state file lands, and up for a start cleanup_start is about to roll back
apps/mobile/e2e/github-stub.sh 109 --max-time 30 on the next-auth/tRPC calls is the budget this PR raised to 120s elsewhere for load tolerance; a first-hit dev-route compile that exceeds it turns a slow start into a full rollback

Verified in this increment

  • Donor exclusion of copies: length(github_user_id) < 13 does exclude every copy (8 + 12 digits) and stub seed (9 + 12 digits) while keeping real rows, which come from user.id.toString() in apps/web/src/lib/integrations/platforms/github/user-authorization.ts:139 (numeric REST ids, far short of 13 digits). github_user_id is text, so length() is valid.
  • --remove argument parsing: the flag is only honoured in first position; <email> --remove falls into the rest.length > 0 guard instead of silently copying, and --remove with no email prints usage. The seed runner passes it through untouched (--json is the only flag it strips).
  • --remove scope: the delete is bound to the resolved target id, never another account's row, and it does not call GitHub — the donor's grant is left intact, which is the right choice for a shared grant.
  • tmux new-session failure handling: resetting CREATED_SESSION=0 before exit 1 correctly keeps cleanup_start from killing a survivor's session, and STATE_DIR/env-line rollback stay consistent.
  • Runbook edits: prose-only; no images, links, or commands introduced that contradict the scripts.
Files Reviewed (3 files)
  • dev/seed/app/github-integration-copy.ts - 6 issues (1 new, 5 re-verified as still current)
  • apps/mobile/e2e/github-stub.sh - 3 issues (1 new, 2 re-verified as still current)
  • apps/mobile/e2e/AGENTS.md - 0 issues (its --remove instruction is cited in the line-106 finding)

Notes and assumptions

  • Incremental review of 973fdf583..1a34adb48; new work since the last pass is 1f1eaed9e (the --remove path plus the copy-cannot-donate donor filter) and 1a34adb48 (runbook wording).
  • Static reading only; nothing was executed. Dependencies are not installed here, so pnpm dev:seed app:github-integration-copy --remove <email> was not run.
  • Not flagged: the comment at lines 130-134 still claims the expiry bound "excludes every synthetic row", which the new length(...) < 13 filter exists precisely because it does not (a copy inherits the donor's short expiry).
  • Not flagged: AGENTS.md's "An account holds one token row" is per github_app_type; only the standard row is written and removed, which matches the stub.

Fix these issues in Kilo Cloud

Previous review (commit 973fdf5)

Status: 7 Issues Found | Recommendation: Address before merge

Executive Summary

The new app:github-integration-copy seed relocates a real GitHub OAuth grant between accounts with no environment guard and no protection for the donor: a disconnect on the copy revokes another developer's real authorization at GitHub, and the synthetic github_user_id also defeats the advisory lock that protects the shared single-use refresh token.

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 5
Issue Details (click to expand)

WARNING

File Line Issue
dev/seed/app/github-integration-copy.ts 37 Caveats cover only refresh-token rotation; the copy shares the whole grant, so disconnectUserAuthorization DELETE /applications/{client_id}/grant revokes the donor's real authorization, and the grant advisory lock (keyed on github_user_id) no longer serializes donor and copy refreshes
dev/seed/app/github-integration-copy.ts 79 No environment guard: computeDatabaseUrl() honours USE_PRODUCTION_DB=true, so this credential-relocation script can decrypt and overwrite production tokens; assertLocalDatabaseTarget() in dev/seed/cost-insights/spend-evidence.ts is the existing pattern

SUGGESTION

File Line Issue
dev/seed/app/github-integration-copy.ts 84 Target lookup matches only normalized_email (nullable, NULL for pre-0090 users), so an existing account gets the misleading "no user — create one" error; app:user-id matches google_user_email OR normalized_email
dev/seed/app/github-integration-copy.ts 158 The synthetic id feeds git-token-service's commit author email (<id>+<login>@users.noreply.github.com), so pushes from the copy are attributed to no GitHub account
dev/seed/app/github-integration-copy.ts 190 Upsert drops the setWhere/.returning() guard both other writers use, so a pre-existing real integration on the target is overwritten irreversibly with no report
apps/mobile/e2e/github-stub.sh 40 Unlocked status answers from tmux has-session alone: up with an empty port before the state file lands, and up for a start cleanup_start is about to roll back (carried from previous review, still current)
apps/mobile/e2e/github-stub.sh 109 --max-time 30 on the next-auth/tRPC calls is the budget this PR raised to 120s elsewhere for load tolerance; a first-hit dev-route compile that exceeds it turns a slow start into a full rollback (carried from previous review, still current)

Verified in this increment

  • Envelope compatibility of the copied row: TOKEN_SCHEME and tokenAad() in the new seed match apps/web/src/lib/integrations/platforms/github/user-token-envelope.ts and getCredentialAad in services/git-token-service/src/github-user-authorization-service.ts:436 exactly, so the re-encrypted row stays decryptable by git-token-service.
  • Donor filter: the refresh_token_expires_at < now() + 2 years bound does exclude the stub seed's year-9999 rows (apps/web/src/lib/github-pr-review/dev-seed.ts), and ne(kilo_user_id, target.id) prevents self-donation.
  • Decrypt loop consistency: accessToken/refreshToken can only be read together with the source row that produced them; a partial failure leaves source undefined and falls through to the next candidate or the BLOCKED error.
  • Seed contract: the topic is auto-discovered by dev/seed/index.ts, exports usage/run, returns a flat SeedResult, avoids apps/web/src/... imports, and never logs token material.
  • Lock scoping in github-stub.sh: taking status out of the lock is correctly limited to start | seed | stop, and all five curl calls under the lock carry --max-time.
Files Reviewed (3 files)
  • dev/seed/app/github-integration-copy.ts - 5 issues
  • apps/mobile/e2e/github-stub.sh - 2 issues (both carried from the previous review, unchanged)
  • apps/mobile/e2e/AGENTS.md - 0 issues (referenced in the line-37 finding: the doc omits the donor-account blast radius)

Notes and assumptions

  • Incremental review of 973fdf583..616c33bb3; the only new commit since

[Snapshot truncated.]

Additional previous summary content was truncated to keep this comment within platform limits.


Reviewed by claude-opus-5 · Input: 36 · Output: 9.2K · Cached: 770.6K

Review guidance: REVIEW.md from base branch main

- emulator-start retry: rethrow the already-exists guard (a retry would
  tear down a live emulator) and keep attempt 1's error when the interim
  teardown itself fails
- github-stub: curl transport failures now route through seed_fail, so a
  failed start still rolls back the session and env line
- github-stub: guard against a .env.local without a trailing newline
  before appending, so the marker line can never merge into (and later
  delete) an existing variable
@iscekic
iscekic enabled auto-merge (squash) August 2, 2026 00:38
- tmux targets use exact-match (=name): bare -t prefix-matches, so a sibling
  worktree whose slug extends ours could be probed or killed in its place
- simulator claims write atomically (temp + rename/link): in-place truncation
  let an unlocked reader see a partial file, judge it corrupt, and delete a
  live claim
- a claim abandoned after its boot (rename failure) now powers the device
  off, or keeps the claim when even that fails
- simctl bootstatus is bounded (15 min) so a wedged CoreSimulator cannot
  block a claim forever
- emulator-start tears down after a failed second attempt too
- github-stub start claims its port machine-globally and verifies its own
  session survived, so two worktrees cannot share one stub silently; stop
  releases the claim
- appium: driver install re-checks inside the install lock; server startup
  verifies port ownership before adopting a listener; stop escalates to
  SIGKILL instead of orphaning a live server
- record.sh serializes start/stop per device with the shared process lock
- open-app consumes its launch budget in 30s slices so an ANR dialog is
  answered promptly instead of after the full wait
- login.sh matches the recipient literally, not as a regex
- hierarchy mktemp template puts Xs last (BSD mktemp leaves embedded Xs
  literal, making every bare call after the first fail)
- AGENTS.md: per-platform mixed fixtures, pull-and-grep for ADB dumps,
  stub-stop token residue note
- mobile-workflow tests updated for the raised launch/settle budgets
Comment thread apps/mobile/e2e/github-stub.sh
iscekic added 3 commits August 2, 2026 03:06
- appium: the recorded-server fast path now verifies the recorded pid owns
  the listener before adopting (a recycled pid plus a sibling's server on
  the same port answered /status and got adopted)
- github-stub: seeding extracted into seed_token; new 'seed <email>'
  subcommand adds a token row for a second signed-in account (the other
  platform's verifier) without touching the running stub
- github-stub: the port claim covers only the choose-to-bind window and an
  EXIT trap releases it, so an interrupted start cannot leak claims and
  erode the port range
- github-stub: githubUserId carries the pid — concurrent seeds in the same
  second cannot collide on the unique github_user_id
- AGENTS.md: the ADB dump example creates its own mktemp file instead of
  assuming $SCRATCH
devSeedUserGithubToken can answer 200 with success:false (row not
upserted); the status code alone reported a seed that never landed.
A process that survives even SIGKILL kept its state file removed and the
stop reported success; the caller then released device claims over a live
server. stop now fails and keeps the state in that case.
Comment thread apps/mobile/e2e/github-stub.sh Outdated
Comment thread apps/mobile/e2e/appium.sh Outdated
- claim_port reaps an orphaned port claim (older than 120s with nothing
  listening) and the start path traps INT/TERM/HUP besides EXIT, so a
  killed start can no longer shrink the port pool permanently
- githubUserId derives deterministically from the account email: the
  upsert's setWhere only updates a matching row, so the fresh random id
  made every re-seed of an already-seeded account report success:false
  and tear the stub down with a misleading hint
- lsof ownership probes tolerate no-match exits; pipefail otherwise killed
  appium.sh before the fresh-start fallback could run
Comment thread apps/mobile/e2e/github-stub.sh Outdated
Comment thread apps/mobile/e2e/github-stub.sh
iscekic added 2 commits August 2, 2026 03:37
…ling

- appium startup adopts a port only on proven ownership (listener pid equals
  ours); an empty lsof keeps polling instead of adopting a vanished or
  foreign server
- github-stub signal traps exit instead of continuing a start that just
  released its port claim
- success:false with 200 and no error means an account seeded under an older
  random id — keep that usable row instead of tearing the stub down
success:false can come from an older random-id seed or from a real GitHub
connection on the dev account; the keep-message now says so instead of
implying only legacy seeds, and notes the stub accepts any token.
Comment thread apps/mobile/e2e/github-stub.sh
Comment thread apps/mobile/e2e/github-stub.sh
Comment thread apps/mobile/e2e/appium.sh
…lback

- a kept token row is verified to be the stub's (getUserAuthorization
  githubLogin == kilo-stub-user); a real GitHub connection row fails with a
  drop-the-row hint — under the stub its refresh bypasses
  GITHUB_API_BASE_URL, hits real github.com, and revokes itself
- start rolls back everything (session, env line, state, port claim) via one
  EXIT trap when it dies before the final state write, so an aborted start
  no longer blocks the next one behind 'session already runs'
- appium ownership checks degrade to pid+status when lsof is missing or
  cannot attribute the socket, instead of killing healthy servers across 50
  port blocks
Comment thread apps/mobile/e2e/github-stub.sh Outdated
Comment thread apps/mobile/e2e/github-stub.sh
Comment thread apps/mobile/e2e/appium.sh
- the row probe checks HTTP status and error envelope before advising: a
  failed probe reports 'could not determine the existing row' instead of
  confidently telling the operator to delete a row it never examined; a
  kept row must also be connected (a revoked stub row gets the drop hint)
- start's rollback undoes only what this run did (session-created and
  env-added flags), so a start losing the session-creation race cannot kill
  the winner's session, env line, or state
- the recorded-server reuse path applies the same blind-lsof rule as the
  start loop: adopt on pid+status after three blind probes instead of
  orphaning a healthy server and consuming a port block per invocation
- drop dead SEED_FAIL_MODE and the stale random-id comment
Comment thread apps/mobile/e2e/appium.sh
Comment thread apps/mobile/e2e/github-stub.sh
Comment thread apps/mobile/e2e/github-stub.sh
- github-stub creates its session under a pid-unique name armed before the
  create and renames to claim the canonical one: a signal at any instant
  tears down only our own session, and a lost rename race touches nothing
  but our port claim; the env-added flag arms before the append for the
  same reason
- the appium reuse probe rechecks pid liveness and /status on every pass,
  so a server dying mid-probe is cleaned up (stop_server) instead of
  adopted; a foreign listener still only drops our state
Comment thread apps/mobile/e2e/github-stub.sh
iscekic added 2 commits August 2, 2026 04:11
…windows

Concurrent stub commands raced over the session name, env line, and state
dir; each guard narrowed the window without closing it. The shared
process-lock (the appium.sh pattern) removes the class: one stub command per
worktree at a time, so the entry check is authoritative, the rollback flags
only order against signals, and the unique-name rename dance goes away.
Both row branches run after the account answered the seed call, so the
sign-in hint contradicted what the script had just proved.
@iscekic
iscekic disabled auto-merge August 2, 2026 02:15
Comment thread apps/mobile/e2e/github-stub.sh Outdated
- every network call under the stub lock carries --max-time 30, so a hung
  local API cannot hold the lock to its 300s wait bound; status runs
  unlocked (read-only) and answers during a long start
- a failed session create (a survivor of a SIGKILLed lock wrapper got there
  first) resets the created flag before exiting, so cleanup cannot touch
  the survivor's session
- align the env-append comment with the lock (the rename step it cited is
  gone)
Comment thread apps/mobile/e2e/github-stub.sh
Comment thread apps/mobile/e2e/github-stub.sh Outdated
pnpm dev:seed app:github-integration-copy <email> finds the newest real
integration in the shared dev database (fakes excluded: kilo-stub* logins
and far-future expiries — a real refresh token expires within months),
decrypts it with git-token-service's local dev key, re-encrypts for the
target account under a synthetic github_user_id (the pair index is unique),
and upserts. Scenarios that need a real integration (cloud agents) run
against the copy; with no valid donor the command fails with an explicit
BLOCKED message, and the runbook tells verifiers to report VERIFICATION
BLOCKED rather than fake or silently skip. Verified against the dev db:
copy mechanics end to end, and the blocked path on a database holding only
seeded rows.
iscekic added a commit that referenced this pull request Aug 2, 2026
The e2e harness changes belong to #4960.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fqia1fHqrSCcW5WzHQ4gPM
iscekic added a commit that referenced this pull request Aug 2, 2026
The e2e harness changes belong to #4960.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fqia1fHqrSCcW5WzHQ4gPM
iscekic added a commit that referenced this pull request Aug 2, 2026
The e2e harness changes belong to #4960.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fqia1fHqrSCcW5WzHQ4gPM
iscekic added a commit that referenced this pull request Aug 2, 2026
The e2e harness changes belong to #4960.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fqia1fHqrSCcW5WzHQ4gPM
iscekic added a commit that referenced this pull request Aug 2, 2026
The e2e harness changes belong to #4960.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fqia1fHqrSCcW5WzHQ4gPM
iscekic added a commit that referenced this pull request Aug 2, 2026
The e2e harness changes belong to #4960.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fqia1fHqrSCcW5WzHQ4gPM
iscekic added a commit that referenced this pull request Aug 2, 2026
The assertions pin apps/mobile/e2e content that #4960 owns.
Point the seed help text at the stub that exists on main.
iscekic added a commit that referenced this pull request Aug 2, 2026
The assertions pin apps/mobile/e2e content that #4960 owns.
Point the seed help text at the stub that exists on main.
iscekic added a commit that referenced this pull request Aug 2, 2026
The assertions pin apps/mobile/e2e content that #4960 owns.
Point the seed help text at the stub that exists on main.
iscekic added a commit that referenced this pull request Aug 2, 2026
The assertions pin apps/mobile/e2e content that #4960 owns.
iscekic added a commit that referenced this pull request Aug 2, 2026
The e2e harness changes belong to #4960.
iscekic added a commit that referenced this pull request Aug 2, 2026
The e2e harness changes belong to #4960.
iscekic added a commit that referenced this pull request Aug 2, 2026
The e2e harness changes belong to #4960.
iscekic added a commit that referenced this pull request Aug 2, 2026
The assertions pin apps/mobile/e2e content that #4960 owns.
Point the seed help text at the stub that exists on main.
iscekic added a commit that referenced this pull request Aug 2, 2026
The e2e harness changes belong to #4960.
iscekic added a commit that referenced this pull request Aug 2, 2026
The assertions pin apps/mobile/e2e content that #4960 owns.
Point the seed help text at the stub that exists on main.
iscekic added a commit that referenced this pull request Aug 2, 2026
The e2e harness changes belong to #4960.
iscekic added a commit that referenced this pull request Aug 2, 2026
The assertions pin apps/mobile/e2e content that #4960 owns.
Point the seed help text at the stub that exists on main.
iscekic added a commit that referenced this pull request Aug 2, 2026
The e2e harness changes belong to #4960.
iscekic added a commit that referenced this pull request Aug 2, 2026
The assertions pin apps/mobile/e2e content that #4960 owns.
iscekic added a commit that referenced this pull request Aug 2, 2026
PR #4960 carries these changes and merges first. This branch keeps no copy.

- dev/local/mobile-android.ts
- dev/local/mobile-simulator.ts
- dev/local/tmux.ts
- dev/seed/app/github-integration-copy.ts
iscekic added a commit that referenced this pull request Aug 2, 2026
PR #4960 carries these changes and merges first. This branch keeps no copy.

- dev/local/mobile-android.ts
- dev/local/mobile-simulator.ts
- dev/local/tmux.ts
- dev/seed/app/github-integration-copy.ts
iscekic added a commit that referenced this pull request Aug 2, 2026
PR #4960 carries these changes and merges first. This branch keeps no copy.

- dev/local/mobile-android.ts
- dev/local/mobile-simulator.ts
- dev/local/tmux.ts
- dev/seed/app/github-integration-copy.ts
iscekic added a commit that referenced this pull request Aug 2, 2026
PR #4960 carries these changes and merges first. This branch keeps no copy.

- dev/local/mobile-android.ts
- dev/local/mobile-simulator.ts
- dev/local/tmux.ts
- dev/seed/app/github-integration-copy.ts

Drop dev/local/github-integration-copy.test.ts. It imports the reverted seed
file.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant