docs(gateway): WS 帧门相关 11 个文件的注释改中文(只改注释,代码零字节改动)
主人 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:
1 parent
770a83c3fb
commit
fe1ff33115
11 files changed
+392
-431
No files matched your search
@@ -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}})
|
||||
|
||||
Reference in new issue
Block a user