<!-- task=m-artifact-browser/04+03_download_staging plan=1 tag=REVIEW_DOWNLOAD_STAGING -->

# Plan - REVIEW_DOWNLOAD_STAGING

## 이 파일을 읽는 구현 에이전트에게

이 계획은 `code_review_local_G07_0.log`의 FAIL Required 항목만 닫는 follow-up이다. 구현의 마지막 단계는 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이다. 검증 명령을 실행하고 실제 출력, 계획 대비 변경, 설계 결정을 기록한 뒤 active 파일을 유지한 상태로 리뷰 준비를 보고한다.

사용자 결정, 사용자 소유 외부 환경, 범위 충돌 없이는 진행할 수 없을 때만 review stub의 `사용자 리뷰 요청` 섹션에 근거를 채우고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행으로 메울 수 있는 증거 공백은 사용자 리뷰 요청 사유가 아니다. finalization, `complete.log`, archive 이동은 code-review-skill 전용이다.

## 배경

첫 리뷰는 local focused test와 analyzer가 통과했지만, 실제 Jenkins job 이름과 다운로드 API 연결 계약에서 Required 결함을 발견했다. 이 follow-up은 staging 파일명 안전성, Jenkins streaming event와 UI/stager 연결, verified APK handoff 계약을 보완한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/usable-mvp/milestones/artifact-browser.md`
- Task ids:
  - `download-progress`: artifact 다운로드 진행률과 취소 상태를 표시한다.
  - `temp-download`: 다운로드 파일을 앱 임시/staging 경로에 저장하고 설치 완료 또는 취소 후 삭제한다.
  - `artifact-verify`: 다운로드 완료 후 파일 존재, 확장자, 크기 정보를 확인한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-roadmap/current.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-ops/rules/project/domain/artifact-flow/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/artifact-flow-smoke.md`
- `agent-task/m-artifact-browser/04+03_download_staging/plan_local_G07_0.log`
- `agent-task/m-artifact-browser/04+03_download_staging/code_review_local_G07_0.log`
- `lib/src/services/jenkins_client.dart`
- `lib/src/services/artifact_staging_service.dart`
- `lib/src/features/builds/builds_page.dart`
- `lib/src/models/jenkins_build.dart`
- `lib/src/features/app_shell.dart`
- `test/jenkins_client_test.dart`
- `test/artifact_staging_service_test.dart`
- `test/builds_page_test.dart`

### 리뷰 근거

- `git diff --check`: 통과.
- `flutter analyze`: `No issues found!`.
- `flutter test test/jenkins_client_test.dart test/artifact_staging_service_test.dart test/builds_page_test.dart`: `All tests passed!`.
- 통과한 테스트는 slash 포함 job name, Jenkins streaming chunk와 UI total progress 연결, verified APK path handoff를 검증하지 않는다.

### 범위 결정 근거

- 포함: `code_review_local_G07_0.log`의 Required 3건과 직접 관련 테스트.
- 제외: 실제 `adb install`, device 선택, Jenkins credential 획득, 앱 shell wiring. 해당 작업은 dependent split 또는 다른 Milestone 책임이다.
- 새 패키지는 추가하지 않는다.

### 빌드 등급

- `local-G08`: 파일 I/O edge case와 UI/API 계약 정렬이 필요하지만 변경 파일과 테스트 범위가 `artifact-flow` 안에 갇혀 있고 결정적 unit/widget test로 검증 가능하다.

## 구현 체크리스트

- [ ] [REVIEW_DOWNLOAD_STAGING-1] staging 파일명이 Jenkins folder/full job name의 path separator에도 안전하도록 보강하고 테스트를 추가한다.
- [ ] [REVIEW_DOWNLOAD_STAGING-2] Jenkins streaming download 계약과 `BuildsPage` downloader/stager 계약을 연결해 bytes, received/total progress, cancel이 한 흐름으로 동작하게 한다.
- [ ] [REVIEW_DOWNLOAD_STAGING-3] verified APK install handoff가 `StagedApk` 또는 local APK path를 전달하도록 callback 계약과 widget test를 보강한다.
- [ ] 최종 검증 명령을 실행하고 remote runner 결과를 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [REVIEW_DOWNLOAD_STAGING-1] Safe Staging File Name

### 문제

`ArtifactStagingService._fileName`은 artifact 파일명만 sanitize하고 `build.jobName`은 그대로 사용한다. Jenkins folder/full job name이 `team/android-app`처럼 path separator를 포함하면 staging root 아래 단일 파일이 아니라 하위 경로가 만들어져 `openWrite()`가 실패할 수 있다.

### 해결 방법

- `build.jobName`과 `artifact.fileName` 모두 같은 sanitize helper를 거치게 한다.
- slash와 backslash를 `_`로 치환하고 빈 값이 되지 않도록 한다.
- `Directory` injection 테스트에서 job name이 `team/android-app` 또는 `team\\android-app`인 경우에도 staging root 바로 아래 파일이 생성되는지 확인한다.

### 수정 파일 및 체크리스트

- [ ] `lib/src/services/artifact_staging_service.dart`: job/artifact 공통 sanitize helper를 추가한다.
- [ ] `test/artifact_staging_service_test.dart`: separator 포함 job name 테스트를 추가한다.

