From 387a806ae514887450ec95bb2ae3f51479cef3c7 Mon Sep 17 00:00:00 2001 From: Bretton <36870434+BrettM86@users.noreply.github.com> Date: Sun, 19 Jul 2026 23:43:08 -0700 Subject: [PATCH] docs(qa): track the QA loop spec and run log in-repo; add round-2 B row Both files existed only in the working tree; every round appends a row, so persist them where the loop's ground rules say they live. Co-Authored-By: Claude Fable 5 --- docs/QA_LOOP.md | 290 ++++++++++++++++++++++++++++++++++++++++++++ docs/QA_LOOP_LOG.md | 18 +++ 2 files changed, 308 insertions(+) create mode 100644 docs/QA_LOOP.md create mode 100644 docs/QA_LOOP_LOG.md diff --git a/docs/QA_LOOP.md b/docs/QA_LOOP.md new file mode 100644 index 0000000..30518c9 --- /dev/null +++ b/docs/QA_LOOP.md @@ -0,0 +1,290 @@ +# Coves Mobile — QA Verification Loop + +Reference spec for the recurring emulator-based QA loop. Each iteration of the +loop picks the next section below (round-robin), spawns a QA subagent that +exercises it end-to-end on the Android emulator against the **local** backend, +fixes any bugs found, and commits the fixes. + +## Ground rules (every iteration) + +- **One QA agent drives the emulator at a time.** Never run two + emulator-driving agents in parallel; read-only code-audit agents may run + alongside. +- **Local backend only.** `flutter run --flavor dev` alone still talks to + PRODUCTION — always pass `--dart-define=ENVIRONMENT=local`. +- **Test account**: handle `mari.local.coves.dev`, password `password`. +- **Verify before fixing**: reproduce the bug on the emulator, fix it, then + re-run the same steps to confirm the fix. +- **Quality gates before any commit**: `flutter analyze` clean, and + `flutter test ` passing. +- **Commit style** (matches history — conventional commits, scoped): + `fix(comments): handle absent author on deleted comments`. One logical fix + per commit. Another agent may be committing too — `git pull --rebase` before + pushing/committing, and only ever commit files you changed. +- **Log every run**: append a row to `docs/QA_LOOP_LOG.md` + (`| date | section | result | bugs found | commits |`). Create the file with + that header if missing. + +## Environment playbook + +### 1. Backend stack (usually already up) +```bash +curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/xrpc/_health # expect 200 +# If down: +cd /Users/bretton/Code/coves && make dev-up +``` + +### 2. Emulator +```bash +adb devices # if empty: +~/Library/Android/sdk/emulator/emulator -avd Medium_Phone_API_36.1 -no-snapshot-save & +adb wait-for-device +adb shell 'while [ "$(getprop sys.boot_completed)" != "1" ]; do sleep 1; done' +# Port-reverse the local stack (required every boot): +for p in 3001 3002 8080 8081; do adb reverse tcp:$p tcp:$p; done +``` + +### 3. Run the app +```bash +cd /Users/bretton/Code/coves-mobile +flutter run --flavor dev --dart-define=ENVIRONMENT=local +# (run in background; hot reload with `r` is unavailable non-interactively — +# rebuild or use `flutter run --machine` if needed) +``` + +### 4. Driving the UI — Maestro first, adb as fallback + +**Primary: Maestro** (installed via Homebrew; needs Android Studio's JDK): +```bash +export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home" +export PATH="$JAVA_HOME/bin:$PATH" +maestro test .maestro/.yaml # run a flow +maestro hierarchy # inspect the semantics tree (find selectors) +``` +Flows live in `.maestro/` at the repo root, named `_.yaml` +(e.g. `a_login.yaml`). **Write each check as a flow and COMMIT passing flows** — +they accumulate into a regression suite that later iterations re-run first. +Example: +```yaml +appId: social.coves.dev +--- +- launchApp +- tapOn: "Sign in" # matches semantics label / visible text +- tapOn: "Handle" +- inputText: "mari.local.coves.dev" +- tapOn: "Continue" +- assertVisible: "Home" # auto-waits; fails with a screenshot +- takeScreenshot: scratch/a2_logged_in +``` +Maestro reads Flutter's accessibility tree and can also drive native UI +(OAuth browser tab, permission dialogs). If a widget isn't findable, add a +`Semantics` label/tooltip to it in the app — that's a legitimate a11y fix, +commit it as one. + +**Fallback: raw adb** (coordinate-based — only when Maestro can't do it): +```bash +adb exec-out screencap -p > shot.png # then Read the image to inspect +adb shell input tap +adb shell input text 'hello%sworld' # %s = space +adb shell input keyevent 66 # enter +adb shell uiautomator dump && adb pull /sdcard/window_dump.xml # widget tree +``` + +**Durable in-app tests**: for pure in-app flows (no OAuth tab, no native +dialogs), also consider seeding `integration_test/` cases — they're the best +long-term regression artifact. Don't block a section run on this. + +### 5. Legal-gate bypass (fresh installs, debug builds) +The EULA checkbox requires scrolling the entire agreement — bypass instead: +```bash +adb shell am force-stop social.coves.dev +# via run-as social.coves.dev, add to shared_prefs/FlutterSharedPreferences.xml: +# +# +``` +(Do NOT bypass when testing Section G — the gates ARE the test there.) + +### 6. Cold-load deep link (no http intent filters exist; use explicit component) +```bash +adb shell am start -n social.coves.dev/social.coves.MainActivity \ + -a android.intent.action.VIEW \ + -d 'social.coves:///post/' # note THREE slashes +``` + +--- + +## Section A — Auth & Session + +**Code**: `lib/screens/auth/`, `lib/screens/landing_screen.dart`, +`lib/services/coves_auth_service.dart`, `lib/services/auth_interceptor.dart`, +`lib/services/pds_discovery_service.dart`, `lib/providers/auth_provider.dart`, +`lib/widgets/sign_in_dialog.dart`, `lib/widgets/bluesky_sign_in_button.dart` + +**Emulator checks** +1. Fresh install → landing screen renders, no overflow/blank states. +2. Sign in with `mari.local.coves.dev` / `password` → OAuth completes, lands + on home feed. +3. Kill the app (`adb shell am force-stop social.coves.dev`), relaunch → + session persisted, no re-login. +4. Wrong password → graceful error, no crash, can retry. +5. Sign out → returns to landing, tokens cleared (no authed calls succeed). +6. Airplane-mode login attempt (`adb shell cmd connectivity airplane-mode enable`) + → friendly error, restore afterwards. + +**Static checks**: tokens only in flutter_secure_storage; no tokens in logs; +`flutter test test/services test/providers`. + +## Section B — Home Feed + +**Code**: `lib/screens/home/feed_screen.dart`, `lib/screens/home/main_shell_screen.dart`, +`lib/providers/multi_feed_provider.dart`, `lib/providers/vote_provider.dart`, +`lib/widgets/feed_page.dart`, `lib/widgets/post_card.dart`, +`lib/widgets/post_card_actions.dart`, `lib/widgets/post_action_bar.dart` + +**Emulator checks** +1. Feed loads with visible loading state (no blank flash), posts render. +2. Scroll 30+ posts: no jank, no overflow boxes, images load, list recycles. +3. Pull-to-refresh works; pagination fetches next page at bottom. +4. Upvote/downvote a post → optimistic update, survives refresh; un-vote works. +5. Switch feed tabs (if present) → each retains scroll position/state. +6. Airplane mode + refresh → error state with retry, not a crash. + +**Static checks**: `flutter test test/widgets test/providers`. + +## Section C — Post Detail & Comments + +**Code**: `lib/screens/home/post_detail_screen.dart`, `post_detail_loader.dart`, +`focused_thread_screen.dart`, `lib/providers/comments_provider.dart`, +`lib/services/comment_service.dart`, `lib/services/comments_provider_cache.dart`, +`lib/widgets/comment_thread.dart`, `comment_card.dart`, `comment_composer.dart`, +`comments_header.dart`, `detailed_post_view.dart` + +**Emulator checks** +1. Tap a feed post → detail renders full content + comments. +2. Cold-load: deep link straight to a post (playbook §6) with app killed → + loads via `social.coves.community.post.get`, no crash. +3. Comment thread: nesting renders, collapse/expand, load-more replies. +4. Post a comment → appears in thread; reply to a comment → correct nesting. +5. Deleted comment with absent author renders placeholder (regression: + 652f075), no crash. +6. Vote on comments; comment sort (if present); keyboard doesn't cover the + composer (`resizeToAvoidBottomInset`). +7. **Deep chains** (seed via API if needed — reply chain of 15–20 nested + comments, alternating authors mari/test-aggregator): full chain renders in + correct nesting order with no overflow/clipped indent rails at max depth; + "load more replies" appears past the fetch depth/limit and each load + continues the chain correctly; collapse/expand works at deep levels; + scroll performance stays smooth through the chain. +8. **Focused thread screen** (`focused_thread_screen.dart` — the + "continue thread" view): entering it from a deep comment shows the right + subtree rooted at that comment; replying from inside it lands the reply at + the correct depth and it appears in both the focused view and the full + thread; back returns to the parent thread at the right position; deep-link + cold-load into a focused thread doesn't crash. + +**Static checks**: `flutter test test/widgets test/services`. + +## Section D — Communities + +**Code**: `lib/screens/home/communities_screen.dart`, +`communities_discovery_screen.dart`, `communities_see_all_screen.dart`, +`communities_admin_panel.dart`, `lib/screens/community/community_feed_screen.dart`, +`lib/providers/community_subscription_provider.dart`, +`lib/widgets/community_*.dart`, `tappable_community.dart` + +**Emulator checks** +1. Communities tab → discovery screen renders (browse + search + widgets — + regression: 55f15fb). +2. Search communities: results update, empty-query and no-results states. +3. Open a community → feed loads, header/avatar/hero card render. +4. Join → button state flips, feed reflects membership; leave → reverts. +5. See-all screen paginates; admin panel opens without crash (if accessible). +6. Community chips/tappable-community navigate correctly from feed posts. + +**Static checks**: `flutter test test/screens test/widgets`. + +## Section E — Compose & Posting + +**Code**: `lib/screens/home/create_post_screen.dart`, +`lib/screens/compose/community_picker_screen.dart`, `reply_screen.dart`, +`lib/widgets/image_source_picker.dart`, `lib/services/streamable_service.dart` + +**Emulator checks** +1. Create post: pick community, title + body, submit → appears in community + feed and cold-loads by deep link. +2. Validation: empty title/body blocked with clear message; over-length input + handled. +3. Community picker: search, select, cancel — state consistent after each. +4. Reply screen: open from comment, submit, cancel-with-draft behavior. +5. Keyboard behavior: no overflow, fields visible while typing. +6. Kill app mid-compose → no crash on relaunch (draft loss acceptable, crash + is not). + +**Static checks**: controllers disposed (grep `TextEditingController` for +matching `dispose()`); `flutter test test/screens`. + +## Section F — Profile + +**Code**: `lib/screens/home/profile_screen.dart`, `edit_profile_screen.dart`, +`lib/providers/user_profile_provider.dart`, `lib/widgets/profile_header.dart`, +`tappable_author.dart` + +**Emulator checks** +1. Own profile tab renders: avatar, handle, stats, posts list. +2. Tap an author in the feed → their profile opens. +3. Edit profile: change display name/bio → persists after app restart. +4. Avatar change flow opens image picker without crash (emulator has no + camera — gallery path only). +5. Profile of user with no posts → sane empty state. + +**Static checks**: `flutter test test/providers test/screens`. + +## Section G — Moderation, Safety & Legal Gates + +**Code**: `lib/providers/block_provider.dart`, `eula_provider.dart`, +`community_guidelines_provider.dart`, `lib/widgets/block_action_helpers.dart`, +`report_dialog.dart`, `lib/screens/eula_screen.dart`, +`community_guidelines_screen.dart` + +**Emulator checks** (use a FRESH install; do NOT bypass gates here) +1. EULA gate: shown on first launch, accept requires full scroll, acceptance + persists across restart. +2. Community guidelines gate (regression: 1481ef9): same lifecycle. +3. Report a post → dialog opens, reasons selectable, submit + cancel work. +4. Block a user → their content disappears from feed; unblock restores. +5. Blocked-state edge cases: viewing a blocked user's profile, their comments + in threads. + +**Static checks**: `flutter test test/providers`. + +## Section H — Media & Rich Content + +**Code**: `lib/widgets/fullscreen_video_player.dart`, +`minimal_video_controls.dart`, `rich_text_renderer.dart`, +`external_link_bar.dart`, `source_link_bar.dart`, `bluesky_post_card.dart`, +`share_button.dart`, `lib/utils/` + +**Emulator checks** +1. Post with video: inline plays, fullscreen toggle, controls + show/hide, back gesture exits cleanly (player disposed — no audio after + exit). +2. Rich text: links, mentions, formatting render; tapping a link opens + browser/in-app correctly. +3. External/source link bars show domain and open target. +4. Bluesky-sourced post card renders distinctly and doesn't crash on missing + fields. +5. Share button produces a share sheet with a sensible URL. +6. Rotate device during video playback → no crash, state preserved. + +--- + +## Iteration protocol (what the loop does each wake) + +1. Read `docs/QA_LOOP_LOG.md`; pick the next section round-robin (A→H, wrap). +2. Verify env: backend health, emulator booted, `adb reverse` applied. +3. Spawn ONE QA subagent with: this file's ground rules + playbook + the + section's checklist. It tests on the emulator, screenshots as evidence, + fixes bugs it can verify, runs analyze/tests, commits per the style above. +4. If the subagent reports bugs it could not fix, record them in the log row + as `OPEN: ` so a later iteration (or the user) picks them up. +5. Append the log row, then schedule the next wake. diff --git a/docs/QA_LOOP_LOG.md b/docs/QA_LOOP_LOG.md new file mode 100644 index 0000000..3ee7a30 --- /dev/null +++ b/docs/QA_LOOP_LOG.md @@ -0,0 +1,18 @@ +# QA Loop Run Log + +Round-robin order: A → B → C → D → E → F → G → H → (wrap). +Each iteration appends one row. `OPEN:` items are unfixed bugs for later pickup. + +| Date | Section | Result | Bugs found | Commits | +|------|---------|--------|------------|---------| +| 2026-07-16 | A — Auth & Session | 6/6 PASS | Fixed: injected-Dio auth regression (18 test fails on main); stale API test fixtures (8 fails); missing bottom-nav Semantics. OPEN: (backend, Coves repo) cancelling on PDS OAuth consent page strands user on raw error page at :8081 instead of redirecting to app. OPEN: pre-existing analyze errors in test/widgets/comment_thread_test.dart (countDescendants undefined — fix during Section C). | 620a929, 0803fc5, b4eb35c, 861c782 (Maestro auth flows) | +| 2026-07-16 | B — Home Feed | 6/6 PASS | Fixed: 8 feed_screen_test failures (missing provider registrations after ff070a1's Consumer3 change). OPEN: a1_login_session Maestro flow flakes in suite mode (~2/3 early failure rate, signed-in self-recovery `when:` check reads stale semantics tree; passed standalone) — harden with retry/waitForAnimationToEnd. | 03de299, 5a17056 (Maestro feed flows b1–b5) | +| 2026-07-16 | C — Post Detail & Comments | 6/6 PASS (load-more-replies button not exercisable — no seeded thread exceeds fetch depth/limit) | Fixed: comment/reply invisible after posting (single refresh raced AppView async indexing — reply persisted server-side but never rendered); bearer tokens logged raw to logcat by debug LogInterceptor; post-detail action bar buttons had no Semantics labels; stale comment_thread_test (countDescendants removed in e134a88). OPEN: (minor UX) ReplyScreen on very long posts opens with the focused composer below the fold — jump-to-bottom fires before async images grow the context; first keystroke scrolls caret into view, so typing is never hidden. | b4148e8, 9110ee0, 967181a, 05610a9, 3ef0ad7 (Maestro c1–c3 + a1 hardening) | +| 2026-07-16 | D — Communities | 6/6 PASS (see-all cursor pagination not exercisable — 6 seeded communities < 50/page, seeding 45+ PDS-backed communities would pollute other sections' fixtures; discovery screen tested signed-out via landing "Explore communities" plus new non-admin PDS account kai.local.coves.dev, because mari is in kAdminHandles and the Communities tab shows the admin panel instead) | Fixed: join-button spinner stuck forever after subscribing from discovery tiles (provider cleared pending flag without notifyListeners — 3edf824); "See all" buttons unreachable by a11y/UI tests (merged into the section-header semantics node, taps hit dead space — container:true, 68ff3a4); admin back arrow unlabeled (tooltip 'Back'). OPEN: (backend, Coves repo) social.coves.community.get never populates viewer state — internal/api/handlers/community/get.go lacks the PopulateCommunityViewerState call + OptionalAuth middleware that list.go has, so the community screen shows "Join" for already-subscribed users on cold load (verified: joined, cold-restarted, count incremented but button reverted); not patched because the backend working tree is mid-refactor (uncommitted jetstream changes) and restarting the user's dev server was too risky. OPEN: system back anywhere in the main shell jumps to the Home tab — CreatePostScreen's always-alive PopScope (canPop:false) inside MainShellScreen's IndexedStack intercepts every back press, so admin subpage back skips its menu and back can never background the app; needs a shell-level back design, not a per-screen PopScope. NOTE: community header subscriber count lags join/leave by firehose indexing (eventual consistency; refresh corrects). Pre-existing: 2 create_post_screen_test thumbnail failures (Section E scope, fail on clean main). | 3edf824, 68ff3a4, 8756906 (Maestro d1–d4, suite 15/15) | +| 2026-07-16 | E — Compose & Posting | 6/6 PASS (incl. deep-link cold load of freshly created post; empty-input "block" is the disabled Post button by design — no snackbar exists for it) | Fixed: reply drafts never saved/restored/cleared (ReplyScreen context.read above a root-navigator route where no ancestor provider exists — every draft call threw ProviderNotFoundException and was swallowed; now uses widget.commentsProvider); shell back-press hijack from iteration 4 (moved PopScope to MainShellScreen, intercepts only when Create tab active AND composer dirty — verified: back backgrounds app from Home/Notifications, dirty composer redirects to Home with draft intact, empty composer exits); 2 pre-existing create_post_screen_test thumbnail failures (tests asserted a field intentionally removed in 746df36 — replaced with a stays-removed regression test); unlabeled X close buttons on compose + picker (tooltip 'Close'). QUIRKS: Maestro hideKeyboard = BACK press on Android (dirty-draft guard redirects it to Home — never use it mid-compose); popping back from post detail refocuses the title field and the keyboard covers the bottom nav; TextField maxLength counter's semantics label is "N characters remaining"/"No characters remaining", not "300/300". | 9b048f3, daece2f, 3664d89, e115a0a, 2fb7b25 (Maestro e1–e5) | +| 2026-07-16 | F — Profile | 5/5 PASS (avatar check passes to picker/crop/staging; the actual blob UPLOAD is broken by an OPEN backend issue) | Fixed: saved bio never rendered anywhere in the app (getProfile returns the bio as `description` per atProto convention; UserProfile.fromJson only read `json['bio']`); avatar edit control on Edit Profile had no semantics (bare GestureDetector — invisible to screen readers and UI tests). OPEN: (backend/PDS, Coves repo) com.atproto.repo.uploadBlob is rejected with 403 ScopeMissingError "blob:*/*" even on a FRESH OAuth session, although cmd/server/main.go requests blob:*/* and the callback's critical-scope validation logs no warning; update_profile.go maps the 403 to 401 AuthExpired, the app's refresh-and-retry also 401s, and the auth interceptor then SIGNS THE USER OUT — avatar/banner upload is fully broken on the local stack and costs the session (repro: Me > Edit Profile > Change Avatar > gallery > crop > Save; PDS log shows use_dpop_nonce then ScopeMissingError). QUIRKS: profile header handle+DID merge into one multiline semantics node (wrap asserts in `[\s\S]*`); clearing a TextField in Maestro needs longPress > "Select all" > eraseText (a plain tap can drop the cursor mid-text and eraseText only deletes before it); the post-save profile refresh races firehose indexing — poll via pull-to-refresh instead of asserting immediately; f4 needs a gallery image (adb push + MEDIA_SCANNER_SCAN_FILE recipe in flow header); f5 needs an adb deep link (no VIEW intent filter) so it is committed but excluded from flowsOrder. | f2e073c, 400021c, de44fe5 (Maestro f1–f5) | +| 2026-07-16 | G — Moderation, Safety & Legal Gates | 3/5 PASS, G4 partial (mobile side correct, server never filters), G5 partial (blocked filtering + comment surfaces unexercisable) | Fixed: post-detail overflow "Report" was a dead "coming soon" stub although ReportDialog exists and works from feed cards — wired to the real dialog with sign-in guard + hidden on own posts (af733ec, verified via g3 flow). G1/G2 PASS on true fresh install: EULA then Guidelines gates shown in order, pre-scroll checkbox taps inert, un-accepted state survives restart, full-scroll unlock works, both acceptances persist (prefs eula_accepted_version=1, community_guidelines_accepted_version=1). G3 PASS incl. server receipt (admin_reports id 13, reason=spam, explanation intact); cancel works. OPEN: (backend, Coves repo) .env.dev JETSTREAM_URL only subscribes wantedCollections=social.coves.actor.profile — social.coves.actor.block events are never consumed, so user_blocks never populates: block "succeeds" (PDS record written) but feeds/comments are NEVER filtered, and unblockUser 404s (repo.GetBlock finds nothing) surfacing "Content not found" — users get permanently stuck "blocked". Fix = append &wantedCollections=social.coves.actor.block to .env.dev:99 + server restart (not restarted: server is another session's process on a mid-refactor tree). Read-path SQL (feed/discover/comment repos) already filters on user_blocks and posts/service.go rewrites blocked authors' posts to blockedPost markers, so index arrival should light it all up. Blocked author's profile renders fine (actor.getPosts has no viewer-block filter — by design or gap). QA block record cleaned off PDS via repo.deleteRecord as mari; no active blocks left. NOTE: seed data has no non-mari comment authors, so comment-level block/report menus on others' comments were exercised via post cards only; own-comment menu correctly shows Delete only. QUIRKS: EULA/guidelines unlock needs the TRUE bottom (last heading visible ≠ unlocked — assert the scroll hint is GONE before tapping agree; g1 flow does extra swipes); fast `adb shell input swipe` flings (100ms) don't scroll the Markdown gates, use ≥400ms; post-detail report selector is "More options" → "Report" (feed cards: "Post options" → "Report post"). | af733ec, ddda92d (Maestro g1 excluded/destructive, g3 in suite) | +| 2026-07-19 | H — Media & Rich Content | 5/6 PASS, H5 partial (share is a deliberate app-wide "Share coming soon!" stub — snackbar verified graceful on feed cards and post detail; no share sheet exists to test; completed on third attempt after two agent crashes) | Fixed (by prior H agents, verified this run): fc6719e Play-video Semantics labels, 166201a URL-field auto-capitalization (a pre-fix casualty "Https://streamable.com" post from 07-16 still sits in !science unfurled as a plain external link — harmless fixture). This run: installed APK was stale (2026-07-04, predating ALL section C–H fixes) — rebuilt+reinstalled; hardened h1's mid-flow relaunch with retry (partial-semantics-tree flake + transient empty Discover feed while appview restarts). H1 PASS: seeded Streamable post unfurls to video (embedType=video+thumb), inline play from feed card and detail, fullscreen player renders scrubber, tap toggles pause/play both ways (dumpsys audio state started<->paused), BACK exits with player disposed — zero USAGE_MEDIA playback configs after exit; controls are an always-visible scrubber by design (no show/hide). H2 PASS: NASA post facets render as separate tappable link nodes, [dawn.com] tap opens Chrome at www.dawn.com (VIEW intent verified); mention facets NOT supported by RichTextRenderer (links only) — render as plain text, no crash. H3 PASS: feed-card ExternalLinkBar + detail source bar show domains; Telegraph post two-bar case (external URL bar + source domain bar) both open Chrome at telegraph.co.uk; recognizers disposed. H4 PASS: Bluesky card (untitled post, !ai) renders distinctly — butterfly logo, author, text, nested chronicle.com link card, counts; tap opens bsky.app; resolved==null falls back to unavailable-card (static). H5 PARTIAL: ShareButton is a documented stub; More options > Copy link copies the raw at:// URI. OPEN: wire share_plus (already a pubspec dep) once a canonical web post URL exists — needs product decision, not invented here. H6 PASS: landscape/portrait rotation mid-playback via user_rotation — relayout correct, position preserved, no crash, audio uninterrupted; settings restored. Static: test/widgets+test/utils 175 pass, analyze at 536 baseline. ENV NOTE: mari signed out twice mid-run — backend agent's appview restarts rotated the unset OAUTH_SEAL_SECRET, killing sessions (environmental, not an app bug); emulator adbd also wedged during long video playback (device offline) — required emulator kill+relaunch, after which Chrome first-run reappeared and had to be redismissed before a1. | eb2f998 (Maestro h1–h2 + suite wiring) | +| 2026-07-19 | A — Auth & Session (ROUND 2) | PASS — a1/a2/a3 regression + NEW OAuth-cancel surfaces (both cancel points) + corrupt-session relaunch | Fixed: OAuth cancel/deny mishandled — backend cc98f26 now 302s cancels back to the app (social.coves://callback?error=access_denied&error_description=...), which the callback parser read as FormatException "Missing required parameter: token" → scary "Sign in failed. Please try again later." snackbar + Sentry capture as 'unexpected' for a deliberate user action; now parseCallbackUrl surfaces SignInCancelledException (also from tab-close CANCELED) → quiet "Sign in cancelled." snackbar, no Sentry noise. Verified all three cancel shapes on-device: PDS sign-in page Cancel, consent "Deny access", custom-tab close. a3 updated to the new redirect (old flow asserted the removed dead-end page); NEW a4_oauth_cancel codifies the consent-deny path (runs last in suite, starts+ends signed out). Sign-out token clearing re-verified at the storage level (FlutterSecureStorage.xml holds only crypto keysets after sign-out); corrupt-ciphertext session blob + relaunch → decryption failure caught, lands signed-out on landing, no crash, no secrets logged (cheap stand-in for seal-secret rotation; live rotation already observed twice in the H run signing out gracefully). ENV: installed APK had rolled back to the 2026-07-04 build again (H's emulator relaunch apparently restored an old snapshot) — reinstalled Jul 19 build, then rebuilt with the fix; session survived install -r. QUIRKS: consent deny button is "Deny access" (not "Deny"); Chrome's "Save password?" sheet floats over the consent page but not the buttons — tap through it, never BACK (closes the whole tab → CANCELED); hideKeyboard-as-BACK on the PDS pages can rarely fire when the IME is already hidden and cancel the auth session (observed once — app recovered gracefully). Static: flutter test test/services test/providers 309 pass (incl. 4 new callback tests); analyze 536 = baseline. | 142bc05, 828fe48 | +| 2026-07-19 | RE-VERIFY G4/G5+F4 after backend fixes | PASS — G4 block/unblock full cycle on-device; G5 blocked-content surfaces; F4 avatar upload end-to-end incl. Save (twice) | Backend fixes confirmed from the mobile side: block events now consumed (block from feed-card "Post options" > confirm > "Blocked @…" snackbar > PDS record social.coves.actor.block written > refresh removes ALL test-aggregator posts from Discover), unblock endpoint returns 200 (was 404) — "Unblocked @…" snackbar and posts return after refresh; uploadBlob fixed (0feddc1): Me > Edit Profile > Change Avatar > gallery > uCrop > Save now succeeds ("Profile updated successfully", NO 403, NO sign-out, session intact), new 1000x1000 JPEG blob served by appview and rendered in the profile header after relaunch. Fixed (mobile): round-1 unblock design gap CLOSED — ProfileViewerState.fromJson only read blocked/blockUri but the backend sends viewer.blocking (record URI), and nothing seeded BlockProvider from profile data, so after a restart a blocked user's profile menu showed "Block" and NO unblock surface existed anywhere (feeds/comments now filter correctly); profile screen now seeds block state from viewer.blocking (seed-only, never clobbers optimistic state) → profile card menu shows "Unblock @handle" (79f3637, verified on-device pre/post-fix). G5: blocked-author post detail (deep link) renders clean "Post Unavailable — This post is from an account you've blocked" + Go Back, no crash (app-bar title says "Not Found" — minor copy nit); blocked author's comments are filtered OUT of threads entirely (no marker; seeded a test-aggregator comment on a mari post to exercise this — comment left in place as seed data, first non-mari comment author); blocked profile itself renders unfiltered by design. OPEN: thread header comment count includes blocked comments ("1 Comment" over an empty list — backend count vs filtered list, cosmetic); no "Blocked accounts" management list, so reaching a blocked user's profile to unblock still requires a direct route (deep link/mention) — product gap, mitigated by the profile-menu unblock. Suite: NEW g4_block_unblock (self-contained block>menu-flips-to-Unblock>unblock>restored; filtering not assertable in-flow — social.coves scheme routes to the OAuth CallbackActivity so Maestro openLink can't deep-link MainActivity); f4 extended through Save (repeated PASS). QUIRKS: card "Post options" index is per-screen order — index 1 after scrolling Rubin into view, guarded by asserting the menu names @test-aggregator before tapping; avatar shown immediately after save can be the PREVIOUS one (instant refetch races firehose indexing — appears on next profile load); test-aggregator PDS password is test-password-12345 (scripts/setup_dev_aggregator.go). Static: analyze 536 = baseline; test/models+providers+screens 368 pass (+4 new ProfileViewerState tests). End state: mari signed in on home feed, zero block records, avatar = QA test image. | 79f3637, ddd366d | +| 2026-07-19 | B — Home Feed (ROUND 2) | PASS — full-suite regression gate (all 28 ordered flows green across gate + post-fix re-runs) + deeper checks 2a–2d + queued cosmetic fix | Fixed (app): Join button reverting to "Join" despite subscribe 200 — the post-toggle community.get refetch races firehose indexing and still reports the pre-toggle viewer state; setInitialSubscriptionState clobbered the optimistic update (repro'd via d3: subscribe 200 at t, refetch at t → subscribed=false, "✓ Indexed subscription" at t+1s; DB row present while UI showed Join). Provider now keeps user-toggled communities authoritative for the session (f4b9fb6, d3 re-verified on-device). Fixed (app, queued cosmetic): blocked-post app bar said "Not Found" over a "Post Unavailable" body — NotFoundError hardcoded the app-bar title; now uses the passed title (9df9035, verified via block > deep-link > screenshot > unblock). Fixed (suite): flows OUTSIDE flowsOrder still execute at the end of a workspace run — g1 clearState wiped the session mid-suite and f5 ran without its deep-link precursor; both tagged `standalone` + config excludeTags (semantics proven on a synthetic workspace). Seed-post drift broke d2 (NASA delays below the fold → scrollUntilVisible), e4 (card peeking over the bottom edge → tap resolves to its off-screen centre and hits the bottom nav → centerElement, repro'd 3x then green), g4 hardened the same way pre-emptively. DEEPER CHECKS: 2a For You populates after subscribing to !science and empties to "No posts yet" after unsubscribing (committed as b6); 2b feed-like carries into detail, detail-unlike updates the feed card (committed as b7); 2c one scroll session renders text, image+external-link (kite.kagi.com bars), Streamable video, and the Bluesky-embed card (only seeded one is 175d deep — verified via deep link; butterfly card + nested link render clean); 2d fast double-tap on like never double-counts (VoteProvider pending guard swallows tap 2 in flight; on the fast local backend tap 2 can legitimately toggle off — either way score returns to exact baseline after refresh), 3x refresh spam + scroll-while-refreshing: no crash, no dupes. OPEN: a1's OAuth Custom Tab lingers in recents; back-exiting the app surfaces a dead "This sign-in session has expired" page (cosmetic, killed via force-stop com.android.chrome; consider closing the tab post-callback). OPEN (re-confirmed from round-1 G): once feeds/comments filter a blocked user there is NO in-app unblock surface (their thread comment is hidden entirely, count still says "1 Comment") — cleanup required deleting the block record via PDS as mari; "Blocked accounts" management screen still the product gap. FLAKES (documented, passed on retry): d4 blank-white launch 30s after signout (1x, transient); b7-style cold-start stale semantics (standard retry-launch wrapper added to b6/b7). Static: analyze 536 = baseline; test/widgets+providers 190 pass (loader tests updated for the title fix). End state: mari signed in on home feed, zero blocks (user+community), zero subscriptions. | f4b9fb6, 9df9035, 1a4b404 | -- 2.51.2