open-code-review/cmd/opencodereview/scan_cmd_test.go
kite 9a11fc1302 test: improve coverage for cmd/opencodereview package from 42% to 70%
Add comprehensive unit tests covering config dispatch, emit run result,
git helpers, provider commands, provider TUI pure functions and View
rendering, shared utilities, flags parsing, scan command flags, and
small file entry points (version, viewer, llm, rules commands).
2026-06-27 01:47:48 +08:00

249 lines
7 KiB
Go

package main
import (
"reflect"
"strings"
"testing"
"github.com/open-code-review/open-code-review/internal/llm"
)
func TestExcludeToolDef(t *testing.T) {
defs := []llm.ToolDef{
{Type: "function", Function: llm.FunctionDef{Name: "task_done"}},
{Type: "function", Function: llm.FunctionDef{Name: "file_read"}},
{Type: "function", Function: llm.FunctionDef{Name: "file_read_diff"}},
{Type: "function", Function: llm.FunctionDef{Name: "code_comment"}},
}
got := excludeToolDef(defs, "file_read_diff")
if len(got) != 3 {
t.Fatalf("expected 3 defs, got %d", len(got))
}
for _, d := range got {
if d.Function.Name == "file_read_diff" {
t.Errorf("file_read_diff should have been removed")
}
}
// Input slice must not be mutated.
if len(defs) != 4 {
t.Errorf("input slice was mutated: len=%d, want 4", len(defs))
}
}
func TestExcludeToolDef_AbsentName(t *testing.T) {
defs := []llm.ToolDef{
{Type: "function", Function: llm.FunctionDef{Name: "task_done"}},
}
got := excludeToolDef(defs, "does_not_exist")
if !reflect.DeepEqual(got, defs) {
t.Errorf("removing absent name should return identical content")
}
}
func TestSplitPaths(t *testing.T) {
tests := []struct {
name string
in string
want []string
}{
{"empty", "", nil},
{"single", "internal/agent", []string{"internal/agent"}},
{"multiple", "a.go,b.go,c.go", []string{"a.go", "b.go", "c.go"}},
{"trims whitespace", " a.go , b.go ", []string{"a.go", "b.go"}},
{"drops empty segments", "a.go,,b.go,", []string{"a.go", "b.go"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := splitPaths(tt.in)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("splitPaths(%q) = %v, want %v", tt.in, got, tt.want)
}
})
}
}
func TestParseScanFlags_BareCommandScansWholeRepo(t *testing.T) {
opts, err := parseScanFlags([]string{}) // no flags
if err != nil {
t.Fatalf("bare `ocr scan` should not error, got: %v", err)
}
if opts.paths != "" {
t.Errorf("default paths should be empty (= whole repo), got %q", opts.paths)
}
}
func TestParseScanFlags_RejectsInvalidAudience(t *testing.T) {
_, err := parseScanFlags([]string{"--audience", "robot"})
if err == nil {
t.Fatal("expected error for invalid --audience")
}
if !strings.Contains(err.Error(), "invalid --audience") {
t.Errorf("error message = %q; want invalid --audience", err.Error())
}
}
func TestParseScanFlags_RejectsNegativeMaxTools(t *testing.T) {
_, err := parseScanFlags([]string{"--max-tools", "-1"})
if err == nil {
t.Fatal("expected error for negative --max-tools")
}
if !strings.Contains(err.Error(), "--max-tools") {
t.Errorf("error message = %q; want it to mention --max-tools", err.Error())
}
}
func TestParseScanFlags_RejectsNegativeMaxGitProcs(t *testing.T) {
_, err := parseScanFlags([]string{"--max-git-procs", "-3"})
if err == nil {
t.Fatal("expected error for negative --max-git-procs")
}
if !strings.Contains(err.Error(), "--max-git-procs") {
t.Errorf("error message = %q; want it to mention --max-git-procs", err.Error())
}
}
func TestParseScanFlags_DefaultsValid(t *testing.T) {
opts, err := parseScanFlags([]string{}) // bare command
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if opts.paths != "" {
t.Errorf("opts = %+v; want paths empty (whole repo)", opts)
}
if opts.audience != "human" {
t.Errorf("default audience = %q, want \"human\"", opts.audience)
}
if opts.outputFormat != "text" {
t.Errorf("default outputFormat = %q, want \"text\"", opts.outputFormat)
}
if opts.concurrency != 8 {
t.Errorf("default concurrency = %d, want 8", opts.concurrency)
}
}
func TestParseScanFlags_PathNarrowsScope(t *testing.T) {
opts, err := parseScanFlags([]string{"--path", "internal/agent,internal/diff"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := splitPaths(opts.paths); !reflect.DeepEqual(got, []string{"internal/agent", "internal/diff"}) {
t.Errorf("splitPaths(opts.paths) = %v", got)
}
}
func TestParseScanFlags_HelpFlag(t *testing.T) {
opts, err := parseScanFlags([]string{"-h"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !opts.showHelp {
t.Error("opts.showHelp should be true when -h is supplied")
}
}
func TestParseScanFlags_RejectsNegativeMaxTokensBudget(t *testing.T) {
_, err := parseScanFlags([]string{"--max-tokens-budget", "-100"})
if err == nil {
t.Fatal("expected error for negative --max-tokens-budget")
}
if !strings.Contains(err.Error(), "--max-tokens-budget") {
t.Errorf("error message = %q; want it to mention --max-tokens-budget", err.Error())
}
}
func TestParseScanFlags_BooleanFlags(t *testing.T) {
opts, err := parseScanFlags([]string{"--no-plan", "--no-dedup", "--no-summary", "--preview"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !opts.noPlan {
t.Error("noPlan should be true")
}
if !opts.noDedup {
t.Error("noDedup should be true")
}
if !opts.noSummary {
t.Error("noSummary should be true")
}
if !opts.preview {
t.Error("preview should be true")
}
}
func TestParseScanFlags_ModelOverride(t *testing.T) {
opts, err := parseScanFlags([]string{"--model", "claude-opus-4-6"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if opts.model != "claude-opus-4-6" {
t.Errorf("model = %q, want claude-opus-4-6", opts.model)
}
}
func TestParseScanFlags_AllStringFlags(t *testing.T) {
opts, err := parseScanFlags([]string{
"--tools", "/tmp/tools.json",
"--rule", "/tmp/rule.json",
"--repo", "/tmp/repo",
"--exclude", "*.md,*.txt",
"--batch", "by-language",
"--background", "test context",
"--audience", "agent",
"-f", "json",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if opts.toolConfigPath != "/tmp/tools.json" {
t.Errorf("toolConfigPath = %q", opts.toolConfigPath)
}
if opts.rulePath != "/tmp/rule.json" {
t.Errorf("rulePath = %q", opts.rulePath)
}
if opts.repoDir != "/tmp/repo" {
t.Errorf("repoDir = %q", opts.repoDir)
}
if opts.excludes != "*.md,*.txt" {
t.Errorf("excludes = %q", opts.excludes)
}
if opts.batch != "by-language" {
t.Errorf("batch = %q", opts.batch)
}
if opts.background != "test context" {
t.Errorf("background = %q", opts.background)
}
if opts.audience != "agent" {
t.Errorf("audience = %q", opts.audience)
}
if opts.outputFormat != "json" {
t.Errorf("outputFormat = %q", opts.outputFormat)
}
}
func TestParseScanFlags_IntFlags(t *testing.T) {
opts, err := parseScanFlags([]string{
"--concurrency", "16",
"--timeout", "20",
"--max-tools", "50",
"--max-git-procs", "32",
"--max-tokens-budget", "100000",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if opts.concurrency != 16 {
t.Errorf("concurrency = %d", opts.concurrency)
}
if opts.perFileTimeout != 20 {
t.Errorf("perFileTimeout = %d", opts.perFileTimeout)
}
if opts.maxTools != 50 {
t.Errorf("maxTools = %d", opts.maxTools)
}
if opts.maxGitProcs != 32 {
t.Errorf("maxGitProcs = %d", opts.maxGitProcs)
}
if opts.maxTokensBudget != 100000 {
t.Errorf("maxTokensBudget = %d", opts.maxTokensBudget)
}
}