<!-- task=m-usb-install/01_install_lifecycle_cleanup plan=1 tag=UILC -->

# Plan - UILC Install Lifecycle Cleanup

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

`CODE_REVIEW-local-G06.md`의 구현 에이전트 소유 섹션을 채우는 것이 구현의 마지막 필수 단계다. 구현 후 검증을 실행하고 실제 변경 내용, 설계 결정, stdout/stderr를 기록한 뒤 active 파일을 그대로 둔 채 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review 전용이다. 구현 중 사용자만 결정할 수 있는 범위 충돌, 사용자 소유 외부 환경, secret 또는 서비스 준비가 없으면 사용자에게 직접 질문하지 말고, 필요한 경우 review stub의 `사용자 리뷰 요청`에 근거를 적고 멈춘다. 후속 에이전트가 명령 재실행이나 증거 수집으로 닫을 수 있는 검증 공백은 사용자 리뷰 요청이 아니다.

## 배경

`Artifact 탐색과 다운로드`는 staging 파일을 device 선택으로 넘긴다. 현재 작은 보완으로 handoff 전 폐기와 설치 대기 취소 cleanup은 처리했지만, 실제 `adb install -r` 종료 후 cleanup은 설치 실행/result owner가 없으면 안전하게 구현할 수 없다. 이 계획은 `USB 설치와 device 선택` 마일스톤에서 설치 실행, 결과 표시, ready guard, staged APK cleanup을 같은 생명주기로 묶어 완료한다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 에이전트는 채팅에서 직접 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. code-review가 사용자 리뷰 요청의 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/usable-mvp/milestones/usb-install.md`
- Task ids:
  - `install-run`: 선택한 APK와 device serial로 `adb install -r`를 실행한다.
  - `install-output`: stdout/stderr와 exit code를 UI에 표시한다.
  - `install-guard`: APK가 선택되지 않았거나 device가 ready가 아니면 설치 버튼을 비활성화한다.
  - `install-result`: 성공/실패 결과를 이후 이력 milestone에서 재사용할 수 있는 구조로 남긴다.
  - `install-cleanup`: `adb install -r` 종료 후 성공/실패 결과를 보존한 뒤 staged APK를 cleanup한다.
- 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-ops/rules/project/domain/app-shell/rules.md`
- `agent-ops/rules/project/domain/device-console/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/artifact-flow-smoke.md`
- `agent-test/local/device-console-smoke.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/usable-mvp/PHASE.md`
- `agent-roadmap/phase/usable-mvp/milestones/artifact-browser.md`
- `agent-roadmap/phase/usable-mvp/milestones/usb-install.md`
- `lib/src/features/app_shell.dart`
- `lib/src/features/builds/builds_page.dart`
- `lib/src/features/devices/devices_page.dart`
- `lib/src/models/adb_device.dart`
- `lib/src/models/pending_install.dart`
- `lib/src/services/adb_service.dart`
- `lib/src/services/artifact_staging_service.dart`
- `test/widget_test.dart`
- `test/builds_page_test.dart`
- `test/artifact_staging_service_test.dart`

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md`를 읽었다. 기본 완료 evidence는 remote Mac runner의 `flutter analyze`, `flutter test` 기준이다.
- 적용 profile: `agent-test/local/device-console-smoke.md`, `agent-test/local/artifact-flow-smoke.md`.
- 적용 명령: `flutter analyze`, `flutter test`; device-console 변경은 ADB parsing 변경 시 fixture test가 필요하다. 이 계획은 parsing 변경이 아니므로 UI/service mock test로 install lifecycle을 검증한다.
- `<확인 필요>` 값은 없었다.
- 실제 Android device 수동 설치는 optional smoke이며, mock install test가 성공/실패/cleanup 계약의 필수 증거다.

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

- 현재 `test/widget_test.dart`는 pending APK handoff와 pending 취소 cleanup을 확인하지만 실제 install completion cleanup은 확인하지 않는다.
- 현재 `test/builds_page_test.dart`는 staged APK abandon/handoff 경계를 확인하지만 device install 결과와 cleanup은 확인하지 않는다.
- 새 구현은 success/failure install mock에서 cleanup이 정확히 한 번 호출되는 회귀 테스트가 필요하다.
- ready가 아닌 device의 설치 버튼 비활성화 테스트가 필요하다.

### 심볼 참조

- 제거 예정 심볼: 없음.
- 변경 가능 심볼: `PendingInstallSelectionHandler`의 의미가 "설치 대상 선택 알림"에서 "설치 실행 진입"과 충돌할 수 있다. 변경한다면 call site는 `lib/src/features/app_shell.dart`, `lib/src/features/devices/devices_page.dart`, `test/widget_test.dart` 전체를 갱신한다.

### 분할 판단

- split policy를 검토했다.
- 이 작업은 `USB 설치와 device 선택`의 `[install]` epic 안에서 하나의 설치 lifecycle을 구성한다. `install-run`, `install-output`, `install-guard`, `install-result`, `install-cleanup`을 분리하면 cleanup 시점과 결과 보존 계약이 서로 다른 리뷰로 흩어진다.
- ADB runtime bundling, device list parsing, refresh는 별도 epic이므로 이 plan에서 제외한다.
- 따라서 `m-usb-install/01_install_lifecycle_cleanup` 단일 subtask로 둔다. predecessor는 없다.

### 범위 결정 근거

- 포함: `DevicesPage`의 install action, ready guard, install 진행/결과 UI, install 완료 후 pending cleanup.
- 포함: shell에서 pending install/result를 보존하고 cleanup callback을 설치 완료 이후 호출하는 wiring.
- 포함: `AdbService.installApk`를 사용하는 injectable installer 또는 equivalent seam.
- 제외: bundled ADB runtime, custom adb path UI, `adb devices -l` 실제 list loading, refresh 구현, logcat, split APK/APKS/AAB.
- 제외 이유: 해당 항목은 `adb-runtime`, `device-list`, 후속 logcat epic의 별도 책임이다.

### 빌드 등급

- `local-G06`: UI state, async install lifecycle, cleanup idempotency가 있어 중간 위험이지만 범위와 테스트가 bounded이고 mock 기반 검증이 가능하다.

## 구현 체크리스트

- [ ] [UILC-1] `DevicesPage`에 pending APK 설치 실행 상태, ready-device guard, stdout/stderr/exitCode 결과 표시를 구현한다.
- [ ] [UILC-2] install success/failure 완료 후 결과를 보존한 뒤 staged APK cleanup을 정확히 한 번 호출한다.
- [ ] [UILC-3] widget/unit regression test를 추가한다: ready device install success cleanup, failure cleanup, non-ready device disabled, cleanup-before-complete 금지.
- [ ] [UILC-4] `flutter analyze`와 `flutter test`를 실행해 통과를 확인한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [UILC-1] Install Action And Result UI

### 문제

`lib/src/features/devices/devices_page.dart:157`의 설치 버튼은 현재 `onInstallTargetSelected`를 즉시 호출할 뿐 `adb install -r` 실행 상태나 결과를 갖지 않는다.

```dart
// lib/src/features/devices/devices_page.dart:157
_CompactIconButton(
  tooltip: '설치 대상',
  icon: Icons.add_to_queue,
  onPressed:
      pendingInstall == null || onInstallTargetSelected == null
      ? null
      : () => onInstallTargetSelected!(device, pendingInstall!),
),
```

### 해결 방법

`DevicesPage` 또는 shell-owned controller에 `Future<AdbInstallResult> Function(AdbDevice, PendingInstall)` 형태의 installer를 주입한다. 화면은 설치 중, 성공, 실패 결과를 표시하고 result object를 state에 보존한다. ready가 아닌 device는 설치 버튼을 비활성화한다.

```dart
typedef PendingInstallRunner =
    Future<AdbInstallResult> Function(AdbDevice device, PendingInstall pending);

