<!-- task=m-install-session-history/01_install_result_model plan=0 tag=REFACTOR -->

# Plan - REFACTOR

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

이 작업은 구현 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채워야 완료된다. 검증을 실행하고 실제 출력과 구현 메모를 남긴 뒤 active 파일을 유지한 채 리뷰 준비 상태로 보고한다. 사용자 결정, 사용자 소유 외부 환경, scope conflict 없이는 사용자에게 직접 묻지 말고, 차단 시 review stub의 `사용자 리뷰 요청`에 증거를 남긴다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review 전용이다.

## 배경

현재 shell은 설치 결과를 `_lastInstallDevice`와 `_lastInstallResult` 두 필드로 나누어 보관한다. 현재 Milestone은 저장 이력이 아니라 "현재 설치 시도 결과"만 다루기로 확정되었으므로, pending artifact metadata까지 함께 담는 얇은 화면 상태 모델이 필요하다.

## 사용자 리뷰 요청 흐름

구현 중 사용자만 결정할 수 있는 차단이 생기면 active `CODE_REVIEW-*-G??.md`의 `사용자 리뷰 요청` 섹션을 채우고 멈춘다. 구현 에이전트는 chat에서 직접 질문하거나 `request_user_input`을 호출하지 않는다. code-review가 차단 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/workflow-integration/milestones/install-session-history.md`
- Task ids:
  - `session-model`: 현재 설치 시도 결과를 화면 상태로 표현할 최소 모델을 정의한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/workflow-integration/milestones/install-session-history.md`
- `agent-ops/rules/project/domain/app-shell/rules.md`
- `agent-ops/rules/project/domain/artifact-flow/rules.md`
- `agent-ops/rules/project/domain/device-console/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/app-shell-smoke.md`
- `agent-test/local/artifact-flow-smoke.md`
- `agent-test/local/device-console-smoke.md`
- `lib/src/features/app_shell.dart`
- `lib/src/features/devices/devices_page.dart`
- `lib/src/models/pending_install.dart`
- `lib/src/models/adb_device.dart`
- `lib/src/services/adb_service.dart`
- `test/widget_test.dart`
- `analysis_options.yaml`

### 테스트 환경 규칙

`test_env=local`을 적용한다. local 규칙과 app-shell/device-console/artifact-flow smoke 프로필을 읽었다. 필수 명령은 remote runner checkout에서 `flutter analyze`와 `flutter test`이며, 현재 checkout에서는 `git diff --check`를 preflight로 사용한다. 이 작업은 shell handoff와 device install UI 모델을 바꾸므로 `test/widget_test.dart`에 regression을 보강한다.

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

- 현재 설치 결과 모델: 기존 테스트는 결과 배너 노출만 확인하고, build/artifact metadata가 결과 상태에 보존되는지 확인하지 않는다. regression 추가 필요.
- 영구 저장 없음: 별도 저장소가 없고 이번 작업도 storage API를 추가하지 않는다. analyzer와 widget test로 call-site를 검증한다.

### 심볼 참조

- 변경 예정: `_lastInstallDevice`, `_lastInstallResult`, `DevicesPage.lastInstallDevice`, `DevicesPage.lastInstallResult`.
- 참조 위치: `lib/src/features/app_shell.dart:82`, `lib/src/features/app_shell.dart:83`, `lib/src/features/app_shell.dart:109`, `lib/src/features/app_shell.dart:126`, `lib/src/features/app_shell.dart:127`, `lib/src/features/app_shell.dart:147`, `lib/src/features/app_shell.dart:207`, `lib/src/features/app_shell.dart:208`, `lib/src/features/devices/devices_page.dart:21`, `lib/src/features/devices/devices_page.dart:22`, `lib/src/features/devices/devices_page.dart:31`, `lib/src/features/devices/devices_page.dart:32`, `lib/src/features/devices/devices_page.dart:131`, `lib/src/features/devices/devices_page.dart:134`, `lib/src/features/devices/devices_page.dart:135`.

### 분할 판단

Split 정책을 먼저 평가했다. 공유 화면 상태 모델과 UI 표시 강화가 API-vs-call-site 경계로 나뉘므로 split한다. 공유 task group은 `agent-task/m-install-session-history/`다. `01_install_result_model`은 선행 작업이 없고, `02+01_current_result_ui`는 `01_install_result_model`의 `complete.log` 이후 구현한다.

### 범위 결정 근거

이 작업은 모델과 wiring만 다룬다. 오류 요약, masking, clipboard 리포트는 `report` Epic으로 제외한다. `recent-list` 화면 문구와 build/artifact 표시 강화는 후속 `02+01_current_result_ui`에서 다룬다. 로컬 저장소, 중앙 감사 로그, device allowlist는 Milestone 범위 제외다.

