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>
This commit is contained in:
1 parent
58420d7b2a
commit
2e97ee9a84
29 files changed
+10
-550
No files matched your search
@@ -8,14 +8,6 @@ fun ServerStoreState.addOrReplaceServer(profile: ConnectionProfile): ServerStore
|
||||
return copy(connectionProfiles = updated)
|
||||
}
|
||||
|
||||
fun ServerStoreState.removeServer(id: String): ServerStoreState {
|
||||
val updated = connectionProfiles.filter { it.id != id }
|
||||
val newSelected = if (selectedProfileId == id) null else selectedProfileId
|
||||
return copy(connectionProfiles = updated, selectedProfileId = newSelected)
|
||||
}
|
||||
|
||||
fun ServerStoreState.switchToServer(id: String?): ServerStoreState = copy(selectedProfileId = id)
|
||||
|
||||
fun ServerStoreState.selfHealed(): ServerStoreState {
|
||||
val hasActive = connectionProfiles.any { it.id == selectedProfileId }
|
||||
val newSelected = if (selectedProfileId != null && !hasActive) null else selectedProfileId
|
||||
|
||||
@@ -5,7 +5,6 @@ import androidx.datastore.core.Serializer
|
||||
import com.m57.hermescontrol.data.remote.ServerEndpoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -22,7 +21,6 @@ class ServerStore(
|
||||
) {
|
||||
private val _stateFlow: MutableStateFlow<ServerStoreState>
|
||||
val stateFlow: StateFlow<ServerStoreState>
|
||||
val state: Flow<ServerStoreState>
|
||||
|
||||
init {
|
||||
val initial =
|
||||
@@ -35,7 +33,6 @@ class ServerStore(
|
||||
}
|
||||
_stateFlow = MutableStateFlow(initial)
|
||||
stateFlow = _stateFlow.asStateFlow()
|
||||
state = stateFlow
|
||||
}
|
||||
|
||||
fun getLatestState(): ServerStoreState = _stateFlow.value
|
||||
|
||||
@@ -450,18 +450,6 @@ object AuthManager {
|
||||
|
||||
fun getPort(): Int = serverStore.getLatestState().resolvedPort
|
||||
|
||||
fun setHost(host: String) {
|
||||
val endpoint = endpoint()
|
||||
val normalizedHost = host.trim().removePrefix("[").removeSuffix("]")
|
||||
setBaseUrl(endpoint.baseUrl.newBuilder().host(normalizedHost).build().toString())
|
||||
}
|
||||
|
||||
fun setPort(port: Int) {
|
||||
require(port in 1..65535) { "Port is out of range" }
|
||||
val endpoint = endpoint()
|
||||
setBaseUrl(endpoint.baseUrl.newBuilder().port(port).build().toString())
|
||||
}
|
||||
|
||||
// ── Auto-reconnect ───────────────────────────────────────────────────
|
||||
|
||||
fun isAutoReconnect(): Boolean = serverStore.getLatestState().autoReconnect
|
||||
|
||||
@@ -125,11 +125,6 @@ data class UsageBar(
|
||||
|
||||
// ── subscription.preview ──────────────────────────────────────────────────
|
||||
|
||||
@Serializable
|
||||
data class SubscriptionPreviewRequest(
|
||||
val subscription_type_id: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SubscriptionPreviewResponse(
|
||||
val ok: Boolean? = null,
|
||||
@@ -184,11 +179,6 @@ data class SubscriptionResumeResponse(
|
||||
|
||||
// ── subscription.upgrade ──────────────────────────────────────────────────
|
||||
|
||||
@Serializable
|
||||
data class SubscriptionUpgradeRequest(
|
||||
val subscription_type_id: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SubscriptionUpgradeResponse(
|
||||
val ok: Boolean? = null,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.m57.hermescontrol.data.remote
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
||||
@Serializable
|
||||
@@ -12,11 +11,6 @@ private data class PasswordLoginPayload(
|
||||
val next: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class WebSocketTicketPayload(
|
||||
val ticket: String,
|
||||
)
|
||||
|
||||
/** JSON serialization for authentication requests and responses. */
|
||||
object AuthPayloads {
|
||||
fun passwordLogin(
|
||||
@@ -31,11 +25,4 @@ object AuthPayloads {
|
||||
next = "",
|
||||
),
|
||||
)
|
||||
|
||||
fun webSocketTicket(body: String): String? =
|
||||
runCatching {
|
||||
OkHttpProvider.json.decodeFromString<WebSocketTicketPayload>(body)
|
||||
.ticket
|
||||
.takeIf { it.isNotBlank() }
|
||||
}.getOrNull()
|
||||
}
|
||||
@@ -77,13 +77,6 @@ fun wrapSessionCookie(rawValue: String): Cookie? {
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a permissive [Cookie] from a Set-Cookie `hermes_session_at=...`
|
||||
* header captured during login. Used by [PersistentCookieJar] when the server
|
||||
* does not echo an explicit domain (common for same-host dashboard logins).
|
||||
*/
|
||||
fun sessionCookieFromValue(value: String): Cookie? = wrapSessionCookie(value)
|
||||
|
||||
const val SESSION_COOKIE_NAME = "hermes_session_at"
|
||||
|
||||
/**
|
||||
|
||||
@@ -76,7 +76,6 @@ import com.m57.hermescontrol.data.model.SkillContentResponse
|
||||
import com.m57.hermescontrol.data.model.SkillHubInstallRequest
|
||||
import com.m57.hermescontrol.data.model.SkillHubSearchResponse
|
||||
import com.m57.hermescontrol.data.model.SkillHubUninstallRequest
|
||||
import com.m57.hermescontrol.data.model.SkillScanResponse
|
||||
import com.m57.hermescontrol.data.model.StatusResponse
|
||||
import com.m57.hermescontrol.data.model.SystemStatsResponse
|
||||
import com.m57.hermescontrol.data.model.TelegramOnboardingApplyRequest
|
||||
@@ -211,14 +210,6 @@ interface HermesApiService {
|
||||
@Query("identifier") identifier: String,
|
||||
): Response<SkillContentResponse>
|
||||
|
||||
@GET("api/skills/hub/scan")
|
||||
suspend fun scanHubSkill(
|
||||
@Query("identifier") identifier: String,
|
||||
): Response<SkillScanResponse>
|
||||
|
||||
@GET("api/skills/hub/sources")
|
||||
suspend fun getHubSources(): Response<List<String>>
|
||||
|
||||
@POST("api/skills/hub/install")
|
||||
suspend fun installHubSkill(
|
||||
@Body body: SkillHubInstallRequest,
|
||||
|
||||
@@ -21,9 +21,6 @@ class ServerEndpoint private constructor(
|
||||
val baseUrl: HttpUrl,
|
||||
val securityWarning: String?,
|
||||
) {
|
||||
val isCleartext: Boolean
|
||||
get() = baseUrl.scheme == "http"
|
||||
|
||||
fun resolve(relativePath: String): HttpUrl {
|
||||
val path = relativePath.trim().trimStart('/')
|
||||
require(path.isNotEmpty()) { "Endpoint path must not be blank" }
|
||||
|
||||
@@ -24,7 +24,6 @@ object WsMethods {
|
||||
// ── Commands catalog ──────────────────────────────────────────────────
|
||||
const val COMMANDS_CATALOG = "commands.catalog"
|
||||
const val COMMAND_DISPATCH = "command.dispatch"
|
||||
const val COMMAND_RESOLVE = "command.resolve"
|
||||
const val SLASH_EXEC = "slash.exec"
|
||||
const val CONFIG_SET = "config.set"
|
||||
|
||||
|
||||
@@ -77,21 +77,12 @@ val StatusGreyLight = Color(0xFFF5F5F5)
|
||||
|
||||
// ── Chat-specific colours (always brand-defined for consistency) ───────
|
||||
|
||||
val UserBubble = HermesPurple
|
||||
val UserBubbleDark = HermesPurpleDark
|
||||
val AssistantBubble = Color(0xFF1C1C26)
|
||||
val AssistantBubbleLight = Color(0xFFEDEAF4)
|
||||
val SystemMessageColor = Color(0xFF8B879A)
|
||||
val ToolChipColor = Color(0xFF262633)
|
||||
val ToolChipColorLight = Color(0xFFE6E3EE)
|
||||
|
||||
// ── Skeleton shimmer colours ───────────────────────────────────────────
|
||||
|
||||
val SkeletonBase = Color(0xFF1C1C26)
|
||||
val SkeletonHighlight = Color(0xFF2F2F3D)
|
||||
val SkeletonBaseLight = Color(0xFFE6E3EE)
|
||||
val SkeletonHighlightLight = Color(0xFFEFEDF4)
|
||||
|
||||
// ── Syntax highlighting tokens (code blocks) ─────────────────────────────
|
||||
// VS Code Dark+ palette — good general readability across all presets.
|
||||
// These are consumed exclusively by CodeBlockCard (MessageCards.kt).
|
||||
|
||||
@@ -1,78 +1,13 @@
|
||||
package com.m57.hermescontrol.theme
|
||||
|
||||
import androidx.compose.animation.core.CubicBezierEasing
|
||||
import androidx.compose.animation.core.Easing
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.SpringSpec
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
|
||||
/**
|
||||
* Motion tokens — duration, easing, and spring specs.
|
||||
*
|
||||
* Design intent (ui-ux-pro-max §7):
|
||||
* - Micro-interactions: 150–300 ms
|
||||
* - Complex transitions: ≤ 400 ms
|
||||
* - Never exceed 500 ms
|
||||
* - Enter: ease-out (decelerated), Exit: ease-in (accelerated)
|
||||
* - Exit shorter than enter (~60–70 %)
|
||||
* - Spring physics for interactive elements
|
||||
* - All animations must be interruptible
|
||||
*
|
||||
* Access via `LocalMotion.current` in composables.
|
||||
*/
|
||||
object Motion {
|
||||
// ── Duration tokens (ms) ─────────────────────────────────────────
|
||||
|
||||
/** Instant — state changes with no perceptible delay. */
|
||||
const val INSTANT = 0
|
||||
|
||||
/** Fast — micro-interactions, ripples, opacity toggles. */
|
||||
const val FAST = 150
|
||||
|
||||
/** Normal — standard UI transitions, sheet slides, expansion. */
|
||||
const val NORMAL = 250
|
||||
|
||||
/** Slow — complex multi-element transitions, screen navigation. */
|
||||
const val SLOW = 400
|
||||
|
||||
// ── Easing curves ────────────────────────────────────────────────
|
||||
|
||||
/** Emphasized decelerate — for ENTERING elements (ease-out). */
|
||||
val emphasizedDecelerate: Easing =
|
||||
CubicBezierEasing(0.05f, 0.7f, 0.1f, 1.0f)
|
||||
|
||||
/** Emphasized accelerate — for EXITING elements (ease-in). */
|
||||
val emphasizedAccelerate: Easing =
|
||||
CubicBezierEasing(0.3f, 0.0f, 0.8f, 0.15f)
|
||||
|
||||
/** Standard — default M3 easing for neutral transitions. */
|
||||
val standard: Easing = FastOutSlowInEasing
|
||||
|
||||
// ── Spring specs ─────────────────────────────────────────────────
|
||||
|
||||
/** Bouncy spring for interactive elements (press, release, drag). */
|
||||
fun <T> bouncySpring(): SpringSpec<T> =
|
||||
spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessMedium,
|
||||
)
|
||||
|
||||
/** Snappy spring for state toggles (switch, selection). */
|
||||
fun <T> snappySpring(): SpringSpec<T> =
|
||||
spring(
|
||||
dampingRatio = Spring.DampingRatioNoBouncy,
|
||||
stiffness = Spring.StiffnessHigh,
|
||||
)
|
||||
|
||||
/** Gentle spring for layout animations (expand, collapse). */
|
||||
fun <T> gentleSpring(): SpringSpec<T> =
|
||||
spring(
|
||||
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||
stiffness = Spring.StiffnessLow,
|
||||
)
|
||||
}
|
||||
object Motion
|
||||
|
||||
val MotionDefaults = Motion
|
||||
|
||||
|
||||
@@ -86,24 +86,6 @@ class AchievementsViewModel : ViewModel(), ToastHost {
|
||||
)
|
||||
}
|
||||
|
||||
fun loadScanStatus() {
|
||||
safeLaunchLoad(
|
||||
apiCall = { safeApiCall { api.getAchievementScanStatus() } },
|
||||
onStart = { /* silent background refresh */ },
|
||||
onSuccess = { status ->
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
scanState = status.state,
|
||||
scanLastError = status.lastError,
|
||||
scanLastDurationMs = status.lastDurationMs,
|
||||
scanRunCount = status.runCount,
|
||||
)
|
||||
}
|
||||
},
|
||||
onError = { /* silent — scan runs independently */ },
|
||||
)
|
||||
}
|
||||
|
||||
fun loadRecentUnlocks() {
|
||||
safeLaunchLoad(
|
||||
apiCall = { safeApiCall { api.getRecentUnlocks() } },
|
||||
|
||||
@@ -260,6 +260,4 @@ class BillingViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearActionMessage() = _uiState.update { it.copy(actionMessage = null) }
|
||||
}
|
||||
@@ -260,10 +260,6 @@ class ChannelsViewModel :
|
||||
)
|
||||
}
|
||||
|
||||
fun dismissRestartNeeded() {
|
||||
_uiState.update { it.copy(restartNeeded = false) }
|
||||
}
|
||||
|
||||
// ── Telegram onboarding ────────────────────────────────────────────────
|
||||
|
||||
fun startTelegramOnboarding() {
|
||||
@@ -502,10 +498,6 @@ class ChannelsViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissOnboardingError() {
|
||||
_uiState.update { it.copy(onboardingError = null) }
|
||||
}
|
||||
|
||||
override fun clearToast() {
|
||||
_uiState.update { it.copy(toastMessage = null) }
|
||||
}
|
||||
|
||||
@@ -91,7 +91,6 @@ import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||
import com.m57.hermescontrol.theme.SystemMessageColor
|
||||
import com.m57.hermescontrol.theme.ToolChipColor
|
||||
import com.m57.hermescontrol.theme.ToolChipColorLight
|
||||
import com.m57.hermescontrol.theme.UserBubble
|
||||
import com.m57.hermescontrol.theme.onColorFor
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -1607,10 +1607,6 @@ class ChatViewModel(
|
||||
|
||||
// ── UI actions ───────────────────────────────────────────────────────
|
||||
|
||||
fun toggleSessionPicker() {
|
||||
_uiState.update { it.copy(showSessionPicker = !it.showSessionPicker) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss the active clarify prompt and reject it (tell the agent no answer
|
||||
* was given).
|
||||
|
||||
@@ -96,20 +96,6 @@ class ConnectViewModel(
|
||||
_uiState.update { it.copy(baseUrl = trimmed, transportWarning = warning, errorMessage = null) }
|
||||
}
|
||||
|
||||
fun onHostChange(value: String) {
|
||||
val endpoint = AuthManager.endpoint()
|
||||
val next = endpoint.baseUrl.newBuilder().host(value.trim()).build().toString()
|
||||
onBaseUrlChange(next)
|
||||
}
|
||||
|
||||
fun onPortChange(value: String) {
|
||||
val portText = value.filter { c -> c.isDigit() }
|
||||
val port = portText.toIntOrNull() ?: return
|
||||
val endpoint = AuthManager.endpoint()
|
||||
val next = endpoint.baseUrl.newBuilder().port(port).build().toString()
|
||||
onBaseUrlChange(next)
|
||||
}
|
||||
|
||||
fun connect() {
|
||||
val state = _uiState.value
|
||||
if (state.token.isBlank()) {
|
||||
|
||||
@@ -234,10 +234,6 @@ class SessionsViewModel : ViewModel(), ToastHost {
|
||||
}
|
||||
}
|
||||
|
||||
fun exitSelecting() {
|
||||
_uiState.update { it.copy(isSelecting = false, selectedIds = emptySet()) }
|
||||
}
|
||||
|
||||
fun toggleSessionSelection(id: String) {
|
||||
_uiState.update {
|
||||
val updated = it.selectedIds.toMutableSet()
|
||||
@@ -258,14 +254,6 @@ class SessionsViewModel : ViewModel(), ToastHost {
|
||||
|
||||
// ── Rename ───────────────────────────────────────────────────────────
|
||||
|
||||
fun startRenaming(sessionId: String) {
|
||||
_uiState.update { it.copy(renamingSessionId = sessionId) }
|
||||
}
|
||||
|
||||
fun cancelRenaming() {
|
||||
_uiState.update { it.copy(renamingSessionId = null) }
|
||||
}
|
||||
|
||||
fun renameSession(
|
||||
sessionId: String,
|
||||
newTitle: String,
|
||||
@@ -308,27 +296,6 @@ class SessionsViewModel : ViewModel(), ToastHost {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Copy prompt ──────────────────────────────────────────────────────
|
||||
|
||||
fun copySessionPrompt(sessionId: String) {
|
||||
viewModelScope.launch {
|
||||
val result =
|
||||
safeApiCall {
|
||||
ApiClient.hermesApi.getSessionPrompt(sessionId)
|
||||
}
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
val promptText = result.data.prompt ?: "No prompt available"
|
||||
_uiState.update { it.copy(toastMessage = promptText) }
|
||||
}
|
||||
|
||||
is NetworkResult.Failure -> {
|
||||
_uiState.update { it.copy(toastMessage = "Failed to get prompt: ${result.error.message}") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Delete (single) ──────────────────────────────────────────────────
|
||||
|
||||
fun requestDeleteSession(sessionId: String) {
|
||||
|
||||
@@ -108,22 +108,6 @@ class SettingsViewModel(
|
||||
ApiClient.rebuild()
|
||||
}
|
||||
|
||||
fun onRenameProfileNameChange(value: String) {
|
||||
_uiState.update { it.copy(renameProfileName = value, isSaved = false) }
|
||||
}
|
||||
|
||||
fun renameProfile() {
|
||||
val currentId = _uiState.value.selectedProfileId ?: return
|
||||
val newName = _uiState.value.renameProfileName.trim()
|
||||
if (newName.isBlank()) return
|
||||
val updatedProfiles =
|
||||
AuthManager.getConnectionProfiles().map {
|
||||
if (it.id == currentId) it.copy(name = newName) else it
|
||||
}
|
||||
AuthManager.saveConnectionProfiles(updatedProfiles)
|
||||
viewModelScope.launch(ioDispatcher) { loadSettings() }
|
||||
}
|
||||
|
||||
fun deleteProfile(profileId: String) {
|
||||
val updatedProfiles = AuthManager.getConnectionProfiles().filter { it.id != profileId }
|
||||
AuthManager.saveConnectionProfiles(updatedProfiles)
|
||||
@@ -235,10 +219,6 @@ class SettingsViewModel(
|
||||
ApiClient.rebuild()
|
||||
}
|
||||
|
||||
fun clearNavigateToLogin() {
|
||||
_uiState.update { it.copy(navigateToLogin = false) }
|
||||
}
|
||||
|
||||
// ── Delete confirmation ──────────────────────────────────────────────
|
||||
|
||||
fun requestDeleteProfile(profileId: String) {
|
||||
|
||||
@@ -205,17 +205,6 @@ class SkillsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun clearHubPreview() {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
hubPreviewIdentifier = null,
|
||||
hubPreviewContent = null,
|
||||
isHubPreviewing = false,
|
||||
hubPreviewError = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hub install ─────────────────────────────────────────────────────────
|
||||
|
||||
fun installSkill(identifier: String) {
|
||||
@@ -293,43 +282,6 @@ class SkillsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
// ── SKILL.md preview ────────────────────────────────────────────────────
|
||||
|
||||
fun previewSkill(skillName: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
previewSkillName = skillName,
|
||||
previewSkillContent = null,
|
||||
isLoadingPreview = true,
|
||||
)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
safeApiCall { ApiClient.hermesApi.getSkillContent(skillName) }
|
||||
}
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoadingPreview = false,
|
||||
previewSkillContent = result.data.content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkResult.Failure -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoadingPreview = false,
|
||||
toastMessage = "Failed to load preview: ${result.error.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearPreview() {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
|
||||
@@ -700,23 +700,4 @@ class SystemViewModel :
|
||||
override fun clearToast() {
|
||||
_uiState.update { it.copy(toastMessage = null) }
|
||||
}
|
||||
|
||||
fun clearTransientState() {
|
||||
actionPollingJob?.cancel()
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
errorMessage = null,
|
||||
toastMessage = null,
|
||||
activeAction = null,
|
||||
actionLog = null,
|
||||
isLoading = false,
|
||||
addingCred = false,
|
||||
creatingHook = false,
|
||||
sharing = false,
|
||||
checkingUpdate = false,
|
||||
updateConfirmOpen = false,
|
||||
hookModalOpen = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,9 @@
|
||||
|
||||
<!-- Chat -->
|
||||
<string name="chat_send_desc">전송</string>
|
||||
<string name="chat_mic_desc">음성 입력</string>
|
||||
<string name="chat_mic_listening">듣는 중…</string>
|
||||
<string name="stt_permission_denied">음성 입력을 위해 마이크 권한이 필요합니다</string>
|
||||
<string name="stt_not_available">이 기기에서 음성 인식을 사용할 수 없습니다</string>
|
||||
<string name="chat_clarify_title">확인 필요</string>
|
||||
<string name="chat_clarify_hint">옵션을 탭하면 입력창에 복사되고, 길게 누르면 바로 전송됩니다.</string>
|
||||
<string name="chat_clarify_response">응답</string>
|
||||
<string name="chat_send">전송</string>
|
||||
<string name="chat_dismiss">닫기</string>
|
||||
<string name="chat_sudo_title">Sudo 비밀번호 필요</string>
|
||||
@@ -43,13 +39,8 @@
|
||||
<string name="error_unknown">문제가 발생했습니다</string>
|
||||
<string name="billing_free_plan">무료</string>
|
||||
<string name="billing_usage">사용량</string>
|
||||
<string name="billing_resume">재개</string>
|
||||
<string name="billing_cancel">취소</string>
|
||||
<string name="billing_renews">갱신: %1$s</string>
|
||||
<string name="billing_status_unknown">알 수 없음</string>
|
||||
<string name="billing_status_active">활성</string>
|
||||
<string name="billing_status_cancelled">취소됨</string>
|
||||
<string name="billing_status_paused">일시중지됨</string>
|
||||
<string name="billing_feature_unavailable">이 서버에서는 결제를 사용할 수 없습니다 (지원되지 않는 메서드)</string>
|
||||
<string name="billing_no_active_plan">활성 요금제 없음 — 무료 등급입니다.</string>
|
||||
<string name="billing_login_required">요금제를 보려면 Nous Portal에 로그인하세요.</string>
|
||||
@@ -120,11 +111,6 @@
|
||||
<string name="chat_status_disconnected">연결 끊김</string>
|
||||
<string name="chat_status_no_network">네트워크 없음</string>
|
||||
<string name="chat_status_auth_expired">세션 만료</string>
|
||||
<string name="chat_thinking">생각 중…</string>
|
||||
<string name="chat_thinking_param">생각 중: %1$s…</string>
|
||||
<string name="chat_reasoning">추론</string>
|
||||
<string name="chat_reasoning_expand">추론 펼치기</string>
|
||||
<string name="chat_reasoning_collapse">추론 접기</string>
|
||||
<string name="chat_input_placeholder_not_connected">연결되지 않음…</string>
|
||||
<string name="chat_input_placeholder_waiting">응답 대기 중…</string>
|
||||
<string name="chat_input_placeholder_type_message">메시지 입력…</string>
|
||||
@@ -139,7 +125,6 @@
|
||||
<string name="content_desc_edit">편집</string>
|
||||
|
||||
<!-- Attachment / File Picker -->
|
||||
<string name="chat_attach_desc">파일 첨부</string>
|
||||
<string name="chat_attach_remove_desc">첨부 제거</string>
|
||||
|
||||
<!-- Camera -->
|
||||
@@ -147,8 +132,6 @@
|
||||
|
||||
<!-- Settings Screen -->
|
||||
<string name="settings_sec_connection">연결</string>
|
||||
<string name="settings_field_host">호스트</string>
|
||||
<string name="settings_field_port">포트</string>
|
||||
<string name="settings_field_token">인증 토큰</string>
|
||||
<string name="settings_sec_behavior">동작</string>
|
||||
<string name="settings_item_auto_reconnect">자동 재연결</string>
|
||||
@@ -199,8 +182,6 @@
|
||||
|
||||
<!-- Connect Error Messages -->
|
||||
<string name="connect_error_token_required">토큰이 필요합니다</string>
|
||||
<string name="connect_error_host_required">호스트가 필요합니다</string>
|
||||
<string name="connect_error_port_invalid">포트는 1~65535 사이여야 합니다</string>
|
||||
<string name="connect_error_401">잘못된 토큰 (401 Unauthorized)</string>
|
||||
<string name="connect_error_403">접근 거부 (403 Forbidden)</string>
|
||||
<string name="connect_error_http_code">서버가 HTTP %1$d 반환</string>
|
||||
@@ -208,19 +189,13 @@
|
||||
<string name="connect_error_refused">연결 거부됨 – Hermes가 실행 중인가요?</string>
|
||||
<string name="connect_error_resolve">호스트를 찾을 수 없음</string>
|
||||
<string name="connect_error_connection_failed">연결 실패: %1$s</string>
|
||||
<string name="connect_error_missing_fields">잘못된 페어링 문자열 — host, port 또는 token 누락</string>
|
||||
<string name="connect_error_malformed">잘못된 페어링 문자열 — URL 또는 Base64 JSON이어야 함</string>
|
||||
<string name="connect_error_parse_failed">페어링 문자열 파싱 실패: %1$s</string>
|
||||
|
||||
<string name="connect_placeholder_pairing">hermes://… 또는 Base64 설정 붙여넣기</string>
|
||||
<string name="connect_save_profile">연결 프로필 저장</string>
|
||||
<string name="connect_profile_name">프로필 이름</string>
|
||||
<string name="connect_placeholder_profile_name">예: 집 헤르메스, 사무실 서버</string>
|
||||
<string name="connect_action_hide_manual">수동 입력 숨기기</string>
|
||||
<string name="connect_action_show_manual">수동으로 입력</string>
|
||||
<string name="connect_placeholder_token">API 토큰</string>
|
||||
<string name="connect_placeholder_host">127.0.0.1</string>
|
||||
<string name="connect_placeholder_port">9119</string>
|
||||
<string name="action_connect">연결</string>
|
||||
|
||||
<!-- Kanban Screen -->
|
||||
@@ -640,16 +615,12 @@
|
||||
<!-- Auth Login Screen -->
|
||||
<string name="auth_login_title">인증 로그인</string>
|
||||
<string name="auth_login_desc">Hermes 대시보드에 연결</string>
|
||||
<string name="auth_login_host_label">호스트</string>
|
||||
<string name="auth_login_port_label">포트</string>
|
||||
<string name="auth_login_token_label">세션 토큰</string>
|
||||
<string name="auth_login_username_label">사용자명</string>
|
||||
<string name="auth_login_password_label">비밀번호</string>
|
||||
<string name="auth_login_action_connect">연결</string>
|
||||
<string name="auth_login_action_probing">대시보드 확인 중…</string>
|
||||
<string name="auth_login_probing_dashboard">대시보드 설정 확인 중…</string>
|
||||
<string name="auth_login_error_host_required">호스트가 필요합니다</string>
|
||||
<string name="auth_login_error_port_invalid">잘못된 포트 (1~65535)</string>
|
||||
<string name="auth_login_error_token_required">세션 토큰이 필요합니다</string>
|
||||
<string name="auth_login_error_username_required">사용자명이 필요합니다</string>
|
||||
<string name="auth_login_error_password_required">비밀번호가 필요합니다</string>
|
||||
@@ -663,9 +634,6 @@
|
||||
<string name="landing_action_auth_login">인증 로그인</string>
|
||||
|
||||
<!-- Loading State -->
|
||||
<string name="loading_state_subtitle_models">모델 제공자 확인 중…</string>
|
||||
<string name="loading_state_subtitle_channels">채널 로딩 중…</string>
|
||||
<string name="loading_state_subtitle_providers">OAuth 제공자 로딩 중…</string>
|
||||
|
||||
<!-- Providers Screen -->
|
||||
<string name="providers_empty_title">OAuth 제공자가 없습니다</string>
|
||||
|
||||
@@ -3,13 +3,9 @@
|
||||
|
||||
<!-- Chat -->
|
||||
<string name="chat_send_desc">Send</string>
|
||||
<string name="chat_mic_desc">Voice input</string>
|
||||
<string name="chat_mic_listening">Listening…</string>
|
||||
<string name="stt_permission_denied">Microphone permission is required for voice input</string>
|
||||
<string name="stt_not_available">Speech recognition is not available on this device</string>
|
||||
<string name="chat_clarify_title">Clarification Needed</string>
|
||||
<string name="chat_clarify_hint">Tap an option to copy to input, or long-press to send directly.</string>
|
||||
<string name="chat_clarify_response">Response</string>
|
||||
<string name="chat_send">Send</string>
|
||||
<string name="chat_dismiss">Dismiss</string>
|
||||
<string name="chat_sudo_title">Sudo Password Required</string>
|
||||
@@ -42,11 +38,9 @@
|
||||
<string name="empty_action_retry_search">Retry search</string>
|
||||
<string name="empty_action_create_job">Create your first job</string>
|
||||
<string name="empty_action_add_server">Add a server</string>
|
||||
<string name="empty_action_create_subscription">Create subscription</string>
|
||||
<string name="empty_action_connect_telegram">Connect a Telegram channel</string>
|
||||
<string name="empty_action_configure_model">Configure model</string>
|
||||
<string name="empty_action_start_chat">Start a new chat</string>
|
||||
<string name="empty_action_install_plugin">Install from URL</string>
|
||||
<string name="empty_action_explore">Explore Skills or Cron jobs</string>
|
||||
<string name="empty_action_clear_search">Clear search</string>
|
||||
<string name="common_cancel">Cancel</string>
|
||||
@@ -56,13 +50,8 @@
|
||||
<string name="error_unknown">Unknown error</string>
|
||||
<string name="billing_free_plan">Free</string>
|
||||
<string name="billing_usage">Usage</string>
|
||||
<string name="billing_resume">Resume</string>
|
||||
<string name="billing_cancel">Cancel</string>
|
||||
<string name="billing_renews">Renews: %1$s</string>
|
||||
<string name="billing_status_unknown">Unknown</string>
|
||||
<string name="billing_status_active">Active</string>
|
||||
<string name="billing_status_cancelled">Cancelled</string>
|
||||
<string name="billing_status_paused">Paused</string>
|
||||
<string name="billing_feature_unavailable">Billing is unavailable on this server (method not supported)</string>
|
||||
<string name="billing_no_active_plan">No active plan — you\'re on the free tier.</string>
|
||||
<string name="billing_login_required">Log in to Nous Portal to view your plan.</string>
|
||||
@@ -133,11 +122,6 @@
|
||||
<string name="chat_status_disconnected">Disconnected</string>
|
||||
<string name="chat_status_no_network">No network</string>
|
||||
<string name="chat_status_auth_expired">Session expired</string>
|
||||
<string name="chat_thinking">Thinking\u2026</string>
|
||||
<string name="chat_thinking_param">Thinking: %1$s\u2026</string>
|
||||
<string name="chat_reasoning">Reasoning</string>
|
||||
<string name="chat_reasoning_expand">Expand reasoning</string>
|
||||
<string name="chat_reasoning_collapse">Collapse reasoning</string>
|
||||
<string name="chat_input_placeholder_not_connected">Not connected\u2026</string>
|
||||
<string name="chat_input_placeholder_waiting">Waiting for response\u2026</string>
|
||||
<string name="chat_input_placeholder_queue">Queued \u2014 sends when agent is free\u2026</string>
|
||||
@@ -153,7 +137,6 @@
|
||||
<string name="content_desc_edit">Edit</string>
|
||||
|
||||
<!-- Attachment / File Picker -->
|
||||
<string name="chat_attach_desc">Attach file</string>
|
||||
<string name="chat_attach_remove_desc">Remove attachment</string>
|
||||
|
||||
<!-- Camera -->
|
||||
@@ -161,8 +144,6 @@
|
||||
|
||||
<!-- Settings Screen -->
|
||||
<string name="settings_sec_connection">Connection</string>
|
||||
<string name="settings_field_host">Host</string>
|
||||
<string name="settings_field_port">Port</string>
|
||||
<string name="settings_field_token">Auth Token</string>
|
||||
<string name="settings_sec_behavior">Behavior</string>
|
||||
<string name="settings_item_auto_reconnect">Auto-Reconnect</string>
|
||||
@@ -229,8 +210,6 @@
|
||||
|
||||
<!-- Connect Error Messages -->
|
||||
<string name="connect_error_token_required">Token is required</string>
|
||||
<string name="connect_error_host_required">Host is required</string>
|
||||
<string name="connect_error_port_invalid">Port must be between 1 and 65535</string>
|
||||
<string name="connect_error_401">Invalid token (401 Unauthorized)</string>
|
||||
<string name="connect_error_403">Access denied (403 Forbidden)</string>
|
||||
<string name="connect_error_http_code">Server returned HTTP %1$d</string>
|
||||
@@ -238,19 +217,13 @@
|
||||
<string name="connect_error_refused">Connection refused – is Hermes running?</string>
|
||||
<string name="connect_error_resolve">Could not resolve host</string>
|
||||
<string name="connect_error_connection_failed">Connection failed: %1$s</string>
|
||||
<string name="connect_error_missing_fields">Malformed pairing string — missing host, port, or token</string>
|
||||
<string name="connect_error_malformed">Malformed pairing string — expected URL or Base64-encoded JSON</string>
|
||||
<string name="connect_error_parse_failed">Failed to parse pairing string: %1$s</string>
|
||||
|
||||
<string name="connect_placeholder_pairing">Paste hermes://… or Base64 config</string>
|
||||
<string name="connect_save_profile">Save connection profile</string>
|
||||
<string name="connect_profile_name">Profile Name</string>
|
||||
<string name="connect_placeholder_profile_name">e.g. Local Hermit, Remote Server</string>
|
||||
<string name="connect_action_hide_manual">Hide manual entry</string>
|
||||
<string name="connect_action_show_manual">Enter manually instead</string>
|
||||
<string name="connect_placeholder_token">API token</string>
|
||||
<string name="connect_placeholder_host">127.0.0.1</string>
|
||||
<string name="connect_placeholder_port">9119</string>
|
||||
<string name="connect_server_url_label">Server URL</string>
|
||||
<string name="connect_placeholder_server_url">https://host:9119/ or http://127.0.0.1:9119/</string>
|
||||
<string name="connect_error_url_invalid">Enter a valid server URL (https:// or http://)</string>
|
||||
@@ -788,16 +761,12 @@
|
||||
<string name="auth_login_title">Auth Login</string>
|
||||
<string name="auth_login_desc">Connect to your Hermes dashboard</string>
|
||||
<string name="auth_login_base_url_label">Server URL</string>
|
||||
<string name="auth_login_host_label">Host</string>
|
||||
<string name="auth_login_port_label">Port</string>
|
||||
<string name="auth_login_token_label">Session Token</string>
|
||||
<string name="auth_login_username_label">Username</string>
|
||||
<string name="auth_login_password_label">Password</string>
|
||||
<string name="auth_login_action_connect">Connect</string>
|
||||
<string name="auth_login_action_probing">Probing dashboard…</string>
|
||||
<string name="auth_login_probing_dashboard">Checking dashboard configuration…</string>
|
||||
<string name="auth_login_error_host_required">Host is required</string>
|
||||
<string name="auth_login_error_port_invalid">Invalid port (1–65535)</string>
|
||||
<string name="auth_login_error_token_required">Session token is required</string>
|
||||
<string name="auth_login_error_username_required">Username is required</string>
|
||||
<string name="auth_login_error_password_required">Password is required</string>
|
||||
@@ -813,9 +782,6 @@
|
||||
<string name="landing_action_auth_login">Auth Login</string>
|
||||
|
||||
<!-- Loading State -->
|
||||
<string name="loading_state_subtitle_models">Fetching model providers…</string>
|
||||
<string name="loading_state_subtitle_channels">Loading channels…</string>
|
||||
<string name="loading_state_subtitle_providers">Loading OAuth providers…</string>
|
||||
|
||||
<!-- Providers Screen (issue #534) -->
|
||||
<string name="providers_empty_title">No OAuth providers</string>
|
||||
|
||||
@@ -38,7 +38,6 @@ import com.m57.hermescontrol.data.model.ProfilesResponse
|
||||
import com.m57.hermescontrol.data.model.SessionInfo
|
||||
import com.m57.hermescontrol.data.model.SessionListResponse
|
||||
import com.m57.hermescontrol.data.model.Skill
|
||||
import com.m57.hermescontrol.data.model.SkillContentResponse
|
||||
import com.m57.hermescontrol.data.model.SkillHubSearchResponse
|
||||
import com.m57.hermescontrol.data.model.StatusResponse
|
||||
import com.m57.hermescontrol.data.model.SystemStatsResponse
|
||||
@@ -100,8 +99,6 @@ class E2eIntegrationTest {
|
||||
every { AuthManager.getPort() } returns 9119
|
||||
every { AuthManager.getBaseUrl() } returns "https://127.0.0.1:9119/"
|
||||
every { AuthManager.setToken(any()) } returns Unit
|
||||
every { AuthManager.setHost(any()) } returns Unit
|
||||
every { AuthManager.setPort(any()) } returns Unit
|
||||
every { AuthManager.setBaseUrl(any()) } returns Unit
|
||||
every {
|
||||
AuthManager.endpoint()
|
||||
@@ -471,47 +468,6 @@ class E2eIntegrationTest {
|
||||
|
||||
// ── Skills Preview ────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
fun testSkillsPreview_success() =
|
||||
runTest {
|
||||
val content = SkillContentResponse("my-skill", "# My Skill\n\nDo stuff")
|
||||
coEvery { mockApiService.getSkillContent(any()) } returns
|
||||
Response.success(content)
|
||||
|
||||
val viewModel = SkillsViewModel(mockApp)
|
||||
viewModel.previewSkill("my-skill")
|
||||
|
||||
advanceUntilIdle()
|
||||
assertFalse(viewModel.uiState.value.isLoadingPreview)
|
||||
assertEquals(
|
||||
"my-skill",
|
||||
viewModel.uiState.value.previewSkillName,
|
||||
)
|
||||
assertEquals(
|
||||
content.content,
|
||||
viewModel.uiState.value.previewSkillContent,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSkillsPreview_error() =
|
||||
runTest {
|
||||
coEvery { mockApiService.getSkillContent(any()) } returns
|
||||
createErrorResponse(500)
|
||||
|
||||
val viewModel = SkillsViewModel(mockApp)
|
||||
viewModel.previewSkill("failing-skill")
|
||||
|
||||
advanceUntilIdle()
|
||||
assertFalse(viewModel.uiState.value.isLoadingPreview)
|
||||
assertNull(viewModel.uiState.value.previewSkillContent)
|
||||
assertNotNull(viewModel.uiState.value.toastMessage)
|
||||
assertTrue(
|
||||
viewModel.uiState.value.toastMessage!!
|
||||
.contains("HTTP 500"),
|
||||
)
|
||||
}
|
||||
|
||||
// Cron Jobs Screen:
|
||||
@Test
|
||||
fun testCronJobsListing_success() =
|
||||
@@ -1031,8 +987,6 @@ class E2eIntegrationTest {
|
||||
|
||||
val connectViewModel = ConnectViewModel(mockApp)
|
||||
connectViewModel.onTokenChange("valid-token")
|
||||
connectViewModel.onHostChange("127.0.0.1")
|
||||
connectViewModel.onPortChange("9119")
|
||||
connectViewModel.connect()
|
||||
advanceUntilIdle()
|
||||
|
||||
|
||||
@@ -35,29 +35,6 @@ class ServerStoreTest {
|
||||
assertEquals("10.0.0.2", finalState.connectionProfiles[0].host)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPureOps_removeServer() {
|
||||
val profile1 = ConnectionProfile(id = "1", name = "Test1", host = "10.0.0.1", port = 8080)
|
||||
val profile2 = ConnectionProfile(id = "2", name = "Test2", host = "10.0.0.2", port = 8081)
|
||||
val state =
|
||||
ServerStoreState(
|
||||
connectionProfiles = listOf(profile1, profile2),
|
||||
selectedProfileId = "2",
|
||||
)
|
||||
|
||||
val newState = state.removeServer("2")
|
||||
assertEquals(1, newState.connectionProfiles.size)
|
||||
assertEquals("1", newState.connectionProfiles[0].id)
|
||||
assertNull(newState.selectedProfileId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPureOps_switchToServer() {
|
||||
val state = ServerStoreState(selectedProfileId = null)
|
||||
val newState = state.switchToServer("prof-1")
|
||||
assertEquals("prof-1", newState.selectedProfileId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPureOps_selfHealed() {
|
||||
// Test orphaned selected profile ID
|
||||
|
||||
@@ -130,14 +130,9 @@ class AuthManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetAndSetHost() {
|
||||
AuthManager.setHost("10.0.0.1")
|
||||
fun testGetHostAndPortFromBaseUrl() {
|
||||
AuthManager.setBaseUrl("https://10.0.0.1:9090/")
|
||||
assertEquals("10.0.0.1", AuthManager.getHost())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetAndSetPort() {
|
||||
AuthManager.setPort(9090)
|
||||
assertEquals(9090, AuthManager.getPort())
|
||||
}
|
||||
|
||||
@@ -149,10 +144,7 @@ class AuthManagerTest {
|
||||
|
||||
@Test
|
||||
fun testBaseUrl() {
|
||||
AuthManager.setHost("hermes.local")
|
||||
AuthManager.setPort(1234)
|
||||
// setHost/setPort derive from the canonical base URL (default is https),
|
||||
// so the result keeps the https scheme and swaps host/port.
|
||||
AuthManager.setBaseUrl("https://hermes.local:1234/")
|
||||
assertEquals("https://hermes.local:1234/", AuthManager.getBaseUrl())
|
||||
}
|
||||
|
||||
@@ -181,8 +173,7 @@ class AuthManagerTest {
|
||||
AuthManager.ensureDefaultProfile()
|
||||
every { mockPrefs.getString("token_${AuthManager.DEFAULT_PROFILE_ID}", null) } returns "token123"
|
||||
AuthManager.setSelectedProfileId(AuthManager.DEFAULT_PROFILE_ID)
|
||||
AuthManager.setHost("hermes.local")
|
||||
AuthManager.setPort(1234)
|
||||
AuthManager.setBaseUrl("https://hermes.local:1234/")
|
||||
// Scheme tracks the canonical base URL (default https) → wss.
|
||||
assertEquals("wss://hermes.local:1234/api/ws?token=token123", AuthManager.wsUrl())
|
||||
}
|
||||
@@ -192,8 +183,7 @@ class AuthManagerTest {
|
||||
AuthManager.ensureDefaultProfile()
|
||||
every { mockPrefs.getString("token_${AuthManager.DEFAULT_PROFILE_ID}", null) } returns null
|
||||
AuthManager.setSelectedProfileId(AuthManager.DEFAULT_PROFILE_ID)
|
||||
AuthManager.setHost("hermes.local")
|
||||
AuthManager.setPort(1234)
|
||||
AuthManager.setBaseUrl("https://hermes.local:1234/")
|
||||
assertEquals("wss://hermes.local:1234/api/ws?token=", AuthManager.wsUrl())
|
||||
}
|
||||
|
||||
@@ -265,34 +255,21 @@ class AuthManagerTest {
|
||||
AuthManager.ensureDefaultProfile()
|
||||
AuthManager.setSelectedProfileId("nonexistent")
|
||||
AuthManager.saveConnectionProfiles(emptyList())
|
||||
AuthManager.setHost("192.168.1.1")
|
||||
AuthManager.setBaseUrl("http://192.168.1.1:9119/")
|
||||
|
||||
assertEquals("192.168.1.1", AuthManager.getHost())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSetHost_updatesProfileWhenSelected() {
|
||||
fun testSetBaseUrl_updatesProfileWhenSelected() {
|
||||
val profile = ConnectionProfile(id = "prof-1", name = "Home", baseUrl = "http://10.0.0.1:9119/")
|
||||
AuthManager.saveConnectionProfiles(listOf(profile))
|
||||
AuthManager.setSelectedProfileId("prof-1")
|
||||
|
||||
AuthManager.setHost("10.0.0.99")
|
||||
AuthManager.setBaseUrl("http://10.0.0.99:9999/")
|
||||
|
||||
val updated = AuthManager.getConnectionProfiles().first()
|
||||
// host/port are now canonicalized into the base URL (profile was http)
|
||||
assertEquals("http://10.0.0.99:9119/", updated.resolvedBaseUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSetPort_updatesProfileWhenSelected() {
|
||||
val profile = ConnectionProfile(id = "prof-1", name = "Home", baseUrl = "http://10.0.0.1:9119/")
|
||||
AuthManager.saveConnectionProfiles(listOf(profile))
|
||||
AuthManager.setSelectedProfileId("prof-1")
|
||||
|
||||
AuthManager.setPort(9999)
|
||||
|
||||
val updated = AuthManager.getConnectionProfiles().first()
|
||||
assertEquals("http://10.0.0.1:9999/", updated.resolvedBaseUrl)
|
||||
assertEquals("http://10.0.0.99:9999/", updated.resolvedBaseUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package com.m57.hermescontrol.data.remote
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Test
|
||||
|
||||
@@ -13,7 +11,6 @@ class ServerEndpointTest {
|
||||
val ep = ServerEndpoint.parse("https://example.com/hermes", CleartextPolicy.ALLOW_WITH_WARNING)
|
||||
assertEquals("https://example.com/hermes/", ep.baseUrl.toString())
|
||||
assertNull(ep.securityWarning)
|
||||
assertFalse(ep.isCleartext)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -21,7 +18,6 @@ class ServerEndpointTest {
|
||||
val ep = ServerEndpoint.parse("http://127.0.0.1:9119/", CleartextPolicy.ALLOW_WITH_WARNING)
|
||||
assertEquals("http://127.0.0.1:9119/", ep.baseUrl.toString())
|
||||
assertEquals(ServerEndpoint.CLEARTEXT_WARNING, ep.securityWarning)
|
||||
assertTrue(ep.isCleartext)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -130,7 +126,6 @@ class ServerEndpointTest {
|
||||
fun `fromLegacy preserves http host and port`() {
|
||||
val ep = ServerEndpoint.fromLegacy("10.0.0.1", 8080)
|
||||
assertEquals("http://10.0.0.1:8080/", ep.baseUrl.toString())
|
||||
assertTrue(ep.isCleartext)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -79,13 +79,6 @@ class ConnectViewModelTest {
|
||||
every { mockApp.getString(R.string.connect_error_refused) } returns "Connection refused – is Hermes running?"
|
||||
every { mockApp.getString(R.string.connect_error_timeout) } returns "Connection timed out"
|
||||
every { mockApp.getString(R.string.connect_error_connection_failed) } returns "Connection failed: %1\$s"
|
||||
every {
|
||||
mockApp.getString(R.string.connect_error_malformed)
|
||||
} returns "Malformed pairing string — expected URL or Base64-encoded JSON"
|
||||
every {
|
||||
mockApp.getString(R.string.connect_error_missing_fields)
|
||||
} returns "Malformed pairing string — missing host, port, or token"
|
||||
every { mockApp.getString(R.string.connect_error_parse_failed) } returns "Failed to parse pairing string: %1\$s"
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -117,22 +110,6 @@ class ConnectViewModelTest {
|
||||
assertNull(viewModel.uiState.value.errorMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnHostChange() {
|
||||
val viewModel = ConnectViewModel(mockApp)
|
||||
viewModel.onHostChange(" 192.168.1.50 ")
|
||||
assertEquals("https://192.168.1.50:9119/", viewModel.uiState.value.baseUrl)
|
||||
assertNull(viewModel.uiState.value.errorMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnPortChange() {
|
||||
val viewModel = ConnectViewModel(mockApp)
|
||||
viewModel.onPortChange("90abc90")
|
||||
assertEquals("https://127.0.0.1:9090/", viewModel.uiState.value.baseUrl)
|
||||
assertNull(viewModel.uiState.value.errorMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testConnect_blankToken_showsError() {
|
||||
val viewModel = ConnectViewModel(mockApp)
|
||||
|
||||
@@ -16,7 +16,6 @@ import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@@ -67,8 +66,6 @@ class SettingsViewModelTest {
|
||||
every { AuthManager.getConnectionProfiles() } returns emptyList()
|
||||
every { AuthManager.getSelectedProfileId() } answers { storedSelectedProfileId }
|
||||
every { AuthManager.baseUrl() } returns "http://127.0.0.1:9119/"
|
||||
every { AuthManager.setHost(any()) } returns Unit
|
||||
every { AuthManager.setPort(any()) } returns Unit
|
||||
every { AuthManager.setBaseUrl(any()) } returns Unit
|
||||
every { AuthManager.setToken(any()) } returns Unit
|
||||
every { AuthManager.setAutoReconnect(any()) } returns Unit
|
||||
@@ -146,47 +143,6 @@ class SettingsViewModelTest {
|
||||
assertEquals(AuthManager.DEFAULT_PROFILE_ID, viewModel.uiState.value.selectedProfileId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnRenameProfileNameChange_updatesState() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.onRenameProfileNameChange("New Name")
|
||||
assertEquals("New Name", viewModel.uiState.value.renameProfileName)
|
||||
assertFalse(viewModel.uiState.value.isSaved)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRenameProfile_updatesProfileName() {
|
||||
every { AuthManager.getConnectionProfiles() } returns testProfiles
|
||||
every { AuthManager.getSelectedProfileId() } returns "prof-1"
|
||||
|
||||
val viewModel = createViewModel()
|
||||
viewModel.onRenameProfileNameChange("Renamed Work")
|
||||
viewModel.renameProfile()
|
||||
|
||||
verify { AuthManager.saveConnectionProfiles(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRenameProfile_withBlankName_doesNothing() {
|
||||
every { AuthManager.getConnectionProfiles() } returns testProfiles
|
||||
every { AuthManager.getSelectedProfileId() } returns "prof-1"
|
||||
|
||||
val viewModel = createViewModel()
|
||||
viewModel.onRenameProfileNameChange(" ")
|
||||
viewModel.renameProfile()
|
||||
|
||||
verify(exactly = 0) { AuthManager.saveConnectionProfiles(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRenameProfile_noSelectedProfile_doesNothing() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.onRenameProfileNameChange("New Name")
|
||||
viewModel.renameProfile()
|
||||
|
||||
verify(exactly = 0) { AuthManager.saveConnectionProfiles(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDeleteProfile_removesProfileAndToken() {
|
||||
every { AuthManager.getConnectionProfiles() } returns testProfiles
|
||||
@@ -233,8 +189,5 @@ class SettingsViewModelTest {
|
||||
verify { AuthManager.saveConnectionProfiles(any()) }
|
||||
verify { AuthManager.setSelectedProfileId(any()) }
|
||||
assertEquals(true, viewModel.uiState.value.navigateToLogin)
|
||||
|
||||
viewModel.clearNavigateToLogin()
|
||||
assertEquals(false, viewModel.uiState.value.navigateToLogin)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user