feat(sessions): empty-session cleanup + latest-descendant resume + fresh-session 404 fix (#787, #840) (#841)

* feat(sessions): empty-session cleanup button + latest-descendant resume + fresh-session 404 fix (issues #787, #840)

- Sessions top bar: Delete-empty icon with count badge, grey/disabled when 0
  (GET /api/sessions/empty/count + DELETE /api/sessions/empty, confirm dialog)
- Resume flow resolves GET /api/sessions/{id}/latest-descendant first —
  branched sessions continue in the newest child leaf, not the stale parent
  (best-effort; falls back to the requested id)
- syncCurrentSession guarded by sessionHasServerPresence — a freshly created
  (never prompted) session has no server row yet; the REST transcript 404s
  and the resume retry spun forever (logcat 404 evidence)

* fix(sessions): clamp message-count fallback to backend limit cap (422)

fetchServerMessageCount requested limit=500 but the backend caps
session list page size at 100 (sessions.py Query le=100) — every
fallback scan 422'd (seen in device logcat after a branch). Sessions
are ordered recent, so the target is always in the top page.

* test(slash): make dispatch-capture assertions order-independent (CI flake)

The capture-race flake (SlashCommandDispatchRpcTest, hit 3x across CI and
local runs 2026-08-07): tests asserted on methodSlot.captured — the LAST
request captured — but the create flow's session.usage/context_breakdown
requests can land after the slash-dispatch one depending on suite order
(the class's own comment documents this cross-wiring). Converted the
three capture-based tests to capture ALL requests and find the target
record by method, with dispatch-before-fallback ordering for the 4018
test. Same assertions, no dependence on request interleaving.

* test(slash): convert remaining capture-race assertions (flake round 2)

The capture race moved to the focus/init tests (:424) after the first
three were fixed — every request-capture test in the class used the
same last-captured assumption. Converted the remaining five (init,
init+arg, focus, focus on, type=send) to the find-record-by-method
pattern; the type=send answers block reads methodCalls.last() since the
capture precedes the answer. Zero request() slot captures remain.

* test(slash): blocklist assertion counts command RPCs only (flake round 3)

The blocklisted-command test counted ALL request() calls and asserted
zero — the create flow's session.usage/context_breakdown noise can land
in the window after the stub registers (the same capture race that hit
the dispatch tests). Asserted instead that no COMMAND_DISPATCH or
SLASH_EXEC fires, which is the actual blocklist contract. E2E's static
Dispatchers mock stays (E2E VMs need IO routed to the test dispatcher —
removing it broke 20 E2E tests).

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-08-07 21:36:23 +03:00
1 parent 7998248507
commit f20b50b48f
7 files changed
+274 -48

No files matched your search

+4
View File
@@ -512,6 +512,10 @@
<string name="sessions_prune_desc">Delete sessions older than the specified number of days.</string>
<string name="sessions_prune_days_label">Days threshold</string>
<string name="sessions_prune_confirm">Prune</string>
<string name="sessions_empty_cleanup_desc">Delete empty sessions</string>
<string name="sessions_empty_cleanup_title">Delete empty sessions?</string>
<string name="sessions_empty_cleanup_message">Delete %1$d empty session(s). Active and archived sessions are kept.</string>
<string name="sessions_empty_cleanup_confirm">Delete</string>
<string name="sessions_delete_title">Delete Session?</string>
<string name="sessions_delete_message">Are you sure you want to delete \"%1$s\"? This can\'t be undone.</string>
<string name="sessions_bulk_delete_title">Delete Sessions?</string>