From 393b76833eda239dcbc9c43d6044c97503122d7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:02:16 +0000 Subject: [PATCH 1/4] Initial plan From 5fbd9fbf6a39150dcb1c695a5152ee51431515e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:09:40 +0000 Subject: [PATCH 2/4] Add comprehensive test coverage for repver, git, and color packages Agent-Logs-Url: https://github.com/UnitVectorY-Labs/repver/sessions/a7cd52dc-d227-423a-b4d9-6c85cb71d5b3 Co-authored-by: JaredHatfield <208119+JaredHatfield@users.noreply.github.com> --- internal/color/color_test.go | 45 +++ internal/git/git_test.go | 538 +++++++++++++++++++++++++++++++ internal/repver/exec_test.go | 376 +++++++++++++++++++++ internal/repver/validate_test.go | 252 +++++++++++++++ 4 files changed, 1211 insertions(+) create mode 100644 internal/git/git_test.go diff --git a/internal/color/color_test.go b/internal/color/color_test.go index aea9dce..218178e 100644 --- a/internal/color/color_test.go +++ b/internal/color/color_test.go @@ -83,3 +83,48 @@ func TestDisabledFormatFunctions(t *testing.T) { t.Errorf("Boldf with Enabled=false = %q, want %q", got, want) } } + +func TestYellowfFormatFunction(t *testing.T) { + Enabled = true + + got := Yellowf("value: %d", 10) + want := "\033[33mvalue: 10\033[0m" + if got != want { + t.Errorf("Yellowf = %q, want %q", got, want) + } +} + +func TestCyanfFormatFunction(t *testing.T) { + Enabled = true + + got := Cyanf("path: %s", "/tmp") + want := "\033[36mpath: /tmp\033[0m" + if got != want { + t.Errorf("Cyanf = %q, want %q", got, want) + } +} + +func TestAllFormatFunctionsDisabled(t *testing.T) { + Enabled = false + defer func() { Enabled = true }() + + tests := []struct { + name string + got string + expected string + }{ + {"Redf", Redf("test %d", 1), "test 1"}, + {"Greenf", Greenf("test %s", "ok"), "test ok"}, + {"Yellowf", Yellowf("test %s", "warn"), "test warn"}, + {"Cyanf", Cyanf("test %s", "info"), "test info"}, + {"Boldf", Boldf("test %s", "bold"), "test bold"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.got != tc.expected { + t.Errorf("%s with Enabled=false = %q, want %q", tc.name, tc.got, tc.expected) + } + }) + } +} diff --git a/internal/git/git_test.go b/internal/git/git_test.go new file mode 100644 index 0000000..3d9d5dd --- /dev/null +++ b/internal/git/git_test.go @@ -0,0 +1,538 @@ +package git + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +// initTestRepo creates a new git repository in a temporary directory and returns +// the directory path. It sets up user config and makes an initial commit. +func initTestRepo(t *testing.T) string { + t.Helper() + tmpDir := t.TempDir() + + runGit(t, tmpDir, "init", "-b", "main") + runGit(t, tmpDir, "config", "user.name", "Test User") + runGit(t, tmpDir, "config", "user.email", "test@example.com") + + // Create an initial file and commit + filePath := filepath.Join(tmpDir, "README.md") + if err := os.WriteFile(filePath, []byte("# Test\n"), 0644); err != nil { + t.Fatal(err) + } + runGit(t, tmpDir, "add", ".") + runGit(t, tmpDir, "commit", "-m", "Initial commit") + + return tmpDir +} + +// runGit runs a git command in the given directory and fails the test on error. +func runGit(t *testing.T, dir string, args ...string) string { + t.Helper() + cmd := exec.Command("git", args...) + cmd.Dir = dir + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("git %v failed: %v\n%s", args, err, output) + } + return string(output) +} + +func TestIsGitRoot(t *testing.T) { + repoDir := initTestRepo(t) + + // Save and restore the working directory + origDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatal(err) + } + }() + + t.Run("at git root", func(t *testing.T) { + if err := os.Chdir(repoDir); err != nil { + t.Fatal(err) + } + + isRoot, err := IsGitRoot() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !isRoot { + t.Error("expected to be at git root") + } + }) + + t.Run("in subdirectory", func(t *testing.T) { + subDir := filepath.Join(repoDir, "subdir") + if err := os.Mkdir(subDir, 0755); err != nil { + t.Fatal(err) + } + if err := os.Chdir(subDir); err != nil { + t.Fatal(err) + } + + isRoot, err := IsGitRoot() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if isRoot { + t.Error("expected not to be at git root in subdirectory") + } + }) +} + +func TestIsGitRoot_NotARepo(t *testing.T) { + tmpDir := t.TempDir() + + origDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatal(err) + } + }() + + if err := os.Chdir(tmpDir); err != nil { + t.Fatal(err) + } + + _, err = IsGitRoot() + if err == nil { + t.Error("expected error when not in a git repo") + } +} + +func TestBranchExists(t *testing.T) { + repoDir := initTestRepo(t) + + origDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatal(err) + } + }() + if err := os.Chdir(repoDir); err != nil { + t.Fatal(err) + } + + t.Run("existing branch", func(t *testing.T) { + exists, err := BranchExists("main") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !exists { + t.Error("expected 'main' branch to exist") + } + }) + + t.Run("non-existing branch", func(t *testing.T) { + exists, err := BranchExists("nonexistent-branch") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if exists { + t.Error("expected 'nonexistent-branch' to not exist") + } + }) +} + +func TestGetCurrentBranch(t *testing.T) { + repoDir := initTestRepo(t) + + origDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatal(err) + } + }() + if err := os.Chdir(repoDir); err != nil { + t.Fatal(err) + } + + branch, err := GetCurrentBranch() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if branch != "main" { + t.Errorf("expected branch 'main', got %q", branch) + } +} + +func TestCreateAndSwitchBranch(t *testing.T) { + repoDir := initTestRepo(t) + + origDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatal(err) + } + }() + if err := os.Chdir(repoDir); err != nil { + t.Fatal(err) + } + + _, err = CreateAndSwitchBranch("feature-test") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + branch, err := GetCurrentBranch() + if err != nil { + t.Fatalf("unexpected error getting branch: %v", err) + } + if branch != "feature-test" { + t.Errorf("expected to be on branch 'feature-test', got %q", branch) + } + + exists, err := BranchExists("feature-test") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !exists { + t.Error("expected 'feature-test' branch to exist") + } +} + +func TestSwitchToBranch(t *testing.T) { + repoDir := initTestRepo(t) + + origDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatal(err) + } + }() + if err := os.Chdir(repoDir); err != nil { + t.Fatal(err) + } + + // Create another branch to switch to + runGit(t, repoDir, "checkout", "-b", "other-branch") + runGit(t, repoDir, "checkout", "main") + + _, err = SwitchToBranch("other-branch") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + branch, err := GetCurrentBranch() + if err != nil { + t.Fatal(err) + } + if branch != "other-branch" { + t.Errorf("expected to be on 'other-branch', got %q", branch) + } +} + +func TestSwitchToBranch_NonExistent(t *testing.T) { + repoDir := initTestRepo(t) + + origDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatal(err) + } + }() + if err := os.Chdir(repoDir); err != nil { + t.Fatal(err) + } + + _, err = SwitchToBranch("does-not-exist") + if err == nil { + t.Error("expected error when switching to non-existent branch") + } +} + +func TestCheckGitClean(t *testing.T) { + repoDir := initTestRepo(t) + + origDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatal(err) + } + }() + if err := os.Chdir(repoDir); err != nil { + t.Fatal(err) + } + + t.Run("clean repo", func(t *testing.T) { + err := CheckGitClean() + if err != nil { + t.Fatalf("expected clean repo, got error: %v", err) + } + }) + + t.Run("dirty repo", func(t *testing.T) { + // Create an untracked file to make the repo dirty + dirtyFile := filepath.Join(repoDir, "dirty.txt") + if err := os.WriteFile(dirtyFile, []byte("dirty"), 0644); err != nil { + t.Fatal(err) + } + + err := CheckGitClean() + if err == nil { + t.Error("expected error for dirty repo") + } + + // Clean up + os.Remove(dirtyFile) + }) +} + +func TestAddAndCommitFiles(t *testing.T) { + repoDir := initTestRepo(t) + + origDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatal(err) + } + }() + if err := os.Chdir(repoDir); err != nil { + t.Fatal(err) + } + + // Create a new file to commit + newFile := filepath.Join(repoDir, "newfile.txt") + if err := os.WriteFile(newFile, []byte("new content\n"), 0644); err != nil { + t.Fatal(err) + } + + output, err := AddAndCommitFiles([]string{"newfile.txt"}, "Add new file") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if output == "" { + t.Error("expected non-empty output") + } + + // Verify the commit happened + err = CheckGitClean() + if err != nil { + t.Fatalf("repo should be clean after commit, got: %v", err) + } + + // Verify commit message + logOutput := runGit(t, repoDir, "log", "--oneline", "-1") + if !strings.Contains(logOutput, "Add new file") { + t.Errorf("expected commit message in log, got: %s", logOutput) + } +} + +func TestAddAndCommitFiles_MultipleFiles(t *testing.T) { + repoDir := initTestRepo(t) + + origDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatal(err) + } + }() + if err := os.Chdir(repoDir); err != nil { + t.Fatal(err) + } + + // Create multiple files + for _, name := range []string{"file1.txt", "file2.txt"} { + if err := os.WriteFile(filepath.Join(repoDir, name), []byte(name+"\n"), 0644); err != nil { + t.Fatal(err) + } + } + + _, err = AddAndCommitFiles([]string{"file1.txt", "file2.txt"}, "Add two files") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + err = CheckGitClean() + if err != nil { + t.Fatalf("repo should be clean after commit, got: %v", err) + } +} + +func TestDeleteLocalBranch(t *testing.T) { + repoDir := initTestRepo(t) + + origDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatal(err) + } + }() + if err := os.Chdir(repoDir); err != nil { + t.Fatal(err) + } + + // Create a branch, switch back to main, then delete the branch + runGit(t, repoDir, "checkout", "-b", "to-delete") + runGit(t, repoDir, "checkout", "main") + + _, err = DeleteLocalBranch("to-delete") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + exists, err := BranchExists("to-delete") + if err != nil { + t.Fatal(err) + } + if exists { + t.Error("expected branch 'to-delete' to no longer exist") + } +} + +func TestDeleteLocalBranch_NonExistent(t *testing.T) { + repoDir := initTestRepo(t) + + origDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatal(err) + } + }() + if err := os.Chdir(repoDir); err != nil { + t.Fatal(err) + } + + _, err = DeleteLocalBranch("does-not-exist") + if err == nil { + t.Error("expected error when deleting non-existent branch") + } +} + +func TestPushChanges(t *testing.T) { + // Create a bare remote repo and a working repo that pushes to it + tmpDir := t.TempDir() + bareDir := filepath.Join(tmpDir, "bare.git") + workDir := filepath.Join(tmpDir, "work") + + // Create bare repo + cmd := exec.Command("git", "init", "--bare", bareDir) + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("failed to create bare repo: %v\n%s", err, output) + } + + // Create a working repo (not a clone, to control the branch name) + if err := os.MkdirAll(workDir, 0755); err != nil { + t.Fatal(err) + } + runGit(t, workDir, "init", "-b", "main") + runGit(t, workDir, "config", "user.name", "Test User") + runGit(t, workDir, "config", "user.email", "test@example.com") + runGit(t, workDir, "remote", "add", "origin", bareDir) + + // Create initial commit and push + initFile := filepath.Join(workDir, "init.txt") + if err := os.WriteFile(initFile, []byte("initial\n"), 0644); err != nil { + t.Fatal(err) + } + runGit(t, workDir, "add", ".") + runGit(t, workDir, "commit", "-m", "Initial commit") + runGit(t, workDir, "push", "-u", "origin", "main") + + // Create a new commit to push via the PushChanges function + newFile := filepath.Join(workDir, "new.txt") + if err := os.WriteFile(newFile, []byte("new content\n"), 0644); err != nil { + t.Fatal(err) + } + runGit(t, workDir, "add", ".") + runGit(t, workDir, "commit", "-m", "New commit") + + // Save and restore working directory + origDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatal(err) + } + }() + if err := os.Chdir(workDir); err != nil { + t.Fatal(err) + } + + _, err = PushChanges("origin", "main") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestPushChanges_InvalidRemote(t *testing.T) { + repoDir := initTestRepo(t) + + origDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatal(err) + } + }() + if err := os.Chdir(repoDir); err != nil { + t.Fatal(err) + } + + _, err = PushChanges("nonexistent-remote", "main") + if err == nil { + t.Error("expected error when pushing to non-existent remote") + } +} + +func TestCreateGitHubPullRequest_NoGH(t *testing.T) { + // This test verifies error handling when gh CLI is not available or + // not configured. In CI/test environments, gh is typically not authenticated. + _, err := CreateGitHubPullRequest() + if err == nil { + // If gh is available and authenticated, the call might succeed (unlikely in tests), + // but we still want to exercise the code path. + t.Skip("gh CLI appears to be available and configured; skipping error test") + } + // Verify we get a meaningful error + if err != nil { + if !strings.Contains(err.Error(), "error creating GitHub pull request") { + t.Errorf("expected error about creating PR, got: %v", err) + } + } +} diff --git a/internal/repver/exec_test.go b/internal/repver/exec_test.go index 152c229..2172d37 100644 --- a/internal/repver/exec_test.go +++ b/internal/repver/exec_test.go @@ -3,6 +3,7 @@ package repver import ( "os" "path/filepath" + "strings" "testing" ) @@ -45,3 +46,378 @@ func TestExecutionPlanNoOp(t *testing.T) { t.Fatalf("file was modified unexpectedly: %q", string(content)) } } + +func TestPlanWithModification(t *testing.T) { + tmpDir := t.TempDir() + targetPath := filepath.Join(tmpDir, "version.txt") + if err := os.WriteFile(targetPath, []byte("version: 1.0.0\n"), 0644); err != nil { + t.Fatal(err) + } + + target := RepverTarget{ + Path: targetPath, + Pattern: `^version: (?P.*)$`, + } + + plan, err := target.Plan(map[string]string{"version": "2.0.0"}, nil) + if err != nil { + t.Fatalf("Plan returned error: %v", err) + } + if plan == nil { + t.Fatal("expected execution plan") + } + if !plan.Modified { + t.Fatal("expected changes to be planned") + } + if len(plan.Changes) != 1 { + t.Fatalf("expected 1 change, got %d", len(plan.Changes)) + } + if plan.Changes[0].OldLine != "version: 1.0.0" { + t.Errorf("expected old line 'version: 1.0.0', got %q", plan.Changes[0].OldLine) + } + if plan.Changes[0].NewLine != "version: 2.0.0" { + t.Errorf("expected new line 'version: 2.0.0', got %q", plan.Changes[0].NewLine) + } + if plan.Changes[0].LineNumber != 1 { + t.Errorf("expected line number 1, got %d", plan.Changes[0].LineNumber) + } +} + +func TestPlanWithMultipleMatches(t *testing.T) { + tmpDir := t.TempDir() + targetPath := filepath.Join(tmpDir, "multi.txt") + content := "version: 1.0.0\nother line\nversion: 1.0.0\n" + if err := os.WriteFile(targetPath, []byte(content), 0644); err != nil { + t.Fatal(err) + } + + target := RepverTarget{ + Path: targetPath, + Pattern: `^version: (?P.*)$`, + } + + plan, err := target.Plan(map[string]string{"version": "2.0.0"}, nil) + if err != nil { + t.Fatalf("Plan returned error: %v", err) + } + if !plan.Modified { + t.Fatal("expected changes to be planned") + } + if len(plan.Changes) != 2 { + t.Fatalf("expected 2 changes, got %d", len(plan.Changes)) + } + if plan.Changes[0].LineNumber != 1 { + t.Errorf("expected first change on line 1, got %d", plan.Changes[0].LineNumber) + } + if plan.Changes[1].LineNumber != 3 { + t.Errorf("expected second change on line 3, got %d", plan.Changes[1].LineNumber) + } +} + +func TestPlanWithTransform(t *testing.T) { + tmpDir := t.TempDir() + targetPath := filepath.Join(tmpDir, "version.txt") + if err := os.WriteFile(targetPath, []byte("go 1.20\n"), 0644); err != nil { + t.Fatal(err) + } + + target := RepverTarget{ + Path: targetPath, + Pattern: `^go (?P.*)$`, + Transform: "{{major}}.{{minor}}", + } + + extractedGroups := map[string]string{ + "major": "1", + "minor": "26", + } + + plan, err := target.Plan(map[string]string{"gover": "1.26"}, extractedGroups) + if err != nil { + t.Fatalf("Plan returned error: %v", err) + } + if !plan.Modified { + t.Fatal("expected changes to be planned") + } + if len(plan.Changes) != 1 { + t.Fatalf("expected 1 change, got %d", len(plan.Changes)) + } + if plan.Changes[0].NewLine != "go 1.26" { + t.Errorf("expected new line 'go 1.26', got %q", plan.Changes[0].NewLine) + } +} + +func TestPlanNoMatchingLines(t *testing.T) { + tmpDir := t.TempDir() + targetPath := filepath.Join(tmpDir, "noop.txt") + if err := os.WriteFile(targetPath, []byte("no matching lines here\n"), 0644); err != nil { + t.Fatal(err) + } + + target := RepverTarget{ + Path: targetPath, + Pattern: `^version: (?P.*)$`, + } + + plan, err := target.Plan(map[string]string{"version": "1.0.0"}, nil) + if err != nil { + t.Fatalf("Plan returned error: %v", err) + } + if plan.Modified { + t.Fatal("expected no modification when no lines match") + } +} + +func TestPlanInvalidPattern(t *testing.T) { + tmpDir := t.TempDir() + targetPath := filepath.Join(tmpDir, "test.txt") + if err := os.WriteFile(targetPath, []byte("test\n"), 0644); err != nil { + t.Fatal(err) + } + + target := RepverTarget{ + Path: targetPath, + Pattern: `^(?P[invalid$`, + } + + _, err := target.Plan(map[string]string{"v": "test"}, nil) + if err == nil { + t.Fatal("expected error for invalid pattern") + } +} + +func TestPlanFileNotFound(t *testing.T) { + target := RepverTarget{ + Path: "/nonexistent/path/file.txt", + Pattern: `^(?P.*)$`, + } + + _, err := target.Plan(map[string]string{"v": "test"}, nil) + if err == nil { + t.Fatal("expected error for missing file") + } +} + +func TestExecutePlanNil(t *testing.T) { + target := RepverTarget{ + Path: "file.txt", + Pattern: `^.*$`, + } + + _, err := target.ExecutePlan(nil) + if err == nil { + t.Fatal("expected error for nil plan") + } +} + +func TestExecutePlanWriteFile(t *testing.T) { + // Save and restore DryRun state + origDryRun := DryRun + defer func() { DryRun = origDryRun }() + DryRun = false + + tmpDir := t.TempDir() + targetPath := filepath.Join(tmpDir, "version.txt") + if err := os.WriteFile(targetPath, []byte("version: 1.0.0\n"), 0644); err != nil { + t.Fatal(err) + } + + target := RepverTarget{ + Path: targetPath, + Pattern: `^version: (?P.*)$`, + } + + plan, err := target.Plan(map[string]string{"version": "2.0.0"}, nil) + if err != nil { + t.Fatalf("Plan returned error: %v", err) + } + + modified, err := target.ExecutePlan(plan) + if err != nil { + t.Fatalf("ExecutePlan returned error: %v", err) + } + if !modified { + t.Fatal("expected file to be modified") + } + + content, err := os.ReadFile(targetPath) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(content), "version: 2.0.0") { + t.Fatalf("expected file to contain 'version: 2.0.0', got %q", string(content)) + } +} + +func TestExecutePlanDryRun(t *testing.T) { + // Save and restore DryRun state + origDryRun := DryRun + defer func() { DryRun = origDryRun }() + DryRun = true + + tmpDir := t.TempDir() + targetPath := filepath.Join(tmpDir, "version.txt") + originalContent := "version: 1.0.0\n" + if err := os.WriteFile(targetPath, []byte(originalContent), 0644); err != nil { + t.Fatal(err) + } + + target := RepverTarget{ + Path: targetPath, + Pattern: `^version: (?P.*)$`, + } + + plan, err := target.Plan(map[string]string{"version": "2.0.0"}, nil) + if err != nil { + t.Fatalf("Plan returned error: %v", err) + } + + modified, err := target.ExecutePlan(plan) + if err != nil { + t.Fatalf("ExecutePlan returned error: %v", err) + } + if !modified { + t.Fatal("expected dry run to report modification") + } + + // Verify file was NOT actually modified + content, err := os.ReadFile(targetPath) + if err != nil { + t.Fatal(err) + } + if string(content) != originalContent { + t.Fatalf("expected file to remain unchanged in dry run, got %q", string(content)) + } +} + +func TestExecuteConvenienceMethod(t *testing.T) { + // Save and restore DryRun state + origDryRun := DryRun + defer func() { DryRun = origDryRun }() + DryRun = false + + tmpDir := t.TempDir() + targetPath := filepath.Join(tmpDir, "version.txt") + if err := os.WriteFile(targetPath, []byte("version: 1.0.0\n"), 0644); err != nil { + t.Fatal(err) + } + + target := RepverTarget{ + Path: targetPath, + Pattern: `^version: (?P.*)$`, + } + + modified, err := target.Execute(map[string]string{"version": "3.0.0"}, nil) + if err != nil { + t.Fatalf("Execute returned error: %v", err) + } + if !modified { + t.Fatal("expected file to be modified") + } + + content, err := os.ReadFile(targetPath) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(content), "version: 3.0.0") { + t.Fatalf("expected file to contain 'version: 3.0.0', got %q", string(content)) + } +} + +func TestExecuteNoOp(t *testing.T) { + tmpDir := t.TempDir() + targetPath := filepath.Join(tmpDir, "version.txt") + if err := os.WriteFile(targetPath, []byte("version: 1.0.0\n"), 0644); err != nil { + t.Fatal(err) + } + + target := RepverTarget{ + Path: targetPath, + Pattern: `^version: (?P.*)$`, + } + + modified, err := target.Execute(map[string]string{"version": "1.0.0"}, nil) + if err != nil { + t.Fatalf("Execute returned error: %v", err) + } + if modified { + t.Fatal("expected no modification when value is the same") + } +} + +func TestPlanPreservesFinalNewline(t *testing.T) { + tmpDir := t.TempDir() + targetPath := filepath.Join(tmpDir, "version.txt") + // File with trailing newline + if err := os.WriteFile(targetPath, []byte("version: 1.0.0\n"), 0644); err != nil { + t.Fatal(err) + } + + target := RepverTarget{ + Path: targetPath, + Pattern: `^version: (?P.*)$`, + } + + plan, err := target.Plan(map[string]string{"version": "2.0.0"}, nil) + if err != nil { + t.Fatalf("Plan returned error: %v", err) + } + if !strings.HasSuffix(plan.ModifiedContent, "\n") { + t.Error("expected modified content to end with newline") + } +} + +func TestPlanFileWithoutTrailingNewline(t *testing.T) { + tmpDir := t.TempDir() + targetPath := filepath.Join(tmpDir, "version.txt") + // File without trailing newline + if err := os.WriteFile(targetPath, []byte("version: 1.0.0"), 0644); err != nil { + t.Fatal(err) + } + + target := RepverTarget{ + Path: targetPath, + Pattern: `^version: (?P.*)$`, + } + + plan, err := target.Plan(map[string]string{"version": "2.0.0"}, nil) + if err != nil { + t.Fatalf("Plan returned error: %v", err) + } + if strings.HasSuffix(plan.ModifiedContent, "\n") { + t.Error("expected modified content to NOT end with newline when original didn't") + } +} + +func TestPlanMultipleLinesUnchanged(t *testing.T) { + tmpDir := t.TempDir() + targetPath := filepath.Join(tmpDir, "config.txt") + content := "line one\nversion: 1.0.0\nline three\n" + if err := os.WriteFile(targetPath, []byte(content), 0644); err != nil { + t.Fatal(err) + } + + target := RepverTarget{ + Path: targetPath, + Pattern: `^version: (?P.*)$`, + } + + plan, err := target.Plan(map[string]string{"version": "2.0.0"}, nil) + if err != nil { + t.Fatalf("Plan returned error: %v", err) + } + if !plan.Modified { + t.Fatal("expected changes") + } + + // Verify non-matching lines are preserved + if !strings.Contains(plan.ModifiedContent, "line one") { + t.Error("expected 'line one' to be preserved") + } + if !strings.Contains(plan.ModifiedContent, "line three") { + t.Error("expected 'line three' to be preserved") + } + if !strings.Contains(plan.ModifiedContent, "version: 2.0.0") { + t.Error("expected 'version: 2.0.0' in modified content") + } +} diff --git a/internal/repver/validate_test.go b/internal/repver/validate_test.go index f42061d..76b2331 100644 --- a/internal/repver/validate_test.go +++ b/internal/repver/validate_test.go @@ -327,6 +327,258 @@ func TestParamValidateValue(t *testing.T) { } } +func TestValidateConfig(t *testing.T) { + tests := []struct { + name string + config RepverConfig + valid bool + }{ + { + "empty config", + RepverConfig{}, + true, + }, + { + "valid single command", + RepverConfig{ + Commands: []RepverCommand{ + { + Name: "test", + Targets: []RepverTarget{ + {Path: "validate_test.go", Pattern: `^(?P.*)$`}, + }, + }, + }, + }, + true, + }, + { + "duplicate command names", + RepverConfig{ + Commands: []RepverCommand{ + { + Name: "test", + Targets: []RepverTarget{ + {Path: "validate_test.go", Pattern: `^(?P.*)$`}, + }, + }, + { + Name: "test", + Targets: []RepverTarget{ + {Path: "validate_test.go", Pattern: `^(?P.*)$`}, + }, + }, + }, + }, + false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := tc.config.Validate() + if (err == nil) != tc.valid { + t.Errorf("expected valid: %v, got error: %v", tc.valid, err) + } + }) + } +} + +func TestValidateGitOptions(t *testing.T) { + tests := []struct { + name string + git RepverGit + valid bool + }{ + { + "empty git options", + RepverGit{}, + true, + }, + { + "create branch with name", + RepverGit{CreateBranch: true, BranchName: "feature"}, + true, + }, + { + "create branch without name", + RepverGit{CreateBranch: true}, + false, + }, + { + "delete branch without create", + RepverGit{DeleteBranch: true}, + false, + }, + { + "commit with message", + RepverGit{Commit: true, CommitMessage: "test"}, + true, + }, + { + "commit without message", + RepverGit{Commit: true}, + false, + }, + { + "push with remote", + RepverGit{Push: true, Remote: "origin"}, + true, + }, + { + "push without remote", + RepverGit{Push: true}, + false, + }, + { + "return to original without create", + RepverGit{ReturnToOriginalBranch: true}, + false, + }, + { + "return to original with create", + RepverGit{CreateBranch: true, BranchName: "test", ReturnToOriginalBranch: true}, + true, + }, + { + "valid pull request GITHUB_CLI", + RepverGit{PullRequest: "GITHUB_CLI"}, + true, + }, + { + "valid pull request NO", + RepverGit{PullRequest: "NO"}, + true, + }, + { + "invalid pull request value", + RepverGit{PullRequest: "INVALID"}, + false, + }, + { + "all options set correctly", + RepverGit{ + CreateBranch: true, + BranchName: "release", + DeleteBranch: true, + Commit: true, + CommitMessage: "release", + Push: true, + Remote: "origin", + PullRequest: "GITHUB_CLI", + ReturnToOriginalBranch: true, + }, + true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := tc.git.Validate() + if (err == nil) != tc.valid { + t.Errorf("expected valid: %v, got error: %v", tc.valid, err) + } + }) + } +} + +func TestValidateTarget(t *testing.T) { + tests := []struct { + name string + target RepverTarget + valid bool + }{ + { + "empty path", + RepverTarget{Path: "", Pattern: `^.*$`}, + false, + }, + { + "valid target", + RepverTarget{Path: "validate_test.go", Pattern: `^(?P.*)$`}, + true, + }, + { + "non-existent file", + RepverTarget{Path: "nonexistent_file_abc123.txt", Pattern: `^(?P.*)$`}, + false, + }, + { + "empty pattern", + RepverTarget{Path: "validate_test.go", Pattern: ""}, + false, + }, + { + "invalid pattern", + RepverTarget{Path: "validate_test.go", Pattern: `^[invalid$`}, + false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := tc.target.Validate() + if (err == nil) != tc.valid { + t.Errorf("expected valid: %v, got error: %v", tc.valid, err) + } + }) + } +} + +func TestValidateCommand(t *testing.T) { + tests := []struct { + name string + command RepverCommand + valid bool + }{ + { + "empty name", + RepverCommand{Name: ""}, + false, + }, + { + "valid command no targets", + RepverCommand{Name: "test"}, + true, + }, + { + "valid command with target", + RepverCommand{ + Name: "test", + Targets: []RepverTarget{ + {Path: "validate_test.go", Pattern: `^(?P.*)$`}, + }, + }, + true, + }, + { + "duplicate param names", + RepverCommand{ + Name: "test", + Params: []RepverParam{ + {Name: "version", Pattern: `^.*$`}, + {Name: "version", Pattern: `^.*$`}, + }, + }, + false, + }, + { + "invalid command name", + RepverCommand{Name: "invalid-name"}, + false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := tc.command.Validate() + if (err == nil) != tc.valid { + t.Errorf("expected valid: %v, got error: %v", tc.valid, err) + } + }) + } +} + func TestApplyTransform(t *testing.T) { tests := []struct { name string From e1745e30ac8c50b837b682b53b8a092d072f167c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:10:43 +0000 Subject: [PATCH 3/4] Add missing test files for config, load, and params packages Agent-Logs-Url: https://github.com/UnitVectorY-Labs/repver/sessions/a7cd52dc-d227-423a-b4d9-6c85cb71d5b3 Co-authored-by: JaredHatfield <208119+JaredHatfield@users.noreply.github.com> --- internal/repver/config_test.go | 529 +++++++++++++++++++++++++++++++++ internal/repver/load_test.go | 201 +++++++++++++ internal/repver/params_test.go | 75 +++++ 3 files changed, 805 insertions(+) create mode 100644 internal/repver/config_test.go create mode 100644 internal/repver/load_test.go create mode 100644 internal/repver/params_test.go diff --git a/internal/repver/config_test.go b/internal/repver/config_test.go new file mode 100644 index 0000000..6f8f584 --- /dev/null +++ b/internal/repver/config_test.go @@ -0,0 +1,529 @@ +package repver + +import ( + "testing" +) + +func TestGetCommand_Found(t *testing.T) { + config := &RepverConfig{ + Commands: []RepverCommand{ + {Name: "alpha"}, + {Name: "beta"}, + {Name: "gamma"}, + }, + } + + cmd, err := config.GetCommand("beta") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cmd == nil { + t.Fatal("expected non-nil command") + } + if cmd.Name != "beta" { + t.Errorf("expected command name 'beta', got %q", cmd.Name) + } +} + +func TestGetCommand_NotFound(t *testing.T) { + config := &RepverConfig{ + Commands: []RepverCommand{ + {Name: "alpha"}, + }, + } + + cmd, err := config.GetCommand("nonexistent") + if err == nil { + t.Fatal("expected error for missing command") + } + if cmd != nil { + t.Fatal("expected nil command for missing command") + } +} + +func TestGetCommand_EmptyCommands(t *testing.T) { + config := &RepverConfig{ + Commands: []RepverCommand{}, + } + + cmd, err := config.GetCommand("any") + if err == nil { + t.Fatal("expected error for empty commands") + } + if cmd != nil { + t.Fatal("expected nil command for empty commands") + } +} + +func TestConfigGetParameterNames(t *testing.T) { + config := &RepverConfig{ + Commands: []RepverCommand{ + { + Name: "cmd1", + Targets: []RepverTarget{ + {Path: "a.txt", Pattern: `^(?P.*)$`}, + }, + }, + { + Name: "cmd2", + Targets: []RepverTarget{ + {Path: "b.txt", Pattern: `^(?P.*)$`}, + }, + }, + }, + } + + names, err := config.GetParameterNames() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + nameSet := make(map[string]bool) + for _, n := range names { + nameSet[n] = true + } + + if !nameSet["version"] { + t.Error("expected 'version' in parameter names") + } + if !nameSet["name"] { + t.Error("expected 'name' in parameter names") + } + if len(names) != 2 { + t.Errorf("expected 2 unique parameter names, got %d", len(names)) + } +} + +func TestConfigGetParameterNames_Dedup(t *testing.T) { + config := &RepverConfig{ + Commands: []RepverCommand{ + { + Name: "cmd1", + Targets: []RepverTarget{ + {Path: "a.txt", Pattern: `^(?P.*)$`}, + }, + }, + { + Name: "cmd2", + Targets: []RepverTarget{ + {Path: "b.txt", Pattern: `^(?P.*)$`}, + }, + }, + }, + } + + names, err := config.GetParameterNames() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(names) != 1 { + t.Errorf("expected 1 unique parameter name after dedup, got %d", len(names)) + } +} + +func TestConfigGetParameterNames_InvalidPattern(t *testing.T) { + config := &RepverConfig{ + Commands: []RepverCommand{ + { + Name: "cmd1", + Targets: []RepverTarget{ + {Path: "a.txt", Pattern: `^(?P[invalid$`}, + }, + }, + }, + } + + _, err := config.GetParameterNames() + if err == nil { + t.Fatal("expected error for invalid pattern") + } +} + +func TestCommandGetParameterNames(t *testing.T) { + cmd := &RepverCommand{ + Name: "test", + Targets: []RepverTarget{ + {Path: "a.txt", Pattern: `^(?P\d+)\.(?P\d+)$`}, + }, + } + + names, err := cmd.GetParameterNames() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + nameSet := make(map[string]bool) + for _, n := range names { + nameSet[n] = true + } + + if !nameSet["major"] { + t.Error("expected 'major' in parameter names") + } + if !nameSet["minor"] { + t.Error("expected 'minor' in parameter names") + } +} + +func TestCommandGetParameterNames_Dedup(t *testing.T) { + cmd := &RepverCommand{ + Name: "test", + Targets: []RepverTarget{ + {Path: "a.txt", Pattern: `^(?P.*)$`}, + {Path: "b.txt", Pattern: `^(?P.*)$`}, + }, + } + + names, err := cmd.GetParameterNames() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(names) != 1 { + t.Errorf("expected 1 unique parameter name after dedup, got %d", len(names)) + } +} + +func TestTargetGetParameterNames(t *testing.T) { + target := &RepverTarget{ + Path: "file.txt", + Pattern: `^(?P\d+)\.(?P\d+)\.(?P\d+)$`, + } + + names, err := target.GetParameterNames() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(names) != 3 { + t.Fatalf("expected 3 parameter names, got %d", len(names)) + } + + expected := map[string]bool{"major": false, "minor": false, "patch": false} + for _, n := range names { + expected[n] = true + } + for k, found := range expected { + if !found { + t.Errorf("expected parameter name %q not found", k) + } + } +} + +func TestTargetGetParameterNames_NoGroups(t *testing.T) { + target := &RepverTarget{ + Path: "file.txt", + Pattern: `^abc$`, + } + + names, err := target.GetParameterNames() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(names) != 0 { + t.Errorf("expected 0 parameter names, got %d", len(names)) + } +} + +func TestTargetGetParameterNames_InvalidPattern(t *testing.T) { + target := &RepverTarget{ + Path: "file.txt", + Pattern: `^(?P[invalid$`, + } + + _, err := target.GetParameterNames() + if err == nil { + t.Fatal("expected error for invalid pattern") + } +} + +func TestBuildBranchName(t *testing.T) { + tests := []struct { + name string + branchName string + vals map[string]string + expected string + }{ + { + "single placeholder", + "release-{{version}}", + map[string]string{"version": "1.2.3"}, + "release-1.2.3", + }, + { + "multiple placeholders", + "{{project}}-release-{{version}}", + map[string]string{"project": "myapp", "version": "2.0.0"}, + "myapp-release-2.0.0", + }, + { + "no placeholders", + "static-branch", + map[string]string{"version": "1.0"}, + "static-branch", + }, + { + "empty vals", + "release-{{version}}", + map[string]string{}, + "release-{{version}}", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + git := &RepverGit{BranchName: tc.branchName} + result := git.BuildBranchName(tc.vals) + if result != tc.expected { + t.Errorf("expected %q, got %q", tc.expected, result) + } + }) + } +} + +func TestBuildCommitMessage(t *testing.T) { + tests := []struct { + name string + commitMessage string + vals map[string]string + expected string + }{ + { + "single placeholder", + "Update to {{version}}", + map[string]string{"version": "3.0.0"}, + "Update to 3.0.0", + }, + { + "multiple placeholders", + "Update {{name}} to {{version}}", + map[string]string{"name": "app", "version": "1.0"}, + "Update app to 1.0", + }, + { + "no placeholders", + "Static commit message", + map[string]string{"version": "1.0"}, + "Static commit message", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + git := &RepverGit{CommitMessage: tc.commitMessage} + result := git.BuildCommitMessage(tc.vals) + if result != tc.expected { + t.Errorf("expected %q, got %q", tc.expected, result) + } + }) + } +} + +func TestGitOptionsSpecified(t *testing.T) { + tests := []struct { + name string + git RepverGit + expected bool + }{ + {"no options", RepverGit{}, false}, + {"create branch", RepverGit{CreateBranch: true}, true}, + {"delete branch", RepverGit{DeleteBranch: true}, true}, + {"commit", RepverGit{Commit: true}, true}, + {"push", RepverGit{Push: true}, true}, + {"return to original", RepverGit{ReturnToOriginalBranch: true}, true}, + {"multiple options", RepverGit{CreateBranch: true, Commit: true, Push: true}, true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := tc.git.GitOptionsSpecified() + if result != tc.expected { + t.Errorf("expected %v, got %v", tc.expected, result) + } + }) + } +} + +func TestGetParam(t *testing.T) { + cmd := &RepverCommand{ + Name: "test", + Params: []RepverParam{ + {Name: "version", Pattern: `^.*$`}, + {Name: "name", Pattern: `^[a-z]+$`}, + }, + } + + t.Run("found", func(t *testing.T) { + param := cmd.GetParam("version") + if param == nil { + t.Fatal("expected non-nil param") + } + if param.Name != "version" { + t.Errorf("expected param name 'version', got %q", param.Name) + } + }) + + t.Run("not found", func(t *testing.T) { + param := cmd.GetParam("nonexistent") + if param != nil { + t.Fatal("expected nil param for nonexistent name") + } + }) + + t.Run("empty params", func(t *testing.T) { + emptyCmd := &RepverCommand{Name: "empty"} + param := emptyCmd.GetParam("any") + if param != nil { + t.Fatal("expected nil param from empty params list") + } + }) +} + +func TestExtractNamedGroups(t *testing.T) { + tests := []struct { + name string + param RepverParam + value string + expectedGroups map[string]string + shouldError bool + }{ + { + "semver extraction", + RepverParam{Name: "version", Pattern: `^(?P\d+)\.(?P\d+)\.(?P\d+)$`}, + "1.2.3", + map[string]string{"major": "1", "minor": "2", "patch": "3"}, + false, + }, + { + "no match", + RepverParam{Name: "version", Pattern: `^(?P\d+)$`}, + "abc", + nil, + true, + }, + { + "invalid pattern", + RepverParam{Name: "bad", Pattern: `^(?P[invalid$`}, + "test", + nil, + true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + groups, err := tc.param.ExtractNamedGroups(tc.value) + if tc.shouldError { + if err == nil { + t.Fatal("expected error") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + for k, v := range tc.expectedGroups { + if groups[k] != v { + t.Errorf("expected group %q = %q, got %q", k, v, groups[k]) + } + } + }) + } +} + +func TestValidateValue(t *testing.T) { + tests := []struct { + name string + param RepverParam + value string + shouldError bool + }{ + { + "valid match", + RepverParam{Name: "v", Pattern: `^[0-9]+$`}, + "123", + false, + }, + { + "no match", + RepverParam{Name: "v", Pattern: `^[0-9]+$`}, + "abc", + true, + }, + { + "invalid pattern", + RepverParam{Name: "v", Pattern: `^[invalid$`}, + "test", + true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := tc.param.ValidateValue(tc.value) + if tc.shouldError && err == nil { + t.Error("expected error") + } else if !tc.shouldError && err != nil { + t.Errorf("unexpected error: %v", err) + } + }) + } +} + +func TestGetTransformParamNames(t *testing.T) { + tests := []struct { + name string + transform string + expected []string + }{ + { + "single placeholder", + "{{major}}", + []string{"major"}, + }, + { + "multiple placeholders", + "{{major}}.{{minor}}.{{patch}}", + []string{"major", "minor", "patch"}, + }, + { + "duplicate placeholders", + "{{major}}-{{major}}", + []string{"major"}, + }, + { + "no placeholders", + "static", + nil, + }, + { + "empty transform", + "", + nil, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + target := &RepverTarget{ + Path: "file.txt", + Pattern: `^.*$`, + Transform: tc.transform, + } + + names := target.GetTransformParamNames() + if tc.expected == nil { + if names != nil { + t.Errorf("expected nil, got %v", names) + } + return + } + if len(names) != len(tc.expected) { + t.Fatalf("expected %d names, got %d: %v", len(tc.expected), len(names), names) + } + for i, expected := range tc.expected { + if names[i] != expected { + t.Errorf("expected names[%d] = %q, got %q", i, expected, names[i]) + } + } + }) + } +} diff --git a/internal/repver/load_test.go b/internal/repver/load_test.go new file mode 100644 index 0000000..714db2e --- /dev/null +++ b/internal/repver/load_test.go @@ -0,0 +1,201 @@ +package repver + +import ( + "os" + "path/filepath" + "testing" +) + +func TestParseValidYAML(t *testing.T) { + yamlContent := `commands: + - name: "goversion" + targets: + - path: "version.txt" + pattern: "^version: (?P.*)$" +` + config, err := Parse(yamlContent) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if config == nil { + t.Fatal("expected non-nil config") + } + if len(config.Commands) != 1 { + t.Fatalf("expected 1 command, got %d", len(config.Commands)) + } + if config.Commands[0].Name != "goversion" { + t.Errorf("expected command name 'goversion', got %q", config.Commands[0].Name) + } + if len(config.Commands[0].Targets) != 1 { + t.Fatalf("expected 1 target, got %d", len(config.Commands[0].Targets)) + } + if config.Commands[0].Targets[0].Path != "version.txt" { + t.Errorf("expected target path 'version.txt', got %q", config.Commands[0].Targets[0].Path) + } +} + +func TestParseMultipleCommands(t *testing.T) { + yamlContent := `commands: + - name: "cmd1" + targets: + - path: "a.txt" + pattern: "^(?P.*)$" + - name: "cmd2" + targets: + - path: "b.txt" + pattern: "^(?P.*)$" +` + config, err := Parse(yamlContent) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(config.Commands) != 2 { + t.Fatalf("expected 2 commands, got %d", len(config.Commands)) + } + if config.Commands[0].Name != "cmd1" { + t.Errorf("expected first command name 'cmd1', got %q", config.Commands[0].Name) + } + if config.Commands[1].Name != "cmd2" { + t.Errorf("expected second command name 'cmd2', got %q", config.Commands[1].Name) + } +} + +func TestParseWithParams(t *testing.T) { + yamlContent := `commands: + - name: "test" + params: + - name: "version" + pattern: "^(?P\\d+)\\.(?P\\d+)$" + targets: + - path: "file.txt" + pattern: "^v(?P.*)$" + transform: "{{major}}.{{minor}}" +` + config, err := Parse(yamlContent) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + cmd := config.Commands[0] + if len(cmd.Params) != 1 { + t.Fatalf("expected 1 param, got %d", len(cmd.Params)) + } + if cmd.Params[0].Name != "version" { + t.Errorf("expected param name 'version', got %q", cmd.Params[0].Name) + } + if cmd.Targets[0].Transform != "{{major}}.{{minor}}" { + t.Errorf("expected transform '{{major}}.{{minor}}', got %q", cmd.Targets[0].Transform) + } +} + +func TestParseWithGitOptions(t *testing.T) { + yamlContent := `commands: + - name: "release" + targets: + - path: "ver.txt" + pattern: "^(?P.*)$" + git: + create_branch: true + branch_name: "release-{{v}}" + commit: true + commit_message: "Release {{v}}" + push: true + remote: "origin" + pull_request: "GITHUB_CLI" + return_to_original_branch: true + delete_branch: true +` + config, err := Parse(yamlContent) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + git := config.Commands[0].GitOptions + if !git.CreateBranch { + t.Error("expected CreateBranch to be true") + } + if git.BranchName != "release-{{v}}" { + t.Errorf("expected BranchName 'release-{{v}}', got %q", git.BranchName) + } + if !git.Commit { + t.Error("expected Commit to be true") + } + if git.CommitMessage != "Release {{v}}" { + t.Errorf("expected CommitMessage 'Release {{v}}', got %q", git.CommitMessage) + } + if !git.Push { + t.Error("expected Push to be true") + } + if git.Remote != "origin" { + t.Errorf("expected Remote 'origin', got %q", git.Remote) + } + if git.PullRequest != "GITHUB_CLI" { + t.Errorf("expected PullRequest 'GITHUB_CLI', got %q", git.PullRequest) + } + if !git.ReturnToOriginalBranch { + t.Error("expected ReturnToOriginalBranch to be true") + } + if !git.DeleteBranch { + t.Error("expected DeleteBranch to be true") + } +} + +func TestParseInvalidYAML(t *testing.T) { + yamlContent := `this is not valid yaml: {{{` + config, err := Parse(yamlContent) + if err == nil { + t.Fatal("expected error for invalid YAML") + } + if config != nil { + t.Fatal("expected nil config for invalid YAML") + } +} + +func TestParseEmptyYAML(t *testing.T) { + config, err := Parse("") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if config == nil { + t.Fatal("expected non-nil config for empty YAML") + } + if len(config.Commands) != 0 { + t.Errorf("expected 0 commands for empty YAML, got %d", len(config.Commands)) + } +} + +func TestLoadFromFile(t *testing.T) { + tmpDir := t.TempDir() + filePath := filepath.Join(tmpDir, ".repver") + yamlContent := `commands: + - name: "loadtest" + targets: + - path: "test.txt" + pattern: "^(?P.*)$" +` + if err := os.WriteFile(filePath, []byte(yamlContent), 0644); err != nil { + t.Fatal(err) + } + + config, err := Load(filePath) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if config == nil { + t.Fatal("expected non-nil config") + } + if len(config.Commands) != 1 { + t.Fatalf("expected 1 command, got %d", len(config.Commands)) + } + if config.Commands[0].Name != "loadtest" { + t.Errorf("expected command name 'loadtest', got %q", config.Commands[0].Name) + } +} + +func TestLoadFileNotFound(t *testing.T) { + config, err := Load("/nonexistent/path/.repver") + if err == nil { + t.Fatal("expected error for non-existent file") + } + if config != nil { + t.Fatal("expected nil config for non-existent file") + } +} diff --git a/internal/repver/params_test.go b/internal/repver/params_test.go new file mode 100644 index 0000000..8a5dee7 --- /dev/null +++ b/internal/repver/params_test.go @@ -0,0 +1,75 @@ +package repver + +import ( + "bytes" + "os" + "testing" +) + +func TestDebuglnEnabled(t *testing.T) { + // Save original state + origDebug := Debug + defer func() { Debug = origDebug }() + + Debug = true + + // Capture stderr + origStderr := os.Stderr + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + os.Stderr = w + + Debugln("test message %d", 42) + + w.Close() + os.Stderr = origStderr + + var buf bytes.Buffer + if _, err := buf.ReadFrom(r); err != nil { + t.Fatal(err) + } + + output := buf.String() + if output == "" { + t.Error("expected debug output when Debug is true") + } + if !bytes.Contains([]byte(output), []byte("[DEBUG]")) { + t.Errorf("expected [DEBUG] prefix, got %q", output) + } + if !bytes.Contains([]byte(output), []byte("test message 42")) { + t.Errorf("expected 'test message 42' in output, got %q", output) + } +} + +func TestDebuglnDisabled(t *testing.T) { + // Save original state + origDebug := Debug + defer func() { Debug = origDebug }() + + Debug = false + + // Capture stderr + origStderr := os.Stderr + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + os.Stderr = w + + Debugln("this should not appear %d", 99) + + w.Close() + os.Stderr = origStderr + + var buf bytes.Buffer + if _, err := buf.ReadFrom(r); err != nil { + t.Fatal(err) + } + + output := buf.String() + if output != "" { + t.Errorf("expected no output when Debug is false, got %q", output) + } +} From e09b9066f1754455e6ef2d03f520c87eee3d3dc9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:11:39 +0000 Subject: [PATCH 4/4] Address code review feedback: save/restore original state in color tests, improve skip message Agent-Logs-Url: https://github.com/UnitVectorY-Labs/repver/sessions/a7cd52dc-d227-423a-b4d9-6c85cb71d5b3 Co-authored-by: JaredHatfield <208119+JaredHatfield@users.noreply.github.com> --- internal/color/color_test.go | 7 ++++++- internal/git/git_test.go | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/color/color_test.go b/internal/color/color_test.go index 218178e..5da3739 100644 --- a/internal/color/color_test.go +++ b/internal/color/color_test.go @@ -85,6 +85,8 @@ func TestDisabledFormatFunctions(t *testing.T) { } func TestYellowfFormatFunction(t *testing.T) { + origEnabled := Enabled + defer func() { Enabled = origEnabled }() Enabled = true got := Yellowf("value: %d", 10) @@ -95,6 +97,8 @@ func TestYellowfFormatFunction(t *testing.T) { } func TestCyanfFormatFunction(t *testing.T) { + origEnabled := Enabled + defer func() { Enabled = origEnabled }() Enabled = true got := Cyanf("path: %s", "/tmp") @@ -105,8 +109,9 @@ func TestCyanfFormatFunction(t *testing.T) { } func TestAllFormatFunctionsDisabled(t *testing.T) { + origEnabled := Enabled + defer func() { Enabled = origEnabled }() Enabled = false - defer func() { Enabled = true }() tests := []struct { name string diff --git a/internal/git/git_test.go b/internal/git/git_test.go index 3d9d5dd..03ea100 100644 --- a/internal/git/git_test.go +++ b/internal/git/git_test.go @@ -527,7 +527,7 @@ func TestCreateGitHubPullRequest_NoGH(t *testing.T) { if err == nil { // If gh is available and authenticated, the call might succeed (unlikely in tests), // but we still want to exercise the code path. - t.Skip("gh CLI appears to be available and configured; skipping error test") + t.Skip("gh CLI is authenticated; skipping error path test as success is valid") } // Verify we get a meaningful error if err != nil {