package qq_obv11 // 本文件钉住 OneBot 信封里**自由文本面**的出镜边界(M50,与 httpclient 的信封 // message、Telegram 的 description、iLink 的 x-error-message 同一族): // // - 失败时上游的 `msg` / `wording` **不进错误文本**:NapCat / go-cqhttp 会把失败的 // 请求内容回显在 msg 里,而那些内容里就有她要发的话。留下的是 retcode 与 status。 // - status 是**协议字段**,但同样只按令牌出镜(trace.ErrorToken):上游若在 status // 里塞散文,那一段整体消失。 // - 回执失败的 WARN 日志(logEchoFailure)走同一道边界 —— 否则同一个洞只是换个出口。 // // 每条都是「把字段拼回去就变红」的形状,断言的是**错误文本与日志行**。 import ( "bytes" "context" "encoding/json" "log/slog" "net/http" "net/http/httptest" "strings" "testing" ) // newFailingOneBotAPI 起一个总是回「失败信封」的假 OneBot,body 由调用方给。 func newFailingOneBotAPI(t *testing.T, body map[string]any) *httptest.Server { t.Helper() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(body) })) t.Cleanup(srv.Close) return srv } // TestHTTPClientDropsUpstreamMsg 失败信封里的 msg / wording 带她的原话 ⇒ 错误与日志里 // 都搜不到,而 retcode 与 status 仍在(变异的两个方向各钉一条)。 func TestHTTPClientDropsUpstreamMsg(t *testing.T) { t.Parallel() const sentinel = "她刚才说的那句话" srv := newFailingOneBotAPI(t, map[string]any{ "status": "failed", "retcode": 1404, "msg": "SEND_MSG_API_ERROR: invalid content: " + sentinel, "wording": "参数错误:" + sentinel, "data": nil, }) c, err := newOBHTTPClient(srv.URL, "tok") if err != nil { t.Fatalf("newOBHTTPClient: %v", err) } payload, err := buildActionReq("send_msg", map[string]any{"message": sentinel}, "") if err != nil { t.Fatalf("buildActionReq: %v", err) } _, err = c.post(context.Background(), payload) if err == nil { t.Fatal("retcode=1404 却报成功") } var logBuf bytes.Buffer slog.New(slog.NewJSONHandler(&logBuf, nil)).Error("onebot action failed", "err", err) for _, surface := range []string{err.Error(), logBuf.String()} { if strings.Contains(surface, sentinel) { t.Fatalf("上游 msg / wording 进了错误或日志:%s", surface) } } if !strings.Contains(err.Error(), "retcode=1404") { t.Fatalf("retcode 丢了(唯一可靠的分类信息):%v", err) } if !strings.Contains(err.Error(), `status="failed"`) { t.Fatalf("status 丢了(它是协议字段,应当仍出镜):%v", err) } } // TestHTTPClientDropsProseStatus 上游若把散文塞进 status,令牌守卫让它整体消失 —— // status 不是第二条正文通道。 func TestHTTPClientDropsProseStatus(t *testing.T) { t.Parallel() srv := newFailingOneBotAPI(t, map[string]any{ "status": "failed: 她刚才说的那句话", "retcode": 1404, "data": nil, }) c, err := newOBHTTPClient(srv.URL, "tok") if err != nil { t.Fatalf("newOBHTTPClient: %v", err) } payload, err := buildActionReq("send_msg", map[string]any{"message": "hi"}, "") if err != nil { t.Fatalf("buildActionReq: %v", err) } _, err = c.post(context.Background(), payload) if err == nil { t.Fatal("retcode=1404 却报成功") } if strings.Contains(err.Error(), "她刚才说的那句话") || strings.Contains(err.Error(), "status=") { t.Fatalf("散文 status 出镜了(应被令牌守卫丢弃):%v", err) } if !strings.Contains(err.Error(), "retcode=1404") { t.Fatalf("retcode 丢了:%v", err) } } // TestLogEchoFailureDropsUpstreamMsg 回执失败的 WARN 是**另一个出口**:它同样只记 // retcode 与 status,不记上游 msg(同一个洞换个出口的形态)。 func TestLogEchoFailureDropsUpstreamMsg(t *testing.T) { t.Parallel() const sentinel = "她刚才说的那句话" var buf bytes.Buffer a := NewAdapter(Config{}, WithLogger(slog.New(slog.NewJSONHandler(&buf, nil)))) a.logEchoFailure(&obAPIResponse{ Status: "failed", RetCode: 1404, Msg: "SEND_MSG_API_ERROR: invalid content: " + sentinel, Wording: "参数错误:" + sentinel, }) line := buf.String() if line == "" { t.Fatal("回执失败一条日志都没有(静默即缺陷)") } if strings.Contains(line, sentinel) { t.Fatalf("上游 msg 进了日志:%s", line) } if !strings.Contains(line, `"retcode":1404`) || !strings.Contains(line, `"status":"failed"`) { t.Fatalf("日志里没有 retcode/status(诊断面):%s", line) } }