diff --git a/.gitignore b/.gitignore index ed6a5cb..933b0bc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,4 @@ grokkit *.log *.tmp .env -coverage.out -coverage.html chat_history.json \ No newline at end of file diff --git a/Makefile b/Makefile index b0b4379..1ab39c3 100644 --- a/Makefile +++ b/Makefile @@ -4,14 +4,16 @@ test: go test ./... -v test-cover: - go test ./... -coverprofile=coverage.out - go tool cover -html=coverage.out -o coverage.html - @echo "✅ Coverage report: open coverage.html in your browser" + @mkdir -p build + go test ./... -coverprofile=build/coverage.out + go tool cover -html=build/coverage.out -o build/coverage.html + @echo "✅ Coverage report: open build/coverage.html in your browser" test-agent: go test -run TestAgent ./... -v build: + @mkdir -p build go build -ldflags "-s -w" -trimpath -o build/grokkit . install: build @@ -21,7 +23,7 @@ install: build @echo "✅ grokkit installed to ~/.local/bin" clean: - rm -rf build/ coverage.out coverage.html + rm -rf build/ help: @echo "Available targets:" diff --git a/cmd/chat_test.go b/cmd/chat_test.go new file mode 100644 index 0000000..df1c54a --- /dev/null +++ b/cmd/chat_test.go @@ -0,0 +1,93 @@ +package cmd + +import ( + "os" + "path/filepath" + "testing" +) + +func TestGetChatHistoryFile(t *testing.T) { + // Save original HOME + oldHome := os.Getenv("HOME") + defer os.Setenv("HOME", oldHome) + + tmpDir := t.TempDir() + os.Setenv("HOME", tmpDir) + + histFile := getChatHistoryFile() + expected := filepath.Join(tmpDir, ".config", "grokkit", "chat_history.json") + + if histFile != expected { + t.Errorf("getChatHistoryFile() = %q, want %q", histFile, expected) + } +} + +func TestLoadChatHistory_NoFile(t *testing.T) { + tmpDir := t.TempDir() + oldHome := os.Getenv("HOME") + os.Setenv("HOME", tmpDir) + defer os.Setenv("HOME", oldHome) + + history := loadChatHistory() + if history != nil { + t.Errorf("loadChatHistory() expected nil for non-existent file, got %v", history) + } +} + +func TestSaveAndLoadChatHistory(t *testing.T) { + tmpDir := t.TempDir() + oldHome := os.Getenv("HOME") + os.Setenv("HOME", tmpDir) + defer os.Setenv("HOME", oldHome) + + // Create test messages + messages := []map[string]string{ + {"role": "system", "content": "test system message"}, + {"role": "user", "content": "hello"}, + {"role": "assistant", "content": "hi there"}, + } + + // Save + err := saveChatHistory(messages) + if err != nil { + t.Fatalf("saveChatHistory() error: %v", err) + } + + // Load + loaded := loadChatHistory() + if loaded == nil { + t.Fatal("loadChatHistory() returned nil") + } + + if len(loaded) != len(messages) { + t.Errorf("loadChatHistory() length = %d, want %d", len(loaded), len(messages)) + } + + // Verify content + for i, msg := range loaded { + if msg["role"] != messages[i]["role"] { + t.Errorf("Message[%d] role = %q, want %q", i, msg["role"], messages[i]["role"]) + } + if msg["content"] != messages[i]["content"] { + t.Errorf("Message[%d] content = %q, want %q", i, msg["content"], messages[i]["content"]) + } + } +} + +func TestLoadChatHistory_InvalidJSON(t *testing.T) { + tmpDir := t.TempDir() + oldHome := os.Getenv("HOME") + os.Setenv("HOME", tmpDir) + defer os.Setenv("HOME", oldHome) + + // Create invalid JSON file + histDir := filepath.Join(tmpDir, ".config", "grokkit") + os.MkdirAll(histDir, 0755) + histFile := filepath.Join(histDir, "chat_history.json") + os.WriteFile(histFile, []byte("invalid json{{{"), 0644) + + history := loadChatHistory() + if history != nil { + t.Errorf("loadChatHistory() expected nil for invalid JSON, got %v", history) + } +} diff --git a/cmd/edit_helper_test.go b/cmd/edit_helper_test.go new file mode 100644 index 0000000..4f19aa0 --- /dev/null +++ b/cmd/edit_helper_test.go @@ -0,0 +1,53 @@ +package cmd + +import ( + "testing" +) + +func TestRemoveLastModifiedComments(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "removes last modified comment", + input: "// Last modified: 2024-01-01\npackage main\n\nfunc main() {}", + expected: "package main\n\nfunc main() {}", + }, + { + name: "removes multiple last modified comments", + input: "// Last modified: 2024-01-01\npackage main\n// Last modified by: user\nfunc main() {}", + expected: "package main\nfunc main() {}", + }, + { + name: "preserves code without last modified", + input: "package main\n\nfunc main() {}", + expected: "package main\n\nfunc main() {}", + }, + { + name: "handles empty string", + input: "", + expected: "", + }, + { + name: "preserves other comments", + input: "// This is a regular comment\npackage main\n// Last modified: 2024\n// Another comment\nfunc main() {}", + expected: "// This is a regular comment\npackage main\n// Another comment\nfunc main() {}", + }, + { + name: "handles line with only last modified", + input: "Last modified: 2024-01-01", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := removeLastModifiedComments(tt.input) + if result != tt.expected { + t.Errorf("removeLastModifiedComments() = %q, want %q", result, tt.expected) + } + }) + } +} diff --git a/internal/grok/cleancode_test.go b/internal/grok/cleancode_test.go new file mode 100644 index 0000000..65e1d6a --- /dev/null +++ b/internal/grok/cleancode_test.go @@ -0,0 +1,86 @@ +package grok + +import "testing" + +func TestCleanCodeResponse_Comprehensive(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "removes go markdown fences", + input: "```go\npackage main\n\nfunc main() {}\n```", + expected: "package main\n\nfunc main() {}", + }, + { + name: "removes python markdown fences", + input: "```python\ndef hello():\n print('hello')\n```", + expected: "def hello():\n print('hello')", + }, + { + name: "removes plain markdown fences", + input: "```\nsome code\n```", + expected: "some code", + }, + { + name: "handles no fences", + input: "func main() {}", + expected: "func main() {}", + }, + { + name: "preserves internal blank lines", + input: "```\nline1\n\nline2\n\nline3\n```", + expected: "line1\n\nline2\n\nline3", + }, + { + name: "trims leading whitespace", + input: " \n\n```\ncode\n```", + expected: "code", + }, + { + name: "trims trailing whitespace", + input: "```\ncode\n```\n\n ", + expected: "code", + }, + { + name: "handles multiple languages", + input: "```javascript\nconst x = 1;\n```", + expected: "const x = 1;", + }, + { + name: "handles fences with extra spaces", + input: "``` \ncode\n``` ", + expected: "code", + }, + { + name: "removes only fence lines", + input: "text before\n```go\ncode\n```\ntext after", + expected: "text before\n\ncode\n\ntext after", + }, + { + name: "handles empty input", + input: "", + expected: "", + }, + { + name: "handles only fences", + input: "```\n```", + expected: "", + }, + { + name: "preserves code indentation", + input: "```\nfunc main() {\n fmt.Println(\"hello\")\n}\n```", + expected: "func main() {\n fmt.Println(\"hello\")\n}", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := CleanCodeResponse(tt.input) + if result != tt.expected { + t.Errorf("CleanCodeResponse():\ngot: %q\nwant: %q", result, tt.expected) + } + }) + } +}