<!-- task=m-jenkins-credential/01_session_foundation plan=1 tag=API -->

# Plan - API

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

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

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

## 배경

`Artifact 탐색과 다운로드` 검토에서 실제 `AppSokApp` 경로가 Jenkins job/build/download 흐름에 닿지 않는 문제가 확인됐다. [app.dart](/config/workspace/appsok/lib/src/app.dart:15)는 `const AppSokShell()`만 생성하고, shell의 loader/downloader는 테스트에서만 주입된다. 앱 루트 wiring 전에 먼저 저장된 Jenkins base URL과 credential을 읽고 검증할 수 있는 session foundation이 필요하다.

## 사용자 리뷰 요청 흐름

구현 중 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` 작성을 하지 않는다.

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.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-roadmap/current.md`
- `agent-roadmap/phase/usable-mvp/PHASE.md`
- `agent-roadmap/phase/usable-mvp/milestones/artifact-browser.md`
- `agent-roadmap/phase/usable-mvp/milestones/jenkins-credential.md`
- `pubspec.yaml`
- `lib/src/app.dart`
- `lib/src/features/app_shell.dart`
- `lib/src/features/builds/builds_page.dart`
- `lib/src/features/settings/settings_page.dart`
- `lib/src/models/jenkins_build.dart`
- `lib/src/models/pending_install.dart`
- `lib/src/services/artifact_staging_service.dart`
- `lib/src/services/jenkins_client.dart`
- `lib/src/services/token_store.dart`
- `test/artifact_staging_service_test.dart`
- `test/builds_page_test.dart`
- `test/jenkins_client_test.dart`
- `test/widget_test.dart`

### 테스트 환경 규칙

- test_env: local.
- `agent-test/local/rules.md` 존재 및 읽음.
- matched profile: `agent-test/local/artifact-flow-smoke.md` 읽음.
- 적용 명령: remote runner checkout에서 `flutter analyze`, `flutter test`; 보조로 현재 checkout `git diff --check`.
- 구조상 빈/skeleton 규칙 없음.
- `<확인 필요>` 값 없음.
- Jenkins sandbox endpoint는 제공되지 않았으므로 실제 Jenkins smoke는 이 계획의 필수 검증이 아니다.

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

- `TokenStore`는 현재 save/read/clear 구현이 있으나 mock secure storage 기반 테스트가 없다. 이 작업에서 `FlutterSecureStorage.setMockInitialValues` 기반 `test/token_store_test.dart`를 추가한다.
- `JenkinsClient`는 job/build/download stream 테스트가 있으나 저장된 credential 검증에 쓸 current user API가 없다. 이 작업에서 `fetchCurrentUser`와 200/401/403/parser 테스트를 추가한다.

### 심볼 참조

- 변경 예정 symbol: `TokenStore`에 session/base URL API 추가. 기존 `readCredentials`, `saveCredentials`, `clearCredentials` call site는 `rg --sort path "readCredentials|saveCredentials|clearCredentials|TokenStore" lib test`로 확인하고 backward compatibility를 유지한다.
- 변경 예정 symbol: `JenkinsClient`에 `fetchCurrentUser` 추가. 기존 call site 변경 없음.
- 제거 예정 symbol: none.

### 분할 판단

- split decision policy를 적용했다.
- shared task group: `agent-task/m-jenkins-credential/`.
- `01_session_foundation`: 저장된 Jenkins session config와 current-user 검증 API를 만든다. 의존성 없음.
- `02+01_app_wiring`: 01 완료 뒤 앱 루트와 artifact loader/downloader를 연결한다. `02+01_...` 디렉터리명으로 01의 `complete.log`에 의존한다.
- 분할 근거: storage/client foundation과 UI/app root wiring은 서로 다른 실패 모드와 테스트 범위를 갖는다. API foundation이 깨지면 UI wiring 계획을 다시 짜야 하므로 분리한다.

### 범위 결정 근거

- WebView 로그인, crumb 기반 API token 자동 발급, token 폐기 UX, logout UI는 제외한다. 이들은 `jenkins-credential`의 별도 기능 Task이며 이 작업의 목적은 저장된 session을 검증할 foundation이다.
- `BuildsPage`의 build row/download UX는 이미 `artifact-browser` 완료 근거가 있으므로 변경하지 않는다.
- `macos/` entitlement 변경은 제외한다. 이 작업은 새 platform 권한을 요구하지 않는다.

### 빌드 등급

- build lane: `local-G05`.
- review lane: `local-G05`.
- 근거: storage/client API와 단위 테스트 중심의 bounded 변경이며 remote runner에서 deterministic 검증 가능하다.

## 구현 체크리스트

- [ ] `TokenStore`에 Jenkins base URL을 포함한 session config read/save/clear API를 추가하고 기존 credential API 호환성을 유지한다.
- [ ] `JenkinsClient`에 저장 credential 검증용 current-user API와 parser/error 처리를 추가한다.
- [ ] `test/token_store_test.dart`와 `test/jenkins_client_test.dart`에 session/current-user 정상 및 경계 테스트를 추가한다.
- [ ] `git diff --check`와 remote runner `flutter analyze`, `flutter test`를 실행해 결과를 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] Session config storage

