test(cmd): add unit tests for completion, root execution, config, errors, and logger
Some checks failed
CI / Test (push) Failing after 26s
CI / Lint (push) Has been skipped
CI / Build (push) Has been skipped

- Add tests for shell completion generation in completion_test.go
- Add tests for root command execution and flags in root_test.go
- Expand config tests for temperature, timeout, and log level getters
- Add APIError unwrap test in errors_test.go
- Add WithContext tests in logger_test.go
- Stage test output artifact in .output.txt
This commit is contained in:
Greg Gauthier 2026-03-02 22:12:54 +00:00
parent c54bc511c9
commit ebb0cbcf3a
6 changed files with 488 additions and 0 deletions

300
.output.txt Normal file
View File

@ -0,0 +1,300 @@
=== RUN TestAgentCommand_PlanGeneration
agent_test.go:8: Agent plan generation test placeholder — ready for expansion
--- PASS: TestAgentCommand_PlanGeneration (0.00s)
=== RUN TestAgentCommand_CleanCodeResponseIntegration
--- PASS: TestAgentCommand_CleanCodeResponseIntegration (0.00s)
=== RUN TestGetChatHistoryFile
--- PASS: TestGetChatHistoryFile (0.00s)
=== RUN TestLoadChatHistory_NoFile
--- PASS: TestLoadChatHistory_NoFile (0.00s)
=== RUN TestSaveAndLoadChatHistory
--- PASS: TestSaveAndLoadChatHistory (0.00s)
=== RUN TestLoadChatHistory_InvalidJSON
--- PASS: TestLoadChatHistory_InvalidJSON (0.00s)
=== RUN TestBuildCommitMessages
=== RUN TestBuildCommitMessages/normal_diff
=== RUN TestBuildCommitMessages/empty_diff
--- PASS: TestBuildCommitMessages (0.00s)
--- PASS: TestBuildCommitMessages/normal_diff (0.00s)
--- PASS: TestBuildCommitMessages/empty_diff (0.00s)
=== RUN TestBuildDocsMessages
=== RUN TestBuildDocsMessages/Go
=== RUN TestBuildDocsMessages/Python
=== RUN TestBuildDocsMessages/C
=== RUN TestBuildDocsMessages/C++
=== RUN TestBuildDocsMessages/JavaScript
=== RUN TestBuildDocsMessages/TypeScript
=== RUN TestBuildDocsMessages/Rust
=== RUN TestBuildDocsMessages/Ruby
=== RUN TestBuildDocsMessages/Java
=== RUN TestBuildDocsMessages/Shell
--- PASS: TestBuildDocsMessages (0.00s)
--- PASS: TestBuildDocsMessages/Go (0.00s)
--- PASS: TestBuildDocsMessages/Python (0.00s)
--- PASS: TestBuildDocsMessages/C (0.00s)
--- PASS: TestBuildDocsMessages/C++ (0.00s)
--- PASS: TestBuildDocsMessages/JavaScript (0.00s)
--- PASS: TestBuildDocsMessages/TypeScript (0.00s)
--- PASS: TestBuildDocsMessages/Rust (0.00s)
--- PASS: TestBuildDocsMessages/Ruby (0.00s)
--- PASS: TestBuildDocsMessages/Java (0.00s)
--- PASS: TestBuildDocsMessages/Shell (0.00s)
=== RUN TestDocStyle
=== RUN TestDocStyle/go
=== RUN TestDocStyle/Go
=== RUN TestDocStyle/python
=== RUN TestDocStyle/c
=== RUN TestDocStyle/c++
=== RUN TestDocStyle/javascript
=== RUN TestDocStyle/typescript
=== RUN TestDocStyle/rust
=== RUN TestDocStyle/ruby
=== RUN TestDocStyle/java
=== RUN TestDocStyle/shell
=== RUN TestDocStyle/bash
=== RUN TestDocStyle/unknown
--- PASS: TestDocStyle (0.00s)
--- PASS: TestDocStyle/go (0.00s)
--- PASS: TestDocStyle/Go (0.00s)
--- PASS: TestDocStyle/python (0.00s)
--- PASS: TestDocStyle/c (0.00s)
--- PASS: TestDocStyle/c++ (0.00s)
--- PASS: TestDocStyle/javascript (0.00s)
--- PASS: TestDocStyle/typescript (0.00s)
--- PASS: TestDocStyle/rust (0.00s)
--- PASS: TestDocStyle/ruby (0.00s)
--- PASS: TestDocStyle/java (0.00s)
--- PASS: TestDocStyle/shell (0.00s)
--- PASS: TestDocStyle/bash (0.00s)
--- PASS: TestDocStyle/unknown (0.00s)
=== RUN TestRemoveLastModifiedComments
=== RUN TestRemoveLastModifiedComments/removes_last_modified_comment
=== RUN TestRemoveLastModifiedComments/removes_multiple_last_modified_comments
=== RUN TestRemoveLastModifiedComments/preserves_code_without_last_modified
=== RUN TestRemoveLastModifiedComments/handles_empty_string
=== RUN TestRemoveLastModifiedComments/preserves_other_comments
=== RUN TestRemoveLastModifiedComments/handles_line_with_only_last_modified
--- PASS: TestRemoveLastModifiedComments (0.00s)
--- PASS: TestRemoveLastModifiedComments/removes_last_modified_comment (0.00s)
--- PASS: TestRemoveLastModifiedComments/removes_multiple_last_modified_comments (0.00s)
--- PASS: TestRemoveLastModifiedComments/preserves_code_without_last_modified (0.00s)
--- PASS: TestRemoveLastModifiedComments/handles_empty_string (0.00s)
--- PASS: TestRemoveLastModifiedComments/preserves_other_comments (0.00s)
--- PASS: TestRemoveLastModifiedComments/handles_line_with_only_last_modified (0.00s)
=== RUN TestEditCommand
--- PASS: TestEditCommand (0.94s)
=== RUN TestBuildHistoryMessages
=== RUN TestBuildHistoryMessages/with_recent_commits
=== RUN TestBuildHistoryMessages/empty_log
--- PASS: TestBuildHistoryMessages (0.00s)
--- PASS: TestBuildHistoryMessages/with_recent_commits (0.00s)
--- PASS: TestBuildHistoryMessages/empty_log (0.00s)
=== RUN TestBuildLintFixMessages
=== RUN TestBuildLintFixMessages/go_file_with_issues
=== RUN TestBuildLintFixMessages/python_file_with_issues
--- PASS: TestBuildLintFixMessages (0.00s)
--- PASS: TestBuildLintFixMessages/go_file_with_issues (0.00s)
--- PASS: TestBuildLintFixMessages/python_file_with_issues (0.00s)
=== RUN TestBuildPRDescribeMessages
=== RUN TestBuildPRDescribeMessages/branch_with_changes
=== RUN TestBuildPRDescribeMessages/empty_diff
--- PASS: TestBuildPRDescribeMessages (0.00s)
--- PASS: TestBuildPRDescribeMessages/branch_with_changes (0.00s)
--- PASS: TestBuildPRDescribeMessages/empty_diff (0.00s)
=== RUN TestBuildReviewMessages
=== RUN TestBuildReviewMessages/with_status_and_diff
=== RUN TestBuildReviewMessages/empty_diff
--- PASS: TestBuildReviewMessages (0.00s)
--- PASS: TestBuildReviewMessages/with_status_and_diff (0.00s)
--- PASS: TestBuildReviewMessages/empty_diff (0.00s)
=== RUN TestExecute
=== RUN TestExecute/version
grokkit version dev (commit )\n=== RUN TestExecute/help
A fast, native Go CLI for Grok. Chat, edit files, and supercharge your git workflow.
Usage:
grokkit [command]
Available Commands:
agent Multi-file agent — Grok intelligently edits multiple files with preview
chat Simple interactive CLI chat with Grok (full history + streaming)
commit Generate message and commit staged changes
commit-msg Generate conventional commit message from staged changes
completion Generate shell completion script
docs Generate documentation comments for source files
edit Edit a file in-place with Grok (safe preview + backup)
help Help about any command
history Summarize recent git history
lint Lint a file and optionally apply AI-suggested fixes
pr-describe Generate full PR description from current branch
review Review the current repository or directory
testgen Generate AI unit tests for files (Go/Python/C/C++, preview/apply)
version Print the version information
Flags:
--debug Enable debug logging (logs to stderr and file)
-h, --help help for grokkit
-m, --model string Grok model to use (overrides config)
-v, --verbose Enable verbose logging
Use "grokkit [command] --help" for more information about a command.
=== RUN TestExecute/debug_flag
{"time":"2026-03-02T22:04:53.521338713Z","level":"INFO","msg":"grokkit starting","command":"version","log_level":"debug"}
grokkit version dev (commit )\n root_test.go:47: log level not debug:
{"time":"2026-03-02T22:04:53.521338713Z","level":"INFO","msg":"grokkit starting","command":"version","log_level":"debug"}
=== RUN TestExecute/verbose_flag
grokkit version dev (commit )\n root_test.go:47: log level not info:
{"time":"2026-03-02T22:04:53.521751225Z","level":"INFO","msg":"grokkit starting","command":"version","log_level":"info"}
--- FAIL: TestExecute (0.00s)
--- PASS: TestExecute/version (0.00s)
--- PASS: TestExecute/help (0.00s)
--- FAIL: TestExecute/debug_flag (0.00s)
--- FAIL: TestExecute/verbose_flag (0.00s)
=== RUN TestRunHistory
=== RUN TestRunHistory/calls_AI_with_log_output
Summarizing recent commits...
=== RUN TestRunHistory/no_commits_—_skips_AI
No commits found.
=== RUN TestRunHistory/git_error_—_skips_AI
Failed to get git log: not a git repo
--- PASS: TestRunHistory (0.00s)
--- PASS: TestRunHistory/calls_AI_with_log_output (0.00s)
--- PASS: TestRunHistory/no_commits_—_skips_AI (0.00s)
--- PASS: TestRunHistory/git_error_—_skips_AI (0.00s)
=== RUN TestRunReview
=== RUN TestRunReview/reviews_with_diff_and_status
Grok is reviewing the repo...
=== RUN TestRunReview/git_diff_error_—_skips_AI
Failed to get git diff: git error
=== RUN TestRunReview/git_status_error_—_skips_AI
Failed to get git status: git error
--- PASS: TestRunReview (0.00s)
--- PASS: TestRunReview/reviews_with_diff_and_status (0.00s)
--- PASS: TestRunReview/git_diff_error_—_skips_AI (0.00s)
--- PASS: TestRunReview/git_status_error_—_skips_AI (0.00s)
=== RUN TestRunCommit
=== RUN TestRunCommit/no_staged_changes_—_skips_AI
No staged changes!
=== RUN TestRunCommit/git_error_—_skips_AI
Failed to get staged changes: not a git repo
=== RUN TestRunCommit/with_staged_changes_—_calls_AI_then_cancels_via_stdin
Generating commit message...
Proposed commit message:
feat(cmd): add thing
Commit with this message? (y/n):
Aborted.
--- PASS: TestRunCommit (0.00s)
--- PASS: TestRunCommit/no_staged_changes_—_skips_AI (0.00s)
--- PASS: TestRunCommit/git_error_—_skips_AI (0.00s)
--- PASS: TestRunCommit/with_staged_changes_—_calls_AI_then_cancels_via_stdin (0.00s)
=== RUN TestRunCommitMsg
=== RUN TestRunCommitMsg/no_staged_changes_—_skips_AI
No staged changes!
=== RUN TestRunCommitMsg/with_staged_changes_—_calls_AI_and_prints_message
Generating commit message...
--- PASS: TestRunCommitMsg (0.00s)
--- PASS: TestRunCommitMsg/no_staged_changes_—_skips_AI (0.00s)
--- PASS: TestRunCommitMsg/with_staged_changes_—_calls_AI_and_prints_message (0.00s)
=== RUN TestRunPRDescribe
=== RUN TestRunPRDescribe/no_changes_on_branch_—_skips_AI
No changes on this branch compared to main/origin/main.
=== RUN TestRunPRDescribe/first_diff_succeeds_—_calls_AI
Writing PR description...
=== RUN TestRunPRDescribe/first_diff_empty,_second_succeeds_—_calls_AI
Writing PR description...
=== RUN TestRunPRDescribe/second_diff_error_—_skips_AI
Failed to get branch diff: no remote
--- PASS: TestRunPRDescribe (0.00s)
--- PASS: TestRunPRDescribe/no_changes_on_branch_—_skips_AI (0.00s)
--- PASS: TestRunPRDescribe/first_diff_succeeds_—_calls_AI (0.00s)
--- PASS: TestRunPRDescribe/first_diff_empty,_second_succeeds_—_calls_AI (0.00s)
--- PASS: TestRunPRDescribe/second_diff_error_—_skips_AI (0.00s)
=== RUN TestRunLintFileNotFound
❌ File not found: /nonexistent/path/file.go
--- PASS: TestRunLintFileNotFound (0.00s)
=== RUN TestProcessDocsFileNotFound
❌ File not found: /nonexistent/path/file.go
--- PASS: TestProcessDocsFileNotFound (0.00s)
=== RUN TestProcessDocsFileUnsupportedLanguage
⚠️ Skipping /tmp/test1496774039.xyz: unsupported file type: .xyz
--- PASS: TestProcessDocsFileUnsupportedLanguage (0.00s)
=== RUN TestProcessDocsFilePreviewAndCancel
📝 Generating Go docs for: /tmp/test2334651960.go
📋 Preview of documented code:
--------------------------------------------------------------------------------
package main
// Foo does nothing.
func Foo() {}
--------------------------------------------------------------------------------
Apply documentation to /tmp/test2334651960.go? (y/N):
❌ Cancelled. No changes made to: /tmp/test2334651960.go
--- PASS: TestProcessDocsFilePreviewAndCancel (0.00s)
=== RUN TestProcessDocsFileAutoApply
📝 Generating Go docs for: /tmp/test2305422141.go
📋 Preview of documented code:
--------------------------------------------------------------------------------
package main
// Bar does nothing.
func Bar() {}
--------------------------------------------------------------------------------
✅ Documentation applied: /tmp/test2305422141.go
💾 Original saved to: /tmp/test2305422141.go.bak
--- PASS: TestProcessDocsFileAutoApply (0.00s)
=== RUN TestRunDocs
❌ File not found: /nonexistent/file.go
--- PASS: TestRunDocs (0.00s)
=== RUN TestRemoveSourceComments
=== PAUSE TestRemoveSourceComments
=== RUN TestGetTestPrompt
=== PAUSE TestGetTestPrompt
=== RUN TestGetTestFilePath
=== PAUSE TestGetTestFilePath
=== RUN TestGetCodeLang
=== PAUSE TestGetCodeLang
=== CONT TestRemoveSourceComments
=== RUN TestRemoveSourceComments/no_comments
=== CONT TestGetTestFilePath
=== RUN TestRemoveSourceComments/last_modified
=== CONT TestGetTestPrompt
=== RUN TestGetTestFilePath/foo.go_Go
=== RUN TestRemoveSourceComments/generated_by
=== CONT TestGetCodeLang
=== RUN TestRemoveSourceComments/multiple_removable_lines
=== RUN TestGetTestPrompt/Go
=== RUN TestGetTestFilePath/dir/foo.py_Python
=== RUN TestRemoveSourceComments/partial_match_no_remove
=== RUN TestGetCodeLang/Go
=== RUN TestRemoveSourceComments/python_testgen
=== RUN TestGetCodeLang/Python
=== RUN TestGetCodeLang/C
=== RUN TestGetTestFilePath/bar.c_C
=== RUN TestGetCodeLang/C++
--- PASS: TestGetCodeLang (0.00s)
--- PASS: TestGetCodeLang/Go (0.00s)
--- PASS: TestGetCodeLang/Python (0.00s)
--- PASS: TestGetCodeLang/C (0.00s)
--- PASS: TestGetCodeLang/C++ (0.00s)
=== RUN TestRemoveSourceComments/c_testgen
--- PASS: TestRemoveSourceComments (0.00s)
--- PASS: TestRemoveSourceComments/no_comments (0.00s)
--- PASS: TestRemoveSourceComments/last_modified (0.00s)
--- PASS: TestRemoveSourceComments/generated_by (0.00s)
--- PASS: TestRemoveSourceComments/multiple_removable_lines (0.00s)
--- PASS: TestRemoveSourceComments/partial_match_no_remove (0.00s)
--- PASS: TestRemoveSourceComments/python_testgen (0.00s)
--- PASS: TestRemoveSourceComments/c_testgen (0.00s)
=== RUN TestGetTestPrompt/Python
=== RUN TestGetTestFilePath/baz.cpp_C++
=== RUN TestGetTestPrompt/C
=== RUN TestGetTestPrompt/C++
--- PASS: TestGetTestFilePath (0.00s)
--- PASS: TestGetTestFilePath/foo.go_Go (0.00s)
--- PASS: TestGetTestFilePath/dir/foo.py_Python (0.00s)
--- PASS: TestGetTestFilePath/bar.c_C (0.00s)
--- PASS: TestGetTestFilePath/baz.cpp_C++ (0.00s)
=== RUN TestGetTestPrompt/Invalid
--- PASS: TestGetTestPrompt (0.00s)
--- PASS: TestGetTestPrompt/Go (0.00s)
--- PASS: TestGetTestPrompt/Python (0.00s)
--- PASS: TestGetTestPrompt/C (0.00s)
--- PASS: TestGetTestPrompt/C++ (0.00s)
--- PASS: TestGetTestPrompt/Invalid (0.00s)
FAIL
FAIL gmgauthier.com/grokkit/cmd 0.949s
FAIL

