fix(memory): scope memory endpoints to active profile and auto-reload on profile switch (#1035)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
11dd1eff92
commit
21db9bfdab
4 files changed
+62
-1
No files matched your search
@@ -34,6 +34,7 @@ object ProfileScopeInterceptor : Interceptor {
|
|||||||
"/api/env",
|
"/api/env",
|
||||||
"/api/gateway",
|
"/api/gateway",
|
||||||
"/api/mcp",
|
"/api/mcp",
|
||||||
|
"/api/memory",
|
||||||
"/api/messaging/platforms",
|
"/api/messaging/platforms",
|
||||||
"/api/messaging/telegram/onboarding",
|
"/api/messaging/telegram/onboarding",
|
||||||
"/api/messaging/whatsapp/onboarding",
|
"/api/messaging/whatsapp/onboarding",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.m57.hermescontrol.data.model.MemoryResponse
|
|||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
import com.m57.hermescontrol.data.remote.ApiClient
|
||||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
import com.m57.hermescontrol.data.remote.safeApiCall
|
||||||
|
import com.m57.hermescontrol.data.session.ProfileSwitchCoordinator
|
||||||
import com.m57.hermescontrol.ui.common.ToastHost
|
import com.m57.hermescontrol.ui.common.ToastHost
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -37,9 +38,24 @@ class MemoryViewModel :
|
|||||||
private val _uiState = MutableStateFlow(MemoryUiState())
|
private val _uiState = MutableStateFlow(MemoryUiState())
|
||||||
val uiState: StateFlow<MemoryUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<MemoryUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun load() {
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
ProfileSwitchCoordinator.switched.collect {
|
||||||
|
load(silent = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
ProfileSwitchCoordinator.connectionSwitched.collect {
|
||||||
|
load(silent = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun load(silent: Boolean = false) {
|
||||||
if (_uiState.value.isLoading) return
|
if (_uiState.value.isLoading) return
|
||||||
|
if (!silent) {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = safeApiCall { ApiClient.hermesApi.getMemory() }
|
val result = safeApiCall { ApiClient.hermesApi.getMemory() }
|
||||||
when (result) {
|
when (result) {
|
||||||
|
|||||||
@@ -118,6 +118,19 @@ class ProfileScopeInterceptorTest {
|
|||||||
assertEquals("alpha", url.queryParameter("profile"))
|
assertEquals("alpha", url.queryParameter("profile"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun memoryEndpoints_areScoped() {
|
||||||
|
val client = clientFor("yasmin")
|
||||||
|
for (path in listOf("api/memory", "api/memory/learning-graph", "api/memory/providers/mem0/config")) {
|
||||||
|
server.enqueue(MockResponse().setResponseCode(200).setBody("{}"))
|
||||||
|
val req = Request.Builder().url(server.url(path)).build()
|
||||||
|
client.newCall(req).execute().close()
|
||||||
|
val url = server.takeRequest().requestUrl!!
|
||||||
|
assertEquals("yasmin", url.queryParameter("profile"))
|
||||||
|
assertEquals("/$path", url.encodedPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun lookalikePath_notScoped() {
|
fun lookalikePath_notScoped() {
|
||||||
// Sourcery review (PR #540): `startsWith` must not match non-segment
|
// Sourcery review (PR #540): `startsWith` must not match non-segment
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.m57.hermescontrol.data.model.MemoryResetResponse
|
|||||||
import com.m57.hermescontrol.data.model.MemoryResponse
|
import com.m57.hermescontrol.data.model.MemoryResponse
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
import com.m57.hermescontrol.data.remote.ApiClient
|
||||||
import com.m57.hermescontrol.data.remote.HermesApiService
|
import com.m57.hermescontrol.data.remote.HermesApiService
|
||||||
|
import com.m57.hermescontrol.data.session.ProfileSwitchCoordinator
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.coVerify
|
import io.mockk.coVerify
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
@@ -14,6 +15,7 @@ import io.mockk.mockkObject
|
|||||||
import io.mockk.unmockkAll
|
import io.mockk.unmockkAll
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
import kotlinx.coroutines.test.advanceUntilIdle
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
import kotlinx.coroutines.test.resetMain
|
import kotlinx.coroutines.test.resetMain
|
||||||
@@ -36,6 +38,8 @@ import retrofit2.Response
|
|||||||
class MemoryViewModelTest {
|
class MemoryViewModelTest {
|
||||||
private val testDispatcher = StandardTestDispatcher()
|
private val testDispatcher = StandardTestDispatcher()
|
||||||
private lateinit var mockApi: HermesApiService
|
private lateinit var mockApi: HermesApiService
|
||||||
|
private val mockSwitchFlow = MutableSharedFlow<String>(extraBufferCapacity = 1)
|
||||||
|
private val mockConnSwitchFlow = MutableSharedFlow<String>(extraBufferCapacity = 1)
|
||||||
|
|
||||||
private val memoryResponse =
|
private val memoryResponse =
|
||||||
MemoryResponse(
|
MemoryResponse(
|
||||||
@@ -56,6 +60,9 @@ class MemoryViewModelTest {
|
|||||||
fun setUp() {
|
fun setUp() {
|
||||||
Dispatchers.setMain(testDispatcher)
|
Dispatchers.setMain(testDispatcher)
|
||||||
mockkObject(ApiClient)
|
mockkObject(ApiClient)
|
||||||
|
mockkObject(ProfileSwitchCoordinator)
|
||||||
|
every { ProfileSwitchCoordinator.switched } returns mockSwitchFlow
|
||||||
|
every { ProfileSwitchCoordinator.connectionSwitched } returns mockConnSwitchFlow
|
||||||
mockApi = mockk(relaxed = true)
|
mockApi = mockk(relaxed = true)
|
||||||
every { ApiClient.hermesApi } returns mockApi
|
every { ApiClient.hermesApi } returns mockApi
|
||||||
coEvery { mockApi.getMemory() } returns Response.success(memoryResponse)
|
coEvery { mockApi.getMemory() } returns Response.success(memoryResponse)
|
||||||
@@ -79,6 +86,30 @@ class MemoryViewModelTest {
|
|||||||
assertEquals(1024L, state.memory?.builtin_files?.memory)
|
assertEquals(1024L, state.memory?.builtin_files?.memory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `profile switch triggers silent reload`() {
|
||||||
|
val vm = MemoryViewModel()
|
||||||
|
testDispatcher.scheduler.advanceUntilIdle()
|
||||||
|
coVerify(exactly = 0) { mockApi.getMemory() }
|
||||||
|
|
||||||
|
mockSwitchFlow.tryEmit("yasmin")
|
||||||
|
testDispatcher.scheduler.advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify(exactly = 1) { mockApi.getMemory() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `connection profile switch triggers silent reload`() {
|
||||||
|
val vm = MemoryViewModel()
|
||||||
|
testDispatcher.scheduler.advanceUntilIdle()
|
||||||
|
coVerify(exactly = 0) { mockApi.getMemory() }
|
||||||
|
|
||||||
|
mockConnSwitchFlow.tryEmit("server-remote")
|
||||||
|
testDispatcher.scheduler.advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify(exactly = 1) { mockApi.getMemory() }
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `load also fetches learning graph for self-improvement section`() {
|
fun `load also fetches learning graph for self-improvement section`() {
|
||||||
val graph =
|
val graph =
|
||||||
|
|||||||
Reference in new issue
Block a user