// after
onPressed: pendingInstall == null || installer == null || !isReady
    ? null
    : () => _runInstall(device, pendingInstall!),
```

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

- [ ] `lib/src/features/devices/devices_page.dart`: installer typedef, install state/result UI, ready guard 추가.
- [ ] `lib/src/features/app_shell.dart`: installer wiring과 pending result 보존 위치 결정.
- [ ] `lib/src/services/adb_service.dart`: 기존 `installApk`를 재사용하고 필요 시 테스트 가능한 wrapper만 추가.

### 테스트 작성

- `test/widget_test.dart`: `ready device runs installer and shows success output`.
- `test/widget_test.dart`: `non-ready device keeps install action disabled`.

### 중간 검증

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

기대 결과: 추가/기존 widget test 통과.

## [UILC-2] Cleanup After Install Completion

### 문제

`lib/src/features/app_shell.dart:71`은 target selection callback만 전달한다. 실제 install 종료를 기다린 뒤 cleanup하는 owner가 없다.

```dart
// lib/src/features/app_shell.dart:71
void _handleInstallTargetSelected(AdbDevice device, PendingInstall pending) {
  widget.onInstallTargetSelected?.call(device, pending);
}
```

`lib/src/services/adb_service.dart:28`에는 install 실행 API가 있지만 staged APK cleanup과 연결되어 있지 않다.

```dart
// lib/src/services/adb_service.dart:28
Future<AdbInstallResult> installApk({
  required String serial,
  required String apkPath,
  bool replace = true,
}) async {
```

### 해결 방법

install Future가 완료된 이후에만 cleanup을 호출한다. cleanup은 success/failure 모두에서 실행하되, `AdbInstallResult`는 UI와 이후 이력 milestone이 재사용할 수 있게 먼저 state에 저장한다. cleanup 실패는 결과 표시를 지우지 않으며, 중복 tap으로 cleanup이 두 번 호출되지 않도록 installing state에서 버튼을 비활성화한다.

```dart
// after sketch
setState(() => _installState = installing);
final result = await installer(device, pending);
setState(() {
  _installResult = result;
  _installState = result.succeeded ? succeeded : failed;
  _pendingInstall = null;
});
await cleanup(pending.apkPath);
```

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

- [ ] `lib/src/features/app_shell.dart`: pending/result/cleanup 순서가 명확한 handler 작성.
- [ ] `lib/src/features/devices/devices_page.dart`: installing 중복 실행 방지.
- [ ] `test/widget_test.dart`: cleanup 호출 횟수와 result 보존 검증.

### 테스트 작성

- `test/widget_test.dart`: `cleans staged apk once after successful install result`.
- `test/widget_test.dart`: `cleans staged apk once after failed install result while keeping stderr visible`.
- `test/widget_test.dart`: `does not clean staged apk before install future completes`.

### 중간 검증

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

기대 결과: success/failure cleanup test 통과.

## [UILC-3] Regression Coverage

### 문제

`agent-roadmap/phase/usable-mvp/milestones/usb-install.md:64`는 cleanup 검증을 요구한다. 현재 테스트는 pending 취소와 build handoff만 검증한다.

### 해결 방법

mock installer와 mock cleaner를 사용한다. installer completer를 잡아 cleanup-before-complete 금지를 확인하고, success/failure 각각에서 result text와 cleanup path를 함께 검증한다.

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

- [ ] `test/widget_test.dart`: install lifecycle widget tests 추가.
- [ ] 필요 시 `test/adb_service_test.dart`: service wrapper를 새로 만들면 unit test 추가.

### 테스트 작성

- 작성한다. bug fix이자 새 public callback/contract 가능성이 있으므로 normal + boundary test가 필요하다.

### 중간 검증

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

기대 결과: 모든 install lifecycle regression test 통과.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/src/features/devices/devices_page.dart` | UILC-1, UILC-2 |
| `lib/src/features/app_shell.dart` | UILC-1, UILC-2 |
| `lib/src/services/adb_service.dart` | UILC-1 |
| `test/widget_test.dart` | UILC-1, UILC-2, UILC-3 |
| `test/adb_service_test.dart` | UILC-3, wrapper 추가 시에만 |

## 최종 검증

```bash
git diff --check
flutter analyze
flutter test
```

기대 결과: diff whitespace 오류 없음, analyzer issue 없음, 모든 Flutter test 통과.

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