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

# Plan - REVIEW2_INSTALL_HANDOFF

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

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

## 배경

G07은 path 표시와 callback 주입 케이스를 닫았지만, default `AppSokShell` 경로에서 외부 callback이 없어도 wrapper가 `DevicesPage`에 전달되어 버튼이 활성화된 no-op으로 남는다. 이 follow-up은 null callback 상태를 보존하고 그 회귀 테스트만 추가한다. 실제 `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/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/m-artifact-browser/05+04_install_handoff/plan_local_G07_1.log`
- `agent-task/m-artifact-browser/05+04_install_handoff/code_review_local_G07_1.log`
- `lib/src/app.dart`
- `lib/src/features/app_shell.dart`
- `lib/src/features/devices/devices_page.dart`
- `lib/src/models/pending_install.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 또는 `<확인 필요>` 값은 없었다.

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

- `AppSokShell`에 `onInstallTargetSelected`가 없을 때 handoff 이후 "설치 대상" 버튼이 disabled인지 검증하는 테스트가 없다.
- callback 주입 경로는 `passes selected device and pending apk to install target callback`으로 이미 검증된다.

### 심볼 참조

- 제거 예정 심볼: 없음.
- 확장 대상:
  - `AppSokShell.onInstallTargetSelected`: call site는 `lib/src/app.dart`의 default `const AppSokShell()`과 `test/widget_test.dart`의 injected shell이다.
  - `DevicesPage.onInstallTargetSelected`: shell wiring과 direct constructor 계약이다.

### 분할 판단

split decision policy를 재평가했다. 이 follow-up은 기존 split subtask `05+04_install_handoff` 안의 single-line wiring 보완과 regression test이며 새 subtask를 만들지 않는다.

- predecessor `04`: `agent-task/archive/2026/06/m-artifact-browser/04+03_download_staging/complete.log`로 이미 충족된 상태에서 이어진 subtask다.
- 실제 ADB install, device discovery, logcat은 범위 밖이다.

### 범위 결정 근거

- 포함: shell이 null external callback을 `DevicesPage`에 null로 전달하도록 보존, default no-op 버튼 방지 test.
- 제외: USB 설치 실행, install target state 화면, device filtering, unauthorized/offline state 정책 변경.
- ready/offline device별 enable 정책은 후속 USB 설치 Milestone의 세부 UX로 남긴다. 이번 plan은 callback 부재 no-op만 닫는다.

### 빌드 등급

- `local-G08`: 두 번째 review failure지만 원인은 명시적이고 deterministic widget test로 닫을 수 있어 local lane을 유지하고 grade만 올린다.

## 구현 체크리스트

- [ ] [REVIEW2_INSTALL_HANDOFF-1] `AppSokShell`이 외부 install target callback이 없을 때 `DevicesPage`에도 null callback을 전달하도록 수정한다.
- [ ] [REVIEW2_INSTALL_HANDOFF-2] callback 없는 shell handoff 후 "설치 대상" 버튼이 disabled이고, callback 주입 경로는 계속 전달되는지 widget test로 고정한다.
- [ ] 최종 검증 명령을 실행하고 remote runner 결과를 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [REVIEW2_INSTALL_HANDOFF-1] Preserve Null Callback

### 문제

[lib/src/features/app_shell.dart](/config/workspace/appsok/lib/src/features/app_shell.dart:77)는 `widget.onInstallTargetSelected`가 null이어도 `_handleInstallTargetSelected`를 `DevicesPage`에 넘긴다. [lib/src/app.dart](/config/workspace/appsok/lib/src/app.dart:12)의 default `const AppSokShell()`에서는 외부 callback이 없으므로 button tap이 no-op이다.

Before:

```dart
// lib/src/features/app_shell.dart:75
child: DevicesPage(
  pendingInstall: _pendingInstall,
  onInstallTargetSelected: _handleInstallTargetSelected,
),
```

### 해결 방법

- 외부 callback이 없으면 `DevicesPage.onInstallTargetSelected`에 null을 전달한다.
- 외부 callback이 있으면 기존 `_handleInstallTargetSelected` 포워딩을 유지한다.

After:

```dart
child: DevicesPage(
  pendingInstall: _pendingInstall,
  onInstallTargetSelected: widget.onInstallTargetSelected == null
      ? null
      : _handleInstallTargetSelected,
),
```

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

- [ ] `lib/src/features/app_shell.dart`: null callback 보존 wiring을 적용한다.

### 테스트 작성

- 작성: `test/widget_test.dart`
- test name: `keeps install target disabled when shell has no target callback`
- assertion goal: default shell handoff 후 첫 "설치 대상" `IconButton.onPressed`가 null이다.

### 중간 검증

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

예상: widget test 통과.

## [REVIEW2_INSTALL_HANDOFF-2] Regression Coverage

### 문제

[test/widget_test.dart](/config/workspace/appsok/test/widget_test.dart:224)는 callback 주입 케이스만 검증한다. null callback 경로가 없어서 shell wrapper가 no-op을 다시 만들 수 있었다.

### 해결 방법

- 기존 handoff helper 중복은 과도하게 리팩터링하지 말고, 현재 test style 안에서 작은 regression test를 추가한다.
- callback 주입 테스트는 유지해서 enabled + callback 전달 경로를 계속 고정한다.

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

- [ ] `test/widget_test.dart`: callback 없는 shell handoff regression test를 추가한다.
- [ ] 기존 `passes selected device and pending apk to install target callback` test가 계속 통과하는지 확인한다.

### 테스트 작성

- 작성: `test/widget_test.dart`
- test names:
  - `keeps install target disabled when shell has no target callback`
  - existing `passes selected device and pending apk to install target callback`

### 중간 검증

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

예상: widget test 통과.

## 의존 관계 및 구현 순서

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

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/src/features/app_shell.dart` | REVIEW2_INSTALL_HANDOFF-1 |
| `test/widget_test.dart` | REVIEW2_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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
