From 5e414a663a099df774dbc9da2af435d38363f15f Mon Sep 17 00:00:00 2001 From: choiminyeong Date: Wed, 26 Aug 2026 21:33:07 +0900 Subject: [PATCH] fix: use saveable repository list keys --- .../repositories/RepositoryListScreen.kt | 5 ++- .../repositories/RepositoryListScreenTest.kt | 23 +++++++++++ docs/development-log.md | 8 ++++ tasks/020-repository-list-saveable-key.md | 38 +++++++++++++++++++ 4 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 app/src/test/java/com/ubisam/ubgitea/feature/repositories/RepositoryListScreenTest.kt create mode 100644 tasks/020-repository-list-saveable-key.md diff --git a/app/src/main/java/com/ubisam/ubgitea/feature/repositories/RepositoryListScreen.kt b/app/src/main/java/com/ubisam/ubgitea/feature/repositories/RepositoryListScreen.kt index f08d9b9..4f04c1d 100644 --- a/app/src/main/java/com/ubisam/ubgitea/feature/repositories/RepositoryListScreen.kt +++ b/app/src/main/java/com/ubisam/ubgitea/feature/repositories/RepositoryListScreen.kt @@ -223,7 +223,7 @@ private fun RepositoryContent( if (state.mode == RepositoryListMode.ALL && state.organizations.isNotEmpty()) { item { OrganizationFilterRow(state.organization, state.organizations, onSelectOrganization) } } - items(state.repositories, key = { it.coordinates }) { repository -> + items(state.repositories, key = { repositoryLazyListKey(it.coordinates) }) { repository -> Column( Modifier .fillMaxWidth() @@ -265,6 +265,9 @@ private fun RepositoryContent( } } +internal fun repositoryLazyListKey(coordinates: RepositoryCoordinates): String = + "${coordinates.owner}/${coordinates.name}" + @Composable private fun OrganizationFilterRow( selected: String?, diff --git a/app/src/test/java/com/ubisam/ubgitea/feature/repositories/RepositoryListScreenTest.kt b/app/src/test/java/com/ubisam/ubgitea/feature/repositories/RepositoryListScreenTest.kt new file mode 100644 index 0000000..4f1249b --- /dev/null +++ b/app/src/test/java/com/ubisam/ubgitea/feature/repositories/RepositoryListScreenTest.kt @@ -0,0 +1,23 @@ +package com.ubisam.ubgitea.feature.repositories + +import com.ubisam.ubgitea.domain.model.RepositoryCoordinates +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test + +class RepositoryListScreenTest { + @Test + fun `lazy list key is a stable saveable string`() { + val coordinates = RepositoryCoordinates.of("team", "mobile") + + assertEquals("team/mobile", repositoryLazyListKey(coordinates)) + } + + @Test + fun `lazy list keys distinguish the same repository name under different owners`() { + val first = repositoryLazyListKey(RepositoryCoordinates.of("team-a", "mobile")) + val second = repositoryLazyListKey(RepositoryCoordinates.of("team-b", "mobile")) + + assertNotEquals(first, second) + } +} diff --git a/docs/development-log.md b/docs/development-log.md index 1a51435..ee2bfcb 100644 --- a/docs/development-log.md +++ b/docs/development-log.md @@ -305,3 +305,11 @@ This is the append-only implementation and handoff record shared by Codex and Cl - Known limitations: Physical-device callback verification is still required. Gitea star/unstar mutation is intentionally unavailable with the restored read-only token. - Next recommended task: Install the rebuilt debug APK, clear UBGitea app data once, complete login approval, and verify repository loading. - Next owner: Human product owner for device verification, then Claude for review. + +## 2026-08-26 — TASK-020 repository-list crash fix + +- Agent and role: Codex, incident resolver. +- Root cause: The repository `LazyColumn` used `RepositoryCoordinates` as its item key. Compose passes lazy-item keys to Android saved state, but this domain object is not a `Bundle`-saveable type, causing an uncaught main-thread `IllegalArgumentException` as soon as authenticated rows rendered. +- Implementation: Repository rows now use the stable string key `owner/name`; repository components cannot contain `/`, so the key remains unique across owners and repositories while being Android-saveable. +- Regression coverage: Added tests for deterministic key encoding and for equal repository names under different owners. +- Checks: Focused tests and `assembleDebug testDebugUnitTest lintDebug` passed. The APK installed wirelessly over the authenticated device, `MainActivity` remained resumed, and the cleared crash buffer stayed empty. Final visual inspection is pending because the handset locked. diff --git a/tasks/020-repository-list-saveable-key.md b/tasks/020-repository-list-saveable-key.md new file mode 100644 index 0000000..bab8f36 --- /dev/null +++ b/tasks/020-repository-list-saveable-key.md @@ -0,0 +1,38 @@ +# TASK-020: Prevent repository-list crash after login + +Status: implemented; unlocked-screen confirmation pending +Owner: Codex +Reviewer: Claude + +## Outcome + +After a successful OAuth login, the repository list renders without terminating the app, and subsequent launches remain usable. + +## Risk + +Compose lazy-list item keys participate in saved UI state on Android. A key that cannot be stored in a `Bundle` throws on the main thread while the list is composed. + +## Writable paths + +- `app/src/main/java/com/ubisam/ubgitea/feature/repositories/RepositoryListScreen.kt` +- matching unit tests +- this task file and `docs/development-log.md` + +## Acceptance criteria + +- [x] Repository rows use a unique, stable, Android-saveable key. +- [x] A regression test covers repositories with the same name under different owners. +- [x] Debug build, unit tests, and Android lint pass. +- [ ] The rebuilt APK launches and renders the authenticated repository list on the physical device. +- [x] Repository secret-pattern scan reports no credential material. + +## Incident evidence + +Physical-device crash buffer on 2026-08-26 reported `IllegalArgumentException: Type of the key RepositoryCoordinates(...) is not supported` from `SaveableStateHolderImpl.SaveableStateProvider`, originating while the repository `LazyColumn` was composed. + +## Verification evidence + +- Focused regression tests passed. +- `assembleDebug testDebugUnitTest lintDebug` passed. +- The rebuilt APK installed over the existing authenticated installation successfully; the app process remained alive with `MainActivity` resumed and the post-launch crash buffer empty. +- The handset locked before the rendered repository rows could be inspected, so visual confirmation remains pending.