feat(#445): full Models tab parity — settings, aux tasks, MOA config (#458)

* feat(#445): full Models tab — settings panel, aux tasks, MOA config, model assignment

Main features added:
- Model Settings card at top of screen with main model, aux tasks, MOA summary
- Main model assignment via /api/model/set with expensive model confirm flow
- Auxiliary tasks dialog with per-task model override and reset-all
- MOA config dialog with preset management and reference/aggregator model picker
- Model picker dialog reused for main model, aux tasks, and MOA slot selection
- Kept existing provider browse + pin/unpin/search

New data models: AuxiliaryModelsResponse, MoaConfigResponse,
ModelAssignmentRequest/Response
New API endpoints: GET /api/model/auxiliary, GET/PUT /api/model/moa,
POST /api/model/set

* test(#445): add aux + MOA mock endpoints to E2eIntegrationTest

testModelOptionsSelection_success was failing because the ViewModel's init
block now calls getAuxiliaryModels() and getMoaConfig() which weren't mocked.
Added proper mock returns + imports for the new types.

* fix(#445): correct mock endpoint name in E2eIntegrationTest

- getMoaConfig -> getMoaModels (matches API service method name)
- Add explicit type parameter Response.success<MoaConfigResponse>
  to fix Kotlin type inference

* fix(#445): add missing main param to AuxiliaryModelsResponse mock

AuxiliaryModelsResponse requires a MainModelAssignment for its
'main' field. Added the parameter with a dummy value and the
corresponding import.

* fix(#445): update toast assertion in test to match new message format

ViewModel now produces 'Successfully set profile model to ronia'
instead of the old 'Successfully set model to ronia'.

* fix(#445): remove top padding from Models tab LazyColumn

Top inset already provided by HermesScaffold; double padding was chunky next to TopAppBar.

* fix(#445): remove double-stacked top padding on Models tab LazyColumn

HermesScaffold's Box already applies padding(paddingValues) to
offset content below the TopAppBar. The LazyColumn was re-applying
it on the modifier, doubling the top gap. Keep the modifier clean
(.fillMaxSize() only) — the Box handles the offset.

* refactor(#445): inline loadModelOptions into loadAll

loadAll() was just delegating to loadModelOptions() with no extra
logic. Consolidate into a single method to remove the redundancy.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-07-03 16:36:23 +03:00
1 parent 57a815c67e
commit 41fde55c11
5 files changed
+1673 -400

No files matched your search

@@ -5,6 +5,7 @@ import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import com.m57.hermescontrol.data.local.AuthManager
import com.m57.hermescontrol.data.model.ActiveProfileResponse
import com.m57.hermescontrol.data.model.AuxiliaryModelsResponse
import com.m57.hermescontrol.data.model.CronJob
import com.m57.hermescontrol.data.model.DoctorResponse
import com.m57.hermescontrol.data.model.EnvVarConfig
@@ -14,8 +15,11 @@ import com.m57.hermescontrol.data.model.KanbanBoardsResponse
import com.m57.hermescontrol.data.model.KanbanColumn
import com.m57.hermescontrol.data.model.KanbanTask
import com.m57.hermescontrol.data.model.LogResponse
import com.m57.hermescontrol.data.model.MainModelAssignment
import com.m57.hermescontrol.data.model.MessagingPlatform
import com.m57.hermescontrol.data.model.MessagingPlatformResponse
import com.m57.hermescontrol.data.model.MoaConfigResponse
import com.m57.hermescontrol.data.model.MoaModelSlot
import com.m57.hermescontrol.data.model.ModelOptionsResponse
import com.m57.hermescontrol.data.model.ModelProvider
import com.m57.hermescontrol.data.model.PluginInfo
@@ -1047,9 +1051,28 @@ class E2eIntegrationTest {
Response.success(ActiveProfileResponse("default", null))
coEvery { mockApiService.getProfiles() } returns Response.success(ProfilesResponse(listOf(profile)))
coEvery { mockApiService.updateProfileModel("default", any()) } returns Response.success(Unit)
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"),
reference_temperature = 0.7,
aggregator_temperature = 0.7,
max_tokens = 4096,
),
)
val viewModel = ModelViewModel()
viewModel.loadModelOptions()
viewModel.loadAll()
advanceUntilIdle()
assertEquals(1, viewModel.uiState.value.providers.size)
@@ -1068,7 +1091,7 @@ class E2eIntegrationTest {
advanceUntilIdle()
coVerify { mockApiService.updateProfileModel("default", any()) }
assertEquals("Successfully set model to llama3", viewModel.uiState.value.toastMessage)
assertEquals("Successfully set profile model to llama3", viewModel.uiState.value.toastMessage)
}
@Test