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

# Plan - REVIEW_AUTH_TOKEN

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

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

## 배경

이전 구현은 WebView에서 발급한 token을 Keychain에 저장하는 경로를 추가했지만, 저장 성공 판정이 Jenkins session 검증 성공과 분리되어 있다. 또한 새 테스트가 실제 `onTokenIssued` -> `_saveToken` -> `onSessionSaved` 경로를 실행하지 않아 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/private/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-roadmap/current.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-ops/skills/common/plan/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/macos-platform/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/artifact-flow-smoke.md`
- `agent-test/local/app-shell-smoke.md`
- `agent-test/local/macos-platform-smoke.md`
- `agent-task/m-jenkins-credential/02+01_crumb_token_store/plan_cloud_G06_0.log`
- `agent-task/m-jenkins-credential/02+01_crumb_token_store/code_review_cloud_G06_0.log`
- `agent-task/archive/2026/06/m-jenkins-credential/01_web_login_entry/complete.log`
- `lib/src/app.dart`
- `lib/src/features/app_shell.dart`
- `lib/src/features/settings/jenkins_web_login_page.dart`
- `lib/src/features/settings/settings_page.dart`
- `lib/src/services/jenkins_artifact_session.dart`
- `lib/src/services/jenkins_client.dart`
- `lib/src/services/token_store.dart`
- `test/jenkins_web_login_page_test.dart`
- `test/settings_page_test.dart`
- `test/widget_test.dart`
- `test/token_store_test.dart`
- `test/app_auth_test.dart`
- `test/jenkins_web_auth_bridge_test.dart`

### 테스트 환경 규칙

- 선택 환경: `local`.
- `agent-test/local/rules.md`를 읽었고 `artifact-flow-smoke`, `app-shell-smoke`, `macos-platform-smoke` profile을 읽었다.
- 필수 remote runner 명령: `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'`.
- macOS build evidence는 기존 plan의 final verification 계약을 유지해 `zsh -lc 'cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && flutter build macos'`를 포함한다.
- 리뷰 중 local focused preflight로 `flutter test test/jenkins_web_auth_bridge_test.dart test/app_auth_test.dart test/token_store_test.dart`를 실행했고 통과했지만, test가 핵심 callback 경로를 실제로 운동하지 않아 완료 evidence로 충분하지 않았다.

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

- `AppSokApp._saveToken`이 실제 Jenkins session 검증 성공 전에는 `true`를 반환하지 않는지 검증하는 테스트가 없다.
- `SettingsPage`의 `_defaultLauncher`가 받은 token callback이 `tokenSaver`, 성공 UI, 실패 UI, `onSessionSaved`를 실제로 연결하는지 검증하는 테스트가 없다.
- Web Login 후 저장된 session restore로 build 목록 loader가 활성화되는 end-to-end widget test가 현재는 pre-populated Keychain만 검증한다.

### 심볼 참조

- 변경 후보: `JenkinsWebLoginLauncher`, `JenkinsTokenIssuedCallback`, `JenkinsTokenSaver`.
- 현재 참조: `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`, `test/widget_test.dart`, `test/settings_page_test.dart`, `test/app_auth_test.dart`.

### 분할 판단

- shared task group: `m-jenkins-credential`.
- 현재 subtask: `02+01_crumb_token_store`.
- predecessor `01_web_login_entry`는 `agent-task/archive/2026/06/m-jenkins-credential/01_web_login_entry/complete.log`로 충족됐다.
- 이번 follow-up은 이전 subtask 안의 Required 이슈 2건을 해결하는 좁은 수리 작업이라 새 split subtask를 만들지 않는다.

### 범위 결정 근거

- 포함: token 저장 성공 판정, session restore 검증/rollback, Web Login token callback testability, focused unit/widget tests.
- 제외: 실제 Jenkins sandbox 수동 smoke, LDAP/조직 정책 분기, logout-clear UI 전반, macOS entitlement 변경.
- 실제 Jenkins endpoint가 없으면 bridge/parser는 mock/fake test로 유지하고 생략 사유를 review stub에 남긴다.

### 빌드 등급

- `cloud-G06`: Jenkins auth/token, Keychain persistence, session restore, widget wiring이 결합된 보안/저장 흐름 수리다.

## 구현 체크리스트

- [ ] 저장 성공 판정이 Jenkins session 검증 성공을 의미하도록 `AppSokApp._saveToken`과 관련 callback 흐름을 수정하고 실패 시 partial credential을 제거한다.
- [ ] Web Login token callback을 테스트에서 실제 호출할 수 있게 launcher/callback surface를 정리하고 모든 call site를 업데이트한다.
- [ ] 성공/실패 저장 UI, `onSessionSaved`, restore 후 build loader 활성화 regression test를 추가하거나 기존 테스트를 실제 callback 경로로 고친다.
- [ ] 최종 검증 명령을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- 이 subtask는 directory name `02+01_crumb_token_store` 기준으로 `01_web_login_entry` 완료에 의존한다.
- predecessor evidence: `agent-task/archive/2026/06/m-jenkins-credential/01_web_login_entry/complete.log`.

### [REVIEW_AUTH_TOKEN-1] Verified Token Save Contract

#### 문제

