14 files changed
+451
-43
No files matched your search
@@ -2,6 +2,7 @@ package com.m57.hermescontrol.ui.chat
|
||||
|
||||
import com.m57.hermescontrol.data.local.DataScope
|
||||
import com.m57.hermescontrol.data.model.ModelCapabilities
|
||||
import com.m57.hermescontrol.data.model.ModelInfoResponse
|
||||
import com.m57.hermescontrol.data.model.ModelOptionsResponse
|
||||
import com.m57.hermescontrol.data.model.ModelProvider
|
||||
import com.m57.hermescontrol.data.model.PinnedModel
|
||||
@@ -11,6 +12,7 @@ import com.m57.hermescontrol.data.ws.WsMethods
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
@@ -175,6 +177,41 @@ class ChatModelSwitchDelegateTest {
|
||||
scopeObserverJob.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun modelInfoCapabilities_mergeWithCatalogAndClearOnScopeSwitch() =
|
||||
testScope.runTest {
|
||||
val scopeObserverJob = kotlinx.coroutines.Job()
|
||||
val observerScope = kotlinx.coroutines.CoroutineScope(testDispatcher + scopeObserverJob)
|
||||
delegate.attachScopeObserver(observerScope)
|
||||
delegate.preloadModelOptions()
|
||||
advanceUntilIdle()
|
||||
uiState.update { it.copy(currentSessionModel = "openai/gpt-4o") }
|
||||
|
||||
delegate.applyModelInfo(
|
||||
ModelInfoResponse(
|
||||
provider = "openai",
|
||||
model = "gpt-4o",
|
||||
capabilities =
|
||||
ModelCapabilities(
|
||||
supports_tools = false,
|
||||
supports_vision = true,
|
||||
supports_reasoning = true,
|
||||
),
|
||||
),
|
||||
initialDataScope,
|
||||
)
|
||||
|
||||
assertEquals(true, uiState.value.currentModelCapabilities?.fast)
|
||||
assertEquals(false, uiState.value.currentModelCapabilities?.supports_tools)
|
||||
assertEquals(true, uiState.value.currentModelCapabilities?.supports_vision)
|
||||
|
||||
dataScope.value = initialDataScope.copy(activeProfileId = "work")
|
||||
runCurrent()
|
||||
|
||||
assertNull(uiState.value.currentModelCapabilities)
|
||||
scopeObserverJob.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun closeAndReopenDuringPreload_doesNotDuplicateOrReopenAfterDismiss() =
|
||||
testScope.runTest {
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.m57.hermescontrol.ui.model
|
||||
|
||||
import com.m57.hermescontrol.data.local.AuthManager
|
||||
import com.m57.hermescontrol.data.local.DataScope
|
||||
import com.m57.hermescontrol.data.model.ModelCapabilities
|
||||
import com.m57.hermescontrol.data.model.ModelInfoResponse
|
||||
import com.m57.hermescontrol.data.model.ModelOptionsResponse
|
||||
import com.m57.hermescontrol.data.model.ModelProvider
|
||||
import com.m57.hermescontrol.data.model.PinnedModel
|
||||
@@ -206,6 +208,84 @@ class ModelViewModelTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `model info enriches active model and is cleared on profile switch`() =
|
||||
runTest(testDispatcher) {
|
||||
var currentScope =
|
||||
DataScope(
|
||||
connectionProfileId = "conn-1",
|
||||
baseUrl = "http://localhost:9119",
|
||||
activeProfileId = "default",
|
||||
inMemoryAuthGeneration = 1L,
|
||||
)
|
||||
val response =
|
||||
ModelOptionsResponse(
|
||||
providers =
|
||||
listOf(
|
||||
ModelProvider(
|
||||
slug = "openai",
|
||||
name = "OpenAI",
|
||||
models = listOf("gpt-5"),
|
||||
capabilities = mapOf("gpt-5" to ModelCapabilities(reasoning = true)),
|
||||
),
|
||||
),
|
||||
)
|
||||
val store =
|
||||
ModelCatalogStore(
|
||||
repository =
|
||||
ModelOptionsRepository(
|
||||
connected = { false },
|
||||
rest = { NetworkResult.Success(response) },
|
||||
),
|
||||
getCurrentScope = { currentScope },
|
||||
scope = this,
|
||||
)
|
||||
val requestedProfiles = mutableListOf<String?>()
|
||||
val viewModel =
|
||||
ModelViewModel(
|
||||
catalogStore = store,
|
||||
getCurrentScope = { currentScope },
|
||||
ioDispatcher = testDispatcher,
|
||||
getModelInfoCall = { profile ->
|
||||
requestedProfiles += profile
|
||||
NetworkResult.Success(
|
||||
ModelInfoResponse(
|
||||
provider = "openai",
|
||||
model = "gpt-5",
|
||||
capabilities = ModelCapabilities(supports_vision = true),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
store.ensureLoaded()
|
||||
viewModel.loadModelCapabilities()
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(listOf("default"), requestedProfiles)
|
||||
assertEquals(
|
||||
true,
|
||||
viewModel.uiState.value.providers
|
||||
.single()
|
||||
.capabilities
|
||||
?.get("gpt-5")
|
||||
?.supports_vision,
|
||||
)
|
||||
|
||||
currentScope = currentScope.copy(activeProfileId = "work")
|
||||
store.onScopeChanged(currentScope)
|
||||
store.ensureLoaded()
|
||||
advanceUntilIdle()
|
||||
|
||||
assertNull(
|
||||
viewModel.uiState.value.providers
|
||||
.single()
|
||||
.capabilities
|
||||
?.get("gpt-5")
|
||||
?.supports_vision,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `pinModel shows toast when max cap reached`() {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
Reference in new issue
Block a user