feat: 初始化 Android 客户端(M0 骨架 + M1 登录 + M2 基础模块)

M0:Gradle 9.7.1 / AGP 9.2.1 / Kotlin 2.4 / Miuix 0.9.3,minSdk 33,compileSdk 37。
M1:登录页(服务器地址可配、服务端/本地错误分流、EncryptedSharedPreferences、自动登录与静默重登、401 登出)。
M2 基础模块:消息内容解析器(<p>/<action>/<narration> + 流式增量)、WebSocket 客户端、REST 数据层与分页去重。
45 个单元测试全绿,真机验证通过(登录页 + 启动图标)。
devdocs/ 收录设计文档、前后端协同事项与细化方案。
This commit is contained in:
AskaEth committed 2026-09-08 20:40:58 +08:00
commit 84de51c7b7
67 files changed
+3960

No files matched your search

@@ -0,0 +1,88 @@
package top.yeij.cyrene.data.repository
import top.yeij.cyrene.data.api.dto.HistoryMessageDto
import org.junit.Assert.assertEquals
import org.junit.Test
class MessageMergeTest {
private fun msg(id: String, createdAt: Long, content: String = id) = HistoryMessageDto(
id = id,
sessionId = "web",
role = "user",
content = content,
createdAt = createdAt,
)
private fun idsOf(messages: List<HistoryMessageDto>) = messages.map { it.id }
@Test
fun `overlapping page dedupes by id`() {
val existing = listOf(msg("m2", 200), msg("m1", 100))
val incoming = listOf(msg("m3", 300), msg("m2", 200))
val merged = mergeMessages(existing, incoming)
assertEquals(listOf("m1", "m2", "m3"), idsOf(merged))
}
@Test
fun `out of order incoming page is sorted ascending`() {
val existing = listOf(msg("m1", 100), msg("m2", 200))
val incoming = listOf(msg("m4", 400), msg("m3", 300))
val merged = mergeMessages(existing, incoming)
assertEquals(listOf("m1", "m2", "m3", "m4"), idsOf(merged))
}
@Test
fun `same millisecond falls back to id ordering`() {
val existing = listOf(msg("b", 100), msg("a", 100))
val incoming = listOf(msg("c", 100))
val merged = mergeMessages(existing, incoming)
assertEquals(listOf("a", "b", "c"), idsOf(merged))
}
@Test
fun `empty incoming page keeps existing`() {
val existing = listOf(msg("m1", 100), msg("m2", 200))
val merged = mergeMessages(existing, emptyList())
assertEquals(listOf("m1", "m2"), idsOf(merged))
}
@Test
fun `empty existing returns sorted incoming`() {
val incoming = listOf(msg("m3", 300), msg("m1", 100))
val merged = mergeMessages(emptyList(), incoming)
assertEquals(listOf("m1", "m3"), idsOf(merged))
}
@Test
fun `incoming wins on duplicate id`() {
val existing = listOf(msg("m1", 100, content = "old"))
val incoming = listOf(msg("m1", 100, content = "new"))
val merged = mergeMessages(existing, incoming)
assertEquals(1, merged.size)
assertEquals("new", merged.single().content)
}
@Test
fun `merge does not mutate inputs`() {
val existing = listOf(msg("m2", 200), msg("m1", 100))
val incoming = listOf(msg("m3", 300))
mergeMessages(existing, incoming)
assertEquals(listOf("m2", "m1"), idsOf(existing))
assertEquals(listOf("m3"), idsOf(incoming))
}
}