Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions internal/git/operations_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package git

import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"

"github.com/dfanso/commit-msg/pkg/types"
)

func TestIsRepository(t *testing.T) {
t.Parallel()

if _, err := exec.LookPath("git"); err != nil {
t.Skip("git executable not available")
}

t.Run("returns true for initialized repo", func(t *testing.T) {
t.Parallel()

dir := t.TempDir()

cmd := exec.Command("git", "init", dir)
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("failed to init git repo: %v: %s", err, string(output))
}

if got := IsRepository(dir); !got {
t.Fatalf("IsRepository(%q) = false, want true", dir)
}
})

t.Run("returns false for non repo", func(t *testing.T) {
t.Parallel()

dir := t.TempDir()
if got := IsRepository(dir); got {
t.Fatalf("IsRepository(%q) = true, want false", dir)
}
})

t.Run("returns false for missing path", func(t *testing.T) {
t.Parallel()

dir := t.TempDir()
missing := filepath.Join(dir, "does-not-exist")
if got := IsRepository(missing); got {
t.Fatalf("IsRepository(%q) = true, want false", missing)
}
})
}

func TestGetChangesIncludesSections(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration-style test in short mode")
}

if _, err := exec.LookPath("git"); err != nil {
t.Skip("git executable not available")
}

dir := t.TempDir()

runGit(t, dir, "init")
runGit(t, dir, "config", "user.name", "Test User")
runGit(t, dir, "config", "user.email", "test@example.com")

tracked := filepath.Join(dir, "tracked.txt")
if err := os.WriteFile(tracked, []byte("first version\n"), 0o644); err != nil {
t.Fatalf("failed to write tracked file: %v", err)
}
runGit(t, dir, "add", "tracked.txt")
runGit(t, dir, "commit", "-m", "initial commit")

if err := os.WriteFile(tracked, []byte("updated version\n"), 0o644); err != nil {
t.Fatalf("failed to modify tracked file: %v", err)
}

staged := filepath.Join(dir, "staged.txt")
if err := os.WriteFile(staged, []byte("staged content\n"), 0o644); err != nil {
t.Fatalf("failed to write staged file: %v", err)
}
runGit(t, dir, "add", "staged.txt")

untracked := filepath.Join(dir, "new.txt")
if err := os.WriteFile(untracked, []byte("brand new file\n"), 0o644); err != nil {
t.Fatalf("failed to write untracked file: %v", err)
}

output, err := GetChanges(&types.RepoConfig{Path: dir})
if err != nil {
t.Fatalf("GetChanges returned error: %v", err)
}

fragments := []string{
"Unstaged changes:",
"Staged changes:",
"Untracked files:",
"Content of new file new.txt:",
"Recent commits for context:",
"brand new file",
}

for _, fragment := range fragments {
if !strings.Contains(output, fragment) {
t.Fatalf("output missing fragment %q\noutput: %s", fragment, output)
}
}
}

func TestGetChangesErrorsOutsideRepo(t *testing.T) {
t.Parallel()

dir := t.TempDir()

if _, err := GetChanges(&types.RepoConfig{Path: dir}); err == nil {
t.Fatal("expected error for directory without git repository")
}
}

func runGit(t *testing.T, dir string, args ...string) {
t.Helper()

cmdArgs := append([]string{"-C", dir}, args...)
cmd := exec.Command("git", cmdArgs...)
cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("git %v failed: %v\n%s", args, err, output)
}
}
114 changes: 114 additions & 0 deletions internal/utils/utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package utils

import (
"bytes"
"os"
"path/filepath"
"testing"
)

func TestNormalizePath(t *testing.T) {
t.Parallel()

tests := []struct {
name string
input string
expected string
}{
{name: "windows style", input: "foo\\bar\\", expected: "foo/bar"},
{name: "already normalized", input: "foo/bar", expected: "foo/bar"},
{name: "no trailing slash", input: "foo", expected: "foo"},
}

for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

if got := NormalizePath(tc.input); got != tc.expected {
t.Fatalf("NormalizePath(%q) = %q, want %q", tc.input, got, tc.expected)
}
})
}
}

func TestIsTextFile(t *testing.T) {
t.Parallel()

tests := []struct {
name string
filename string
want bool
}{
{name: "go source", filename: "main.go", want: true},
{name: "markdown upper", filename: "README.MD", want: true},
{name: "binary extension", filename: "image.png", want: false},
{name: "no extension", filename: "LICENSE", want: false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

if got := IsTextFile(tt.filename); got != tt.want {
t.Fatalf("IsTextFile(%q) = %v, want %v", tt.filename, got, tt.want)
}
})
}
}

func TestIsSmallFile(t *testing.T) {
t.Parallel()

dir := t.TempDir()

smallPath := filepath.Join(dir, "small.txt")
if err := os.WriteFile(smallPath, bytes.Repeat([]byte("x"), 1024), 0o644); err != nil {
t.Fatalf("failed to write small file: %v", err)
}

largePath := filepath.Join(dir, "large.txt")
if err := os.WriteFile(largePath, bytes.Repeat([]byte("y"), 11*1024), 0o644); err != nil {
t.Fatalf("failed to write large file: %v", err)
}

tests := []struct {
name string
path string
want bool
}{
{name: "small file", path: smallPath, want: true},
{name: "large file", path: largePath, want: false},
{name: "missing file", path: filepath.Join(dir, "missing.txt"), want: false},
}

for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

if got := IsSmallFile(tc.path); got != tc.want {
t.Fatalf("IsSmallFile(%q) = %v, want %v", tc.path, got, tc.want)
}
})
}
}

func TestFilterEmpty(t *testing.T) {
t.Parallel()

input := []string{"feat", "", "test", " "}
want := []string{"feat", "test", " "}

got := FilterEmpty(input)

if len(got) != len(want) {
t.Fatalf("FilterEmpty returned %d items, want %d", len(got), len(want))
}

for i := range want {
if got[i] != want[i] {
t.Fatalf("FilterEmpty mismatch at index %d: got %q want %q", i, got[i], want[i])
}
}
}
50 changes: 50 additions & 0 deletions pkg/types/types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package types

import (
"encoding/json"
"strings"
"testing"
)

func TestConfigJSONMarshalling(t *testing.T) {
t.Parallel()

cfg := Config{
GrokAPI: "https://api.x.ai/v1/chat/completions",
Repos: map[string]RepoConfig{
"repo-a": {
Path: "/tmp/project",
LastRun: "2024-06-01T12:00:00Z",
},
},
}

data, err := json.Marshal(cfg)
if err != nil {
t.Fatalf("json.Marshal returned error: %v", err)
}

jsonStr := string(data)
if !strings.Contains(jsonStr, "\"grok_api\"") {
t.Fatalf("expected grok_api key in JSON: %s", jsonStr)
}
if !strings.Contains(jsonStr, "\"repos\"") {
t.Fatalf("expected repos key in JSON: %s", jsonStr)
}
}

func TestCommitPromptContent(t *testing.T) {
t.Parallel()

requiredFragments := []string{
"Starts with a verb",
"Is clear and descriptive",
"Here are the changes",
}

for _, fragment := range requiredFragments {
if !strings.Contains(CommitPrompt, fragment) {
t.Fatalf("CommitPrompt missing fragment %q", fragment)
}
}
}
Loading