From d377e6b345b0525b132759590769d1c86534eeca Mon Sep 17 00:00:00 2001 From: Greg Gauthier Date: Sun, 8 Mar 2026 12:53:01 +0000 Subject: [PATCH] test: add tests for recipe package and disable parallelism in cmd tests - Introduce new test suite in internal/recipe/recipe_test.go covering recipe loading, parameter overrides, safety checks, work dir resolution, file discovery, and unified patch creation. - Remove t.Parallel() from tests in cmd/changelog_test.go, cmd/root_test.go, and cmd/scaffold_test.go that modify global state (e.g., os.Args, HOME, or use os.Chdir()) to avoid race conditions and ensure test isolation. --- cmd/changelog_test.go | 4 - cmd/root_test.go | 1 + cmd/scaffold_test.go | 1 + internal/recipe/recipe_test.go | 189 +++++++++++++++++++++++++++++++++ 4 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 internal/recipe/recipe_test.go diff --git a/cmd/changelog_test.go b/cmd/changelog_test.go index 87fa126..ae737de 100644 --- a/cmd/changelog_test.go +++ b/cmd/changelog_test.go @@ -34,12 +34,9 @@ fix: typo in docs } func TestBuildFullChangelog(t *testing.T) { - t.Parallel() - newSection := "## [v0.2.0] - 2026-03-03\n\n### Added\n- changelog command\n" t.Run("creates new file with header", func(t *testing.T) { - t.Parallel() // nolint:tparallel // os.Chdir affects the entire process tmpDir := t.TempDir() @@ -58,7 +55,6 @@ func TestBuildFullChangelog(t *testing.T) { }) t.Run("prepends to existing file", func(t *testing.T) { - t.Parallel() // nolint:tparallel // os.Chdir affects the entire process tmpDir := t.TempDir() diff --git a/cmd/root_test.go b/cmd/root_test.go index 7569e9b..03da1bb 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -16,6 +16,7 @@ func rootSetHomeForTest(t *testing.T, dir string) { } func TestExecute(t *testing.T) { + // Not using t.Parallel() because it modifies os.Args and environment variables (HOME) tests := []struct { name string args []string diff --git a/cmd/scaffold_test.go b/cmd/scaffold_test.go index 66ea05a..339331f 100644 --- a/cmd/scaffold_test.go +++ b/cmd/scaffold_test.go @@ -53,6 +53,7 @@ func TestScaffoldCmd_Live(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + // Not using t.Parallel() because it uses os.Chdir() t.Logf("Live test: %s", tt.name) tmpDir := t.TempDir() diff --git a/internal/recipe/recipe_test.go b/internal/recipe/recipe_test.go new file mode 100644 index 0000000..547be2b --- /dev/null +++ b/internal/recipe/recipe_test.go @@ -0,0 +1,189 @@ +package recipe + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLoad(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + recipePath := filepath.Join(tmpDir, "test.yaml") + + content := `--- +name: Test Recipe +description: A recipe for testing +version: 1.0.0 +parameters: + target_file: + type: string + default: "main.go" + description: File to process +allowed_shell_commands: + - ls -la + - pwd +project_languages: + - go +extensions: + go: [".go"] +search_pattern: "func main" +--- +## Execution Steps + +### Step 1: Discover +**Objective:** Find files. +**Instructions:** Use discovery. +**Expected output:** List of files. + +### Step 2: Refactor +**Objective:** Refactor files. +**Instructions:** Use refactoring. +**Expected output:** JSON. + +This is the final summary prompt. +` + err := os.WriteFile(recipePath, []byte(content), 0600) + require.NoError(t, err) + + t.Run("successful load with defaults", func(t *testing.T) { + r, err := Load(recipePath, nil) + require.NoError(t, err) + assert.Equal(t, "Test Recipe", r.Name) + assert.Equal(t, "1.0.0", r.Version) + assert.Equal(t, "main.go", r.ResolvedParams["target_file"]) + assert.Len(t, r.Steps, 2) + assert.Equal(t, 1, r.Steps[0].Number) + assert.Equal(t, "Discover", r.Steps[0].Title) + assert.Equal(t, "Find files.", r.Steps[0].Objective) + assert.Equal(t, "Use discovery.", r.Steps[0].Instructions) + assert.Equal(t, "List of files.", r.Steps[0].Expected) + assert.Contains(t, r.FinalSummaryPrompt, "This is the final summary prompt.") + }) + + t.Run("load with parameter override", func(t *testing.T) { + params := map[string]any{"target_file": "app.go"} + r, err := Load(recipePath, params) + require.NoError(t, err) + assert.Equal(t, "app.go", r.ResolvedParams["target_file"]) + }) + + t.Run("load unsafe command", func(t *testing.T) { + unsafeContent := `--- +name: Unsafe Recipe +allowed_shell_commands: + - rm -rf / +--- +## Execution Steps +### Step 1: Bad +` + unsafePath := filepath.Join(tmpDir, "unsafe.yaml") + err := os.WriteFile(unsafePath, []byte(unsafeContent), 0600) + require.NoError(t, err) + + _, err = Load(unsafePath, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "unsafe shell command") + }) + + t.Run("missing frontmatter", func(t *testing.T) { + badPath := filepath.Join(tmpDir, "bad.yaml") + err := os.WriteFile(badPath, []byte("no frontmatter here"), 0600) + require.NoError(t, err) + + _, err = Load(badPath, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "missing YAML frontmatter") + }) +} + +func TestResolveWorkDir(t *testing.T) { + // Not using t.Parallel() because it might depend on HOME env which we might want to mock if needed. + + r := &Recipe{ + ResolvedParams: make(map[string]any), + } + runner := &Runner{Recipe: r} + + t.Run("default to dot", func(t *testing.T) { + dir := runner.resolveWorkDir() + absDot, _ := filepath.Abs(".") + assert.Equal(t, absDot, dir) + }) + + t.Run("explicit path", func(t *testing.T) { + tmpDir := t.TempDir() + r.ResolvedParams["package_path"] = tmpDir + dir := runner.resolveWorkDir() + absTmp, _ := filepath.Abs(tmpDir) + assert.Equal(t, absTmp, dir) + }) + + t.Run("tilde expansion", func(t *testing.T) { + r.ResolvedParams["package_path"] = "~/projects" + dir := runner.resolveWorkDir() + home, _ := os.UserHomeDir() + expected := filepath.Join(home, "projects") + assert.Equal(t, expected, dir) + }) +} + +func TestDiscoverFiles(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + + // Create some files + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte("func main() { if err != nil { return } }"), 0600)) + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "app.go"), []byte("package main"), 0600)) + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "README.md"), []byte("if err != nil"), 0600)) + + r := &Recipe{ + ProjectLanguages: []string{"go"}, + Extensions: map[string][]string{"go": {".go"}}, + SearchPattern: "if err != nil", + } + runner := &Runner{Recipe: r} + + t.Run("finds matching files", func(t *testing.T) { + files := runner.discoverFiles(tmpDir) + assert.Len(t, files, 1) + assert.Contains(t, files[0], "main.go") + }) + + t.Run("no matching files", func(t *testing.T) { + r.SearchPattern = "nonexistent" + files := runner.discoverFiles(tmpDir) + assert.Len(t, files, 1) + assert.Equal(t, "No files found matching the criteria.", files[0]) + }) +} + +func TestCreateUnifiedPatch(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + patchPath := filepath.Join(tmpDir, "test.patch") + + changes := []FileChange{ + { + File: "main.go", + Content: "package main\n\nfunc main() {}\n", + }, + } + + err := createUnifiedPatch(changes, patchPath) + require.NoError(t, err) + + content, err := os.ReadFile(patchPath) + require.NoError(t, err) + + assert.Contains(t, string(content), "--- main.go") + assert.Contains(t, string(content), "+++ main.go") + assert.Contains(t, string(content), "+package main") + assert.Contains(t, string(content), "+func main() {}") +}