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

# Plan - DOWNLOAD_STAGING

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

구현의 마지막 단계는 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이다. 검증 명령을 실행하고 실제 출력, 계획 대비 변경, 설계 결정을 기록한 뒤 active 파일을 유지한 상태로 리뷰 준비를 보고한다. 사용자 결정, 사용자 소유 외부 환경, 범위 충돌 없이는 진행할 수 없을 때만 review stub의 `사용자 리뷰 요청` 섹션에 근거를 채우고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행으로 메울 수 있는 증거 공백은 사용자 리뷰 요청 사유가 아니다. finalization, `complete.log`, archive 이동은 code-review-skill 전용이다.

## 배경

`install-cta`는 build/artifact 선택 직후 설치 진입점을 노출하지만, 아직 APK를 임시 staging 경로로 내려받거나 진행률과 취소를 보여주지 않는다. 이 작업은 Jenkins artifact 다운로드를 사용자가 신뢰할 수 있는 상태 전이로 만들고, 설치 handoff가 사용할 verified local APK 산출물을 만든다. 실제 `adb install` 실행과 device 선택은 다음 split 작업과 `device-console`/USB 설치 Milestone 경계에 남긴다.

## 사용자 리뷰 요청 흐름

구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 해당 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`에서 복사한 형식이며, 구현 에이전트는 직접 사용자 프롬프트를 만들지 않는다. code-review가 차단의 정당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## 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-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/update-roadmap/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-roadmap/current.md`
- `agent-roadmap/phase/usable-mvp/PHASE.md`
- `agent-roadmap/phase/usable-mvp/milestones/artifact-browser.md`
- `agent-task/archive/2026/06/m-artifact-browser/03+02_build_list_ui/complete.log`
- `pubspec.yaml`
- `lib/src/features/builds/builds_page.dart`
- `lib/src/models/jenkins_build.dart`
- `lib/src/services/jenkins_client.dart`
- `test/builds_page_test.dart`
- `test/jenkins_client_test.dart`

### 테스트 환경 규칙

- 선택 환경: `local`.
- `agent-test/local/rules.md`가 존재하며 읽었다. Flutter/macOS/ADB/runtime evidence는 standard remote Mac runner 기준이다.
- matched profile: `agent-test/local/artifact-flow-smoke.md`를 읽었다.
- 적용 명령:
  - local preflight: `git diff --check`, `flutter test test/builds_page_test.dart`, `flutter test test/jenkins_client_test.dart`
  - remote required: `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'\'''`
  - remote required: `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'\'''`
- 구조적 blank/skeleton 또는 `<확인 필요>` 값은 없었다.

### 테스트 커버리지 공백

- `JenkinsClient.downloadArtifact`는 현재 `bodyBytes` 반환만 검증이 없고, 진행률/취소/streaming 동작 테스트가 없다.
- 임시/staging 경로 저장, 취소 후 삭제, 완료 후 verify 동작을 검증하는 테스트가 없다.
- `BuildsPage`는 선택 CTA callback 테스트가 있지만 다운로드 중/완료/취소/error UI 테스트가 없다.

### 심볼 참조

- 이름 변경 또는 제거 예정 심볼: 없음.
- 확장 대상:
  - `JenkinsClient.downloadArtifact`: `rg --sort path -n "downloadArtifact"` 결과 현재 정의만 존재한다.
  - `BuildsPage.onInstallRequested`: `lib/src/features/builds/builds_page.dart`, `test/builds_page_test.dart`에서 사용 중이다.

### 분할 판단

split decision policy를 계획 파일 선택 전에 평가했다. 공유 task group은 `agent-task/m-artifact-browser/`다.

- `04+03_download_staging`: `03` 선행 완료 후 다운로드/임시파일/검증을 구현한다. predecessor `03`은 `agent-task/archive/2026/06/m-artifact-browser/03+02_build_list_ui/complete.log`로 충족됨.
- `05+04_install_handoff`: `04` 완료 후 verified APK 경로를 device 선택/USB 설치 흐름으로 넘긴다. predecessor `04`는 아직 active/archive `complete.log`가 없으므로 구현 시작 전 `agent-task/m-artifact-browser/04+03_download_staging/complete.log` 또는 archive의 matching complete.log가 필요하다.

다운로드 I/O와 device handoff는 도메인 경계, 테스트 전략, 실패 복구 방식이 달라 분리한다.