### 중간 검증

```bash
flutter test test/artifact_staging_service_test.dart
```

## [REVIEW_DOWNLOAD_STAGING-2] Streaming Contract Alignment

### 문제

`JenkinsClient.downloadArtifactStream`은 `DownloadProgressEvent`만 내보내고 실제 chunk bytes를 전달하지 않는다. 반대로 `BuildsPage.ArtifactDownloader`는 raw `Stream<List<int>>`만 받으므로 Jenkins의 `contentLength`를 `_totalBytes`에 설정할 수 없다. 현재 계약으로는 실제 Jenkins 다운로드를 사용해 파일을 쓰면서 determinate progress와 cancel을 함께 제공할 수 없다.

### 해결 방법

- `DownloadProgressEvent` 또는 새 event 타입이 chunk bytes, `receivedBytes`, `totalBytes?`, `isComplete`를 함께 담도록 계약을 정렬한다.
- `ArtifactDownloader`가 Jenkins streaming 결과를 직접 표현할 수 있게 한다. 예: `DownloadTask`를 반환하도록 하거나, cancel forwarding이 가능한 event stream wrapper를 사용한다.
- `BuildsPage`는 event의 chunk bytes를 stager stream에 전달하고, event의 `receivedBytes`/`totalBytes`로 progress label/bar를 갱신한다.
- cancel 버튼은 raw subscription 취소만이 아니라 실제 download task cancel까지 호출되게 한다.
- tests:
  - Jenkins client stream test에서 chunk bytes와 total bytes를 검증한다.
  - BuildsPage widget test에서 total bytes가 있을 때 determinate label/value가 표시되는지 검증한다.
  - cancel test에서 download task cancel path가 호출되고 partial cleanup 신호가 stager로 전달되는지 검증한다.

### 수정 파일 및 체크리스트

- [ ] `lib/src/services/jenkins_client.dart`: streaming event/download task 계약을 bytes+progress+cancel이 가능한 형태로 보강한다.
- [ ] `lib/src/features/builds/builds_page.dart`: downloader/stager 연결을 새 streaming 계약에 맞춘다.
- [ ] `test/jenkins_client_test.dart`: chunk bytes, total bytes, cancel forwarding 테스트를 추가/수정한다.
- [ ] `test/builds_page_test.dart`: determinate progress와 cancel forwarding 테스트를 추가/수정한다.

### 중간 검증

```bash
flutter test test/jenkins_client_test.dart test/builds_page_test.dart
```

## [REVIEW_DOWNLOAD_STAGING-3] Verified APK Handoff Contract

### 문제

verified 상태는 `_stagedPath`를 화면에 표시하지만 설치 callback은 여전히 `JenkinsBuild`와 `BuildArtifact`만 전달한다. 다음 handoff가 검증된 staging APK를 식별하려면 callback 계약 또는 전달 객체에 local APK path/size가 포함되어야 한다.

### 해결 방법

- 설치 callback 계약을 `StagedApk` 또는 local APK path를 포함하는 형태로 보강한다.
- 아직 downloader/stager가 없는 fallback UI가 필요하면 staged 값이 없는 legacy install 요청과 verified install 요청을 명확히 분리한다.
- widget test에서 staging 성공 후 설치 버튼을 누르면 callback이 verified path와 size를 받는지 검증한다.

### 수정 파일 및 체크리스트

- [ ] `lib/src/features/builds/builds_page.dart`: verified install handoff에 staged APK 정보를 포함한다.
- [ ] `test/builds_page_test.dart`: verified APK path/size callback 테스트를 추가한다.

### 중간 검증

```bash
flutter test test/builds_page_test.dart
```

## 의존 관계 및 구현 순서

1. `REVIEW_DOWNLOAD_STAGING-1`
2. `REVIEW_DOWNLOAD_STAGING-2`
3. `REVIEW_DOWNLOAD_STAGING-3`
4. 최종 검증과 review stub 작성

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/src/services/artifact_staging_service.dart` | REVIEW_DOWNLOAD_STAGING-1 |
| `lib/src/services/jenkins_client.dart` | REVIEW_DOWNLOAD_STAGING-2 |
| `lib/src/features/builds/builds_page.dart` | REVIEW_DOWNLOAD_STAGING-2, REVIEW_DOWNLOAD_STAGING-3 |
| `test/artifact_staging_service_test.dart` | REVIEW_DOWNLOAD_STAGING-1 |
| `test/jenkins_client_test.dart` | REVIEW_DOWNLOAD_STAGING-2 |
| `test/builds_page_test.dart` | REVIEW_DOWNLOAD_STAGING-2, REVIEW_DOWNLOAD_STAGING-3 |

## 최종 검증

```bash
git diff --check
flutter analyze
flutter test test/jenkins_client_test.dart test/artifact_staging_service_test.dart test/builds_page_test.dart
ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com 'zsh -lc '\''cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && flutter analyze'\'''
ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com 'zsh -lc '\''cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && flutter test'\'''
```

예상: diff whitespace 문제 없음, local focused tests 통과, remote `flutter analyze`에서 `No issues found!`, remote `flutter test`에서 `All tests passed!`.
