Remove :storage-inmemory module, tests, and related code.
ci / JVM build + tests (push) Successful in 6m46s
ci / JVM build + tests (push) Successful in 6m46s
This commit is contained in:
+5
-5
@@ -14,15 +14,15 @@ import kotlinx.coroutines.withContext
|
||||
/**
|
||||
* ksqlite-реализация [MutableJournalStore] (append-only audit log).
|
||||
*
|
||||
* Миграция завершена: это ЕДИНСТВЕННЫЙ класс для message-таблицы. Параллельная
|
||||
* копия `:storage-ksqlite/KsqliteMessageStore.kt` удалена вместе со своим
|
||||
* тестом (consumer `KsqliteStores.assemble()` уже мигрировал на этот класс).
|
||||
* Единственный класс для message-таблицы. Используется напрямую через
|
||||
* `:journal-ksqlite` зависимость; bundle'ом собирает
|
||||
* `pw.binom.agentik.standalone.persistence.SqliteStores`.
|
||||
*
|
||||
* ## Lifecycle соединения
|
||||
*
|
||||
* Три формы конструктора с разной семантикой владения:
|
||||
* - `KsqliteJournalStore(connection)` — внешнее соединение, store НЕ закрывает
|
||||
* его в [close]. Для shared-connection bundles (`KsqliteStores.assemble`),
|
||||
* его в [close]. Для shared-connection bundles (`SqliteStores.assemble`),
|
||||
* где один connection используется многими store'ами и закрывается bundle'ом.
|
||||
* - `KsqliteJournalStore(path)` — открывает файловое соединение, закрывает
|
||||
* его в [close].
|
||||
@@ -63,7 +63,7 @@ class KsqliteJournalStore private constructor(
|
||||
|
||||
/**
|
||||
* Внешнее соединение — store НЕ закрывает его в [close]. Для
|
||||
* shared-connection bundles (`KsqliteStores.assemble`), где один
|
||||
* shared-connection bundles (`SqliteStores.assemble`), где один
|
||||
* connection используется многими store'ами и закрывается bundle'ом.
|
||||
*/
|
||||
constructor(connection: SQLiteConnection) : this(
|
||||
|
||||
+2
-2
@@ -24,7 +24,7 @@ import kotlinx.coroutines.withContext
|
||||
* Семантика владения connection'ом идентична [KsqliteJournalStore]:
|
||||
* - `KsqliteMutableConversationStore(connection)` — внешнее соединение,
|
||||
* store НЕ закрывает его в [close] (используется shared-connection
|
||||
* bundle'ом `KsqliteStores.assemble`).
|
||||
* bundle'ом `SqliteStores.assemble`).
|
||||
* - `KsqliteMutableConversationStore(path)` — открывает файловое соединение,
|
||||
* закрывает его в [close].
|
||||
* - `KsqliteMutableConversationStore.memory(name)` — in-memory, закрывает
|
||||
@@ -47,7 +47,7 @@ class KsqliteMutableConversationStore private constructor(
|
||||
|
||||
/**
|
||||
* Внешнее соединение — store НЕ закрывает его в [close]. Для
|
||||
* shared-connection bundles (`KsqliteStores.assemble`).
|
||||
* shared-connection bundles (`SqliteStores.assemble`).
|
||||
*/
|
||||
constructor(connection: SQLiteConnection) : this(
|
||||
connection = connection,
|
||||
|
||||
+2
-4
@@ -10,10 +10,8 @@ import kotlin.time.Instant
|
||||
/**
|
||||
* Кодирование [MessageRecord] → пара (kind, payloadJson) для SQLite.
|
||||
*
|
||||
* Копия `MessageCodecs.kt` из `:storage-ksqlite` — `internal` helpers
|
||||
* нельзя переиспользовать между модулями, поэтому в каждом backend свой набор.
|
||||
* Чтобы избежать дрейфа при изменении формата payload'а, оба набора синхронизируются
|
||||
* через эти data class'ы (CallPayload/ResultPayload/ErrorPayload).
|
||||
* `internal` helpers живут рядом со своим store'ом (в `:journal-ksqlite`),
|
||||
* не в каком-то внешнем общем модуле.
|
||||
*/
|
||||
internal fun encodeRecord(record: MessageRecord): Pair<String, String> = when (record) {
|
||||
is MessageRecord.UserMessage -> "user" to encodeBodyPayload(
|
||||
|
||||
@@ -82,7 +82,7 @@ object Schema {
|
||||
* user_version как gate перестал работать — два модуля ставят его в 1,
|
||||
* второй вызов short-circuit'ит. Поэтому migrate() просто прогоняет DDL
|
||||
* idempotently; координация multi-module миграций — ответственность
|
||||
* вызывающего (см. `KsqliteStores.open()` в `:storage-ksqlite`).
|
||||
* вызывающего (см. `pw.binom.agentik.standalone.persistence.SqliteStores`).
|
||||
*/
|
||||
fun migrate(conn: SQLiteConnection) {
|
||||
conn.exec("BEGIN")
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
package pw.binom.agentik.journal.ksqlite
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import pw.binom.agentik.journal.ConversationRecord
|
||||
import pw.binom.db.ksqlite.SQLiteConnection
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Instant
|
||||
|
||||
/**
|
||||
* Тесты для [KsqliteMutableConversationStore]. Автономная фикстура —
|
||||
* `SQLiteConnection.memory(...)` + конструктор `KsqliteMutableConversationStore(connection)`.
|
||||
* Store сам прогоняет `Schema.migrate` в init, явный вызов не нужен.
|
||||
*/
|
||||
class KsqliteMutableConversationStoreTest {
|
||||
|
||||
private lateinit var conn: SQLiteConnection
|
||||
private lateinit var store: KsqliteMutableConversationStore
|
||||
|
||||
@BeforeTest
|
||||
fun setup() {
|
||||
conn = SQLiteConnection.memory("conv-${kotlin.random.Random.nextLong()}")
|
||||
store = KsqliteMutableConversationStore(conn)
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
fun tearDown() {
|
||||
store.close()
|
||||
conn.close()
|
||||
}
|
||||
|
||||
private fun rec(id: String, title: String? = null, ts: Instant = Instant.parse("2026-09-15T10:00:00Z")) =
|
||||
ConversationRecord(id, title, false, ts, ts)
|
||||
|
||||
@Test
|
||||
fun testUpsertAndGetRoundtrip() = runTest {
|
||||
store.upsert(rec("c1", "test"))
|
||||
assertEquals(rec("c1", "test"), store.get("c1"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetReturnsNullForMissing() = runTest {
|
||||
assertNull(store.get("nope"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDeleteRemovesAndReturnsTrue() = runTest {
|
||||
store.upsert(rec("c1"))
|
||||
assertTrue(store.delete("c1"))
|
||||
assertNull(store.get("c1"))
|
||||
assertEquals(false, store.delete("c1"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testListSortsByUpdatedAtDesc() = runTest {
|
||||
val t0 = Instant.parse("2026-09-15T10:00:00Z")
|
||||
store.upsert(rec("c1", ts = t0))
|
||||
store.upsert(rec("c2", ts = t0))
|
||||
store.upsert(rec("c3", ts = t0))
|
||||
store.upsert(rec("c4", ts = t0))
|
||||
|
||||
store.touch("c2", t0 + Duration.parse("PT60S"))
|
||||
store.touch("c3", t0 + Duration.parse("PT120S"))
|
||||
store.touch("c4", t0 + Duration.parse("PT180S"))
|
||||
|
||||
val page = store.list(offset = 0, limit = 4)
|
||||
assertEquals(listOf("c4", "c3", "c2", "c1"), page.map { it.id })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testListRespectsOffsetAndLimit() = runTest {
|
||||
val t0 = Instant.parse("2026-09-15T10:00:00Z")
|
||||
for (i in 1..5) store.upsert(rec("c$i", ts = t0 + Duration.parse("PT${i}S")))
|
||||
val p0 = store.list(offset = 0, limit = 2)
|
||||
assertEquals(2, p0.size)
|
||||
val p2 = store.list(offset = 4, limit = 2)
|
||||
assertEquals(1, p2.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRenameUpdatesTitleAndUpdatedAt() = runTest {
|
||||
store.upsert(rec("c1"))
|
||||
val newTs = store.rename("c1", "new title")
|
||||
assertNotNull(newTs)
|
||||
assertEquals("new title", store.get("c1")?.title)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRenameWithNullClearsTitle() = runTest {
|
||||
store.upsert(rec("c1", "old"))
|
||||
store.rename("c1", null)
|
||||
assertNull(store.get("c1")?.title)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRenameReturnsNullForMissing() = runTest {
|
||||
assertNull(store.rename("nope", "x"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTouchUpdatesUpdatedAtOnly() = runTest {
|
||||
val t0 = Instant.parse("2026-09-15T10:00:00Z")
|
||||
val t1 = Instant.parse("2026-09-15T10:01:00Z")
|
||||
store.upsert(ConversationRecord("c1", "title", false, t0, t0))
|
||||
store.touch("c1", t1)
|
||||
val got = store.get("c1")
|
||||
assertEquals("title", got?.title)
|
||||
assertEquals(t1, got?.updatedAt)
|
||||
assertEquals(t0, got?.createdAt)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMemoryFactoryAutoMigratesSchema() = runTest {
|
||||
// Smoke-test: .memory() companion-фабрика должна прогнать Schema.migrate()
|
||||
// автоматически. Если бы миграция не сработала — storePreparedStatement'ы
|
||||
// упали бы на `prepare failed: no such table: conversation` ещё в конструкторе.
|
||||
val owned = KsqliteMutableConversationStore.memory("conv-auto-${kotlin.random.Random.nextLong()}")
|
||||
try {
|
||||
owned.upsert(rec("c1", "hello"))
|
||||
assertEquals("hello", owned.get("c1")?.title)
|
||||
} finally {
|
||||
owned.close()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExternalConnectionConstructorAlsoMigrates() = runTest {
|
||||
// Внешний конструктор `(connection)` ТОЖЕ мигрирует (Schema.migrate idempotent).
|
||||
// Caller может не звать Schema.migrate перед конструктором.
|
||||
val externalConn = SQLiteConnection.memory("conv-external-${kotlin.random.Random.nextLong()}")
|
||||
val s = KsqliteMutableConversationStore(externalConn)
|
||||
try {
|
||||
s.upsert(rec("c1"))
|
||||
assertNotNull(s.get("c1"))
|
||||
} finally {
|
||||
s.close()
|
||||
externalConn.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
package pw.binom.agentik.journal.ksqlite
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import pw.binom.agentik.journal.Content
|
||||
import pw.binom.agentik.journal.ConversationRecord
|
||||
import pw.binom.agentik.journal.MessageRecord
|
||||
import pw.binom.db.ksqlite.SQLiteConnection
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Тесты на Schema.migrate() в `:journal-ksqlite`:
|
||||
* - fresh DB → создаются `conversation` + `message` + индексы
|
||||
* (`idx_conv_updated`, `idx_msg_conv`);
|
||||
* - уже мигрированная БД → migrate() идемпотентен (no-op);
|
||||
* - DB, открытая напрямую через SQLiteConnection (минуя SqliteStores),
|
||||
* migrate() приводит её в боевое состояние;
|
||||
* - `idx_msg_conv` покрывает обе колонки — без этого list()/cascade-clear
|
||||
* делают full-scan по message.
|
||||
*
|
||||
* `working_memory` тестируется в `pw.binom.agentik.context.ksqlite`; `reflection` —
|
||||
* в `pw.binom.agentik.reflection.ksqlite`.
|
||||
*/
|
||||
class SchemaMigrationTest {
|
||||
|
||||
@Test
|
||||
fun `fresh DB gets conversation and message tables and indexes`() = runTest {
|
||||
val conn = SQLiteConnection.memory("mig-fresh-${kotlin.random.Random.nextLong()}")
|
||||
try {
|
||||
Schema.migrate(conn)
|
||||
|
||||
for (table in listOf(
|
||||
Schema.TABLE_CONVERSATION,
|
||||
Schema.TABLE_MESSAGE,
|
||||
)) {
|
||||
assertTrue(tableExists(conn, table), "table '$table' should exist after migrate()")
|
||||
}
|
||||
|
||||
for (index in listOf(
|
||||
Schema.IDX_CONV_UPDATED,
|
||||
Schema.IDX_MSG_CONV,
|
||||
)) {
|
||||
assertTrue(indexExists(conn, index), "index '$index' should exist after migrate()")
|
||||
}
|
||||
} finally {
|
||||
conn.close()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `migrate is idempotent on already-migrated DB`() = runTest {
|
||||
val conn = SQLiteConnection.memory("mig-idem-${kotlin.random.Random.nextLong()}")
|
||||
try {
|
||||
Schema.migrate(conn)
|
||||
// повторный вызов не должен ни упасть, ни пересоздать таблицы
|
||||
// (CREATE IF NOT EXISTS — no-op)
|
||||
Schema.migrate(conn)
|
||||
Schema.migrate(conn)
|
||||
assertTrue(tableExists(conn, Schema.TABLE_CONVERSATION))
|
||||
assertTrue(tableExists(conn, Schema.TABLE_MESSAGE))
|
||||
} finally {
|
||||
conn.close()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `raw SQLiteConnection plus migrate gives working stores`() = runTest {
|
||||
val conn = SQLiteConnection.memory("mig-bundle-${kotlin.random.Random.nextLong()}")
|
||||
Schema.migrate(conn)
|
||||
|
||||
val convStore = KsqliteMutableConversationStore(conn)
|
||||
val msgStore = KsqliteJournalStore(conn)
|
||||
try {
|
||||
convStore.upsert(
|
||||
ConversationRecord(
|
||||
id = "c1", title = "t", isTemporal = false,
|
||||
createdAt = kotlin.time.Instant.parse("2026-09-15T10:00:00Z"),
|
||||
updatedAt = kotlin.time.Instant.parse("2026-09-15T10:00:00Z"),
|
||||
)
|
||||
)
|
||||
msgStore.append(
|
||||
MessageRecord.UserMessage(
|
||||
id = "m1", conversationId = "c1",
|
||||
content = listOf(Content.Text("hi")),
|
||||
createdAt = kotlin.time.Instant.parse("2026-09-15T10:00:01Z"),
|
||||
)
|
||||
)
|
||||
val got = msgStore.list("c1", kotlin.time.Instant.DISTANT_PAST, offset = 0, limit = 10)
|
||||
assertEquals(1, got.size)
|
||||
assertEquals("m1", got[0].id)
|
||||
} finally {
|
||||
convStore.close()
|
||||
msgStore.close()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `idx_msg_conv covers conversation_id and created_at columns`() = runTest {
|
||||
val conn = SQLiteConnection.memory("mig-idx-${kotlin.random.Random.nextLong()}")
|
||||
try {
|
||||
Schema.migrate(conn)
|
||||
val cols = indexColumns(conn, Schema.IDX_MSG_CONV)
|
||||
assertEquals(listOf(Schema.COL_CONVERSATION_ID, Schema.COL_CREATED_AT), cols)
|
||||
} finally {
|
||||
conn.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun tableExists(conn: SQLiteConnection, name: String): Boolean {
|
||||
conn.prepare(
|
||||
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?"
|
||||
).use { stmt ->
|
||||
stmt.bindText(1, name)
|
||||
stmt.executeQuery().use { rs -> return rs.next() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun indexExists(conn: SQLiteConnection, name: String): Boolean {
|
||||
conn.prepare(
|
||||
"SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ?"
|
||||
).use { stmt ->
|
||||
stmt.bindText(1, name)
|
||||
stmt.executeQuery().use { rs -> return rs.next() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun indexColumns(conn: SQLiteConnection, indexName: String): List<String> {
|
||||
// PRAGMA index_info возвращает одну строку на колонку индекса
|
||||
// (seqno, cid, name). Параметризовать через `?` нельзя — собираем
|
||||
// строку (name — контролируемая константа, не user input).
|
||||
val cols = mutableListOf<String>()
|
||||
conn.prepare("PRAGMA index_info($indexName)").use { stmt ->
|
||||
stmt.executeQuery().use { rs ->
|
||||
while (rs.next()) {
|
||||
rs.getText(2)?.let(cols::add)
|
||||
}
|
||||
}
|
||||
}
|
||||
return cols
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user