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>
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-testrunTest). - 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 테스트, 파일 안에 privateQueueTransportfake 포함),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 를 돌려 현재 깨진 테스트 목록을 확인한다.
-
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++ 예시 위주).
- 대상 코드:
-
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)는 빈 목록,supportsOutlinefalse. enclosingSymbols: 메서드 본문 중간 줄에서 namespace → class → method 순서로 반환, 필드는 제외.
- C# (Allman 중괄호): namespace, class, 필드 3개, 메서드 2개가 있는 파일에서 이름,
-
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")로 만든다.
-
HomeSummariesTest — 대상:
.../feature/home/HomeSummaries.kt의activeRepositoriesOf,todayCountsOf(internal).ZoneId.of("Asia/Seoul")고정.- 최신 활동순 정렬,
limit, 7일 범위 밖 활동은 막대에 안 들어감,dailyCounts크기 7 이고 마지막 칸이 오늘,latestRefName은 ref 가 있는 활동 중 최신. todayCountsOf: PUSH 와 ISSUE_OPENED 만 세고, 어제 것은 제외. 자정 경계(서울 기준)를 확인.
- 최신 활동순 정렬,
-
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 는 첫 줄만,commitCount3,refName에서refs/heads/제거. - 이슈/PR:
content":"42|제목"→subjectNumber42,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.
- push:
-
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.mergedtrue 면isMerged. X-Total-Count헤더 →Page.totalCount.count()는limit=1로 요청하고 헤더 값을 반환, 헤더 없으면 null.- state 가 open/closed 가 아니면
ApiFailure.InvalidResponse.
-
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, 페이지 이동, 오류 매핑.
- Home: 로드 성공 시
-
UnifiedDiffParserTest 추가 / FileDiffChangesTest
UnifiedDiffParser.parseFileEntries: hunk 마다 맨 앞에 헤더 행(isHunkHeader,hunkLabel은 두 번째@@뒤 텍스트, 없으면 빈 문자열),@@ -10,3 +12,4 @@이면 왼쪽 번호 10부터 오른쪽 12부터, 추가만 있는 행은leftNumbernull.changeStartsOf(.../feature/history/FileDiffScreen.kt, internal): 연속된 변경 묶음의 첫 인덱스만 반환.
-
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").
- 1분 미만
-
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.
-
마지막에 전체 실행:
./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 항목마다 테스트가 존재.
보고 형식
- 추가/수정한 테스트 파일 목록과 테스트 개수
- 전체 테스트 실행 결과 (총 개수, 실패 수)
- 메인 코드의 버그로 의심되는 것 (파일, 함수, 재현 입력, 기대와 실제)
막히면
- 추측해서 진행하지 말고 멈춘 뒤, 무엇이 불명확한지 보고할 것