TASK-034: fix favorite star count not incrementing after a successful star
toggleFavorite's ALL-list success branch updated favoriteAction to Starred but never touched starsCount, so a row kept showing its pre-star count even after the server had actually accepted it. Now increments starsCount by 1 in the same update. Also guards against a repeat tap on an already-Starred row (the ALL list can't tell a repo's already-starred state on load, so the star stays tappable) double-incrementing the displayed count. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
b38ddc99e3
commit
6dbb37d067
@ -284,13 +284,24 @@ class RepositoryListViewModel(
|
||||
fun toggleFavorite(repository: RepositoryRowUiModel) {
|
||||
val content = mutableState.value as? RepositoryListUiState.Content ?: return
|
||||
val mode = content.mode
|
||||
// On the ALL list, a repeat tap after a successful star would otherwise re-call star()
|
||||
// (harmless server-side -- PUT is idempotent) and, before this guard, double-increment the
|
||||
// client-displayed count each time. Once this session has confirmed a star succeeded,
|
||||
// further taps here are a no-op; unstarring from the ALL list isn't supported (see the
|
||||
// FavoriteStar doc comment in RepositoryListScreen.kt for why).
|
||||
if (mode == RepositoryListMode.ALL && repository.favoriteAction == FavoriteActionUiState.Starred) return
|
||||
updateRow(repository.coordinates) { it.copy(favoriteAction = FavoriteActionUiState.InProgress) }
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
when (mode) {
|
||||
RepositoryListMode.ALL -> {
|
||||
favoritesApi.star(repository.coordinates)
|
||||
updateRow(repository.coordinates) { it.copy(favoriteAction = FavoriteActionUiState.Starred) }
|
||||
// starsCount only ever reflects what the last full list load returned
|
||||
// otherwise -- a successful star here needs its own +1, or the row keeps
|
||||
// showing the pre-star count until the next reload.
|
||||
updateRow(repository.coordinates) {
|
||||
it.copy(favoriteAction = FavoriteActionUiState.Starred, starsCount = it.starsCount + 1)
|
||||
}
|
||||
}
|
||||
RepositoryListMode.FAVORITES -> {
|
||||
favoritesApi.unstar(repository.coordinates)
|
||||
|
||||
@ -338,6 +338,40 @@ class RepositoryListViewModelTest {
|
||||
assertEquals(listOf(RepositoryCoordinates.of("owner", "repo")), browser.starredCalls)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `starring a repository increments its displayed star count`() = runTest(dispatcher.scheduler) {
|
||||
val browser = FakeBrowser(repositories = listOf(page1Of(repo("repo", starsCount = 4))))
|
||||
val viewModel = viewModel(browser)
|
||||
advanceUntilIdle()
|
||||
|
||||
val row = (viewModel.state.value as RepositoryListUiState.Content).repositories.single()
|
||||
assertEquals(4, row.starsCount)
|
||||
viewModel.toggleFavorite(row)
|
||||
advanceUntilIdle()
|
||||
|
||||
val updated = (viewModel.state.value as RepositoryListUiState.Content).repositories.single()
|
||||
assertEquals(5, updated.starsCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a repeat tap after a successful star is a no-op, not a second star call or count increment`() = runTest(dispatcher.scheduler) {
|
||||
val browser = FakeBrowser(repositories = listOf(page1Of(repo("repo"))))
|
||||
val viewModel = viewModel(browser)
|
||||
advanceUntilIdle()
|
||||
|
||||
val row = (viewModel.state.value as RepositoryListUiState.Content).repositories.single()
|
||||
viewModel.toggleFavorite(row)
|
||||
advanceUntilIdle()
|
||||
val afterFirstTap = (viewModel.state.value as RepositoryListUiState.Content).repositories.single()
|
||||
|
||||
viewModel.toggleFavorite(afterFirstTap)
|
||||
advanceUntilIdle()
|
||||
|
||||
val afterSecondTap = (viewModel.state.value as RepositoryListUiState.Content).repositories.single()
|
||||
assertEquals(1, afterSecondTap.starsCount)
|
||||
assertEquals(listOf(RepositoryCoordinates.of("owner", "repo")), browser.starredCalls)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unstarring from the favorites tab removes the row without affecting others`() = runTest(dispatcher.scheduler) {
|
||||
val browser = FakeBrowser(
|
||||
@ -480,13 +514,14 @@ class RepositoryListViewModelTest {
|
||||
|
||||
private fun page1Of(vararg repositories: RepositorySummary) = Page.of(repositories.toList(), null)
|
||||
|
||||
private fun repo(name: String, updatedAtEpochMillis: Long = 1_000L) = RepositorySummary(
|
||||
private fun repo(name: String, updatedAtEpochMillis: Long = 1_000L, starsCount: Int = 0) = RepositorySummary(
|
||||
coordinates = RepositoryCoordinates.of("owner", name),
|
||||
description = null,
|
||||
defaultBranch = RepositoryRef.of("main"),
|
||||
isPrivate = true,
|
||||
isFork = false,
|
||||
updatedAtEpochMillis = updatedAtEpochMillis,
|
||||
starsCount = starsCount,
|
||||
)
|
||||
|
||||
private fun viewModel(
|
||||
|
||||
@ -566,3 +566,17 @@ This is the append-only implementation and handoff record shared by Codex and Cl
|
||||
- Known limitations: the device's current session still carries scope from an earlier attempt in this same investigation — needs one more revoke (Gitea Settings → Applications → Authorized OAuth2 Applications) and re-login before starring can be confirmed working. Not yet done — the human product owner was tired and this was left for a later session rather than pushed further tonight.
|
||||
- Next recommended task: revoke + re-login when convenient, then confirm starring actually works; otherwise continue per the standing direction on everything else still queued from this session (TASK-030/031/032, pinch-zoom/pan, CSV, PDF, Issues/PR pagination).
|
||||
- Next owner: Claude, continuing per the standing direction.
|
||||
|
||||
## 2026-08-29 — TASK-034: favorite star count doesn't increment after a successful star
|
||||
|
||||
- Agent and role: Claude, direct continuation of the same live session — the human product owner confirmed TASK-033's fix worked ("별표 눌러서 즐겨찾기는 되는데") but reported the count itself doesn't update ("카운팅은 안올라가네").
|
||||
- Commit/diff: on `claude/task-015-notifications`; `feature/repositories/RepositoryListViewModel.kt` (+ test additions), `tasks/034-favorite-star-count-not-incrementing.md`.
|
||||
- Bug found: `toggleFavorite`'s ALL-list success branch updated only `favoriteAction` to `Starred`, never `starsCount` — the row kept showing whatever count the last full list load had returned, even after a star the server had actually accepted.
|
||||
- How it was implemented: incremented `starsCount` by 1 in the same state update that marks the row `Starred` (optimistic, since Gitea's star endpoint returns 204 with no body — a second network call just to re-fetch one number wasn't worth it). While fixing this, also closed a related gap: since the ALL list can't know a repository's already-starred state on initial load (an existing, accepted limitation from TASK-018/TASK-030), a `Starred` row's star stayed tappable, and a repeat tap would have re-called `star()` and double-incremented the displayed count each time — added an early-return guard so a repeat tap on an already-`Starred` row is now a no-op.
|
||||
- Files changed: `RepositoryListViewModel.kt`, `RepositoryListViewModelTest.kt`, `tasks/034-favorite-star-count-not-incrementing.md`.
|
||||
- Checks and results: `./gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug` BUILD SUCCESSFUL, all unit tests passing (new coverage: starring increments displayed count by 1; a repeat tap after a successful star is a no-op), 0 lint errors. Installed on the human product owner's device over wireless `adb`.
|
||||
- Also this turn: the `task-015-notifications` worktree directory itself had been removed between sessions (its branch and commits were intact); recreated it with `git worktree add`, plus a fresh `local.properties` (gitignored, not tracked) pointing at the local Android SDK, before continuing.
|
||||
- Review findings and resolutions: None from a separate review pass.
|
||||
- Known limitations: still optimistic, not authoritative — if Gitea's real count ever differs from this app's local +1 (e.g. a concurrent star from another client), the display stays off until the next full list reload. The underlying "can't tell if already starred on load" limitation is unchanged; this only stops it from causing a visible count bug.
|
||||
- Next recommended task: get live confirmation that the count now increments on tap; otherwise continue per the standing direction on everything else still queued from this session.
|
||||
- Next owner: Claude, continuing per the standing direction.
|
||||
|
||||
88
tasks/034-favorite-star-count-not-incrementing.md
Normal file
88
tasks/034-favorite-star-count-not-incrementing.md
Normal file
@ -0,0 +1,88 @@
|
||||
# TASK-034: Star count doesn't increment after a successful favorite
|
||||
|
||||
Status: implemented; all required checks pass; live-installed, awaiting the human product owner's confirmation
|
||||
Owner: Claude
|
||||
Reviewer: Codex
|
||||
|
||||
## Outcome
|
||||
|
||||
Following TASK-033's OAuth scope fix, the human product owner confirmed starring itself now works
|
||||
("별표 눌러서 즐겨찾기는 되는데") but reported the displayed star count doesn't go up
|
||||
("카운팅은 안올라가네"). `toggleFavorite`'s ALL-list success branch updated only `favoriteAction`
|
||||
(to `Starred`) and never touched `starsCount`, so a row kept showing whatever count the last full
|
||||
list load had returned, even after a star that the server had actually accepted. Fixed by
|
||||
incrementing the row's `starsCount` in the same state update that marks it `Starred`. While fixing
|
||||
this, found and closed a related gap: a repeat tap on an already-`Starred` row (the ALL list has no
|
||||
way to know a repository's already-starred state on initial load, so the star stays tappable) would
|
||||
have re-called `star()` and double-incremented the client-displayed count each time — now a no-op.
|
||||
|
||||
## Scope
|
||||
|
||||
- Included: `feature/repositories/RepositoryListViewModel.kt` (`toggleFavorite` only), matching
|
||||
test additions in `RepositoryListViewModelTest.kt`.
|
||||
- Excluded: No change to the FAVORITES-list unstar path (removing the row entirely already made
|
||||
its count irrelevant, no bug there). No change to how the initial list load populates
|
||||
`starsCount` (that already came from Gitea's own response, unaffected). No attempt to fetch the
|
||||
server's authoritative post-star count instead of incrementing client-side (see Design notes).
|
||||
|
||||
## Design notes
|
||||
|
||||
- **Optimistic +1, not a re-fetch**: Gitea's star endpoint (`PUT`, 204 No Content) doesn't return
|
||||
the new count, so getting the authoritative number back would mean a second network call
|
||||
(re-fetching the repository) purely to update one number — not worth it for a value the app
|
||||
already knows how to derive from something it just confirmed itself (the star succeeded, so the
|
||||
count is exactly one higher than what was last displayed).
|
||||
- **The repeat-tap guard closes a real double-count bug, not just a hypothetical one**: because the
|
||||
ALL list can't determine a repository's already-starred state on load (an existing, accepted
|
||||
limitation from TASK-018/TASK-030), a `Starred` row's star icon stays enabled and tappable —
|
||||
nothing before this fix stopped a second tap from calling `star()` again and incrementing the
|
||||
displayed count a second time for a repository the user only starred once. The guard checks the
|
||||
row's own `favoriteAction` before doing anything, so it also saves a redundant network call, not
|
||||
just a redundant `+1`.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] Starring a repository from the ALL list increments its displayed star count by exactly 1.
|
||||
- [x] Tapping an already-`Starred` row again does not call `star()` a second time and does not
|
||||
change the displayed count further.
|
||||
- [x] Unstarring from the FAVORITES list is unaffected (row removal, no count to update).
|
||||
- [x] Build, unit tests, and lint pass.
|
||||
- [ ] Live confirmation from the human product owner that the count now visibly increments on tap —
|
||||
installed on the device but not yet re-tested in this exact session.
|
||||
|
||||
## Required checks
|
||||
|
||||
```text
|
||||
./gradlew compileDebugKotlin compileDebugUnitTestKotlin
|
||||
./gradlew testDebugUnitTest
|
||||
./gradlew assembleDebug lintDebug
|
||||
```
|
||||
|
||||
## Evidence
|
||||
|
||||
- `RepositoryListViewModel.kt`: `toggleFavorite`'s ALL-list success branch now updates
|
||||
`starsCount = it.starsCount + 1` alongside `favoriteAction = Starred`; new early-return guard
|
||||
when the row's `favoriteAction` is already `Starred`.
|
||||
- `RepositoryListViewModelTest.kt`: `repo()` test helper gained a `starsCount` parameter; two new
|
||||
tests — starring increments the displayed count by 1; a repeat tap after a successful star is a
|
||||
no-op (no second `star()` call, count stays at 1).
|
||||
- `./gradlew compileDebugKotlin compileDebugUnitTestKotlin`: BUILD SUCCESSFUL.
|
||||
- `./gradlew testDebugUnitTest`: BUILD SUCCESSFUL, all unit tests passing (including the two new
|
||||
ones above).
|
||||
- `./gradlew assembleDebug lintDebug`: BUILD SUCCESSFUL, 0 lint errors.
|
||||
- Installed on the human product owner's device over wireless `adb` (`adb install -r`, streamed
|
||||
install succeeded); not yet re-tested live in this exact session.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Still optimistic, not authoritative: if Gitea's real count differs from this app's local
|
||||
increment for any reason (e.g. another client starred the same repo around the same time), the
|
||||
displayed number stays off until the next full list reload. Considered an acceptable tradeoff
|
||||
given the alternative (a second network call per star) for a rarely-contended personal count.
|
||||
- The underlying "can't tell if already starred on initial load" limitation (TASK-018/TASK-030)
|
||||
is unchanged — this task only stops it from causing a *visible count* bug, it doesn't resolve the
|
||||
limitation itself.
|
||||
|
||||
## Review findings
|
||||
|
||||
None yet — pending Codex/human review.
|
||||
Loading…
x
Reference in New Issue
Block a user