package main import ( "bufio" "encoding/json" "errors" "os" "strings" "testing" "github.com/go-git/go-git/v6" "github.com/go-git/go-git/v6/plumbing/object" "github.com/shirou/gopsutil/v4/process" ) func TestFormatContextInfo_NilUsage(t *testing.T) { result := formatContextInfo(200000, nil) expected := "0/200k" if result != expected { t.Errorf("formatContextInfo(200000, nil) = %q, want %q", result, expected) } } func TestFormatContextInfo_WithUsage(t *testing.T) { usage := &TokenUsage{ InputTokens: 8500, CacheCreationTokens: 5000, CacheReadInputTokens: 2000, } result := formatContextInfo(200000, usage) expected := "15k/200k" if result != expected { t.Errorf("formatContextInfo(200000, usage) = %q, want %q", result, expected) } } func TestFormatContextInfo_SmallValues(t *testing.T) { usage := &TokenUsage{ InputTokens: 500, CacheCreationTokens: 0, CacheReadInputTokens: 0, } result := formatContextInfo(100000, usage) expected := "0k/100k" if result != expected { t.Errorf("formatContextInfo(100000, usage) = %q, want %q", result, expected) } } func TestFormatContextInfo_ZeroContextSize(t *testing.T) { result := formatContextInfo(0, nil) expected := "0/0k" if result != expected { t.Errorf("formatContextInfo(0, nil) = %q, want %q", result, expected) } } func TestFormatContextInfo_LargeValues(t *testing.T) { usage := &TokenUsage{ InputTokens: 150000, CacheCreationTokens: 25000, CacheReadInputTokens: 10000, } result := formatContextInfo(200000, usage) expected := "185k/200k" if result != expected { t.Errorf("formatContextInfo(200000, large usage) = %q, want %q", result, expected) } } func TestFormatContextInfo_ExactThousand(t *testing.T) { usage := &TokenUsage{ InputTokens: 1000, CacheCreationTokens: 0, CacheReadInputTokens: 0, } result := formatContextInfo(100000, usage) expected := "1k/100k" if result != expected { t.Errorf("formatContextInfo(100000, 1000 tokens) = %q, want %q", result, expected) } } func TestFormatContextInfo_MaxTokens(t *testing.T) { usage := &TokenUsage{ InputTokens: 200000, CacheCreationTokens: 0, CacheReadInputTokens: 0, } result := formatContextInfo(200000, usage) expected := "200k/200k" if result != expected { t.Errorf("formatContextInfo(max) = %q, want %q", result, expected) } } func TestFormatContextInfo_OverflowTokens(t *testing.T) { usage := &TokenUsage{ InputTokens: 250000, CacheCreationTokens: 0, CacheReadInputTokens: 0, } result := formatContextInfo(200000, usage) expected := "250k/200k" if result != expected { t.Errorf("formatContextInfo(overflow) = %q, want %q", result, expected) } } func TestFormatContextInfo_AllCacheTypes(t *testing.T) { usage := &TokenUsage{ InputTokens: 10000, CacheCreationTokens: 20000, CacheReadInputTokens: 30000, } result := formatContextInfo(100000, usage) expected := "60k/100k" if result != expected { t.Errorf("formatContextInfo(all cache) = %q, want %q", result, expected) } } func TestFormatContextInfo_OnlyCacheCreation(t *testing.T) { usage := &TokenUsage{ InputTokens: 0, CacheCreationTokens: 5000, CacheReadInputTokens: 0, } result := formatContextInfo(100000, usage) expected := "5k/100k" if result != expected { t.Errorf("formatContextInfo(cache creation) = %q, want %q", result, expected) } } func TestFormatContextInfo_OnlyCacheRead(t *testing.T) { usage := &TokenUsage{ InputTokens: 0, CacheCreationTokens: 0, CacheReadInputTokens: 8000, } result := formatContextInfo(100000, usage) expected := "8k/100k" if result != expected { t.Errorf("formatContextInfo(cache read) = %q, want %q", result, expected) } } func TestTokenUsage_ZeroValues(t *testing.T) { usage := &TokenUsage{ InputTokens: 0, CacheCreationTokens: 0, CacheReadInputTokens: 0, } result := formatContextInfo(100000, usage) expected := "0k/100k" if result != expected { t.Errorf("formatContextInfo(zero usage) = %q, want %q", result, expected) } } func TestStripANSI(t *testing.T) { tests := []struct { name string input string expected string }{ {"no ansi", "hello world", "hello world"}, {"single color", "\033[32mgreen\033[0m", "green"}, {"multiple colors", "\033[31mred\033[0m \033[32mgreen\033[0m", "red green"}, {"bold", "\033[1mbold\033[0m", "bold"}, {"complex", "\033[32m●\033[0m \033[35mOpus\033[0m \033[1;32m➜\033[0m", "● Opus ➜"}, {"empty", "", ""}, {"only ansi", "\033[31m\033[0m", ""}, {"nested", "\033[1m\033[31mbold red\033[0m\033[0m", "bold red"}, {"256 color", "\033[38;5;196mred\033[0m", "red"}, {"true color", "\033[38;2;255;0;0mred\033[0m", "red"}, {"multiline", "\033[31mline1\033[0m\n\033[32mline2\033[0m", "line1\nline2"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := stripANSI(tt.input) if result != tt.expected { t.Errorf("stripANSI(%q) = %q, want %q", tt.input, result, tt.expected) } }) } } func TestStatusInputParsing(t *testing.T) { jsonData := `{ "model": {"display_name": "Opus 4.5"}, "workspace": {"current_dir": "/root/projects/statusline"}, "context_window": { "context_window_size": 200000, "current_usage": { "input_tokens": 5000, "cache_creation_input_tokens": 1000, "cache_read_input_tokens": 500 } } }` var data StatusInput err := json.Unmarshal([]byte(jsonData), &data) if err != nil { t.Fatalf("Failed to parse JSON: %v", err) } if data.Model.DisplayName != "Opus 4.5" { t.Errorf("Model.DisplayName = %q, want %q", data.Model.DisplayName, "Opus 4.5") } if data.Workspace.CurrentDir != "/root/projects/statusline" { t.Errorf("Workspace.CurrentDir = %q, want %q", data.Workspace.CurrentDir, "/root/projects/statusline") } if data.ContextWindow.ContextWindowSize != 200000 { t.Errorf("ContextWindow.ContextWindowSize = %d, want %d", data.ContextWindow.ContextWindowSize, 200000) } if data.ContextWindow.CurrentUsage == nil { t.Fatal("ContextWindow.CurrentUsage is nil") } if data.ContextWindow.CurrentUsage.InputTokens != 5000 { t.Errorf("CurrentUsage.InputTokens = %d, want %d", data.ContextWindow.CurrentUsage.InputTokens, 5000) } } func TestStatusInputParsing_NilUsage(t *testing.T) { jsonData := `{ "model": {"display_name": "Sonnet"}, "workspace": {"current_dir": "/tmp"}, "context_window": {"context_window_size": 100000} }` var data StatusInput err := json.Unmarshal([]byte(jsonData), &data) if err != nil { t.Fatalf("Failed to parse JSON: %v", err) } if data.ContextWindow.CurrentUsage != nil { t.Errorf("ContextWindow.CurrentUsage should be nil, got %+v", data.ContextWindow.CurrentUsage) } } func TestStatusInputParsing_EmptyJSON(t *testing.T) { var data StatusInput err := json.Unmarshal([]byte(`{}`), &data) if err != nil { t.Fatalf("Failed to parse empty JSON: %v", err) } if data.Model.DisplayName != "" { t.Errorf("Expected empty DisplayName, got %q", data.Model.DisplayName) } } func TestStatusInputParsing_PartialJSON(t *testing.T) { var data StatusInput err := json.Unmarshal([]byte(`{"model": {"display_name": "Test"}}`), &data) if err != nil { t.Fatalf("Failed to parse partial JSON: %v", err) } if data.Model.DisplayName != "Test" { t.Errorf("DisplayName = %q, want %q", data.Model.DisplayName, "Test") } if data.Workspace.CurrentDir != "" { t.Errorf("Expected empty CurrentDir, got %q", data.Workspace.CurrentDir) } } func TestStatusInputParsing_InvalidJSON(t *testing.T) { var data StatusInput if err := json.Unmarshal([]byte(`{invalid json}`), &data); err == nil { t.Error("Expected error for invalid JSON, got nil") } } func TestANSIConstants(t *testing.T) { tests := []struct { name string constant string }{ {"reset", reset}, {"red", red}, {"green", green}, {"yellow", yellow}, {"magenta", magenta}, {"cyan", cyan}, {"boldGreen", boldGreen}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if !strings.HasPrefix(tt.constant, "\033[") { t.Errorf("%s constant doesn't start with ANSI escape", tt.name) } }) } } func TestReadInputFromStdin(t *testing.T) { tests := []struct { name string input string expected string }{ {"multiline", "line1\nline2\nline3", "line1\nline2\nline3"}, {"empty", "", ""}, {"single line", "single line", "single line"}, {"json", `{"key": "value"}`, `{"key": "value"}`}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { reader := bufio.NewReader(strings.NewReader(tt.input)) result := readInputFromStdin(reader) if result != tt.expected { t.Errorf("readInputFromStdin = %q, want %q", result, tt.expected) } }) } } func TestParseStatusInput_Valid(t *testing.T) { data, err := parseStatusInput(`{"model": {"display_name": "Test"}, "workspace": {"current_dir": "/test"}}`) if err != nil { t.Fatalf("parseStatusInput failed: %v", err) } if data.Model.DisplayName != "Test" { t.Errorf("DisplayName = %q, want Test", data.Model.DisplayName) } if data.Workspace.CurrentDir != "/test" { t.Errorf("CurrentDir = %q, want /test", data.Workspace.CurrentDir) } } func TestParseStatusInput_Invalid(t *testing.T) { if _, err := parseStatusInput("invalid json"); err == nil { t.Error("parseStatusInput should fail on invalid JSON") } } func TestParseStatusInput_AllFields(t *testing.T) { jsonStr := `{ "model": {"display_name": "FullTest"}, "workspace": {"current_dir": "/full/path"}, "context_window": { "context_window_size": 150000, "current_usage": { "input_tokens": 1000, "cache_creation_input_tokens": 2000, "cache_read_input_tokens": 3000 } } }` data, err := parseStatusInput(jsonStr) if err != nil { t.Fatalf("parseStatusInput failed: %v", err) } if data.Model.DisplayName != "FullTest" { t.Errorf("DisplayName = %q, want FullTest", data.Model.DisplayName) } if data.ContextWindow.CurrentUsage.CacheCreationTokens != 2000 { t.Errorf("CacheCreationTokens = %d, want 2000", data.ContextWindow.CurrentUsage.CacheCreationTokens) } } func TestParseStatusInput_ExtraFields(t *testing.T) { data, err := parseStatusInput(`{"model": {"display_name": "Test", "unknown": "field"}, "extra": "data"}`) if err != nil { t.Fatalf("parseStatusInput with extra fields failed: %v", err) } if data.Model.DisplayName != "Test" { t.Errorf("DisplayName = %q, want Test", data.Model.DisplayName) } } func TestBuildStatusLine_ContainsComponents(t *testing.T) { data := &StatusInput{} data.Model.DisplayName = "TestModel" data.Workspace.CurrentDir = "/home/user/project" data.ContextWindow.ContextWindowSize = 100000 data.ContextWindow.CurrentUsage = &TokenUsage{ InputTokens: 5000, CacheCreationTokens: 1000, CacheReadInputTokens: 500, } result := buildStatusLine(data) if !strings.Contains(result, "TestModel") { t.Errorf("statusline missing model: %q", result) } if !strings.Contains(result, "project") { t.Errorf("statusline missing directory: %q", result) } if !strings.Contains(result, "6k/100k") { t.Errorf("statusline missing context info: %q", result) } if !strings.Contains(result, "●") { t.Errorf("statusline missing gitea status: %q", result) } } func TestBuildStatusLine_NilUsage(t *testing.T) { data := &StatusInput{} data.Model.DisplayName = "Model" data.Workspace.CurrentDir = "/test" data.ContextWindow.ContextWindowSize = 100000 result := buildStatusLine(data) if !strings.Contains(result, "0/100k") { t.Errorf("nil usage should show 0: %q", result) } } func TestBuildStatusLine_EmptyDir(t *testing.T) { data := &StatusInput{} data.Model.DisplayName = "Model" data.Workspace.CurrentDir = "" data.ContextWindow.ContextWindowSize = 100000 result := buildStatusLine(data) if result == "" { t.Error("buildStatusLine with empty dir should produce output") } } func TestBuildStatusLine_LongModelName(t *testing.T) { data := &StatusInput{} data.Model.DisplayName = "Claude 3.5 Sonnet with Extended Context" data.Workspace.CurrentDir = "/home/user/my-very-long-project-name-here" data.ContextWindow.ContextWindowSize = 200000 data.ContextWindow.CurrentUsage = &TokenUsage{InputTokens: 50000} result := buildStatusLine(data) if !strings.Contains(result, "Claude 3.5 Sonnet with Extended Context") { t.Errorf("long model name not in output: %q", result) } if !strings.Contains(result, "50k/200k") { t.Errorf("context info not in output: %q", result) } } func TestBuildStatusLine_RootDir(t *testing.T) { data := &StatusInput{} data.Model.DisplayName = "Model" data.Workspace.CurrentDir = "/" data.ContextWindow.ContextWindowSize = 100000 result := buildStatusLine(data) if !strings.Contains(result, "Model") { t.Errorf("root dir handling failed: %q", result) } } func TestGetGitInfo_CurrentRepo(t *testing.T) { cwd, err := os.Getwd() if err != nil { t.Skipf("Could not get working directory: %v", err) } result := getGitInfo(cwd) if result == "" { t.Skip("Not in a git repository") } if !strings.Contains(result, "git:(") { t.Errorf("getGitInfo(%q) = %q, expected to contain 'git:('", cwd, result) } } func TestGetGitInfo_NonRepo(t *testing.T) { result := getGitInfo("/tmp") if result != "" && !strings.Contains(result, "git:(") { t.Errorf("getGitInfo(/tmp) = %q, expected empty or valid git info", result) } } func TestGetGitInfo_InvalidPath(t *testing.T) { if result := getGitInfo("/nonexistent/path"); result != "" { t.Errorf("getGitInfo(invalid) = %q, expected empty string", result) } } func TestGetGitInfo_RootDir(t *testing.T) { result := getGitInfo("/") if result != "" && !strings.Contains(result, "git:(") { t.Errorf("getGitInfo(/) = %q, expected empty or valid git info", result) } } func TestGetGitInfo_DetachedHEAD(t *testing.T) { dir := t.TempDir() repo, err := git.PlainInit(dir, false) if err != nil { t.Fatalf("git init: %v", err) } wt, err := repo.Worktree() if err != nil { t.Fatalf("worktree: %v", err) } if err := os.WriteFile(dir+"/file.txt", []byte("hello"), 0o644); err != nil { t.Fatalf("write: %v", err) } wt.Add("file.txt") hash, err := wt.Commit("initial", &git.CommitOptions{ Author: &object.Signature{Name: "test", Email: "test@test.com"}, }) if err != nil { t.Fatalf("commit: %v", err) } if err := wt.Checkout(&git.CheckoutOptions{Hash: hash}); err != nil { t.Fatalf("checkout: %v", err) } result := getGitInfo(dir) expected := " git:(" + hash.String()[:7] + ")" if result != expected { t.Errorf("getGitInfo(detached) = %q, want %q", result, expected) } } func TestGetGitInfo_DirtyWorktree(t *testing.T) { dir := t.TempDir() repo, err := git.PlainInit(dir, false) if err != nil { t.Fatalf("git init: %v", err) } wt, err := repo.Worktree() if err != nil { t.Fatalf("worktree: %v", err) } if err := os.WriteFile(dir+"/file.txt", []byte("hello"), 0o644); err != nil { t.Fatalf("write: %v", err) } wt.Add("file.txt") if _, err = wt.Commit("initial", &git.CommitOptions{ Author: &object.Signature{Name: "test", Email: "test@test.com"}, }); err != nil { t.Fatalf("commit: %v", err) } if err := os.WriteFile(dir+"/file.txt", []byte("modified"), 0o644); err != nil { t.Fatalf("write: %v", err) } result := getGitInfo(dir) if !strings.Contains(result, "✗") { t.Errorf("getGitInfo(dirty) = %q, expected dirty marker ✗", result) } if !strings.Contains(result, "git:(master)") { t.Errorf("getGitInfo(dirty) = %q, expected branch name", result) } } func TestGetGitInfo_CleanWorktree(t *testing.T) { dir := t.TempDir() repo, err := git.PlainInit(dir, false) if err != nil { t.Fatalf("git init: %v", err) } wt, err := repo.Worktree() if err != nil { t.Fatalf("worktree: %v", err) } if err := os.WriteFile(dir+"/file.txt", []byte("hello"), 0o644); err != nil { t.Fatalf("write: %v", err) } wt.Add("file.txt") if _, err = wt.Commit("initial", &git.CommitOptions{ Author: &object.Signature{Name: "test", Email: "test@test.com"}, }); err != nil { t.Fatalf("commit: %v", err) } result := getGitInfo(dir) if result != " git:(master)" { t.Errorf("getGitInfo(clean) = %q, want %q", result, " git:(master)") } } func TestGetGitInfo_EmptyRepo(t *testing.T) { dir := t.TempDir() if _, err := git.PlainInit(dir, false); err != nil { t.Fatalf("git init: %v", err) } if result := getGitInfo(dir); result != "" { t.Errorf("getGitInfo(empty repo) = %q, want empty", result) } } func TestGetGitInfo_UntrackedFile(t *testing.T) { dir := t.TempDir() repo, err := git.PlainInit(dir, false) if err != nil { t.Fatalf("git init: %v", err) } wt, err := repo.Worktree() if err != nil { t.Fatalf("worktree: %v", err) } if err := os.WriteFile(dir+"/tracked.txt", []byte("tracked"), 0o644); err != nil { t.Fatalf("write: %v", err) } wt.Add("tracked.txt") if _, err = wt.Commit("initial", &git.CommitOptions{ Author: &object.Signature{Name: "test", Email: "test@test.com"}, }); err != nil { t.Fatalf("commit: %v", err) } if err := os.WriteFile(dir+"/untracked.txt", []byte("new"), 0o644); err != nil { t.Fatalf("write: %v", err) } result := getGitInfo(dir) if !strings.Contains(result, "✗") { t.Errorf("getGitInfo(untracked) = %q, expected dirty marker", result) } } func TestGetGiteaStatus(t *testing.T) { result := getGiteaStatus() greenDot := green + "●" + reset redDot := red + "●" + reset if result != greenDot && result != redDot { t.Errorf("getGiteaStatus() = %q, expected green or red dot", result) } } func TestGetGiteaStatus_ReturnsValidColor(t *testing.T) { result := getGiteaStatus() if !strings.Contains(result, "●") { t.Errorf("getGiteaStatus() = %q, expected to contain dot", result) } if stripped := stripANSI(result); stripped != "●" { t.Errorf("stripped getGiteaStatus() = %q, expected just dot", stripped) } } func TestGetGiteaStatus_ProcessListError(t *testing.T) { orig := processLister t.Cleanup(func() { processLister = orig }) processLister = func() ([]*process.Process, error) { return nil, errors.New("mock error") } result := getGiteaStatus() if expected := red + "●" + reset; result != expected { t.Errorf("getGiteaStatus(error) = %q, want red dot", result) } } func TestGetGiteaStatus_NoGiteaProcess(t *testing.T) { orig := processLister t.Cleanup(func() { processLister = orig }) processLister = func() ([]*process.Process, error) { return []*process.Process{}, nil } result := getGiteaStatus() if expected := red + "●" + reset; result != expected { t.Errorf("getGiteaStatus(no gitea) = %q, want red dot", result) } } func TestRun_ValidInput(t *testing.T) { jsonStr := `{"model":{"display_name":"Test"},"workspace":{"current_dir":"/tmp"},"context_window":{"context_window_size":100000}}` r := bufio.NewReader(strings.NewReader(jsonStr)) var out strings.Builder if err := run(r, &out); err != nil { t.Fatalf("run() returned error: %v", err) } result := out.String() if result == "" { t.Error("run() produced empty output") } if !strings.Contains(result, "Test") { t.Errorf("run() output missing model name: %q", result) } } func TestRun_InvalidJSON(t *testing.T) { r := bufio.NewReader(strings.NewReader("not json")) var out strings.Builder if err := run(r, &out); err == nil { t.Error("run() should return error on invalid JSON") } } func TestRun_EmptyInput(t *testing.T) { r := bufio.NewReader(strings.NewReader("")) var out strings.Builder if err := run(r, &out); err == nil { t.Error("run() should return error on empty input") } } // Benchmark tests func BenchmarkFormatContextInfo(b *testing.B) { usage := &TokenUsage{ InputTokens: 50000, CacheCreationTokens: 10000, CacheReadInputTokens: 5000, } for b.Loop() { formatContextInfo(200000, usage) } } func BenchmarkStripANSI(b *testing.B) { input := "\033[32m●\033[0m \033[35mOpus\033[0m \033[1;32m➜\033[0m \033[36mproject\033[0m" for b.Loop() { stripANSI(input) } } func BenchmarkParseStatusInput(b *testing.B) { jsonStr := `{"model": {"display_name": "Test"}, "workspace": {"current_dir": "/test"}, "context_window": {"context_window_size": 200000, "current_usage": {"input_tokens": 50000}}}` for b.Loop() { parseStatusInput(jsonStr) } } func BenchmarkBuildStatusLine(b *testing.B) { data := &StatusInput{} data.Model.DisplayName = "Claude Opus" data.Workspace.CurrentDir = "/home/user/project" data.ContextWindow.ContextWindowSize = 200000 data.ContextWindow.CurrentUsage = &TokenUsage{InputTokens: 50000} for b.Loop() { buildStatusLine(data) } } func BenchmarkReadInputFromStdin(b *testing.B) { jsonStr := `{"model": {"display_name": "Test"}, "workspace": {"current_dir": "/test"}}` for b.Loop() { reader := bufio.NewReader(strings.NewReader(jsonStr)) readInputFromStdin(reader) } }