diff --git a/agent-roadmap/phase/usable-mvp/milestones/jenkins-credential.md b/agent-roadmap/archive/phase/usable-mvp/milestones/jenkins-credential.md similarity index 78% rename from agent-roadmap/phase/usable-mvp/milestones/jenkins-credential.md rename to agent-roadmap/archive/phase/usable-mvp/milestones/jenkins-credential.md index ac000ae..2953705 100644 --- a/agent-roadmap/phase/usable-mvp/milestones/jenkins-credential.md +++ b/agent-roadmap/archive/phase/usable-mvp/milestones/jenkins-credential.md @@ -11,7 +11,7 @@ AppSok이 Jenkins에 접근할 수 있는 사용자인지 확인하고, 사용 ## 상태 -[진행중] +[완료] ## 승격 조건 @@ -48,18 +48,22 @@ AppSok이 Jenkins에 접근할 수 있는 사용자인지 확인하고, 사용 - [x] [startup-check] 앱 시작 시 Keychain credential을 읽고 Jenkins 사용자 확인 API로 조용히 검증한다. - [x] [auth-state] 검증 성공 시 로그인 화면을 건너뛰고 build 목록으로 진입한다. -- [ ] [reauth] token 폐기, 권한 없음, Jenkins URL 변경, 네트워크 실패를 구분해 재로그인 또는 재시도 흐름으로 안내한다. +- [x] [reauth] token 폐기, 권한 없음, Jenkins URL 변경, 네트워크 실패를 구분해 재로그인 또는 재시도 흐름으로 안내한다. - [x] [logout-clear] 사용자가 저장된 credential을 삭제하고 다시 로그인할 수 있다. ## 완료 리뷰 -- 상태: 없음 -- 요청일: 없음 -- 완료 근거: 모든 기능 Task와 Task 안에 명시된 검증 충족 후 기록한다. +- 상태: 승인됨 +- 요청일: 2026-06-13 +- 완료 근거: + - `agent-task/archive/2026/06/m-jenkins-credential/04+03_reauth_ui/complete.log`의 Roadmap Completion에서 `reauth` PASS가 기록됐다. + - `plan_cloud_G06_2.log`와 `code_review_cloud_G06_2.log`가 PASS evidence로 남았고, `flutter analyze`와 전체 `flutter test` 검증이 통과했다. + - Web Login token 발급 실패가 성공 로그인으로 보이는 UX를 보완했고, remote Mac runner에서 `flutter analyze`, `flutter test`, `flutter build macos`가 통과했다. + - Milestone 기능 Task 전체가 `[x]`가 되어 Jenkins credential 수명주기 범위가 완료됐다. - 리뷰 필요: - - [ ] 사용자가 완료 결과를 확인했다 - - [ ] archive 이동을 승인했다 -- 리뷰 코멘트: 없음 + - [x] 사용자가 완료 결과를 확인했다 + - [x] archive 이동을 승인했다 +- 리뷰 코멘트: 사용자 요청에 따라 완료 처리하고 archive로 이동한다. ## 범위 제외 diff --git a/agent-roadmap/phase/usable-mvp/PHASE.md b/agent-roadmap/phase/usable-mvp/PHASE.md index 98b2480..2815d2e 100644 --- a/agent-roadmap/phase/usable-mvp/PHASE.md +++ b/agent-roadmap/phase/usable-mvp/PHASE.md @@ -23,10 +23,10 @@ Jenkins에서 Android artifact를 찾고, Mac에 내려받고, USB로 연결된 - [완료] USB 설치와 device 선택 - 경로: `agent-roadmap/archive/phase/usable-mvp/milestones/usb-install.md` - 요약: 연결된 Android device를 고르고 선택한 APK를 `adb install -r`로 설치한다. -- [진행중] Jenkins 로그인과 credential 수명주기 - - 경로: `agent-roadmap/phase/usable-mvp/milestones/jenkins-credential.md` +- [완료] Jenkins 로그인과 credential 수명주기 + - 경로: `agent-roadmap/archive/phase/usable-mvp/milestones/jenkins-credential.md` - 요약: 사용자별 Jenkins API token 또는 WebView 기반 token 자동화 흐름을 안전하게 저장/검증한다. -- [계획] ADB 콘솔과 logcat +- [진행중] ADB 콘솔과 logcat - 경로: `agent-roadmap/phase/usable-mvp/milestones/logcat-console.md` - 요약: 설치 후 바로 문제를 볼 수 있도록 logcat stream, filter, pause/clear 기능을 제공한다. diff --git a/agent-roadmap/phase/usable-mvp/milestones/logcat-console.md b/agent-roadmap/phase/usable-mvp/milestones/logcat-console.md index 643055c..f5cf319 100644 --- a/agent-roadmap/phase/usable-mvp/milestones/logcat-console.md +++ b/agent-roadmap/phase/usable-mvp/milestones/logcat-console.md @@ -11,7 +11,7 @@ ## 상태 -[계획] +[진행중] ## 승격 조건 diff --git a/lib/src/features/settings/settings_page.dart b/lib/src/features/settings/settings_page.dart index 3d2e47f..d900598 100644 --- a/lib/src/features/settings/settings_page.dart +++ b/lib/src/features/settings/settings_page.dart @@ -144,14 +144,21 @@ class _SettingsPageState extends State { final result = await launcher(baseUrl); if (!mounted) return; if (result != null) { + if (result.apiToken == null) { + setState(() { + _webLoginError = 'API token 발급에 실패했습니다. Web Login을 다시 실행하세요.'; + _lastLoginResult = null; + _webLoginLoading = false; + }); + return; + } + widget.onWebLoginCompleted?.call(result); setState(() { _lastLoginResult = result; _webLoginLoading = false; }); - if (result.apiToken != null) { - await _saveToken(baseUrl, result.username, result.apiToken!); - } + await _saveToken(baseUrl, result.username, result.apiToken!); } else { setState(() => _webLoginLoading = false); } diff --git a/test/app_auth_test.dart b/test/app_auth_test.dart index 184f824..ef70417 100644 --- a/test/app_auth_test.dart +++ b/test/app_auth_test.dart @@ -371,7 +371,7 @@ void main() { ); testWidgets( - 'SettingsPage skips _saveToken when launcher returns result without apiToken', + 'SettingsPage shows error when launcher returns result without apiToken', (tester) async { var tokenSaverCalled = false; @@ -403,7 +403,8 @@ void main() { await tester.pumpAndSettle(); expect(tokenSaverCalled, isFalse); - expect(find.byKey(const ValueKey('web-login-result')), findsOneWidget); + expect(find.byKey(const ValueKey('web-login-result')), findsNothing); + expect(find.textContaining('API token 발급에 실패했습니다'), findsOneWidget); expect(tester.takeException(), isNull); }, ); diff --git a/test/settings_page_test.dart b/test/settings_page_test.dart index d8889cf..3f65fa5 100644 --- a/test/settings_page_test.dart +++ b/test/settings_page_test.dart @@ -77,6 +77,7 @@ void main() { webLoginLauncher: (_) async => const JenkinsWebLoginResult( username: 'toki', displayName: 'Toki Lab', + apiToken: 'issued-token', ), ), ), @@ -99,6 +100,33 @@ void main() { expect(find.textContaining('apiToken'), findsNothing); }); + testWidgets('shows token issue error when web login returns no apiToken', ( + tester, + ) async { + await tester.pumpWidget( + _wrap( + SettingsPage( + webLoginLauncher: (_) async => const JenkinsWebLoginResult( + username: 'toki', + displayName: 'Toki Lab', + ), + ), + ), + ); + + await tester.enterText( + find.byType(TextField).first, + 'https://jenkins.example.com', + ); + await tester.pump(); + + await tester.tap(find.byKey(const ValueKey('jenkins-web-login-button'))); + await tester.pumpAndSettle(); + + expect(find.byKey(const ValueKey('web-login-result')), findsNothing); + expect(find.textContaining('API token 발급에 실패했습니다'), findsOneWidget); + }); + testWidgets('clears stored credential through sessionClearer', ( tester, ) async { @@ -158,10 +186,7 @@ void main() { ); await tester.pumpAndSettle(); - expect( - find.byKey(const ValueKey('token-saved-label')), - findsOneWidget, - ); + expect(find.byKey(const ValueKey('token-saved-label')), findsOneWidget); // Change URL — all transient labels must be cleared. await tester.enterText( diff --git a/test/widget_test.dart b/test/widget_test.dart index ecf3c6f..380c3fe 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -41,7 +41,9 @@ class _FakeJenkinsArtifactSession extends JenkinsArtifactSession { @override Future restoreDetailed() async => - const JenkinsSessionRestoreResult(kind: JenkinsSessionRestoreKind.restored); + const JenkinsSessionRestoreResult( + kind: JenkinsSessionRestoreKind.restored, + ); @override Future> loadJobs() async => [ @@ -1395,6 +1397,7 @@ void main() { webLoginLauncher: (_) async => const JenkinsWebLoginResult( username: 'toki', displayName: 'Toki Lab', + apiToken: 'issued-token', ), onWebLoginCompleted: (result) => capturedResult = result, ),