Files
agentik-desktop/src/jvmTest/kotlin/pw/binom/agentik/desktop/session/AgentRegistryTest.kt
T
subochev 2c71542b13 Десктоп-клиент: Compose-UI, сессии, SQLite-кэш, голос, живые тесты
- settings/: settings.json (агенты, тема, clientId), атомарная запись
- persistence/: ConversationMetaRepository — группы, lastSeen, превью,
  watermark синхронизации (synced_at через PRAGMA table_info)
- session/: AgentRegistry (параллельное подключение, деградация в офлайн),
  AgentConnection (журнал + мета + живой список диалогов conversationsFlow,
  refresh по SSE-потоку), ChatSession (history + backfill + live + preview,
  дедуп по id, 1 мс шаг watermark, syncMutex), LiveTurn
- UI: рейка агентов, список диалогов (живое обновление), чат со стримингом,
  композер (Enter отправляет, Shift+Enter переносит, Ctrl+M — голос),
  окна «Агенты»/«Группы»/«Новый диалог», тёмная и светлая темы
- voice/: mic-api → Silero VAD → Vosk на одном выделенном потоке;
  VoiceNative.warmUp() до skia (иначе SIGSEGV), модель в артефакте
- markdown/: вендоренный рендер из ai/assistent
- ошибки в баннер через AppState.reportError (CancellationException игнорируется)
- тесты 103 (реальная SQLite + фейковый транспорт); против живого агента —
  RealServerIT, RealAppStateIT, RealSpeechIT
- LIVE-RUN-NOTES.md: живой прогон в GUI и найденные/починенные баги
2026-09-28 10:08:10 +03:00

129 lines
4.6 KiB
Kotlin

package pw.binom.agentik.desktop.session
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.io.TempDir
import pw.binom.agentik.desktop.settings.AgentConfig
import pw.binom.agentik.desktop.settings.SettingsRepository
import pw.binom.agentik.desktop.settings.ThemeId
import java.nio.file.Path
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
/**
* Тесты [AgentRegistry]: persist настроек и переходы статусов. Подключение
* подменено [AgentConnector], который всегда падает — так проверяем
* деградацию в [AgentStatus.Offline] без сети. Успешное [AgentStatus.Online]
* потребовало бы настоящего [AgentConnection] (его конструктор приватный),
* поэтому проверяется косвенно — см. подключение к реальному серверу вручную.
*/
class AgentRegistryTest {
@TempDir
lateinit var dir: Path
private fun registry(scope: CoroutineScope) = AgentRegistry(
settingsRepo = SettingsRepository.inDirectory(dir),
scope = scope,
connector = { _, _ -> throw RuntimeException("нет сети") },
)
@Test
fun `add agent persists config and goes offline on failure`() = runBlocking {
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
val reg = registry(scope)
try {
reg.load()
val cfg = AgentConfig.create("http://192.168.1.5:8080/agentik", nameOverride = "Тестовый")
val status = reg.addAgent(cfg)
assertTrue(status is AgentStatus.Offline)
assertEquals(1, reg.settings.value.agents.size)
assertNull(reg.connection(cfg.id))
// persist переживает переоткрытие
val reopened = SettingsRepository.inDirectory(dir).load()
assertEquals(listOf(cfg), reopened.agents)
} finally {
reg.close()
scope.cancel()
}
}
@Test
fun `update agent persists new baseUrl`() = runBlocking {
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
val reg = registry(scope)
try {
reg.load()
val cfg = AgentConfig.create("http://old:8080/agentik")
reg.addAgent(cfg)
reg.updateAgent(cfg.copy(baseUrl = "http://new:9090/agentik"))
assertEquals("http://new:9090/agentik", reg.settings.value.agents.single().baseUrl)
assertEquals("http://new:9090/agentik", SettingsRepository.inDirectory(dir).load().agents.single().baseUrl)
} finally {
reg.close()
scope.cancel()
}
}
@Test
fun `remove agent drops it from settings and statuses`() = runBlocking {
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
val reg = registry(scope)
try {
reg.load()
val cfg = AgentConfig.create("http://host:8080/agentik")
reg.addAgent(cfg)
reg.removeAgent(cfg.id)
assertTrue(reg.settings.value.agents.isEmpty())
assertNull(reg.statuses.value[cfg.id])
assertTrue(SettingsRepository.inDirectory(dir).load().agents.isEmpty())
} finally {
reg.close()
scope.cancel()
}
}
@Test
fun `updateSettings persists theme`() = runBlocking {
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
val reg = registry(scope)
try {
reg.load()
reg.updateSettings { it.copy(themeId = ThemeId.LIGHT) }
assertEquals(ThemeId.LIGHT, reg.settings.value.themeId)
assertEquals(ThemeId.LIGHT, SettingsRepository.inDirectory(dir).load().themeId)
} finally {
reg.close()
scope.cancel()
}
}
@Test
fun `update nonexistent agent is offline and does not add it`() = runBlocking {
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
val reg = registry(scope)
try {
reg.load()
val status = reg.updateAgent(AgentConfig.create("http://host:8080/agentik"))
assertTrue(status is AgentStatus.Offline)
assertTrue(reg.settings.value.agents.isEmpty())
} finally {
reg.close()
scope.cancel()
}
}
}