test: add unit tests for chat history, edit helper, and code cleaning

- Introduce tests for chat history file handling, loading/saving, and error cases in cmd/chat_test.go
- Add tests for removeLastModifiedComments in cmd/edit_helper_test.go
- Add comprehensive tests for CleanCodeResponse in internal/grok/cleancode_test.go
- Update Makefile to centralize build artifacts in build/ directory for coverage reports and binary
- Adjust .gitignore to ignore chat_history.json and remove obsolete coverage file entries
This commit is contained in:
Greg Gauthier 2026-03-01 12:44:20 +00:00
parent 8b6449c947
commit 13519438a2
5 changed files with 238 additions and 6 deletions

2
.gitignore vendored
View File

@ -5,6 +5,4 @@ grokkit
*.log
*.tmp
.env
coverage.out
coverage.html
chat_history.json

View File

@ -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:"

93
cmd/chat_test.go Normal file
View File

@ -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)
}
}

53
cmd/edit_helper_test.go Normal file
View File

@ -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)
}
})
}
}

View File

@ -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)
}
})
}
}