267 lines
6.6 KiB
Go
267 lines
6.6 KiB
Go
package fs_test
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
olog "ollie/pkg/log"
|
|
fs "olliesrv/fs"
|
|
)
|
|
|
|
// --- stub agent.Core ---
|
|
|
|
// --- helpers ---
|
|
|
|
func testSink() *olog.Sink {
|
|
return olog.NewSink(io.Discard, io.Discard, olog.LevelError)
|
|
}
|
|
|
|
func seedSkill(t *testing.T, base, name string) {
|
|
t.Helper()
|
|
d := filepath.Join(base, name)
|
|
os.MkdirAll(d, 0755)
|
|
os.WriteFile(filepath.Join(d, "SKILL.md"), []byte("---\ndescription: test\n---\n"), 0644)
|
|
}
|
|
|
|
// storeRead is a test helper: Open + Read.
|
|
func storeRead(t *testing.T, s fs.FileTree, name string) []byte {
|
|
t.Helper()
|
|
e, err := s.Open(name)
|
|
if err != nil {
|
|
t.Fatalf("Open(%q): %v", name, err)
|
|
}
|
|
data, err := e.Read()
|
|
if err != nil {
|
|
t.Fatalf("Read(%q): %v", name, err)
|
|
}
|
|
return data
|
|
}
|
|
|
|
// storeWrite is a test helper: Open + Write.
|
|
func storeWrite(t *testing.T, s fs.FileTree, name string, data []byte) {
|
|
t.Helper()
|
|
e, err := s.Open(name)
|
|
if err != nil {
|
|
t.Fatalf("Open(%q): %v", name, err)
|
|
}
|
|
if err := e.Write(data); err != nil {
|
|
t.Fatalf("Write(%q): %v", name, err)
|
|
}
|
|
}
|
|
|
|
// --- contract checks ---
|
|
|
|
func checkReadableContract(t *testing.T, s fs.FileTree, name string) {
|
|
t.Helper()
|
|
fi, err := s.Stat(name)
|
|
if err != nil {
|
|
t.Fatalf("Stat(%q): %v", name, err)
|
|
}
|
|
if fi.Name() != name {
|
|
t.Errorf("Stat(%q).Name() = %q", name, fi.Name())
|
|
}
|
|
if _, err := s.Stat("__nonexistent__"); err == nil {
|
|
t.Error("Stat(nonexistent) should error")
|
|
}
|
|
entries, err := s.List()
|
|
if err != nil {
|
|
t.Fatalf("List: %v", err)
|
|
}
|
|
found := false
|
|
for _, e := range entries {
|
|
if e.Name() == name {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("List() missing %q", name)
|
|
}
|
|
e, err := s.Open(name)
|
|
if err != nil {
|
|
t.Fatalf("Open(%q): %v", name, err)
|
|
}
|
|
if _, err := e.Read(); err != nil {
|
|
t.Fatalf("Read(%q): %v", name, err)
|
|
}
|
|
}
|
|
|
|
func checkReadWriteContract(t *testing.T, s fs.FileTree, name string) {
|
|
t.Helper()
|
|
want := []byte("hello")
|
|
e, err := s.Open(name)
|
|
if err != nil {
|
|
t.Fatalf("Open: %v", err)
|
|
}
|
|
if err := e.Write(want); err != nil {
|
|
t.Fatalf("Write: %v", err)
|
|
}
|
|
got, err := e.Read()
|
|
if err != nil {
|
|
t.Fatalf("Read after Write: %v", err)
|
|
}
|
|
if string(got) != string(want) {
|
|
t.Errorf("Read = %q; want %q", got, want)
|
|
}
|
|
want2 := []byte("world")
|
|
if err := e.Write(want2); err != nil {
|
|
t.Fatalf("Write overwrite: %v", err)
|
|
}
|
|
got2, err := e.Read()
|
|
if err != nil {
|
|
t.Fatalf("Read after overwrite: %v", err)
|
|
}
|
|
if string(got2) != string(want2) {
|
|
t.Errorf("Read after overwrite = %q; want %q", got2, want2)
|
|
}
|
|
checkReadableContract(t, s, name)
|
|
}
|
|
|
|
func checkStoreContract(t *testing.T, s fs.FileTree, name string) {
|
|
t.Helper()
|
|
if err := s.Create(name); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
if _, err := s.Stat(name); err != nil {
|
|
t.Fatalf("Stat after Create: %v", err)
|
|
}
|
|
renamed := name + "-renamed"
|
|
if err := s.Rename(name, renamed); err != nil {
|
|
t.Fatalf("Rename: %v", err)
|
|
}
|
|
if _, err := s.Stat(name); err == nil {
|
|
t.Error("Stat(old) should error after Rename")
|
|
}
|
|
if _, err := s.Stat(renamed); err != nil {
|
|
t.Errorf("Stat(new) after Rename: %v", err)
|
|
}
|
|
s.Delete(renamed) //nolint:errcheck
|
|
if err := s.Create(name); err != nil {
|
|
t.Fatalf("Create after rename cleanup: %v", err)
|
|
}
|
|
if err := s.Delete(name); err != nil {
|
|
t.Fatalf("Delete: %v", err)
|
|
}
|
|
if _, err := s.Stat(name); err == nil {
|
|
t.Error("Stat after Delete should error")
|
|
}
|
|
if err := s.Delete("__nonexistent__"); err == nil {
|
|
t.Error("Delete(nonexistent) should error")
|
|
}
|
|
checkReadWriteContract(t, s, name)
|
|
}
|
|
|
|
// ===== FlatDir =====
|
|
|
|
func TestFlatDirContract(t *testing.T) {
|
|
checkStoreContract(t, fs.NewTree([]string{t.TempDir()}, 0644), "test-file")
|
|
}
|
|
|
|
func TestFlatDirCreateMkdirError(t *testing.T) {
|
|
fd := fs.NewTree([]string{"/nonexistent/path"}, 0644)
|
|
if err := fd.Create("f"); err == nil {
|
|
t.Error("Create should fail when dir doesn't exist")
|
|
}
|
|
}
|
|
|
|
func TestFlatDirPutMkdirError(t *testing.T) {
|
|
fd := fs.NewTree([]string{"/nonexistent/path"}, 0644)
|
|
e, err := fd.Open("f")
|
|
if err != nil {
|
|
t.Fatalf("Open: %v", err)
|
|
}
|
|
if err := e.Write([]byte("x")); err == nil {
|
|
t.Error("Write should fail when dir doesn't exist")
|
|
}
|
|
}
|
|
|
|
// ===== SkillTree =====
|
|
|
|
func TestSkillStoreContract(t *testing.T) {
|
|
dir := t.TempDir()
|
|
t.Setenv("OLLIE_SKILLS_PATH", dir)
|
|
seedSkill(t, dir, "test-skill")
|
|
|
|
s := fs.NewSkillTree()
|
|
|
|
// Rename (needs seeded skill with valid front matter)
|
|
seedSkill(t, dir, "rename-src")
|
|
if err := s.Rename("rename-src.md", "rename-dst.md"); err != nil {
|
|
t.Fatalf("Rename: %v", err)
|
|
}
|
|
if _, err := s.Stat("rename-src.md"); err == nil {
|
|
t.Error("Stat(old) should error after Rename")
|
|
}
|
|
if _, err := s.Stat("rename-dst.md"); err != nil {
|
|
t.Errorf("Stat(new) after Rename: %v", err)
|
|
}
|
|
s.Delete("rename-dst.md") //nolint:errcheck
|
|
|
|
// Delete
|
|
seedSkill(t, dir, "del-test")
|
|
if err := s.Delete("del-test.md"); err != nil {
|
|
t.Fatalf("Delete: %v", err)
|
|
}
|
|
if _, err := s.Stat("del-test.md"); err == nil {
|
|
t.Error("Stat after Delete should error")
|
|
}
|
|
if err := s.Delete("__nonexistent__.md"); err == nil {
|
|
t.Error("Delete(nonexistent) should error")
|
|
}
|
|
|
|
// Write/Read round-trip (needs valid front matter)
|
|
content := []byte("---\ndescription: rw test\n---\nbody\n")
|
|
if err := s.Create("rw-skill.md"); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
storeWrite(t, s, "rw-skill.md", content)
|
|
got := storeRead(t, s, "rw-skill.md")
|
|
if string(got) != string(content) {
|
|
t.Errorf("Read = %q; want %q", got, content)
|
|
}
|
|
|
|
// Readable
|
|
checkReadableContract(t, s, "test-skill.md")
|
|
|
|
// idx
|
|
idx := storeRead(t, s, "idx")
|
|
if len(idx) == 0 {
|
|
t.Error("idx should be non-empty with seeded skills")
|
|
}
|
|
|
|
// Rename nonexistent
|
|
if err := s.Rename("__nope__.md", "x.md"); err == nil {
|
|
t.Error("Rename(nonexistent) should error")
|
|
}
|
|
|
|
// Create calls through injected mkdirAll + writeFile
|
|
var createdDir, createdFile string
|
|
s2 := fs.NewSkillTreeWith(fs.SkillConfig{
|
|
Dirs: []string{dir},
|
|
MkdirAll: func(p string, _ os.FileMode) error { createdDir = p; return nil },
|
|
WriteFile: func(p string, _ []byte, _ os.FileMode) error { createdFile = p; return nil },
|
|
})
|
|
if err := s2.Create("newskill.md"); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
if !strings.HasSuffix(createdDir, "newskill") {
|
|
t.Errorf("mkdirAll path = %q; want suffix newskill", createdDir)
|
|
}
|
|
if !strings.HasSuffix(createdFile, "SKILL.md") {
|
|
t.Errorf("writeFile path = %q; want suffix SKILL.md", createdFile)
|
|
}
|
|
|
|
// Create mkdirAll error
|
|
s3 := fs.NewSkillTreeWith(fs.SkillConfig{
|
|
Dirs: []string{dir},
|
|
MkdirAll: func(string, os.FileMode) error { return fmt.Errorf("denied") },
|
|
})
|
|
if err := s3.Create("x.md"); err == nil {
|
|
t.Error("Create should fail when mkdirAll errors")
|
|
}
|
|
}
|
|
|