From 73ea3649048b7ba4558745a2fa0427854b847ddb Mon Sep 17 00:00:00 2001 From: Bretton <36870434+BrettM86@users.noreply.github.com> Date: Mon, 20 Jul 2026 01:34:22 -0700 Subject: [PATCH] test(qa): add c4/c5 deep-chain+wide flows, harden c1-c3, log round-2 C row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - c4_wide_level: 10-sibling wide level on the permanent deep-chain fixture — sibling cap (5) -> load-more reveals all 10 in order -> collapse hides all 10 (+10 badge) -> expand restores; asserts the wide level coexists with the deep chains in the same thread. - c5_deep_chain_focused: 20-level chain — tombstone-guarded load-more continues past the sibling cap, nesting through the depth-10 fetch boundary, three chained focused-thread screens to the depth-20 tail, back-stack returns to the detail. Both flows ran green twice. - c1-c3 hardened for seed growth: 60s feed scrolls, retry-launch (documented cold-start stale-semantics quirk), centerElement before tapping the NASA card (plain taps started landing on the author row and opening the profile instead of the post). - config.yaml: c4/c5 slotted after c3 in flowsOrder. - docs/QA_LOOP_LOG.md: ROUND 2 deep-chain row (final loop iteration), incl. the permanent fixture URI and open backend/product items. Co-Authored-By: Claude Fable 5 --- .maestro/c1_post_detail.yaml | 18 ++- .maestro/c2_comment_reply_collapse.yaml | 18 ++- .maestro/c3_comment_vote_sort.yaml | 3 +- .maestro/c4_wide_level.yaml | 89 ++++++++++++++ .maestro/c5_deep_chain_focused.yaml | 157 ++++++++++++++++++++++++ .maestro/config.yaml | 2 + docs/QA_LOOP_LOG.md | 1 + 7 files changed, 275 insertions(+), 13 deletions(-) create mode 100644 .maestro/c4_wide_level.yaml create mode 100644 .maestro/c5_deep_chain_focused.yaml diff --git a/.maestro/c1_post_detail.yaml b/.maestro/c1_post_detail.yaml index 6d006cf..d19b11c 100644 --- a/.maestro/c1_post_detail.yaml +++ b/.maestro/c1_post_detail.yaml @@ -6,11 +6,16 @@ # comment thread that includes deleted comments. appId: social.coves.dev --- -- launchApp: - stopApp: true -- extendedWaitUntil: - visible: "Discover feed.*" - timeout: 30000 +# Cold start can flake on stale semantics — retry the launch once +# (documented quirk, same pattern as g4). +- retry: + maxRetries: 2 + commands: + - launchApp: + stopApp: true + - extendedWaitUntil: + visible: "Discover feed.*" + timeout: 30000 - extendedWaitUntil: visible: "Feed post in [\\s\\S]*" timeout: 30000 @@ -19,7 +24,8 @@ appId: social.coves.dev element: text: "Feed post in [\\s\\S]*NASA delays[\\s\\S]*" direction: DOWN - timeout: 20000 + timeout: 60000 + centerElement: true - tapOn: text: "Feed post in [\\s\\S]*NASA delays[\\s\\S]*" index: 0 diff --git a/.maestro/c2_comment_reply_collapse.yaml b/.maestro/c2_comment_reply_collapse.yaml index 43d77de..cf9b666 100644 --- a/.maestro/c2_comment_reply_collapse.yaml +++ b/.maestro/c2_comment_reply_collapse.yaml @@ -8,11 +8,16 @@ # text selectors are always wrapped in [\s\S]. appId: social.coves.dev --- -- launchApp: - stopApp: true -- extendedWaitUntil: - visible: "Discover feed.*" - timeout: 30000 +# Cold start can flake on stale semantics — retry the launch once +# (documented quirk, same pattern as g4). +- retry: + maxRetries: 2 + commands: + - launchApp: + stopApp: true + - extendedWaitUntil: + visible: "Discover feed.*" + timeout: 30000 - extendedWaitUntil: visible: "Feed post in [\\s\\S]*" timeout: 30000 @@ -20,7 +25,8 @@ appId: social.coves.dev element: text: "Feed post in [\\s\\S]*NASA delays[\\s\\S]*" direction: DOWN - timeout: 20000 + timeout: 60000 + centerElement: true - tapOn: text: "Feed post in [\\s\\S]*NASA delays[\\s\\S]*" index: 0 diff --git a/.maestro/c3_comment_vote_sort.yaml b/.maestro/c3_comment_vote_sort.yaml index da3cd71..12bb065 100644 --- a/.maestro/c3_comment_vote_sort.yaml +++ b/.maestro/c3_comment_vote_sort.yaml @@ -16,7 +16,8 @@ appId: social.coves.dev element: text: "Feed post in [\\s\\S]*NASA delays[\\s\\S]*" direction: DOWN - timeout: 20000 + timeout: 60000 + centerElement: true - tapOn: text: "Feed post in [\\s\\S]*NASA delays[\\s\\S]*" index: 0 diff --git a/.maestro/c4_wide_level.yaml b/.maestro/c4_wide_level.yaml new file mode 100644 index 0000000..1b56426 --- /dev/null +++ b/.maestro/c4_wide_level.yaml @@ -0,0 +1,89 @@ +# Section C — check 7 (WIDE dimension): a single parent with 10 direct +# replies ("QA deep chain fixture" post, permanent seed: +# at://did:plc:7qijxb47zixbu3xqgdmruaui/social.coves.community.post/3mr2pxaxgq422). +# +# Verifies: +# - the per-parent sibling cap (5) truncates the wide level and shows the +# per-node "Load more replies" button (wired 2026-07-20 — it fetches the +# parentRkey subtree; before that fix the button was dead), +# - loading reveals ALL 10 siblings in correct (newest-first) order, +# - collapse of the wide parent hides all 10 (+10 badge), expand restores, +# - the wide level coexists with the deep chains in the same thread. +# +# The thread also contains a user-seeded 10-deep chain ("wide reply 10: +# horizontal direct replies" x10 — records are chain-parented) and the +# 20-deep alternating chain used by c5. +# Assumes a signed-in session (suite order: after a1/b-flows). +appId: social.coves.dev +--- +- launchApp: + stopApp: true +- extendedWaitUntil: + visible: "Discover feed.*" + timeout: 30000 +- extendedWaitUntil: + visible: "Feed post in [\\s\\S]*" + timeout: 30000 +- scrollUntilVisible: + element: + text: "Feed post in [\\s\\S]*QA deep chain fixture[\\s\\S]*" + direction: DOWN + timeout: 30000 +- tapOn: + text: "Feed post in [\\s\\S]*QA deep chain fixture[\\s\\S]*" + index: 0 +- extendedWaitUntil: + visible: "QA deep chain fixture[\\s\\S]*" + timeout: 15000 +# The user-seeded chain coexists at the top of the thread +- scrollUntilVisible: + element: + text: "[\\s\\S]*wide reply 10: horizontal direct replies[\\s\\S]*" + direction: DOWN + timeout: 30000 +# --- Wide level: capped at 5 of 10, newest-first --- +- scrollUntilVisible: + element: + text: "[\\s\\S]*wide reply 06 of 7[\\s\\S]*" + direction: DOWN + timeout: 60000 + centerElement: true +# Newest three are the topup replies 10/09/08 +- assertVisible: "[\\s\\S]*wide reply 09 of 10 \\(topup\\)[\\s\\S]*" +# The capped tail (01..05) is NOT rendered yet +- assertNotVisible: "[\\s\\S]*wide reply 01 of 7[\\s\\S]*" +- takeScreenshot: build/maestro/c4_wide_capped +# --- Load more replies reveals the remaining 5 siblings --- +- tapOn: + text: "Load more replies" + index: 0 +- extendedWaitUntil: + visible: "[\\s\\S]*wide reply 02 of 7[\\s\\S]*" + timeout: 20000 +- scrollUntilVisible: + element: + text: "[\\s\\S]*wide reply 01 of 7[\\s\\S]*" + direction: DOWN + timeout: 30000 +- takeScreenshot: build/maestro/c4_wide_all10 +# --- Collapse the wide parent: all 10 hidden behind the +10 badge --- +- scrollUntilVisible: + element: + text: "[\\s\\S]*WIDE parent: 7 direct replies below[\\s\\S]*" + direction: UP + timeout: 30000 + centerElement: true +- longPressOn: "[\\s\\S]*WIDE parent: 7 direct replies below[\\s\\S]*" +- extendedWaitUntil: + visible: "[\\s\\S]*expand thread[\\s\\S]*" + timeout: 10000 +- assertNotVisible: "[\\s\\S]*wide reply 10 of 10 \\(topup\\)[\\s\\S]*" +- assertNotVisible: "[\\s\\S]*wide reply 01 of 7[\\s\\S]*" +- takeScreenshot: build/maestro/c4_wide_collapsed +# --- Expand restores the wide level --- +- longPressOn: "[\\s\\S]*expand thread[\\s\\S]*" +- extendedWaitUntil: + visible: "[\\s\\S]*wide reply 10 of 10 \\(topup\\)[\\s\\S]*" + timeout: 10000 +- takeScreenshot: build/maestro/c4_wide_expanded +- back diff --git a/.maestro/c5_deep_chain_focused.yaml b/.maestro/c5_deep_chain_focused.yaml new file mode 100644 index 0000000..6500b62 --- /dev/null +++ b/.maestro/c5_deep_chain_focused.yaml @@ -0,0 +1,157 @@ +# Section C — checks 7 + 8 (DEEP dimension): the 20-level alternating +# mari/test-aggregator reply chain on the "QA deep chain fixture" post +# (permanent seed: +# at://did:plc:7qijxb47zixbu3xqgdmruaui/social.coves.community.post/3mr2pxaxgq422). +# +# Verifies: +# - "Load more replies" under "chain depth 01" continues the chain (the +# real depth-02 child sits past the sibling cap behind 6 deleted +# tombstones — regression for the dead load-more button fixed +# 2026-07-20 via parentRkey subtree fetch), +# - nesting renders in correct order through depth 10 (the API fetch-depth +# boundary) and on to depth 20 (max), with no overflow, +# - the focused thread screen ("Read N more replies") roots the subtree at +# the tapped comment, shows dimmed ancestors + highlighted anchor, and +# chains deeper focused screens to the chain tail, +# - back from the focused stack returns to the post detail. +# +# The flow first exhausts the WIDE parent's load-more so that the only +# remaining "Load more replies" button belongs to "chain depth 01" +# (index-0 tap stays unambiguous). +# Assumes a signed-in session (suite order: after c4). +appId: social.coves.dev +--- +- launchApp: + stopApp: true +- extendedWaitUntil: + visible: "Discover feed.*" + timeout: 30000 +- extendedWaitUntil: + visible: "Feed post in [\\s\\S]*" + timeout: 30000 +- scrollUntilVisible: + element: + text: "Feed post in [\\s\\S]*QA deep chain fixture[\\s\\S]*" + direction: DOWN + timeout: 30000 +- tapOn: + text: "Feed post in [\\s\\S]*QA deep chain fixture[\\s\\S]*" + index: 0 +- extendedWaitUntil: + visible: "QA deep chain fixture[\\s\\S]*" + timeout: 15000 +# --- Exhaust the WIDE parent's load-more first (disambiguation) --- +- scrollUntilVisible: + element: + text: "[\\s\\S]*wide reply 06 of 7[\\s\\S]*" + direction: DOWN + timeout: 60000 + centerElement: true +- tapOn: + text: "Load more replies" + index: 0 +- extendedWaitUntil: + visible: "[\\s\\S]*wide reply 02 of 7[\\s\\S]*" + timeout: 20000 +# --- Chain depth 01: load-more continues the chain past the tombstones --- +- scrollUntilVisible: + element: + text: "[\\s\\S]*chain depth 01 \\(mari\\)[\\s\\S]*" + direction: DOWN + timeout: 60000 + centerElement: true +- scrollUntilVisible: + element: + text: "Load more replies" + direction: DOWN + timeout: 30000 +- tapOn: + text: "Load more replies" + index: 0 +- extendedWaitUntil: + visible: "[\\s\\S]*chain depth 02 \\(agg\\)[\\s\\S]*" + timeout: 20000 +- takeScreenshot: build/maestro/c5_chain_loaded +# Nesting renders in order to the render cap, then continues via the +# focused thread screen +- scrollUntilVisible: + element: + text: "[\\s\\S]*chain depth 07 \\(mari\\)[\\s\\S]*" + direction: DOWN + timeout: 60000 +- scrollUntilVisible: + element: + text: "Read 1 more reply" + direction: DOWN + timeout: 30000 +- tapOn: + text: "Read 1 more reply" + index: 0 +# --- Focused screen 1: anchor 07, replies 08..13 (depth 10 included) --- +- extendedWaitUntil: + visible: "[\\s\\S]*chain depth 08 \\(agg\\)[\\s\\S]*" + timeout: 20000 +# Depth 10 — the API fetch-depth boundary — renders inside the subtree +- scrollUntilVisible: + element: + text: "[\\s\\S]*chain depth 10 \\(agg\\)[\\s\\S]*" + direction: DOWN + timeout: 30000 +- takeScreenshot: build/maestro/c5_focused_depth10 +- scrollUntilVisible: + element: + text: "[\\s\\S]*chain depth 13 \\(mari\\)[\\s\\S]*" + direction: DOWN + timeout: 30000 +- scrollUntilVisible: + element: + text: "Read 1 more reply" + direction: DOWN + timeout: 30000 +- tapOn: + text: "Read 1 more reply" + index: 0 +# --- Focused screen 2: continues 14..19 --- +- extendedWaitUntil: + visible: "[\\s\\S]*chain depth 14 \\(agg\\)[\\s\\S]*" + timeout: 20000 +- scrollUntilVisible: + element: + text: "[\\s\\S]*chain depth 19 \\(mari\\)[\\s\\S]*" + direction: DOWN + timeout: 30000 +- scrollUntilVisible: + element: + text: "Read 1 more reply" + direction: DOWN + timeout: 30000 +- tapOn: + text: "Read 1 more reply" + index: 0 +# --- Focused screen 3: the chain tail (depth 20) --- +- extendedWaitUntil: + visible: "[\\s\\S]*chain depth 20 \\(agg\\)[\\s\\S]*" + timeout: 20000 +- takeScreenshot: build/maestro/c5_chain_tail +# --- Back through the focused stack to the post detail --- +- back +- extendedWaitUntil: + visible: "[\\s\\S]*chain depth 19 \\(mari\\)[\\s\\S]*" + timeout: 15000 +- back +- extendedWaitUntil: + visible: "[\\s\\S]*chain depth 13 \\(mari\\)[\\s\\S]*" + timeout: 15000 +- back +# Back on the post detail at the previous position (deep in the chain) — +# the chain is still rendered; scroll up to prove we're on the right post. +- extendedWaitUntil: + visible: "[\\s\\S]*chain depth 0[0-9] \\((mari|agg)\\)[\\s\\S]*" + timeout: 15000 +- scrollUntilVisible: + element: + text: "QA deep chain fixture[\\s\\S]*" + direction: UP + timeout: 60000 +- takeScreenshot: build/maestro/c5_back_on_detail +- back diff --git a/.maestro/config.yaml b/.maestro/config.yaml index 0546fd9..5afd8ec 100644 --- a/.maestro/config.yaml +++ b/.maestro/config.yaml @@ -35,6 +35,8 @@ executionOrder: - c1_post_detail - c2_comment_reply_collapse - c3_comment_vote_sort + - c4_wide_level + - c5_deep_chain_focused - d1_admin_panel - d2_community_from_chip - d3_join_leave diff --git a/docs/QA_LOOP_LOG.md b/docs/QA_LOOP_LOG.md index 3ee7a30..25ab005 100644 --- a/docs/QA_LOOP_LOG.md +++ b/docs/QA_LOOP_LOG.md @@ -16,3 +16,4 @@ Each iteration appends one row. `OPEN:` items are unfixed bugs for later pickup. | 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 | +| 2026-07-20 | C — Post Detail & Comments (ROUND 2 deep-chain, final loop iteration) | PASS — checks 7+8 (deep chains + focused thread) with the headline load-more bug FOUND+FIXED; c1–c3 regression green; NEW c4/c5 committed (2x stable each) | PERMANENT FIXTURE (do not delete): "QA deep chain fixture" post at://did:plc:7qijxb47zixbu3xqgdmruaui/social.coves.community.post/3mr2pxaxgq422 in !science — 20-level alternating mari/test-aggregator chain (now 21 after the in-flow reply test), a WIDE parent with 10 direct replies (7 seeded + 3 topups 08–10 added this run), 6 deleted tombstones under chain depth 01 (exercise the sibling cap + deleted placeholders), and a user-added 10-deep chain (see below). HEADLINE FIX: the per-comment "Load more replies" button had NO handler anywhere (CommentThread.onLoadMoreReplies was never passed by any screen — logcat: "Load more replies tapped (no handler provided)"), so replies past the per-parent sibling cap (5) or fetch depth (10) were unreachable: chain depths 2–20 were invisible dead data. Wired end-to-end via the lexicon's getComments parentRkey subtree fetch: provider merges the subtree into the tree (new ThreadViewComment.copyWith/replaceDescendant), per-node loading spinner, snackbar on failure; createComment now hydrates the parent subtree when a nested reply lands past the caps; ALSO fixed en route: at:// URIs cannot be Uri.parse()d (DID colons parse as an invalid port — caught by the new provider tests). FOCUSED THREAD (check 8): screen held an immutable snapshot — replies posted inside it never appeared, subtrees stayed truncated at the parent fetch depth; now hydrates the anchor's live subtree on entry and re-fetches after reply/delete. Verified on-device: chain 01→20 fully browsable (load-more → depth 6 render cap → "Read 1 more reply" → 3 chained focused screens to the tail), depth-21 reply posted from INSIDE the deepest focused view appears immediately at correct depth, back-stack returns through each focused screen to the detail at position, cold-start deep link renders the thread (no crash), collapse/expand verified at depth 1 and depth 10 (+1/+10 badges), indent rails clean at all depths, fling scroll through the loaded chain: 0 janky frames (gfxinfo window). QUEUED FIX: header no longer shows "1 Comment" over an empty list — when the thread loads successfully and every comment is filtered (blocked author), the empty state renders instead (verified via block → refresh → empty state → unblock → count restored, incl. profile-menu Unblock). USER SEED INVESTIGATION (mid-run request): the 10 "wide reply 10: horizontal direct replies" records are chain-parented IN THE PDS RECORDS (each reply.parent = the previous comment, first = post, ~3s apart) — the app renders them faithfully as a 10-deep chain; not an app bug (the post composer always parents to the post; ReplyScreen parents to the tapped comment) — looks like scripted seeding that reused each create response as the next parent. Wide intent honored by topping the WIDE parent up to 10 true siblings; both dimensions verified coexisting in one thread and codified in c4 (wide: cap → load-more → all 10 → collapse +10) + c5 (deep: tombstone-guarded load-more → depth 10 boundary → focused stack to depth 20 → back-stack). OPEN (backend): post stats commentCount counts deleted + viewer-filtered comments (fixture shows 48 incl. 6 tombstones; partially-filtered threads still overcount — viewer-aware stats needed). OPEN (backend, cosmetic): stats.replyCount is direct-children only, so collapsing a 9-descendant chain shows "+1". OPEN (product): no deep-link/permalink route to a comment or focused thread (only /post/:postUri) — check 8's cold-load verified as post cold-load + in-app navigation. SUITE: c1–c3 hardened for seed growth (60s feed scroll, retry-launch on c1/c2, centerElement before the NASA card tap — a plain tap started landing on the author row → profile). QUIRK: long-press collapse on a comment whose tap target shifted mid-test can open ReplyScreen; a long-press in the composer then triggers the Android stylus-IME promo sheet (dismiss with Cancel, not BACK). Static: analyze 535 (baseline 536; removed an orphaned generated mock file test/providers/feed_provider_test.mocks.dart whose source test no longer exists), flutter test test/widgets+services+providers 414 pass (+3 new loadMoreReplies tests, nested-reply test now asserts subtree hydration). End state: mari signed in on home feed, aggregator unblocked, fixture extended (permanent). | ab1d775, a2c8a58, (flows commit follows) | -- 2.51.2