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

11 KiB

작업: 리디자인 신규 로직 단위 테스트 작성

목표

  • 리디자인(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 항목마다 테스트가 존재.

보고 형식

  • 추가/수정한 테스트 파일 목록과 테스트 개수
  • 전체 테스트 실행 결과 (총 개수, 실패 수)
  • 메인 코드의 버그로 의심되는 것 (파일, 함수, 재현 입력, 기대와 실제)

막히면

  • 추측해서 진행하지 말고 멈춘 뒤, 무엇이 불명확한지 보고할 것