202 lines
7.0 KiB
Go
202 lines
7.0 KiB
Go
package transcode
|
||
|
||
import (
|
||
"context"
|
||
"encoding/base64"
|
||
"errors"
|
||
"testing"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
|
||
)
|
||
|
||
// stubTranscoder 是工具面测试用的最小 Transcoder(只为验证包装层,不碰真实音频)。
|
||
type stubTranscoder struct {
|
||
supports bool
|
||
res Result
|
||
err error
|
||
// lastReq 记录最后一次透传的请求(验证 source/target 归一化)。
|
||
lastReq Request
|
||
}
|
||
|
||
func (s *stubTranscoder) Transcode(_ context.Context, req Request) (Result, error) {
|
||
s.lastReq = req
|
||
if s.err != nil {
|
||
return Result{}, s.err
|
||
}
|
||
return s.res, nil
|
||
}
|
||
|
||
func (s *stubTranscoder) Supports(_, _ Format) bool { return s.supports }
|
||
func (s *stubTranscoder) Formats() []Format { return []Format{FormatAMR, FormatWAV} }
|
||
|
||
// TestToolParameters 断言入参 schema 的形状(对象、必填 data_base64/target)。
|
||
func TestToolParameters(t *testing.T) {
|
||
tool := NewTool(&stubTranscoder{})
|
||
if tool.Name() != ToolName {
|
||
t.Fatalf("Name = %q, want %q", tool.Name(), ToolName)
|
||
}
|
||
if tool.Description() == "" {
|
||
t.Fatalf("Description must not be empty")
|
||
}
|
||
schema, err := tool.Parameters()
|
||
if err != nil {
|
||
t.Fatalf("Parameters: %v", err)
|
||
}
|
||
if schema["type"] != "object" {
|
||
t.Fatalf("schema type = %v, want object", schema["type"])
|
||
}
|
||
required, ok := schema["required"].([]any)
|
||
if !ok || len(required) != 2 {
|
||
t.Fatalf("required = %v, want [data_base64 target]", schema["required"])
|
||
}
|
||
props, ok := schema["properties"].(map[string]any)
|
||
if !ok {
|
||
t.Fatalf("properties missing")
|
||
}
|
||
for _, key := range []string{toolArgData, toolArgTarget, toolArgSource} {
|
||
if _, ok := props[key]; !ok {
|
||
t.Fatalf("properties missing %q", key)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestToolExecuteDecodesArgs 断言入参归一化与结果字段。
|
||
func TestToolExecuteDecodesArgs(t *testing.T) {
|
||
stub := &stubTranscoder{supports: true, res: Result{
|
||
Data: []byte("wav-bytes"), Format: FormatWAV, Source: FormatAMR,
|
||
SampleRate: amrSampleRate, Channels: amrChannels, DurationMs: 40, Frames: 2,
|
||
}}
|
||
out, err := NewTool(stub).Execute(context.Background(), map[string]any{
|
||
toolArgData: base64.StdEncoding.EncodeToString([]byte("amr-bytes")),
|
||
toolArgTarget: ".WAV",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("Execute: %v", err)
|
||
}
|
||
if string(stub.lastReq.Data) != "amr-bytes" || stub.lastReq.Target != FormatWAV {
|
||
t.Fatalf("request = %+v, want the decoded bytes and normalized target", stub.lastReq)
|
||
}
|
||
if stub.lastReq.Source != "" {
|
||
t.Fatalf("source = %q, want empty(留空即嗅探)", stub.lastReq.Source)
|
||
}
|
||
res, ok := out.(map[string]any)
|
||
if !ok {
|
||
t.Fatalf("result type = %T, want map[string]any", out)
|
||
}
|
||
encoded, _ := res["data_base64"].(string)
|
||
if decoded, err := base64.StdEncoding.DecodeString(encoded); err != nil || string(decoded) != "wav-bytes" {
|
||
t.Fatalf("data_base64 round trip failed: %q (%v)", encoded, err)
|
||
}
|
||
if res["format"] != "wav" || res["source"] != "amr" || res["frames"] != 2 || res["duration_ms"] != int64(40) {
|
||
t.Fatalf("result fields = %v", res)
|
||
}
|
||
}
|
||
|
||
// TestToolRejectsBadArgs 断言入参不合法一律 ErrBadArgs,绝不猜测。
|
||
func TestToolRejectsBadArgs(t *testing.T) {
|
||
tool := NewTool(&stubTranscoder{supports: true})
|
||
cases := []struct {
|
||
name string
|
||
args any
|
||
}{
|
||
{"not an object", "data"},
|
||
{"nil args", nil},
|
||
{"missing data", map[string]any{toolArgTarget: "wav"}},
|
||
{"missing target", map[string]any{toolArgData: base64.StdEncoding.EncodeToString([]byte("x"))}},
|
||
{"bad base64", map[string]any{toolArgData: "!!!not base64!!!", toolArgTarget: "wav"}},
|
||
{"empty base64 payload", map[string]any{toolArgData: "", toolArgTarget: "wav"}},
|
||
{"wrong type", map[string]any{toolArgData: 42, toolArgTarget: "wav"}},
|
||
}
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
if _, err := tool.Execute(context.Background(), tc.args); !errors.Is(err, ErrBadArgs) {
|
||
t.Fatalf("err = %v, want ErrBadArgs", err)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestToolPropagatesSentinels 断言工具面不吞哨兵(%w 链保持)。
|
||
func TestToolPropagatesSentinels(t *testing.T) {
|
||
stub := &stubTranscoder{err: ErrUnsupportedFormat}
|
||
_, err := NewTool(stub).Execute(context.Background(), map[string]any{
|
||
toolArgData: base64.StdEncoding.EncodeToString([]byte("x")),
|
||
toolArgTarget: "wav",
|
||
})
|
||
if !errors.Is(err, ErrUnsupportedFormat) {
|
||
t.Fatalf("err = %v, want ErrUnsupportedFormat", err)
|
||
}
|
||
}
|
||
|
||
// TestToolResultCap 断言工具结果体量有上限(产物是字节,不进上下文的红线:
|
||
// 要转大文件请走进程内 DI,不要走模型工具面)。
|
||
func TestToolResultCap(t *testing.T) {
|
||
big := make([]byte, MaxToolResultBytes+1)
|
||
_, err := NewTool(&stubTranscoder{res: Result{Data: big}}).Execute(context.Background(), map[string]any{
|
||
toolArgData: base64.StdEncoding.EncodeToString([]byte("x")),
|
||
toolArgTarget: "wav",
|
||
})
|
||
if !errors.Is(err, ErrOutputTooLarge) {
|
||
t.Fatalf("err = %v, want ErrOutputTooLarge", err)
|
||
}
|
||
}
|
||
|
||
// TestToolNilTranscoderFailsLoudly 断言没接转码器时不假装成功。
|
||
func TestToolNilTranscoderFailsLoudly(t *testing.T) {
|
||
_, err := NewTool(nil).Execute(context.Background(), map[string]any{
|
||
toolArgData: base64.StdEncoding.EncodeToString([]byte("x")),
|
||
toolArgTarget: "wav",
|
||
})
|
||
if err == nil {
|
||
t.Fatalf("nil transcoder must fail")
|
||
}
|
||
}
|
||
|
||
// TestRegisterToolIntoGlobalRegistry 是「两层注册」第二层的落点验收:走**既有**
|
||
// 全局能力注册表,进去的是 KindTool、策略是零值(fail-closed = owner-only),
|
||
// **不新增 Kind**(validKind 闭集不动)。
|
||
func TestRegisterToolIntoGlobalRegistry(t *testing.T) {
|
||
caps := registry.New()
|
||
if err := RegisterTool(caps, NewEngine(DefaultLimits(), WithCodecs(DefaultCodecs()...))); err != nil {
|
||
t.Fatalf("RegisterTool: %v", err)
|
||
}
|
||
cap, err := caps.Get(ToolName)
|
||
if err != nil {
|
||
t.Fatalf("Get(%q): %v", ToolName, err)
|
||
}
|
||
if cap.Kind != registry.KindTool {
|
||
t.Fatalf("kind = %q, want %q(不为转码新增 Kind)", cap.Kind, registry.KindTool)
|
||
}
|
||
if cap.Policy.AllowInGroup || cap.Policy.AllowInStranger {
|
||
t.Fatalf("policy = %+v, want zero value (owner-only, fail-closed)", cap.Policy)
|
||
}
|
||
if _, ok := cap.Value.(*Tool); !ok {
|
||
t.Fatalf("value type = %T, want *Tool", cap.Value)
|
||
}
|
||
if len(caps.List(registry.KindTool)) != 1 {
|
||
t.Fatalf("tool listing did not include the transcode tool")
|
||
}
|
||
}
|
||
|
||
// TestRegisterToolNilArgs 断言注册入口的 nil 纪律(与 vision.RegisterTools 同族)。
|
||
func TestRegisterToolNilArgs(t *testing.T) {
|
||
if err := RegisterTool(nil, &stubTranscoder{}); !errors.Is(err, sdk.ErrNilRegistrar) {
|
||
t.Fatalf("nil registrar err = %v, want sdk.ErrNilRegistrar", err)
|
||
}
|
||
caps := registry.New()
|
||
if err := RegisterTool(caps, nil); err != nil {
|
||
t.Fatalf("nil transcoder must be a no-op, got %v", err)
|
||
}
|
||
if len(caps.List(registry.KindTool)) != 0 {
|
||
t.Fatalf("nil transcoder registered a capability")
|
||
}
|
||
}
|
||
|
||
// TestStubTranscoderSatisfiesInterface 断言测试桩确实实现了接口(防止接口漂移
|
||
// 时测试静默失去意义)。
|
||
func TestStubTranscoderSatisfiesInterface(t *testing.T) {
|
||
var _ Transcoder = (*stubTranscoder)(nil)
|
||
var _ sdk.Tool = (*Tool)(nil)
|
||
}
|