mirror of
https://github.com/lukaszraczylo/harness-sync.git
synced 2026-10-12 00:45:22 +00:00
test: introduce afero-based mock filesystem for hermetic tests
This commit is contained in:
@@ -6,12 +6,15 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/lukaszraczylo/harness-sync/internal/canonical"
|
||||
"github.com/lukaszraczylo/harness-sync/internal/fsx"
|
||||
)
|
||||
|
||||
// ReadIfExists returns the file body or empty string when missing.
|
||||
func ReadIfExists(path string) (string, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
// ReadIfExistsFS returns the file body or empty string when missing, using fs.
|
||||
func ReadIfExistsFS(fs fsx.FS, path string) (string, error) {
|
||||
b, err := afero.ReadFile(fs, path)
|
||||
if os.IsNotExist(err) {
|
||||
return "", nil
|
||||
}
|
||||
@@ -21,10 +24,20 @@ func ReadIfExists(path string) (string, error) {
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// ReadIfExists returns the file body or empty string when missing.
|
||||
func ReadIfExists(path string) (string, error) {
|
||||
return ReadIfExistsFS(fsx.OS(), path)
|
||||
}
|
||||
|
||||
// DirExistsFS reports whether path is an existing directory on fs.
|
||||
func DirExistsFS(fs fsx.FS, path string) bool {
|
||||
info, err := fs.Stat(path)
|
||||
return err == nil && info.IsDir()
|
||||
}
|
||||
|
||||
// DirExists reports whether path is an existing directory.
|
||||
func DirExists(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
return err == nil && info.IsDir()
|
||||
return DirExistsFS(fsx.OS(), path)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -41,11 +54,10 @@ func StripJSONComments(s string) string {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
// ImportMCPFromJSONFile reads a JSON file and extracts an MCP server map
|
||||
// nested under the given top-level key (e.g. "mcpServers", "mcp").
|
||||
// Returns nil with no error when the file is missing.
|
||||
func ImportMCPFromJSONFile(path, key string) ([]canonical.MCPServer, error) {
|
||||
body, err := os.ReadFile(path)
|
||||
// ImportMCPFromJSONFileFS reads a JSON file from fs and extracts an MCP server
|
||||
// map nested under the given top-level key. Returns nil with no error when missing.
|
||||
func ImportMCPFromJSONFileFS(fs fsx.FS, path, key string) ([]canonical.MCPServer, error) {
|
||||
body, err := afero.ReadFile(fs, path)
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -55,6 +67,13 @@ func ImportMCPFromJSONFile(path, key string) ([]canonical.MCPServer, error) {
|
||||
return ParseMCPFromJSON(body, key)
|
||||
}
|
||||
|
||||
// ImportMCPFromJSONFile reads a JSON file and extracts an MCP server map
|
||||
// nested under the given top-level key (e.g. "mcpServers", "mcp").
|
||||
// Returns nil with no error when the file is missing.
|
||||
func ImportMCPFromJSONFile(path, key string) ([]canonical.MCPServer, error) {
|
||||
return ImportMCPFromJSONFileFS(fsx.OS(), path, key)
|
||||
}
|
||||
|
||||
// ParseMCPFromJSON extracts MCP servers from a JSON body. The body may be
|
||||
// raw JSON or JSONC (comments are stripped first). Tolerates the dialect
|
||||
// differences across harnesses:
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package common_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/lukaszraczylo/harness-sync/internal/adapter/common"
|
||||
"github.com/lukaszraczylo/harness-sync/internal/fsx"
|
||||
)
|
||||
|
||||
func TestReadIfExistsFSMissingReturnsEmpty(t *testing.T) {
|
||||
mem := fsx.Mem()
|
||||
got, err := common.ReadIfExistsFS(mem, "/no-such-file.txt")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", got)
|
||||
}
|
||||
|
||||
func TestReadIfExistsFSReturnsContent(t *testing.T) {
|
||||
mem := fsx.Mem()
|
||||
want := "hello from memfs"
|
||||
require.NoError(t, afero.WriteFile(mem, "/test.txt", []byte(want), 0o644))
|
||||
|
||||
got, err := common.ReadIfExistsFS(mem, "/test.txt")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
|
||||
func TestImportMCPFromJSONFileFSHandlesMissingFile(t *testing.T) {
|
||||
mem := fsx.Mem()
|
||||
servers, err := common.ImportMCPFromJSONFileFS(mem, "/not-here.json", "mcpServers")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, servers)
|
||||
}
|
||||
|
||||
func TestImportMCPFromJSONFileFSExtractsServers(t *testing.T) {
|
||||
mem := fsx.Mem()
|
||||
body := []byte(`{
|
||||
"mcpServers": {
|
||||
"myserver": {
|
||||
"command": "/usr/bin/mytool",
|
||||
"args": ["--flag"],
|
||||
"type": "stdio"
|
||||
}
|
||||
}
|
||||
}`)
|
||||
require.NoError(t, afero.WriteFile(mem, "/claude.json", body, 0o644))
|
||||
|
||||
servers, err := common.ImportMCPFromJSONFileFS(mem, "/claude.json", "mcpServers")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, servers, 1)
|
||||
assert.Equal(t, "myserver", servers[0].Name)
|
||||
assert.Equal(t, "/usr/bin/mytool", servers[0].Command)
|
||||
assert.Equal(t, []string{"--flag"}, servers[0].Args)
|
||||
assert.Equal(t, "stdio", servers[0].Transport)
|
||||
}
|
||||
|
||||
func TestImportMCPFromJSONFileFSReadOnlyFS(t *testing.T) {
|
||||
// Populate a mem fs then wrap it read-only — reads should still work.
|
||||
mem := fsx.Mem()
|
||||
body := []byte(`{"mcpServers": {"srv": {"command": "bin"}}}`)
|
||||
require.NoError(t, afero.WriteFile(mem, "/config.json", body, 0o644))
|
||||
|
||||
roFS := afero.NewReadOnlyFs(mem)
|
||||
servers, err := common.ImportMCPFromJSONFileFS(roFS, "/config.json", "mcpServers")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, servers, 1)
|
||||
assert.Equal(t, "srv", servers[0].Name)
|
||||
}
|
||||
@@ -5,7 +5,10 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/lukaszraczylo/harness-sync/internal/fsx"
|
||||
)
|
||||
|
||||
// Doc mirrors canonical.Skill / canonical.Agent without importing them.
|
||||
@@ -35,15 +38,15 @@ func ParseFrontmatter(b []byte) (name, description string) {
|
||||
return meta.Name, meta.Description
|
||||
}
|
||||
|
||||
// ImportMarkdownTree walks dir and returns one Doc per markdown file. When
|
||||
// requiredFilename is non-empty (e.g. "SKILL.md"), only files with that exact
|
||||
// basename are included; otherwise any *.md.
|
||||
func ImportMarkdownTree(dir, requiredFilename string) ([]Doc, error) {
|
||||
// ImportMarkdownTreeFS walks dir on fs and returns one Doc per matching file.
|
||||
// When requiredFilename is non-empty (e.g. "SKILL.md"), only files with that
|
||||
// exact basename are included; otherwise any *.md.
|
||||
func ImportMarkdownTreeFS(fs fsx.FS, dir, requiredFilename string) ([]Doc, error) {
|
||||
var docs []Doc
|
||||
if !DirExists(dir) {
|
||||
if !DirExistsFS(fs, dir) {
|
||||
return docs, nil
|
||||
}
|
||||
err := filepath.Walk(dir, func(p string, info os.FileInfo, err error) error {
|
||||
err := afero.Walk(fs, dir, func(p string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -56,7 +59,7 @@ func ImportMarkdownTree(dir, requiredFilename string) ([]Doc, error) {
|
||||
if requiredFilename == "" && !strings.HasSuffix(p, ".md") {
|
||||
return nil
|
||||
}
|
||||
body, err := os.ReadFile(p)
|
||||
body, err := afero.ReadFile(fs, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -75,3 +78,10 @@ func ImportMarkdownTree(dir, requiredFilename string) ([]Doc, error) {
|
||||
})
|
||||
return docs, err
|
||||
}
|
||||
|
||||
// ImportMarkdownTree walks dir and returns one Doc per markdown file. When
|
||||
// requiredFilename is non-empty (e.g. "SKILL.md"), only files with that exact
|
||||
// basename are included; otherwise any *.md.
|
||||
func ImportMarkdownTree(dir, requiredFilename string) ([]Doc, error) {
|
||||
return ImportMarkdownTreeFS(fsx.OS(), dir, requiredFilename)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package common_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/lukaszraczylo/harness-sync/internal/adapter/common"
|
||||
"github.com/lukaszraczylo/harness-sync/internal/fsx"
|
||||
)
|
||||
|
||||
func TestImportMarkdownTreeFSPicksUpSkillsFromMemFS(t *testing.T) {
|
||||
mem := fsx.Mem()
|
||||
|
||||
body := []byte("---\nname: cool-skill\ndescription: does stuff\n---\n# Cool Skill\n")
|
||||
require.NoError(t, mem.MkdirAll("/skills/cool-skill", 0o755))
|
||||
require.NoError(t, afero.WriteFile(mem, "/skills/cool-skill/SKILL.md", body, 0o644))
|
||||
|
||||
docs, err := common.ImportMarkdownTreeFS(mem, "/skills", "SKILL.md")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, docs, 1)
|
||||
assert.Equal(t, "cool-skill", docs[0].Name)
|
||||
assert.Equal(t, "does stuff", docs[0].Description)
|
||||
assert.Contains(t, docs[0].Body, "# Cool Skill")
|
||||
}
|
||||
|
||||
func TestImportMarkdownTreeFSHandlesMissingDir(t *testing.T) {
|
||||
mem := fsx.Mem()
|
||||
|
||||
docs, err := common.ImportMarkdownTreeFS(mem, "/no-such-dir", "SKILL.md")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, docs)
|
||||
}
|
||||
|
||||
func TestImportMarkdownTreeFSSkipsNonMatching(t *testing.T) {
|
||||
mem := fsx.Mem()
|
||||
|
||||
require.NoError(t, mem.MkdirAll("/skills/a", 0o755))
|
||||
require.NoError(t, mem.MkdirAll("/skills/b", 0o755))
|
||||
require.NoError(t, afero.WriteFile(mem, "/skills/a/SKILL.md", []byte("---\nname: a\n---\n"), 0o644))
|
||||
require.NoError(t, afero.WriteFile(mem, "/skills/b/OTHER.md", []byte("# other"), 0o644))
|
||||
require.NoError(t, afero.WriteFile(mem, "/skills/b/notes.txt", []byte("text"), 0o644))
|
||||
|
||||
// requiredFilename = "SKILL.md" → only picks up a/SKILL.md
|
||||
docs, err := common.ImportMarkdownTreeFS(mem, "/skills", "SKILL.md")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, docs, 1)
|
||||
assert.Equal(t, "a", docs[0].Name)
|
||||
}
|
||||
|
||||
func TestImportMarkdownTreeFSAnyMDWhenNoRequired(t *testing.T) {
|
||||
mem := fsx.Mem()
|
||||
|
||||
require.NoError(t, mem.MkdirAll("/agents", 0o755))
|
||||
require.NoError(t, afero.WriteFile(mem, "/agents/alpha.md", []byte("# Alpha\n"), 0o644))
|
||||
require.NoError(t, afero.WriteFile(mem, "/agents/beta.md", []byte("# Beta\n"), 0o644))
|
||||
require.NoError(t, afero.WriteFile(mem, "/agents/skip.txt", []byte("not md"), 0o644))
|
||||
|
||||
docs, err := common.ImportMarkdownTreeFS(mem, "/agents", "")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, docs, 2)
|
||||
}
|
||||
Reference in New Issue
Block a user