# 작업: 리디자인 신규 로직 단위 테스트 작성 ## 목표 - 리디자인(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 항목마다 테스트가 존재. ## 보고 형식 - 추가/수정한 테스트 파일 목록과 테스트 개수 - 전체 테스트 실행 결과 (총 개수, 실패 수) - 메인 코드의 버그로 의심되는 것 (파일, 함수, 재현 입력, 기대와 실제) ## 막히면 - 추측해서 진행하지 말고 멈춘 뒤, 무엇이 불명확한지 보고할 것