diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-16 03:10:55 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-16 03:10:55 +0200 |
| commit | 1fc1611fa99993cab5dc8bf0844183285296e3b2 (patch) | |
| tree | c5c9b8b5abac5b5d4c0d56ed90b0580184cc4383 /internal/tmux | |
| parent | 12090f25a3677291863dbb80277bdad3eaec0324 (diff) | |
Release v0.24.0v0.24.0
Bring unit test coverage from ~75% to 85.1% project-wide. All internal
packages now exceed 80% coverage. Refactored cmd entrypoints to extract
testable run() functions with injectable seams.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/tmux')
| -rw-r--r-- | internal/tmux/status_coverage_test.go | 418 |
1 files changed, 418 insertions, 0 deletions
diff --git a/internal/tmux/status_coverage_test.go b/internal/tmux/status_coverage_test.go new file mode 100644 index 0000000..8f7c034 --- /dev/null +++ b/internal/tmux/status_coverage_test.go @@ -0,0 +1,418 @@ +package tmux + +import ( + "strings" + "testing" + "time" +) + +// --- Enabled --- + +func TestEnabled_DefaultTrue(t *testing.T) { + t.Setenv("HEXAI_TMUX_STATUS", "") + if !Enabled() { + t.Fatal("expected Enabled() true when env is empty") + } +} + +func TestEnabled_TruthyValues(t *testing.T) { + for _, v := range []string{"1", "true", "yes", "on", " TRUE ", " On "} { + t.Run(v, func(t *testing.T) { + t.Setenv("HEXAI_TMUX_STATUS", v) + if !Enabled() { + t.Fatalf("expected Enabled() true for %q", v) + } + }) + } +} + +func TestEnabled_FalsyValues(t *testing.T) { + for _, v := range []string{"0", "false", "no", "off", "random"} { + t.Run(v, func(t *testing.T) { + t.Setenv("HEXAI_TMUX_STATUS", v) + if Enabled() { + t.Fatalf("expected Enabled() false for %q", v) + } + }) + } +} + +// --- SetUserOption (logic paths, not actual tmux calls) --- + +func TestSetUserOption_DisabledByEnv(t *testing.T) { + t.Setenv("HEXAI_TMUX_STATUS", "off") + // Should return nil immediately when disabled + if err := SetUserOption("hexai_status", "test"); err != nil { + t.Fatalf("expected nil error when disabled, got %v", err) + } +} + +func TestSetUserOption_EmptyKey(t *testing.T) { + t.Setenv("HEXAI_TMUX_STATUS", "1") + t.Setenv("TMUX", "/tmp/tmux-1,1,1") + old := lookPath + t.Cleanup(func() { lookPath = old }) + lookPath = func(string) (string, error) { return "/bin/tmux", nil } + // Empty key after trimming should return nil + if err := SetUserOption(" @ ", "test"); err != nil { + t.Fatalf("expected nil for empty key, got %v", err) + } + if err := SetUserOption(" ", "test"); err != nil { + t.Fatalf("expected nil for blank key, got %v", err) + } +} + +// --- SetStatus (just verifies it delegates; no tmux binary needed when disabled) --- + +func TestSetStatus_DisabledNoOp(t *testing.T) { + t.Setenv("HEXAI_TMUX_STATUS", "off") + if err := SetStatus("anything"); err != nil { + t.Fatalf("expected nil when status disabled, got %v", err) + } +} + +// --- FormatLLMStartStatus --- + +func TestFormatLLMStartStatus(t *testing.T) { + s := FormatLLMStartStatus("openai", "gpt-4.1") + if !strings.Contains(s, "LLM:openai:gpt-4.1") { + t.Fatalf("missing provider:model in %q", s) + } + if !strings.Contains(s, "⏳") { + t.Fatalf("missing hourglass emoji in %q", s) + } + // Should contain baseFGToken placeholders (pre-theme) + if !strings.Contains(s, baseFGToken) { + t.Fatalf("expected baseFGToken placeholder in %q", s) + } +} + +// --- humanWindow --- + +func TestHumanWindow(t *testing.T) { + tests := []struct { + name string + d time.Duration + want string + }{ + {"zero", 0, "?"}, + {"negative", -5 * time.Minute, "?"}, + {"exact hour", time.Hour, "1h"}, + {"two hours", 2 * time.Hour, "2h"}, + {"30 minutes", 30 * time.Minute, "30m"}, + {"90 minutes", 90 * time.Minute, "90m"}, + {"45 minutes", 45 * time.Minute, "45m"}, + {"120 minutes", 120 * time.Minute, "2h"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := humanWindow(tt.d) + if got != tt.want { + t.Errorf("humanWindow(%v) = %q, want %q", tt.d, got, tt.want) + } + }) + } +} + +// --- truncateStatus --- + +func TestTruncateStatus(t *testing.T) { + tests := []struct { + name string + s string + n int + want string + }{ + {"zero limit", "hello", 0, ""}, + {"negative limit", "hello", -1, ""}, + {"within limit", "hi", 5, "hi"}, + {"exact limit", "hello", 5, "hello"}, + {"over limit", "hello world", 5, "hell…"}, + {"limit 1", "hello", 1, "h"}, + {"limit 2", "hello", 2, "h…"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := truncateStatus(tt.s, tt.n) + if got != tt.want { + t.Errorf("truncateStatus(%q, %d) = %q, want %q", tt.s, tt.n, got, tt.want) + } + }) + } +} + +// --- stringsTrim --- + +func TestStringsTrim(t *testing.T) { + tests := []struct { + name string + s string + want string + }{ + {"empty", "", ""}, + {"no whitespace", "hello", "hello"}, + {"leading spaces", " hello", "hello"}, + {"trailing spaces", "hello ", "hello"}, + {"both sides", " hello ", "hello"}, + {"tabs", "\thello\t", "hello"}, + {"newlines", "\nhello\n", "hello"}, + {"carriage returns", "\rhello\r", "hello"}, + {"mixed whitespace", " \t\n\rhello \t\n\r", "hello"}, + {"all whitespace", " \t\n ", ""}, + {"internal spaces preserved", "hello world", "hello world"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := stringsTrim(tt.s) + if got != tt.want { + t.Errorf("stringsTrim(%q) = %q, want %q", tt.s, got, tt.want) + } + }) + } +} + +// --- maxStatusLen --- + +func TestMaxStatusLen(t *testing.T) { + tests := []struct { + name string + env string + want int + }{ + {"empty", "", 0}, + {"valid", "80", 80}, + {"negative", "-5", 0}, + {"zero", "0", 0}, + {"non-numeric", "abc", 0}, + {"whitespace padded", " 100 ", 100}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("HEXAI_TMUX_STATUS_MAXLEN", tt.env) + got := maxStatusLen() + if got != tt.want { + t.Errorf("maxStatusLen() = %d, want %d (env=%q)", got, tt.want, tt.env) + } + }) + } +} + +// --- narrowEnabled --- + +func TestNarrowEnabled(t *testing.T) { + tests := []struct { + name string + env string + want bool + }{ + {"empty", "", false}, + {"1", "1", true}, + {"true", "true", true}, + {"yes", "yes", true}, + {"on", "on", true}, + {"0", "0", false}, + {"false", "false", false}, + {"random", "random", false}, + {"TRUE uppercase", "TRUE", true}, + {"padded", " 1 ", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("HEXAI_TMUX_STATUS_NARROW", tt.env) + got := narrowEnabled() + if got != tt.want { + t.Errorf("narrowEnabled() = %v, want %v (env=%q)", got, tt.want, tt.env) + } + }) + } +} + +// --- applyTheme --- + +func TestApplyTheme_NoTheme(t *testing.T) { + t.Setenv("HEXAI_TMUX_STATUS_THEME", "") + t.Setenv("HEXAI_TMUX_STATUS_FG", "") + t.Setenv("HEXAI_TMUX_STATUS_BG", "") + input := baseFGToken + "hello" + arrowUpToken + "up" + arrowDownToken + "down" + got := applyTheme(input) + // Should replace tokens with default fg and default arrow colors + if strings.Contains(got, baseFGToken) { + t.Fatalf("baseFGToken not replaced in %q", got) + } + if !strings.Contains(got, "#[fg=default]") { + t.Fatalf("expected default fg in %q", got) + } + // No wrap, so no bg=default suffix + if strings.HasSuffix(got, "#[fg=default,bg=default]") { + t.Fatalf("should not wrap without theme in %q", got) + } +} + +func TestApplyTheme_PurpleTheme(t *testing.T) { + t.Setenv("HEXAI_TMUX_STATUS_THEME", "purple") + t.Setenv("HEXAI_TMUX_STATUS_FG", "") + t.Setenv("HEXAI_TMUX_STATUS_BG", "") + input := baseFGToken + "hello" + got := applyTheme(input) + if !strings.Contains(got, "#[fg=white") { + t.Fatalf("expected white fg for purple theme in %q", got) + } + if !strings.Contains(got, "bg=magenta") { + t.Fatalf("expected magenta bg for purple theme in %q", got) + } + if !strings.HasSuffix(got, "#[fg=default,bg=default]") { + t.Fatalf("expected reset suffix for themed output in %q", got) + } +} + +func TestApplyTheme_YellowTheme(t *testing.T) { + t.Setenv("HEXAI_TMUX_STATUS_THEME", "yellow") + t.Setenv("HEXAI_TMUX_STATUS_FG", "") + t.Setenv("HEXAI_TMUX_STATUS_BG", "") + input := baseFGToken + "test" + arrowUpToken + "u" + arrowDownToken + "d" + got := applyTheme(input) + if !strings.Contains(got, "#[fg=black") { + t.Fatalf("expected black fg for yellow theme in %q", got) + } + if !strings.Contains(got, "bg=yellow") { + t.Fatalf("expected yellow bg in %q", got) + } +} + +func TestApplyTheme_BlueTheme(t *testing.T) { + t.Setenv("HEXAI_TMUX_STATUS_THEME", "blue") + t.Setenv("HEXAI_TMUX_STATUS_FG", "") + t.Setenv("HEXAI_TMUX_STATUS_BG", "") + input := baseFGToken + "test" + arrowUpToken + "u" + arrowDownToken + "d" + got := applyTheme(input) + if !strings.Contains(got, "#[fg=white") { + t.Fatalf("expected white fg for blue theme in %q", got) + } + if !strings.Contains(got, "bg=blue") { + t.Fatalf("expected blue bg in %q", got) + } +} + +func TestApplyTheme_ExplicitFGBG(t *testing.T) { + t.Setenv("HEXAI_TMUX_STATUS_THEME", "") + t.Setenv("HEXAI_TMUX_STATUS_FG", "red") + t.Setenv("HEXAI_TMUX_STATUS_BG", "green") + input := baseFGToken + "test" + arrowUpToken + "u" + arrowDownToken + "d" + got := applyTheme(input) + if !strings.Contains(got, "#[fg=red") { + t.Fatalf("expected red fg in %q", got) + } + if !strings.Contains(got, "bg=green") { + t.Fatalf("expected green bg in %q", got) + } + if !strings.HasSuffix(got, "#[fg=default,bg=default]") { + t.Fatalf("expected reset suffix in %q", got) + } +} + +func TestApplyTheme_ExplicitBGOnly(t *testing.T) { + t.Setenv("HEXAI_TMUX_STATUS_THEME", "") + t.Setenv("HEXAI_TMUX_STATUS_FG", "") + t.Setenv("HEXAI_TMUX_STATUS_BG", "cyan") + input := baseFGToken + "test" + got := applyTheme(input) + // When only bg is set, fg defaults to "default" + if !strings.Contains(got, "#[fg=default") { + t.Fatalf("expected default fg when only bg set in %q", got) + } + if !strings.Contains(got, "bg=cyan") { + t.Fatalf("expected cyan bg in %q", got) + } +} + +func TestApplyTheme_NoTokensInInput(t *testing.T) { + t.Setenv("HEXAI_TMUX_STATUS_THEME", "") + t.Setenv("HEXAI_TMUX_STATUS_FG", "") + t.Setenv("HEXAI_TMUX_STATUS_BG", "") + // Input without any tokens should pass through unchanged + got := applyTheme("plain text") + if got != "plain text" { + t.Fatalf("expected unchanged output for tokenless input, got %q", got) + } +} + +func TestApplyTheme_ThemeAliases(t *testing.T) { + // Test theme aliases that map to the same preset + aliases := map[string]string{ + "white-on-purple": "magenta", + "magenta": "magenta", + "white-on-magenta": "magenta", + "black-on-yellow": "yellow", + "black-on-gold": "yellow", + "white-on-blue": "blue", + "white-on-navy": "blue", + } + for theme, expectBG := range aliases { + t.Run(theme, func(t *testing.T) { + t.Setenv("HEXAI_TMUX_STATUS_THEME", theme) + t.Setenv("HEXAI_TMUX_STATUS_FG", "") + t.Setenv("HEXAI_TMUX_STATUS_BG", "") + got := applyTheme(baseFGToken + "x") + if !strings.Contains(got, "bg="+expectBG) { + t.Fatalf("theme %q: expected bg=%s in %q", theme, expectBG, got) + } + }) + } +} + +// --- FormatGlobalStatusColored branch coverage --- + +func TestFormatGlobalStatusColored_EmptyProvider(t *testing.T) { + t.Setenv("HEXAI_TMUX_STATUS_NARROW", "") + t.Setenv("HEXAI_TMUX_STATUS_MAXLEN", "") + // Empty provider should return head only (no tail) + s := FormatGlobalStatusColored(10, 3.3, 1000, 2000, "", "model", 1.1, 4, time.Hour) + if strings.Contains(s, "|") { + t.Fatalf("expected no tail with empty provider: %q", s) + } +} + +func TestFormatGlobalStatusColored_EmptyModel(t *testing.T) { + t.Setenv("HEXAI_TMUX_STATUS_NARROW", "") + t.Setenv("HEXAI_TMUX_STATUS_MAXLEN", "") + // Empty model should return head only + s := FormatGlobalStatusColored(10, 3.3, 1000, 2000, "openai", "", 1.1, 4, time.Hour) + if strings.Contains(s, "|") { + t.Fatalf("expected no tail with empty model: %q", s) + } +} + +func TestFormatGlobalStatusColored_WithTail(t *testing.T) { + t.Setenv("HEXAI_TMUX_STATUS_NARROW", "") + t.Setenv("HEXAI_TMUX_STATUS_MAXLEN", "") + // With valid provider and model, should include tail + s := FormatGlobalStatusColored(10, 3.3, 1000, 2000, "openai", "gpt-4.1", 1.1, 4, time.Hour) + if !strings.Contains(s, "|") || !strings.Contains(s, "openai:gpt-4.1") { + t.Fatalf("expected tail with provider:model: %q", s) + } +} + +func TestFormatGlobalStatusColored_MaxLenTruncatesHead(t *testing.T) { + t.Setenv("HEXAI_TMUX_STATUS_NARROW", "") + t.Setenv("HEXAI_TMUX_STATUS_THEME", "") + t.Setenv("HEXAI_TMUX_STATUS_FG", "") + t.Setenv("HEXAI_TMUX_STATUS_BG", "") + // Set maxlen very small so even head gets truncated + t.Setenv("HEXAI_TMUX_STATUS_MAXLEN", "5") + s := FormatGlobalStatusColored(10, 3.3, 1000, 2000, "openai", "gpt-4.1", 1.1, 4, time.Hour) + // The string contains control-char tokens; truncateStatus works on raw bytes. + // Just verify it ends with the ellipsis character and is shorter than untruncated. + if !strings.HasSuffix(s, "…") { + t.Fatalf("expected truncated output ending with ellipsis, got %q", s) + } +} + +func TestFormatGlobalStatusColored_MaxLenFitsBoth(t *testing.T) { + t.Setenv("HEXAI_TMUX_STATUS_NARROW", "") + // Set maxlen very large so both head and tail fit + t.Setenv("HEXAI_TMUX_STATUS_MAXLEN", "500") + s := FormatGlobalStatusColored(10, 3.3, 1000, 2000, "openai", "gpt-4.1", 1.1, 4, time.Hour) + if !strings.Contains(s, "|") || !strings.Contains(s, "openai:gpt-4.1") { + t.Fatalf("expected full output with large maxlen: %q", s) + } +} |
