diff --git a/app/src/main/java/com/m57/hermescontrol/data/remote/HermesApiService.kt b/app/src/main/java/com/m57/hermescontrol/data/remote/HermesApiService.kt index fcff05b6..ea588208 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/remote/HermesApiService.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/remote/HermesApiService.kt @@ -430,7 +430,9 @@ interface HermesApiService { ): Response @GET("api/model/options") - suspend fun getModelOptions(): Response + suspend fun getModelOptions( + @Query("refresh") refresh: Boolean = false, + ): Response @GET("api/model/auxiliary") suspend fun getAuxiliaryModels(): Response diff --git a/app/src/main/java/com/m57/hermescontrol/ui/model/ModelScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/model/ModelScreen.kt index a4eb2098..4ca8f8ae 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/model/ModelScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/model/ModelScreen.kt @@ -101,7 +101,7 @@ fun ModelScreen( title = { Text(stringResource(R.string.screen_models)) }, navigationIcon = onOpenDrawer?.let { NavIcon.Menu(it) }, isRefreshing = state.isLoading, - onRefresh = { viewModel.loadAll() }, + onRefresh = { viewModel.loadAll(refresh = true) }, ) { paddingValues -> when { state.isLoading && state.providers.isEmpty() -> { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/model/ModelViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/model/ModelViewModel.kt index 52dd31a3..0e4e414d 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/model/ModelViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/model/ModelViewModel.kt @@ -76,12 +76,12 @@ class ModelViewModel : _uiState.update { it.copy(pinnedModels = AuthManager.getPinnedModels()) } } - fun loadAll() { + fun loadAll(refresh: Boolean = false) { _uiState.update { it.copy(isLoading = true, errorMessage = null) } viewModelScope.launch { val optionsDeferred = async(Dispatchers.IO) { - safeApiCall { ApiClient.hermesApi.getModelOptions() } + safeApiCall { ApiClient.hermesApi.getModelOptions(refresh = refresh) } } val activeProfileDeferred = async(Dispatchers.IO) { diff --git a/app/src/test/java/com/m57/hermescontrol/E2eIntegrationTest.kt b/app/src/test/java/com/m57/hermescontrol/E2eIntegrationTest.kt index 4690c4b6..fbab956b 100644 --- a/app/src/test/java/com/m57/hermescontrol/E2eIntegrationTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/E2eIntegrationTest.kt @@ -1284,7 +1284,7 @@ class E2eIntegrationTest { val provider = ModelProvider("ollama", "Ollama", false, true, listOf("llama3"), 1, null, true, null, null) val profile = ProfileInfo("default", null, true, "llama3", "ollama", null, null, null, null) - coEvery { mockApiService.getModelOptions() } returns + coEvery { mockApiService.getModelOptions(any()) } returns Response.success(ModelOptionsResponse(listOf(provider))) coEvery { mockApiService.getActiveProfile() } returns Response.success(ActiveProfileResponse("default", null)) @@ -1333,6 +1333,53 @@ class E2eIntegrationTest { assertEquals("Successfully set profile model to llama3", viewModel.uiState.value.toastMessage) } + @Test + fun testModelOptions_refreshFlagPropagation() = + runTest { + val provider = ModelProvider("ollama", "Ollama", false, true, listOf("llama3"), 1, null, true, null, null) + val profile = ProfileInfo("default", null, true, "llama3", "ollama", null, null, null, null) + + coEvery { mockApiService.getModelOptions(any()) } returns + Response.success(ModelOptionsResponse(listOf(provider))) + coEvery { mockApiService.getActiveProfile() } returns + Response.success(ActiveProfileResponse("default", null)) + coEvery { mockApiService.getProfiles() } returns Response.success(ProfilesResponse(listOf(profile))) + coEvery { mockApiService.getAuxiliaryModels() } returns + Response.success( + AuxiliaryModelsResponse( + tasks = emptyList(), + main = MainModelAssignment("openai", "gpt-4"), + ), + ) + coEvery { mockApiService.getMoaModels() } returns + Response.success( + MoaConfigResponse( + presets = emptyMap(), + default_preset = "", + reference_models = emptyList(), + aggregator = MoaModelSlot("openai", "gpt-4"), + max_tokens = 4096, + ), + ) + + val capturedRefresh = mutableListOf() + coEvery { mockApiService.getModelOptions(any()) } answers { + capturedRefresh.add(firstArg()) + Response.success(ModelOptionsResponse(listOf(provider))) + } + + // Default loadAll() must call getModelOptions with refresh = false + val viewModel = ModelViewModel() + viewModel.loadAll() + advanceUntilIdle() + + // Pull-to-refresh path must call getModelOptions with refresh = true + viewModel.loadAll(refresh = true) + advanceUntilIdle() + + assertEquals(listOf(false, true), capturedRefresh) + } + @Test fun testConnectViewModel_onPairingString_urlFormat() = runTest {