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

# Plan - REVIEW_REVIEW_AUTH_TOKEN

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

이 plan은 직전 리뷰의 Required 이슈만 해결한다. `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이 구현의 마지막 단계다. 검증 명령을 실행하고 실제 stdout/stderr를 기록한 뒤 active 파일을 그대로 두고 리뷰를 요청한다. 사용자만 결정할 수 있는 Jenkins endpoint, credential, 조직 정책 blocker가 생기면 review stub의 `사용자 리뷰 요청`에 근거를 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 archive/log/complete.log를 직접 만들지 않는다.

## 배경

직전 follow-up은 token 저장 검증을 추가했지만, production WebView token 성공 경로에서 dialog close owner가 둘로 갈라졌다. `SettingsPage._defaultLauncher`의 `onTokenIssued` callback이 route를 pop하고, `JenkinsWebLoginPage._issueToken`도 같은 성공 경로에서 다시 pop한다. 또한 새 테스트는 `AppSokApp._saveToken` 실제 private 경로를 호출하지 않고 테스트 내부 복제 로직만 검증해, roadmap의 `keychain-store` 완료 근거가 아직 부족하다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 후속 에이전트가 재실행으로 해소할 수 있는 검증 공백은 사용자 리뷰 요청이 아니며, code-review가 최종 stop 여부를 판단한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/usable-mvp/milestones/jenkins-credential.md`
- Task ids:
  - `crumb-token`: WebView same-origin 요청으로 crumb를 받고 사용자별 API token을 자동 발급한다.
  - `keychain-store`: 자동 발급한 username/API token을 `TokenStore`를 통해 macOS Keychain에 저장하고 삭제할 수 있다.
