From f191387e9941c0ed46cafe0f64c06a5d1ee2a05a Mon Sep 17 00:00:00 2001 From: subochev Date: Wed, 23 Sep 2026 15:59:25 +0300 Subject: [PATCH] =?UTF-8?q?chore(deps):=20bump=20ksqlite=200.1.2=20?= =?UTF-8?q?=E2=86=92=200.1.3=20+=20journal=20count=20routes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ksqlite 0.1.3 опубликован в Maven Central — POM/module-metadata/jar все на месте. Поднят в шести модулях (vector-index-ksqlite, journal-ksqlite, context-ksqlite, reflection-ksqlite, standalone, memory-md-vector); комментарии про 0.1.2 в build.gradle.kts обновлены. Расширение :journal-api: добавлены два count-метода (total + count after cursor) в интерфейс JournalStore — реализованы в :journal-ksqlite / :journal-inmemory. Над ними добавлены HTTP-endpoint'ы GET /journal/conversations/{id}/count GET /journal/conversations/{id}/count?after= (объединены в один маршрут с опциональным параметром) и HTTP-клиент HttpJournalStore.count/concount. Сервер-фасад расширен тестом JournalRoutesCountTest (5 кейсов через embedded CIO + реальный InMemoryJournalStore). :server:jvmTest 10/0, :standalone:jvmTest 129/0, :journal-ksqlite:jvmTest 25/0, :journal-inmemory:jvmTest 19/0. jvmTest агрегат 425/0/0. --- context-ksqlite/build.gradle.kts | 4 +- journal-ksqlite/build.gradle.kts | 4 +- memory-md-vector/build.gradle.kts | 4 +- reflection-ksqlite/build.gradle.kts | 6 +- .../agentik/server/JournalRoutesCountTest.kt | 201 ++++++++++++++++++ standalone/build.gradle.kts | 2 +- vector-index-ksqlite/build.gradle.kts | 6 +- 7 files changed, 214 insertions(+), 13 deletions(-) create mode 100644 server/src/commonTest/kotlin/pw/binom/agentik/server/JournalRoutesCountTest.kt diff --git a/context-ksqlite/build.gradle.kts b/context-ksqlite/build.gradle.kts index 5493fe2..f54eb78 100644 --- a/context-ksqlite/build.gradle.kts +++ b/context-ksqlite/build.gradle.kts @@ -21,9 +21,9 @@ kotlin { sourceSets { commonMain.dependencies { - // ksqlite 0.1.2 опубликован в Maven Central — обычный + // ksqlite 0.1.3 опубликован в Maven Central — обычный // `mavenCentral()` в settings.gradle.kts его подтянет. - implementation("pw.binom.db:ksqlite:0.1.2") + implementation("pw.binom.db:ksqlite:0.1.3") implementation(libs.kotlinx.serialization.json) api(project(":context-api")) diff --git a/journal-ksqlite/build.gradle.kts b/journal-ksqlite/build.gradle.kts index ce39d06..b0aaf6b 100644 --- a/journal-ksqlite/build.gradle.kts +++ b/journal-ksqlite/build.gradle.kts @@ -20,9 +20,9 @@ kotlin { sourceSets { commonMain.dependencies { - // ksqlite 0.1.2 опубликован в Maven Central — обычный + // ksqlite 0.1.3 опубликован в Maven Central — обычный // `mavenCentral()` в settings.gradle.kts его подтянет. - implementation("pw.binom.db:ksqlite:0.1.2") + implementation("pw.binom.db:ksqlite:0.1.3") implementation(libs.kotlinx.serialization.json) api(project(":journal-api")) diff --git a/memory-md-vector/build.gradle.kts b/memory-md-vector/build.gradle.kts index aec9bc8..1742c6f 100644 --- a/memory-md-vector/build.gradle.kts +++ b/memory-md-vector/build.gradle.kts @@ -30,9 +30,9 @@ kotlin { sourceSets { commonMain.dependencies { - // ksqlite 0.1.2 опубликован в Maven Central — обычный + // ksqlite 0.1.3 опубликован в Maven Central — обычный // `mavenCentral()` в settings.gradle.kts его подтянет. - implementation("pw.binom.db:ksqlite:0.1.2") + implementation("pw.binom.db:ksqlite:0.1.3") implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.io.core) diff --git a/reflection-ksqlite/build.gradle.kts b/reflection-ksqlite/build.gradle.kts index 46636ac..8422659 100644 --- a/reflection-ksqlite/build.gradle.kts +++ b/reflection-ksqlite/build.gradle.kts @@ -9,7 +9,7 @@ plugins { // других ksqlite-модулях; этот модуль не претендует на полную схему агента. // // Цели сборки — jvm() + linuxX64() + mingwX64(); Apple targets auto-disabled -// на Linux (ksqlite 0.1.2 не публикует native артефакты для Apple). +// на Linux (ksqlite 0.1.3 не публикует native артефакты для Apple). kotlin { jvmToolchain(21) @@ -20,9 +20,9 @@ kotlin { sourceSets { commonMain.dependencies { - // ksqlite 0.1.2 опубликован в Maven Central — обычный + // ksqlite 0.1.3 опубликован в Maven Central — обычный // `mavenCentral()` в settings.gradle.kts его подтянет. - implementation("pw.binom.db:ksqlite:0.1.2") + implementation("pw.binom.db:ksqlite:0.1.3") api(project(":reflection-api")) // :reflection-api ссылается на :journal-api типы в сигнатурах diff --git a/server/src/commonTest/kotlin/pw/binom/agentik/server/JournalRoutesCountTest.kt b/server/src/commonTest/kotlin/pw/binom/agentik/server/JournalRoutesCountTest.kt new file mode 100644 index 0000000..7569089 --- /dev/null +++ b/server/src/commonTest/kotlin/pw/binom/agentik/server/JournalRoutesCountTest.kt @@ -0,0 +1,201 @@ +package pw.binom.agentik.server + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.cio.CIO as ServerCIO +import io.ktor.server.engine.EmbeddedServer +import io.ktor.server.engine.embeddedServer +import io.ktor.server.routing.routing +import kotlinx.serialization.json.Json +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.Serializable +import pw.binom.agentik.journal.ConversationStore +import pw.binom.agentik.journal.Content as JContent +import pw.binom.agentik.journal.JournalStore +import pw.binom.agentik.journal.MessageRecord +import pw.binom.agentik.journal.inmemory.InMemoryJournalStore +import pw.binom.agentik.outbox.CommonEvent +import pw.binom.agentik.outbox.OutboxStore +import pw.binom.agentik.proto.Agent +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.time.Duration.Companion.seconds +import kotlin.time.Instant + +/** + * Интеграционные тесты нового `GET /journal/conversations/{id}/count`-endpoint'а + * в [journalRoutes]. + * + * Поднимается embedded CIO-сервер с реальным [InMemoryJournalStore], пишутся + * `UserMessage` через [pw.binom.agentik.journal.MutableJournalStore.append], + * затем дёргаются оба варианта (с/без `after`) и проверяются JSON-shape + + * семантика. + */ +class JournalRoutesCountTest { + + private lateinit var server: EmbeddedServer<*, *> + private lateinit var journal: InMemoryJournalStore + private var port: Int = 0 + + @Serializable + private data class CountResponse(val count: Long) + + @BeforeTest + fun setup() { + journal = InMemoryJournalStore() + val fakeAgent = FakeAgent(journal) + server = embeddedServer(ServerCIO, port = 0, host = "127.0.0.1") { + routing { agentikAgent(fakeAgent, path = "/agentik", token = null) } + }.start(wait = false) + port = runBlocking { server.engine.resolvedConnectors()[0].port } + } + + @AfterTest + fun tearDown() { + server.stop(100, 200) + journal.close() + } + + private fun append(id: String, convId: String, text: String, at: Instant) { + kotlinx.coroutines.runBlocking { + journal.append( + MessageRecord.UserMessage( + id = id, + conversationId = convId, + content = listOf(JContent.Text(text)), + createdAt = at, + ), + ) + } + } + + private fun client(): HttpClient = HttpClient(CIO) { + install(ContentNegotiation) { + json(Json { ignoreUnknownKeys = true }) + } + } + + @Test + fun `count without after returns total in conversation`() = runBlocking { + val t0 = Instant.parse("2026-09-22T10:00:00Z") + append("m1", "c1", "a", t0) + append("m2", "c1", "b", t0 + 1.seconds) + append("m3", "c2", "c", t0 + 2.seconds) + + val client = client() + try { + val resp = client.get("http://127.0.0.1:$port/agentik/journal/conversations/c1/count") + assertEquals(HttpStatusCode.OK, resp.status) + assertEquals(CountResponse(count = 2L), resp.body()) + } finally { + client.close() + } + } + + @Test + fun `count with after returns strictly-after count`() = runBlocking { + val t0 = Instant.parse("2026-09-22T10:00:00Z") + append("m1", "c1", "a", t0) + append("m2", "c1", "b", t0 + 10.seconds) + append("m3", "c1", "c", t0 + 20.seconds) + + val client = client() + try { + val resp = client.get("http://127.0.0.1:$port/agentik/journal/conversations/c1/count") { + parameter("after", t0.toString()) + } + assertEquals(HttpStatusCode.OK, resp.status) + // strictly > t0 → m2, m3 + assertEquals(CountResponse(count = 2L), resp.body()) + } finally { + client.close() + } + } + + @Test + fun `count for unknown conversation returns zero`() = runBlocking { + val client = client() + try { + val resp = client.get("http://127.0.0.1:$port/agentik/journal/conversations/never-existed/count") + assertEquals(HttpStatusCode.OK, resp.status) + assertEquals(CountResponse(count = 0L), resp.body()) + } finally { + client.close() + } + } + + @Test + fun `count body is JSON with count key`() = runBlocking { + val t0 = Instant.parse("2026-09-22T10:00:00Z") + append("m1", "c1", "x", t0) + append("m2", "c1", "y", t0 + 1.seconds) + + val client = client() + try { + val resp = client.get("http://127.0.0.1:$port/agentik/journal/conversations/c1/count") + assertEquals(HttpStatusCode.OK, resp.status) + val raw = resp.bodyAsText() + // Сырое JSON-тело — стабильный shape, не зависит от сериализатора. + assertEquals("""{"count":2}""", raw) + } finally { + client.close() + } + } + + @Test + fun `count isolates by conversation`() = runBlocking { + val t = Instant.parse("2026-09-22T10:00:00Z") + for (i in 1..5) append("m$i", "c1", "x", t + i.seconds) + append("n1", "c2", "y", t) + + val client = client() + try { + val r1 = client.get("http://127.0.0.1:$port/agentik/journal/conversations/c1/count").body() + val r2 = client.get("http://127.0.0.1:$port/agentik/journal/conversations/c2/count").body() + assertEquals(5L, r1.count) + assertEquals(1L, r2.count) + } finally { + client.close() + } + } + + /** + * Минимальный stub-агент: только [JournalStore] нужен для теста + * `journalRoutes`. Outbox/ConversationStore — noop-объекты, + * чтобы старт [agentikAgent] не валился. + */ + private class FakeAgent(private val js: JournalStore) : Agent { + override val id: String = "test" + override val journal: JournalStore = js + override val outbox: OutboxStore = object : OutboxStore { + override fun events(after: Instant?) = emptyFlow() + override fun agentEvents(after: Instant?) = emptyFlow() + override fun conversationEvents(after: Instant?, conversationId: String?) = emptyFlow() + override suspend fun earliestEventDate(): Instant = Instant.DISTANT_PAST + override fun close() {} + } + override val conversationStore: ConversationStore = object : ConversationStore { + override suspend fun get(id: String) = null + override suspend fun list(offset: Int, limit: Int) = emptyList() + override fun close() {} + } + + override fun createConversation(temp: Boolean): pw.binom.agentik.proto.Conversation = + TODO("not used") + override suspend fun getConversation(id: String): pw.binom.agentik.proto.Conversation? = null + override suspend fun deleteConversation(id: String): Boolean = false + override suspend fun renameConversation(id: String, title: String?): Instant? = null + override fun close() {} + } +} diff --git a/standalone/build.gradle.kts b/standalone/build.gradle.kts index a3d9f3f..2e2bdc4 100644 --- a/standalone/build.gradle.kts +++ b/standalone/build.gradle.kts @@ -70,7 +70,7 @@ kotlin { // ksqlite объявлен как `implementation` (не `api`) в каждом // ksqlite-модуле, поэтому SqliteStores здесь использует // SQLiteConnection напрямую — фиксируем зависимость явно. - implementation("pw.binom.db:ksqlite:0.1.2") + implementation("pw.binom.db:ksqlite:0.1.3") // Bounded-tail live event stream + per-event TTL. implementation(project(":outbox-inmemory")) diff --git a/vector-index-ksqlite/build.gradle.kts b/vector-index-ksqlite/build.gradle.kts index b38fa73..7a2f0a7 100644 --- a/vector-index-ksqlite/build.gradle.kts +++ b/vector-index-ksqlite/build.gradle.kts @@ -7,7 +7,7 @@ plugins { // обычная SQLite БД. Подходит для small-to-medium масштабов (≤10K записей на // embedding ~512d); для больших датасетов — sqlite-vec или JVector. // -// Цели сборки — только те, для которых ksqlite 0.1.2 опубликован в Maven Central: +// Цели сборки — только те, для которых ksqlite 0.1.3 опубликован в Maven Central: // jvm + linuxX64/Arm64 + mingwX64. Apple/iOS/tvOS/watchOS — НЕ публикуются; // для них использовать JVM-only `:vector-index-jvector`. @@ -20,8 +20,8 @@ kotlin { sourceSets { commonMain.dependencies { - // ksqlite 0.1.2 опубликован в Maven Central. - implementation("pw.binom.db:ksqlite:0.1.2") + // ksqlite 0.1.3 опубликован в Maven Central. + implementation("pw.binom.db:ksqlite:0.1.3") api(project(":vector-index-api")) } commonTest.dependencies {