### 범위 결정 근거

- 포함: Jenkins artifact streaming download, UI progress/cancel/error/success 상태, temp/staging 파일 생성과 cleanup, 완료 파일 검증.
- 제외: `adb install` 실행, device 선택 UI 변경, logcat 이동, Jenkins credential 획득 방식 변경, 실제 Jenkins endpoint 수동 smoke. 해당 항목은 handoff split 또는 다른 Milestone 책임이다.
- 새 패키지는 추가하지 않는다. `pubspec.yaml`에 `path_provider`와 `http`가 이미 있다.

### 빌드 등급

- `local-G07`: 비동기 다운로드/취소와 파일 cleanup이 있어 일반 UI보다 위험이 높지만, artifact-flow 안에 갇히고 mock HTTP/widget/unit test로 결정적으로 검증 가능하다.

## 구현 체크리스트

- [ ] [DOWNLOAD_STAGING-1] Jenkins artifact 다운로드를 진행률/취소 가능한 흐름으로 확장하고 mock HTTP 테스트를 추가한다.
- [ ] [DOWNLOAD_STAGING-2] APK를 앱 임시/staging 경로에 저장하고 취소/실패/완료 cleanup 및 파일 검증 테스트를 추가한다.
- [ ] [DOWNLOAD_STAGING-3] `BuildsPage` 설치 CTA를 다운로드 상태 UI에 연결하고 진행률, 취소, 완료, 실패 widget test를 추가한다.
- [ ] 최종 검증 명령을 실행하고 remote runner 결과를 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [DOWNLOAD_STAGING-1] Download Progress And Cancel

### 문제

`JenkinsClient.downloadArtifact`는 [lib/src/services/jenkins_client.dart](/config/workspace/appsok/lib/src/services/jenkins_client.dart:65)에서 `http.get` 전체 응답을 받은 뒤 bytes만 반환한다. 이 구조는 진행률을 표시할 수 없고, 취소 상태를 UI에 반영하기 어렵다.

Before:

```dart
// lib/src/services/jenkins_client.dart:65
Future<List<int>> downloadArtifact({
  required Uri artifactUrl,
  required JenkinsCredentials credentials,
}) async {
  final response = await _client.get(
    artifactUrl,
    headers: _headers(credentials),
  );

  _throwIfFailed(response);

  return response.bodyBytes;
}
```

### 해결 방법

- `http.Client.send` 기반 streaming 다운로드 API를 추가한다. 기존 `downloadArtifact`는 테스트 호환이 필요하면 wrapper로 유지한다.
- progress payload에는 `receivedBytes`, `totalBytes?`, `isComplete`를 포함한다.
- 취소는 구현 범위를 작게 유지하기 위해 download task/controller 내부 flag와 response stream subscription cancel로 처리한다. `http.Client.close()`를 전체 client에 호출해 다른 요청을 깨는 방식은 사용하지 않는다.
- HTTP 실패는 기존 `JenkinsClientException` 계약을 유지한다.

After 방향:

```dart
final task = client.downloadArtifactStream(
  artifactUrl: artifactUrl,
  credentials: credentials,
);
await for (final event in task.events) {
  // update progress and write chunks through staging service
}
task.cancel();
```

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

- [ ] `lib/src/services/jenkins_client.dart`: streaming download API, progress event, cancel task를 추가한다.
- [ ] `test/jenkins_client_test.dart`: chunked response, content-length progress, forbidden response, cancel path를 검증한다.

### 테스트 작성

- 작성: `test/jenkins_client_test.dart`
- test names:
  - `downloadArtifactStream emits progress while reading chunks`
  - `downloadArtifactStream reports forbidden response`
  - `downloadArtifactStream can be cancelled before completion`

### 중간 검증

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

예상: 모든 Jenkins client unit test 통과.

## [DOWNLOAD_STAGING-2] Temp Staging And Verification

### 문제

다운로드 파일을 저장할 staging 책임이 없다. Milestone은 임시/staging 경로 저장, 취소/설치 완료 후 삭제, 완료 후 파일 존재/확장자/크기 확인을 요구한다.

Before:

```dart
// lib/src/services/jenkins_client.dart:74
_throwIfFailed(response);

return response.bodyBytes;
```

### 해결 방법

