822 lines
22 KiB
Go
822 lines
22 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
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 TestStripANSI(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "no ansi",
|
|
input: "hello world",
|
|
expected: "hello world",
|
|
},
|
|
{
|
|
name: "single color",
|
|
input: "\033[32mgreen\033[0m",
|
|
expected: "green",
|
|
},
|
|
{
|
|
name: "multiple colors",
|
|
input: "\033[31mred\033[0m \033[32mgreen\033[0m",
|
|
expected: "red green",
|
|
},
|
|
{
|
|
name: "bold",
|
|
input: "\033[1mbold\033[0m",
|
|
expected: "bold",
|
|
},
|
|
{
|
|
name: "complex",
|
|
input: "\033[32m●\033[0m \033[35mOpus\033[0m \033[1;32m➜\033[0m",
|
|
expected: "● Opus ➜",
|
|
},
|
|
}
|
|
|
|
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 TestGetGitInfo_CurrentRepo(t *testing.T) {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Skipf("Could not get working directory: %v", err)
|
|
}
|
|
|
|
result := getGitInfo(cwd)
|
|
|
|
// Should return something like " git:(master)" or " git:(master) ✗"
|
|
if result == "" {
|
|
t.Skip("Not in a git repository")
|
|
}
|
|
|
|
if !contains(result, "git:(") {
|
|
t.Errorf("getGitInfo(%q) = %q, expected to contain 'git:('", cwd, result)
|
|
}
|
|
}
|
|
|
|
func TestGetGitInfo_NonRepo(t *testing.T) {
|
|
result := getGitInfo("/tmp")
|
|
|
|
// /tmp is unlikely to be a git repo
|
|
if result != "" && !contains(result, "git:(") {
|
|
t.Errorf("getGitInfo(/tmp) = %q, expected empty or valid git info", result)
|
|
}
|
|
}
|
|
|
|
func TestGetGiteaStatus(t *testing.T) {
|
|
result := getGiteaStatus()
|
|
|
|
// Should return either green or red dot
|
|
greenDot := green + "●" + reset
|
|
redDot := red + "●" + reset
|
|
|
|
if result != greenDot && result != redDot {
|
|
t.Errorf("getGiteaStatus() = %q, expected green or red dot", result)
|
|
}
|
|
}
|
|
|
|
func contains(s, substr string) bool {
|
|
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr))
|
|
}
|
|
|
|
func containsHelper(s, substr string) bool {
|
|
for i := 0; i <= len(s)-len(substr); i++ {
|
|
if s[i:i+len(substr)] == substr {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func TestGetTerminalWidth(t *testing.T) {
|
|
// getTerminalWidth should return a positive integer
|
|
// In test environment (no TTY), it should fall back to 80
|
|
width := getTerminalWidth()
|
|
if width <= 0 {
|
|
t.Errorf("getTerminalWidth() = %d, expected positive value", width)
|
|
}
|
|
}
|
|
|
|
func TestGetTerminalWidth_DefaultFallback(t *testing.T) {
|
|
// When not connected to a terminal, should return 80
|
|
width := getTerminalWidth()
|
|
// In CI/test environments, this typically returns 80
|
|
if width != 80 && width < 40 {
|
|
t.Errorf("getTerminalWidth() = %d, expected 80 or reasonable terminal width", width)
|
|
}
|
|
}
|
|
|
|
func TestGetGitInfo_InvalidPath(t *testing.T) {
|
|
result := getGitInfo("/nonexistent/path/that/does/not/exist")
|
|
if result != "" {
|
|
t.Errorf("getGitInfo(invalid) = %q, expected empty string", result)
|
|
}
|
|
}
|
|
|
|
func TestGetGitInfo_RootDir(t *testing.T) {
|
|
// Root directory is unlikely to be a git repo
|
|
result := getGitInfo("/")
|
|
if result != "" && !contains(result, "git:(") {
|
|
t.Errorf("getGitInfo(/) = %q, expected empty or valid git info", result)
|
|
}
|
|
}
|
|
|
|
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 TestStripANSI_Empty(t *testing.T) {
|
|
result := stripANSI("")
|
|
if result != "" {
|
|
t.Errorf("stripANSI(\"\") = %q, want empty", result)
|
|
}
|
|
}
|
|
|
|
func TestStripANSI_OnlyANSI(t *testing.T) {
|
|
result := stripANSI("\033[31m\033[0m")
|
|
if result != "" {
|
|
t.Errorf("stripANSI(only codes) = %q, want empty", result)
|
|
}
|
|
}
|
|
|
|
func TestStripANSI_NestedCodes(t *testing.T) {
|
|
input := "\033[1m\033[31mbold red\033[0m\033[0m"
|
|
result := stripANSI(input)
|
|
expected := "bold red"
|
|
if result != expected {
|
|
t.Errorf("stripANSI(%q) = %q, want %q", input, result, expected)
|
|
}
|
|
}
|
|
|
|
func TestStatusInputParsing_EmptyJSON(t *testing.T) {
|
|
jsonData := `{}`
|
|
var data StatusInput
|
|
err := json.Unmarshal([]byte(jsonData), &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) {
|
|
jsonData := `{"model": {"display_name": "Test"}}`
|
|
var data StatusInput
|
|
err := json.Unmarshal([]byte(jsonData), &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) {
|
|
jsonData := `{invalid json}`
|
|
var data StatusInput
|
|
err := json.Unmarshal([]byte(jsonData), &data)
|
|
if err == nil {
|
|
t.Error("Expected error for invalid JSON, got nil")
|
|
}
|
|
}
|
|
|
|
func TestANSIConstants(t *testing.T) {
|
|
// Verify ANSI constants are properly defined
|
|
tests := []struct {
|
|
name string
|
|
constant string
|
|
prefix string
|
|
}{
|
|
{"reset", reset, "\033["},
|
|
{"red", red, "\033["},
|
|
{"green", green, "\033["},
|
|
{"yellow", yellow, "\033["},
|
|
{"magenta", magenta, "\033["},
|
|
{"cyan", cyan, "\033["},
|
|
{"boldGreen", boldGreen, "\033["},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if !contains(tt.constant, tt.prefix) {
|
|
t.Errorf("%s constant doesn't start with ANSI escape", tt.name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetGiteaStatus_ReturnsValidColor(t *testing.T) {
|
|
result := getGiteaStatus()
|
|
|
|
// Must contain the dot character
|
|
if !contains(result, "●") {
|
|
t.Errorf("getGiteaStatus() = %q, expected to contain dot", result)
|
|
}
|
|
|
|
// Must contain ANSI codes
|
|
stripped := stripANSI(result)
|
|
if stripped != "●" {
|
|
t.Errorf("stripped getGiteaStatus() = %q, expected just dot", stripped)
|
|
}
|
|
}
|
|
|
|
func TestReadInputFromStdin(t *testing.T) {
|
|
input := "line1\nline2\nline3"
|
|
reader := bufio.NewReader(strings.NewReader(input))
|
|
result := readInputFromStdin(reader)
|
|
expected := "line1\nline2\nline3"
|
|
if result != expected {
|
|
t.Errorf("readInputFromStdin = %q, want %q", result, expected)
|
|
}
|
|
}
|
|
|
|
func TestReadInputFromStdin_Empty(t *testing.T) {
|
|
reader := bufio.NewReader(strings.NewReader(""))
|
|
result := readInputFromStdin(reader)
|
|
if result != "" {
|
|
t.Errorf("readInputFromStdin (empty) = %q, want empty", result)
|
|
}
|
|
}
|
|
|
|
func TestParseStatusInput_Valid(t *testing.T) {
|
|
jsonStr := `{"model": {"display_name": "Test"}, "workspace": {"current_dir": "/test"}}`
|
|
data, err := parseStatusInput(jsonStr)
|
|
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) {
|
|
_, err := parseStatusInput("invalid json")
|
|
if err == nil {
|
|
t.Error("parseStatusInput should fail on invalid JSON")
|
|
}
|
|
}
|
|
|
|
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,
|
|
}
|
|
|
|
left, right := buildStatusLine(data)
|
|
|
|
// Check left contains model name and directory
|
|
if !contains(left, "TestModel") {
|
|
t.Errorf("left statusline missing model: %q", left)
|
|
}
|
|
if !contains(left, "project") {
|
|
t.Errorf("left statusline missing directory: %q", left)
|
|
}
|
|
|
|
// Check right contains context info
|
|
if !contains(right, "6k/100k") {
|
|
t.Errorf("right statusline missing context info: %q", right)
|
|
}
|
|
}
|
|
|
|
func TestBuildStatusLine_HasGiteaStatus(t *testing.T) {
|
|
data := &StatusInput{}
|
|
data.Model.DisplayName = "Model"
|
|
data.Workspace.CurrentDir = "/tmp"
|
|
data.ContextWindow.ContextWindowSize = 100000
|
|
|
|
left, _ := buildStatusLine(data)
|
|
|
|
// Check for gitea status (dot)
|
|
if !contains(left, "●") {
|
|
t.Errorf("left statusline missing gitea status: %q", left)
|
|
}
|
|
}
|
|
|
|
func TestCalculatePadding_ZeroWidth(t *testing.T) {
|
|
result := calculatePadding("left", "right", 0)
|
|
expected := 1
|
|
if result != expected {
|
|
t.Errorf("calculatePadding(\"left\", \"right\", 0) = %d, want %d", result, expected)
|
|
}
|
|
}
|
|
|
|
func TestCalculatePadding_NegativeResult(t *testing.T) {
|
|
result := calculatePadding("left", "right", 5)
|
|
expected := 1
|
|
if result != expected {
|
|
t.Errorf("calculatePadding with overflow = %d, want minimum of %d", result, expected)
|
|
}
|
|
}
|
|
|
|
func TestCalculatePadding_Normal(t *testing.T) {
|
|
result := calculatePadding("left", "right", 50)
|
|
expected := 50 - len("left") - len("right")
|
|
if result != expected {
|
|
t.Errorf("calculatePadding(\"left\", \"right\", 50) = %d, want %d", result, expected)
|
|
}
|
|
}
|
|
|
|
func TestFormatOutput_Composition(t *testing.T) {
|
|
result := formatOutput("LEFT", "RIGHT", 5)
|
|
expected := "LEFT RIGHT"
|
|
if result != expected {
|
|
t.Errorf("formatOutput = %q, want %q", result, expected)
|
|
}
|
|
}
|
|
|
|
func TestFormatOutput_Empty(t *testing.T) {
|
|
result := formatOutput("", "", 0)
|
|
expected := ""
|
|
if result != expected {
|
|
t.Errorf("formatOutput (empty) = %q, want %q", result, expected)
|
|
}
|
|
}
|
|
|
|
func TestFormatOutput_WithANSI(t *testing.T) {
|
|
left := red + "text" + reset
|
|
right := green + "info" + reset
|
|
result := formatOutput(left, right, 3)
|
|
|
|
stripped := stripANSI(result)
|
|
if !contains(stripped, "text") || !contains(stripped, "info") {
|
|
t.Errorf("formatOutput with ANSI = %q, expected both parts visible", stripped)
|
|
}
|
|
}
|
|
|
|
// Additional edge case tests
|
|
|
|
func TestFormatContextInfo_MaxTokens(t *testing.T) {
|
|
// Test when usage equals context size
|
|
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) {
|
|
// Test when usage exceeds context size
|
|
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) {
|
|
// Test all cache token types contributing
|
|
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 TestBuildStatusLine_EmptyDir(t *testing.T) {
|
|
data := &StatusInput{}
|
|
data.Model.DisplayName = "Model"
|
|
data.Workspace.CurrentDir = ""
|
|
data.ContextWindow.ContextWindowSize = 100000
|
|
|
|
left, right := buildStatusLine(data)
|
|
|
|
// Should not panic, should produce valid output
|
|
if left == "" {
|
|
t.Error("buildStatusLine with empty dir should produce left output")
|
|
}
|
|
if right == "" {
|
|
t.Error("buildStatusLine with empty dir should produce right 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,
|
|
}
|
|
|
|
left, right := buildStatusLine(data)
|
|
|
|
if !contains(left, "Claude 3.5 Sonnet with Extended Context") {
|
|
t.Errorf("long model name not in left: %q", left)
|
|
}
|
|
if !contains(left, "my-very-long-project-name-here") {
|
|
t.Errorf("long dir name not in left: %q", left)
|
|
}
|
|
if !contains(right, "50k/200k") {
|
|
t.Errorf("context info not in right: %q", right)
|
|
}
|
|
}
|
|
|
|
func TestBuildStatusLine_NilUsage(t *testing.T) {
|
|
data := &StatusInput{}
|
|
data.Model.DisplayName = "Model"
|
|
data.Workspace.CurrentDir = "/test"
|
|
data.ContextWindow.ContextWindowSize = 100000
|
|
data.ContextWindow.CurrentUsage = nil
|
|
|
|
left, right := buildStatusLine(data)
|
|
|
|
if !contains(right, "0/100k") {
|
|
t.Errorf("nil usage should show 0: %q", right)
|
|
}
|
|
if left == "" {
|
|
t.Error("left should not be empty")
|
|
}
|
|
}
|
|
|
|
func TestBuildStatusLine_RootDir(t *testing.T) {
|
|
data := &StatusInput{}
|
|
data.Model.DisplayName = "Model"
|
|
data.Workspace.CurrentDir = "/"
|
|
data.ContextWindow.ContextWindowSize = 100000
|
|
|
|
left, _ := buildStatusLine(data)
|
|
|
|
// filepath.Base("/") returns "/"
|
|
if !contains(left, "/") || !contains(left, "Model") {
|
|
t.Errorf("root dir handling failed: %q", left)
|
|
}
|
|
}
|
|
|
|
func TestCalculatePadding_ExactFit(t *testing.T) {
|
|
// When content exactly fills the width
|
|
result := calculatePadding("12345", "67890", 10)
|
|
expected := 1 // max(10-5-5, 1) = max(0, 1) = 1
|
|
if result != expected {
|
|
t.Errorf("calculatePadding(exact fit) = %d, want %d", result, expected)
|
|
}
|
|
}
|
|
|
|
func TestCalculatePadding_LargeWidth(t *testing.T) {
|
|
result := calculatePadding("left", "right", 200)
|
|
expected := 200 - 4 - 5 // 191
|
|
if result != expected {
|
|
t.Errorf("calculatePadding(large) = %d, want %d", result, expected)
|
|
}
|
|
}
|
|
|
|
func TestStripANSI_256Color(t *testing.T) {
|
|
// 256-color escape sequences
|
|
input := "\033[38;5;196mred\033[0m"
|
|
result := stripANSI(input)
|
|
expected := "red"
|
|
if result != expected {
|
|
t.Errorf("stripANSI(256color) = %q, want %q", result, expected)
|
|
}
|
|
}
|
|
|
|
func TestStripANSI_TrueColor(t *testing.T) {
|
|
// 24-bit true color escape sequences
|
|
input := "\033[38;2;255;0;0mred\033[0m"
|
|
result := stripANSI(input)
|
|
expected := "red"
|
|
if result != expected {
|
|
t.Errorf("stripANSI(truecolor) = %q, want %q", result, expected)
|
|
}
|
|
}
|
|
|
|
func TestStripANSI_Multiline(t *testing.T) {
|
|
input := "\033[31mline1\033[0m\n\033[32mline2\033[0m"
|
|
result := stripANSI(input)
|
|
expected := "line1\nline2"
|
|
if result != expected {
|
|
t.Errorf("stripANSI(multiline) = %q, want %q", result, expected)
|
|
}
|
|
}
|
|
|
|
func TestReadInputFromStdin_SingleLine(t *testing.T) {
|
|
reader := bufio.NewReader(strings.NewReader("single line"))
|
|
result := readInputFromStdin(reader)
|
|
if result != "single line" {
|
|
t.Errorf("readInputFromStdin(single) = %q, want 'single line'", result)
|
|
}
|
|
}
|
|
|
|
func TestReadInputFromStdin_JSONLike(t *testing.T) {
|
|
jsonStr := `{"key": "value", "nested": {"a": 1}}`
|
|
reader := bufio.NewReader(strings.NewReader(jsonStr))
|
|
result := readInputFromStdin(reader)
|
|
if result != jsonStr {
|
|
t.Errorf("readInputFromStdin(json) = %q, want %q", result, jsonStr)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
if data.ContextWindow.CurrentUsage.CacheReadInputTokens != 3000 {
|
|
t.Errorf("CacheReadInputTokens = %d, want 3000", data.ContextWindow.CurrentUsage.CacheReadInputTokens)
|
|
}
|
|
}
|
|
|
|
func TestParseStatusInput_ExtraFields(t *testing.T) {
|
|
// JSON with extra unknown fields should still parse
|
|
jsonStr := `{"model": {"display_name": "Test", "unknown": "field"}, "extra": "data"}`
|
|
data, err := parseStatusInput(jsonStr)
|
|
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 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 TestStatuslineWidthOffset_Constant(t *testing.T) {
|
|
// Verify the constant is defined and reasonable
|
|
if statuslineWidthOffset <= 0 || statuslineWidthOffset > 20 {
|
|
t.Errorf("statuslineWidthOffset = %d, expected between 1 and 20", statuslineWidthOffset)
|
|
}
|
|
}
|
|
|
|
// Benchmark tests
|
|
|
|
func BenchmarkFormatContextInfo(b *testing.B) {
|
|
usage := &TokenUsage{
|
|
InputTokens: 50000,
|
|
CacheCreationTokens: 10000,
|
|
CacheReadInputTokens: 5000,
|
|
}
|
|
for i := 0; i < b.N; i++ {
|
|
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 i := 0; i < b.N; i++ {
|
|
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 i := 0; i < b.N; i++ {
|
|
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 i := 0; i < b.N; i++ {
|
|
buildStatusLine(data)
|
|
}
|
|
}
|
|
|
|
func BenchmarkCalculatePadding(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
calculatePadding("left side content", "right side", 120)
|
|
}
|
|
}
|
|
|
|
func BenchmarkFormatOutput(b *testing.B) {
|
|
left := red + "left" + reset
|
|
right := green + "right" + reset
|
|
for i := 0; i < b.N; i++ {
|
|
formatOutput(left, right, 50)
|
|
}
|
|
}
|
|
|
|
func BenchmarkReadInputFromStdin(b *testing.B) {
|
|
jsonStr := `{"model": {"display_name": "Test"}, "workspace": {"current_dir": "/test"}}`
|
|
for i := 0; i < b.N; i++ {
|
|
reader := bufio.NewReader(strings.NewReader(jsonStr))
|
|
readInputFromStdin(reader)
|
|
}
|
|
}
|