package wire import ( "errors" "strings" "testing" "git.flytoex.net/yuanwei/flyto-agent/core/pkg/flyto" ) // TestValidateToolNames_EmptyRegex_SkipsCheck verifies zero-regression: // regex=="" means unknown/unenforced, ValidateToolNames returns nil // regardless of tool name shape (preserves pre-ADR-0007 behavior). // // 覆盖零回归: regex 空时 ValidateToolNames 跳过校验保旧行为不破现有 // caller. func TestValidateToolNames_EmptyRegex_SkipsCheck(t *testing.T) { tools := []flyto.Tool{{Name: "billcost.reflect"}, {Name: "weird.name"}} if err := ValidateToolNames(tools, ""); err != nil { t.Errorf("empty regex should skip check, got %v", err) } } // TestValidateToolNames_OpenAIRegex_RejectsDot covers ADR-0007 Bug U // real fixture: tool name "billcost.reflect" violates OpenAI regex // ^[a-zA-Z0-9_-]+$ (dot is not in char class) -> typed // ErrModelToolUnsupported with regex pattern in Detail. // // 覆盖 ADR-0007 Bug U 真实 fixture: "billcost.reflect" 含 dot 违反 // OpenAI regex -> ErrModelToolUnsupported typed error. func TestValidateToolNames_OpenAIRegex_RejectsDot(t *testing.T) { tools := []flyto.Tool{{Name: "billcost.reflect"}} err := ValidateToolNames(tools, `^[a-zA-Z0-9_-]+$`) if err == nil { t.Fatal("expected error for dot-containing tool name") } var engErr *flyto.EngineError if !errors.As(err, &engErr) { t.Fatalf("expected *flyto.EngineError, got %T", err) } if engErr.Code != flyto.ErrModelToolUnsupported { t.Errorf("Code = %q, want %q", engErr.Code, flyto.ErrModelToolUnsupported) } if !strings.Contains(engErr.Detail, "billcost.reflect") { t.Errorf("Detail = %q, want to contain tool name", engErr.Detail) } } // TestValidateToolNames_AcceptsValidNames covers underscore/hyphen // passing the regex (post-Bug-U fix uses billcost_reflect). // // 覆盖合法名 (下划线/连字符) 通过 regex 校验. func TestValidateToolNames_AcceptsValidNames(t *testing.T) { tools := []flyto.Tool{ {Name: "billcost_reflect"}, {Name: "do-something"}, {Name: "Read"}, } if err := ValidateToolNames(tools, `^[a-zA-Z0-9_-]+$`); err != nil { t.Errorf("valid names should pass, got %v", err) } } // TestValidateToolNames_BadRegex_SilentSkip verifies defensive // fallback: malformed regex from ModelInfo (consumer config bug) // must not block business -- return nil and let provider 4xx surface // via ADR-0006 typed error path. // // 覆盖防御性回退: regex 编译失败时 silent skip 让 provider 4xx 自然 // 冒泡. func TestValidateToolNames_BadRegex_SilentSkip(t *testing.T) { tools := []flyto.Tool{{Name: "billcost.reflect"}} // "[" 是 unmatched bracket, regex.Compile 报错. if err := ValidateToolNames(tools, `[`); err != nil { t.Errorf("malformed regex should silent skip, got %v", err) } } // TestValidateToolNames_EmptyToolName_Skipped verifies empty tool // names are not checked (defensive: avoid spurious match failures // against a regex requiring 1+ chars). // // 覆盖空 tool 名跳过校验 (防御性: 避免对 1+ 字符 regex 的虚假失败). func TestValidateToolNames_EmptyToolName_Skipped(t *testing.T) { tools := []flyto.Tool{{Name: ""}, {Name: "Read"}} if err := ValidateToolNames(tools, `^[a-zA-Z]+$`); err != nil { t.Errorf("empty tool name should be skipped, got %v", err) } } // makeTools 构造指定数量的占位工具(测试辅助). func makeTools(n int) []flyto.Tool { tools := make([]flyto.Tool, n) for i := range tools { tools[i] = flyto.Tool{Name: "tool", Description: "test"} } return tools } // TestCheckToolCount_Unlimited 验证 max=0 时(未知/无限制)始终返回 nil. func TestCheckToolCount_Unlimited(t *testing.T) { if err := CheckToolCount(makeTools(9999), 0); err != nil { t.Errorf("max=0 should be unlimited, got error: %v", err) } if err := CheckToolCount(makeTools(0), 0); err != nil { t.Errorf("max=0 empty tools should be nil, got: %v", err) } // 负数 max 同样视为无限制 if err := CheckToolCount(makeTools(256), -1); err != nil { t.Errorf("max=-1 should be unlimited, got error: %v", err) } } // TestCheckToolCount_WithinLimit 验证工具数量 ≤ max 时返回 nil. func TestCheckToolCount_WithinLimit(t *testing.T) { cases := []struct { count int max int }{ {0, 128}, {1, 128}, {5, 128}, {128, 128}, {0, 20}, {20, 20}, } for _, c := range cases { err := CheckToolCount(makeTools(c.count), c.max) if err != nil { t.Errorf("count=%d max=%d: expected nil, got %v", c.count, c.max, err) } } } // TestCheckToolCount_ExceedsLimit 验证工具数量 > max 时返回包含数量的错误. func TestCheckToolCount_ExceedsLimit(t *testing.T) { cases := []struct { count int max int }{ {129, 128}, // OpenAI limit {21, 20}, // Anthropic strict limit {1, 0}, // max=0 → unlimited,不应触发(由 Unlimited 测试覆盖) } // 只测试真正应报错的 cases(max>0 且 count>max) for _, c := range cases { if c.max <= 0 { continue } err := CheckToolCount(makeTools(c.count), c.max) if err == nil { t.Errorf("count=%d max=%d: expected error, got nil", c.count, c.max) continue } // 错误信息应包含实际数量和上限 if !strings.Contains(err.Error(), "129") && c.count == 129 { // pass: just check non-nil above } _ = err // 错误非 nil 即符合预期 } } // TestCheckToolCount_ErrorMessage 验证错误消息格式包含实际数量和上限. func TestCheckToolCount_ErrorMessage(t *testing.T) { err := CheckToolCount(makeTools(129), 128) if err == nil { t.Fatal("expected error") } msg := err.Error() if !strings.Contains(msg, "129") { t.Errorf("error message should contain actual count 129: %q", msg) } if !strings.Contains(msg, "128") { t.Errorf("error message should contain limit 128: %q", msg) } err2 := CheckToolCount(makeTools(21), 20) if err2 == nil { t.Fatal("expected error for count=21 max=20") } msg2 := err2.Error() if !strings.Contains(msg2, "21") { t.Errorf("error message should contain actual count 21: %q", msg2) } if !strings.Contains(msg2, "20") { t.Errorf("error message should contain limit 20: %q", msg2) } }