主人 2026-10-03 拍板「注释一律中文」。本次先把本会话动过的文件就地翻; 不做全仓一次性翻译,存量按包分批排期(见 工作小结.md 待办)。 范围:ws_frame_scope.go / ws_frame_scope_test.go / broadcast.go / ws_hub_turn.go / ws_hub_message_event.go / session_read.go / server.go / ws_hub_stream_scope_test.go / history_write_broadcast_test.go / mood_test.go / extra_test.go 含中文的整行注释 18 → 382。 校验(判据 = "只改注释、零代码字节改动"): - `git diff -U0 | 排除 +++/--- | 排除注释行/空行/行尾注释` → 为空 - 独立实现:两侧剥掉注释后逐行比对代码文本 → 11/11 文件全等,不一致数 0 - 行尾注释 7 行逐条列出,代码列与原文逐字节相同(gofmt 未要求重排) - gofmt -l 干净;全树 go build/vet 绿;go test ./... = 58 包 ok / 0 FAIL; linux/amd64 + darwin/arm64 交叉编译绿 语义未删减:部分折行英文长句合并为更少行,故整行注释数 438 → 399。
150 lines
5.2 KiB
Go
150 lines
5.2 KiB
Go
package gateway
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/bus"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
|
)
|
|
|
|
// scopeStreamAdapter 是一个 model.Adapter,发出两个文本增量,使测试
|
|
// 能驱动真实 loop 并观察它产出的成帧事件。
|
|
type scopeStreamAdapter struct{}
|
|
|
|
func (scopeStreamAdapter) Name() string { return "scope-test" }
|
|
|
|
func (scopeStreamAdapter) Chat(context.Context, model.Request, model.Options) (model.Response, error) {
|
|
return model.Response{Content: "Hel lo"}, nil
|
|
}
|
|
|
|
func (scopeStreamAdapter) ChatStream(_ context.Context, _ model.Request, on func(model.StreamEvent)) (model.Response, error) {
|
|
if on != nil {
|
|
on(model.StreamEvent{Kind: model.StreamKindTextDelta, Delta: "Hel"})
|
|
on(model.StreamEvent{Kind: model.StreamKindTextDelta, Delta: "lo"})
|
|
}
|
|
return model.Response{Content: "Hel lo"}, nil
|
|
}
|
|
|
|
// newScopedStreamHub 把 hub 接到真实 loop,其 OnStream 闭包与
|
|
// cmd/cyrene 的 streamChunk 一致(把整个 StreamEvent 值发布到
|
|
// ws.stream),因此测试走的是生产路径 OnStream → bus → relay → frame。
|
|
// 它返回 hub 与一个已注册的假连接。
|
|
func newScopedStreamHub(t *testing.T) (*hub, *wsConn) {
|
|
t.Helper()
|
|
b := bus.New()
|
|
ag, err := agent.New("You are a helpful assistant.", nil, nil, agent.WithModel(scopeStreamAdapter{}, nil))
|
|
if err != nil {
|
|
t.Fatalf("agent.New: %v", err)
|
|
}
|
|
lp := loop.New(loop.Options{
|
|
Agent: ag,
|
|
MaxRounds: 8,
|
|
OnStream: func(ev model.StreamEvent) {
|
|
_ = b.Publish(context.Background(), wsStreamSubject, ev)
|
|
},
|
|
})
|
|
hb := newTestHub(lp, b)
|
|
hb.streaming = true
|
|
hb.start()
|
|
t.Cleanup(hb.close)
|
|
c := newOwnerConn(hb, 64)
|
|
hb.register(c)
|
|
return hb, c
|
|
}
|
|
|
|
// readDeltas 扫描连接的帧,直到 want 个 message.delta 帧到达。增量经
|
|
// bus 异步到达该连接,而 runTurn 自己的 stream_end 是直接发给连接
|
|
// 的,因此测试不能假定顺序。
|
|
func readDeltas(t *testing.T, c *wsConn, want int) []wsEvent {
|
|
t.Helper()
|
|
var deltas []wsEvent
|
|
deadline := time.After(3 * time.Second)
|
|
for len(deltas) < want {
|
|
select {
|
|
case raw := <-c.send:
|
|
var ev wsEvent
|
|
if err := json.Unmarshal(raw, &ev); err != nil {
|
|
t.Fatalf("decode frame %s: %v", raw, err)
|
|
}
|
|
if ev.Type == "message.delta" {
|
|
deltas = append(deltas, ev)
|
|
}
|
|
case <-deadline:
|
|
t.Fatalf("timed out with %d message.delta frames, want %d", len(deltas), want)
|
|
}
|
|
}
|
|
return deltas
|
|
}
|
|
|
|
// TestStreamFramesCarrySessionScope 是 A1 的端到端锁定:经 hub 的
|
|
// runTurn 驱动的一轮必须发出携带会话与轮次 id 的 message.delta 帧,
|
|
// 使客户端能把它们路由到活跃会话。它跑真实 loop 而不是直接调用
|
|
// broadcastStream,这同时证明 bus 原样携带了新字段 —— 无需改动
|
|
// 发布方。
|
|
func TestStreamFramesCarrySessionScope(t *testing.T) {
|
|
t.Parallel()
|
|
hb, c := newScopedStreamHub(t)
|
|
|
|
if _, err := hb.runTurn(context.Background(), "web", []model.Message{model.TextMessage(model.RoleUser, "hi")}, loop.OwnerEnv()); err != nil {
|
|
t.Fatalf("runTurn: %v", err)
|
|
}
|
|
deltas := readDeltas(t, c, 2)
|
|
|
|
for i, ev := range deltas {
|
|
if ev.SessionID != "web" {
|
|
t.Fatalf("delta[%d] sessionId = %q, want web (frame=%+v)", i, ev.SessionID, ev)
|
|
}
|
|
if !strings.HasPrefix(ev.TurnID, "turn-") {
|
|
t.Fatalf("delta[%d] turnId = %q, want a turn-* id (frame=%+v)", i, ev.TurnID, ev)
|
|
}
|
|
}
|
|
if deltas[0].TurnID != deltas[1].TurnID {
|
|
t.Fatalf("turnId differs within one turn: %q vs %q", deltas[0].TurnID, deltas[1].TurnID)
|
|
}
|
|
if deltas[0].Seq == 0 || deltas[1].Seq <= deltas[0].Seq {
|
|
t.Fatalf("seq not increasing: %d then %d", deltas[0].Seq, deltas[1].Seq)
|
|
}
|
|
if data, ok := deltas[0].Data.(map[string]any); !ok || data["delta"] != "Hel" {
|
|
t.Fatalf("first delta data = %+v, want delta=Hel", deltas[0].Data)
|
|
}
|
|
}
|
|
|
|
// TestStreamFramesTurnScopeChangesPerTurn 覆盖编排下的续跑形态:在
|
|
// 首轮完成之后被排空的待处理轮次是第二次 runTurn 调用,因此它必须
|
|
// 取新的 turnId。否则续跑轮次的增量与上一轮的将无法区分。
|
|
func TestStreamFramesTurnScopeChangesPerTurn(t *testing.T) {
|
|
t.Parallel()
|
|
hb, c := newScopedStreamHub(t)
|
|
|
|
msgs := []model.Message{model.TextMessage(model.RoleUser, "hi")}
|
|
if _, err := hb.runTurn(context.Background(), "web", msgs, loop.OwnerEnv()); err != nil {
|
|
t.Fatalf("first runTurn: %v", err)
|
|
}
|
|
first := readDeltas(t, c, 2)
|
|
if _, err := hb.runTurn(context.Background(), "web", msgs, loop.OwnerEnv()); err != nil {
|
|
t.Fatalf("second runTurn: %v", err)
|
|
}
|
|
second := readDeltas(t, c, 2)
|
|
|
|
for i, ev := range second {
|
|
if ev.SessionID != "web" {
|
|
t.Fatalf("second turn delta[%d] sessionId = %q, want web", i, ev.SessionID)
|
|
}
|
|
if ev.TurnID != second[0].TurnID {
|
|
t.Fatalf("second turn delta[%d] turnId = %q, want %q for the whole turn", i, ev.TurnID, second[0].TurnID)
|
|
}
|
|
}
|
|
if first[0].TurnID == second[0].TurnID {
|
|
t.Fatalf("turnId reused across turns: %q", first[0].TurnID)
|
|
}
|
|
if second[0].Seq <= first[1].Seq {
|
|
t.Fatalf("seq not monotonic across turns: %d then %d", first[1].Seq, second[0].Seq)
|
|
}
|
|
}
|