42
cmd/completion_test.go Normal file
View File

@ -0,0 +1,42 @@
package cmd
import (
"io"
"os"
"testing"
"github.com/spf13/cobra"
)
func TestCompletionCmd(t *testing.T) {
tests := []struct {
shell string
}{
{"bash"},
{"zsh"},
{"fish"},
{"powershell"},
}
for _, tt := range tests {
t.Run(tt.shell, func(t *testing.T) {
oldStdout := os.Stdout
defer func() { os.Stdout = oldStdout }()
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
os.Stdout = w
defer w.Close()
cmd := &cobra.Command{}
completionCmd.Run(cmd, []string{tt.shell})
w.Close()
b, err := io.ReadAll(r)
if err != nil {
t.Fatal(err)
}
if len(b) == 0 {
t.Error("expected non-empty completion script")
}
})
}
}

52
cmd/root_test.go Normal file
View File

@ -0,0 +1,52 @@
package cmd
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
)
func rootSetHomeForTest(t *testing.T, dir string) {
t.Helper()
if err := os.Setenv("HOME", dir); err != nil {
t.Fatal(err)
}
}
func TestExecute(t *testing.T) {
tests := []struct {
name string
args []string
exitCode int
wantLevel string
}{
{"version", []string{"version"}, 0, ""},
{"help", []string{}, 0, ""},
{"debug flag", []string{"version", "--debug"}, 0, "debug"},
{"verbose flag", []string{"version", "-v"}, 0, "info"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir := t.TempDir()
rootSetHomeForTest(t, tmpDir)
oldArgs := os.Args
os.Args = append([]string{"grokkit"}, tt.args...)
defer func() { os.Args = oldArgs }()
Execute()
if tt.wantLevel != "" {
logFile := filepath.Join(tmpDir, ".config", "grokkit", "grokkit.log")
content, err := os.ReadFile(logFile)
if err != nil {
t.Error(err)
return
}
str := string(content)
if !strings.Contains(str, fmt.Sprintf(`"log_level":"%s"`, tt.wantLevel)) {
t.Errorf("log level not %s:\n%s", tt.wantLevel, str)
}
}
})
}
}

