Commit Graph
26 Commits
Author SHA1 Message Date
M57andhy4ri 220779bf19 perf(#1171): async AuthManager startup with readiness gate and lazy database (#1182)
Co-authored-by: hy4ri <hy4ri@users.noreply.github.com>
2026-09-17 20:54:03 +03:00
M57 065c7fd75f feat(chat): display token usage and tps in message footers (#1094) 2026-09-12 16:04:13 +03:00
M57andM57 4d9dfb99db feat(db): migrate to androidx.room3 3.0.2 with SQLCipherDriver (#994)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-27 13:47:32 +03:00
M57andM57 4be1e965f2 feat: render display_kind timeline markers as chips, not user bubbles (#904) (#909)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-15 14:06:51 +03:00
M57andM57 daf33dcb4c feat(update): notify about app updates on launch (chat banner) (#891)
Fires one silent update check per installed version at app start
(UpdateNoticeManager, release builds only) and shows a non-blocking
banner on the chat screen when a newer release exists. The About tab
adopts the launch result instead of re-pinging GitHub; dismissals
return on the next launch via the persisted latest tag.

Closes #890

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-13 01:08:42 +03:00
M57andM57 683a24becf fix(profiles): connection-profile switch re-dials the WebSocket (#888)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-12 23:27:08 +03:00
M57andM57 d9f437939a test(connect): phase 5 — restart lands on the selected profile, no login (#827)
* test(connect): pin restart-with-scoped-profile token resolution (phase 5)

* fix(profiles): scope switch via un-clamped active-profile state

The switch persisted the server profile through serverStore.selectedProfileId,
whose selfHealed() clamps ids absent from the local connection list — server
profiles like "meow" never exist there, so the scope silently reset to
default and the re-dialed socket created the fresh session in the wrong
profile (live logcat 2026-08-06: session.create carried profile=default
right after switching to meow).

- AuthManager: prefs-backed activeProfileId state, immune to the clamp,
  restored on startup; resetAuthStateForTest clears it
- ProfileScopeInterceptor + WsProfileParams + ChatViewModel fetchContextUsage:
  read the active profile instead of the connection selection
- ProfileSwitchCoordinator: write activeProfileId; dial the socket on
  Dispatchers.IO (the ticket mint did blocking I/O on the main thread —
  NetworkOnMainThreadException — and fell back to a 1s reconnect)
- ProfilesViewModel: inject the IO dispatcher; ProfilesViewModelTest drops
  its static Dispatchers mock (JVM-wide bleed class)
- Tests: WsProfileParams/Interceptor tests use the real prefs-backed state
  instead of mockkObject(AuthManager) (re-mock breaks in suite order after
  real AuthManager init); AuthManagerTest + Issue647ProfileUrlTest cancel
  the init() DataStore scope in tearDown (UncaughtExceptionsBeforeTest
  phantoms); regression test pins the clamp on the old path vs the new state

* fix(profiles): resolve getter/property JVM collision; de-poison slash test

- AuthManager.activeProfileId (StateFlow) and a getActiveProfileId()
  function generated the same JVM getter name — calls silently resolved to
  the property getter at runtime (ReadonlyStateFlow -> String
  ClassCastException, CI unit red 2026-08-06). Function removed; callers
  read activeProfileId.value.
- ChatViewModel: inject ioDispatcher (38 sites) so SlashCommandDispatchRpcTest
  can drop its mockkStatic(Dispatchers) bleed without racing real IO.
- SlashCommandDispatchRpcTest: drop the static Dispatchers mock (JVM-wide
  bleed; CI cross-wired COMMAND_DISPATCH captures in suite order), construct
  the VM with the test dispatcher.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-06 15:07:35 +03:00
M57andM57 398268d105 feat(profiles): ProfileContext single source of truth + per-server token fallback (phase 1) (#821)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-06 15:06:55 +03:00
M57 5c1492a80e feat(chat): add animated GIF rendering with tap-to-play animation (#721) (#731)
* feat(chat): add animated GIF rendering with tap-to-play animation

Closes #721

* fix(#721): add missing GifImageThumbnail component and format ktlint

* fix(analytics): resolve ktlint indentation error in itemsIndexed

* feat(chat): add interactive play/pause toggle for animated GIFs

* fix(chat): control GIF playback via Animatable start and stop
2026-07-27 18:32:45 +03:00
M57 d3836ace7f [Testing Improvement] Add mapping tests for ChatMessageEntity (#693)
* 🧪 test: Add mapping tests for ChatMessageEntity to/from ChatMessage

* Added comprehensive tests to `ChatMessageMapperTest.kt` verifying the pure mappings between Room database entities (`ChatMessageEntity`) and UI models (`ChatMessage`).
* Verified fallback logics for enums (MessageRole and ToolStatus).
* Ensures regression prevention when underlying data models change.

* 🧪 test: Refactor mapper boolean assertions to assertTrue/assertFalse

* Changed assertEquals(true/false, isStreaming) to assertTrue/assertFalse
as suggested in the review.
2026-07-23 13:14:12 +03:00
M57andM57 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>
2026-07-22 15:51:07 +03:00
M57andM57 e44a53366b Fix #647: resolve Default profile URL from top-level login URL (#649)
* Fix #647: resolve Default profile URL from top-level login URL

ConnectionProfile.resolvedBaseUrl (used directly by ConnectionSection for
the profile list display) silently fell back to the hardcoded loopback
127.0.0.1:9119 whenever a profile's own baseUrl was null, instead of using
the URL the user actually authenticated against.

- Add ConnectionProfile.resolveBaseUrl(topLevelBaseUrl) that prefers the
  profile's own baseUrl, then the top-level login URL, then legacy loopback.
- Store-level ServerCollectionOps.resolvedBaseUrl now applies the same
  fallback so a null-baseUrl profile still shows the real gateway.
- ConnectionSection, SettingsViewModel and ConnectViewModel resolve against
  AuthManager.getBaseUrl() so the loopback fallback can never surface.
- Add Issue647ProfileUrlTest covering login-stamp, null-baseUrl fallback,
  own-URL precedence, fresh-install loopback, and store-level resolution.
- Update existing AuthManagerTest assertions to the new property form.

* Fix #647 (root cause): cache login URL at top-level baseUrl

The previous change only made profile resolution prefer the top-level
login URL, but setBaseUrl() never actually wrote the top-level baseUrl
field — so it stayed DEFAULT_BASE_URL (https://127.0.0.1:9119/) forever.
A Default profile seeded with the loopback default (or whose own baseUrl
was never stamped) therefore still displayed the loopback URL after
logging in via a custom LAN address.

setBaseUrl() now also stamps the top-level baseUrl, making it the
authoritative "last successful login URL" cache. getBaseUrl() and the
store-level resolvedBaseUrl (profile-list display) now reflect the URL
actually authenticated against.

Add regression tests mirroring the exact AuthLoginViewModel.connect()
sequence (seed with loopback -> setBaseUrl(loginUrl) -> token) asserting
the Default profile, top-level baseUrl and store display all resolve to
the login URL, never loopback.

---------

Co-authored-by: M57 <Hy4ri@users.noreply.github.com>
2026-07-20 22:03:31 +03:00
M57andhy4ri 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>
2026-07-20 17:26:10 +03:00
M57 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
2026-07-14 21:55:37 +03:00
M57andM57 53b3966931 feat(#525): render reasoning.delta / reasoning.available stream (#543)
* feat(#525): add ReasoningDelta + ReasoningAvailable WsEvent types

* feat(#525): parse reasoning.delta + reasoning.available; route through reducer + StreamingState

* feat(#525): add reasoning streaming buffer to ChatStreamingController

* feat(#525): dispatch ReasoningDelta/ReasoningAvailable in ChatViewModel

* feat(#525): render collapsible ReasoningIndicator in chat list

* fix(#525): address review — icons+semantics, LocalHermesStatusColors, drop redundant ReasoningAvailable branch

* feat(#525): persist reasoningText on model, entity, mapper + Room migration 3->4

* feat(#525): reducer + streaming carry reasoningText onto message

* feat(#525): render persisted reasoning above each assistant bubble, drop global indicator

* nix: flake

* nix: flake

---------

Co-authored-by: M57 <m57@hermes-control.app>
2026-07-10 23:41:53 +03:00
M57andronia aa59e704a4 fix(#478): rework default login to use unified ConnectionProfile model (#482)
* fix(#478): rework default login to use unified ConnectionProfile model

- AuthManager: drop legacy auth_token path; getToken/setToken now delegate
  to the selected profile (profile token only, no global fallback)
- setHost/setPort always write the selected profile (no top-level host/port branch)
- Add DEFAULT_PROFILE_ID + ensureDefaultProfile()/ensureDefaultSelected() so the
  selected profile is never null; ensureDefaultSelected() only falls back when
  nothing is selected (never clobbers an existing choice); ensureDefaultProfile()
  only injects a Default profile on fresh install, never into an existing list
- One-time migration folds legacy standalone auth_token into the Default profile,
  batched into a single SharedPreferences transaction (guarded by legacy_default_migrated)
- ConnectViewModel.connect() persists via setToken/setHost/setPort (delegate to
  the selected profile) — keeps the existing persistence contract
- Connect/Settings: remove the '(none)'/standalone profile option; deleteProfile
  falls back to Default profile instead of null selection
- Update AuthManagerTest/ConnectViewModelTest to the unified model; fix test
  stub ordering + migration re-init; remove dead connect_profile_none / settings_profile_none strings

Closes #478

* fix: stub AuthManager.ensureDefaultSelected() in E2eIntegrationTest

The #478 ConnectViewModel.connect() success branch now calls
AuthManager.ensureDefaultSelected() on the non-saveProfile path.
E2eIntegrationTest.setUp() mocks AuthManager as a non-relaxed mockkObject
but never stubs that method, so the unstubbed call throws MockKException inside
viewModelScope.launch, leaving connectionSuccess unset and failing 3 tests:
- testFullUserSessionFlow
- testConnectViewModel_onPairingString_base64Format
- testConnectViewModel_onPairingString_urlFormat

Add the missing stub. Verified locally: full suite 322/322 passing.

---------

Co-authored-by: ronia <ronia@m57>
2026-07-07 20:50:53 +03:00
M57 430a083647 feat(#471): implement DataStore-based server configuration and self-h… (#477)
* feat(#471): implement DataStore-based server configuration and self-healing

* refactor(#471): address Sourcery feedback on ANR risk and reset method splitting

* refactor(#471): implement synchronous ServerStore initialization to fully remove ANR risk

* feat(#471): show profile name, host and port on add profile, redirect to login on save, and support shortcut login buttons for existing profiles
2026-07-06 22:40:44 +03:00
M57 8f14aca647 perf: optimize startup, database, and ui rendering (#405)
* perf: optimize startup, database, and ui rendering

* fix(#405): address code review feedback

* test: wait for async prefs initialization to prevent coroutine leaks

* test: stub mockViewModel.streamingState in ChatScreenTest to prevent ClassCastException on Android
2026-06-27 01:38:44 +03:00
M57andM57 42fcf2a14b fix: implement in-memory token caching to avoid redundant prefs decryptions (#320)
* fix: implement in-memory token caching to avoid redundant prefs decryptions

Resolves #277

Previously, the `AuthManager.getToken()` method decrypted the entire
`EncryptedSharedPreferences` file on every call. Because this method is
used in the OkHttp interceptor on every HTTP/WS request, and frequently
during navigation, it introduced 10-30ms of redundant work per call.
This compounded to noticeable UI lag (e.g., during chat session switches).

This commit implements thread-safe in-memory caching for the auth token
inside `AuthManager`.
- Added a `@Volatile` `cachedToken` and `tokenInitialized` flag.
- Modified `getToken()` to return the cached value after initial evaluation,
  using double-checked locking (`synchronized(this)`) to avoid race conditions.
- Updated `setToken()` and `setSelectedProfileId()` to correctly invalidate
  or update the cache.
- Added comprehensive unit tests in `AuthManagerTest.kt` to verify caching
  behavior and proper invalidation upon profile switches.

* fix: implement in-memory token caching to avoid redundant prefs decryptions

Resolves #277

Previously, the `AuthManager.getToken()` method decrypted the entire
`EncryptedSharedPreferences` file on every call. Because this method is
used in the OkHttp interceptor on every HTTP/WS request, and frequently
during navigation, it introduced 10-30ms of redundant work per call.
This compounded to noticeable UI lag (e.g., during chat session switches).

This commit implements thread-safe in-memory caching for the auth token
inside `AuthManager`.
- Added a `@Volatile` `cachedToken` and `tokenInitialized` flag.
- Modified `getToken()` to return the cached value after initial evaluation,
  using double-checked locking (`synchronized(this)`) to avoid race conditions.
- Updated `setToken()` and `setSelectedProfileId()` to correctly invalidate
  or update the cache.
- Added comprehensive unit tests in `AuthManagerTest.kt` to verify caching
  behavior and proper invalidation upon profile switches.

* fix: implement in-memory token caching to avoid redundant prefs decryptions

Resolves #277

Previously, the `AuthManager.getToken()` method decrypted the entire
`EncryptedSharedPreferences` file on every call. Because this method is
used in the OkHttp interceptor on every HTTP/WS request, and frequently
during navigation, it introduced 10-30ms of redundant work per call.
This compounded to noticeable UI lag (e.g., during chat session switches).

This commit implements thread-safe in-memory caching for the auth token
inside `AuthManager`.
- Added a `@Volatile` `cachedToken` and `tokenInitialized` flag.
- Modified `getToken()` to return the cached value after initial evaluation,
  using double-checked locking (`synchronized(this)`) to avoid race conditions.
- Updated `setToken()` and `setSelectedProfileId()` to correctly invalidate
  or update the cache.
- Added comprehensive unit tests in `AuthManagerTest.kt` to verify caching
  behavior and proper invalidation upon profile switches.
- Fixed GitGuardian warning by removing the "my_secret_token" from the test.

* fix: implement in-memory token caching to avoid redundant prefs decryptions

Resolves #277

Previously, the `AuthManager.getToken()` method decrypted the entire
`EncryptedSharedPreferences` file on every call. Because this method is
used in the OkHttp interceptor on every HTTP/WS request, and frequently
during navigation, it introduced 10-30ms of redundant work per call.
This compounded to noticeable UI lag (e.g., during chat session switches).

This commit implements thread-safe in-memory caching for the auth token
inside `AuthManager`.
- Added a `@Volatile` `cachedToken` and `tokenInitialized` flag.
- Modified `getToken()` to return the cached value after initial evaluation,
  using double-checked locking (`synchronized(this)`) to avoid race conditions.
- Updated `setToken()` and `setSelectedProfileId()` to correctly invalidate
  or update the cache.
- Added comprehensive unit tests in `AuthManagerTest.kt` to verify caching
  behavior and proper invalidation upon profile switches.
- Fixed GitGuardian warning by removing the "my_secret_token" from the test.
- Fixed merge conflicts and ensured code still tests well.

* fix: implement in-memory token caching to avoid redundant prefs decryptions

Resolves #277

Previously, the `AuthManager.getToken()` method decrypted the entire
`EncryptedSharedPreferences` file on every call. Because this method is
used in the OkHttp interceptor on every HTTP/WS request, and frequently
during navigation, it introduced 10-30ms of redundant work per call.
This compounded to noticeable UI lag (e.g., during chat session switches).

This commit implements thread-safe in-memory caching for the auth token
inside `AuthManager`.
- Added a `@Volatile` `cachedToken` and `tokenInitialized` flag.
- Modified `getToken()` to return the cached value after initial evaluation,
  using double-checked locking (`synchronized(this)`) to avoid race conditions.
- Updated `setToken()` and `setSelectedProfileId()` to correctly invalidate
  or update the cache.
- Added comprehensive unit tests in `AuthManagerTest.kt` to verify caching
  behavior and proper invalidation upon profile switches.
- Fixed GitGuardian warning by removing the "my_secret_token" from the test.

* fix: implement in-memory token caching to avoid redundant prefs decryptions

Resolves #277

Previously, the `AuthManager.getToken()` method decrypted the entire
`EncryptedSharedPreferences` file on every call. Because this method is
used in the OkHttp interceptor on every HTTP/WS request, and frequently
during navigation, it introduced 10-30ms of redundant work per call.
This compounded to noticeable UI lag (e.g., during chat session switches).

This commit implements thread-safe in-memory caching for the auth token
inside `AuthManager`.
- Added a `@Volatile` `cachedToken` and `tokenInitialized` flag.
- Modified `getToken()` to return the cached value after initial evaluation,
  using double-checked locking (`synchronized(this)`) to avoid race conditions.
- Updated `setToken()` and `setSelectedProfileId()` to correctly invalidate
  or update the cache.
- Added comprehensive unit tests in `AuthManagerTest.kt` to verify caching
  behavior and proper invalidation upon profile switches.
- Fixed GitGuardian warning by removing the "my_secret_token" from the test.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-23 22:19:35 +03:00
M57andM57 eb71ae3a46 Fix WebSocket events being silently dropped on buffer overflow (#313)
* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow `extraBufferCapacity` from 64 to 256
and changes the `onBufferOverflow` policy to `BufferOverflow.DROP_OLDEST`
in `HermesWsClient.kt`. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting `DROP_OLDEST`, `tryEmit()` remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

* Fix ktlint formatting issues in HermesWsClient.kt

The formatting changes adhere to ktlint rules required by the CI pipeline:
- Removed a trailing comma in MutableSharedFlow initialization to avoid multiline expression wrapping errors.
- Sorted imports to be in lexicographical order.

These formatting changes fix the failing CI check.

Fixes #275

* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow `extraBufferCapacity` from 64 to 256
and changes the `onBufferOverflow` policy to `BufferOverflow.DROP_OLDEST`
in `HermesWsClient.kt`. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting `DROP_OLDEST`, `tryEmit()` remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow extraBufferCapacity from 64 to 256
and changes the onBufferOverflow policy to BufferOverflow.DROP_OLDEST
in HermesWsClient.kt. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting DROP_OLDEST, tryEmit() remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow extraBufferCapacity from 64 to 256
and changes the onBufferOverflow policy to BufferOverflow.DROP_OLDEST
in HermesWsClient.kt. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting DROP_OLDEST, tryEmit() remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow extraBufferCapacity from 64 to 256
and changes the onBufferOverflow policy to BufferOverflow.DROP_OLDEST
in HermesWsClient.kt. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting DROP_OLDEST, tryEmit() remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-23 22:13:55 +03:00
M57andM57 2c6e437a5b Fix: Prevent AuthManager state pollution on connection verification (#348)
* Fix: Prevent AuthManager state pollution on connection verification

Modified `ConnectViewModel.connect()` to construct a temporary OkHttpClient and Retrofit builder, isolated from the global `ApiClient` state. The connection verification is executed using this temporary service. Only upon a successful HTTP response does the system persist the configured `host`, `port`, and `token` to the global `AuthManager` and triggers an `ApiClient.rebuild()`. This resolves the issue where a failed connection attempt would overwrite active and verified profiles. Extracted `createTempService` factory to `ApiClient` for robustness and clean tests.

Fixes #260

* Fix: Prevent AuthManager state pollution on connection verification

Modified `ConnectViewModel.connect()` to construct a temporary OkHttpClient and Retrofit builder, isolated from the global `ApiClient` state. The connection verification is executed using this temporary service. Only upon a successful HTTP response does the system persist the configured `host`, `port`, and `token` to the global `AuthManager` and triggers an `ApiClient.rebuild()`. This resolves the issue where a failed connection attempt would overwrite active and verified profiles. Extracted `createTempService` factory to `ApiClient` for robustness and clean tests.

Fixes #260

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-23 20:49:58 +03:00
M57 5295b7762e Fix multi-profile token leakage fallback bug (#337)
This commit modifies `AuthManager.getToken()` to strictly return the chosen profile's token or `null` if the profile has no token, removing the previous logic that incorrectly fell back to the global token (`KEY_TOKEN`).

Why this was necessary:
Before this change, if a user switched to a profile that lacked a saved token (Profile B), the system would fall back to the global token, effectively leaking Profile A's credentials to Profile B's configured host. This was an unintended and critical cross-profile authentication leak.

Impact:
This ensures strong isolation between user profiles. If a profile doesn't have its own credentials, the system will correctly register it as unauthenticated rather than trying to reuse potentially sensitive global tokens.
Unit tests have also been updated to ensure the fallback logic is tested as strictly returning `null`.

Fixes #258
2026-06-23 12:40:33 +03:00
M57 e5f0df9273 chore(#297)
* fix: fix ktlint issues in gradle files

Fixed code style and formatting issues across `build.gradle.kts` and `app/build.gradle.kts` to resolve CI failures during the `ktlint` task.

* fix: fix ktlint and tests for PR 306

Updates dependencies logic to use `MasterKeys` properly based on the `androidx.security:security-crypto` upgrade to 1.0.0, and fixes `ktlint` formatting errors across the build scripts.
2026-06-22 21:35:40 +03:00
M57andM57 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>
2026-06-22 13:49:04 +03:00
M57 8b4f5f91e0 style: fix ktlint violations (unused imports + line length) 2026-06-16 01:22:48 +03:00
Hy4ri d6ff3de594 Initial commit of HermesControl Android app 2026-06-16 00:00:36 +03:00