mirror of
https://github.com/lukaszraczylo/harness-sync.git
synced 2026-10-14 00:57:49 +00:00
test: introduce afero-based mock filesystem for hermetic tests
This commit is contained in:
+47
-24
@@ -6,11 +6,22 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/lukaszraczylo/harness-sync/internal/fsx"
|
||||
)
|
||||
|
||||
// Load loads a canonical Bundle from the given root directory using the OS
|
||||
// filesystem. It is a thin wrapper around LoadFS.
|
||||
func Load(root string) (*Bundle, error) {
|
||||
info, err := os.Stat(root)
|
||||
return LoadFS(fsx.OS(), root)
|
||||
}
|
||||
|
||||
// LoadFS loads a canonical Bundle from root using the provided filesystem.
|
||||
// Use fsx.Mem() in tests to avoid touching real disk.
|
||||
func LoadFS(fs fsx.FS, root string) (*Bundle, error) {
|
||||
info, err := fs.Stat(root)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("canonical root %s: %w", root, err)
|
||||
}
|
||||
@@ -20,7 +31,7 @@ func Load(root string) (*Bundle, error) {
|
||||
|
||||
b := &Bundle{Root: root}
|
||||
|
||||
err = loadYAML(filepath.Join(root, "config.yaml"), &b.Config)
|
||||
err = loadYAMLFS(fs, filepath.Join(root, "config.yaml"), &b.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -29,7 +40,7 @@ func Load(root string) (*Bundle, error) {
|
||||
}
|
||||
|
||||
profPath := filepath.Join(root, "profiles", b.Config.ActiveProfile+".yaml")
|
||||
err = loadYAML(profPath, &b.Profile)
|
||||
err = loadYAMLFS(fs, profPath, &b.Profile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("profile %q: %w", b.Config.ActiveProfile, err)
|
||||
}
|
||||
@@ -38,14 +49,14 @@ func Load(root string) (*Bundle, error) {
|
||||
}
|
||||
|
||||
mcpPath := filepath.Join(root, "mcp.yaml")
|
||||
if fileExists(mcpPath) {
|
||||
err = loadYAML(mcpPath, &b.MCP)
|
||||
if fileExistsFS(fs, mcpPath) {
|
||||
err = loadYAMLFS(fs, mcpPath, &b.MCP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
skills, err := loadMarkdownDir(filepath.Join(root, "skills"), "SKILL.md")
|
||||
skills, err := loadMarkdownDirFS(fs, filepath.Join(root, "skills"), "SKILL.md")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -53,7 +64,7 @@ func Load(root string) (*Bundle, error) {
|
||||
b.Skills = append(b.Skills, Skill(m))
|
||||
}
|
||||
|
||||
agents, err := loadMarkdownDir(filepath.Join(root, "agents"), "")
|
||||
agents, err := loadMarkdownDirFS(fs, filepath.Join(root, "agents"), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -62,24 +73,35 @@ func Load(root string) (*Bundle, error) {
|
||||
}
|
||||
|
||||
b.Instructions.PerHarness = map[string]string{}
|
||||
body, err := readFileIfExists(filepath.Join(root, "instructions", "global.md"))
|
||||
body, err := readFileIfExistsFS(fs, filepath.Join(root, "instructions", "global.md"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.Instructions.Global = body
|
||||
|
||||
perHarnessDir := filepath.Join(root, "instructions", "per-harness")
|
||||
if entries, err := os.ReadDir(perHarnessDir); err == nil {
|
||||
if dirExistsFS(fs, perHarnessDir) {
|
||||
dir, err := fs.Open(perHarnessDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries, err := dir.Readdir(-1)
|
||||
if closeErr := dir.Close(); closeErr != nil && err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") {
|
||||
continue
|
||||
}
|
||||
body, err := os.ReadFile(filepath.Join(perHarnessDir, e.Name()))
|
||||
entryBody, err := afero.ReadFile(fs, filepath.Join(perHarnessDir, e.Name()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
harness := strings.TrimSuffix(e.Name(), ".md")
|
||||
b.Instructions.PerHarness[harness] = string(body)
|
||||
b.Instructions.PerHarness[harness] = string(entryBody)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,12 +115,13 @@ type markdownDoc struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
func loadMarkdownDir(dir, requiredFilename string) ([]markdownDoc, error) {
|
||||
// loadMarkdownDirFS walks dir on fs and returns one markdownDoc per matching file.
|
||||
func loadMarkdownDirFS(fs fsx.FS, dir, requiredFilename string) ([]markdownDoc, error) {
|
||||
var docs []markdownDoc
|
||||
if !dirExists(dir) {
|
||||
if !dirExistsFS(fs, dir) {
|
||||
return docs, nil
|
||||
}
|
||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
err := afero.Walk(fs, dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -111,7 +134,7 @@ func loadMarkdownDir(dir, requiredFilename string) ([]markdownDoc, error) {
|
||||
if requiredFilename == "" && !strings.HasSuffix(path, ".md") {
|
||||
return nil
|
||||
}
|
||||
body, err := os.ReadFile(path)
|
||||
body, err := afero.ReadFile(fs, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -149,8 +172,8 @@ func parseFrontmatter(b []byte) (name, description string) {
|
||||
return meta.Name, meta.Description
|
||||
}
|
||||
|
||||
func loadYAML(path string, out interface{}) error {
|
||||
b, err := os.ReadFile(path)
|
||||
func loadYAMLFS(fs fsx.FS, path string, out interface{}) error {
|
||||
b, err := afero.ReadFile(fs, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read %s: %w", path, err)
|
||||
}
|
||||
@@ -160,9 +183,9 @@ func loadYAML(path string, out interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func readFileIfExists(path string) (string, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
if os.IsNotExist(err) {
|
||||
func readFileIfExistsFS(fs fsx.FS, path string) (string, error) {
|
||||
b, err := afero.ReadFile(fs, path)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
return "", nil
|
||||
}
|
||||
if err != nil {
|
||||
@@ -171,12 +194,12 @@ func readFileIfExists(path string) (string, error) {
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
func fileExistsFS(fs fsx.FS, path string) bool {
|
||||
info, err := fs.Stat(path)
|
||||
return err == nil && !info.IsDir()
|
||||
}
|
||||
|
||||
func dirExists(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
func dirExistsFS(fs fsx.FS, path string) bool {
|
||||
info, err := fs.Stat(path)
|
||||
return err == nil && info.IsDir()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package canonical_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/lukaszraczylo/harness-sync/internal/canonical"
|
||||
"github.com/lukaszraczylo/harness-sync/internal/fsx"
|
||||
)
|
||||
|
||||
// writeFile is a convenience helper: writes bytes to path on fs, creating any
|
||||
// parent directories first.
|
||||
func writeFile(t *testing.T, fs fsx.FS, path string, content []byte) {
|
||||
t.Helper()
|
||||
require.NoError(t, fs.MkdirAll(parentDir(path), 0o755))
|
||||
require.NoError(t, afero.WriteFile(fs, path, content, 0o644))
|
||||
}
|
||||
|
||||
func parentDir(path string) string {
|
||||
for i := len(path) - 1; i >= 0; i-- {
|
||||
if path[i] == '/' {
|
||||
return path[:i]
|
||||
}
|
||||
}
|
||||
return "/"
|
||||
}
|
||||
|
||||
func TestLoadFSFromMemFS(t *testing.T) {
|
||||
mem := fsx.Mem()
|
||||
|
||||
configYAML := []byte("active_profile: test\n")
|
||||
profileYAML := []byte("name: Test Profile\ngateway:\n url: https://example.com\n token: tok\n default_model: gpt4\n")
|
||||
skillBody := []byte("---\nname: my-skill\ndescription: A test skill\n---\n# My Skill\nsome content\n")
|
||||
|
||||
writeFile(t, mem, "/root/config.yaml", configYAML)
|
||||
writeFile(t, mem, "/root/profiles/test.yaml", profileYAML)
|
||||
writeFile(t, mem, "/root/skills/my-skill/SKILL.md", skillBody)
|
||||
|
||||
bundle, err := canonical.LoadFS(mem, "/root")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, bundle)
|
||||
|
||||
assert.Equal(t, "test", bundle.Config.ActiveProfile)
|
||||
assert.Equal(t, "Test Profile", bundle.Profile.Name)
|
||||
require.Len(t, bundle.Skills, 1)
|
||||
assert.Equal(t, "my-skill", bundle.Skills[0].Name)
|
||||
assert.Equal(t, "A test skill", bundle.Skills[0].Description)
|
||||
}
|
||||
|
||||
func TestLoadFSEmptyProfile(t *testing.T) {
|
||||
mem := fsx.Mem()
|
||||
|
||||
// config.yaml present but profile file is missing
|
||||
writeFile(t, mem, "/root/config.yaml", []byte("active_profile: missing\n"))
|
||||
|
||||
_, err := canonical.LoadFS(mem, "/root")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "missing")
|
||||
}
|
||||
|
||||
func TestLoadFSMissingRoot(t *testing.T) {
|
||||
mem := fsx.Mem()
|
||||
|
||||
_, err := canonical.LoadFS(mem, "/does-not-exist")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestLoadFSActiveProfileRequired(t *testing.T) {
|
||||
mem := fsx.Mem()
|
||||
|
||||
writeFile(t, mem, "/root/config.yaml", []byte("enabled_harnesses: []\n"))
|
||||
|
||||
_, err := canonical.LoadFS(mem, "/root")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "active_profile is required")
|
||||
}
|
||||
Reference in New Issue
Block a user