feat(capability): 转码能力包(vendor 纯 Go AMR-NB 解码器 + 黄金向量逐样本对照 + 8 哨兵 + 注册面)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

This commit is contained in:
AskaEth committed 2026-10-05 13:02:28 +08:00
1 parent 89715d1813
commit 4a2d276881
100 files changed
+11319

No files matched your search

+201
View File
@@ -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)
}