chore: eliminate all Kotlin compiler warnings in main source (#877)

* chore: eliminate all Kotlin compiler warnings in main source

* test(chat): fix cross-class coroutine leaks poisoning the unit suite

- ChatUpdateCommandTest: inject FakeSlashUsageStore — the real DataStore
  store inits on real IO threads with a relaxed-mock app (cacheDir NPE)
  and leaks an uncaught exception into the next runTest class.
- ChatUpdateCommandTest + SlashCommandDispatchRpcTest: mock
  ProfileSwitchCoordinator.switched (ChatViewModelTest pattern) — VMs
  otherwise park collectors on the singleton flow that crash later
  classes (ProfileSwitchCoordinatorTest) on stale-Main resumption.

Full suite: 944 tests, 0 failures (was 2 flaky per run).

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-08-10 22:56:16 +03:00
1 parent b05cffa9c4
commit 1e6e248891
14 files changed
+88 -38

No files matched your search

@@ -94,8 +94,8 @@ class MainActivity : ComponentActivity() {
// Issue #832: MainActivity is exported (launcher requirement) — only
// honor intents stamped with our own notification action.
if (intent?.action != ACTION_OPEN_CHAT_FROM_NOTIFICATION) return
val sessionId = intent?.getStringExtra(NotificationReplyReceiver.EXTRA_SESSION_ID)
intent?.removeExtra(NotificationReplyReceiver.EXTRA_SESSION_ID)
val sessionId = intent.getStringExtra(NotificationReplyReceiver.EXTRA_SESSION_ID)
intent.removeExtra(NotificationReplyReceiver.EXTRA_SESSION_ID)
sessionId?.takeIf { it.isNotBlank() }?.let(NavigationController::openChatSession)
}
}
@@ -86,7 +86,7 @@ open class AppUpdateChecker(
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) return@use null
val body = response.body?.string().orEmpty()
val body = response.body.string().orEmpty()
parseUpdateInfo(body)
}
}
@@ -106,7 +106,7 @@ open class AppUpdateChecker(
try {
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) return@withContext false
val body = response.body ?: return@withContext false
val body = response.body
val total = body.contentLength()
dest.outputStream().use { out ->
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
@@ -303,7 +303,7 @@ object HermesWsClient {
}
if (socketToCancel != null) {
rejectAllPending()
socketToCancel?.cancel()
socketToCancel.cancel()
}
if (shouldOpen) openSocket()
}
@@ -153,7 +153,7 @@ class KanbanEventsClient {
request,
object : WebSocketListener() {
override fun onOpen(
ws: WebSocket,
webSocket: WebSocket,
response: Response,
) {
if (closed || gen != generation) return
@@ -162,7 +162,7 @@ class KanbanEventsClient {
}
override fun onMessage(
ws: WebSocket,
webSocket: WebSocket,
text: String,
) {
if (closed || gen != generation) return
@@ -173,20 +173,20 @@ class KanbanEventsClient {
}
override fun onMessage(
ws: WebSocket,
webSocket: WebSocket,
bytes: ByteString,
) = Unit
override fun onClosing(
ws: WebSocket,
webSocket: WebSocket,
code: Int,
reason: String,
) {
ws.close(code, reason)
webSocket.close(code, reason)
}
override fun onClosed(
ws: WebSocket,
webSocket: WebSocket,
code: Int,
reason: String,
) {
@@ -199,7 +199,7 @@ class KanbanEventsClient {
}
override fun onFailure(
ws: WebSocket,
webSocket: WebSocket,
t: Throwable,
response: Response?,
) {
@@ -135,7 +135,7 @@ private fun BillingContent(
shape = RoundedCornerShape(12.dp),
) {
Text(
text = state.errorMessage ?: "",
text = state.errorMessage,
modifier = Modifier.padding(12.dp),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onErrorContainer,
@@ -152,7 +152,7 @@ private fun BillingContent(
shape = RoundedCornerShape(12.dp),
) {
Text(
text = state.actionMessage ?: "",
text = state.actionMessage,
modifier = Modifier.padding(12.dp),
style = MaterialTheme.typography.bodyMedium,
)
@@ -207,7 +207,7 @@ private fun PlanCard(subscription: SubscriptionCurrent) {
}
if (subscription.pending_downgrade_display != null) {
Text(
text = subscription.pending_downgrade_display ?: "",
text = subscription.pending_downgrade_display,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 4.dp),
@@ -2890,7 +2890,7 @@ class ChatViewModel(
if (lastAssistantIdx >= 0) {
val target = mapped[lastAssistantIdx]
if (target.reasoningText.isBlank()) {
mapped[lastAssistantIdx] = target.copy(reasoningText = pendingReasoning!!)
mapped[lastAssistantIdx] = target.copy(reasoningText = pendingReasoning)
}
}
}
@@ -28,9 +28,9 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.InsertDriveFile
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
@@ -405,7 +405,7 @@ fun AttachmentChip(
Spacer(modifier = Modifier.width(4.dp))
} else {
Icon(
imageVector = Icons.Default.InsertDriveFile,
imageVector = Icons.AutoMirrored.Filled.InsertDriveFile,
contentDescription = attachment.name,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -90,7 +90,7 @@ object ToolViewBuilder {
raw = firstString(parseMaybeObject(value), listOf("context"))
}
return compactPreview(raw ?: "", max)
return compactPreview(raw, max)
}
private fun contextValue(value: JsonElement?): String {
@@ -1328,8 +1328,8 @@ object ToolViewBuilder {
val elements = result?.get("elements") as? JsonArray
if (elements != null) {
val mode = firstString(result, listOf("mode"))
val width = intValue(result?.get("width"))
val height = intValue(result?.get("height"))
val width = intValue(result.get("width"))
val height = intValue(result.get("height"))
val modePart = mode.takeIf { it.isNotEmpty() }?.let { " ($it)" } ?: ""
val sizePart = if (width != null && height != null) " ${width}x$height" else ""
return "${elements.size} elements$modePart$sizePart"
@@ -5,6 +5,7 @@
package com.m57.hermescontrol.ui.keys
import android.content.ClipData
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
@@ -29,6 +30,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Delete
@@ -41,7 +43,6 @@ import androidx.compose.material.icons.outlined.Build
import androidx.compose.material.icons.outlined.ContentCopy
import androidx.compose.material.icons.outlined.Folder
import androidx.compose.material.icons.outlined.Forum
import androidx.compose.material.icons.outlined.OpenInNew
import androidx.compose.material.icons.outlined.SmartToy
import androidx.compose.material.icons.outlined.Tune
import androidx.compose.material3.AlertDialog
@@ -66,11 +67,13 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
@@ -97,6 +100,7 @@ import com.m57.hermescontrol.ui.common.SkeletonListState
import com.m57.hermescontrol.ui.common.StatusBadge
import com.m57.hermescontrol.ui.common.StatusBadgeType
import com.m57.hermescontrol.ui.common.ToastEffect
import kotlinx.coroutines.launch
private const val FILTER_ALL = "ALL"
@@ -527,8 +531,9 @@ private fun EnvVarCard(
val isRevealed = revealedValue != null
var editedValue by remember(config, revealedValue) { mutableStateOf(revealedValue ?: "") }
val clipboardManager = LocalClipboardManager.current
val clipboard = LocalClipboard.current
val uriHandler = LocalUriHandler.current
val scope = rememberCoroutineScope()
val displayValue =
if (isRevealed) {
@@ -645,7 +650,7 @@ private fun EnvVarCard(
}.padding(vertical = 2.dp),
) {
Icon(
imageVector = Icons.Outlined.OpenInNew,
imageVector = Icons.AutoMirrored.Outlined.OpenInNew,
contentDescription = stringResource(R.string.keys_action_open_url),
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(14.dp),
@@ -759,7 +764,10 @@ private fun EnvVarCard(
}
if (isRevealed && !revealedValue.isNullOrEmpty()) {
IconButton(onClick = {
clipboardManager.setText(AnnotatedString(revealedValue))
val text = AnnotatedString(revealedValue)
scope.launch {
clipboard.setClipEntry(ClipEntry(ClipData.newPlainText(null, text)))
}
onShowToast(copiedMessage)
}) {
Icon(
@@ -1,5 +1,6 @@
package com.m57.hermescontrol.ui.profiles
import android.content.ClipData
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -41,11 +42,13 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontFamily
@@ -64,6 +67,7 @@ import com.m57.hermescontrol.ui.common.ToastEffect
import com.m57.hermescontrol.ui.model.components.ModelPickerDialog
import com.m57.hermescontrol.ui.profiles.components.ProfileBuilderView
import com.m57.hermescontrol.ui.profiles.components.validateProfileName
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -91,7 +95,8 @@ fun ProfilesScreen(
var setupCmdProfileName by remember { mutableStateOf<String?>(null) }
var copiedSetupCmd by remember { mutableStateOf(false) }
val clipboardManager = LocalClipboardManager.current
val clipboard = LocalClipboard.current
val scope = rememberCoroutineScope()
var isBuildingProfile by remember { mutableStateOf(false) }
@@ -839,7 +844,8 @@ fun ProfilesScreen(
if (!state.isLoadingSetupCommand && state.setupCommand != null) {
Button(
onClick = {
clipboardManager.setText(AnnotatedString(state.setupCommand.orEmpty()))
val text = AnnotatedString(state.setupCommand.orEmpty())
scope.launch { clipboard.setClipEntry(ClipEntry(ClipData.newPlainText(null, text))) }
copiedSetupCmd = true
},
) {
@@ -288,7 +288,7 @@ class ToolsetDetailViewModel : ViewModel(), ToastHost {
state.copy(
postSetup =
state.postSetup?.copy(
lines = data.lines ?: state.postSetup?.lines ?: emptyList(),
lines = data.lines ?: state.postSetup.lines,
running = data.running == true,
exitCode = data.exit_code,
),
@@ -31,24 +31,34 @@ object LocaleContextWrapper {
code.contains("-r", ignoreCase = true) -> {
val parts = code.split("-r", ignoreCase = true)
Locale(parts[0], parts.getOrElse(1) { "" })
localeFor(parts[0], parts.getOrElse(1) { "" })
}
code.contains("-") -> {
val parts = code.split("-")
Locale(parts[0], parts.getOrElse(1) { "" })
localeFor(parts[0], parts.getOrElse(1) { "" })
}
code.contains("_") -> {
val parts = code.split("_")
Locale(parts[0], parts.getOrElse(1) { "" })
localeFor(parts[0], parts.getOrElse(1) { "" })
}
else -> {
Locale(code)
Locale.forLanguageTag(code)
}
}
private fun localeFor(
language: String,
region: String,
): Locale =
if (region.isEmpty()) {
Locale.forLanguageTag(language)
} else {
Locale.forLanguageTag("$language-$region")
}
/**
* Wrap [base] so the configuration carries [locale].
*