feat(agentik-tui): TuiBackend, health-check, unit-тесты
ci / JVM build + tests (push) Failing after 1m25s

Закрывает разрыв между :proto и UI-композицией: TuiBackend маршрутизирует
onUserMessage → Conversation.send и Event → AppState.

Изменения:
- agentik-tui/.../TuiBackend.kt — новый commonMain-файл (138 строк):
  инкапсулирует Agent-общение, авто-создание первого диалога,
  подписку на Conversation.events, диспетчеризацию Event в AppState.
- agentik-tui/.../Main.kt — обязательный health-check GET {baseUrl}/health
  ДО старта UI: понятная ошибка и exit 1 при недоступном сервере,
  понятное сообщение при не-200/не-'ok'. JVM-only API (java.net.*,
  ktor.*Timeout) обёрнуты в catch (Exception) — commonMain собирается
  под все desktop-native.
- agentik-tui/.../AppState.kt — добавлены attachBackend/setConversation/
  newConversation/postSystem; submitInput теперь не пишет AssistantStreaming
  сам (его рисует TuiBackend по Event.AppendText).
- agentik-tui/.../TuiApp.kt — TuiBackend монтируется в LaunchedEffect,
  делит scope с recomposer'ом.
- agentik-tui/.../Platform.jvm.kt — expect/actual platformEnv + platformCreateAgent.
- agentik-tui/.../Platform.native.kt — stub actual.
- agentik-tui/build.gradle.kts — kotlinx-coroutines-test в commonTest.
- agentik-cli/build.gradle.kts — binaries.executable entryPoint для native
  (тот же фикс, что прошёл для agentik-tui в предыдущем коммите).
- TuiBackend.dispatch: Event.End теперь зовёт finishAssistant()
  (конвертирует streaming-чанк в финальный Assistant), Interrupted —
  finishAssistant + 'прервано' system message. Раньше оба только
  выключали streaming, и последний чанк висел как AssistantStreaming
  с курсором.

Тесты: agentik-tui/src/commonTest/.../TuiBackendTest.kt — 10 кейсов
против FakeAgent/FakeConversation: auto-create, переиспользование,
AppendText-coalesce, End finalize, Interrupted system, ToolCall/ToolResult
visibility, Error handling, exception path, StartReasoning, connect
message. Используется runTest.backgroundScope + runCurrent — backgroundScope
не двигается через advanceUntilIdle (документированное поведение).

Сборка: jvm + linuxX64 + linuxArm64 + macosX64 + macosArm64 + mingwX64,
10/10 jvmTest green, full project jvmTest не задет.
This commit is contained in:
2026-09-17 00:30:06 +03:00
parent 7a47131f6f
commit ddd9d076c1
10 changed files with 617 additions and 21 deletions
+23
View File
@@ -24,6 +24,23 @@ kotlin {
linuxArm64()
mingwX64()
// Native executables. По умолчанию Kotlin/Native для каждого target'а
// собирает только .klib (библиотеку) — для запускаемого .kexe надо
// явно попросить binaries.executable(). entryPoint нужно задать явно:
// KMP-линкер ищет функцию по FQN (без `Kt`-суффикса), а Kotlin/Native
// добавляет суффикс только для файлов с именем `Main.kt`, поэтому
// указываем точку входа как `pw.binom.agentik.tui.main` (без суффикса).
//
// Применяем к каждому из linuxX64/macosX64/macosArm64/linuxArm64/mingwX64
// явно (а не через targets.withType), потому что targets DSL в KMP не
// поддерживает реифицированный withType<KotlinNativeTarget>().
@OptIn(ExperimentalKotlinGradlePluginApi::class)
listOf(linuxX64(), linuxArm64(), macosX64(), macosArm64(), mingwX64()).forEach {
it.binaries.executable {
entryPoint = "pw.binom.agentik.tui.main"
}
}
sourceSets {
commonMain.dependencies {
implementation(project(":proto"))
@@ -34,6 +51,11 @@ kotlin {
// JetBrains Compose runtime — тащит Mosaic как обёртку.
implementation(libs.mosaic.runtime)
implementation(libs.mosaic.tty.terminal)
// Health-check в Main.kt: Ktor CIO на JVM, на native не собирается —
// там работает stub actual через expect/actual.
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio)
}
jvmMain.dependencies {
implementation(project(":client"))
@@ -41,6 +63,7 @@ kotlin {
commonTest.dependencies {
implementation(kotlin("test"))
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.test)
}
}