- 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/rules/project/domain/artifact-flow/rules.md`
- `agent-ops/rules/project/domain/app-shell/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/artifact-flow-smoke.md`
- `agent-test/local/app-shell-smoke.md`
- `agent-task/m-jenkins-credential/02+01_crumb_token_store/plan_cloud_G06_1.log`
- `agent-task/m-jenkins-credential/02+01_crumb_token_store/code_review_cloud_G06_1.log`
- `lib/src/app.dart`
- `lib/src/features/app_shell.dart`
- `lib/src/features/settings/settings_page.dart`
- `lib/src/features/settings/jenkins_web_login_page.dart`
- `lib/src/services/jenkins_artifact_session.dart`
- `lib/src/services/jenkins_client.dart`
- `test/app_auth_test.dart`
- `test/settings_page_test.dart`
- `test/widget_test.dart`
- `test/jenkins_web_auth_bridge_test.dart`

### 리뷰 검증

- `flutter analyze`: 통과.
- `flutter test test/app_auth_test.dart test/settings_page_test.dart test/widget_test.dart`: 통과.
- `flutter test`: 통과.
- `git diff --check`: 통과.
- 현재 Linux/container checkout의 Flutter 검증은 preflight다. 완료 evidence는 remote Mac runner 기준으로 다시 기록한다.

### 범위 결정 근거

- 포함: token 발급 성공 시 route pop owner 단일화, `JenkinsWebLoginResult.apiToken` 전달 계약 정리, `AppSokApp` 실제 `webLoginLauncher` -> `_saveToken` -> `JenkinsArtifactSession.restore()` -> jobs loader 활성화/rollback regression test.
- 제외: 실제 Jenkins sandbox 수동 smoke, LDAP/조직 정책 분기, macOS entitlement 변경, Jenkins token revoke lifecycle.

### 빌드 등급

- `cloud-G06`: Jenkins auth/token, Keychain persistence, session restore, widget wiring이 결합된 보안/저장 흐름 수리다. follow-up은 좁지만 production WebView navigation contract와 검증 신뢰도 회복이 핵심이다.

## 구현 체크리스트

- [ ] WebView token 발급 성공 경로가 route를 한 번만 닫고 `JenkinsWebLoginResult.apiToken`을 production default launcher에 전달하도록 `JenkinsWebLoginPage`/`SettingsPage` 계약을 정리한다.
- [ ] `AppSokApp` no-session 상태에서 fake `webLoginLauncher`가 `apiToken`을 반환할 때 실제 `_saveToken`과 restore 성공/실패 경로를 검증하는 regression test를 추가하거나 기존 테스트를 고친다.
- [ ] 최종 검증 명령을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- 이 subtask는 directory name `02+01_crumb_token_store` 기준으로 `01_web_login_entry` 완료에 의존한다.
- predecessor evidence는 이전 loop에서 확인됐다.

### [REVIEW_REVIEW_AUTH_TOKEN-1] Single-Pop Token Result Contract

#### 문제

[jenkins_web_login_page.dart](/config/workspace/appsok/lib/src/features/settings/jenkins_web_login_page.dart:289)는 token 발급 성공 시 `onTokenIssued`를 호출한 뒤 다시 `Navigator.pop`을 실행한다. 그런데 [settings_page.dart](/config/workspace/appsok/lib/src/features/settings/settings_page.dart:186)의 default callback도 root navigator를 pop한다. 실제 production WebView 성공 경로에서는 route close가 두 번 일어날 수 있고, 두 번째 pop이 dialog 아래 route에 전파될 위험이 있다.

Before:

```dart
widget.onTokenIssued?.call(username, displayName, token);
Navigator.of(context).pop(JenkinsWebLoginResult(...));
```

#### 해결 방법

- route close owner를 하나로 고정한다.
- 권장: `JenkinsWebLoginPage._issueToken`이 token 발급 성공 시 `JenkinsWebLoginResult(username: ..., displayName: ..., apiToken: token)`을 한 번만 `Navigator.pop`으로 반환하게 한다.
- `SettingsPage._defaultLauncher`는 token callback에서 pop하지 않게 하고, showDialog 결과의 `apiToken`으로 기존 `_handleWebLogin` -> `_saveToken` 경로를 탄다.
- `onTokenIssued`를 유지한다면 navigation side-effect 없는 관찰 callback으로 제한하거나, callback이 pop을 소유할 때 page가 추가 pop을 하지 않도록 명확히 분기한다.

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

- [ ] `lib/src/features/settings/jenkins_web_login_page.dart`: token 발급 성공 result에 `apiToken` 포함, single pop 보장.
- [ ] `lib/src/features/settings/settings_page.dart`: `_defaultLauncher`가 route를 중복 pop하지 않도록 callback/wiring 정리.
- [ ] `test/jenkins_web_auth_bridge_test.dart` 또는 관련 widget test: token 발급 성공 result가 `apiToken`을 포함하고 navigation close owner가 하나라는 계약을 검증 가능한 형태로 보강.

#### 테스트 작성

- 작성/보강: `test/jenkins_web_auth_bridge_test.dart` 또는 `test/settings_page_test.dart`.
- assertion: token bridge success result가 `JenkinsWebLoginResult.apiToken`으로 전달되는 계약을 검증하고, default launcher 쪽 callback이 별도 `Navigator.pop`을 수행하지 않는 구조를 확인한다.

#### 중간 검증

```bash
flutter test test/jenkins_web_auth_bridge_test.dart test/settings_page_test.dart
```

예상: Web Login result/callback contract 관련 테스트 통과.

### [REVIEW_REVIEW_AUTH_TOKEN-2] AppSokApp Save/Restore Regression Coverage

#### 문제

[app_auth_test.dart](/config/workspace/appsok/test/app_auth_test.dart:71)의 `app._saveToken` 테스트들은 `AppSokApp._saveToken`을 호출하지 않고 테스트 내부 복제 함수만 검증한다. [widget_test.dart](/config/workspace/appsok/test/widget_test.dart:1391)의 AppSokApp web login test도 `apiToken` 없는 result만 반환해 login 후 `_saveToken` -> `restore()` -> jobs loader 활성화 경로를 검증하지 않는다.

Before:

```dart
Future<bool> saveAndValidate(...) async { ... } // test-local copy
webLoginLauncher: (_) async => const JenkinsWebLoginResult(username: 'toki')
```

#### 해결 방법

- `AppSokApp`을 no-session 상태로 띄우고 Settings 탭에서 fake `webLoginLauncher`가 `JenkinsWebLoginResult(apiToken: ...)`을 반환하게 한다.
- mock `JenkinsClient`가 `whoAmI/api/json` 200과 jobs API 200을 반환할 때 저장 성공 UI와 jobs loader 활성화를 검증한다.
- mock `JenkinsClient`가 401/403 또는 restore failure를 반환할 때 `token-save-error`가 표시되고 `TokenStore.readSession()`이 `null`인지 검증한다.
- 기존 test-local `saveAndValidate` 복제 테스트는 제거하거나 실제 AppSokApp 경로를 타는 테스트로 대체한다.

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

- [ ] `test/app_auth_test.dart`: 실제 AppSokApp token save success/failure regression test 추가 또는 기존 fake-saver 테스트 대체.
- [ ] `test/widget_test.dart`: 필요 시 AppSokApp web login wiring test를 `apiToken` 포함 경로로 확장.
- [ ] `lib/src/app.dart`: 테스트가 드러내는 URL host validation 또는 rollback edge가 있으면 함께 수정.

#### 테스트 작성

- 작성/보강: `test/app_auth_test.dart`.
- assertion:
  - success: no stored session -> fake web login result with `apiToken` -> `whoAmI` 검증 성공 -> `token-saved-label` 표시 -> jobs loader 활성화.
  - failure: fake web login result with `apiToken` -> `whoAmI` 401/403 또는 restore false -> `token-save-error` 표시 -> stored session null.

#### 중간 검증

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

예상: actual AppSokApp save/restore regression tests pass.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/src/features/settings/jenkins_web_login_page.dart` | REVIEW_REVIEW_AUTH_TOKEN-1 |
| `lib/src/features/settings/settings_page.dart` | REVIEW_REVIEW_AUTH_TOKEN-1 |
| `lib/src/app.dart` | REVIEW_REVIEW_AUTH_TOKEN-2, 필요 시 edge fix |
| `test/jenkins_web_auth_bridge_test.dart` | REVIEW_REVIEW_AUTH_TOKEN-1 |
| `test/settings_page_test.dart` | REVIEW_REVIEW_AUTH_TOKEN-1 |
| `test/app_auth_test.dart` | REVIEW_REVIEW_AUTH_TOKEN-2 |
| `test/widget_test.dart` | REVIEW_REVIEW_AUTH_TOKEN-2, 필요 시 |

## 최종 검증

```bash
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'
```

예상: analyzer issue 없음, 모든 테스트 통과. 실제 Jenkins sandbox endpoint가 없으면 수동 smoke 생략 사유를 review stub에 기록한다.

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