Files
Cyrene_v2/internal/capability/transcode/tool_test.go
T
AskaEth 4a2d276881
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(capability): 转码能力包(vendor 纯 Go AMR-NB 解码器 + 黄金向量逐样本对照 + 8 哨兵 + 注册面)
2026-10-05 13:02:28 +08:00

202 lines
7.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}