From 3aea635e44b20dddb75603c47f76f21ae95123df Mon Sep 17 00:00:00 2001 From: "Ringo.Typowriter" Date: Tue, 17 Feb 2026 17:55:07 +0800 Subject: [PATCH] fix: skill normalize (#57) --- internal/conversation/flow/resolver.go | 32 +++++++++++--- .../conversation/flow/resolver_skills_test.go | 32 ++++++++++++++ internal/handlers/skills.go | 32 +++++++++++--- internal/handlers/skills_test.go | 42 +++++++++++++++++++ 4 files changed, 126 insertions(+), 12 deletions(-) create mode 100644 internal/conversation/flow/resolver_skills_test.go create mode 100644 internal/handlers/skills_test.go diff --git a/internal/conversation/flow/resolver.go b/internal/conversation/flow/resolver.go index 2132d33d..73f77e25 100644 --- a/internal/conversation/flow/resolver.go +++ b/internal/conversation/flow/resolver.go @@ -236,12 +236,11 @@ func (r *Resolver) resolve(ctx context.Context, req conversation.ChatRequest) (r } else { usableSkills = make([]gatewaySkill, 0, len(entries)) for _, e := range entries { - usableSkills = append(usableSkills, gatewaySkill{ - Name: e.Name, - Description: e.Description, - Content: e.Content, - Metadata: e.Metadata, - }) + skill, ok := normalizeGatewaySkill(e) + if !ok { + continue + } + usableSkills = append(usableSkills, skill) } } } @@ -1099,6 +1098,27 @@ func sanitizeMessages(messages []conversation.ModelMessage) []conversation.Model return cleaned } +func normalizeGatewaySkill(entry SkillEntry) (gatewaySkill, bool) { + name := strings.TrimSpace(entry.Name) + if name == "" { + return gatewaySkill{}, false + } + description := strings.TrimSpace(entry.Description) + if description == "" { + description = name + } + content := strings.TrimSpace(entry.Content) + if content == "" { + content = description + } + return gatewaySkill{ + Name: name, + Description: description, + Content: content, + Metadata: entry.Metadata, + }, true +} + func dedup(items []string) []string { seen := make(map[string]struct{}, len(items)) result := make([]string, 0, len(items)) diff --git a/internal/conversation/flow/resolver_skills_test.go b/internal/conversation/flow/resolver_skills_test.go new file mode 100644 index 00000000..551a14aa --- /dev/null +++ b/internal/conversation/flow/resolver_skills_test.go @@ -0,0 +1,32 @@ +package flow + +import "testing" + +func TestNormalizeGatewaySkill_Fallbacks(t *testing.T) { + got, ok := normalizeGatewaySkill(SkillEntry{ + Name: " demo-skill ", + }) + if !ok { + t.Fatal("expected valid skill") + } + if got.Name != "demo-skill" { + t.Fatalf("expected trimmed name demo-skill, got %q", got.Name) + } + if got.Description != "demo-skill" { + t.Fatalf("expected description fallback to name, got %q", got.Description) + } + if got.Content != "demo-skill" { + t.Fatalf("expected content fallback to description, got %q", got.Content) + } +} + +func TestNormalizeGatewaySkill_RejectsEmptyName(t *testing.T) { + _, ok := normalizeGatewaySkill(SkillEntry{ + Name: " ", + Description: "desc", + Content: "content", + }) + if ok { + t.Fatal("expected invalid skill when name is empty") + } +} diff --git a/internal/handlers/skills.go b/internal/handlers/skills.go index 4a636ff5..1d534152 100644 --- a/internal/handlers/skills.go +++ b/internal/handlers/skills.go @@ -311,11 +311,13 @@ type parsedSkill struct { // --- // # Body content ... func parseSkillFile(raw string, fallbackName string) parsedSkill { - result := parsedSkill{Name: fallbackName} - trimmed := strings.TrimSpace(raw) + result := parsedSkill{ + Name: strings.TrimSpace(fallbackName), + Content: trimmed, + } if !strings.HasPrefix(trimmed, "---") { - return result + return normalizeParsedSkill(result) } // Find closing "---". @@ -328,7 +330,7 @@ func parseSkillFile(raw string, fallbackName string) parsedSkill { } closingIdx := strings.Index(rest, "\n---") if closingIdx < 0 { - return result + return normalizeParsedSkill(result) } frontmatterRaw := rest[:closingIdx] @@ -342,7 +344,7 @@ func parseSkillFile(raw string, fallbackName string) parsedSkill { Metadata map[string]any `yaml:"metadata"` } if err := yaml.Unmarshal([]byte(frontmatterRaw), &fm); err != nil { - return result + return normalizeParsedSkill(result) } if strings.TrimSpace(fm.Name) != "" { @@ -351,7 +353,25 @@ func parseSkillFile(raw string, fallbackName string) parsedSkill { result.Description = strings.TrimSpace(fm.Description) result.Metadata = fm.Metadata - return result + return normalizeParsedSkill(result) +} + +func normalizeParsedSkill(skill parsedSkill) parsedSkill { + if strings.TrimSpace(skill.Name) == "" { + skill.Name = "default" + } + skill.Name = strings.TrimSpace(skill.Name) + skill.Description = strings.TrimSpace(skill.Description) + skill.Content = strings.TrimSpace(skill.Content) + + if skill.Description == "" { + skill.Description = skill.Name + } + if skill.Content == "" { + skill.Content = skill.Description + } + + return skill } func buildSkillContent(name, description string) string { diff --git a/internal/handlers/skills_test.go b/internal/handlers/skills_test.go new file mode 100644 index 00000000..f0bc8765 --- /dev/null +++ b/internal/handlers/skills_test.go @@ -0,0 +1,42 @@ +package handlers + +import "testing" + +func TestParseSkillFile_NoFrontmatterFallbacks(t *testing.T) { + raw := "# Use this skill\n\nDo something useful." + got := parseSkillFile(raw, "plain-skill") + + if got.Name != "plain-skill" { + t.Fatalf("expected name plain-skill, got %q", got.Name) + } + if got.Description != "plain-skill" { + t.Fatalf("expected description plain-skill, got %q", got.Description) + } + if got.Content != raw { + t.Fatalf("expected content to keep original markdown, got %q", got.Content) + } +} + +func TestParseSkillFile_FrontmatterDescriptionFallback(t *testing.T) { + raw := "---\nname: hello-skill\n---\n\nBody content" + got := parseSkillFile(raw, "fallback") + + if got.Name != "hello-skill" { + t.Fatalf("expected frontmatter name hello-skill, got %q", got.Name) + } + if got.Description != "hello-skill" { + t.Fatalf("expected description fallback to name, got %q", got.Description) + } + if got.Content != "Body content" { + t.Fatalf("expected content Body content, got %q", got.Content) + } +} + +func TestParseSkillFile_EmptyBodyFallbacksToDescription(t *testing.T) { + raw := "---\nname: hello-skill\ndescription: say hello\n---\n" + got := parseSkillFile(raw, "fallback") + + if got.Content != "say hello" { + t.Fatalf("expected content fallback to description, got %q", got.Content) + } +}