Десктоп-клиент: 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 и найденные/починенные баги
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user