go-agentic/completion_git_test.go
Claude 23aa635c91
test: achieve 85.6% coverage with 7 new test files
Phase 1 complete: coverage from 70.1% to 85.6% (130+ tests, all passing).

New test files:
- lifecycle_test.go: full claim -> process -> complete integration flows
- allowance_edge_test.go: boundary conditions for token/job/concurrent limits
- allowance_error_test.go: mock errorStore covering all RecordUsage error paths
- embed_test.go: Prompt() hit/miss and content trimming
- service_test.go: DefaultServiceOptions, TaskPrompt, TaskCommit type coverage
- completion_git_test.go: real git repos for AutoCommit, CreateBranch, CommitAndSync
- context_git_test.go: findRelatedCode with keyword search, file limits, truncation

Updated config_test.go with YAML fallback, env override, and empty-dir paths.

Co-Authored-By: Charon <developers@lethean.io>
2026-02-20 00:59:58 +00:00

474 lines
14 KiB
Go

package agentic
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// initGitRepo creates a temporary git repo with an initial commit.
func initGitRepo(t *testing.T) string {
t.Helper()
dir := t.TempDir()
// Initialise a git repo.
_, err := runCommandCtx(context.Background(), dir, "git", "init")
require.NoError(t, err, "git init should succeed")
// Configure git identity for commits.
_, err = runCommandCtx(context.Background(), dir, "git", "config", "user.email", "test@example.com")
require.NoError(t, err)
_, err = runCommandCtx(context.Background(), dir, "git", "config", "user.name", "Test User")
require.NoError(t, err)
// Create initial commit so HEAD exists.
readmePath := filepath.Join(dir, "README.md")
err = os.WriteFile(readmePath, []byte("# Test Repo\n"), 0644)
require.NoError(t, err)
_, err = runCommandCtx(context.Background(), dir, "git", "add", "-A")
require.NoError(t, err)
_, err = runCommandCtx(context.Background(), dir, "git", "commit", "-m", "initial commit")
require.NoError(t, err)
return dir
}
// --- runCommandCtx / runGitCommandCtx tests ---
func TestRunCommandCtx_Good(t *testing.T) {
output, err := runCommandCtx(context.Background(), "/tmp", "echo", "hello world")
require.NoError(t, err)
assert.Contains(t, output, "hello world")
}
func TestRunCommandCtx_Bad_NonexistentCommand(t *testing.T) {
_, err := runCommandCtx(context.Background(), "/tmp", "nonexistent-command-xyz")
assert.Error(t, err)
}
func TestRunCommandCtx_Bad_CommandFails(t *testing.T) {
_, err := runCommandCtx(context.Background(), "/tmp", "false")
assert.Error(t, err)
}
func TestRunCommandCtx_Bad_StderrIncluded(t *testing.T) {
// git status in a non-git directory should produce stderr.
dir := t.TempDir()
_, err := runCommandCtx(context.Background(), dir, "git", "status")
assert.Error(t, err)
}
func TestRunGitCommandCtx_Good(t *testing.T) {
dir := initGitRepo(t)
output, err := runGitCommandCtx(context.Background(), dir, "log", "--oneline", "-1")
require.NoError(t, err)
assert.Contains(t, output, "initial commit")
}
// --- GetCurrentBranch tests ---
func TestGetCurrentBranch_Good(t *testing.T) {
dir := initGitRepo(t)
branch, err := GetCurrentBranch(context.Background(), dir)
require.NoError(t, err)
// Depending on git config, default branch could be master or main.
assert.True(t, branch == "main" || branch == "master",
"expected main or master, got %q", branch)
}
func TestGetCurrentBranch_Bad_NotAGitRepo(t *testing.T) {
dir := t.TempDir()
_, err := GetCurrentBranch(context.Background(), dir)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to get current branch")
}
// --- HasUncommittedChanges tests ---
func TestHasUncommittedChanges_Good_Clean(t *testing.T) {
dir := initGitRepo(t)
hasChanges, err := HasUncommittedChanges(context.Background(), dir)
require.NoError(t, err)
assert.False(t, hasChanges, "fresh repo with initial commit should be clean")
}
func TestHasUncommittedChanges_Good_WithChanges(t *testing.T) {
dir := initGitRepo(t)
// Create a new file.
err := os.WriteFile(filepath.Join(dir, "new-file.txt"), []byte("content"), 0644)
require.NoError(t, err)
hasChanges, err := HasUncommittedChanges(context.Background(), dir)
require.NoError(t, err)
assert.True(t, hasChanges, "should detect untracked file")
}
func TestHasUncommittedChanges_Good_WithModifiedFile(t *testing.T) {
dir := initGitRepo(t)
// Modify the existing README.
err := os.WriteFile(filepath.Join(dir, "README.md"), []byte("# Updated\n"), 0644)
require.NoError(t, err)
hasChanges, err := HasUncommittedChanges(context.Background(), dir)
require.NoError(t, err)
assert.True(t, hasChanges, "should detect modified file")
}
func TestHasUncommittedChanges_Bad_NotAGitRepo(t *testing.T) {
dir := t.TempDir()
_, err := HasUncommittedChanges(context.Background(), dir)
assert.Error(t, err)
}
// --- GetDiff tests ---
func TestGetDiff_Good_Unstaged(t *testing.T) {
dir := initGitRepo(t)
// Modify a tracked file.
err := os.WriteFile(filepath.Join(dir, "README.md"), []byte("# Modified\n"), 0644)
require.NoError(t, err)
diff, err := GetDiff(context.Background(), dir, false)
require.NoError(t, err)
assert.Contains(t, diff, "Modified", "diff should show the change")
}
func TestGetDiff_Good_Staged(t *testing.T) {
dir := initGitRepo(t)
// Modify and stage a file.
err := os.WriteFile(filepath.Join(dir, "README.md"), []byte("# Staged change\n"), 0644)
require.NoError(t, err)
_, err = runCommandCtx(context.Background(), dir, "git", "add", "README.md")
require.NoError(t, err)
diff, err := GetDiff(context.Background(), dir, true)
require.NoError(t, err)
assert.Contains(t, diff, "Staged change", "staged diff should show the change")
}
func TestGetDiff_Good_NoDiff(t *testing.T) {
dir := initGitRepo(t)
diff, err := GetDiff(context.Background(), dir, false)
require.NoError(t, err)
assert.Empty(t, diff, "clean repo should have no diff")
}
func TestGetDiff_Bad_NotAGitRepo(t *testing.T) {
dir := t.TempDir()
_, err := GetDiff(context.Background(), dir, false)
assert.Error(t, err)
}
// --- AutoCommit tests (with real git) ---
func TestAutoCommit_Good(t *testing.T) {
dir := initGitRepo(t)
// Create a file to commit.
err := os.WriteFile(filepath.Join(dir, "feature.go"), []byte("package main\n"), 0644)
require.NoError(t, err)
task := &Task{ID: "T-100", Title: "Add feature"}
err = AutoCommit(context.Background(), task, dir, "feat: add feature module")
require.NoError(t, err)
// Verify commit was created.
output, err := runGitCommandCtx(context.Background(), dir, "log", "--oneline", "-1")
require.NoError(t, err)
assert.Contains(t, output, "feat: add feature module")
// Verify task reference in full message.
fullLog, err := runGitCommandCtx(context.Background(), dir, "log", "-1", "--pretty=format:%B")
require.NoError(t, err)
assert.Contains(t, fullLog, "Task: #T-100")
assert.Contains(t, fullLog, "Co-Authored-By: Claude <noreply@anthropic.com>")
}
func TestAutoCommit_Bad_NoChangesToCommit(t *testing.T) {
dir := initGitRepo(t)
// No changes to commit.
task := &Task{ID: "T-200", Title: "No changes"}
err := AutoCommit(context.Background(), task, dir, "feat: nothing")
assert.Error(t, err, "should fail when there is nothing to commit")
}
// --- CreateBranch tests (with real git) ---
func TestCreateBranch_Good(t *testing.T) {
dir := initGitRepo(t)
task := &Task{
ID: "BR-42",
Title: "Implement new feature",
Labels: []string{"enhancement"},
}
branchName, err := CreateBranch(context.Background(), task, dir)
require.NoError(t, err)
assert.Equal(t, "feat/BR-42-implement-new-feature", branchName)
// Verify we're on the new branch.
currentBranch, err := GetCurrentBranch(context.Background(), dir)
require.NoError(t, err)
assert.Equal(t, branchName, currentBranch)
}
func TestCreateBranch_Good_BugLabel(t *testing.T) {
dir := initGitRepo(t)
task := &Task{
ID: "BR-43",
Title: "Fix login bug",
Labels: []string{"bug"},
}
branchName, err := CreateBranch(context.Background(), task, dir)
require.NoError(t, err)
assert.Equal(t, "fix/BR-43-fix-login-bug", branchName)
}
// --- PushChanges test ---
func TestPushChanges_Bad_NoRemote(t *testing.T) {
dir := initGitRepo(t)
// No remote configured, push should fail.
err := PushChanges(context.Background(), dir)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to push changes")
}
// --- CommitAndSync tests ---
func TestCommitAndSync_Good_WithoutClient(t *testing.T) {
dir := initGitRepo(t)
// Create a file to commit.
err := os.WriteFile(filepath.Join(dir, "sync.go"), []byte("package sync\n"), 0644)
require.NoError(t, err)
task := &Task{ID: "CS-1", Title: "Sync test"}
// nil client: should commit but skip sync.
err = CommitAndSync(context.Background(), nil, task, dir, "feat: sync test", 50)
require.NoError(t, err)
// Verify commit.
output, err := runGitCommandCtx(context.Background(), dir, "log", "--oneline", "-1")
require.NoError(t, err)
assert.Contains(t, output, "feat: sync test")
}
func TestCommitAndSync_Good_WithClient(t *testing.T) {
dir := initGitRepo(t)
// Create a file to commit.
err := os.WriteFile(filepath.Join(dir, "synced.go"), []byte("package synced\n"), 0644)
require.NoError(t, err)
var receivedUpdate TaskUpdate
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPatch {
_ = json.NewDecoder(r.Body).Decode(&receivedUpdate)
w.WriteHeader(http.StatusOK)
}
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
task := &Task{ID: "CS-2", Title: "Sync with client"}
err = CommitAndSync(context.Background(), client, task, dir, "feat: synced", 75)
require.NoError(t, err)
// Verify the update was sent.
assert.Equal(t, StatusInProgress, receivedUpdate.Status)
assert.Equal(t, 75, receivedUpdate.Progress)
assert.Contains(t, receivedUpdate.Notes, "feat: synced")
}
func TestCommitAndSync_Bad_CommitFails(t *testing.T) {
dir := initGitRepo(t)
// No changes to commit.
task := &Task{ID: "CS-3", Title: "Will fail"}
err := CommitAndSync(context.Background(), nil, task, dir, "feat: no changes", 50)
assert.Error(t, err, "should fail when commit fails")
}
func TestCommitAndSync_Bad_SyncFails(t *testing.T) {
dir := initGitRepo(t)
// Create a file to commit.
err := os.WriteFile(filepath.Join(dir, "fail-sync.go"), []byte("package failsync\n"), 0644)
require.NoError(t, err)
// Server returns an error.
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(w).Encode(APIError{Message: "sync failed"})
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
task := &Task{ID: "CS-4", Title: "Sync fails"}
err = CommitAndSync(context.Background(), client, task, dir, "feat: sync-fail", 50)
assert.Error(t, err, "should report sync failure")
assert.Contains(t, err.Error(), "sync failed")
}
// --- SyncStatus with working client ---
func TestSyncStatus_Good(t *testing.T) {
var receivedUpdate TaskUpdate
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = json.NewDecoder(r.Body).Decode(&receivedUpdate)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
task := &Task{ID: "sync-1", Title: "Sync test"}
err := SyncStatus(context.Background(), client, task, TaskUpdate{
Status: StatusCompleted,
Progress: 100,
Notes: "All done",
})
require.NoError(t, err)
assert.Equal(t, StatusCompleted, receivedUpdate.Status)
assert.Equal(t, 100, receivedUpdate.Progress)
}
// --- CreatePR with default title/body ---
func TestCreatePR_Good_DefaultTitleFromTask(t *testing.T) {
// CreatePR requires gh CLI which may not be available.
// Test the option building logic by checking that the title
// defaults to the task title.
task := &Task{
ID: "PR-1",
Title: "Add authentication",
Description: "OAuth2 login",
Priority: PriorityHigh,
}
opts := PROptions{}
// Verify the defaulting logic that would be used.
title := opts.Title
if title == "" {
title = task.Title
}
assert.Equal(t, "Add authentication", title)
body := opts.Body
if body == "" {
body = buildPRBody(task)
}
assert.Contains(t, body, "OAuth2 login")
}
func TestCreatePR_Good_CustomOptions(t *testing.T) {
opts := PROptions{
Title: "Custom title",
Body: "Custom body",
Draft: true,
Labels: []string{"enhancement", "v2"},
Base: "develop",
}
assert.Equal(t, "Custom title", opts.Title)
assert.True(t, opts.Draft)
assert.Equal(t, "develop", opts.Base)
assert.Len(t, opts.Labels, 2)
}
// --- Client checkResponse edge cases ---
func TestClient_CheckResponse_Good_GenericError(t *testing.T) {
// Test checkResponse with a non-JSON error body.
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadGateway)
_, _ = w.Write([]byte("plain text error"))
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
_, err := client.GetTask(context.Background(), "test-task")
assert.Error(t, err)
assert.Contains(t, err.Error(), "Bad Gateway")
}
func TestClient_CheckResponse_Good_EmptyBody(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusForbidden)
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
_, err := client.GetTask(context.Background(), "test-task")
assert.Error(t, err)
assert.Contains(t, err.Error(), "Forbidden")
}
// --- Client Ping edge case ---
func TestClient_Ping_Bad_ServerReturns4xx(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
err := client.Ping(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "status 401")
}
// --- Client ClaimTask without AgentID ---
func TestClient_ClaimTask_Good_NoAgentID(t *testing.T) {
claimedTask := Task{
ID: "task-no-agent",
Status: StatusInProgress,
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify no body sent when AgentID is empty.
assert.Equal(t, http.MethodPost, r.Method)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(claimedTask)
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
// Explicitly leave AgentID empty.
client.AgentID = ""
task, err := client.ClaimTask(context.Background(), "task-no-agent")
require.NoError(t, err)
assert.Equal(t, "task-no-agent", task.ID)
}