<!-- task=m-artifact-browser/05+04_install_handoff plan=1 tag=REVIEW_INSTALL_HANDOFF -->

# Plan - REVIEW_INSTALL_HANDOFF

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

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

## 배경

이 follow-up은 첫 리뷰의 Required 2건을 닫는다. 현재 shell은 verified `StagedApk.path`를 `PendingInstall.apkPath`에 저장하지만 device 화면이 그 path를 표시하지 않고, device별 "설치 대상" 버튼도 다음 USB 설치 흐름으로 선택 정보를 넘기지 않는다. 실제 `adb install` 실행은 여전히 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:
  - `handoff`: 다운로드된 APK 경로를 device 선택과 USB 설치 흐름으로 넘긴다.
- 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/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/app-shell/rules.md`
- `agent-ops/rules/project/domain/device-console/rules.md`
- `agent-ops/rules/project/domain/artifact-flow/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/app-shell-smoke.md`
- `agent-test/local/device-console-smoke.md`
- `agent-test/local/artifact-flow-smoke.md`
- `agent-task/archive/2026/06/m-artifact-browser/04+03_download_staging/complete.log`
- `agent-task/m-artifact-browser/05+04_install_handoff/plan_local_G06_0.log`
- `agent-task/m-artifact-browser/05+04_install_handoff/code_review_local_G06_0.log`
- `lib/src/features/app_shell.dart`
- `lib/src/features/builds/builds_page.dart`
- `lib/src/features/devices/devices_page.dart`
- `lib/src/models/pending_install.dart`
- `lib/src/models/jenkins_build.dart`
- `lib/src/services/artifact_staging_service.dart`
- `test/builds_page_test.dart`
- `test/widget_test.dart`

### 테스트 환경 규칙

- 선택 환경: `local`.
- `agent-test/local/rules.md`가 존재하며 읽었다.
- matched profiles: `app-shell-smoke`, `device-console-smoke`, `artifact-flow-smoke`.
- 적용 명령:
  - local preflight: `git diff --check`, `flutter test test/widget_test.dart`, `flutter test test/builds_page_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 또는 `<확인 필요>` 값은 없었다.

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

- `PendingInstall.apkPath`는 [lib/src/models/pending_install.dart](/config/workspace/appsok/lib/src/models/pending_install.dart:10)에 있지만 device banner나 test assertion에서 사용되지 않는다.
- `DevicesPage`는 pending install 버튼 활성화만 검증하고, 선택한 `AdbDevice`와 `PendingInstall`을 callback으로 넘기는 테스트가 없다.

### 심볼 참조

- 제거 예정 심볼: 없음.
- 확장 대상:
  - `PendingInstall.apkPath`: `rg --sort path -n "apkPath" lib test` 기준 shell 저장과 model 필드 외 사용처가 없다.
  - `DevicesPage`: call site는 `lib/src/features/app_shell.dart`와 direct widget tests다.

### 분할 판단

split decision policy를 재평가했다. 이 follow-up은 기존 split subtask `05+04_install_handoff` 내부의 리뷰 보완이며 새 subtask를 만들지 않는다.

- predecessor `04`: `agent-task/archive/2026/06/m-artifact-browser/04+03_download_staging/complete.log`로 충족.
- 변경 범위는 `DevicesPage` callback 계약, `AppSokShell` wiring, widget tests로 작고 서로 같은 handoff 결함을 닫는다.
- 실제 ADB install, device discovery, staging cleanup은 범위 밖이므로 별도 split이 필요하지 않다.

### 범위 결정 근거

- 포함: pending install banner의 local APK path 표시, device tile install-target callback 계약, shell wiring, widget regression tests.
- 제외: `AdbService.installApk` 호출, 실제 USB 설치 실행, ADB device discovery, logcat, Jenkins credential 또는 download/staging 로직 변경.
- device selection callback은 다음 USB 설치 Milestone이 받을 계약만 만든다. 이 plan에서 APK 설치를 수행하지 않는다.

### 빌드 등급

- `local-G07`: 첫 리뷰 Required를 닫는 bounded UI/API follow-up이며, 파일과 테스트 범위가 작고 deterministic widget tests로 검증 가능하다.

## 구현 체크리스트

- [ ] [REVIEW_INSTALL_HANDOFF-1] pending install banner에 verified local APK path 요약을 표시하고 widget test로 고정한다.
- [ ] [REVIEW_INSTALL_HANDOFF-2] device별 설치 대상 선택 callback이 `AdbDevice`와 `PendingInstall`을 전달하도록 연결하고 widget test로 고정한다.
- [ ] 최종 검증 명령을 실행하고 remote runner 결과를 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [REVIEW_INSTALL_HANDOFF-1] Pending Path Summary

### 문제