[app.dart](/config/workspace/appsok/lib/src/app.dart:61)의 `_saveToken`은 `TokenStore.saveSession`만 성공하면 `true`를 반환한다. 계획은 저장 직후 `JenkinsArtifactSession.restore()` 또는 `JenkinsClient.fetchCurrentUser` 검증을 요구했으므로, invalid token이 저장되거나 restore가 실패해도 [settings_page.dart](/config/workspace/appsok/lib/src/features/settings/settings_page.dart:156)가 저장 성공 UI를 표시할 수 있다.

Before:

```dart
await store.saveSession(...);
return true;
```

#### 해결 방법

- `baseUrl`은 `http`/`https` scheme과 non-empty host를 요구한다.
- `saveSession` 후 `_session.restore()` 또는 `client.fetchCurrentUser` 동등 검증을 기다린다.
- 검증이 `false` 또는 예외이면 `store.clearSession()`을 호출하고 `false`를 반환한다.
- 검증 성공 후에만 `true`를 반환한다.
- `onSessionSaved`가 중복 restore를 유발하면 `_session.isRestored`를 재사용하거나 idempotent하게 유지하되, UI 활성화는 검증 성공 이후에만 발생해야 한다.

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

- [ ] `lib/src/app.dart`: `_saveToken` URL 검증, session 검증, rollback.
- [ ] `lib/src/features/settings/settings_page.dart`: 저장 성공/실패 상태가 검증 결과와 일치하도록 필요 시 조정.
- [ ] `test/app_auth_test.dart` 또는 `test/widget_test.dart`: invalid token/restore failure에서 저장 성공 UI와 jobs loader가 활성화되지 않고 credential이 제거되는지 검증.

#### 테스트 작성

- 작성: `test/app_auth_test.dart`.
- assertion: successful token save returns saved UI only after `whoAmI` 검증 200; 401/403 또는 restore false는 `token-save-error`를 표시하고 `TokenStore.readSession()`이 null이다.

#### 중간 검증

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

예상: token save success/failure regression tests pass.

### [REVIEW_AUTH_TOKEN-2] Real Token Callback Coverage

#### 문제

[app_auth_test.dart](/config/workspace/appsok/test/app_auth_test.dart:164)와 [app_auth_test.dart](/config/workspace/appsok/test/app_auth_test.dart:210)는 fake `webLoginLauncher`가 `JenkinsWebLoginResult`만 반환하게 해서 `JenkinsWebLoginPage.onTokenIssued`와 `SettingsPage._saveToken`을 실행하지 않는다. 따라서 `keychain-store` 검증으로 요구된 token 저장 callback, 성공/실패 UI, session restore callback coverage가 없다.

Before:

```dart
webLoginLauncher: (_) async => const JenkinsWebLoginResult(...)
expect(onSessionSavedCalled, isFalse);
```

#### 해결 방법

- fake launcher가 token callback을 호출할 수 있도록 `JenkinsWebLoginLauncher` signature를 확장하거나, 동등하게 테스트 가능한 seam을 추가한다.
- production default launcher는 `JenkinsWebLoginPage(onTokenIssued: ...)`를 계속 사용한다.
- fake launcher 테스트는 issued token을 callback으로 전달하고 `tokenSaver` 호출 인자, `token-saved-label`, `token-save-error`, `onSessionSaved` 호출 여부를 검증한다.
- `AppSokApp` widget test는 초기 no-session 상태에서 Web Login fake callback을 통해 token 저장/restore 후 job loader가 활성화되는 경로를 검증한다.

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

- [ ] `lib/src/features/settings/settings_page.dart`: launcher/callback surface 정리.
- [ ] `lib/src/features/app_shell.dart`: export와 call site 업데이트.
- [ ] `lib/src/app.dart`: updated typedef에 맞게 wiring 유지.
- [ ] `test/settings_page_test.dart`: launcher call signature 업데이트.
- [ ] `test/widget_test.dart`: AppSokApp web login surface test 업데이트.
- [ ] `test/app_auth_test.dart`: 실제 token callback success/failure/restore tests.

#### 테스트 작성

- 작성/보강:
  - `test/app_auth_test.dart`: fake launcher invokes token callback; success and failure UI assertions.
  - `test/widget_test.dart` 또는 `test/app_auth_test.dart`: fake login callback 후 stored session restore and jobs visible.
  - 기존 `test/settings_page_test.dart`: typedef 변경에 따른 compile/update.

#### 중간 검증

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

예상: actual callback path tests pass.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/src/app.dart` | REVIEW_AUTH_TOKEN-1, REVIEW_AUTH_TOKEN-2 |
| `lib/src/features/settings/settings_page.dart` | REVIEW_AUTH_TOKEN-1, REVIEW_AUTH_TOKEN-2 |
| `lib/src/features/app_shell.dart` | REVIEW_AUTH_TOKEN-2 |
| `test/app_auth_test.dart` | REVIEW_AUTH_TOKEN-1, REVIEW_AUTH_TOKEN-2 |
| `test/settings_page_test.dart` | REVIEW_AUTH_TOKEN-2 |
| `test/widget_test.dart` | 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'
zsh -lc 'cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && flutter build macos'
```

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

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