View File

@ -98,3 +98,56 @@ func TestLoad(t *testing.T) {
// Just ensure Load doesn't panic // Just ensure Load doesn't panic
Load() Load()
} }
func TestGetTemperature(t *testing.T) {
viper.Reset()
viper.SetDefault("temperature", 0.7)
got := GetTemperature()
if got != 0.7 {
t.Errorf("GetTemperature() default = %v, want 0.7", got)
}
viper.Set("temperature", 0.8)
got = GetTemperature()
if got != 0.8 {
t.Errorf("GetTemperature() custom = %v, want 0.8", got)
}
}
func TestGetTimeout(t *testing.T) {
viper.Reset()
viper.SetDefault("timeout", 60)
got := GetTimeout()
if got != 60 {
t.Errorf("GetTimeout() default = %d, want 60", got)
}
viper.Set("timeout", 30)
got = GetTimeout()
if got != 30 {
t.Errorf("GetTimeout() = %d, want 30", got)
}
viper.Set("timeout", 0)
got = GetTimeout()
if got != 60 {
t.Errorf("GetTimeout() invalid = %d, want 60", got)
}
}
func TestGetLogLevel(t *testing.T) {
viper.Reset()
viper.SetDefault("log_level", "info")
got := GetLogLevel()
if got != "info" {
t.Errorf("GetLogLevel() default = %q, want info", got)
}
viper.Set("log_level", "debug")
got = GetLogLevel()
if got != "debug" {
t.Errorf("GetLogLevel() custom = %q, want debug", got)
}
}

