Eliminate .bak file backups from edit, docs, lint, testgen, and agent commands to simplify safety features, relying on previews and confirmations instead. Update README, architecture docs, troubleshooting, and TODOs to reflect changes. Adjust tests to remove backup assertions.
461 lines
11 KiB
Go
461 lines
11 KiB
Go
package cmd
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/spf13/cobra"
|
|
"gmgauthier.com/grokkit/internal/grok"
|
|
)
|
|
|
|
// mockStreamer records calls made to it and returns a canned response.
|
|
type mockStreamer struct {
|
|
response string
|
|
calls int
|
|
}
|
|
|
|
func (m *mockStreamer) Stream(messages []map[string]string, model string) string {
|
|
m.calls++
|
|
return m.response
|
|
}
|
|
|
|
func (m *mockStreamer) StreamWithTemp(messages []map[string]string, model string, temp float64) string {
|
|
m.calls++
|
|
return m.response
|
|
}
|
|
|
|
func (m *mockStreamer) StreamSilent(messages []map[string]string, model string) string {
|
|
m.calls++
|
|
return m.response
|
|
}
|
|
|
|
// Ensure mockStreamer satisfies the interface at compile time.
|
|
var _ grok.AIClient = (*mockStreamer)(nil)
|
|
|
|
// withMockClient injects mock and returns a restore func.
|
|
func withMockClient(mock grok.AIClient) func() {
|
|
orig := newGrokClient
|
|
newGrokClient = func() grok.AIClient { return mock }
|
|
return func() { newGrokClient = orig }
|
|
}
|
|
|
|
// withMockGit injects a fake git runner and returns a restore func.
|
|
func withMockGit(fn func([]string) (string, error)) func() {
|
|
orig := gitRun
|
|
gitRun = fn
|
|
return func() { gitRun = orig }
|
|
}
|
|
|
|
// testCmd returns a minimal cobra command with the model flag registered.
|
|
func testCmd() *cobra.Command {
|
|
c := &cobra.Command{}
|
|
c.Flags().String("model", "", "")
|
|
return c
|
|
}
|
|
|
|
// --- runHistory ---
|
|
|
|
func TestRunHistory(t *testing.T) {
|
|
t.Run("calls AI with log output", func(t *testing.T) {
|
|
mock := &mockStreamer{response: "3 commits: feat, fix, chore"}
|
|
defer withMockClient(mock)()
|
|
defer withMockGit(func(args []string) (string, error) {
|
|
return "abc1234 feat: add thing\ndef5678 fix: bug", nil
|
|
})()
|
|
|
|
runHistory(testCmd(), nil)
|
|
|
|
if mock.calls != 1 {
|
|
t.Errorf("expected 1 AI call, got %d", mock.calls)
|
|
}
|
|
})
|
|
|
|
t.Run("no commits — skips AI", func(t *testing.T) {
|
|
mock := &mockStreamer{}
|
|
defer withMockClient(mock)()
|
|
defer withMockGit(func(args []string) (string, error) {
|
|
return "", nil
|
|
})()
|
|
|
|
runHistory(testCmd(), nil)
|
|
|
|
if mock.calls != 0 {
|
|
t.Errorf("expected 0 AI calls, got %d", mock.calls)
|
|
}
|
|
})
|
|
|
|
t.Run("git error — skips AI", func(t *testing.T) {
|
|
mock := &mockStreamer{}
|
|
defer withMockClient(mock)()
|
|
defer withMockGit(func(args []string) (string, error) {
|
|
return "", errors.New("not a git repo")
|
|
})()
|
|
|
|
runHistory(testCmd(), nil)
|
|
|
|
if mock.calls != 0 {
|
|
t.Errorf("expected 0 AI calls, got %d", mock.calls)
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- runReview ---
|
|
|
|
func TestRunReview(t *testing.T) {
|
|
t.Run("reviews with diff and status", func(t *testing.T) {
|
|
mock := &mockStreamer{response: "looks good"}
|
|
defer withMockClient(mock)()
|
|
callCount := 0
|
|
defer withMockGit(func(args []string) (string, error) {
|
|
callCount++
|
|
switch args[0] {
|
|
case "diff":
|
|
return "diff content", nil
|
|
case "status":
|
|
return "M main.go", nil
|
|
}
|
|
return "", nil
|
|
})()
|
|
|
|
runReview(testCmd(), nil)
|
|
|
|
if mock.calls != 1 {
|
|
t.Errorf("expected 1 AI call, got %d", mock.calls)
|
|
}
|
|
if callCount != 2 {
|
|
t.Errorf("expected 2 git calls (diff + status), got %d", callCount)
|
|
}
|
|
})
|
|
|
|
t.Run("git diff error — skips AI", func(t *testing.T) {
|
|
mock := &mockStreamer{}
|
|
defer withMockClient(mock)()
|
|
defer withMockGit(func(args []string) (string, error) {
|
|
if args[0] == "diff" {
|
|
return "", errors.New("git error")
|
|
}
|
|
return "", nil
|
|
})()
|
|
|
|
runReview(testCmd(), nil)
|
|
|
|
if mock.calls != 0 {
|
|
t.Errorf("expected 0 AI calls, got %d", mock.calls)
|
|
}
|
|
})
|
|
|
|
t.Run("git status error — skips AI", func(t *testing.T) {
|
|
mock := &mockStreamer{}
|
|
defer withMockClient(mock)()
|
|
defer withMockGit(func(args []string) (string, error) {
|
|
if args[0] == "status" {
|
|
return "", errors.New("git error")
|
|
}
|
|
return "diff content", nil
|
|
})()
|
|
|
|
runReview(testCmd(), nil)
|
|
|
|
if mock.calls != 0 {
|
|
t.Errorf("expected 0 AI calls, got %d", mock.calls)
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- runCommit ---
|
|
|
|
func TestRunCommit(t *testing.T) {
|
|
t.Run("no staged changes — skips AI", func(t *testing.T) {
|
|
mock := &mockStreamer{}
|
|
defer withMockClient(mock)()
|
|
defer withMockGit(func(args []string) (string, error) {
|
|
return "", nil
|
|
})()
|
|
|
|
runCommit(testCmd(), nil)
|
|
|
|
if mock.calls != 0 {
|
|
t.Errorf("expected 0 AI calls, got %d", mock.calls)
|
|
}
|
|
})
|
|
|
|
t.Run("git error — skips AI", func(t *testing.T) {
|
|
mock := &mockStreamer{}
|
|
defer withMockClient(mock)()
|
|
defer withMockGit(func(args []string) (string, error) {
|
|
return "", errors.New("not a git repo")
|
|
})()
|
|
|
|
runCommit(testCmd(), nil)
|
|
|
|
if mock.calls != 0 {
|
|
t.Errorf("expected 0 AI calls, got %d", mock.calls)
|
|
}
|
|
})
|
|
|
|
t.Run("with staged changes — calls AI then cancels via stdin", func(t *testing.T) {
|
|
mock := &mockStreamer{response: "feat(cmd): add thing"}
|
|
defer withMockClient(mock)()
|
|
defer withMockGit(func(args []string) (string, error) {
|
|
return "diff --git a/foo.go b/foo.go\n+func bar() {}", nil
|
|
})()
|
|
|
|
// Pipe "n\n" so the confirmation prompt returns without committing.
|
|
r, w, _ := os.Pipe()
|
|
origStdin := os.Stdin
|
|
os.Stdin = r
|
|
if _, err := w.WriteString("n\n"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := w.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { os.Stdin = origStdin }()
|
|
|
|
runCommit(testCmd(), nil)
|
|
|
|
if mock.calls != 1 {
|
|
t.Errorf("expected 1 AI call, got %d", mock.calls)
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- runCommitMsg ---
|
|
|
|
func TestRunCommitMsg(t *testing.T) {
|
|
t.Run("no staged changes — skips AI", func(t *testing.T) {
|
|
mock := &mockStreamer{}
|
|
defer withMockClient(mock)()
|
|
defer withMockGit(func(args []string) (string, error) {
|
|
return "", nil
|
|
})()
|
|
|
|
runCommitMsg(testCmd(), nil)
|
|
|
|
if mock.calls != 0 {
|
|
t.Errorf("expected 0 AI calls, got %d", mock.calls)
|
|
}
|
|
})
|
|
|
|
t.Run("with staged changes — calls AI and prints message", func(t *testing.T) {
|
|
mock := &mockStreamer{response: "feat(api): add endpoint"}
|
|
defer withMockClient(mock)()
|
|
defer withMockGit(func(args []string) (string, error) {
|
|
return "diff --git a/api.go b/api.go", nil
|
|
})()
|
|
|
|
runCommitMsg(testCmd(), nil)
|
|
|
|
if mock.calls != 1 {
|
|
t.Errorf("expected 1 AI call, got %d", mock.calls)
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- runPRDescribe ---
|
|
|
|
func TestRunPRDescribe(t *testing.T) {
|
|
t.Run("no changes on branch — skips AI", func(t *testing.T) {
|
|
mock := &mockStreamer{}
|
|
defer withMockClient(mock)()
|
|
defer withMockGit(func(args []string) (string, error) {
|
|
return "", nil // both diff calls return empty
|
|
})()
|
|
|
|
runPRDescribe(testCmd(), nil)
|
|
|
|
if mock.calls != 0 {
|
|
t.Errorf("expected 0 AI calls, got %d", mock.calls)
|
|
}
|
|
})
|
|
|
|
t.Run("first diff succeeds — calls AI", func(t *testing.T) {
|
|
mock := &mockStreamer{response: "## PR Title\n\nDescription"}
|
|
defer withMockClient(mock)()
|
|
callCount := 0
|
|
defer withMockGit(func(args []string) (string, error) {
|
|
callCount++
|
|
if callCount == 1 {
|
|
return "diff --git a/foo.go b/foo.go", nil
|
|
}
|
|
return "", nil
|
|
})()
|
|
|
|
runPRDescribe(testCmd(), nil)
|
|
|
|
if mock.calls != 1 {
|
|
t.Errorf("expected 1 AI call, got %d", mock.calls)
|
|
}
|
|
})
|
|
|
|
t.Run("first diff empty, second succeeds — calls AI", func(t *testing.T) {
|
|
mock := &mockStreamer{response: "PR description"}
|
|
defer withMockClient(mock)()
|
|
callCount := 0
|
|
defer withMockGit(func(args []string) (string, error) {
|
|
callCount++
|
|
if callCount == 2 {
|
|
return "diff --git a/bar.go b/bar.go", nil
|
|
}
|
|
return "", nil
|
|
})()
|
|
|
|
runPRDescribe(testCmd(), nil)
|
|
|
|
if mock.calls != 1 {
|
|
t.Errorf("expected 1 AI call, got %d", mock.calls)
|
|
}
|
|
})
|
|
|
|
t.Run("second diff error — skips AI", func(t *testing.T) {
|
|
mock := &mockStreamer{}
|
|
defer withMockClient(mock)()
|
|
callCount := 0
|
|
defer withMockGit(func(args []string) (string, error) {
|
|
callCount++
|
|
if callCount == 2 {
|
|
return "", errors.New("no remote")
|
|
}
|
|
return "", nil
|
|
})()
|
|
|
|
runPRDescribe(testCmd(), nil)
|
|
|
|
if mock.calls != 0 {
|
|
t.Errorf("expected 0 AI calls, got %d", mock.calls)
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- runLint / processDocsFile (error paths only — linter/file I/O) ---
|
|
|
|
func TestRunLintFileNotFound(t *testing.T) {
|
|
// Reset flags to defaults
|
|
dryRun = false
|
|
autoFix = false
|
|
|
|
// Pass a non-existent file; should return without calling AI.
|
|
mock := &mockStreamer{}
|
|
defer withMockClient(mock)()
|
|
|
|
runLint(testCmd(), []string{"/nonexistent/path/file.go"})
|
|
|
|
if mock.calls != 0 {
|
|
t.Errorf("expected 0 AI calls for missing file, got %d", mock.calls)
|
|
}
|
|
}
|
|
|
|
func TestProcessDocsFileNotFound(t *testing.T) {
|
|
mock := &mockStreamer{}
|
|
processDocsFile(mock, "grok-4", "/nonexistent/path/file.go")
|
|
|
|
if mock.calls != 0 {
|
|
t.Errorf("expected 0 AI calls for missing file, got %d", mock.calls)
|
|
}
|
|
}
|
|
|
|
func TestProcessDocsFileUnsupportedLanguage(t *testing.T) {
|
|
// Create a temp file with an unsupported extension.
|
|
f, err := os.CreateTemp("", "test*.xyz")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := f.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = os.Remove(f.Name()) }()
|
|
|
|
mock := &mockStreamer{}
|
|
processDocsFile(mock, "grok-4", f.Name())
|
|
|
|
if mock.calls != 0 {
|
|
t.Errorf("expected 0 AI calls for unsupported language, got %d", mock.calls)
|
|
}
|
|
}
|
|
|
|
func TestProcessDocsFilePreviewAndCancel(t *testing.T) {
|
|
f, err := os.CreateTemp("", "test*.go")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := f.WriteString("package main\n\nfunc Foo() {}\n"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := f.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = os.Remove(f.Name()) }()
|
|
|
|
mock := &mockStreamer{response: "package main\n\n// Foo does nothing.\nfunc Foo() {}\n"}
|
|
|
|
origAutoApply := autoApply
|
|
autoApply = false
|
|
defer func() { autoApply = origAutoApply }()
|
|
|
|
// Pipe "n\n" so confirmation returns without writing.
|
|
r, w, _ := os.Pipe()
|
|
origStdin := os.Stdin
|
|
os.Stdin = r
|
|
if _, err := w.WriteString("n\n"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := w.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { os.Stdin = origStdin }()
|
|
|
|
processDocsFile(mock, "grok-4", f.Name())
|
|
|
|
if mock.calls != 1 {
|
|
t.Errorf("expected 1 AI call, got %d", mock.calls)
|
|
}
|
|
}
|
|
|
|
func TestProcessDocsFileAutoApply(t *testing.T) {
|
|
f, err := os.CreateTemp("", "test*.go")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
original := "package main\n\nfunc Bar() {}\n"
|
|
if _, err := f.WriteString(original); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := f.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = os.Remove(f.Name()) }()
|
|
|
|
// CleanCodeResponse will trim the trailing newline from the AI response.
|
|
aiResponse := "package main\n\n// Bar does nothing.\nfunc Bar() {}\n"
|
|
documented := "package main\n\n// Bar does nothing.\nfunc Bar() {}"
|
|
mock := &mockStreamer{response: aiResponse}
|
|
|
|
origAutoApply := autoApply
|
|
autoApply = true
|
|
defer func() { autoApply = origAutoApply }()
|
|
|
|
processDocsFile(mock, "grok-4", f.Name())
|
|
|
|
if mock.calls != 1 {
|
|
t.Errorf("expected 1 AI call, got %d", mock.calls)
|
|
}
|
|
// Verify file was rewritten with documented content.
|
|
content, _ := os.ReadFile(f.Name())
|
|
if string(content) != documented {
|
|
t.Errorf("file content = %q, want %q", string(content), documented)
|
|
}
|
|
}
|
|
|
|
func TestRunDocs(t *testing.T) {
|
|
// runDocs with a missing file: should call processDocsFile which returns early.
|
|
mock := &mockStreamer{}
|
|
defer withMockClient(mock)()
|
|
|
|
runDocs(testCmd(), []string{"/nonexistent/file.go"})
|
|
|
|
if mock.calls != 0 {
|
|
t.Errorf("expected 0 AI calls for missing file, got %d", mock.calls)
|
|
}
|
|
}
|