<!-- task=m-jenkins-credential/02+01_app_wiring plan=2 tag=REVIEW_API -->

# Plan - REVIEW_API

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

이 작업은 이전 리뷰 `code_review_local_G06_0.log`의 Required 이슈만 해결한다. 구현 후 active `CODE_REVIEW-local-G07.md`의 구현 에이전트 소유 섹션을 채워야 완료된다. 검증 명령은 계획에 적힌 대로 실행하고 실제 stdout/stderr를 붙여 넣은 뒤, active 파일을 그대로 둔 채 리뷰 준비를 보고한다. `코드리뷰 결과`, log archive, `complete.log`, task directory archive는 code-review 전용이다.

구현 중 사용자만 결정할 수 있는 범위 변경, 사용자 소유 secret/외부 Jenkins 준비, 또는 이 계획과 충돌하는 요구가 발견되면 사용자에게 직접 묻지 않는다. `CODE_REVIEW-local-G07.md`의 `사용자 리뷰 요청` 섹션에 결정 항목, 근거, 실행한 명령과 출력, 재개 조건을 기록하고 멈춘다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 닫을 수 있는 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과 또는 계획 대비 변경 사항으로 기록한다.

## 배경

첫 리뷰에서 stored Jenkins session 복원 후 production job loader가 실제로 실행되지 않는 Required 이슈가 확인됐다. `AppSokApp`는 startup `FutureBuilder`의 첫 프레임에서 `isReady=false` 상태로 `AppSokShell`을 만들고, 이때 `BuildsPage`는 `jobLoader=null`로 mount된다. `restore()` 성공 후 loader prop은 non-null로 바뀌지만 `BuildsPage`는 `initState`에서만 `_load()`를 호출하므로 Jenkins job 조회가 시작되지 않는다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`에서 복사된 형식을 사용한다. 구현 에이전트는 직접 사용자 프롬프트, 채팅 선택지, `request_user_input`, `USER_REVIEW.md` 생성, log archive, `complete.log` 작성을 하지 않는다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/usable-mvp/milestones/jenkins-credential.md`
- Task ids:
  - `startup-check`: 앱 시작 시 Keychain credential을 읽고 Jenkins 사용자 확인 API로 조용히 검증한다.
  - `auth-state`: 검증 성공 시 로그인 화면을 건너뛰고 build 목록으로 진입한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-task/m-jenkins-credential/02+01_app_wiring/code_review_local_G06_0.log`
- `agent-task/m-jenkins-credential/02+01_app_wiring/plan_local_G06_0.log`
- `agent-ops/rules/project/domain/app-shell/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/artifact-flow-smoke.md`
- `lib/src/app.dart`
- `lib/src/features/app_shell.dart`
- `lib/src/features/builds/builds_page.dart`
- `test/widget_test.dart`

### 테스트 환경 규칙

- test_env: local.
- matched profiles: `agent-test/local/app-shell-smoke.md`, `agent-test/local/artifact-flow-smoke.md`.
- 적용 명령: remote runner checkout에서 `flutter test test/widget_test.dart`, `flutter analyze`, `flutter test`; 보조로 현재 checkout `git diff --check`.
- 실제 Jenkins endpoint/credential은 필요하지 않으며 mock HTTP와 secure storage mock으로 검증한다.

### 범위 결정 근거

- `JenkinsArtifactSession`의 storage/client delegation과 artifact URL 조립은 이번 Required 이슈와 무관하므로 변경하지 않는다.
- `AppSokApp`의 service 생성 구조는 유지한다. loader prop 전환을 받는 consumer인 `BuildsPage` lifecycle을 고치는 것이 가장 좁다.
- WebView 로그인, logout UX, 실제 Jenkins smoke는 이번 follow-up 범위가 아니다.

### 빌드 등급

- build lane: `local-G07`.
- review lane: `local-G07`.
- 근거: 명확한 widget lifecycle correctness 이슈와 회귀 테스트 보강이다. deterministic local/remote runner 검증으로 판단 가능하지만 첫 리뷰가 놓친 production wiring 경로라 grade를 한 단계 올린다.

## 구현 체크리스트

- [ ] `BuildsPage`가 `jobLoader` null -> non-null 전환을 받으면 Jenkins job load를 시작하고, non-null -> null 전환에서는 연결 필요 상태로 안전하게 돌아가도록 lifecycle 처리를 추가한다.
- [ ] `AppSokApp wires stored Jenkins session into builds page` 테스트가 실제 `/api/json` job request 발생과 post-load UI를 검증하도록 강화한다.
- [ ] `git diff --check`와 remote runner `flutter test test/widget_test.dart`, `flutter analyze`, `flutter test`를 실행해 결과를 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_API-1] Stored-session loader lifecycle

문제: [builds_page.dart](/config/workspace/appsok/lib/src/features/builds/builds_page.dart:67)는 `initState`에서만 `jobLoader`를 확인한다. [app.dart](/config/workspace/appsok/lib/src/app.dart:41)의 startup restore 흐름에서는 첫 mount 후 loader prop이 바뀌므로 `_load()`가 호출되지 않는다.

해결 방법:

- `BuildsPage`에 `didUpdateWidget`을 추가한다.
- `oldWidget.jobLoader == null && widget.jobLoader != null`이면 `_load()`를 호출한다.
- `oldWidget.jobLoader != null && widget.jobLoader == null`이면 `_state`, `_jobs`, `_filtered`, `_selectedJob`을 초기 연결 필요 상태와 일치하도록 reset한다.
- 이미 non-null loader에서 다른 non-null loader로 바뀌는 경우는 테스트 DI 재구성 또는 client 교체를 고려해 `_load()` 재실행을 허용해도 된다. 단, 불필요한 반복 load가 생기지 않게 조건을 명확히 둔다.

수정 파일 및 체크리스트:

- [ ] `lib/src/features/builds/builds_page.dart`: `didUpdateWidget` lifecycle 처리 추가.
- [ ] `lib/src/features/builds/builds_page.dart`: loader가 사라지는 경우 stale job/build 선택 상태를 남기지 않는다.
- [ ] `test/widget_test.dart`: stored session test에서 job request capture와 job row 또는 empty-job status를 검증한다.

테스트 작성:

- 수정: `test/widget_test.dart`
- update test name: `AppSokApp wires stored Jenkins session into builds page`
- required assertions:
  - `whoAmI/api/json` 요청 후 `api/json` job 요청이 발생한다.
  - job response가 non-empty이면 job name이 표시된다. empty response를 유지한다면 captured job request와 `접근 가능한 job 없음`을 함께 확인한다.
  - `Jenkins 연결 필요` 부재만으로 성공 판정하지 않는다.

중간 검증:

```bash
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 test/widget_test.dart'"
```

기대 결과: 기존 shell tests와 강화된 app root wiring test 모두 통과.

## 최종 검증

```bash
git diff --check
```

기대 결과: 출력 없음, exit code 0.

```bash
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 && flutter test'"
```

기대 결과: `No issues found!`와 `All tests passed!`.

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