merge: W10 — 接上可观测性下行帧(概览页实时刷新 metrics / trace / 日志)

This commit is contained in:
AskaEth committed 2026-10-04 18:42:58 +08:00
commit 93941cd1ea
9 files changed
+831 -11

No files matched your search

@@ -0,0 +1,135 @@
package top.yeij.cyrene.ui.admin
import org.junit.Assert.assertEquals
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Test
import top.yeij.cyrene.data.api.dto.AdminTraceSpanDto
import top.yeij.cyrene.data.ws.ObservabilityLogEntryDto
/**
* 可观测推送帧的合并与本地过滤(契约 v0.55 §3.12)的纯函数。
*
* 断言的是**三条不会写进类型里的语义**:新帧在前(列表两端本来就最新在前,不排序)、
* 按业务键去重(帧与 REST 竞态时不会出现两条同样的数据)、窗口截断(不许无界增长);
* 以及日志的等级过滤保留「判不出等级」的条目。
*/
class AdminOverviewMergeTest {
private fun span(id: String) = AdminTraceSpanDto(id = id, name = "span-$id", startedAt = "2026-10-04T12:00:00Z")
private fun entry(msg: String, level: String = "info") =
ObservabilityLogEntryDto(time = "2026-10-04T12:00:00Z", level = level, source = "gateway", msg = msg)
@Test
fun `an empty delta leaves the snapshot untouched`() {
val existing = listOf(span("a"))
// 空增量不是「清空」:REST 引导读取拿到的快照必须原样留着。
assertSame(existing, mergeTraceSpans(existing, emptyList()))
val logs = listOf(entry("启动"))
assertSame(logs, mergeLogEntries(logs, emptyList()))
}
@Test
fun `new spans go in front of the guide snapshot`() {
val merged = mergeTraceSpans(listOf(span("old")), listOf(span("new")))
assertEquals(listOf("new", "old"), merged.map { it.id })
}
@Test
fun `a span already present does not appear twice`() {
// 帧与那次 REST 引导读取可能覆盖同一段:按 id 去重,且**新到的那一份**留在前面。
val merged = mergeTraceSpans(listOf(span("a"), span("b")), listOf(span("b"), span("c")))
assertEquals(listOf("b", "c", "a"), merged.map { it.id })
}
@Test
fun `the trace window keeps only the newest entries`() {
val existing = (1..5).map { span("s$it") }
val merged = mergeTraceSpans(existing, listOf(span("n")), window = 3)
assertEquals(listOf("n", "s1", "s2"), merged.map { it.id })
}
@Test
fun `log entries are deduplicated by their whole business key`() {
// 服务端两端都是**最新在前**,合并保留各自的相对顺序(不排序):帧里新到的在前,
// 已经在列表里的那条不会被重复追加。
val merged = mergeLogEntries(listOf(entry("启动")), listOf(entry("慢请求"), entry("启动")))
assertEquals(listOf("慢请求", "启动"), merged.map { it.msg })
}
@Test
fun `log entries differing only in level are distinct`() {
val merged = mergeLogEntries(
listOf(entry("同一句", level = "info")),
listOf(entry("同一句", level = "error")),
)
assertEquals(2, merged.size)
}
@Test
fun `the log window keeps only the newest entries`() {
val existing = (1..4).map { entry("m$it") }
val merged = mergeLogEntries(existing, listOf(entry("fresh")), window = 2)
assertEquals(listOf("fresh", "m1"), merged.map { it.msg })
}
@Test
fun `no level filter keeps every entry`() {
val entries = listOf(entry("a", "debug"), entry("b", "info"), entry("c", "error"))
assertSame(entries, filterLogEntries(entries, null))
assertSame(entries, filterLogEntries(entries, ""))
// 未知的过滤档不猜:退回「不过滤」,而不是把列表过滤空。
assertSame(entries, filterLogEntries(entries, "verbose"))
}
@Test
fun `a minimum level keeps that level and above`() {
val entries = listOf(entry("d", "debug"), entry("i", "info"), entry("w", "warn"), entry("e", "error"))
// 与后端 `?level=` 同口径:是最低等级,不是精确匹配。
assertEquals(listOf("w", "e"), filterLogEntries(entries, "warn").map { it.msg })
assertEquals(listOf("e"), filterLogEntries(entries, "error").map { it.msg })
assertEquals(4, filterLogEntries(entries, "debug").size)
}
@Test
fun `the level filter tolerates case and blanks and keeps unknown levels`() {
val entries = listOf(entry("w", " WARN "), entry("u", "trace"), entry("i", "info"))
// 等级是开放集合:判不出高低的条目一律保留,绝不因为「可能低于阈值」被静默藏掉。
assertEquals(listOf("w", "u"), filterLogEntries(entries, "warn").map { it.msg })
}
@Test
fun `log keys distinguish the four fields of an entry`() {
val base = entry("同一句")
val keys = setOf(
logEntryKey(base),
logEntryKey(base.copy(time = "2026-10-04T12:00:01Z")),
logEntryKey(base.copy(level = "warn")),
logEntryKey(base.copy(source = "other")),
logEntryKey(base.copy(msg = "另一句")),
)
assertEquals(5, keys.size)
}
@Test
fun `window constants match what the contract promises`() {
// 契约 §3.12:单帧上限 trace 100 / log 200,客户端保留窗口与它们对齐。
assertEquals(100, TRACE_WINDOW)
assertEquals(200, LOG_WINDOW)
assertTrue(TRACE_WINDOW > 0)
}
}