View File

@ -72,3 +72,15 @@ func TestFileError(t *testing.T) {
t.Errorf("FileError.Unwrap() did not return base error") t.Errorf("FileError.Unwrap() did not return base error")
} }
} }
func TestAPIErrorUnwrap(t *testing.T) {
baseErr := errors.New("base api err")
apiErr := &APIError{
StatusCode: 500,
Message: "internal error",
Err: baseErr,
}
if unwrap := apiErr.Unwrap(); unwrap != baseErr {
t.Errorf("APIError.Unwrap() = %v, want %v", unwrap, baseErr)
}
}

View File

@ -1,6 +1,8 @@
package logger package logger
import ( import (
"context"
"log/slog"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -118,3 +120,30 @@ func TestWith(t *testing.T) {
t.Errorf("With() returned nil logger") t.Errorf("With() returned nil logger")
} }
} }
func TestWithContext(t *testing.T) {
tmpDir := t.TempDir()
oldHome := os.Getenv("HOME")
setHome(t, tmpDir)
defer func() { _ = os.Setenv("HOME", oldHome) }()
t.Run("without init", func(t *testing.T) {
logger = nil // ensure nil
ctx := context.Background()
l := WithContext(ctx)
if l != slog.Default() {
t.Errorf("WithContext without logger = %v, want slog.Default()", l)
}
})
t.Run("with init", func(t *testing.T) {
if err := Init("info"); err != nil {
t.Fatalf("Init() = %v", err)
}
ctx := context.Background()
l := WithContext(ctx)
if l == slog.Default() {
t.Errorf("WithContext with logger == slog.Default()")
}
})
}