문제: [token_store.dart](/config/workspace/appsok/lib/src/services/token_store.dart:14)는 username/api token만 읽고 [token_store.dart](/config/workspace/appsok/lib/src/services/token_store.dart:25)는 credentials만 저장한다. 실제 앱 시작 시 [app.dart](/config/workspace/appsok/lib/src/app.dart:15)에서 Jenkins base URL을 알 방법이 없다.

해결 방법:

Before:

```dart
14  Future<JenkinsCredentials?> readCredentials() async {
15    final username = await _storage.read(key: _usernameKey);
16    final apiToken = await _storage.read(key: _apiTokenKey);
17
18    if (username == null || apiToken == null) {
19      return null;
20    }
21
22    return JenkinsCredentials(username: username, apiToken: apiToken);
23  }
```

After:

```dart
class JenkinsSessionConfig {
  const JenkinsSessionConfig({required this.baseUrl, required this.credentials});

  final Uri baseUrl;
  final JenkinsCredentials credentials;
}

Future<JenkinsSessionConfig?> readSession() async { ... }
Future<void> saveSession(JenkinsSessionConfig session) async { ... }
Future<void> clearSession() async { ... }
```

기존 `readCredentials`, `saveCredentials`, `clearCredentials`는 유지한다. `readSession`은 base URL, username, token 중 하나라도 없거나 base URL parse가 실패하면 `null`을 반환한다. `clearSession`은 base URL과 credential keys를 모두 삭제한다.

수정 파일 및 체크리스트:

- [ ] `lib/src/services/token_store.dart`: `_baseUrlKey`, `JenkinsSessionConfig`, `readSession`, `saveSession`, `clearSession` 추가.
- [ ] `lib/src/services/token_store.dart`: 기존 credential API가 기존 테스트/호출과 호환되도록 유지.
- [ ] `test/token_store_test.dart`: `FlutterSecureStorage.setMockInitialValues({})`로 mock storage 초기화.

테스트 작성:

- 추가: `test/token_store_test.dart`
- test names:
  - `readSession returns null when config is incomplete`
  - `saveSession and readSession round trip base url and credentials`
  - `clearSession removes base url and credentials`
  - `readCredentials remains backward compatible`

중간 검증:

```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/token_store_test.dart'"
```

기대 결과: 해당 파일의 모든 테스트 통과.

### [API-2] Current user validation API

문제: [jenkins_client.dart](/config/workspace/appsok/lib/src/services/jenkins_client.dart:44)는 job 목록 API, [jenkins_client.dart](/config/workspace/appsok/lib/src/services/jenkins_client.dart:68)는 build 목록 API만 제공한다. 저장 credential을 앱 시작 시 검증할 API가 없어 `jenkins-credential`의 `[startup-check]`를 구현할 foundation이 없다.

해결 방법:

Before:

```dart
68  Future<List<JenkinsBuild>> fetchRecentBuilds({
69    required Uri jobUrl,
70    required String jobName,
71    required JenkinsCredentials credentials,
72  }) async {
```

After:

```dart
Future<JenkinsUser> fetchCurrentUser({
  required Uri baseUrl,
  required JenkinsCredentials credentials,
}) async { ... }

class JenkinsUser {
  const JenkinsUser({required this.id, required this.displayName});
  final String id;
  final String displayName;
}
```

`fetchCurrentUser`는 `baseUrl` 아래 `whoAmI/api/json` 또는 Jenkins 사용자 확인용 Remote API endpoint를 호출하고 `_headers(credentials)`를 재사용한다. 401/403은 기존 `JenkinsClientException`으로 전달해 후속 wiring에서 auth failure와 network failure를 구분할 수 있게 한다. 응답 parser는 null/blank display name을 안전하게 fallback 처리한다.

수정 파일 및 체크리스트:

- [ ] `lib/src/services/jenkins_client.dart`: `JenkinsUser` 모델 추가.
- [ ] `lib/src/services/jenkins_client.dart`: `fetchCurrentUser` 추가, `_appendPath` 재사용.
- [ ] `test/jenkins_client_test.dart`: current user API URI, auth header, success parsing, forbidden error 테스트 추가.

테스트 작성:

- 추가/수정: `test/jenkins_client_test.dart`
- test names:
  - `fetchCurrentUser requests whoAmI api with auth header`
  - `fetchCurrentUser parses id and display name with fallback`
  - `fetchCurrentUser throws JenkinsClientException on forbidden response`

중간 검증:

```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/jenkins_client_test.dart'"
```

기대 결과: 기존 Jenkins client 테스트와 신규 current-user 테스트 모두 통과.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/src/services/token_store.dart` | API-1 |
| `lib/src/services/jenkins_client.dart` | API-2 |
| `test/token_store_test.dart` | API-1 |
| `test/jenkins_client_test.dart` | API-2 |

## 최종 검증

```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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