- artifact-flow 책임 안에 staging helper를 만든다. 새 파일이 필요하면 `lib/src/services/artifact_staging_service.dart`로 한정한다.
- path 결정은 `path_provider.getTemporaryDirectory()`를 쓰되, 테스트는 `Directory` injection으로 실제 앱 temp에 의존하지 않는다.
- 파일명은 artifact `fileName`을 기준으로 하되 path separator를 제거하고 build number/job name을 포함해 충돌과 오설치를 줄인다.
- verify 결과는 파일 존재, `.apk` 확장자, byte size > 0을 포함한다.
- 취소/실패 시 partial file을 삭제한다. 설치 완료 cleanup은 handoff 이후 호출 가능한 method로 남기고, 이 plan에서는 method와 unit test까지만 만든다.

After 방향:

```dart
final staged = await stagingService.stageApk(
  build: build,
  artifact: artifact,
  stream: task.events,
);
final verified = await stagingService.verify(staged.path);
```

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

- [ ] `lib/src/services/artifact_staging_service.dart` 또는 기존 service 파일: staging, cleanup, verify API를 구현한다.
- [ ] `test/artifact_staging_service_test.dart`: temp directory injection으로 success/cancel/failure cleanup과 verify를 검증한다.

### 테스트 작성

- 작성: `test/artifact_staging_service_test.dart`
- test names:
  - `stageApk writes downloaded bytes into temp directory`
  - `stageApk deletes partial file when cancelled`
  - `verify rejects non apk extension and zero byte file`

### 중간 검증

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

예상: staging service unit test 통과.

## [DOWNLOAD_STAGING-3] Builds UI Download States

### 문제

`_InstallCta`는 [lib/src/features/builds/builds_page.dart](/config/workspace/appsok/lib/src/features/builds/builds_page.dart:513)에서 선택된 APK를 callback으로 넘길 뿐 다운로드 상태를 표시하지 않는다. 사용자는 다운로드가 시작됐는지, 얼마나 진행됐는지, 취소됐는지 알 수 없다.

Before:

```dart
// lib/src/features/builds/builds_page.dart:513
ElevatedButton.icon(
  key: const ValueKey('install-cta-button'),
  onPressed: onInstallRequested == null
      ? null
      : () => onInstallRequested!(item, artifact),
  icon: const Icon(Icons.install_mobile),
  label: const Text('설치'),
),
```

### 해결 방법

- `BuildsPage`에는 테스트 가능한 downloader/staging dependency를 optional로 주입한다. production 기본값은 기존 Jenkins credential wiring이 준비되기 전이면 null/disabled 상태를 유지한다.
- 버튼 클릭 후 상태를 `idle -> downloading -> verified` 또는 `failed/cancelled`로 관리한다.
- 진행률은 `LinearProgressIndicator`와 받은 byte/전체 byte 텍스트로 표시한다. total bytes가 없으면 indeterminate로 표시한다.
- 취소 버튼은 다운로드 subscription과 partial cleanup을 호출한다.
- verified 상태는 다음 split의 handoff callback이 받을 수 있는 local APK path를 보관한다. 실제 device 이동은 이 plan에서 하지 않는다.

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

- [ ] `lib/src/features/builds/builds_page.dart`: CTA 상태, progress/cancel/success/error UI를 추가한다.
- [ ] `test/builds_page_test.dart`: 진행률, 취소, 완료, 실패 상태 widget test를 추가한다.
- [ ] 필요 시 service fake/helper를 test 안에 둔다. tracked secret이나 private endpoint는 쓰지 않는다.

### 테스트 작성

- 작성: `test/builds_page_test.dart`
- test names:
  - `shows download progress after install CTA pressed`
  - `cancels artifact download and hides partial state`
  - `shows verified apk state after staging succeeds`
  - `shows download failure state when staging fails`

### 중간 검증

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

예상: build page widget test 통과.

## 의존 관계 및 구현 순서

`04+03_download_staging`는 predecessor `03`이 완료된 뒤 시작한다. 충족 근거: `agent-task/archive/2026/06/m-artifact-browser/03+02_build_list_ui/complete.log`.

구현 순서:

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

## 수정 파일 요약

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

## 최종 검증

```bash
git diff --check
flutter test test/jenkins_client_test.dart
flutter test test/artifact_staging_service_test.dart
flutter test 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!`.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
