package cmd import ( "os" "path/filepath" "strings" "testing" ) // TestAnalyzeCmd — FAST DEFAULT TEST (runs on make test / go test ./cmd) func TestAnalyzeCmd(t *testing.T) { t.Log("✓ Fast analyze unit test (no Grok API call)") found := false for _, c := range rootCmd.Commands() { if c.Use == "analyze [flags]" { found = true break } } if !found { t.Fatal("analyze command not registered on rootCmd") } } func TestAnalyzeFlagRegistration(t *testing.T) { tests := []struct { name string flagName string shorthand string }{ {"output flag", "output", "o"}, {"yes flag", "yes", "y"}, {"dir flag", "dir", ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { flag := analyzeCmd.Flags().Lookup(tt.flagName) if flag == nil { t.Fatalf("flag %q not found on analyze command", tt.flagName) } if tt.shorthand != "" && flag.Shorthand != tt.shorthand { t.Errorf("flag %q shorthand = %q, want %q", tt.flagName, flag.Shorthand, tt.shorthand) } }) } } func TestDiscoverSourceFiles(t *testing.T) { tmpDir := t.TempDir() // Create some source files if err := os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte("package main"), 0644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(tmpDir, "helper.go"), []byte("package main"), 0644); err != nil { t.Fatal(err) } // Create a non-source file (should be ignored) if err := os.WriteFile(filepath.Join(tmpDir, "notes.txt"), []byte("notes"), 0644); err != nil { t.Fatal(err) } files, err := discoverSourceFiles(tmpDir) if err != nil { t.Fatalf("discoverSourceFiles failed: %v", err) } if len(files) < 2 { t.Errorf("expected at least 2 Go files, got %d: %v", len(files), files) } // Verify .txt was not included for _, f := range files { if strings.HasSuffix(f, ".txt") { t.Errorf("unexpected .txt file in results: %s", f) } } } func TestDiscoverSourceFilesSkipsDotDirs(t *testing.T) { tmpDir := t.TempDir() // Create a hidden directory with a Go file hiddenDir := filepath.Join(tmpDir, ".hidden") if err := os.MkdirAll(hiddenDir, 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(hiddenDir, "secret.go"), []byte("package hidden"), 0644); err != nil { t.Fatal(err) } // Create vendor directory with a Go file vendorDir := filepath.Join(tmpDir, "vendor") if err := os.MkdirAll(vendorDir, 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(vendorDir, "dep.go"), []byte("package dep"), 0644); err != nil { t.Fatal(err) } // Create a normal Go file if err := os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte("package main"), 0644); err != nil { t.Fatal(err) } files, err := discoverSourceFiles(tmpDir) if err != nil { t.Fatalf("discoverSourceFiles failed: %v", err) } for _, f := range files { if strings.Contains(f, ".hidden") { t.Errorf("should not include files from hidden dirs: %s", f) } if strings.Contains(f, "vendor") { t.Errorf("should not include files from vendor dir: %s", f) } } if len(files) != 1 { t.Errorf("expected 1 file (main.go only), got %d: %v", len(files), files) } } func TestDiscoverSourceFilesSkipsBuildAndDist(t *testing.T) { tmpDir := t.TempDir() for _, dir := range []string{"build", "dist", "node_modules"} { d := filepath.Join(tmpDir, dir) if err := os.MkdirAll(d, 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(d, "file.go"), []byte("package x"), 0644); err != nil { t.Fatal(err) } } if err := os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte("package main"), 0644); err != nil { t.Fatal(err) } files, err := discoverSourceFiles(tmpDir) if err != nil { t.Fatalf("discoverSourceFiles failed: %v", err) } if len(files) != 1 { t.Errorf("expected 1 file, got %d: %v", len(files), files) } } func TestDiscoverSourceFilesEmptyDir(t *testing.T) { tmpDir := t.TempDir() files, err := discoverSourceFiles(tmpDir) if err != nil { t.Fatalf("discoverSourceFiles failed: %v", err) } if len(files) != 0 { t.Errorf("expected 0 files for empty dir, got %d", len(files)) } } func TestBuildProjectContext(t *testing.T) { tmpDir := t.TempDir() files := []string{ filepath.Join(tmpDir, "main.go"), filepath.Join(tmpDir, "cmd", "root.go"), } ctx := buildProjectContext(tmpDir, files) if !strings.Contains(ctx, "Project Root:") { t.Error("expected context to contain 'Project Root:'") } if !strings.Contains(ctx, "Total source files discovered: 2") { t.Error("expected context to contain file count") } if !strings.Contains(ctx, "Key files") { t.Error("expected context to contain 'Key files'") } } func TestPreviewLines(t *testing.T) { tests := []struct { name string content string n int }{ { name: "fewer lines than limit", content: "line1\nline2\nline3\n", n: 10, }, { name: "more lines than limit", content: "a\nb\nc\nd\ne\nf\ng\nh\n", n: 3, }, { name: "empty content", content: "", n: 5, }, { name: "zero lines requested", content: "line1\nline2\n", n: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // previewLines prints to stdout; just verify it doesn't panic previewLines(tt.content, tt.n) }) } } func TestBuildProjectContextLimitsSize(t *testing.T) { tmpDir := t.TempDir() // Generate many files to test the 2500 byte limit var files []string for i := 0; i < 200; i++ { files = append(files, filepath.Join(tmpDir, "file_with_a_long_name_for_testing_"+strings.Repeat("x", 20)+".go")) } ctx := buildProjectContext(tmpDir, files) // Should be reasonably bounded (the 2500 byte check truncates the file list) if len(ctx) > 4000 { t.Errorf("context unexpectedly large: %d bytes", len(ctx)) } }