feat(#480): pass refresh=true on Models pull-to-refresh to re-probe custom providers (#481)

* feat(#480): pass refresh=true on Models pull-to-refresh to re-probe custom providers

* test(#480): assert refresh flag propagation for default vs pull-to-refresh loads

* fix(#480): capture refresh arg via slot instead of brittle coVerify in propagation test

---------

Co-authored-by: ronia <ronia@m57>
This commit is contained in:
M57andronia authored and GitHub committed 2026-07-07 02:53:36 +03:00
1 parent 7384b51476
commit bdf566ba3e
4 files changed
+54 -5

No files matched your search

@@ -430,7 +430,9 @@ interface HermesApiService {
): Response<Unit>
@GET("api/model/options")
suspend fun getModelOptions(): Response<ModelOptionsResponse>
suspend fun getModelOptions(
@Query("refresh") refresh: Boolean = false,
): Response<ModelOptionsResponse>
@GET("api/model/auxiliary")
suspend fun getAuxiliaryModels(): Response<AuxiliaryModelsResponse>
@@ -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() -> {
@@ -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) {
@@ -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>(
MoaConfigResponse(
presets = emptyMap(),
default_preset = "",
reference_models = emptyList(),
aggregator = MoaModelSlot("openai", "gpt-4"),
max_tokens = 4096,
),
)
val capturedRefresh = mutableListOf<Boolean>()
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 {