[lib/src/features/devices/devices_page.dart](/config/workspace/appsok/lib/src/features/devices/devices_page.dart:245)는 pending banner 보조 텍스트로 build number, job name, size만 표시한다. [lib/src/models/pending_install.dart](/config/workspace/appsok/lib/src/models/pending_install.dart:10)의 `apkPath`는 shell에서 저장된 뒤 화면과 테스트에서 사용되지 않는다.

Before:

```dart
// lib/src/features/devices/devices_page.dart:245
Text(
  '#${pendingInstall.buildNumber} · ${pendingInstall.jobName} · $sizeLabel',
```

### 해결 방법

- `_PendingInstallBanner`에 local path 요약 텍스트를 추가한다.
- 긴 path는 `maxLines: 1`과 `TextOverflow.ellipsis`로 처리하고, test lookup을 위해 안정적인 key를 둔다.
- filename/build/job/size 표시는 유지한다.

After:

```dart
Text(
  key: const ValueKey('pending-install-path'),
  pendingInstall.apkPath,
  maxLines: 1,
  overflow: TextOverflow.ellipsis,
)
```

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

- [ ] `lib/src/features/devices/devices_page.dart`: banner에 `pendingInstall.apkPath` 표시를 추가한다.
- [ ] `test/widget_test.dart`: pending install summary test가 `/tmp/staged/app-release.apk` 표시를 검증한다.

### 테스트 작성

- 작성: `test/widget_test.dart`
- test name: `shows pending apk install summary on devices page`
- assertion goal: filename, size, local path가 모두 표시된다.

### 중간 검증

```bash
flutter test test/widget_test.dart
```

예상: widget test 통과.

## [REVIEW_INSTALL_HANDOFF-2] Device Selection Callback

### 문제

[lib/src/features/devices/devices_page.dart](/config/workspace/appsok/lib/src/features/devices/devices_page.dart:141)는 pending install이 있으면 "설치 대상" 버튼을 활성화하지만 `() {}` no-op이다. 선택된 device와 `PendingInstall.apkPath`가 다음 USB 설치 흐름으로 넘어가지 않아 roadmap `handoff` Task를 완료했다고 보기 어렵다.

Before:

```dart
// lib/src/features/devices/devices_page.dart:138
_CompactIconButton(
  tooltip: '설치 대상',
  icon: Icons.add_to_queue,
  onPressed: pendingInstall != null ? () {} : null,
),
```

### 해결 방법

- `DevicesPage`에 public callback 계약을 추가한다.
- `_DeviceTile`은 `pendingInstall != null`이고 install-target callback이 있을 때 선택한 `AdbDevice`와 `PendingInstall`을 전달한다.
- `AppSokShell`은 callback을 받아 다음 Milestone이 이어받을 수 있는 state 또는 handler 경계를 만든다. 실제 `AdbService.installApk`는 호출하지 않는다.

After:

```dart
typedef PendingInstallSelectionHandler =
    void Function(AdbDevice device, PendingInstall pendingInstall);

onPressed: pendingInstall == null || onInstallTargetSelected == null
    ? null
    : () => onInstallTargetSelected!(device, pendingInstall!),
```

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

- [ ] `lib/src/features/devices/devices_page.dart`: callback typedef/constructor field/wiring을 추가한다.
- [ ] `lib/src/features/app_shell.dart`: `DevicesPage`에 callback을 연결하고, 다음 USB 설치 흐름이 받을 수 있는 state 또는 handler 경계를 유지한다.
- [ ] `test/widget_test.dart`: "설치 대상" tap이 device serial과 APK path를 callback으로 전달하는지 검증한다.

### 테스트 작성

- 작성: `test/widget_test.dart`
- test name: `passes selected device and pending apk to install target callback`
- assertion goal: button tap 후 callback이 `R5CT90A1B2C`와 `/tmp/staged/app-release.apk`를 받는다.

### 중간 검증

```bash
flutter test test/widget_test.dart
```

예상: widget test 통과.

## 의존 관계 및 구현 순서

1. `REVIEW_INSTALL_HANDOFF-1`
2. `REVIEW_INSTALL_HANDOFF-2`
3. focused local tests
4. remote required verification
5. review stub 작성

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/src/features/devices/devices_page.dart` | REVIEW_INSTALL_HANDOFF-1, REVIEW_INSTALL_HANDOFF-2 |
| `lib/src/features/app_shell.dart` | REVIEW_INSTALL_HANDOFF-2 |
| `test/widget_test.dart` | REVIEW_INSTALL_HANDOFF-1, REVIEW_INSTALL_HANDOFF-2 |

## 최종 검증

```bash
git diff --check
flutter test test/widget_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 문제 없음, focused widget/build tests 통과, remote `flutter analyze`에서 `No issues found!`, remote `flutter test`에서 `All tests passed!`.

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