2c71542b13
- 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 и найденные/починенные баги
129 lines
4.6 KiB
Kotlin
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()
|
|
}
|
|
}
|
|
}
|