feat(profiles): first-class profile switching — phase 0 baseline port (#820)

* feat(profiles): make profile switching actually scope the app (issue #781 follow-up)

* fix(profiles): inherit connection token when switching to same-dashboard profile (issue #781 follow-up)

* fix(chat): reset open session on profile switch + prefill login profile (desktop parity)

* fix(ws): inject profile into profile-scoped RPC params so chat follows the selected profile

* test(chat): de-poison SlashCommandDispatchRpcTest (drop static Dispatchers mock)

mockkStatic(Dispatchers::class) + a stubbed Dispatchers.IO bleeds JVM-wide and
breaks later classes in suite order (CI 2026-08-06: COMMAND_DISPATCH captures
cross-wired → ComparisonFailure on lower stack PRs). ChatViewModel gains an
injected ioDispatcher (38 sites); the slash test constructs the VM with the
test dispatcher so the IO stub is unnecessary.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-08-06 15:06:28 +03:00
1 parent e56d4c9bfa
commit 75ac0c4790
14 files changed
+579 -56

No files matched your search

@@ -79,14 +79,31 @@ class ProfileScopeInterceptorTest {
@Test
fun nonScopedEndpoint_untouched() {
// Pairing is machine-global (not profile-scoped on the backend).
every { AuthManager.getSelectedProfileId() } returns "work"
val client = OkHttpClient.Builder().addInterceptor(ProfileScopeInterceptor).build()
server.enqueue(MockResponse().setResponseCode(200).setBody("{}"))
val req = Request.Builder().url(server.url("api/cron/jobs")).build()
val req = Request.Builder().url(server.url("api/pairing")).build()
client.newCall(req).execute().close()
val url = server.takeRequest().requestUrl!!
assertNull(url.queryParameter("profile"))
assertEquals("/api/cron/jobs", url.encodedPath)
assertEquals("/api/pairing", url.encodedPath)
}
@Test
fun cronSessionsPluginsEndpoints_areScoped() {
// Issue #781 follow-up: cron, sessions and plugin REST are all
// profile-scoped on the backend — switching profiles must switch
// what these screens show, so the interceptor rewrites them too.
val client = clientFor("work")
for (path in listOf("api/cron/jobs", "api/sessions", "api/plugins/hermes-achievements/achievements")) {
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("work", url.queryParameter("profile"))
assertEquals("/$path", url.encodedPath)
}
}
@Test