feat(cmd): add AI documentation generation and command tests
- Implemented `grokkit docs` command for generating language-specific documentation comments (godoc, PEP 257, Doxygen, etc.) with previews, backups, and auto-apply option - Extracted message builder functions for commit, history, pr-describe, and review commands - Added comprehensive unit tests for all command message builders (commit_test.go, docs_test.go, history_test.go, lint_test.go, prdescribe_test.go, review_test.go) - Enforced 70% test coverage threshold in CI workflow - Added .golangci.yml configuration with linters like govet, errcheck, staticcheck - Updated Makefile to include -race in tests and add help target - Updated README.md with new docs command details, workflows, and quality features - Added .claude/ to .gitignore - Configured default model for docs command in config.go
This commit is contained in:
parent
6e3a52728e
commit
0aa806be70
@ -40,9 +40,15 @@ jobs:
|
||||
run: |
|
||||
go tool cover -func=coverage.out | tail -1
|
||||
|
||||
- name: Enforce coverage threshold
|
||||
run: |
|
||||
PCT=$(go tool cover -func=coverage.out | tail -1 | awk '{print $3}' | tr -d '%')
|
||||
awk "BEGIN { exit ($PCT < 70) }" || (echo "Coverage ${PCT}% is below 70%" && exit 1)
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-gitea
|
||||
needs: [test]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@ -60,6 +66,7 @@ jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-gitea
|
||||
needs: [test]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,6 @@
|
||||
.idea/
|
||||
.junie/
|
||||
.claude/
|
||||
build/
|
||||
grokkit
|
||||
*.bak
|
||||
|
||||
22
.golangci.yml
Normal file
22
.golangci.yml
Normal file
@ -0,0 +1,22 @@
|
||||
version: "2"
|
||||
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- govet
|
||||
- errcheck
|
||||
- staticcheck
|
||||
- ineffassign
|
||||
- unused
|
||||
- gosimple
|
||||
- typecheck
|
||||
- misspell
|
||||
- gofmt
|
||||
|
||||
settings:
|
||||
errcheck:
|
||||
check-type-assertions: true
|
||||
check-blank: false
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
4
Makefile
4
Makefile
@ -1,11 +1,11 @@
|
||||
.PHONY: test test-cover test-agent lint build install clean
|
||||
.PHONY: test test-cover test-agent lint build install clean help
|
||||
|
||||
VERSION ?= dev-$(shell git describe --tags --always --dirty 2>/dev/null || echo unknown)
|
||||
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo unknown)
|
||||
|
||||
test:
|
||||
go test ./... -v
|
||||
go test ./... -v -race
|
||||
|
||||
test-cover:
|
||||
@mkdir -p build
|
||||
|
||||
80
README.md
80
README.md
@ -37,6 +37,15 @@ grokkit version
|
||||
## 📋 Table of Contents
|
||||
|
||||
- [Commands](#commands)
|
||||
- [chat](#-grokkit-chat)
|
||||
- [edit](#-grokkit-edit-file-instruction)
|
||||
- [commit / commitmsg](#-grokkit-commitmsg)
|
||||
- [review](#-grokkit-review)
|
||||
- [pr-describe](#-grokkit-pr-describe)
|
||||
- [history](#-grokkit-history)
|
||||
- [lint](#-grokkit-lint-file)
|
||||
- [docs](#-grokkit-docs-file)
|
||||
- [agent](#-grokkit-agent)
|
||||
- [Configuration](#configuration)
|
||||
- [Workflows](#workflows)
|
||||
- [Shell Completions](#shell-completions)
|
||||
@ -148,6 +157,41 @@ Summarize recent git commits.
|
||||
grokkit history # Last 10 commits
|
||||
```
|
||||
|
||||
### 📖 `grokkit docs <file> [file...]`
|
||||
Generate language-appropriate documentation comments using Grok AI.
|
||||
|
||||
```bash
|
||||
# Preview and confirm
|
||||
grokkit docs main.go
|
||||
|
||||
# Auto-apply without confirmation
|
||||
grokkit docs handlers.go models.go --auto-apply
|
||||
|
||||
# Document multiple files at once
|
||||
grokkit docs cmd/*.go --auto-apply
|
||||
|
||||
# Use specific model
|
||||
grokkit docs app.py -m grok-4
|
||||
```
|
||||
|
||||
**Supported doc styles by language:**
|
||||
|
||||
| Language | Style |
|
||||
|----------|-------|
|
||||
| Go | godoc (`// FuncName does...`) |
|
||||
| Python | PEP 257 docstrings (`"""Summary\n\nArgs:..."""`) |
|
||||
| C / C++ | Doxygen (`/** @brief ... @param ... @return ... */`) |
|
||||
| JavaScript / TypeScript | JSDoc (`/** @param {type} name ... */`) |
|
||||
| Rust | rustdoc (`/// Summary\n/// # Arguments`) |
|
||||
| Ruby | YARD (`# @param [Type] name`) |
|
||||
| Java | Javadoc (`/** @param ... @return ... */`) |
|
||||
| Shell | Shell comments (`# function: desc, # Args: ...`) |
|
||||
|
||||
**Safety features:**
|
||||
- Creates `.bak` backup before any changes
|
||||
- Shows first 50 lines of documented code as preview
|
||||
- Requires confirmation (unless `--auto-apply`)
|
||||
|
||||
### 🤖 `grokkit agent`
|
||||
Multi-file agent for complex refactoring (experimental).
|
||||
|
||||
@ -315,6 +359,23 @@ grokkit review
|
||||
grokkit commit
|
||||
```
|
||||
|
||||
### Documentation Generation Workflow
|
||||
|
||||
```bash
|
||||
# 1. Preview docs for a single file
|
||||
grokkit docs internal/api/handler.go
|
||||
|
||||
# 2. Batch-document a package
|
||||
grokkit docs cmd/*.go --auto-apply
|
||||
|
||||
# 3. Document across languages in one pass
|
||||
grokkit docs lib/utils.py src/helpers.ts --auto-apply
|
||||
|
||||
# 4. Review and commit
|
||||
grokkit review
|
||||
grokkit commit
|
||||
```
|
||||
|
||||
## Shell Completions
|
||||
|
||||
Generate shell completions for faster command entry:
|
||||
@ -369,10 +430,13 @@ grokkit review -v
|
||||
- ✅ **Safe file editing** - Automatic backups, preview, confirmation
|
||||
- ✅ **Git workflow integration** - Commit messages, reviews, PR descriptions
|
||||
- ✅ **Multi-language linting** - 9 languages supported with AI-powered fixes
|
||||
- ✅ **AI documentation generation** - 8 doc styles (godoc, PEP 257, Doxygen, JSDoc, rustdoc, YARD, Javadoc, shell)
|
||||
|
||||
### Quality & Testing
|
||||
- ✅ **Test coverage 72%** - Comprehensive unit tests
|
||||
- ✅ **CI/CD with Gitea Actions** - Automated testing and builds
|
||||
- ✅ **Test coverage 72%+** - Comprehensive unit tests including all command message builders
|
||||
- ✅ **Coverage gate in CI** - Builds fail if coverage drops below 70%
|
||||
- ✅ **CI/CD with Gitea Actions** - Tests must pass before lint and build jobs run
|
||||
- ✅ **Golangci-lint configured** - `.golangci.yml` with govet, errcheck, staticcheck, and more
|
||||
- ✅ **Interface-based design** - Testable and maintainable
|
||||
- ✅ **Zero external dependencies** - Only stdlib + well-known libs
|
||||
|
||||
@ -413,7 +477,7 @@ make test-agent
|
||||
make test-cover
|
||||
open build/coverage.html
|
||||
|
||||
# Linting (matches CI)
|
||||
# Linting (matches CI, uses .golangci.yml config)
|
||||
make lint
|
||||
# Install golangci-lint if needed:
|
||||
# go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
@ -436,11 +500,14 @@ make clean
|
||||
```
|
||||
grokkit/
|
||||
├── cmd/ # CLI commands (Cobra)
|
||||
│ ├── docs.go # grokkit docs — AI documentation generation
|
||||
│ ├── lint.go # grokkit lint — AI-powered linting
|
||||
│ └── ... # chat, edit, commit, review, history, pr-describe, agent
|
||||
├── config/ # Viper configuration
|
||||
├── docs/ # Documentation
|
||||
├── todo/ # TODO tracking: queued/ (pending) and completed/ (historical record)
|
||||
│ ├── queued/ # Pending TODO items
|
||||
│ └── completed/ # Completed TODO items with history
|
||||
├── todo/ # TODO tracking: queued/ (pending) and completed/ (historical record)
|
||||
│ ├── queued/ # Pending TODO items
|
||||
│ └── completed/ # Completed TODO items with history
|
||||
├── internal/
|
||||
│ ├── errors/ # Custom error types
|
||||
│ ├── git/ # Git operations
|
||||
@ -451,6 +518,7 @@ grokkit/
|
||||
├── main.go # Application entrypoint
|
||||
├── go.mod # Dependencies
|
||||
├── Makefile # Build automation
|
||||
├── .golangci.yml # Golangci-lint configuration
|
||||
└── scripts/ # Install scripts
|
||||
```
|
||||
|
||||
|
||||
@ -28,10 +28,7 @@ var commitCmd = &cobra.Command{
|
||||
model := config.GetModel("commit", modelFlag)
|
||||
|
||||
client := grok.NewClient()
|
||||
messages := []map[string]string{
|
||||
{"role": "system", "content": "Return ONLY a conventional commit message (type(scope): subject\n\nbody)."},
|
||||
{"role": "user", "content": fmt.Sprintf("Staged changes:\n%s", diff)},
|
||||
}
|
||||
messages := buildCommitMessages(diff)
|
||||
color.Yellow("Generating commit message...")
|
||||
msg := client.Stream(messages, model)
|
||||
|
||||
@ -54,3 +51,10 @@ var commitCmd = &cobra.Command{
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func buildCommitMessages(diff string) []map[string]string {
|
||||
return []map[string]string{
|
||||
{"role": "system", "content": "Return ONLY a conventional commit message (type(scope): subject\n\nbody)."},
|
||||
{"role": "user", "content": fmt.Sprintf("Staged changes:\n%s", diff)},
|
||||
}
|
||||
}
|
||||
|
||||
52
cmd/commit_test.go
Normal file
52
cmd/commit_test.go
Normal file
@ -0,0 +1,52 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildCommitMessages(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
diff string
|
||||
sysCheck string
|
||||
usrCheck []string
|
||||
}{
|
||||
{
|
||||
name: "normal diff",
|
||||
diff: "diff --git a/foo.go b/foo.go\n+func bar() {}",
|
||||
sysCheck: "conventional commit",
|
||||
usrCheck: []string{"diff --git a/foo.go", "Staged changes:"},
|
||||
},
|
||||
{
|
||||
name: "empty diff",
|
||||
diff: "",
|
||||
sysCheck: "conventional commit",
|
||||
usrCheck: []string{"Staged changes:"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
msgs := buildCommitMessages(tt.diff)
|
||||
|
||||
if len(msgs) != 2 {
|
||||
t.Fatalf("expected 2 messages, got %d", len(msgs))
|
||||
}
|
||||
if msgs[0]["role"] != "system" {
|
||||
t.Errorf("first message role = %q, want %q", msgs[0]["role"], "system")
|
||||
}
|
||||
if msgs[1]["role"] != "user" {
|
||||
t.Errorf("second message role = %q, want %q", msgs[1]["role"], "user")
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(msgs[0]["content"]), strings.ToLower(tt.sysCheck)) {
|
||||
t.Errorf("system prompt missing %q; got: %s", tt.sysCheck, msgs[0]["content"])
|
||||
}
|
||||
for _, check := range tt.usrCheck {
|
||||
if !strings.Contains(msgs[1]["content"], check) {
|
||||
t.Errorf("user prompt missing %q", check)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
185
cmd/docs.go
Normal file
185
cmd/docs.go
Normal file
@ -0,0 +1,185 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"gmgauthier.com/grokkit/config"
|
||||
"gmgauthier.com/grokkit/internal/grok"
|
||||
"gmgauthier.com/grokkit/internal/linter"
|
||||
"gmgauthier.com/grokkit/internal/logger"
|
||||
)
|
||||
|
||||
var autoApply bool
|
||||
|
||||
var docsCmd = &cobra.Command{
|
||||
Use: "docs <file> [file...]",
|
||||
Short: "Generate documentation comments for source files",
|
||||
Long: `Detects the programming language of each file and uses Grok AI to generate
|
||||
language-appropriate documentation comments.
|
||||
|
||||
Supported doc styles:
|
||||
Go godoc (// FuncName does...)
|
||||
Python PEP 257 docstrings
|
||||
C/C++ Doxygen (/** @brief ... */)
|
||||
JS/TS JSDoc (/** @param ... */)
|
||||
Rust rustdoc (/// Summary)
|
||||
Ruby YARD (# @param ...)
|
||||
Java Javadoc (/** @param ... */)
|
||||
Shell Shell comments (# function: ...)
|
||||
|
||||
Safety features:
|
||||
- Creates .bak backup before modifying files
|
||||
- Shows preview of changes before applying
|
||||
- Requires confirmation unless --auto-apply is used`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: runDocs,
|
||||
}
|
||||
|
||||
func init() {
|
||||
docsCmd.Flags().BoolVar(&autoApply, "auto-apply", false, "Apply documentation without confirmation")
|
||||
}
|
||||
|
||||
func runDocs(cmd *cobra.Command, args []string) {
|
||||
modelFlag, _ := cmd.Flags().GetString("model")
|
||||
model := config.GetModel("docs", modelFlag)
|
||||
|
||||
client := grok.NewClient()
|
||||
for _, filePath := range args {
|
||||
processDocsFile(client, model, filePath)
|
||||
}
|
||||
}
|
||||
|
||||
func processDocsFile(client *grok.Client, model, filePath string) {
|
||||
logger.Info("starting docs operation", "file", filePath)
|
||||
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
logger.Error("file not found", "file", filePath)
|
||||
color.Red("❌ File not found: %s", filePath)
|
||||
return
|
||||
}
|
||||
|
||||
lang, err := linter.DetectLanguage(filePath)
|
||||
if err != nil {
|
||||
logger.Warn("unsupported language", "file", filePath, "error", err)
|
||||
color.Yellow("⚠️ Skipping %s: %v", filePath, err)
|
||||
return
|
||||
}
|
||||
|
||||
originalContent, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
logger.Error("failed to read file", "file", filePath, "error", err)
|
||||
color.Red("❌ Failed to read file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
color.Cyan("📝 Generating %s docs for: %s", lang.Name, filePath)
|
||||
logger.Info("requesting AI documentation", "file", filePath, "language", lang.Name)
|
||||
|
||||
messages := buildDocsMessages(lang.Name, string(originalContent))
|
||||
response := client.StreamSilent(messages, model)
|
||||
|
||||
if response == "" {
|
||||
logger.Error("AI returned empty response", "file", filePath)
|
||||
color.Red("❌ Failed to get AI response")
|
||||
return
|
||||
}
|
||||
|
||||
documented := grok.CleanCodeResponse(response)
|
||||
logger.Info("received AI documentation", "file", filePath, "size", len(documented))
|
||||
|
||||
// Show preview
|
||||
fmt.Println()
|
||||
color.Cyan("📋 Preview of documented code:")
|
||||
fmt.Println(strings.Repeat("-", 80))
|
||||
lines := strings.Split(documented, "\n")
|
||||
previewLines := 50
|
||||
if len(lines) < previewLines {
|
||||
previewLines = len(lines)
|
||||
}
|
||||
for i := 0; i < previewLines; i++ {
|
||||
fmt.Println(lines[i])
|
||||
}
|
||||
if len(lines) > previewLines {
|
||||
fmt.Printf("... (%d more lines)\n", len(lines)-previewLines)
|
||||
}
|
||||
fmt.Println(strings.Repeat("-", 80))
|
||||
fmt.Println()
|
||||
|
||||
if !autoApply {
|
||||
color.Yellow("Apply documentation to %s? (y/N): ", filePath)
|
||||
var confirm string
|
||||
if _, err := fmt.Scanln(&confirm); err != nil {
|
||||
color.Yellow("❌ Cancelled. No changes made to: %s", filePath)
|
||||
return
|
||||
}
|
||||
confirm = strings.ToLower(strings.TrimSpace(confirm))
|
||||
if confirm != "y" && confirm != "yes" {
|
||||
logger.Info("user cancelled docs application", "file", filePath)
|
||||
color.Yellow("❌ Cancelled. No changes made to: %s", filePath)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Create backup
|
||||
backupPath := filePath + ".bak"
|
||||
if err := os.WriteFile(backupPath, originalContent, 0644); err != nil {
|
||||
logger.Error("failed to create backup", "file", filePath, "error", err)
|
||||
color.Red("❌ Failed to create backup: %v", err)
|
||||
return
|
||||
}
|
||||
logger.Info("backup created", "backup", backupPath)
|
||||
|
||||
if err := os.WriteFile(filePath, []byte(documented), 0644); err != nil {
|
||||
logger.Error("failed to write documented file", "file", filePath, "error", err)
|
||||
color.Red("❌ Failed to write file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("documentation applied successfully", "file", filePath)
|
||||
color.Green("✅ Documentation applied: %s", filePath)
|
||||
color.Cyan("💾 Original saved to: %s", backupPath)
|
||||
}
|
||||
|
||||
func buildDocsMessages(language, code string) []map[string]string {
|
||||
style := docStyle(language)
|
||||
systemPrompt := fmt.Sprintf(
|
||||
"You are a documentation expert. Add %s documentation comments to the provided code. "+
|
||||
"Return ONLY the documented code with no explanations, markdown, or extra text. "+
|
||||
"Do NOT include markdown code fences. Document all public functions, methods, types, and constants.",
|
||||
style,
|
||||
)
|
||||
|
||||
userPrompt := fmt.Sprintf("Add documentation comments to the following %s code:\n\n%s", language, code)
|
||||
|
||||
return []map[string]string{
|
||||
{"role": "system", "content": systemPrompt},
|
||||
{"role": "user", "content": userPrompt},
|
||||
}
|
||||
}
|
||||
|
||||
func docStyle(language string) string {
|
||||
switch strings.ToLower(language) {
|
||||
case "go":
|
||||
return "godoc"
|
||||
case "python":
|
||||
return "PEP 257 docstring"
|
||||
case "c", "c++":
|
||||
return "Doxygen"
|
||||
case "javascript", "typescript":
|
||||
return "JSDoc"
|
||||
case "rust":
|
||||
return "rustdoc"
|
||||
case "ruby":
|
||||
return "YARD"
|
||||
case "java":
|
||||
return "Javadoc"
|
||||
case "shell", "bash":
|
||||
return "shell comment"
|
||||
default:
|
||||
return "standard documentation comment"
|
||||
}
|
||||
}
|
||||
78
cmd/docs_test.go
Normal file
78
cmd/docs_test.go
Normal file
@ -0,0 +1,78 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildDocsMessages(t *testing.T) {
|
||||
tests := []struct {
|
||||
language string
|
||||
code string
|
||||
styleCheck string
|
||||
}{
|
||||
{language: "Go", code: "package main\nfunc Foo() {}", styleCheck: "godoc"},
|
||||
{language: "Python", code: "def foo():\n pass", styleCheck: "PEP 257"},
|
||||
{language: "C", code: "int foo(void) { return 0; }", styleCheck: "Doxygen"},
|
||||
{language: "C++", code: "int foo() { return 0; }", styleCheck: "Doxygen"},
|
||||
{language: "JavaScript", code: "function foo() {}", styleCheck: "JSDoc"},
|
||||
{language: "TypeScript", code: "function foo(): void {}", styleCheck: "JSDoc"},
|
||||
{language: "Rust", code: "pub fn foo() {}", styleCheck: "rustdoc"},
|
||||
{language: "Ruby", code: "def foo; end", styleCheck: "YARD"},
|
||||
{language: "Java", code: "public void foo() {}", styleCheck: "Javadoc"},
|
||||
{language: "Shell", code: "foo() { echo hello; }", styleCheck: "shell comment"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.language, func(t *testing.T) {
|
||||
msgs := buildDocsMessages(tt.language, tt.code)
|
||||
|
||||
if len(msgs) != 2 {
|
||||
t.Fatalf("expected 2 messages, got %d", len(msgs))
|
||||
}
|
||||
if msgs[0]["role"] != "system" {
|
||||
t.Errorf("first message role = %q, want %q", msgs[0]["role"], "system")
|
||||
}
|
||||
if msgs[1]["role"] != "user" {
|
||||
t.Errorf("second message role = %q, want %q", msgs[1]["role"], "user")
|
||||
}
|
||||
if !strings.Contains(msgs[0]["content"], tt.styleCheck) {
|
||||
t.Errorf("system prompt missing %q for language %s; got: %s",
|
||||
tt.styleCheck, tt.language, msgs[0]["content"])
|
||||
}
|
||||
if !strings.Contains(msgs[1]["content"], tt.code) {
|
||||
t.Errorf("user prompt missing code for language %s", tt.language)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocStyle(t *testing.T) {
|
||||
tests := []struct {
|
||||
language string
|
||||
want string
|
||||
}{
|
||||
{"go", "godoc"},
|
||||
{"Go", "godoc"},
|
||||
{"python", "PEP 257 docstring"},
|
||||
{"c", "Doxygen"},
|
||||
{"c++", "Doxygen"},
|
||||
{"javascript", "JSDoc"},
|
||||
{"typescript", "JSDoc"},
|
||||
{"rust", "rustdoc"},
|
||||
{"ruby", "YARD"},
|
||||
{"java", "Javadoc"},
|
||||
{"shell", "shell comment"},
|
||||
{"bash", "shell comment"},
|
||||
{"unknown", "standard documentation comment"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.language, func(t *testing.T) {
|
||||
got := docStyle(tt.language)
|
||||
if got != tt.want {
|
||||
t.Errorf("docStyle(%q) = %q, want %q", tt.language, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -26,11 +26,15 @@ var historyCmd = &cobra.Command{
|
||||
model := config.GetModel("history", modelFlag)
|
||||
|
||||
client := grok.NewClient()
|
||||
messages := []map[string]string{
|
||||
{"role": "system", "content": "Summarize the recent git history in 3-5 bullet points."},
|
||||
{"role": "user", "content": log},
|
||||
}
|
||||
messages := buildHistoryMessages(log)
|
||||
color.Yellow("Summarizing recent commits...")
|
||||
client.Stream(messages, model)
|
||||
},
|
||||
}
|
||||
|
||||
func buildHistoryMessages(log string) []map[string]string {
|
||||
return []map[string]string{
|
||||
{"role": "system", "content": "Summarize the recent git history in 3-5 bullet points."},
|
||||
{"role": "user", "content": log},
|
||||
}
|
||||
}
|
||||
|
||||
53
cmd/history_test.go
Normal file
53
cmd/history_test.go
Normal file
@ -0,0 +1,53 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildHistoryMessages(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
log string
|
||||
sysCheck string
|
||||
usrCheck string
|
||||
}{
|
||||
{
|
||||
name: "with recent commits",
|
||||
log: "abc1234 feat: add new feature\ndef5678 fix: resolve bug",
|
||||
sysCheck: "git history",
|
||||
usrCheck: "abc1234 feat: add new feature",
|
||||
},
|
||||
{
|
||||
name: "empty log",
|
||||
log: "",
|
||||
sysCheck: "git history",
|
||||
usrCheck: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
msgs := buildHistoryMessages(tt.log)
|
||||
|
||||
if len(msgs) != 2 {
|
||||
t.Fatalf("expected 2 messages, got %d", len(msgs))
|
||||
}
|
||||
if msgs[0]["role"] != "system" {
|
||||
t.Errorf("first message role = %q, want %q", msgs[0]["role"], "system")
|
||||
}
|
||||
if msgs[1]["role"] != "user" {
|
||||
t.Errorf("second message role = %q, want %q", msgs[1]["role"], "user")
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(msgs[0]["content"]), strings.ToLower(tt.sysCheck)) {
|
||||
t.Errorf("system prompt missing %q; got: %s", tt.sysCheck, msgs[0]["content"])
|
||||
}
|
||||
if tt.usrCheck != "" && !strings.Contains(msgs[1]["content"], tt.usrCheck) {
|
||||
t.Errorf("user content missing %q", tt.usrCheck)
|
||||
}
|
||||
if msgs[1]["content"] != tt.log {
|
||||
t.Errorf("user content = %q, want %q", msgs[1]["content"], tt.log)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
68
cmd/lint_test.go
Normal file
68
cmd/lint_test.go
Normal file
@ -0,0 +1,68 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gmgauthier.com/grokkit/internal/linter"
|
||||
)
|
||||
|
||||
func TestBuildLintFixMessages(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
result *linter.LintResult
|
||||
code string
|
||||
wantLen int
|
||||
sysCheck string
|
||||
usrCheck []string
|
||||
}{
|
||||
{
|
||||
name: "go file with issues",
|
||||
result: &linter.LintResult{
|
||||
Language: "Go",
|
||||
LinterUsed: "golangci-lint",
|
||||
Output: "line 5: unused variable x",
|
||||
},
|
||||
code: "package main\nfunc main() { x := 1 }",
|
||||
wantLen: 2,
|
||||
sysCheck: "code quality",
|
||||
usrCheck: []string{"Go", "golangci-lint", "unused variable x", "package main"},
|
||||
},
|
||||
{
|
||||
name: "python file with issues",
|
||||
result: &linter.LintResult{
|
||||
Language: "Python",
|
||||
LinterUsed: "flake8",
|
||||
Output: "E501 line too long",
|
||||
},
|
||||
code: "def foo():\n pass",
|
||||
wantLen: 2,
|
||||
sysCheck: "code quality",
|
||||
usrCheck: []string{"Python", "flake8", "E501 line too long", "def foo"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
msgs := buildLintFixMessages(tt.result, tt.code)
|
||||
|
||||
if len(msgs) != tt.wantLen {
|
||||
t.Fatalf("expected %d messages, got %d", tt.wantLen, len(msgs))
|
||||
}
|
||||
if msgs[0]["role"] != "system" {
|
||||
t.Errorf("first message role = %q, want %q", msgs[0]["role"], "system")
|
||||
}
|
||||
if msgs[1]["role"] != "user" {
|
||||
t.Errorf("second message role = %q, want %q", msgs[1]["role"], "user")
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(msgs[0]["content"]), strings.ToLower(tt.sysCheck)) {
|
||||
t.Errorf("system prompt missing %q; got: %s", tt.sysCheck, msgs[0]["content"])
|
||||
}
|
||||
for _, check := range tt.usrCheck {
|
||||
if !strings.Contains(msgs[1]["content"], check) {
|
||||
t.Errorf("user prompt missing %q", check)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -30,11 +30,15 @@ var prDescribeCmd = &cobra.Command{
|
||||
model := config.GetModel("prdescribe", modelFlag)
|
||||
|
||||
client := grok.NewClient()
|
||||
messages := []map[string]string{
|
||||
{"role": "system", "content": "Write a professional GitHub PR title + detailed body (changes, motivation, testing notes)."},
|
||||
{"role": "user", "content": fmt.Sprintf("Diff:\n%s", diff)},
|
||||
}
|
||||
messages := buildPRDescribeMessages(diff)
|
||||
color.Yellow("Writing PR description...")
|
||||
client.Stream(messages, model)
|
||||
},
|
||||
}
|
||||
|
||||
func buildPRDescribeMessages(diff string) []map[string]string {
|
||||
return []map[string]string{
|
||||
{"role": "system", "content": "Write a professional GitHub PR title + detailed body (changes, motivation, testing notes)."},
|
||||
{"role": "user", "content": fmt.Sprintf("Diff:\n%s", diff)},
|
||||
}
|
||||
}
|
||||
|
||||
52
cmd/prdescribe_test.go
Normal file
52
cmd/prdescribe_test.go
Normal file
@ -0,0 +1,52 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildPRDescribeMessages(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
diff string
|
||||
sysCheck string
|
||||
usrCheck []string
|
||||
}{
|
||||
{
|
||||
name: "branch with changes",
|
||||
diff: "diff --git a/cmd/new.go b/cmd/new.go\n+package cmd",
|
||||
sysCheck: "PR",
|
||||
usrCheck: []string{"Diff:", "diff --git a/cmd/new.go"},
|
||||
},
|
||||
{
|
||||
name: "empty diff",
|
||||
diff: "",
|
||||
sysCheck: "PR",
|
||||
usrCheck: []string{"Diff:"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
msgs := buildPRDescribeMessages(tt.diff)
|
||||
|
||||
if len(msgs) != 2 {
|
||||
t.Fatalf("expected 2 messages, got %d", len(msgs))
|
||||
}
|
||||
if msgs[0]["role"] != "system" {
|
||||
t.Errorf("first message role = %q, want %q", msgs[0]["role"], "system")
|
||||
}
|
||||
if msgs[1]["role"] != "user" {
|
||||
t.Errorf("second message role = %q, want %q", msgs[1]["role"], "user")
|
||||
}
|
||||
if !strings.Contains(msgs[0]["content"], tt.sysCheck) {
|
||||
t.Errorf("system prompt missing %q; got: %s", tt.sysCheck, msgs[0]["content"])
|
||||
}
|
||||
for _, check := range tt.usrCheck {
|
||||
if !strings.Contains(msgs[1]["content"], check) {
|
||||
t.Errorf("user prompt missing %q", check)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -29,11 +29,15 @@ var reviewCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
messages := []map[string]string{
|
||||
{"role": "system", "content": "You are an expert code reviewer. Give a concise summary + 3-5 actionable improvements."},
|
||||
{"role": "user", "content": fmt.Sprintf("Git status:\n%s\n\nGit diff:\n%s", status, diff)},
|
||||
}
|
||||
messages := buildReviewMessages(status, diff)
|
||||
color.Yellow("Grok is reviewing the repo...")
|
||||
client.Stream(messages, model)
|
||||
},
|
||||
}
|
||||
|
||||
func buildReviewMessages(status, diff string) []map[string]string {
|
||||
return []map[string]string{
|
||||
{"role": "system", "content": "You are an expert code reviewer. Give a concise summary + 3-5 actionable improvements."},
|
||||
{"role": "user", "content": fmt.Sprintf("Git status:\n%s\n\nGit diff:\n%s", status, diff)},
|
||||
}
|
||||
}
|
||||
|
||||
55
cmd/review_test.go
Normal file
55
cmd/review_test.go
Normal file
@ -0,0 +1,55 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildReviewMessages(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
status string
|
||||
diff string
|
||||
sysCheck string
|
||||
usrCheck []string
|
||||
}{
|
||||
{
|
||||
name: "with status and diff",
|
||||
status: "M main.go",
|
||||
diff: "diff --git a/main.go b/main.go\n+func foo() {}",
|
||||
sysCheck: "code reviewer",
|
||||
usrCheck: []string{"M main.go", "diff --git a/main.go"},
|
||||
},
|
||||
{
|
||||
name: "empty diff",
|
||||
status: "",
|
||||
diff: "",
|
||||
sysCheck: "code reviewer",
|
||||
usrCheck: []string{"Git status:", "Git diff:"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
msgs := buildReviewMessages(tt.status, tt.diff)
|
||||
|
||||
if len(msgs) != 2 {
|
||||
t.Fatalf("expected 2 messages, got %d", len(msgs))
|
||||
}
|
||||
if msgs[0]["role"] != "system" {
|
||||
t.Errorf("first message role = %q, want %q", msgs[0]["role"], "system")
|
||||
}
|
||||
if msgs[1]["role"] != "user" {
|
||||
t.Errorf("second message role = %q, want %q", msgs[1]["role"], "user")
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(msgs[0]["content"]), strings.ToLower(tt.sysCheck)) {
|
||||
t.Errorf("system prompt missing %q; got: %s", tt.sysCheck, msgs[0]["content"])
|
||||
}
|
||||
for _, check := range tt.usrCheck {
|
||||
if !strings.Contains(msgs[1]["content"], check) {
|
||||
t.Errorf("user prompt missing %q", check)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -56,6 +56,7 @@ func init() {
|
||||
rootCmd.AddCommand(agentCmd)
|
||||
rootCmd.AddCommand(completionCmd)
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
rootCmd.AddCommand(docsCmd)
|
||||
|
||||
// Add model flag to all commands
|
||||
rootCmd.PersistentFlags().StringP("model", "m", "", "Grok model to use (overrides config)")
|
||||
|
||||
@ -35,6 +35,7 @@ func Load() {
|
||||
viper.SetDefault("commands.lint.model", "grok-4-1-fast-non-reasoning")
|
||||
viper.SetDefault("commands.prdescribe.model", "grok-4")
|
||||
viper.SetDefault("commands.review.model", "grok-4")
|
||||
viper.SetDefault("commands.docs.model", "grok-4")
|
||||
|
||||
// Config file is optional, so we ignore read errors
|
||||
_ = viper.ReadInConfig()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user