package builtin import ( "context" "encoding/json" "strings" "testing" "git.flytoex.net/yuanwei/flyto-agent/pkg/tools" ) // mockSkillExecutor 用于测试的 mock 执行器 type mockSkillExecutor struct { invokeResult *SkillResult invokeErr error entries []*SkillEntryDesc lastName string lastArgs string } func (m *mockSkillExecutor) InvokeSkill(_ context.Context, name, args string) (*SkillResult, error) { m.lastName = name m.lastArgs = args return m.invokeResult, m.invokeErr } func (m *mockSkillExecutor) ListSkillEntries() []*SkillEntryDesc { return m.entries } // ---- SkillTool 基本属性 ---- func TestSkillTool_Name(t *testing.T) { st := NewSkillTool() if st.Name() != "Skill" { t.Errorf("Name: got %q, want %q", st.Name(), "Skill") } } func TestSkillTool_InputSchema_Valid(t *testing.T) { st := NewSkillTool() schema := st.InputSchema() var parsed map[string]any if err := json.Unmarshal(schema, &parsed); err != nil { t.Fatalf("InputSchema is not valid JSON: %v", err) } // skill 字段必须存在 props, ok := parsed["properties"].(map[string]any) if !ok { t.Fatal("no properties in schema") } if _, ok := props["skill"]; !ok { t.Error("skill property missing in schema") } } func TestSkillTool_Metadata(t *testing.T) { st := NewSkillTool() meta := st.Metadata() if meta.ConcurrencySafe { t.Error("Skill tool should not be concurrency safe") } } // ---- Description ---- func TestSkillTool_Description_NoExecutor(t *testing.T) { st := NewSkillTool() desc := st.Description(context.Background()) if desc == "" { t.Error("description should not be empty even without executor") } } func TestSkillTool_Description_WithEntries(t *testing.T) { mock := &mockSkillExecutor{ entries: []*SkillEntryDesc{ {Name: "commit", Description: "Create a git commit"}, {Name: "review", Description: "Review code changes", ArgumentHint: ""}, }, } st := NewSkillTool() st.SetExecutor(mock) desc := st.Description(context.Background()) if !strings.Contains(desc, "commit") { t.Error("description should contain 'commit'") } if !strings.Contains(desc, "review") { t.Error("description should contain 'review'") } if !strings.Contains(desc, "") { t.Error("description should contain argument hint") } } func TestSkillTool_Description_Truncation(t *testing.T) { // 生成超多 Skill,验证截断 entries := make([]*SkillEntryDesc, 100) for i := range entries { entries[i] = &SkillEntryDesc{ Name: "skill", Description: strings.Repeat("x", 500), } } mock := &mockSkillExecutor{entries: entries} st := NewSkillTool() st.SetExecutor(mock) desc := st.Description(context.Background()) if len(desc) > 9000 { t.Errorf("description too long: %d chars", len(desc)) } } // ---- Execute ---- func TestSkillTool_Execute_MissingSkillName(t *testing.T) { st := NewSkillTool() result, err := st.Execute(context.Background(), json.RawMessage(`{}`), nil) if err != nil { t.Fatalf("Execute: %v", err) } if !result.IsError { t.Error("expected error result for missing skill name") } } func TestSkillTool_Execute_InvalidJSON(t *testing.T) { st := NewSkillTool() result, err := st.Execute(context.Background(), json.RawMessage(`invalid`), nil) if err != nil { t.Fatalf("Execute: %v", err) } if !result.IsError { t.Error("expected error result for invalid JSON") } } func TestSkillTool_Execute_NoExecutor(t *testing.T) { st := NewSkillTool() input, _ := json.Marshal(skillInput{Skill: "commit"}) result, err := st.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("Execute: %v", err) } if result.IsError { t.Error("expected graceful degradation, not error") } if !strings.Contains(result.Output, "executor not configured") { t.Errorf("expected executor not configured message, got: %q", result.Output) } } func TestSkillTool_Execute_InlineMode(t *testing.T) { mock := &mockSkillExecutor{ invokeResult: &SkillResult{ Mode: "inline", Content: "Expanded skill prompt here", }, } st := NewSkillTool() st.SetExecutor(mock) input, _ := json.Marshal(skillInput{Skill: "commit", Args: "feat: login"}) result, err := st.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("Execute: %v", err) } if result.IsError { t.Errorf("unexpected error: %q", result.Output) } if !strings.Contains(result.Output, "Expanded skill prompt here") { t.Errorf("Output should contain expanded prompt, got: %q", result.Output) } if mock.lastName != "commit" { t.Errorf("lastName: got %q", mock.lastName) } if mock.lastArgs != "feat: login" { t.Errorf("lastArgs: got %q", mock.lastArgs) } } func TestSkillTool_Execute_InlineWithAllowedTools(t *testing.T) { mock := &mockSkillExecutor{ invokeResult: &SkillResult{ Mode: "inline", Content: "Skill prompt", AllowedTools: []string{"Read", "Glob"}, Model: "haiku", }, } st := NewSkillTool() st.SetExecutor(mock) input, _ := json.Marshal(skillInput{Skill: "constrained"}) result, err := st.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("Execute: %v", err) } if !strings.Contains(result.Output, "Read") { t.Errorf("output should mention allowed tools, got: %q", result.Output) } if !strings.Contains(result.Output, "haiku") { t.Errorf("output should mention model, got: %q", result.Output) } } func TestSkillTool_Execute_ForkMode(t *testing.T) { mock := &mockSkillExecutor{ invokeResult: &SkillResult{ Mode: "fork", Content: "Sub-agent completed: PR looks good", }, } st := NewSkillTool() st.SetExecutor(mock) input, _ := json.Marshal(skillInput{Skill: "review-pr", Args: "123"}) result, err := st.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("Execute: %v", err) } if result.IsError { t.Errorf("unexpected error: %q", result.Output) } if result.Output != "Sub-agent completed: PR looks good" { t.Errorf("Output: got %q", result.Output) } } func TestSkillTool_Execute_ExecutorError(t *testing.T) { mock := &mockSkillExecutor{ invokeErr: errSkillNotFound("missing-skill"), } st := NewSkillTool() st.SetExecutor(mock) input, _ := json.Marshal(skillInput{Skill: "missing-skill"}) result, err := st.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("Execute: %v", err) } if !result.IsError { t.Error("expected error result when executor returns error") } } // ---- SkillTool 实现 tools.Tool 接口 ---- func TestSkillTool_ImplementsTool(t *testing.T) { var _ tools.Tool = NewSkillTool() } // errSkillNotFound 用于测试的简单错误类型 type errSkillNotFound string func (e errSkillNotFound) Error() string { return "skill not found: " + string(e) }