148 lines
5.0 KiB
Go
148 lines
5.0 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"oci-portal/internal/aiwire"
|
|
)
|
|
|
|
func respReq(t *testing.T, raw string) aiwire.RespRequest {
|
|
t.Helper()
|
|
var req aiwire.RespRequest
|
|
if err := json.Unmarshal([]byte(raw), &req); err != nil {
|
|
t.Fatalf("unmarshal req: %v", err)
|
|
}
|
|
return req
|
|
}
|
|
|
|
func deref(p *int) int {
|
|
if p == nil {
|
|
return 0
|
|
}
|
|
return *p
|
|
}
|
|
|
|
func TestRespServerTools(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
tools []aiwire.RespTool
|
|
want bool
|
|
}{
|
|
{"web_search", []aiwire.RespTool{{Type: "web_search"}}, true},
|
|
{"x_search混用", []aiwire.RespTool{{Type: "function", Name: "f"}, {Type: "x_search"}}, true},
|
|
{"仅function", []aiwire.RespTool{{Type: "function", Name: "f"}}, false},
|
|
{"空", nil, false},
|
|
{"其他类型", []aiwire.RespTool{{Type: "code_interpreter"}}, false},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if got := RespServerTools(test.tools); got != test.want {
|
|
t.Fatalf("got %v, want %v", got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRespPassthroughValidate(t *testing.T) {
|
|
prev := "resp_1"
|
|
bg := true
|
|
tests := []struct {
|
|
name string
|
|
req aiwire.RespRequest
|
|
wantErr bool
|
|
}{
|
|
{"合法", aiwire.RespRequest{Model: "m", Tools: []aiwire.RespTool{{Type: "web_search"}}}, false},
|
|
{"混用function", aiwire.RespRequest{Model: "m", Tools: []aiwire.RespTool{{Type: "web_search"}, {Type: "function", Name: "f"}}}, false},
|
|
{"缺model", aiwire.RespRequest{Tools: []aiwire.RespTool{{Type: "web_search"}}}, true},
|
|
{"工具加流式拒绝", aiwire.RespRequest{Model: "m", Stream: true, Tools: []aiwire.RespTool{{Type: "web_search"}}}, true},
|
|
{"无工具流式放行", aiwire.RespRequest{Model: "m", Stream: true}, false},
|
|
{"function工具流式放行", aiwire.RespRequest{Model: "m", Stream: true, Tools: []aiwire.RespTool{{Type: "function", Name: "f"}}}, false},
|
|
{"有状态拒绝", aiwire.RespRequest{Model: "m", PreviousResponseID: prev}, true},
|
|
{"background拒绝", aiwire.RespRequest{Model: "m", Background: &bg}, true},
|
|
{"未知工具拒绝", aiwire.RespRequest{Model: "m", Tools: []aiwire.RespTool{{Type: "web_search"}, {Type: "mcp"}}}, true},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
err := RespPassthroughValidate(test.req)
|
|
if (err != nil) != test.wantErr {
|
|
t.Fatalf("err = %v, wantErr %v", err, test.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRespPassthroughBody(t *testing.T) {
|
|
raw := []byte(`{"model":"m","input":"hi","stream":true,"store":true,"max_output_tokens":128,"custom_field":{"a":1.5}}`)
|
|
out, err := RespPassthroughBody(raw)
|
|
if err != nil {
|
|
t.Fatalf("RespPassthroughBody: %v", err)
|
|
}
|
|
var body map[string]any
|
|
if err := json.Unmarshal(out, &body); err != nil {
|
|
t.Fatalf("unmarshal out: %v", err)
|
|
}
|
|
if body["store"] != false {
|
|
t.Errorf("store 应强制 false, got %v", body["store"])
|
|
}
|
|
if body["stream"] != true {
|
|
t.Errorf("stream 应原样保留, got %v", body["stream"])
|
|
}
|
|
if string(out) == "" || !strings.Contains(string(out), `"max_output_tokens":128`) {
|
|
t.Errorf("数值字段应保真: %s", out)
|
|
}
|
|
if !strings.Contains(string(out), `"custom_field"`) {
|
|
t.Errorf("未知字段应保留: %s", out)
|
|
}
|
|
if _, err := RespPassthroughBody([]byte("not-json")); err == nil {
|
|
t.Error("非法 JSON 应报错")
|
|
}
|
|
}
|
|
|
|
func TestRespPassthroughUsage(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
payload string
|
|
wantNil bool
|
|
prompt, cached int
|
|
}{
|
|
{"完整", `{"usage":{"input_tokens":9,"output_tokens":3,"total_tokens":12,"input_tokens_details":{"cached_tokens":5}}}`, false, 9, 5},
|
|
{"无细分", `{"usage":{"input_tokens":9,"output_tokens":3,"total_tokens":12}}`, false, 9, 0},
|
|
{"无usage", `{"id":"resp_1"}`, true, 0, 0},
|
|
{"非法JSON", `xx`, true, 0, 0},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
usage := RespPassthroughUsage([]byte(test.payload))
|
|
if (usage == nil) != test.wantNil {
|
|
t.Fatalf("usage = %v, wantNil %v", usage, test.wantNil)
|
|
}
|
|
if usage == nil {
|
|
return
|
|
}
|
|
if usage.PromptTokens != test.prompt || usage.CachedTokens() != test.cached {
|
|
t.Fatalf("prompt=%d cached=%d, want %d/%d", usage.PromptTokens, usage.CachedTokens(), test.prompt, test.cached)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestRespStreamCompletedUsage 断言流式 usage 只从 completed 事件提取。
|
|
func TestRespStreamCompletedUsage(t *testing.T) {
|
|
completed := []byte(`{"type":"response.completed","response":{"usage":{"input_tokens":10,"output_tokens":5,"total_tokens":15,"input_tokens_details":{"cached_tokens":4}}}}`)
|
|
if u := RespStreamCompletedUsage(completed); u == nil || u.PromptTokens != 10 || u.CompletionTokens != 5 ||
|
|
u.PromptTokensDetails == nil || u.PromptTokensDetails.CachedTokens != 4 {
|
|
t.Fatalf("completed 事件 usage 解析失败: %+v", u)
|
|
}
|
|
for _, data := range []string{
|
|
`{"type":"response.output_text.delta","delta":"hi"}`,
|
|
`{"type":"response.completed"}`,
|
|
`not-json`,
|
|
} {
|
|
if u := RespStreamCompletedUsage([]byte(data)); u != nil {
|
|
t.Fatalf("非 completed 或缺 response 应返回 nil: %s", data)
|
|
}
|
|
}
|
|
}
|