docs(gateway): WS 帧门相关 11 个文件的注释改中文(只改注释,代码零字节改动)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

主人 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。
This commit is contained in:
AskaEth committed 2026-10-03 22:07:56 +08:00
1 parent 770a83c3fb
commit fe1ff33115
11 files changed
+392 -431

No files matched your search

+98 -100
View File
@@ -36,13 +36,13 @@ import (
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
)
// cfgDefault builds the minimum config used across tests.
// cfgDefault 构造各测试共用的最小配置。
func cfgDefault() *config.Config {
return &config.Config{Auth: config.AuthConfig{AdminUser: "admin", AdminPass: "secret", JWTSecret: testSecret}}
}
// buildEnv constructs a Server + httptest server from an explicit cfg/deps,
// defaulting Auth to a real JWTService and Bus to a real in-process bus.
// buildEnv 由显式的 cfg/deps 构造 Server + httptest server,Auth 默认为
// 真实 JWTService,Bus 默认为真实的进程内 bus。
func buildEnv(t *testing.T, cfg *config.Config, deps Deps) (*Server, *httptest.Server, *auth.JWTService) {
t.Helper()
if cfg == nil {
@@ -64,8 +64,8 @@ func buildEnv(t *testing.T, cfg *config.Config, deps Deps) (*Server, *httptest.S
return srv, ts, jwt
}
// buildServerOnly constructs a Server without an httptest server (used for
// start/shutdown lifecycle tests).
// buildServerOnly 构造不带 httptest server 的 Server(用于启动/关闭
// 生命周期测试)。
func buildServerOnly(t *testing.T, cfg *config.Config, deps Deps) *Server {
t.Helper()
srv, err := New(cfg, deps)
@@ -75,13 +75,13 @@ func buildServerOnly(t *testing.T, cfg *config.Config, deps Deps) *Server {
return srv
}
// mustJWT builds a real JWTService for a test secret.
// mustJWT 为测试密钥构造真实 JWTService。
func mustJWT(t *testing.T) *auth.JWTService {
t.Helper()
return auth.New(config.AuthConfig{JWTSecret: testSecret})
}
// signAny issues a token for sub under the given role via a real JWTService.
// signAny 经真实 JWTService 为 sub 签发指定角色的 token。
func signAny(t *testing.T, jwt *auth.JWTService, sub string, role auth.Role) string {
t.Helper()
tok, err := jwt.Sign(sub, role, time.Hour)
@@ -91,7 +91,7 @@ func signAny(t *testing.T, jwt *auth.JWTService, sub string, role auth.Role) str
return tok
}
// doRaw performs a request with a raw (already-encoded) body.
// doRaw 用原始(已编码)请求体发起请求。
func doRaw(t *testing.T, method, url, raw, token string) (*http.Response, []byte) {
t.Helper()
req, err := http.NewRequest(method, url, strings.NewReader(raw))
@@ -114,9 +114,9 @@ func doRaw(t *testing.T, method, url, raw, token string) (*http.Response, []byte
return resp, b
}
// --- Mocks ------------------------------------------------------------------
// --- 测试替身(mock) ---------------------------------------------------------
// stubAuth wraps a real JWTService but lets Sign/Verify fail on demand.
// stubAuth 包装真实 JWTService,但可按需让 Sign/Verify 失败。
type stubAuth struct {
jwt *auth.JWTService
signErr error
@@ -141,7 +141,7 @@ func (s *stubAuth) Authorize(required ...auth.Role) func(http.Handler) http.Hand
return s.jwt.Authorize(required...)
}
// stubCaps mimics CapabilityStore with a fixed list and a forced SetPolicy error.
// stubCaps 以固定列表和强制出错的 SetPolicy 模拟 CapabilityStore。
type stubCaps struct {
list []capreg.Capability
setErr error
@@ -167,7 +167,7 @@ func (s *stubCaps) SetPolicy(name string, p capreg.ToolPolicy) error {
return nil
}
// stubSessions mimics SessionStore.
// stubSessions 模拟 SessionStore。
type stubSessions struct {
list []session.Session
err error
@@ -175,20 +175,20 @@ type stubSessions struct {
func (s *stubSessions) List(ctx context.Context) ([]session.Session, error) { return s.list, s.err }
// stubPersonas mimics PersonaStore, returning domain characters that the gateway
// maps to PersonaSummary.
// stubPersonas 模拟 PersonaStore,返回 gateway 会映射为 PersonaSummary
// 的领域角色对象。
type stubPersonas struct {
list []personareg.Character
}
func (s *stubPersonas) List() []personareg.Character { return s.list }
// Persona satisfies PersonaStore; tests don't rely on visual assets, so it
// always reports no persona found (empty visuals, safe fallback).
// Persona 满足 PersonaStore;测试不依赖视觉素材,因此它总是报告未找到
// 角色(空视觉,安全回退)。
func (s *stubPersonas) Persona(string) (*loader.Persona, bool) { return nil, false }
// stubMemories mimics MemoryStore, returning domain memory items that the gateway
// maps to MemorySummary.
// stubMemories 模拟 MemoryStore,返回 gateway 会映射为 MemorySummary
// 的领域记忆条目。
type stubMemories struct {
list []memory.MemoryItem
err error
@@ -196,8 +196,8 @@ type stubMemories struct {
func (s *stubMemories) List(ctx context.Context) ([]memory.MemoryItem, error) { return s.list, s.err }
// stubRag mimics RagStore, returning domain docs/hits the gateway maps to its
// admin wire shape.
// stubRag 模拟 RagStore,返回 gateway 会映射为其 admin 传输结构的
// 领域文档/命中。
type stubRag struct {
docs []knowledge.Doc
hits []knowledge.Hit
@@ -211,7 +211,7 @@ func (s *stubRag) Search(ctx context.Context, query string) ([]knowledge.Hit, er
func (s *stubRag) IndexDocument(ctx context.Context, doc knowledge.Doc) error { return s.err }
func (s *stubRag) DeleteDocument(ctx context.Context, id string) error { return s.err }
// stubIoT mimics IoTStore.
// stubIoT 模拟 IoTStore。
type stubIoT struct {
list []iot.Device
err error
@@ -219,7 +219,7 @@ type stubIoT struct {
func (s *stubIoT) List(ctx context.Context) ([]iot.Device, error) { return s.list, s.err }
// stubAuditor mimics SubagentAuditor for the subagent_ask route.
// stubAuditor 为 subagent_ask 路由模拟 SubagentAuditor。
type stubAuditor struct {
res *orchestration.AuditResult
err error
@@ -229,7 +229,7 @@ func (s *stubAuditor) AuditContext(ctx context.Context, id string, mode orchestr
return s.res, s.err
}
// stubTaskOutputs mimics TaskOutputStore for the task_output_query route.
// stubTaskOutputs 为 task_output_query 路由模拟 TaskOutputStore。
type stubTaskOutputs struct {
ev task.OutputEvent
err error
@@ -239,7 +239,7 @@ func (s *stubTaskOutputs) TaskOutput(ctx context.Context, id string) (task.Outpu
return s.ev, s.err
}
// stubChannel implements sdk.Channel.
// stubChannel 实现 sdk.Channel。
type stubChannel struct{}
func (stubChannel) Name() string { return "qq" }
@@ -250,8 +250,8 @@ func (stubChannel) HealthCheck(ctx context.Context) error { return nil }
var _ sdk.Channel = stubChannel{}
// stubBotChannel mimics a live kook adapter that resolved its bot identity via
// GET /api/v3/user/me on Connect (surfaced through Config()).
// stubBotChannel 模拟一个在 Connect 时经 GET /api/v3/user/me 解析出
// 机器人身份的在线 kook 适配器(经 Config() 暴露)。
type stubBotChannel struct{}
func (stubBotChannel) Name() string { return "kook:conf0" }
@@ -265,7 +265,7 @@ func (stubBotChannel) Config() map[string]any {
var _ sdk.Channel = stubBotChannel{}
// captureRunner records the sessionID each Run call sees.
// captureRunner 记录每次 Run 调用看到的 sessionID。
type captureRunner struct {
got []string
res *loop.Result
@@ -277,14 +277,14 @@ func (c *captureRunner) Run(ctx context.Context, sessionID string, messages []mo
return c.res, c.err
}
// plainWriter is a bare ResponseWriter implementing neither Flusher nor Hijacker.
// plainWriter 是既未实现 Flusher 也未实现 Hijacker 的裸 ResponseWriter。
type plainWriter struct{}
func (plainWriter) Header() http.Header { return http.Header{} }
func (plainWriter) Write(b []byte) (int, error) { return len(b), nil }
func (plainWriter) WriteHeader(code int) {}
// --- Server construction / validation --------------------------------------
// --- Server 构造 / 校验 -------------------------------------------------------
func TestNewValidation(t *testing.T) {
t.Parallel()
@@ -297,7 +297,7 @@ func TestNewValidation(t *testing.T) {
}
}
// --- Login ------------------------------------------------------------------
// --- 登录 --------------------------------------------------------------------
func TestLoginBadBody(t *testing.T) {
t.Parallel()
@@ -321,7 +321,7 @@ func TestLoginSignError(t *testing.T) {
wantErrorShape(t, body)
}
// --- Chat -------------------------------------------------------------------
// --- 聊天 --------------------------------------------------------------------
func TestChatBadBody(t *testing.T) {
t.Parallel()
@@ -356,8 +356,8 @@ func TestChatRequestPersistsMode(t *testing.T) {
History: hist,
})
tok := signAny(t, jwt, "acct", auth.RoleOwner)
// The REST body carries the client's behavior mode (design/16 §5.1); the
// orchestrator persists both the user turn and the assistant reply under it.
// REST 请求体携带客户端的行为模式(design/16 §5.1);orchestrator 在
// 它之下同时持久化用户轮次与助手回复。
resp, body := doReq(t, http.MethodPost, ts.URL+"/api/v1/chat",
map[string]any{"sessionId": "web", "text": "改代码", "mode": "code"}, tok)
if resp.StatusCode != http.StatusOK {
@@ -400,7 +400,7 @@ func TestChatRunnerError(t *testing.T) {
wantErrorShape(t, body)
}
// --- List handlers ---------------------------------------------------------
// --- 列表处理器 --------------------------------------------------------------
func TestListSessionsNonEmpty(t *testing.T) {
t.Parallel()
@@ -523,7 +523,7 @@ func TestAdminMemoriesErrorStillOK(t *testing.T) {
}
}
// --- Admin: IoT ------------------------------------------------------------
// --- Admin:IoT --------------------------------------------------------------
func TestAdminIoTNonEmpty(t *testing.T) {
t.Parallel()
@@ -568,7 +568,7 @@ func TestAdminIoTError(t *testing.T) {
wantErrorShape(t, body)
}
// --- Admin: channels -------------------------------------------------------
// --- Admin:渠道 -------------------------------------------------------------
func TestAdminChannelsFromRegistry(t *testing.T) {
t.Parallel()
@@ -576,7 +576,7 @@ func TestAdminChannelsFromRegistry(t *testing.T) {
if err := env.caps.RegisterChannel(stubChannel{}); err != nil {
t.Fatalf("register channel: %v", err)
}
// A channel-kind capability whose Value is not an sdk.Channel must be skipped.
// Value 不是 sdk.Channel 的 channel 类能力必须被跳过。
if err := env.caps.Register(capreg.Capability{Name: "badchan", Kind: capreg.KindChannel, Value: stubTool{}}); err != nil {
t.Fatalf("register bad channel: %v", err)
}
@@ -597,9 +597,9 @@ func TestAdminChannelsFromRegistry(t *testing.T) {
}
}
// TestAdminChannels_SurfacesBotIdentity verifies a connected kook adapter's
// resolved bot identity (from user/me) reaches the accounts of GET /admin/channels
// so the panel can show who the bot is.
// TestAdminChannels_SurfacesBotIdentity 验证已连接 kook 适配器解析出的
// 机器人身份(来自 user/me)能到达 GET /admin/channels 的 accounts,
// 使面板能展示机器人是谁。
func TestAdminChannels_SurfacesBotIdentity(t *testing.T) {
t.Parallel()
caps := capreg.New()
@@ -637,11 +637,11 @@ func TestAdminChannels_SurfacesBotIdentity(t *testing.T) {
}
}
// --- Admin: policy errors --------------------------------------------------
// --- Admin:策略错误 ---------------------------------------------------------
func TestAdminSetPolicyUnavailable(t *testing.T) {
t.Parallel()
srv, ts, jwt := buildEnv(t, cfgDefault(), Deps{}) // no Registry
srv, ts, jwt := buildEnv(t, cfgDefault(), Deps{}) // 不接 Registry
_ = srv
tok := signAny(t, jwt, "acct", auth.RoleOwner)
resp, body := doReq(t, http.MethodPost, ts.URL+"/api/v1/admin/plugins/policy", map[string]any{"name": "echo"}, tok)
@@ -686,7 +686,7 @@ func TestAdminSetPolicyMissingName(t *testing.T) {
wantErrorShape(t, body)
}
// --- CORS ------------------------------------------------------------------
// --- 跨域(CORS) -------------------------------------------------------------
func TestCORSAllowAll(t *testing.T) {
t.Parallel()
@@ -748,7 +748,7 @@ func TestCORSPreflight(t *testing.T) {
}
}
// --- Recovery + statusRecorder --------------------------------------------
// --- panic 恢复 + statusRecorder ---------------------------------------------
func TestRecoverPanicTo500(t *testing.T) {
t.Parallel()
@@ -778,17 +778,17 @@ func TestStatusRecorderHijackUnsupported(t *testing.T) {
func TestStatusRecorderFlush(t *testing.T) {
t.Parallel()
// Flusher present → forwarded.
// 有 Flusher → 转发。
rr := httptest.NewRecorder()
(&statusRecorder{ResponseWriter: rr}).Flush()
if !rr.Flushed {
t.Fatal("Flush should have been forwarded to the underlying Flusher")
}
// No Flusher → no-op, no panic.
// 无 Flusher → 空操作,不 panic。
(&statusRecorder{ResponseWriter: plainWriter{}}).Flush()
}
// --- SPA -------------------------------------------------------------------
// --- 单页应用(SPA) ---------------------------------------------------------
func TestSPA(t *testing.T) {
t.Parallel()
@@ -799,29 +799,29 @@ func TestSPA(t *testing.T) {
srv, ts, _ := buildEnv(t, cfgDefault(), Deps{Frontend: front})
_ = srv
// Real asset is served.
// 真实静态资源被正常服务。
resp, body := doReq(t, http.MethodGet, ts.URL+"/assets/app.js", nil, "")
if resp.StatusCode != http.StatusOK || string(body) != "var x = 1;" {
t.Fatalf("asset status = %d body = %q, want 200 with js", resp.StatusCode, body)
}
// Unknown non-API path falls back to index.html.
// 未知的非 API 路径回退到 index.html。
resp, body = doReq(t, http.MethodGet, ts.URL+"/chat/somewhere", nil, "")
if resp.StatusCode != http.StatusOK || !strings.Contains(string(body), "root") {
t.Fatalf("fallback status = %d body = %q, want 200 with index", resp.StatusCode, body)
}
// Root serves index.html.
// 根路径提供 index.html。
resp, body = doReq(t, http.MethodGet, ts.URL+"/", nil, "")
if resp.StatusCode != http.StatusOK || !strings.Contains(string(body), "root") {
t.Fatalf("root status = %d body = %q, want 200 with index", resp.StatusCode, body)
}
// API route is passed through untouched.
// API 路由原样透传。
resp, body = doReq(t, http.MethodGet, ts.URL+"/api/v1/health", nil, "")
if resp.StatusCode != http.StatusOK {
t.Fatalf("api status = %d, want 200 (body=%s)", resp.StatusCode, body)
}
}
// --- WS: validation --------------------------------------------------------
// --- WS:校验 ----------------------------------------------------------------
func TestWSRequiresGET(t *testing.T) {
t.Parallel()
@@ -892,7 +892,7 @@ func TestServerCloseShutsHub(t *testing.T) {
conn.Close()
}
// --- WS: hub unit tests ----------------------------------------------------
// --- WS:hub 单元测试 --------------------------------------------------------
func TestHubStreamChat(t *testing.T) {
t.Parallel()
@@ -919,9 +919,8 @@ func TestHubStreamChatPersistsMode(t *testing.T) {
},
History: hist,
})
// The ws.chat payload carries the client's behavior mode (design/16 §5.1);
// streamChat threads it into the orchestrator, which persists both the user
// turn and the assistant reply under it.
// ws.chat 载荷携带客户端的行为模式(design/16 §5.1);streamChat 把它
// 串进 orchestrator,后者在它之下持久化用户轮次与助手回复二者。
hb.streamChat(context.Background(), map[string]any{"sessionId": "web", "text": "改代码", "mode": "code"})
deadline := time.Now().Add(2 * time.Second)
@@ -946,11 +945,11 @@ func TestHubBroadcastStream(t *testing.T) {
c := newOwnerConn(hb, 32)
hb.register(c)
// A text_delta StreamEvent on the OnStream→bus→relay path becomes a typed
// message.delta frame carrying the delta text and a monotonically increasing
// seq, leaving the legacy stream_chunk shape out of the way. The loop stamps
// the session/turn identity on the event and the hub must carry it onto the
// frame, so a client can route the delta to the active session (design/16 §4.2).
// OnStream→bus→relay 路径上的 text_delta StreamEvent 变成带类型的
// message.delta 帧,携带增量文本与单调递增的 seq,让旧的
// stream_chunk 形态不再参与。loop 把会话/轮次身份盖在事件上,
// hub 必须把它带到帧上,使客户端能把增量路由到活跃会话
// (design/16 §4.2)。
hb.broadcastStream(model.StreamEvent{Kind: model.StreamKindTextDelta, Delta: "Hel", Index: 0, SessionID: "web", TurnID: "turn-abc"})
ev := readFakeFrame(t, c)
if ev.Type != "message.delta" {
@@ -967,7 +966,7 @@ func TestHubBroadcastStream(t *testing.T) {
t.Fatalf("frame scope = session %q turn %q, want session web turn turn-abc", ev.SessionID, ev.TurnID)
}
// A tool_call event maps to tool_call.*; a non-StreamEvent payload is dropped.
// tool_call 事件映射为 tool_call.*;非 StreamEvent 的载荷被丢弃。
hb.broadcastStream(model.StreamEvent{Kind: model.StreamKindToolCallEnd, ID: "toolu_1", Index: 0})
ev = readFakeFrame(t, c)
if ev.Type != "tool_call.end" {
@@ -999,11 +998,10 @@ func TestServerBroadcastCapacityAwaken(t *testing.T) {
}
}
// TestServerBroadcastPersistedSplit verifies BroadcastPersisted routes the event
// type by session: the main web session and its web sub-sessions broadcast
// `message_persisted` (web chat realtime sync), while an isolated channel adapter
// session (sess-xxx) broadcasts `channel_persisted` (IM adapter log page) so it is
// naturally ignored by the web chat handler.
// TestServerBroadcastPersistedSplit 验证 BroadcastPersisted 按会话路由事件
// 类型:Web 主会话及其 Web 子会话广播 `message_persisted`(Web 聊天实时
// 同步),而隔离的渠道适配器会话(sess-xxx)广播 `channel_persisted`
// (IM 适配器日志页),因此它被 Web 聊天处理器自然忽略。
func TestServerBroadcastPersistedSplit(t *testing.T) {
t.Parallel()
env := newEnv(t, nil, false)
@@ -1049,7 +1047,7 @@ func TestServerBroadcastPersistedSplit(t *testing.T) {
func TestHubStreamChatIdle(t *testing.T) {
t.Parallel()
hb := newTestHub(nil, nil) // nil runner
hb := newTestHub(nil, nil) // runner 为 nil
c := newOwnerConn(hb, 32)
hb.register(c)
@@ -1080,7 +1078,7 @@ func TestHubRelayChatBranch(t *testing.T) {
if err := hb.relay(context.Background(), bus.Event{Subject: wsChatSubject, Payload: map[string]any{"sessionId": "s1", "text": "hi"}}); err != nil {
t.Fatalf("relay: %v", err)
}
// relay runs streamChat in a goroutine; read with a deadline.
// relay 在 goroutine 中跑 streamChat;读取时带超时。
if ev := readFakeFrame(t, c); ev.Type != "stream_chunk" {
t.Fatalf("frame type = %q, want stream_chunk (frame=%+v)", ev.Type, ev)
}
@@ -1102,12 +1100,12 @@ func TestHubRoute(t *testing.T) {
c := newOwnerConn(hb, 32)
hb.register(c)
// subagent_ask → subagent_event (real audit data, not a hardcoded pending).
// subagent_ask → subagent_event(真实审计数据,不是硬编码的 pending)。
c.route(wsInbound{Type: "subagent_ask", Data: json.RawMessage(`{"id":"sub-1"}`)})
if ev := readFakeFrame(t, c); ev.Type != "subagent_event" {
t.Fatalf("subagent frame type = %q, want subagent_event", ev.Type)
}
// subagent_ask with no auditor → clear error, never a fabricated pending.
// 无 auditor 的 subagent_ask → 明确报错,绝不伪造 pending。
hb.auditor = nil
c.route(wsInbound{Type: "subagent_ask", Data: json.RawMessage(`{"id":"sub-1"}`)})
if ev := readFakeFrame(t, c); ev.Type != "error" {
@@ -1115,12 +1113,12 @@ func TestHubRoute(t *testing.T) {
}
hb.auditor = &stubAuditor{res: &orchestration.AuditResult{}}
// task_output_query → task_output with real output.
// task_output_query → 带真实输出的 task_output。
c.route(wsInbound{Type: "task_output_query", Data: json.RawMessage(`{"taskId":"task-1"}`)})
if ev := readFakeFrame(t, c); ev.Type != "task_output" {
t.Fatalf("task frame type = %q, want task_output", ev.Type)
}
// task_output_query with no task store → clear error, never an empty {}.
// 无 task store 的 task_output_query → 明确报错,绝不返回空的 {}。
hb.taskOutputs = nil
c.route(wsInbound{Type: "task_output_query", Data: json.RawMessage(`{"taskId":"task-1"}`)})
if ev := readFakeFrame(t, c); ev.Type != "error" {
@@ -1128,24 +1126,24 @@ func TestHubRoute(t *testing.T) {
}
hb.taskOutputs = &stubTaskOutputs{ev: task.OutputEvent{ID: "task-1", Status: task.StatusDone, Output: task.Output{Stdout: "out"}}}
// voice_input with no voice service → clear error, never a crash or a drop.
// 无语音服务的 voice_input → 明确报错,绝不崩溃也不丢弃。
c.route(wsInbound{Type: "voice_input", Data: json.RawMessage(`{"audio":"x"}`)})
if ev := readFakeFrame(t, c); ev.Type != "error" {
t.Fatalf("no-voice voice_input frame type = %q, want error", ev.Type)
}
// unknown → error (enqueueJSON).
// 未知类型 → 报错(enqueueJSON)。
c.route(wsInbound{Type: "nope"})
if ev := readFakeFrame(t, c); ev.Type != "error" {
t.Fatalf("unknown frame type = %q, want error", ev.Type)
}
// chat → publishes to bus (publishPublic non-nil path); no frame enqueued.
// chat → 发布到 bus(publishPublic 非 nil 路径);不入队任何帧。
c.route(wsInbound{Type: "chat", Data: json.RawMessage(`{"text":"hi"}`)})
if err := connHasNoFrame(c); err != nil {
t.Fatalf("chat should not enqueue a frame directly: %v", err)
}
}
// stubVoice is a deterministic VoiceService for voice route tests.
// stubVoice 是供语音路由测试使用的确定性 VoiceService。
type stubVoice struct {
sttText string
sttErr error
@@ -1164,9 +1162,9 @@ func TestHubVoiceInputTurn(t *testing.T) {
c := newOwnerConn(hb, 32)
hb.register(c)
// "aGVsbG8=" decodes to "hello" — a valid base64 audio payload.
// "aGVsbG8=" 解码为 "hello" —— 合法的 base64 音频载荷。
c.route(wsInbound{Type: "voice_input", Data: json.RawMessage(`{"audio":"aGVsbG8=","format":"wav"}`)})
// runVoiceTurn runs in a goroutine; read with a deadline.
// runVoiceTurn 在 goroutine 中运行;读取时带超时。
ev := readFakeFrame(t, c)
if ev.Type != "voice_output" {
t.Fatalf("voice frame type = %q, want voice_output (frame=%+v)", ev.Type, ev)
@@ -1218,7 +1216,7 @@ func TestHubBridgeSubagentProgress(t *testing.T) {
t.Fatalf("type = %q, want subagent_event (frame=%+v)", ev.Type, ev)
}
// An internal ask subject must never surface to clients.
// 内部的 ask 主题绝不可暴露给客户端。
if err := hb.relay(context.Background(), bus.Event{
Subject: "cyrene.agent.a.b.ask",
Payload: comm.Event{Name: orchestration.EventDone, Data: orchestration.Progress{AgentID: "sub-1"}},
@@ -1232,9 +1230,9 @@ func TestHubBridgeSubagentProgress(t *testing.T) {
func TestPublishPublicNilBus(t *testing.T) {
t.Parallel()
hb := newTestHub(nil, nil) // nil bus
hb := newTestHub(nil, nil) // bus 为 nil
c := newOwnerConn(hb, 32)
// Must not panic and must not enqueue anything.
// 不得 panic,也不得入队任何东西。
c.publishPublic(wsChatSubject, map[string]any{"text": "hi"})
}
@@ -1272,7 +1270,7 @@ func TestOriginCheckerSameHost(t *testing.T) {
allowed := map[string]bool{"http://ok.example": true}
checker := originChecker(allowed, false)
// Same host (exact) — production mode serving embedded frontend on the gateway port.
// 同主机(精确匹配)—— 生产模式下在网关端口提供内嵌前端。
req := httptest.NewRequest(http.MethodGet, "/ws/chat", nil)
req.Host = "localhost:10942"
req.Header.Set("Origin", "http://localhost:10942")
@@ -1280,7 +1278,7 @@ func TestOriginCheckerSameHost(t *testing.T) {
t.Fatal("same host origin should be allowed")
}
// Loopback alias: host is 127.0.0.1 but client origin uses localhost.
// 回环别名:host 是 127.0.0.1,但客户端 origin 用的是 localhost。
req2 := httptest.NewRequest(http.MethodGet, "/ws/chat", nil)
req2.Host = "127.0.0.1:10942"
req2.Header.Set("Origin", "http://localhost:10942")
@@ -1288,7 +1286,7 @@ func TestOriginCheckerSameHost(t *testing.T) {
t.Fatal("localhost<->127.0.0.1 loopback alias should be allowed")
}
// Different host must still be rejected (not a same-origin, not whitelisted).
// 不同主机仍必须被拒(既非同源,也不在白名单)。
req3 := httptest.NewRequest(http.MethodGet, "/ws/chat", nil)
req3.Host = "evil.example:10942"
req3.Header.Set("Origin", "http://localhost:10942")
@@ -1296,7 +1294,7 @@ func TestOriginCheckerSameHost(t *testing.T) {
t.Fatal("cross-host origin should be rejected")
}
// Same host, different port: still same-origin (same physical host), so allow.
// 同主机、不同端口:仍是同源(同一物理主机),因此放行。
req4 := httptest.NewRequest(http.MethodGet, "/ws/chat", nil)
req4.Host = "localhost:8080"
req4.Header.Set("Origin", "http://localhost:10942")
@@ -1320,7 +1318,7 @@ func TestWSTokenExtraction(t *testing.T) {
}
}
// --- Start / ListenAndServe lifecycle -------------------------------------
// --- Start / ListenAndServe 生命周期 ------------------------------------------
func TestStartAndShutdown(t *testing.T) {
t.Parallel()
@@ -1344,7 +1342,7 @@ func TestStartAndShutdown(t *testing.T) {
}
}
// --- Helpers ---------------------------------------------------------------
// --- 辅助函数 ----------------------------------------------------------------
func doHandler(t *testing.T, req *http.Request) *http.Response {
t.Helper()
@@ -1396,11 +1394,11 @@ func newFakeConn(hb *hub, buf int) *wsConn {
}
}
// newOwnerConn is a fake connection carrying owner claims: the design/11 §7.1 frame
// gate sends every content frame (message.*, thinking.*, tool_*, graph_update, mood,
// persisted-message frames, …) to owner/admin only, so a hub unit test that reads
// one has to connect as the role it is gated to. A nil-claims conn (newFakeConn) is
// the fail-closed bare connection — always reachable by the state signals only.
// newOwnerConn 是携带 owner claims 的假连接:design/11 §7.1 帧门把每个
// 内容帧(message.*、thinking.*、tool_*、graph_update、mood、持久化消息
// 帧,……)只发给 owner/admin,因此读取其中之一的 hub 单元测试必须以
// 被放行的角色连接。claims 为 nil 的连接(newFakeConn)是 fail-closed
// 的裸连接 —— 仅状态信号总能到达。
func newOwnerConn(hb *hub, buf int) *wsConn {
c := newFakeConn(hb, buf)
c.claims = &auth.Claims{Role: auth.RoleOwner}
@@ -1441,7 +1439,7 @@ func reqWithQuery(k, v string) *http.Request {
return httptest.NewRequest(http.MethodGet, "/ws/chat?"+k+"="+v, nil)
}
// --- RAG knowledge base ----------------------------------------------------
// --- RAG 知识库 --------------------------------------------------------------
func TestAdminRagListsAndSearches(t *testing.T) {
t.Parallel()
@@ -1451,7 +1449,7 @@ func TestAdminRagListsAndSearches(t *testing.T) {
_ = srv
tok := signAny(t, jwt, "acct", auth.RoleOwner)
// List only.
// 仅列表。
resp, body := doReq(t, http.MethodGet, ts.URL+"/api/v1/admin/rag", nil, tok)
if resp.StatusCode != http.StatusOK {
t.Fatalf("list status = %d, want 200 (body=%s)", resp.StatusCode, body)
@@ -1470,7 +1468,7 @@ func TestAdminRagListsAndSearches(t *testing.T) {
t.Fatalf("hits = %+v, want omitted without query", list.Hits)
}
// List + search via ?query=.
// 列表 + 经 ?query= 检索。
resp2, body2 := doReq(t, http.MethodGet, ts.URL+"/api/v1/admin/rag?query=rag", nil, tok)
if resp2.StatusCode != http.StatusOK {
t.Fatalf("search status = %d, want 200 (body=%s)", resp2.StatusCode, body2)
@@ -1516,7 +1514,7 @@ func TestAdminRagEmptyWhenUnwired(t *testing.T) {
_ = srv
tok := signAny(t, jwt, "acct", auth.RoleOwner)
// List degrades to empty (never a 500).
// 列表降级为空(绝不 500)。
resp, body := doReq(t, http.MethodGet, ts.URL+"/api/v1/admin/rag", nil, tok)
if resp.StatusCode != http.StatusOK {
t.Fatalf("list status = %d, want 200 (body=%s)", resp.StatusCode, body)
@@ -1531,7 +1529,7 @@ func TestAdminRagEmptyWhenUnwired(t *testing.T) {
t.Fatalf("documents = %+v, want empty", list.Documents)
}
// Query reports a clear 501 (backend not implemented).
// 检索明确返回 501(后端未实现)。
resp2, body2 := doReq(t, http.MethodPost, ts.URL+"/api/v1/admin/rag/query", map[string]any{"query": "rag"}, tok)
if resp2.StatusCode != http.StatusNotImplemented {
t.Fatalf("query status = %d, want 501 (body=%s)", resp2.StatusCode, body2)
@@ -1542,8 +1540,8 @@ func TestAdminRagEmptyWhenUnwired(t *testing.T) {
}
}
// TestAdminRagDeleteUnsupportedStore501 asserts a store that cannot delete is
// answered with an honest 501 rather than a fabricated success.
// TestAdminRagDeleteUnsupportedStore501 断言无法删除的存储会被诚实地以
// 501 应答,而不是伪造成功。
func TestAdminRagDeleteUnsupportedStore501(t *testing.T) {
t.Parallel()
srv, ts, jwt := buildEnv(t, cfgDefault(), Deps{RAG: &stubRag{err: knowledge.ErrWriteUnsupported}})