mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-06 09:25:27 +08:00
174 lines
5.3 KiB
Go
174 lines
5.3 KiB
Go
package compat
|
|
|
|
import (
|
|
"ds2api/internal/toolcall"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"ds2api/internal/sse"
|
|
"ds2api/internal/util"
|
|
)
|
|
|
|
func TestGoCompatSSEFixtures(t *testing.T) {
|
|
files, err := filepath.Glob(compatPath("fixtures", "sse_chunks", "*.json"))
|
|
if err != nil {
|
|
t.Fatalf("glob fixtures failed: %v", err)
|
|
}
|
|
if len(files) == 0 {
|
|
t.Fatal("no sse fixtures found")
|
|
}
|
|
for _, fixturePath := range files {
|
|
name := trimExt(filepath.Base(fixturePath))
|
|
expectedPath := compatPath("expected", "sse_"+name+".json")
|
|
|
|
var fixture struct {
|
|
Chunk map[string]any `json:"chunk"`
|
|
ThinkingEnable bool `json:"thinking_enabled"`
|
|
CurrentType string `json:"current_type"`
|
|
}
|
|
mustLoadJSON(t, fixturePath, &fixture)
|
|
|
|
var expected struct {
|
|
Parts []map[string]any `json:"parts"`
|
|
Finished bool `json:"finished"`
|
|
NewType string `json:"new_type"`
|
|
ContentFilter bool `json:"content_filter"`
|
|
OutputTokens int `json:"output_tokens"`
|
|
ErrorMessage string `json:"error_message"`
|
|
}
|
|
mustLoadJSON(t, expectedPath, &expected)
|
|
|
|
raw, err := json.Marshal(fixture.Chunk)
|
|
if err != nil {
|
|
t.Fatalf("marshal fixture %s failed: %v", name, err)
|
|
}
|
|
res := sse.ParseDeepSeekContentLine(append([]byte("data: "), raw...), fixture.ThinkingEnable, fixture.CurrentType)
|
|
gotParts := make([]map[string]any, 0, len(res.Parts))
|
|
for _, p := range res.Parts {
|
|
gotParts = append(gotParts, map[string]any{
|
|
"text": p.Text,
|
|
"type": p.Type,
|
|
})
|
|
}
|
|
if !reflect.DeepEqual(gotParts, expected.Parts) ||
|
|
res.Stop != expected.Finished ||
|
|
res.NextType != expected.NewType ||
|
|
res.ContentFilter != expected.ContentFilter ||
|
|
res.OutputTokens != expected.OutputTokens ||
|
|
res.ErrorMessage != expected.ErrorMessage {
|
|
t.Fatalf("fixture %s mismatch:\n got parts=%#v finished=%v newType=%q contentFilter=%v outputTokens=%d errorMessage=%q\nwant parts=%#v finished=%v newType=%q contentFilter=%v outputTokens=%d errorMessage=%q",
|
|
name, gotParts, res.Stop, res.NextType, res.ContentFilter, res.OutputTokens, res.ErrorMessage,
|
|
expected.Parts, expected.Finished, expected.NewType, expected.ContentFilter, expected.OutputTokens, expected.ErrorMessage)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGoCompatToolcallFixtures(t *testing.T) {
|
|
files, err := filepath.Glob(compatPath("fixtures", "toolcalls", "*.json"))
|
|
if err != nil {
|
|
t.Fatalf("glob toolcall fixtures failed: %v", err)
|
|
}
|
|
if len(files) == 0 {
|
|
t.Fatal("no toolcall fixtures found")
|
|
}
|
|
for _, fixturePath := range files {
|
|
name := trimExt(filepath.Base(fixturePath))
|
|
expectedPath := compatPath("expected", "toolcalls_"+name+".json")
|
|
|
|
var fixture struct {
|
|
Text string `json:"text"`
|
|
ToolNames []string `json:"tool_names"`
|
|
Mode string `json:"mode"`
|
|
}
|
|
mustLoadJSON(t, fixturePath, &fixture)
|
|
|
|
var expected struct {
|
|
Calls []toolcall.ParsedToolCall `json:"calls"`
|
|
SawToolCallSyntax bool `json:"sawToolCallSyntax"`
|
|
RejectedByPolicy bool `json:"rejectedByPolicy"`
|
|
RejectedToolNames []string `json:"rejectedToolNames"`
|
|
}
|
|
mustLoadJSON(t, expectedPath, &expected)
|
|
|
|
var got toolcall.ToolCallParseResult
|
|
switch strings.ToLower(strings.TrimSpace(fixture.Mode)) {
|
|
case "standalone":
|
|
got = toolcall.ParseStandaloneToolCallsDetailed(fixture.Text, fixture.ToolNames)
|
|
default:
|
|
got = toolcall.ParseToolCallsDetailed(fixture.Text, fixture.ToolNames)
|
|
}
|
|
if got.Calls == nil {
|
|
got.Calls = []toolcall.ParsedToolCall{}
|
|
}
|
|
if got.RejectedToolNames == nil {
|
|
got.RejectedToolNames = []string{}
|
|
}
|
|
if !reflect.DeepEqual(got.Calls, expected.Calls) ||
|
|
got.SawToolCallSyntax != expected.SawToolCallSyntax ||
|
|
got.RejectedByPolicy != expected.RejectedByPolicy ||
|
|
!reflect.DeepEqual(got.RejectedToolNames, expected.RejectedToolNames) {
|
|
t.Fatalf("toolcall fixture %s mismatch:\n got=%#v\nwant=%#v", name, got, expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGoCompatTokenFixtures(t *testing.T) {
|
|
var fixture struct {
|
|
Cases []struct {
|
|
Name string `json:"name"`
|
|
Text string `json:"text"`
|
|
} `json:"cases"`
|
|
}
|
|
mustLoadJSON(t, compatPath("fixtures", "token_cases.json"), &fixture)
|
|
|
|
var expected struct {
|
|
Cases []struct {
|
|
Name string `json:"name"`
|
|
Tokens int `json:"tokens"`
|
|
} `json:"cases"`
|
|
}
|
|
mustLoadJSON(t, compatPath("expected", "token_cases.json"), &expected)
|
|
|
|
expectByName := map[string]int{}
|
|
for _, c := range expected.Cases {
|
|
expectByName[c.Name] = c.Tokens
|
|
}
|
|
for _, c := range fixture.Cases {
|
|
want, ok := expectByName[c.Name]
|
|
if !ok {
|
|
t.Fatalf("missing expected token case: %s", c.Name)
|
|
}
|
|
got := util.EstimateTokens(c.Text)
|
|
if got != want {
|
|
t.Fatalf("token fixture %s mismatch: got=%d want=%d", c.Name, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func mustLoadJSON(t *testing.T, path string, out any) {
|
|
t.Helper()
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read %s failed: %v", path, err)
|
|
}
|
|
if err := json.Unmarshal(b, out); err != nil {
|
|
t.Fatalf("decode %s failed: %v", path, err)
|
|
}
|
|
}
|
|
|
|
func trimExt(name string) string {
|
|
if len(name) > 5 && name[len(name)-5:] == ".json" {
|
|
return name[:len(name)-5]
|
|
}
|
|
return name
|
|
}
|
|
|
|
func compatPath(parts ...string) string {
|
|
prefix := []string{"..", "..", "tests", "compat"}
|
|
return filepath.Join(append(prefix, parts...)...)
|
|
}
|