Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions agent/skills/fsskills/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -436,11 +436,14 @@ func (s *Source) discoverResourceFiles(skillFS fs.FS, skillName string) []skills
seen := make(map[string]bool)
var resources []skills.Resource
s.scanForFiles(skillFS, ".", skillName, 1, s.allowedResourceExtensions, s.resourceFilter, "resource", func(filePath string) {
key := strings.ToLower(filePath)
if seen[key] {
// Dedup by the exact path (guards against the same file being visited
// twice), not a case-folded key, which would wrongly collapse two
// distinct files (e.g. Data.json and data.json) on a case-sensitive
// filesystem and silently drop one.
if seen[filePath] {
return
}
seen[key] = true
seen[filePath] = true
resources = append(resources, skills.Resource{
Name: filePath,
Read: func(context.Context) (any, error) {
Expand All @@ -459,11 +462,13 @@ func (s *Source) discoverScriptFiles(skillFS fs.FS, skillName string) []skills.S
seen := make(map[string]bool)
var scripts []skills.Script
s.scanForFiles(skillFS, ".", skillName, 1, s.allowedScriptExtensions, s.scriptFilter, "script", func(filePath string) {
key := strings.ToLower(filePath)
if seen[key] {
// Dedup by the exact path (not a case-folded key) so two distinct
// files differing only in case are both kept on a case-sensitive
// filesystem. See discoverResourceFiles for details.
if seen[filePath] {
return
}
seen[key] = true
seen[filePath] = true
scripts = append(scripts, newScript(filePath, skillFS, s.scriptRunner))
})
return scripts
Expand Down
32 changes: 32 additions & 0 deletions agent/skills/fsskills/source_script_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"sort"
"strings"
"testing"
"testing/fstest"

"github.com/microsoft/agent-framework-go/agent/skills"
"github.com/microsoft/agent-framework-go/agent/skills/fsskills"
Expand Down Expand Up @@ -493,3 +494,34 @@ func TestFileSkill_ScriptContent_IncludesDefaultArraySchema(t *testing.T) {
t.Fatalf("expected JSON quotes to be preserved in schema content, got: %s", content)
}
}

// On a case-sensitive filesystem, two files in the same skill directory that
// differ only in case (e.g. Data.json and data.json) are distinct files and
// must both be discovered. Deduping on a lowercased path collapsed them.
func TestFileSource_CaseSensitiveFS_KeepsDistinctlyCasedFiles(t *testing.T) {
// fstest.MapFS keys are case-sensitive regardless of the host filesystem.
fsys := fstest.MapFS{
"case-skill/SKILL.md": {Data: []byte("---\nname: case-skill\ndescription: d\n---\nbody")},
"case-skill/Data.json": {Data: []byte("A")},
"case-skill/data.json": {Data: []byte("B")},
"case-skill/Run.py": {Data: []byte("A")},
"case-skill/run.py": {Data: []byte("B")},
}
source := fsskills.NewSourceOptions(fsskills.SourceOptions{
ScriptRunner: func(context.Context, *skills.Skill, *skills.Script, []string) (any, error) { return nil, nil },
}, fsys)

loaded, err := source.Skills(t.Context())
if err != nil {
t.Fatal(err)
}
if len(loaded) != 1 {
t.Fatalf("expected 1 skill, got %d", len(loaded))
}
if got := len(loaded[0].Resources); got != 2 {
t.Errorf("expected 2 resources (Data.json + data.json) on a case-sensitive FS, got %d", got)
}
if got := len(loaded[0].Scripts); got != 2 {
t.Errorf("expected 2 scripts (Run.py + run.py) on a case-sensitive FS, got %d", got)
}
}