feat(v0.5.3): doctor -- checkTmux three-state helper

This commit is contained in:
2026-05-08 14:04:14 -04:00
parent 8dec5e08a4
commit be508e2ec7
2 changed files with 96 additions and 0 deletions
+30
View File
@@ -11,6 +11,7 @@ import (
"github.com/spf13/cobra"
"github.com/warrenronsiek/ctask/internal/config"
"github.com/warrenronsiek/ctask/internal/shell"
"github.com/warrenronsiek/ctask/internal/workspace"
)
@@ -203,6 +204,9 @@ func runDoctor(cmd *cobra.Command, args []string) error {
// Check 8: CTASK_PROJECT_ROOT (v0.5).
checkProjectRoot(&passed, &failed)
// Check 9: tmux availability for persistent session mode (v0.5.3).
checkTmux(&passed, &failed)
// Summary
fmt.Println()
fmt.Printf("%d checks passed, %d failed\n", passed, failed)
@@ -254,3 +258,29 @@ func checkSeedDir(label, envValue, resolved, envName string, passed, failed *int
fmt.Printf(" Fix: create the directory or unset %s to use built-in defaults.\n", envName)
*failed++
}
// checkTmux reports the three-state tmux check (v0.5.3):
// - CTASK_SESSION_MODE != "persistent" -> INFO (direct mode, tmux optional)
// - persistent + tmux on PATH + version OK -> two INFO lines
// - persistent + tmux missing or too old -> FAIL with install/update hint
func checkTmux(passed, failed *int) {
_ = passed
mode := config.ResolveSessionMode()
if mode != "persistent" {
fmt.Printf(" [INFO] Session mode: direct (tmux not required)\n")
return
}
fmt.Printf(" [INFO] Session mode: persistent\n")
tmuxPath, ver, err := shell.LookupTmux()
if err != nil {
fmt.Printf(" [FAIL] tmux not found on PATH or unsupported version: %v\n", err)
fmt.Printf(" Fix: install tmux 3.0+ (apt/brew/pacman/dnf), or unset CTASK_SESSION_MODE\n")
*failed++
return
}
rawVer := ver.Raw
if rawVer == "" {
rawVer = "unknown version"
}
fmt.Printf(" [INFO] tmux found: %s (%s)\n", rawVer, tmuxPath)
}
+66
View File
@@ -2,12 +2,31 @@ package cmd
import (
"bytes"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
// captureStdout runs fn while capturing os.Stdout and returns the output.
func captureStdout(t *testing.T, fn func()) string {
t.Helper()
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("pipe: %v", err)
}
orig := os.Stdout
os.Stdout = w
defer func() { os.Stdout = orig }()
fn()
w.Close()
data, _ := io.ReadAll(r)
return string(data)
}
// This file swaps process-global os.Stdout and env vars. Do not call
// t.Parallel() in this file.
@@ -161,3 +180,50 @@ func TestDoctorProjectRootSetButMissingFails(t *testing.T) {
t.Errorf("expected FAIL line, got:\n%s", out)
}
}
func TestCheckTmuxNotConfigured(t *testing.T) {
os.Unsetenv("CTASK_SESSION_MODE")
out := captureStdout(t, func() {
passed, failed := 0, 0
checkTmux(&passed, &failed)
})
if !strings.Contains(out, "Session mode: direct") {
t.Errorf("expected 'Session mode: direct' info line: %q", out)
}
}
func TestCheckTmuxConfiguredAndPresent(t *testing.T) {
if _, err := exec.LookPath("tmux"); err != nil {
t.Skip("tmux not on PATH")
}
os.Setenv("CTASK_SESSION_MODE", "persistent")
defer os.Unsetenv("CTASK_SESSION_MODE")
out := captureStdout(t, func() {
passed, failed := 0, 0
checkTmux(&passed, &failed)
})
if !strings.Contains(out, "Session mode: persistent") {
t.Errorf("expected 'Session mode: persistent': %q", out)
}
if !strings.Contains(out, "tmux found") {
t.Errorf("expected 'tmux found' info line: %q", out)
}
}
func TestCheckTmuxConfiguredAndMissing(t *testing.T) {
orig := os.Getenv("PATH")
defer os.Setenv("PATH", orig)
os.Setenv("PATH", "")
os.Setenv("CTASK_SESSION_MODE", "persistent")
defer os.Unsetenv("CTASK_SESSION_MODE")
failed := 0
passed := 0
out := captureStdout(t, func() { checkTmux(&passed, &failed) })
if failed != 1 {
t.Errorf("missing tmux must increment failed counter; got %d", failed)
}
if !strings.Contains(out, "tmux not found") {
t.Errorf("expected 'tmux not found' fail line: %q", out)
}
}