2e97ee9a8431537be1b45475f6c1e9637a853f7f
21
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2e97ee9a84 |
refactor: remove verified dead code across data, UI, and resources (#680)
* refactor: remove unused dead code across data, UI, and resources * fix: resolve dangling references left by dead-code removal Restore empty object Motion (braces required) and remove the 12 stale test usages that still called the now-deleted production symbols: - ServerStoreTest: removeServer/switchToServer tests - ServerEndpointTest: isCleartext assertions - ConnectViewModelTest: onHostChange/onPortChange tests + 3 dropped R.string refs - SettingsViewModelTest: renameProfile tests + clearNavigateToLogin - E2eIntegrationTest: previewSkill tests + onHostChange/onPortChange calls Verified locally: compileDebugUnitTestKotlin + 48 tests pass. * fix: remove unused imports flagged by ktlint Drops 4 no-longer-referenced imports left by the dead-code removal: - HermesApiService: SkillScanResponse (scanHubSkill removed) - ServerEndpointTest: assertFalse, assertTrue (isCleartext assertions dropped) - SettingsViewModelTest: assertFalse (rename tests removed) --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
c8a5055a84 |
fix(auth): support __Host-/__Secure- prefixed session cookies and proxy-relative paths (issue #635) (#648)
- CookieSerialization: add SESSION_COOKIE_NAMES (__Host-, __Secure-, bare) and isSessionCookieName(); isSessionCookie now matches any variant. - PersistentCookieJar.getSessionCookieValue(): iterate SESSION_COOKIE_NAMES, returning the strictest prefixed match when present. - HermesWsClient.refreshWsTicketIfNeeded(): drop the bare-name session-cookie preflight in gated mode — the server may hold a valid refresh cookie under a prefixed name; the authenticated ticket endpoint is the authority. Ticket URL already derived from endpoint (issue #634). - HermesWsClient: stop logging WS close/failure reason strings and exception messages (class name only) to avoid leaking server context / URLs. - ProfileScopeInterceptor: match profile-scoped paths via endpoint-relative request path so reverse-proxy prefixes (e.g. /hermes/api/...) are honored; wrap endpoint read in runCatching. Verified: ktlintCheck, testDebugUnitTest, lintDebug all green locally. Co-authored-by: M57 <Hy4ri@users.noreply.github.com> |
||
|
|
26b104b2bb |
[feature] issue #634: HTTPS + reverse-proxy server endpoint support (#638)
* [feature] issue #634: HTTPS + reverse-proxy server endpoint support - Add ServerEndpoint model (scheme-aware base URL, resolve/webSocketUrl/relativeRequestPath, cleartext policy) - AuthManager: baseUrl is canonical; setHost/setPort derive from endpoint; endpointForBuild() for scheme-aware URLs - ApiClient/CookieManager/HermesWsClient: route through ServerEndpoint (no hardcoded http://) - Data migration: ServerUrlMigration converts legacy host/port to baseUrl - UI: Connect/AuthLogin/Settings now use a single Server URL field with transport warning - Build: ALLOW_CLEARTEXT buildConfigField + manifest placeholder for dev - Tests: ServerEndpointTest + ServerUrlMigrationTest; adapt existing suite to baseUrl API Co-Authored-By: M57 <Hy4ri@users.noreply.github.com> * fix(authlogin): basic-auth derives token automatically, no manual entry connectBasicAuth() built a fresh OkHttpClient for the password-login call and a different one for the ws-ticket call, so the Set-Cookie from /auth/password-login (the session token lives in the cookie, not the JSON body) never reached /api/auth/ws-ticket. That 401'd the ticket request, returned null, and the UI fell back to asking the user to paste a token. Both calls now use the shared OkHttpProvider.probe client (same CookieManager jar), so the session cookie flows through and the WS ticket mints automatically — matching the dashboard contract (routes.py: password-login sets Set-Cookie, ws-ticket requires it). Add AuthLoginBasicAuthFlowTest proving the cookie propagates on the shared client (and that ws-ticket 401s without it). Fixes the basic-auth token prompt reported against issue #634. * chore: remove TASK-634-TESTS.md (stray task-tracking doc, not part of feature) * ci: broaden .gitguardian.yml test-file ignores (unit + instrumented + variant dirs) * feat(authlogin): smart auth-mode detection from /api/status + drop pairing-string login - Replace 302-sniff probe with authoritative GET /api/status read (auth_required + auth_providers) to derive DashboardAuthMode. - Add OAUTH mode (stubbed, 'coming soon' UI, TODO tracks issue #639). - Render login UI by detected mode: token (loopback), username+password (basic/gated), or OAuth notice. - Remove legacy pairing-string box + onPairingString/applyLegacyPairing. - Extract internal deriveAuthMode() + add 5 unit tests. Closes nothing; OAuth tracked in #639. * fix(chat): don't block WS connect on empty token in gated (ticket) mode connectWebSocket() gated on getToken() != null, correct for loopback (token) mode but wrong for gated (ticket) mode: the ticket is minted fresh by HermesWsClient.refreshWsTicketIfNeeded() from the persisted session cookie, so getToken() is expected empty at connect time. The early return left the WS unopened -> DISCONNECTED -> chat showed 'reconnect' immediately after basic-auth login. Only require a non-blank token in non-gated mode. Fixes #640. * test: remove onPairingString test cases for deleted pairing-login path The pairing-string login (onPairingString/applyLegacyPairing) was removed from ConnectViewModel, so the 5 tests that exercised it (3 in ConnectViewModelTest, 2 in E2eIntegrationTest) no longer compile. Remove them. Also drop the now-orphaned connect_pairing_string from values-ko. Fixes the Unit & Integration Tests CI failure on PR #638. --------- Co-authored-by: hy4ri <hy4ri@users.noreply.github.com> |
||
|
|
d1fc477bcd |
fix: resolve all build compilation and style check warnings (#597)
* fix: resolve all build compilation and style check warnings * fix: remove unused import in SystemViewModel |
||
|
|
c543eb6470 |
feat(#530): server-backed session search (FTS5) (#555)
* feat(#530): add SessionSearch models + searchSessions endpoint + contract tests * feat(#530): wire server-backed search into Sessions screen (debounce + render) * style(#530): ktlint import ordering on HermesApiService * fix(#530): ktlint-clean overlap wrap + map search snippet to title * fix(#530): map search snippet to title so results show + highlight * fix(#530): render search hits as honest result cards instead of faking title Backend SessionSearchResult carries no session name (only snippet + metadata), so mapping snippet->title made the card show raw excerpt as the title. Instead, render a dedicated SearchResultCard: 'Match' label + session id, highlighted snippet as body, and source/model/played-at chips. Added SessionInfo.model and a friendly relative-time formatter. ktlint clean, compileDebugKotlin + unit tests pass. * fix(#530): strip JSON from search snippet + tighten result card padding * fix(#530): strip FTS highlight markers from search snippet on client agy's backend change re-wraps matched terms in >>>term<<< markers inside the clean snippet, but the mobile highlights by raw query term and never consumed those markers — so literal '>>>'/'<<<' would render in result text. Strip the markers before passing the snippet to highlightText so the query highlight still works on clean text. * fix(#530): integrate tree-flattening hierarchy with search modes in SessionsScreen * fix(#530): fix test assertion and mock ApiClient in SessionsViewModelTest * fix(#530): resolve compose PaddingValues compile error and fix slash-preservation test path * fix(#530): clean search snippet from JSON payload and add unit test --------- Co-authored-by: M57 <m57@hermes-control.app> |
||
|
|
907d247312 |
fix: refresh loopback dashboard token after restart (#568)
* fix: refresh loopback dashboard token after restart The gateway regenerates its ephemeral session token on every restart, so the token the app persisted becomes stale. After a dashboard restart REST calls 401 and the WebSocket auto-reconnect loop hangs in AUTH_EXPIRED until the user manually re-authenticates. - Add DashboardSessionTokenRefresher: re-reads the injected window.__HERMES_SESSION_TOKEN__ from the dashboard SPA and writes it back to AuthManager. - TokenRefreshAuthenticator: on a 401 retry, scrape a fresh token from the dashboard and retry instead of giving up. - HermesWsClient: refresh the token before each non-gated WebSocket handshake so reconnects don't get stuck on a stale token; guard serverStore read against IllegalStateException. Covers the loopback (--insecure) deployment path; gated/ticket mode is untouched. * fix: harden dashboard token refresher (sync, body cap, blank-token test) Second-opinion review flagged robustness gaps (no correctness/security regressions in the happy path): - DashboardSessionTokenRefresher.refresh(): @Synchronized to dedupe concurrent 401 / reconnect storms instead of each firing its own GET. - Cap SPA HTML read at 64 KiB before regex scan (defensive against OOM on low-end devices); we only need the injected token near the document top. - HermesWsClient: log (not silently swallow) the transient uninitialized serverStore case so a real misconfig is still visible. - Add test: blank-injected token returns null. --------- Co-authored-by: M57 <m57@hermes-control.app> |
||
|
|
9e10c813d4 |
feat(#534): add OAuth provider management screen + credential_warning surfacing (#566)
* feat(#534): add OAuth provider management screen + credential_warning surfacing * style(#534): fix ktlint import-ordering in ProvidersViewModel * fix(#534): address agy review — terminal DONE/ERROR dialog states + PKCE browser launch * fix(#534): add Done/Close dialog strings * fix(#534): address agy review - terminal DONE/ERROR dialog states, real PKCE browser launch, clear flow state --------- Co-authored-by: M57 <m57@hermes-control.app> |
||
|
|
3052db7d65 |
feat: paginate, sync, and display branched sessions (#563)
* mobile: 以 Desktop Sessions 作为聊天历史源 * mobile: 修复快速连接崩溃与无标题会话隐藏 * mobile: 对齐 Desktop 标题并恢复完整历史 * mobile: 分离 Desktop 存储 ID 与运行 Session ID * mobile: 将新对话持久化为 Desktop Session * mobile: 渲染 Desktop 本地媒体 * feat: 分页加载并同步当前会话 * fix: 显示分支会话名称与层级 * fix: align PR with upstream app contract * fix: preserve upstream session RPC lifecycle |
||
|
|
9156064d2b |
feat(#537): analytics screen — usage + per-model stats (#558)
* feat(#537): analytics screen — usage + per-model stats from /api/analytics/* * fix(#537): address PR review — cancel-in-flight loads, retain cache on refresh, drop redundant async * fix(#537): surface real analytics failure, mirror backend shape (period_days/tools), round-trip test * fix(#537): tolerate float last_used_at in analytics (backend emits epoch as number) * fix(#537): type tools[] as AnalyticsToolUsage (tool/count/percentage), render Top tools section * fix(analytics): fix crashes and refactor tab layout * perf(analytics): cache responses and prefetch day windows --------- Co-authored-by: M57 <m57@hermes-control.app> |
||
|
|
c5d1ef1c42 |
fix(network): scope management REST calls with ?profile= (fixes #528) (#540)
* fix(network): scope management REST calls with ?profile= (fixes #528) Mobile fired all management REST calls with NO profile scope, so every profile read/wrote the backend's implicit default profile — multi-profile setups silently corrupted config/skills/toolsets/mcp/model/env. Add ProfileScopeInterceptor (OkHttp) that appends ?profile=<activeProfile> to the same endpoint families the desktop/web dashboard scopes (web/src/lib/api.ts PROFILE_SCOPED_PREFIXES), respecting: - no active profile -> pass through (legacy default behavior) - explicit ?profile= on the URL wins over the global scope - only scoped families rewritten; ops/pairing/cron/profiles stay global Wired into ApiClient.buildService(). 5 MockWebServer tests cover scoped append, explicit-param precedence, no-profile passthrough, non-scoped skip, and skills scoping. Mirrors desktop/web contract derived from audit of /opt/hermes-agent. * fix(ci): stub getSelectedProfileId in ApiClientTest + harden prefix match (fixes PR #540) - ApiClientTest: stub AuthManager.getSelectedProfileId() to null. The new ProfileScopeInterceptor (registered in ApiClient.buildService) calls it, and the unstubbed real method threw IllegalStateException at AuthManager.kt:56, breaking all 4 auth-interceptor tests. - ProfileScopeInterceptor: replace startsWith(prefix) with whole-segment match (path == prefix || path.startsWith(prefix/)) so lookalike paths like /api/statusXYZ are NOT wrongly scoped (Sourcery review, PR #540). - ProfileScopeInterceptorTest: add lookalikePath_notScoped + scopedSubPath_isScoped to lock the segment-boundary fix. |
||
|
|
d178d7f69e |
refactor: proper OkHttp CookieJar integration (issue #470) (#483)
Replace the manual session-cookie interceptor in ApiClient and the manual cookie branch in TokenRefreshAuthenticator with a shared PersistentCookieJar backed by an encrypted, per-server EncryptedCookieStore. - Add CookieStore iface + EncryptedCookieStore (EncryptedSharedPreferences, per-server scope, legacy single-cookie migration) - Add CookieSerialization (OkHttp Cookie <-> holder) + PersistentCookieJar (in-mem cache, async persist, atomic useStore scope switching, prune, session-cookie accessor) - Add CookieManager coordinator (init/useStore/prune/clear) and wire its cookieJar into OkHttpProvider base/websocket/probe (lazy) - AuthManager.get/setSessionCookie now delegate to CookieManager; add pruneServerCache(); clear path drops the scope's session cookie - AuthLoginViewModel + HermesWsClient rely on the jar for Set-Cookie capture and session-cookie re-attachment (no manual header injection) - Add fakes (FakeEncryptedCookieStore/buildFakePersistentCookieJar) + unit tests (PersistentCookieJarTest, CookieManagerTest); inject fake jar into ApiClientTest/HermesWsClientTest setup WS token-out-of-URL (step 5) deferred: backend hermes-agent authenticates WS only via ws.query_params, so moving it requires a coordinated backend change (noted for follow-up). Verified: testDebugUnitTest (334 passing) + assembleDebug green via local SDK. |
||
|
|
5697405b19 |
refactor: migrate Gson serialization to kotlinx.serialization (#475)
* refactor: migrate Gson serialization to kotlinx.serialization * fix(dto): resolve serializing Any? and Map<String, Any?> with JsonElement * test(refactor): fix unit test issues under kotlinx.serialization and update nullable DTO defaults * fix(chat): support flexible numeric and string decoding for message timestamps * fix(chat): sequentialize cache loading to avoid overwriting network state |
||
|
|
1c3a5386f4 |
fix(#472): refactor ChatViewModelTest — Fakes + Turbine, drop Mockk (#474)
* fix(#472): refactor ChatViewModelTest to use Fake* + Turbine Replace Mockk-heavy test setup with FakeChatPersistenceRepository (+ in-memory FakeChatMessageDao) and add Turbine for Flow assertions. Changes: - Add Turbine v1.2.0 dependency (libs.versions.toml + build.gradle.kts) - Create FakeChatMessageDao – in-memory DAO with MutableStateFlow - Create FakeChatPersistenceRepository – delegates to FakeDao - ChatViewModel: make repo injectable (@VisibleForTesting lateinit var) - Rewrite ChatViewModelTest (751 lines vs 1,212): clearer helpers, Turbine test for streaming Flow sequence, same 29 tests with identical assertions, ~38% line reduction Test: ktlint --format passes clean * fix(#472): address review — constructor injection, ConcurrentHashMap, MockWebServer tests Addresses all feedback from Sourcery review on PR #474: Constructor injection (was lateinit var): - Move repo to ChatViewModel primary constructor with default - Remove @VisibleForTesting lateinit var + init block assignment - Tests pass repo directly via constructor Simplify FakeChatMessageDao: - Replace manual synchronized(lock) with ConcurrentHashMap - Same thread safety, 48% less code, no boilerplate lock object New MockWebServer tests: - HermesApiServiceMockWebServerTest: 6 tests covering getStatus, getSessions, getSessionMessages + edge cases (null fields, empty lists, slash-encoded session IDs) * fix: CI compilation errors — open class, TestScope receiver, runBlocking Three compilation errors blocking CI: - ChatPersistenceRepository was final, but FakeChatPersistenceRepository extends it → make it open class - createViewModelWithSession() called advanceUntilIdle (TestScope ext) and emit() (suspend) without a TestScope receiver or coroutine ctx → make it fun TestScope.xxx() and use tryEmit() for non-suspend emission - HermesApiServiceMockWebServerTest called suspend API methods from regular @Test methods → wrap each test in runBlocking * fix: test runtime failures — suspend helper + fix JSON escaping - Use so emit() (suspend) is valid and events are delivered synchronously instead of buffered via tryEmit() which was causing 17 test failures - Fix MalformedJsonException in MockWebServer test by using plain string content instead of unescaped JSON-within-JSON * fix: correct req-id values in ChatViewModelTest — SESSION_CREATE is req-id-3 * fix(test): correct ChatViewModelTest assertions for Fakes+Turbine refactor - testMessageStreamingFlow: collect streamingState via Turbine (ThinkingDelta/MessageToken mutate streamingState, not uiState) - testToolExecution_finalizesPreviousStreamingMessage: account for messages[0] = "Session created" system message - testMessageStart_finalizesPreviousStreamingMessage: same index offset (messages[1] is the finalized stream) - testClarifyRequestCustomResponse: options is emptyList(), not null; message count is 3 (session-create + user clarify reply) Fixes CI failures introduced in PR #474. --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
846a9380e2 |
feat(#434): System tab 1:1 parity with dashboard (#461)
* feat(#434): add system tab model files * feat(#434): add system API endpoints * feat(#434): rewrite SystemViewModel with full parity * feat(#434): add system screen UI and string resources * feat(#434): update tests for new SystemStatsResponse & ViewModel * fix(#434): fix compile errors — nullable Boolean, SmallFilledTonalButton -> FilledTonalButton * fix(#434): fix old SystemStatsResponse refs in ChatViewModel, wrap sections in item{} blocks * fix(#434): rename sections to lowercase, fix LazyListScope extensions * fix(#434): wrap hook dialog in item{}, pass spacing param to actionLogSection * fix(#434): update HermesApiServiceTest for new typed SystemStatsResponse * fix(#434): fix E2E test coroutine timing, remove outdated Gson edge-case test * fix(#434): fix JUnit 4 assertNotNull argument order * fix(#434): simplify HermesApiServiceTest, restore all mocks in E2e test * fix(#434): remove Gson-edge-case test, fix E2E test dispatcher wiring - Removed testGetSystemStats_nonNumericPercent_returnsNull: tests Gson type-coercion behavior, not app logic — throws JsonSyntaxException - Fixed E2eIntegrationTest dispatcher: mock Dispatchers.IO with answers {} that returns the current Dispatchers.Main dynamically, so runTest's per-test dispatcher override is honored by both viewModelScope.launch and async(Dispatchers.IO) blocks → advanceUntilIdle() advances all work - Removed Dispatchers.Main static mock so runTest can override it * fix(#434): fix E2E test dispatcher for SystemViewModel - Removed Dispatchers.setMain + testDispatcher field from setUp - runTest handles Dispatchers.Main automatically — don't override it - Mock Dispatchers.IO with answers { Dispatchers.Main } so it dynamically resolves to whatever Main is at call time (runTest's TestScope dispatcher) - This ensures viewModelScope.launch and async(Dispatchers.IO) both run on the same TestScope dispatcher, making advanceUntilIdle() effective for all work * fix(#434): use runTest(testDispatcher) for SystemViewModel test The SystemViewModel.loadAll() uses async(Dispatchers.IO) inside coroutineScope { }. With the mocked Dispatchers setup, both Main and IO return testDispatcher, but runTest()'s default TestScope uses a different dispatcher instance whose advanceUntilIdle() can't advance testDispatcher's scheduler. Fix: pass testDispatcher to runTest() so the TestScope's scheduler matches testDispatcher's scheduler, making advanceUntilIdle() effective for all async work. * fix(#434): align test assertion with toast message change The SystemViewModel.triggerBackup() toast message was changed from 'Backup triggered successfully' to 'Backup triggered', but the E2eIntegrationTest assertion wasn't updated, causing a ComparisonFailure on the unit-tests CI job. * fix(#434): address review issues - Fix #1: Change triggerBackup() return type from Unit to ActionResponse so backupArchive is captured, enabling the download button - Fix #2: Replace deprecated LocalClipboardManager with LocalClipboard suspend API (removes CI deprecation warning) - Fix #3: Consolidate redundant 'Hermes version' InfoRow in gateway section (sessions + PID shown in one row) - Remove orphaned system_gateway_state_pid string resource - Fix import ordering for ktlint compliance * fix(#434): revert LocalClipboard change, use @Suppress('DEPRECATION') instead LocalClipboard.setText is not available in the Compose version used by the project. Reverting to LocalClipboardManager with a suppress annotation to keep the deprecation warning clean without breaking compilation. --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com> Co-authored-by: Hermes Agent <hermes@agent.local> |
||
|
|
85c4662537 |
feat: optimize network stack (#404)
* feat: optimize network stack and connection management Refactor OkHttp connection pools, consolidate Gson serialization, add network-aware WebSocket reconnects, and deduplicate concurrent requests. * test: update mapping assertions for AuthExpired * refactor: address PR review feedback on network stack Simplify health check, improve authenticator cookie matching robustness, resolve nit imports, and add unit tests. |
||
|
|
c1af989220 |
feat(#349): Smart auth login flow — probe dashboard and show correct fields (#351)
* feat(#349): smart auth login flow — probe dashboard, show correct fields Landing screen with two buttons (Auth Login / Pairing Login) replaces ConnectScreen as the start screen. Auth login probes the dashboard to detect what auth it needs (token-only, basic-auth, or both) and shows only the relevant input fields. Changes: - NavigationKeys.kt: add LandingScreen, AuthLoginScreen NavKeys - LandingScreen.kt: new screen with hero icon + two login buttons - AuthLoginScreen.kt: IP/Port input, probe logic, dynamic field rendering - AuthLoginViewModel.kt: probe API, dashboard auth detection, connect flow - Navigation.kt: wire new screens, start on LandingScreen, hide bottom bar - strings.xml: add string resources for both screens * fix(#351): align AuthLoginScreen field names with ViewModel API, fix TAG refs - state.isProbing → state.probing - DashboardAuthMode.BOTH → DashboardAuthMode.ALL - viewModel.probe() → viewModel.probeDashboard() - state.isConnecting → state.isLoading - Removed unused TAG constant, replaced with inline tag string - Made connectTokenOnly suspend to fix safeApiCall compilation This fixes the CI compilation errors. * fix(#351): revert probeDashboard() call to probe() to match VM * fix(#351): rename isProbing to probing in ViewModel * fix(#351): follow redirects in basic auth login — / redirects to /login * fix(#351): show all fields when basic auth detected — still needs session token for WS * refactor(#349): remove old ConnectScreen, clean up NavKey + navigation * fix(#351): validate token against /api/sessions (auth-protected) instead of /api/status (public) /api/status is a public endpoint that returns 200 even with wrong tokens, causing invalid tokens to be saved and the WebSocket to spin forever on the chat screen. /api/sessions requires auth and returns 401 on bad tokens. * fix(#351): rewrite basic auth login — POST to /auth/password-login, then fetch SPA with cookie to extract token * fix(#351): use ticket-based WS auth when basic auth is active The updated dashboard on 0.0.0.0 always has auth_required=True, which rejects legacy ?token= in favor of ?ticket= (single-use, 30s TTL). Added AuthManager.setWsAuthParam() to support both ?token= (loopback) and ?ticket= (gated mode) in wsUrl(). connectBasicAuth now mints a ticket via POST /api/auth/ws-ticket after login instead of extracting the token from the SPA. * fix(#351): use requirePrefs() instead of direct prefs reference in wsUrl() * fix(#351): handle relaxed-mock empty string in wsUrl() authParam fallback * feat(#349): auto-extract session token from SPA HTML during probe The probe now extracts the __HERMES_SESSION_TOKEN__ value from the dashboard SPA and auto-populates the token field in TOKEN_ONLY mode, eliminating the need for manual token pasting. - Introduced ProbeResult data class to carry authMode + extractedToken - Regex extraction of __HERMES_SESSION_TOKEN__ from dashboard HTML - Token field auto-filled on probe completion for TOKEN_ONLY mode * fix(#349): return BASIC_AUTH from probe when basic auth detected Bug: probe returned ALL instead of BASIC_AUTH for basic-auth-only dashboards, causing token+credential fields to show and the connect flow to fall through to connectTokenOnly() on basic auth failure, producing a confusing 'Unauthorized (HTTP 401)' NetworkResult error. Fix: probe now returns DashboardAuthMode.BASIC_AUTH when a redirect to /login is detected. BASIC_AUTH in connect() calls connectBasicAuth() directly with no fallback. ALL mode also no longer falls through to connectTokenOnly(). * fix(#349): use session cookie for REST API in gated mode The dashboard's gated mode authenticates REST API requests via session cookies (hermes_session_at), not Bearer tokens. The app was storing the WS ticket as the token and sending it as Authorization: Bearer <ticket> for all REST calls, which the server rejected with 401. - Extract hermes_session_at cookie from Set-Cookie header after POST /auth/password-login and store it in AuthManager - ApiClient authInterceptor now sends Cookie header when session cookie is present, falls back to Bearer token for loopback mode - ConnectResult carries both WS credential (ticket) and session cookie separately - WS ticket used only for WebSocket (?ticket=), session cookie used for REST API * feat(#349): add logout button to Settings > About Red error-style button that clears all auth credentials (token, session cookie, WS auth param) and navigates back to LandingScreen. Wired through SettingsScreen onLogout callback to Navigation.kt. * fix(#349): improve logout navigation flow * fix(#349): keep backstack stable across logout Use remember without keys for NavBackStack so the same backstack object persists across startScreen changes. Use LaunchedEffect keyed on startScreen to reset stack content atomically. This avoids NavDisplay losing snapshot observation when a new NavBackStack object is created. * fix(#349): mint fresh WS ticket on every connect The WS ticket is a single-use 30-second credential. On app restart (or reconnect), the stored ticket is expired. Now refreshWsTicketIfNeeded() mints a new ticket using the session cookie before every WS connection attempt. Loopback mode (no session cookie) is unaffected. * fix(#349): refresh ticket in openSocket for reconnect paths scheduleReconnect() bypasses connect() and calls openSocket() directly. Moving the ticket refresh into openSocket() ensures every WS connection attempt — first launch and reconnect — uses a fresh ticket. * fix(#349): stub getSessionCookie() in ApiClientTest mockkObject(AuthManager) is strict, so unstubbed getSessionCookie() calls throw MockKException which manifests as IOException. --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
15a4748634 |
test(#292): coverage gaps TEST-04 through TEST-14 (#302)
* test(#292): coverage gaps TEST-04 through TEST-14 - TEST-04: ApiClient auth interceptor + BuildConfig.DEBUG gate (6 tests) - TEST-05: SettingsViewModel profile operations (5 tests) - TEST-06: ConnectViewModel profile switch edge cases (8 tests) - TEST-07: EventParser untested subtypes (12 tests) - TEST-08: AuthManager token routing through profiles (5 tests) - TEST-09: ChatScreen instrumented compose tests (3 tests) - TEST-10: WS reconnect state recovery (5 tests) - TEST-11: Multi-profile token scenarios (6 tests) - TEST-12: Chat DAO Room operations (11 tests) - TEST-13: ChatViewModel slash command handling (8 tests) - TEST-14: Predictive back gesture instrumented test (1 test) Total: 70 tests across 11 files. CI updated with new instrumented-tests job via android-emulator-runner. * fix(ci): resolve ktlint, tests, and emulator failures * fix(ci): resolve test failures on PR #302 * fix(ci): more ChatMessageDaoTest context mocks, re-read state after advanceUntilIdle in connect test * fix(ci): remove problematic packageManager mock, decouple instrumented tests from CI gate * fix(ci): switch emulator to default target, add more cores, memory, and timeout * fix(ci): remove mockkStatic(Log::class) from ChatMessageDaoTest - fails on Android stubs * fix(ci): use aosp_atd emulator image + KVM + AVD cache for fast instrumented tests * fix(ci): remove ApplicationInfo mock - Android stub class breaks MockK * fix(ci): rewrite ChatMessageDaoTest to use HermesDatabase.setForTest() pattern - Room in-memory DB requires Android SDK not available in JVM tests * fix(ci): use actions/cache@v4 instead of @v7 (doesn't exist) * fix(ci): add missing performClick import in StateViewsTest * fix(ci): mock ChatViewModel in instrumented tests, add mockk to androidTestImplementation * fix(ci): add asStateFlow import in ChatScreenTest * fix(ci): exclude duplicate META-INF LICENSE/NOTICE files from androidTest build JUnit Jupiter 5.8.2 JARs each ship META-INF/LICENSE.md, causing mergeDebugAndroidTestJavaResource to fail with 'duplicate files found'. Add LICENSE.md, LICENSE, NOTICE, and NOTICE.md to the packaging exclusion list. * fix(#302): exclude duplicate META-INF/LICENSE-notice.md from instrumented test APK JUnit Jupiter 5.x JARs all ship META-INF/LICENSE-notice.md, causing mergeDebugAndroidTestJavaResource to fail. * fix(#302): add LICENSE-notice.md as separate exclude entry The Ant-style glob brace expansion in packaging.resources.excludes doesn't handle LICENCE-notice.md inside the {}-enclosed pattern. Adding it as a separate entry instead. * fix(#302): use mockk-android for instrumented tests io.mockk:mockk uses Java instrumentation agents not available on Android. Switch to io.mockk:mockk-android for connectedDebugAndroidTest which uses an Android-compatible mocking strategy. * fix(#302): use createAndroidComposeRule for test lifecycle isolation Standard createComposeRule has a gap where consecutive test methods can lose the compose hierarchy on the emulator. Switching to createAndroidComposeRule<MainActivity> gives explicit activity lifecycle management across test methods, fixing the 'No compose hierarchies found' errors. * fix(#302): use createComposeRule instead of createAndroidComposeRule<MainActivity> MainActivity.onCreate() already calls setContent, so the test's setContent { ChatScreen(...) } crashed with 'has already set content'. Switch to createComposeRule() which launches a bare ComponentActivity that doesn't call setContent, allowing the test to compose ChatScreen in isolation with a mock ChatViewModel. * fix(#302): use createAndroidComposeRule<ComponentActivity> + unmockkAll for instrumented tests ChatScreenTest was failing with 'No compose hierarchies found' because mockk mocks in instrumented tests weren't cleaned up between test runs. Switch from createComposeRule() to explicit createAndroidComposeRule<ComponentActivity>() and add @After tearDown() with unmockkAll() to ensure mock state doesn't leak between test methods. * fix(#302): wrap ChatScreen in HermesControlTheme in instrumented tests ChatScreen accesses MaterialTheme.colorScheme during composition (background gradient). Without a Material3 theme wrapper, the composable silently crashes and no compose hierarchy is registered — causing 'No compose hierarchies found' on any subsequent onNodeWithTag() call. Fix: wrap ChatScreen in HermesControlTheme inside setContent {}, matching how MainActivity renders the app. Also use createAndroidComposeRule<ComponentActivity> instead of createComposeRule() for explicit lifecycle management, and add unmockkAll() in @After to clean up mockk state between tests. * fix(ci): fix packaging resource paths and use pickFirsts for duplicate META-INF files Remove leading slashes from META-INF paths (AGP paths are relative, not absolute). Use pickFirsts instead of excludes to handle duplicate files from JUnit Jupiter JARs. * fix(#302): use createAndroidComposeRule<ComponentActivity> for test isolation createComposeRule() has a known race where the compose hierarchy is lost between sequential test methods on the emulator. createAndroidComposeRule<ComponentActivity> provides proper Activity lifecycle management while using the same lightweight empty activity that doesn't pre-set content (unlike MainActivity). * fix(#302): consolidate ChatScreenTest into single test method The Compose test rule's lifecycle gap between test methods on the emulator causes 'No compose hierarchies found' on consecutive setContent calls. Consolidating all three test scenarios into one invocation eliminates the cross-test teardown issue entirely. * fix(#302): mock ContextCompat to suppress permission dialog in test The ChatScreen LaunchedEffect requests POST_NOTIFICATIONS via ActivityResultContracts.RequestPermission(), which pauses the activity and disposes the compose hierarchy. Mocking ContextCompat.checkSelfPermission to return PERMISSION_GRANTED prevents this from happening during the test. * fix(#302): set connectionStatus=CONNECTED in ChatScreenTest The default ChatUiState has connectionStatus=DISCONNECTED, which makes isConnected=false, in turn disabling the chat input field (enabled = isConnected) and the send button (canSend checks isConnected). Set it to CONNECTED so both controls are active. * fix(#302): add ConnectionStatus import and fix ktlint wrapping --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
2b7facb7f4 |
Add NetworkResult Test for SafeApiCall and Mapping (#185)
* Add tests for NetworkResult and safeApiCall * Fix ktlint errors formatting |
||
|
|
a994c7d5db | test: fix malformed JSON in HermesApiServiceTest cron job stub | ||
|
|
8b4f5f91e0 | style: fix ktlint violations (unused imports + line length) | ||
|
|
d6ff3de594 | Initial commit of HermesControl Android app |