mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-12 04:07:34 +00:00
handler_sse_test.go → test/ (6 条判据)
webui/ 167 → 161
webui/test/ 35 → 41
合计 202 不变
## 原来那 6 条判据读的是私有字段
len(r.buf)
r.buf[0].ID / .EventType / .Data
`sseEventRing` 的对外语义就三条:容量上限、按 Last-Event-ID 重放、
未知 ID 回退全量。它们全部可以从**导出接口**断言:
NewSSEEventRing(cap) 构造
Append(id, type, data) 写入
After(id) []SSEEventRecord 重放
所以导出 `sseEventRing → SSEEventRing`、`SSEEventRecord` 的字段、
以及构造入口 `NewSSEEventRing`。
**为什么这不是"为测试导出内部"**:`After` 返回的东西就是客户端断线
重连时实际收到的内容 —— id、event、payload 三样。它们是外部可见的输出,
把字段藏起来只让包内测试能读,才是反过来的怪事。
测试侧的 `r.buf[i]` 统一换成 `r.After("")[i]`(空 ID 命中不了,按约定
回退全量),语义等价且不再触碰内部状态。
## 判据有效性(变异验证)
把容量裁剪短路掉(`if false && len(r.buf) > r.cap`):
--- FAIL: TestSSEEventRing_Append_Capacity
expected 3, got 5
环不再丢最旧条目 ⇒ 断线重连会重放超过窗口的历史,客户端 Last-Event-ID
语义失效。恢复后全绿。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 67 包全过,0 FAIL
98 lines
2.3 KiB
Go
98 lines
2.3 KiB
Go
package test
|
||
|
||
import (
|
||
"encoding/json"
|
||
"github.com/JianFeeeee/HologramAgent/internal/plugins/webui"
|
||
"reflect"
|
||
"testing"
|
||
)
|
||
|
||
func TestSSEEventRing_Append_Capacity(t *testing.T) {
|
||
r := webui.NewSSEEventRing(3)
|
||
data := json.RawMessage(`{"a":1}`)
|
||
for i := 0; i < 5; i++ {
|
||
r.Append("id-"+string(rune('0'+i)), "agent_output", data)
|
||
}
|
||
// cap=3,只保留 id-2,id-3,id-4
|
||
if len(r.After("")) != 3 {
|
||
t.Fatalf("expected 3, got %d", len(r.After("")))
|
||
}
|
||
if r.After("")[0].ID != "id-2" {
|
||
t.Fatalf("expected id-2, got %s", r.After("")[0].ID)
|
||
}
|
||
}
|
||
|
||
func TestSSEEventRing_After_Mid(t *testing.T) {
|
||
r := webui.NewSSEEventRing(10)
|
||
data := json.RawMessage(`{"x":"y"}`)
|
||
r.Append("id-1", "reasoning", data)
|
||
r.Append("id-2", "agent_output", data)
|
||
r.Append("id-3", "tool_call", data)
|
||
|
||
after := r.After("id-2")
|
||
if len(after) != 1 || after[0].ID != "id-3" {
|
||
t.Fatalf("expected [id-3], got %v", after)
|
||
}
|
||
|
||
after = r.After("id-3")
|
||
if len(after) != 0 {
|
||
t.Fatalf("expected 0, got %d", len(after))
|
||
}
|
||
}
|
||
|
||
func TestSSEEventRing_After_NotFound_ReturnsAll(t *testing.T) {
|
||
r := webui.NewSSEEventRing(3)
|
||
data := json.RawMessage(`{}`)
|
||
r.Append("id-1", "agent_output", data)
|
||
r.Append("id-2", "agent_output", data)
|
||
|
||
after := r.After("id-0")
|
||
if len(after) != 2 {
|
||
t.Fatalf("expected 2 (fallback to all), got %d", len(after))
|
||
}
|
||
}
|
||
|
||
func TestSSEEventRing_After_Last_Tip(t *testing.T) {
|
||
r := webui.NewSSEEventRing(5)
|
||
data := json.RawMessage(`{"a":1}`)
|
||
for i := 0; i < 5; i++ {
|
||
r.Append("id-"+string(rune('0'+i)), "agent_output", data)
|
||
}
|
||
after := r.After("id-4")
|
||
if len(after) != 0 {
|
||
t.Fatalf("expected 0 after tip, got %d", len(after))
|
||
}
|
||
}
|
||
|
||
func TestSSEEventRing_Concurrent(t *testing.T) {
|
||
r := webui.NewSSEEventRing(100)
|
||
done := make(chan struct{})
|
||
for i := 0; i < 8; i++ {
|
||
go func(n int) {
|
||
defer func() { done <- struct{}{} }()
|
||
data := json.RawMessage(`{}`)
|
||
for j := 0; j < 100; j++ {
|
||
r.Append("w"+string(rune('0'+n))+"/"+string(rune('0'+j)), "agent_output", data)
|
||
}
|
||
}(i)
|
||
}
|
||
for i := 0; i < 8; i++ {
|
||
go func() {
|
||
defer func() { done <- struct{}{} }()
|
||
for j := 0; j < 100; j++ {
|
||
_ = r.After("w0/0")
|
||
}
|
||
}()
|
||
}
|
||
for i := 0; i < 16; i++ {
|
||
<-done
|
||
}
|
||
}
|
||
|
||
func TestSSEEventRecord_Type(t *testing.T) {
|
||
var got interface{} = webui.SSEEventRecord{}
|
||
if reflect.TypeOf(got).Kind() != reflect.Struct {
|
||
t.Fatal("webui.SSEEventRecord should be a struct")
|
||
}
|
||
}
|