package cmd import ( "errors" "os" "runtime" "strings" "testing" "time" "github.com/warrenronsiek/ctask/internal/session" "github.com/warrenronsiek/ctask/internal/shell" ) // withTTYCheck swaps the package-level isTTYCheck for the duration of the // test. Tests that exercise refusal paths must NOT run in parallel — they // mutate a package global. func withTTYCheck(t *testing.T, fn func() bool) { t.Helper() orig := isTTYCheck isTTYCheck = fn t.Cleanup(func() { isTTYCheck = orig }) } // withInvocationName pins the binary name surfaced in user-facing hints // to a fixed value (typically "ctask") for the duration of the test, so // substring assertions against rendered hints stay stable regardless of // the Go test binary's name. Must NOT run in parallel — mutates a // package global. func withInvocationName(t *testing.T, name string) { t.Helper() orig := invocationNameOverride invocationNameOverride = name t.Cleanup(func() { invocationNameOverride = orig }) } func TestPreflightRefusesNativeWindows(t *testing.T) { if runtime.GOOS != "windows" { t.Skip("native-Windows refusal applies only on Windows") } had := os.Getenv("WSL_DISTRO_NAME") os.Unsetenv("WSL_DISTRO_NAME") t.Cleanup(func() { if had != "" { os.Setenv("WSL_DISTRO_NAME", had) } }) _, err := preflightPersistentEntry("resume") if err == nil { t.Fatal("expected refusal on native Windows") } if !strings.Contains(err.Error(), "tmux") || !strings.Contains(err.Error(), "WSL") { t.Errorf("expected tmux+WSL message: %v", err) } } func TestPreflightRefusesNestedTmux(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("nested-tmux check runs on Unix paths only in this test") } withTTYCheck(t, func() bool { return true }) os.Setenv("TMUX", "/tmp/tmux-1000/default,1234,0") t.Cleanup(func() { os.Unsetenv("TMUX") }) _, err := preflightPersistentEntry("resume") if err == nil { t.Fatal("expected refusal when $TMUX is set") } if !strings.Contains(err.Error(), "already inside tmux") { t.Errorf("expected nested-tmux message: %v", err) } } func TestPreflightRefusesNonTTY(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("focus this case on Unix; Windows TTY semantics covered by manual smoke") } os.Unsetenv("TMUX") withTTYCheck(t, func() bool { return false }) withInvocationName(t, "ctask") _, err := preflightPersistentEntry("resume") if err == nil { t.Fatal("expected refusal when not a TTY") } if !strings.Contains(err.Error(), "interactive terminal") { t.Errorf("expected interactive-terminal message: %v", err) } if !strings.Contains(err.Error(), "ssh -t") { t.Errorf("error should mention ssh -t: %v", err) } if !strings.Contains(err.Error(), "ctask resume --direct") { t.Errorf("error should mention command-specific bypass form: %v", err) } } func TestPreflightCommandNameRendersInHints(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("path-based check covered on Unix") } os.Unsetenv("TMUX") withTTYCheck(t, func() bool { return false }) withInvocationName(t, "ctask") _, err := preflightPersistentEntry("attach") if err == nil || !strings.Contains(err.Error(), "ctask attach --direct") { t.Errorf("commandName must appear in bypass hint: %v", err) } } func TestPreflightTmuxNotFound(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("PATH manipulation applies on Unix here") } os.Unsetenv("TMUX") withTTYCheck(t, func() bool { return true }) orig := os.Getenv("PATH") t.Cleanup(func() { os.Setenv("PATH", orig) }) os.Setenv("PATH", "") _, err := preflightPersistentEntry("resume") if err == nil { t.Fatal("expected refusal when tmux missing") } if !strings.Contains(err.Error(), "tmux is not installed") { t.Errorf("expected install-hint message: %v", err) } } // Confirm the helper returns the validated tmux path on the happy path so // callers can pass it through LaunchOpts.TmuxPath without re-resolving. func TestPreflightSuccessReturnsTmuxPath(t *testing.T) { if _, _, err := shell.LookupTmux(); err != nil { if errors.Is(err, shell.ErrTmuxNotFound) || errors.Is(err, shell.ErrTmuxTooOld) { t.Skip("tmux not adequate on this host") } } if runtime.GOOS == "windows" { t.Skip("happy-path test on WSL/Linux only") } os.Unsetenv("TMUX") withTTYCheck(t, func() bool { return true }) tmuxPath, err := preflightPersistentEntry("resume") if err != nil { t.Fatalf("expected success, got %v", err) } if tmuxPath == "" { t.Error("expected non-empty tmuxPath on success") } } func TestConfirmFreshRemoteAdoptionRefusesOnNonTTY(t *testing.T) { wsDir := t.TempDir() // Write a fresh remote lease so the prompt has data to display. other := "remote-host-xyz" l := &session.Lease{ SessionID: "x", Hostname: other, StartedAt: time.Now().UTC(), LastHeartbeatAt: time.Now().UTC(), } if err := session.WriteLease(session.LeasePath(wsDir), l); err != nil { t.Fatalf("WriteLease: %v", err) } withTTYCheck(t, func() bool { return false }) err := confirmFreshRemoteAdoption(wsDir) if err == nil { t.Fatal("expected refusal on non-TTY") } if !strings.Contains(err.Error(), other) { t.Errorf("error should name the remote host: %v", err) } }