diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatModelSwitchDelegate.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatModelSwitchDelegate.kt index 78b75d7e..092e8a9a 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatModelSwitchDelegate.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatModelSwitchDelegate.kt @@ -340,6 +340,19 @@ class ChatModelSwitchDelegate( it.copy(modelSwitchConfirmMessage = confirmMessage) } } + } else { + if (pending != null && pending.sequence == modelSwitchSequence) { + val warning = (map["warning"] as? String)?.trim() + if (!warning.isNullOrBlank()) { + val formattedWarning = + if (warning.startsWith("⚠") || warning.startsWith("⚠️")) { + warning + } else { + "⚠ $warning" + } + addAssistantMessage(formattedWarning) + } + } } } else if (key == "fast") { val rawVal = map["value"] as? String diff --git a/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatModelSwitchDelegateTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatModelSwitchDelegateTest.kt index b4d7da88..94370290 100644 --- a/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatModelSwitchDelegateTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatModelSwitchDelegateTest.kt @@ -647,4 +647,95 @@ class ChatModelSwitchDelegateTest { delegate.dismissModelSwitchConfirm() assertEquals("anthropic/claude-3", uiState.value.currentSessionModel) } + + @Test + fun handleConfigSetResult_modelWarningWithoutConfirm_addsAssistantMessage() = + testScope.runTest { + delegate.handleModelSwitch("/model gpt-69-sol --provider openai-codex --session") + advanceUntilIdle() + + val warningText = + "Note: `gpt-69-sol` was not found in the OpenAI Codex model listing. " + + "Similar models: `gpt-5.6-sol`" + delegate.handleConfigSetResult( + id = "req-1", + result = + mapOf( + "key" to "model", + "value" to "gpt-69-sol", + "confirm_required" to false, + "warning" to warningText, + ), + ) + + assertEquals(1, assistantMessages.size) + assertEquals("⚠ $warningText", assistantMessages.first()) + } + + @Test + fun handleConfigSetResult_modelWarningWithExistingWarningEmoji_doesNotDuplicateEmoji() = + testScope.runTest { + delegate.handleModelSwitch("/model gpt-69-sol --provider openai-codex --session") + advanceUntilIdle() + + val warningText = "⚠ Note: Already prefixed" + delegate.handleConfigSetResult( + id = "req-1", + result = + mapOf( + "key" to "model", + "value" to "gpt-69-sol", + "confirm_required" to false, + "warning" to warningText, + ), + ) + + assertEquals(listOf(warningText), assistantMessages) + } + + @Test + fun handleConfigSetResult_staleSequenceWarning_isIgnored() = + testScope.runTest { + // First switch + delegate.handleModelSwitch("/model gpt-4o --provider openai --session") + advanceUntilIdle() + + // Second switch supersedes first + delegate.handleModelSwitch("/model claude-3-5-sonnet --provider anthropic --session") + advanceUntilIdle() + + // First switch response arrives with warning + delegate.handleConfigSetResult( + id = "req-1", + result = + mapOf( + "key" to "model", + "value" to "gpt-4o", + "confirm_required" to false, + "warning" to "Stale warning", + ), + ) + + assertTrue(assistantMessages.isEmpty()) + } + + @Test + fun handleConfigSetResult_blankWarning_doesNotAddAssistantMessage() = + testScope.runTest { + delegate.handleModelSwitch("/model gpt-4o --provider openai --session") + advanceUntilIdle() + + delegate.handleConfigSetResult( + id = "req-1", + result = + mapOf( + "key" to "model", + "value" to "gpt-4o", + "confirm_required" to false, + "warning" to " ", + ), + ) + + assertTrue(assistantMessages.isEmpty()) + } } diff --git a/app/src/test/java/com/m57/hermescontrol/ui/kanban/KanbanScopeTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/kanban/KanbanScopeTest.kt index 36e16be9..a9b3d827 100644 --- a/app/src/test/java/com/m57/hermescontrol/ui/kanban/KanbanScopeTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/ui/kanban/KanbanScopeTest.kt @@ -38,7 +38,9 @@ class KanbanScopeTest { repository = mockRepository, preferences = preferences, eventsClientProvider = { mockEventsClient }, - ) + ).also { + it.ioDispatcher = testDispatcher + } @Before fun setUp() {