From c6c82b3c441d07a3f7a32d0e04b452e7bbdd84c0 Mon Sep 17 00:00:00 2001 From: ian <141902143+yumosx@users.noreply.github.com> Date: Mon, 16 Feb 2026 02:12:50 +0800 Subject: [PATCH] feat(skills): add validation for skill info and test cases (#231) Add validation logic for SkillInfo to ensure name and description meet requirements Include test cases covering various validation scenarios Add testify dependency for testing assertions --- go.mod | 10 +++-- go.sum | 6 +++ pkg/skills/loader.go | 45 +++++++++++++++++++++++ pkg/skills/loader_test.go | 77 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 pkg/skills/loader_test.go diff --git a/go.mod b/go.mod index 98aecd6..1f88639 100644 --- a/go.mod +++ b/go.mod @@ -15,11 +15,16 @@ require ( github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1 github.com/openai/openai-go/v3 v3.22.0 github.com/slack-go/slack v0.17.3 + github.com/stretchr/testify v1.11.1 github.com/tencent-connect/botgo v0.2.1 golang.org/x/oauth2 v0.35.0 ) - +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) require ( github.com/andybalholm/brotli v1.2.0 // indirect @@ -28,9 +33,9 @@ require ( github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/github/copilot-sdk/go v0.1.23 - github.com/google/jsonschema-go v0.4.2 // indirect github.com/go-resty/resty/v2 v2.17.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/jsonschema-go v0.4.2 // indirect github.com/grbit/go-json v0.11.0 // indirect github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect @@ -47,5 +52,4 @@ require ( golang.org/x/net v0.50.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.41.0 // indirect - ) diff --git a/go.sum b/go.sum index 6a565b9..0e95bf5 100644 --- a/go.sum +++ b/go.sum @@ -58,6 +58,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -78,9 +80,11 @@ github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzh github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/larksuite/oapi-sdk-go/v3 v3.5.3 h1:xvf8Dv29kBXC5/DNDCLhHkAFW8l/0LlQJimO5Zn+JUk= github.com/larksuite/oapi-sdk-go/v3 v3.5.3/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI= @@ -102,6 +106,7 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g= github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk= @@ -242,6 +247,7 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= diff --git a/pkg/skills/loader.go b/pkg/skills/loader.go index 1f952c1..0c63ae0 100644 --- a/pkg/skills/loader.go +++ b/pkg/skills/loader.go @@ -2,13 +2,22 @@ package skills import ( "encoding/json" + "errors" "fmt" + "log/slog" "os" "path/filepath" "regexp" "strings" ) +var namePattern = regexp.MustCompile(`^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$`) + +const ( + MaxNameLength = 64 + MaxDescriptionLength = 1024 +) + type SkillMetadata struct { Name string `json:"name"` Description string `json:"description"` @@ -21,6 +30,27 @@ type SkillInfo struct { Description string `json:"description"` } +func (info SkillInfo) validate() error { + var errs error + if info.Name == "" { + errs = errors.Join(errs, errors.New("name is required")) + } else { + if len(info.Name) > MaxNameLength { + errs = errors.Join(errs, fmt.Errorf("name exceeds %d characters", MaxNameLength)) + } + if !namePattern.MatchString(info.Name) { + errs = errors.Join(errs, errors.New("name must be alphanumeric with hyphens")) + } + } + + if info.Description == "" { + errs = errors.Join(errs, errors.New("description is required")) + } else if len(info.Description) > MaxDescriptionLength { + errs = errors.Join(errs, fmt.Errorf("description exceeds %d character", MaxDescriptionLength)) + } + return errs +} + type SkillsLoader struct { workspace string workspaceSkills string // workspace skills (项目级别) @@ -54,6 +84,11 @@ func (sl *SkillsLoader) ListSkills() []SkillInfo { metadata := sl.getSkillMetadata(skillFile) if metadata != nil { info.Description = metadata.Description + info.Name = metadata.Name + } + if err := info.validate(); err != nil { + slog.Warn("invalid skill from workspace", "name", info.Name, "error", err) + continue } skills = append(skills, info) } @@ -89,6 +124,11 @@ func (sl *SkillsLoader) ListSkills() []SkillInfo { metadata := sl.getSkillMetadata(skillFile) if metadata != nil { info.Description = metadata.Description + info.Name = metadata.Name + } + if err := info.validate(); err != nil { + slog.Warn("invalid skill from global", "name", info.Name, "error", err) + continue } skills = append(skills, info) } @@ -123,6 +163,11 @@ func (sl *SkillsLoader) ListSkills() []SkillInfo { metadata := sl.getSkillMetadata(skillFile) if metadata != nil { info.Description = metadata.Description + info.Name = metadata.Name + } + if err := info.validate(); err != nil { + slog.Warn("invalid skill from builtin", "name", info.Name, "error", err) + continue } skills = append(skills, info) } diff --git a/pkg/skills/loader_test.go b/pkg/skills/loader_test.go new file mode 100644 index 0000000..e0e7109 --- /dev/null +++ b/pkg/skills/loader_test.go @@ -0,0 +1,77 @@ +package skills + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSkillsInfoValidate(t *testing.T) { + testcases := []struct { + name string + skillName string + description string + wantErr bool + errContains []string + }{ + { + name: "valid-skill", + skillName: "valid-skill", + description: "a valid skill description", + wantErr: false, + }, + { + name: "empty-name", + skillName: "", + description: "description without name", + wantErr: true, + errContains: []string{"name is required"}, + }, + { + name: "empty-description", + skillName: "skill-without-description", + description: "", + wantErr: true, + errContains: []string{"description is required"}, + }, + { + name: "empty-both", + skillName: "", + description: "", + wantErr: true, + errContains: []string{"name is required", "description is required"}, + }, + { + name: "name-with-spaces", + skillName: "skill with spaces", + description: "invalid name with spaces", + wantErr: true, + errContains: []string{"name must be alphanumeric with hyphens"}, + }, + { + name: "name-with-underscore", + skillName: "skill_underscore", + description: "invalid name with underscore", + wantErr: true, + errContains: []string{"name must be alphanumeric with hyphens"}, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + info := SkillInfo{ + Name: tc.skillName, + Description: tc.description, + } + err := info.validate() + if tc.wantErr { + assert.Error(t, err) + for _, msg := range tc.errContains { + assert.ErrorContains(t, err, msg) + } + } else { + assert.NoError(t, err) + } + }) + } +}