271 lines
7.0 KiB
Go
271 lines
7.0 KiB
Go
package filecatalog
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
)
|
|
|
|
func TestFindYAMLFilesNestedSortedAndFiltered(t *testing.T) {
|
|
root := t.TempDir()
|
|
mustWriteFile(t, filepath.Join(root, "z", "prompt.yml"), "id: z")
|
|
mustWriteFile(t, filepath.Join(root, "a", "profile.yaml"), "id: a")
|
|
mustWriteFile(t, filepath.Join(root, "a", "ignore.txt"), "not yaml")
|
|
mustWriteFile(t, filepath.Join(root, "b", "ignore.yaml.bak"), "not yaml")
|
|
|
|
got, err := FindYAMLFiles(context.Background(), root)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
|
|
want := []string{
|
|
filepath.Join(root, "a", "profile.yaml"),
|
|
filepath.Join(root, "z", "prompt.yml"),
|
|
}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("expected sorted YAML files %v, got %v", want, got)
|
|
}
|
|
}
|
|
|
|
func TestFindYAMLFilesHonorsContextCancellation(t *testing.T) {
|
|
root := t.TempDir()
|
|
mustWriteFile(t, filepath.Join(root, "one.yaml"), "id: one")
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
_, err := FindYAMLFiles(ctx, root)
|
|
if !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("expected context.Canceled, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestFindFSYAMLFilesNestedSortedAndFiltered(t *testing.T) {
|
|
fsys := fstest.MapFS{
|
|
"prompts/z/prompt.yml": &fstest.MapFile{Data: []byte("id: z")},
|
|
"prompts/a/profile.yaml": &fstest.MapFile{Data: []byte("id: a")},
|
|
"prompts/a/ignore.txt": &fstest.MapFile{Data: []byte("not yaml")},
|
|
"prompts/b/ignore.yaml.bak": &fstest.MapFile{Data: []byte("not yaml")},
|
|
"other/ignored.yaml": &fstest.MapFile{Data: []byte("id: ignored")},
|
|
}
|
|
|
|
got, err := FindFSYAMLFiles(context.Background(), fsys, " prompts ")
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
|
|
want := []string{
|
|
"prompts/a/profile.yaml",
|
|
"prompts/z/prompt.yml",
|
|
}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("expected sorted YAML files %v, got %v", want, got)
|
|
}
|
|
}
|
|
|
|
func TestFindFSYAMLFilesHonorsContextCancellation(t *testing.T) {
|
|
fsys := fstest.MapFS{
|
|
"one.yaml": &fstest.MapFile{Data: []byte("id: one")},
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
_, err := FindFSYAMLFiles(ctx, fsys, ".")
|
|
if !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("expected context.Canceled, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRelativePathNested(t *testing.T) {
|
|
root := t.TempDir()
|
|
path := filepath.Join(root, "nested", "profiles", "local.yaml")
|
|
got := RelativePath(root, path)
|
|
want := filepath.Join("nested", "profiles", "local.yaml")
|
|
if got != want {
|
|
t.Fatalf("expected relative path %q, got %q", want, got)
|
|
}
|
|
}
|
|
|
|
func TestCleanFSRoot(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
root string
|
|
want string
|
|
}{
|
|
{name: "empty", root: "", want: "."},
|
|
{name: "dot", root: ".", want: "."},
|
|
{name: "trimmed", root: " prompts/../profiles ", want: "profiles"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := CleanFSRoot(tc.root); got != tc.want {
|
|
t.Fatalf("expected %q, got %q", tc.want, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDisplayPath(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
root string
|
|
path string
|
|
want string
|
|
}{
|
|
{name: "root dot", root: ".", path: "profiles/local.yaml", want: "profiles/local.yaml"},
|
|
{name: "nested root", root: "profiles", path: "profiles/local.yaml", want: "local.yaml"},
|
|
{name: "outside root", root: "profiles", path: "other/local.yaml", want: "other/local.yaml"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := DisplayPath(tc.root, tc.path); got != tc.want {
|
|
t.Fatalf("expected %q, got %q", tc.want, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveFSPath(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
root string
|
|
baseDir string
|
|
userPath string
|
|
wantPath string
|
|
wantDisplay string
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "sibling inside root",
|
|
root: "prompts",
|
|
baseDir: "prompts/nested",
|
|
userPath: "./messages/user.tmpl",
|
|
wantPath: "prompts/nested/messages/user.tmpl",
|
|
wantDisplay: "nested/messages/user.tmpl",
|
|
},
|
|
{
|
|
name: "parent inside root",
|
|
root: "prompts",
|
|
baseDir: "prompts/nested",
|
|
userPath: "../shared/user.tmpl",
|
|
wantPath: "prompts/shared/user.tmpl",
|
|
wantDisplay: "shared/user.tmpl",
|
|
},
|
|
{
|
|
name: "escape rejected",
|
|
root: "prompts",
|
|
baseDir: "prompts/nested",
|
|
userPath: "../../outside.tmpl",
|
|
wantErr: "escapes source root",
|
|
},
|
|
{
|
|
name: "absolute path rejected",
|
|
root: "prompts",
|
|
baseDir: "prompts/nested",
|
|
userPath: "/outside.tmpl",
|
|
wantErr: "must be relative",
|
|
},
|
|
{
|
|
name: "empty path rejected",
|
|
root: "prompts",
|
|
baseDir: "prompts/nested",
|
|
userPath: " ",
|
|
wantErr: "path is required",
|
|
},
|
|
{
|
|
name: "dot root allows normal relative path",
|
|
root: ".",
|
|
baseDir: ".",
|
|
userPath: "schemas/events.schema.json",
|
|
wantPath: "schemas/events.schema.json",
|
|
wantDisplay: "schemas/events.schema.json",
|
|
},
|
|
{
|
|
name: "dot root rejects parent escape",
|
|
root: ".",
|
|
baseDir: ".",
|
|
userPath: "../outside.tmpl",
|
|
wantErr: "escapes source root",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
gotPath, gotDisplay, err := ResolveFSPath(tc.root, tc.baseDir, tc.userPath)
|
|
if tc.wantErr != "" {
|
|
if err == nil {
|
|
t.Fatalf("expected error containing %q", tc.wantErr)
|
|
}
|
|
if !strings.Contains(err.Error(), tc.wantErr) {
|
|
t.Fatalf("expected error to contain %q, got %v", tc.wantErr, err)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if gotPath != tc.wantPath || gotDisplay != tc.wantDisplay {
|
|
t.Fatalf("expected path/display %q/%q, got %q/%q", tc.wantPath, tc.wantDisplay, gotPath, gotDisplay)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStemStripsYAMLExtensions(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want string
|
|
}{
|
|
{name: "yaml", in: "prompt.yaml", want: "prompt"},
|
|
{name: "yml", in: "profile.yml", want: "profile"},
|
|
{name: "other", in: "file.txt", want: "file.txt"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := Stem(tc.in); got != tc.want {
|
|
t.Fatalf("expected %q, got %q", tc.want, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsYAMLFile(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want bool
|
|
}{
|
|
{name: "yaml", in: "prompt.yaml", want: true},
|
|
{name: "yml", in: "profile.yml", want: true},
|
|
{name: "backup", in: "profile.yaml.bak", want: false},
|
|
{name: "uppercase", in: "profile.YAML", want: false},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := IsYAMLFile(tc.in); got != tc.want {
|
|
t.Fatalf("expected %v, got %v", tc.want, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func mustWriteFile(t *testing.T, path string, content string) {
|
|
t.Helper()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatalf("failed to create directory: %v", err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
|
t.Fatalf("failed to write file %q: %v", path, err)
|
|
}
|
|
}
|