### 빌드 등급

`local-G04`: 내부 모델과 bounded call-site 변경이며, widget regression으로 검증 가능하다.

## 구현 체크리스트

- [ ] `InstallAttemptResult` 모델을 추가하고 `PendingInstall`, `AdbDevice`, `AdbInstallResult`를 한 객체로 묶는다.
- [ ] `AppSokShell`의 분리된 마지막 설치 결과 상태를 새 모델 상태로 교체한다.
- [ ] `DevicesPage`가 새 모델을 입력으로 받아 기존 결과 배너 동작을 유지한다.
- [ ] `test/widget_test.dart`에 설치 완료 후 build/artifact metadata가 결과 상태에 보존되는 regression을 추가한다.
- [ ] `git diff --check`, `flutter analyze`, `flutter test`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REFACTOR-1] 현재 설치 결과 모델 도입

#### 문제

[app_shell.dart](/config/workspace/appsok/lib/src/features/app_shell.dart:80)의 설치 lifecycle 상태가 device와 result로 분리되어 있고, [devices_page.dart](/config/workspace/appsok/lib/src/features/devices/devices_page.dart:131)는 두 값이 모두 있을 때만 결과 배너를 보여준다. pending artifact metadata는 설치 완료 후 `_pendingInstall = null`이 되면서 결과 상태에 남지 않는다.

#### 해결 방법

새 파일 `lib/src/models/install_attempt_result.dart`를 추가한다.

```dart
import '../services/adb_service.dart';
import 'adb_device.dart';
import 'pending_install.dart';

class InstallAttemptResult {
  const InstallAttemptResult({
    required this.pendingInstall,
    required this.device,
    required this.result,
  });

  final PendingInstall pendingInstall;
  final AdbDevice device;
  final AdbInstallResult result;
}
```

Before [app_shell.dart](/config/workspace/appsok/lib/src/features/app_shell.dart:80):

```dart
// Install lifecycle state — owned by shell so cleanup survives DevicesPage dispose.
bool _isInstalling = false;
AdbDevice? _lastInstallDevice;
AdbInstallResult? _lastInstallResult;
```

After:

```dart
// Install lifecycle state — owned by shell so cleanup survives DevicesPage dispose.
bool _isInstalling = false;
InstallAttemptResult? _lastInstallAttempt;
```

설치 시작과 새 pending install 선택 시 `_lastInstallAttempt = null`로 초기화하고, 완료 시 `InstallAttemptResult(pendingInstall: pending, device: device, result: result)`를 저장한다. `DevicesPage`에는 `lastInstallAttempt` 하나만 전달한다.

Before [devices_page.dart](/config/workspace/appsok/lib/src/features/devices/devices_page.dart:131):

```dart
if (widget.lastInstallResult != null &&
    widget.lastInstallDevice != null) ...[
  _InstallResultBanner(
    device: widget.lastInstallDevice!,
    result: widget.lastInstallResult!,
  ),
```

After:

```dart
if (widget.lastInstallAttempt case final attempt?) ...[
  _InstallResultBanner(
    device: attempt.device,
    result: attempt.result,
  ),
```

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

- [ ] `lib/src/models/install_attempt_result.dart` 추가
- [ ] `lib/src/features/app_shell.dart` import, state, setState, `DevicesPage` argument 변경
- [ ] `lib/src/features/devices/devices_page.dart` constructor/field/result banner argument 변경
- [ ] `test/widget_test.dart` regression 추가

#### 테스트 작성

`test/widget_test.dart`에 설치 완료 후 결과 배너가 유지되고, 다음 작업에서 사용할 `InstallAttemptResult.pendingInstall.fileName/buildNumber/jobName`이 UI나 finder key로 확인 가능하도록 regression을 추가한다. 직접 private state를 검사하지 말고 visible widget 또는 key를 통해 검증한다.

#### 중간 검증

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

기대 결과: widget test 통과.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/src/models/install_attempt_result.dart` | REFACTOR-1 |
| `lib/src/features/app_shell.dart` | REFACTOR-1 |
| `lib/src/features/devices/devices_page.dart` | REFACTOR-1 |
| `test/widget_test.dart` | REFACTOR-1 |

## 최종 검증

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

remote runner 완료 evidence가 필요하면 아래 명령을 같은 순서로 실행한다.

```bash
ssh toki@toki-labs.com
zsh -lc 'cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && flutter analyze'
zsh -lc 'cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && flutter test'
```

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