From ebb0cbcf3a6e0b799f2269cc85565bdcd11691e6 Mon Sep 17 00:00:00 2001 From: Greg Gauthier Date: Mon, 2 Mar 2026 22:12:54 +0000 Subject: [PATCH] test(cmd): add unit tests for completion, root execution, config, errors, and logger - 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 --- .output.txt | 300 +++++++++++++++++++++++++++++++++ cmd/completion_test.go | 42 +++++ cmd/root_test.go | 52 ++++++ config/config_test.go | 53 ++++++ internal/errors/errors_test.go | 12 ++ internal/logger/logger_test.go | 29 ++++ 6 files changed, 488 insertions(+) create mode 100644 .output.txt create mode 100644 cmd/completion_test.go create mode 100644 cmd/root_test.go diff --git a/.output.txt b/.output.txt new file mode 100644 index 0000000..1305e48 --- /dev/null +++ b/.output.txt @@ -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 diff --git a/cmd/completion_test.go b/cmd/completion_test.go new file mode 100644 index 0000000..4220463 --- /dev/null +++ b/cmd/completion_test.go @@ -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") + } + }) + } +} diff --git a/cmd/root_test.go b/cmd/root_test.go new file mode 100644 index 0000000..7569e9b --- /dev/null +++ b/cmd/root_test.go @@ -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) + } + } + }) + } +} diff --git a/config/config_test.go b/config/config_test.go index d645079..22e7c0b 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -98,3 +98,56 @@ func TestLoad(t *testing.T) { // Just ensure Load doesn't panic 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) + } +} diff --git a/internal/errors/errors_test.go b/internal/errors/errors_test.go index 2cff6c5..4d083f4 100644 --- a/internal/errors/errors_test.go +++ b/internal/errors/errors_test.go @@ -72,3 +72,15 @@ func TestFileError(t *testing.T) { 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) + } +} diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go index 0990b61..d1192fa 100644 --- a/internal/logger/logger_test.go +++ b/internal/logger/logger_test.go @@ -1,6 +1,8 @@ package logger import ( + "context" + "log/slog" "os" "path/filepath" "strings" @@ -118,3 +120,30 @@ func TestWith(t *testing.T) { 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()") + } + }) +}