feat(capability): 转码能力包(vendor 纯 Go AMR-NB 解码器 + 黄金向量逐样本对照 + 8 哨兵 + 注册面)
This commit is contained in:
1 parent
89715d1813
commit
4a2d276881
100 files changed
+11319
No files matched your search
@@ -0,0 +1,201 @@
|
||||
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)
|
||||
}
|
||||
Reference in new issue
Block a user