Services can resolve legacy aliases and expose the catalog over CLI and HTTP instead of ad-hoc error strings. Co-authored-by: Cursor <cursoragent@cursor.com>
178 lines
4.7 KiB
Go
178 lines
4.7 KiB
Go
package errcode
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
)
|
|
|
|
func sampleDef(code Code) Definition {
|
|
return Definition{
|
|
Code: code,
|
|
HTTP: 503,
|
|
Severity: SeverityError,
|
|
Retry: RetryTransient,
|
|
SideEffects: SideEffectNone,
|
|
Summary: "提供方不可用",
|
|
Hint: "退避后重试",
|
|
Aliases: []string{"provider_unavailable"},
|
|
}
|
|
}
|
|
|
|
func TestCodeFormat(t *testing.T) {
|
|
ok := []Code{SysInternal, AgentProviderUnavailable, SquadRoleNotFound, "custom-domain.snake_name"}
|
|
for _, c := range ok {
|
|
if !c.Valid() {
|
|
t.Fatalf("%s should be valid", c)
|
|
}
|
|
if c.Domain() == "" || c.Mnemonic() == "" {
|
|
t.Fatalf("%s domain/mnemonic empty", c)
|
|
}
|
|
}
|
|
bad := []Code{"", "NoDot", "A.b", "sys.", ".internal", "sys.Internal", "1sys.internal", "sys.a"}
|
|
for _, c := range bad {
|
|
if c.Valid() {
|
|
t.Fatalf("%s should be invalid", c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRegisterLookupAliasResolve(t *testing.T) {
|
|
r := NewRegistry()
|
|
def := sampleDef(AgentProviderUnavailable)
|
|
if err := r.Register(def); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := r.Register(def); err == nil {
|
|
t.Fatal("duplicate register should fail")
|
|
}
|
|
got, ok := r.Lookup("provider_unavailable")
|
|
if !ok || got.Code != AgentProviderUnavailable {
|
|
t.Fatalf("alias lookup: %+v ok=%v", got, ok)
|
|
}
|
|
if err := r.Alias("TRANSIENT", AgentProviderUnavailable); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := r.Lookup("TRANSIENT"); !ok {
|
|
t.Fatal("added alias missing")
|
|
}
|
|
unknown := r.Resolve("not.a.real")
|
|
if unknown.Code != SysUnknownCode {
|
|
t.Fatalf("resolve fallback = %s", unknown.Code)
|
|
}
|
|
if r.Known("not.a.real") {
|
|
t.Fatal("unknown should not be known")
|
|
}
|
|
if !r.Known(string(AgentProviderUnavailable)) {
|
|
t.Fatal("canonical should be known")
|
|
}
|
|
}
|
|
|
|
func TestRegisterRejectsBadAliasAndRollsBack(t *testing.T) {
|
|
r := NewRegistry()
|
|
def := sampleDef(AgentTimeout)
|
|
def.Aliases = []string{"ok_alias", "bad alias"}
|
|
if err := r.Register(def); err == nil {
|
|
t.Fatal("expected invalid alias")
|
|
}
|
|
if r.Known(string(AgentTimeout)) || r.Known("ok_alias") {
|
|
t.Fatal("failed register must not leave partial state")
|
|
}
|
|
}
|
|
|
|
func TestLoadJSONRollback(t *testing.T) {
|
|
r := NewRegistry()
|
|
raw := []byte(`[
|
|
{"code":"demo.one","http":400,"severity":"warn","retry":"never","side_effects":"none","summary":"一","hint":"检查"},
|
|
{"code":"demo.one","http":400,"severity":"warn","retry":"never","side_effects":"none","summary":"重复","hint":"检查"}
|
|
]`)
|
|
if err := r.LoadJSON(raw); err == nil {
|
|
t.Fatal("expected duplicate failure")
|
|
}
|
|
if len(r.List("")) != 0 {
|
|
t.Fatal("batch should roll back")
|
|
}
|
|
wrapped := []byte(`{"codes":[{"code":"demo.two","http":404,"severity":"info","retry":"never","side_effects":"none","summary":"没有","hint":"确认标识"}]}`)
|
|
if err := r.LoadJSON(wrapped); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !r.Known("demo.two") {
|
|
t.Fatal("wrapped catalog not loaded")
|
|
}
|
|
}
|
|
|
|
func TestSearchAndExport(t *testing.T) {
|
|
r := NewRegistry()
|
|
if err := r.Register(sampleDef(AgentProviderUnavailable)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
found := r.Search("不可用")
|
|
if len(found) != 1 {
|
|
t.Fatalf("search %d", len(found))
|
|
}
|
|
data, err := r.ExportJSON()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var defs []Definition
|
|
if err := json.Unmarshal(data, &defs); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(defs) != 1 || defs[0].Code != AgentProviderUnavailable {
|
|
t.Fatalf("export %+v", defs)
|
|
}
|
|
}
|
|
|
|
func TestErrorWrapIsAsJSON(t *testing.T) {
|
|
root := fmt.Errorf("connection reset")
|
|
err := Wrap(root, AgentProviderUnavailable, "模型不可达", "model", "glm-4")
|
|
if !errors.Is(err, New(AgentProviderUnavailable, "")) {
|
|
t.Fatal("errors.Is should match code")
|
|
}
|
|
if !errors.Is(err, root) {
|
|
t.Fatal("errors.Is should see cause")
|
|
}
|
|
var got *E
|
|
if !errors.As(err, &got) {
|
|
t.Fatal("errors.As")
|
|
}
|
|
if From(fmt.Errorf("wrap: %w", err)).Code != AgentProviderUnavailable {
|
|
t.Fatal("From should walk the chain")
|
|
}
|
|
if err.ErrorCode() != string(AgentProviderUnavailable) {
|
|
t.Fatal(err.ErrorCode())
|
|
}
|
|
fields := err.ErrorFields()
|
|
if fields["model"] != "glm-4" || fields["reason"] != "模型不可达" {
|
|
t.Fatalf("fields %+v", fields)
|
|
}
|
|
fields["model"] = "mutated"
|
|
if err.ErrorFields()["model"] != "glm-4" {
|
|
t.Fatal("ErrorFields must return a copy")
|
|
}
|
|
if Wrap(nil, SysInternal, "x") != nil {
|
|
t.Fatal("Wrap(nil) must be nil")
|
|
}
|
|
data, errJSON := json.Marshal(err)
|
|
if errJSON != nil {
|
|
t.Fatal(errJSON)
|
|
}
|
|
var back E
|
|
if err := json.Unmarshal(data, &back); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if back.Code != AgentProviderUnavailable || back.Unwrap() == nil || back.Unwrap().Error() != root.Error() {
|
|
t.Fatalf("roundtrip %+v cause=%v", back, back.Unwrap())
|
|
}
|
|
}
|
|
|
|
func TestPanicOnOddFields(t *testing.T) {
|
|
defer func() {
|
|
if recover() == nil {
|
|
t.Fatal("expected panic")
|
|
}
|
|
}()
|
|
New(SysInternal, "x", "only-key")
|
|
}
|