Ubisam-MobileGit/handoff/2026-09-28_unit-tests-redesign.md
choiminyeong d1a45abc16 TASK-050..054: tabs, Home, Work, code viewer, C++ pairs, fold layouts
Four tabs in an app shell that becomes a rail from 600dp, a brighter sky-blue palette with the real UbiSam logo, a Home tab built on Gitea's activity feeds, a Work tab that searches issues and pull requests across repositories, a full-screen code viewer with search, outline and Visual Studio colors, C/C++ header/source pairing, and two-pane layouts for wide windows.

The five tasks were built and checked together, so they are one commit: MainActivity, the routes and the shared components belong to all of them. 409 unit tests pass, lint reports 0 errors. Verified on an emulator up to the sign-in screen only; no physical device was available.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 14:35:00 +09:00

114 lines
11 KiB
Markdown

# 작업: 리디자인 신규 로직 단위 테스트 작성
## 목표
- 리디자인(TASK-050~054)으로 새로 생긴 순수 로직과 API 클래스에 JUnit4 단위 테스트를 작성하고, 기존 테스트 중 동작 변경으로 깨진 것을 고친다.
## 배경
- 작업 위치: `D:\Android\UBGitea\.claude\worktrees\awesome-pascal-3f2ada` (git 워크트리). **이 폴더 안에서만 작업한다.** `D:\Android\UBGitea` 본 폴더는 건드리지 않는다.
- Android 앱(Kotlin, Jetpack Compose). 테스트는 `app/src/test/java/com/ubisam/ubgitea/` 아래 JVM 단위 테스트(JUnit4, kotlinx-coroutines-test `runTest`).
- Gradle 실행 전 반드시: `export JAVA_HOME="/d/AndroidStudio/jbr"` (Git Bash 기준).
- 테스트 실행 예: `./gradlew testDebugUnitTest --tests "com.ubisam.ubgitea.feature.home.*" --console=plain -q`
- 다른 작업자가 같은 폴더에서 화면 파일 4개를 동시에 고치고 있다(아래 "건드리지 말 것"). 컴파일 오류가 그 파일들에서 나면 내 잘못이 아니다. 2분 기다렸다가 다시 실행한다.
## 결정사항 (변경 금지)
- 메인 소스(`app/src/main/`)의 동작은 바꾸지 않는다. 테스트가 실패하면 먼저 테스트가 틀렸는지 본다. 메인 코드의 버그라고 판단되면 고치지 말고 보고서에 적는다.
- 테스트 스타일은 기존 파일을 따른다. 참고: `app/src/test/java/com/ubisam/ubgitea/data/notifications/GiteaNotificationsApiTest.kt` (API 테스트, 파일 안에 private `QueueTransport` fake 포함), `app/src/test/java/com/ubisam/ubgitea/feature/codebrowser/content/SyntaxHighlighterTest.kt`.
- 새 의존성 추가 금지. Robolectric, Compose UI 테스트 금지. Composable 함수는 테스트하지 않는다.
- 테스트 이름은 기존처럼 백틱 문장형 또는 camelCase 중 그 파일이 쓰는 방식을 따른다.
## 수정/생성할 파일
- `app/src/test/java/com/ubisam/ubgitea/feature/codebrowser/content/SyntaxHighlighterTest.kt` : 수정 (깨진 기대값)
- `app/src/test/java/com/ubisam/ubgitea/feature/codebrowser/content/SymbolOutlineTest.kt` : 신규
- `app/src/test/java/com/ubisam/ubgitea/feature/codebrowser/content/CodeSearchTest.kt` : 신규
- `app/src/test/java/com/ubisam/ubgitea/feature/home/HomeSummariesTest.kt` : 신규
- `app/src/test/java/com/ubisam/ubgitea/feature/home/HomeViewModelTest.kt` : 신규
- `app/src/test/java/com/ubisam/ubgitea/feature/work/WorkViewModelTest.kt` : 신규
- `app/src/test/java/com/ubisam/ubgitea/data/activity/GiteaActivityApiTest.kt` : 신규
- `app/src/test/java/com/ubisam/ubgitea/data/workitems/GiteaWorkSearchApiTest.kt` : 신규
- `app/src/test/java/com/ubisam/ubgitea/domain/history/UnifiedDiffParserTest.kt` : 수정 (`parseFileEntries` 테스트 추가)
- `app/src/test/java/com/ubisam/ubgitea/feature/history/FileDiffChangesTest.kt` : 신규
- `app/src/test/java/com/ubisam/ubgitea/ui/RelativeTimeTest.kt` : 신규
- `app/src/test/java/com/ubisam/ubgitea/ui/navigation/` 아래 기존 Route 테스트 : 수정 및 추가
- 그 밖에 전체 테스트 실행에서 깨지는 기존 테스트 : 수정
## 구체 작업
먼저 `./gradlew compileDebugUnitTestKotlin testDebugUnitTest --console=plain -q` 를 돌려 현재 깨진 테스트 목록을 확인한다.
1. **SyntaxHighlighterTest 수정**
- 대상 코드: `app/src/main/java/com/ubisam/ubgitea/feature/codebrowser/content/SyntaxHighlighter.kt` 의 `tokenizeLines`, `identifierRole`, `TokenRole`.
- 바뀐 동작: C#, C++, Java, Kotlin, JavaScript 에서 키워드가 아닌 식별자도 토큰이 된다. 뒤에 `(` 가 오면 `METHOD` (앞 단어가 `new` 면 `TYPE`), 대문자로 시작하고 소문자를 포함하면 `TYPE` (단 `.` 뒤에 오면 제외), `_t` 로 끝나면 `TYPE`, `m_` 또는 `_` 로 시작하면 색 없음. `if/else/return/throw/for/while...` 는 `CONTROL_KEYWORD`. `using/import/package/namespace` 만 있는 줄의 식별자는 색 없음.
- 기존 기대값을 위 규칙에 맞게 고치고, 위 규칙 각각에 대한 테스트를 추가한다 (C# 과 C++ 예시 위주).
2. **SymbolOutlineTest** — 대상: `.../content/SymbolOutline.kt` 의 `extractSymbols`, `enclosingSymbols`, `findSymbolNamed`, `supportsOutline`.
- C# (Allman 중괄호): namespace, class, 필드 3개, 메서드 2개가 있는 파일에서 이름, `label`(예: `NextDelay(int)`), `kind`, `depth`, `line`, `endLine` 확인.
- C++ 헤더: `namespace a::b {`, `class Foo {`, 생성자 선언, 메서드 선언(`;` 로 끝남), 멤버 변수, `public:` / `private:` 라벨이 심볼이 아닌지.
- C++ 소스: `Ret Foo::Bar(int x) const {` 형태의 정의에서 `name` 이 `Bar`, 생성자 초기화 리스트(`: m_x(x) {}`)가 심볼로 잡히지 않는지, 함수 본문 안의 호출문이 심볼로 잡히지 않는지.
- 전방 선언 `class Foo;` 는 목록에 없음.
- 주석과 문자열 안의 `{`, `}` 와 선언처럼 보이는 텍스트가 무시되는지 (여러 줄 `/* */` 포함).
- 매개변수가 여러 줄에 걸치면 label 이 `Name(…)`.
- Kotlin: `class`, `fun`, 식 본문 함수(`fun a() = 1`) 뒤의 선언도 잡히는지.
- 지원하지 않는 언어(`SyntaxLanguage.PYTHON`)는 빈 목록, `supportsOutline` false.
- `enclosingSymbols`: 메서드 본문 중간 줄에서 namespace → class → method 순서로 반환, 필드는 제외.
3. **CodeSearchTest** — 대상: `.../content/CodeSearch.kt` 의 `findMatches`, `firstMatchFrom`, `parseLineNumber`, `counterpartOf`, `cppRoleOf`.
- 대소문자 무시, 한 줄에 여러 번, 겹치지 않게 전진, 빈 검색어는 결과 없음.
- `firstMatchFrom`: 해당 줄 이후 첫 결과, 없으면 0.
- `parseLineNumber`: 범위 밖, 0, 음수, 공백, `:12` 형태.
- `counterpartOf`: 같은 폴더 같은 이름의 `.h`↔`.cpp`, 확장자 우선순위(`cpp` 가 `cc` 보다 먼저), 다른 폴더의 같은 이름은 제외, C++ 이 아니면 null, 자기 자신은 제외. `RepositoryPath.of("src/a/Foo.h")` 로 만든다.
4. **HomeSummariesTest** — 대상: `.../feature/home/HomeSummaries.kt` 의 `activeRepositoriesOf`, `todayCountsOf` (internal). `ZoneId.of("Asia/Seoul")` 고정.
- 최신 활동순 정렬, `limit`, 7일 범위 밖 활동은 막대에 안 들어감, `dailyCounts` 크기 7 이고 마지막 칸이 오늘, `latestRefName` 은 ref 가 있는 활동 중 최신.
- `todayCountsOf`: PUSH 와 ISSUE_OPENED 만 세고, 어제 것은 제외. 자정 경계(서울 기준)를 확인.
5. **GiteaActivityApiTest** — 대상: `app/src/main/java/com/ubisam/ubgitea/data/activity/GiteaActivityApi.kt`.
- push: `op_type":"commit_repo"`, `content` 가 `{"Commits":[{"Sha1":"abcdef1234","Message":"첫 줄\n둘째 줄"}],"Len":3}` 인 JSON **문자열**. `commits[0].shortId` 가 7자, message 는 첫 줄만, `commitCount` 3, `refName` 에서 `refs/heads/` 제거.
- 이슈/PR: `content":"42|제목"` → `subjectNumber` 42, `subjectText` "제목". `|` 가 없으면 number null.
- content 가 깨진 JSON 이어도 예외 없이 빈 commits.
- `star_repo`, `watch_repo`, `mirror_sync_push` 는 결과에서 빠짐. 모르는 op_type 은 `ActivityKind.OTHER`.
- 페이지 50개 요청에 50개를 받았지만 그중 일부가 star 라서 걸러져도 `nextPage` 는 2 (받은 개수 기준).
- `created` 가 `2026-09-28T10:00:00+09:00` (오프셋 형식) 과 `...Z` 둘 다 파싱.
- `date` 인자가 있으면 쿼리에 `date=2026-09-28`, 없으면 없음. URL 경로가 `/api/v1/users/{login}/activities/feeds`, `/api/v1/orgs/{org}/activities/feeds`.
- body 가 `null` 이면 빈 목록. 401 → `ApiFailure.AuthenticationExpired`, 토큰 공백 → `AuthenticationExpired`.
- 응답 JSON 필드: `id`, `op_type`, `act_user{login,full_name}`, `repo{name,owner{login}}`, `ref_name`, `content`, `created`.
6. **GiteaWorkSearchApiTest** — 대상: `.../data/workitems/GiteaWorkSearchApi.kt`.
- `WorkScope` 별 쿼리 파라미터: `REVIEW_REQUESTED`→`review_requested=true`, `ASSIGNED_TO_ME`→`assigned=true`, `CREATED_BY_ME`→`created=true`, `EVERYONE`→셋 다 없음.
- `WorkTypeFilter`: `ISSUES`→`type=issues`, `PULL_REQUESTS`→`type=pulls`, `ALL`→type 없음. `state=open|closed`. `owner` 있으면 포함.
- 응답 매핑: `repository{name, owner}` 에서 **owner 는 문자열**. `pull_request` 객체가 있으면 `isPullRequest`, `pull_request.merged` true 면 `isMerged`.
- `X-Total-Count` 헤더 → `Page.totalCount`. `count()` 는 `limit=1` 로 요청하고 헤더 값을 반환, 헤더 없으면 null.
- state 가 open/closed 가 아니면 `ApiFailure.InvalidResponse`.
7. **HomeViewModelTest / WorkViewModelTest** — 대상: `.../feature/home/HomeViewModel.kt`, `.../feature/work/WorkViewModel.kt`. 기존 ViewModel 테스트(`app/src/test/java/com/ubisam/ubgitea/feature/notifications/` 등)의 디스패처 설정 방식(`Dispatchers.setMain`)을 그대로 따른다. Repository 는 테스트 파일 안의 fake 로 만든다.
- Home: 로드 성공 시 `Content`, 피드 실패 시 `Error` 타입 매핑, 조직 목록 실패는 무시되고 Content, 요약 숫자 하나가 실패해도 나머지는 채워짐, `selectFilter` 가 `visibleTimeline` 을 거름, `loadMore` 가 id 중복 없이 뒤에 붙임, `open()` 을 두 번 불러도 한 번만 로드, `close()` 후 `Idle`.
- Work: `open()` 은 Idle 일 때만 로드, `selectScope/State/Type` 은 1페이지부터 다시 로드, 결과 없으면 `Empty`, 페이지 이동, 오류 매핑.
8. **UnifiedDiffParserTest 추가 / FileDiffChangesTest**
- `UnifiedDiffParser.parseFileEntries`: hunk 마다 맨 앞에 헤더 행(`isHunkHeader`, `hunkLabel` 은 두 번째 `@@` 뒤 텍스트, 없으면 빈 문자열), `@@ -10,3 +12,4 @@` 이면 왼쪽 번호 10부터 오른쪽 12부터, 추가만 있는 행은 `leftNumber` null.
- `changeStartsOf` (`.../feature/history/FileDiffScreen.kt`, internal): 연속된 변경 묶음의 첫 인덱스만 반환.
9. **RelativeTimeTest** — 대상: `app/src/main/java/com/ubisam/ubgitea/ui/DateTimeFormatting.kt` 의 `relativeTimeOf`. zone 을 `Asia/Seoul` 로 넘긴다.
- 1분 미만 `JustNow`, 59분 `Minutes(59)`, 같은 날 3시간 전 `Hours(3)`, 자정을 넘긴 2시간 전은 `Yesterday`, 3일 전 `Days(3)`, 7일 이상은 `OnDate("yyyy-MM-dd")`.
10. **Route 테스트** — 대상: `app/src/main/java/com/ubisam/ubgitea/ui/navigation/RouteStackSaver.kt` 의 `RouteEncoding`.
- `Route.Home`, `Route.Work` 왕복, `Route.WorkItems(owner, name, number=42, isPullRequest=true/false)` 왕복, number 가 숫자가 아니거나 0 이하면 decodeOrNull null, 빈 스택 복원 기본값이 `Route.Home`.
11. 마지막에 전체 실행: `./gradlew compileDebugUnitTestKotlin testDebugUnitTest --console=plain -q`. 실패 0 이 될 때까지 테스트를 고친다.
## 건드리지 말 것
- `app/src/main/` 아래 모든 파일 (읽기만).
- 특히 다른 작업자가 수정 중인 파일: `feature/repositories/RepositoryListScreen.kt`, `feature/notifications/NotificationsScreen.kt`, `feature/workitems/WorkItemsScreen.kt`, `feature/settings/SettingsScreen.kt`, `res/values/strings_*.xml`.
- git commit, push, stash 금지.
## 완료 기준
- `./gradlew testDebugUnitTest` 실패 0.
- 위 1~10 항목마다 테스트가 존재.
## 보고 형식
- 추가/수정한 테스트 파일 목록과 테스트 개수
- 전체 테스트 실행 결과 (총 개수, 실패 수)
- 메인 코드의 버그로 의심되는 것 (파일, 함수, 재현 입력, 기대와 실제)
## 막히면
- 추측해서 진행하지 말고 멈춘 뒤, 무엇이 불명확한지 보고할 것