Output left + right with a single space separator instead of trying to right-align to a detected terminal width. Terminal detection is unreliable when all fds are piped by Claude Code, and hardcoding a width defeats the purpose. Also removes the x/term dependency.
740 lines
20 KiB
Go
740 lines
20 KiB
Go
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)
|
|
}
|
|
}
|