Files
nyoung ed51efd8bc fix: 网关与控制台代码审查修复批次(含回归测试)
- Chat 接口拒绝 n>1(此前静默丢弃多余 choice),返回 400 结构化错误
- Responses 接口透传 text.format(json_schema / json_object),三种接口协议对齐
- TLS 指纹统一升级到 Chrome_150(internal/httpx.DefaultProfile),与 UA 匹配
- metrics 落盘失败不再静默:Save() 返回错误并打日志,临时文件失败会清理
- GET /api/config 脱敏 admin.password 与 turnstile.api_key(控制台从不展示,
  不应随 5s 轮询下发);PUT 改走 ReplaceJSON,先校验后替换、失败回滚
- set_limits / set_turnstile 改指针部分更新:缺字段保持现状,不会把并发、
  限流或求解开关按零值清掉
- 读请求体统一 64MB 上限;配置写盘原子替换 + 0600 权限
- main.go 改为 run() 返回退出码:日志落盘、指标落盘、xray 子进程回收全部
  挂 defer,任何退出路径(含启动失败)都会执行,杜绝孤儿 xray
- proxypool / turnstile / upstream / openai / config / httpx / logbuf 等
  逐项补红绿回归测试,新增 14 个测试文件,race 下全绿
2026-10-08 15:13:23 +08:00

82 lines
3.1 KiB
Go

package openai
import (
"encoding/json"
"reflect"
"strings"
"testing"
)
// 回归:OpenAI 的 stop 允许单个字符串或字符串数组;字段类型是 []string 时
// 单字符串会让整个请求体解码失败(400),而这是合法的 OpenAI 请求。
func TestStopAcceptsSingleString(t *testing.T) {
var req ChatCompletionRequest
raw := `{"model":"m","messages":[{"role":"user","content":"hi"}],"stop":"END"}`
if err := json.Unmarshal([]byte(raw), &req); err != nil {
t.Fatalf("stop 写成字符串时解码失败(OpenAI 合法请求): %v", err)
}
if !reflect.DeepEqual([]string(req.Stop), []string{"END"}) {
t.Fatalf("stop = %#v, want [END]", req.Stop)
}
}
// 回归:数组写法必须照常工作。
func TestStopStillAcceptsArray(t *testing.T) {
var req ChatCompletionRequest
raw := `{"model":"m","messages":[],"stop":["A","B"]}`
if err := json.Unmarshal([]byte(raw), &req); err != nil {
t.Fatalf("数组写法解码失败: %v", err)
}
if !reflect.DeepEqual([]string(req.Stop), []string{"A", "B"}) {
t.Fatalf("stop = %#v, want [A B]", req.Stop)
}
}
// 回归:response_format.type = json_schema 时,schema 必须进入生成约束,
// 否则结构化输出没有任何约束,客户端拿到的对象不符合声明的 schema。
func TestPromptCarriesJSONSchema(t *testing.T) {
const schema = `{"type":"object","properties":{"title":{"type":"string"}},"required":["title"]}`
prompt := BuildPrompt([]ChatMessage{{Role: "user", Content: MessageContent{Text: "hi"}}},
PromptOptions{ResponseFormat: &ResponseFormat{Type: "json_schema", JSONSchema: json.RawMessage(schema)}})
if !strings.Contains(prompt, "json_schema") && !strings.Contains(prompt, "JSON Schema") {
t.Fatalf("提示词没有提到 schema:\n%s", prompt)
}
for _, want := range []string{"title", `"required"`} {
if !strings.Contains(prompt, want) {
t.Fatalf("提示词缺少 schema 内容 %q:\n%s", want, prompt)
}
}
}
// 回归:json_object 老行为不能回归。
func TestPromptStillHandlesJSONObject(t *testing.T) {
prompt := BuildPrompt([]ChatMessage{{Role: "user", Content: MessageContent{Text: "hi"}}},
PromptOptions{ResponseFormat: &ResponseFormat{Type: "json_object"}})
if !strings.Contains(prompt, "JSON object") {
t.Fatalf("json_object 提示缺失:\n%s", prompt)
}
}
// 回归:tool_calls 条目缺少/损坏的 function 时不能吞掉错误并产出一个
// name 为空的调用丢给客户端(客户端会在执行阶段才炸)。
func TestMalformedToolCallFunctionIsRejected(t *testing.T) {
_, calls, ok := TryParseToolCalls(`{"tool_calls":[{"id":"c1","type":"function"}]}`)
for _, c := range calls {
if c.Function.Name == "" {
t.Fatalf("解析出 name 为空的 tool call: %+v (ok=%v)", c, ok)
}
}
}
// 回归:正常的 tool_calls 解析不能受影响。
func TestValidToolCallStillParses(t *testing.T) {
_, calls, ok := TryParseToolCalls(`{"tool_calls":[{"id":"c1","type":"function","function":{"name":"get_weather","arguments":"{\"city\":\"SH\"}"}}]}`)
if !ok {
t.Fatalf("正常 tool_calls 没被识别")
}
if len(calls) != 1 || calls[0].Function.Name != "get_weather" {
t.Fatalf("calls = %+v", calls)
}
}