Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,36 @@ import (
"github.com/supermodeltools/cli/internal/shards"
)

// termIsTerminal wraps term.IsTerminal so tests can stub the syscall check
// independently of the /dev/tty fallback.
var termIsTerminal = func() bool {
return term.IsTerminal(int(syscall.Stdin)) //nolint:unconvert // syscall.Stdin is uintptr on Windows
}

// openDevTty opens /dev/tty (the process's controlling terminal). It is a
// package-level var so tests can replace it with a mock. On Windows,
// os.Open("/dev/tty") will fail, which is expected — Windows Console API
// (used by term.IsTerminal above) handles Windows Terminal/PowerShell.
var openDevTty = func() (*os.File, error) {
return os.Open("/dev/tty")
}

// stdinIsTerminal reports whether stdin is connected to an interactive
// terminal. Pulled into a var so tests can stub it.
//
// MinTTY (Windows Git Bash) reports a non-terminal stdin fd even though the
// user is at an interactive prompt, so we fall back to opening /dev/tty — the
// POSIX controlling-terminal device. If it opens, stdin is interactive.
// See issue #154.
var stdinIsTerminal = func() bool {
return term.IsTerminal(int(syscall.Stdin)) //nolint:unconvert // syscall.Stdin is uintptr on Windows
if termIsTerminal() {
return true
}
if f, err := openDevTty(); err == nil {
f.Close()
return true
}
return false
}

// rootAction enumerates the three branches the bare `supermodel` command
Expand Down
39 changes: 38 additions & 1 deletion cmd/root_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,43 @@
package cmd

import "testing"
import (
"os"
"testing"
)

// TestStdinIsTerminalUsesDevTtyFallback verifies that stdinIsTerminal falls
// back to opening /dev/tty when term.IsTerminal returns false (e.g. in CI or
// MinTTY/Git Bash on Windows). The fix is tracked in issue #154.
func TestStdinIsTerminalUsesDevTtyFallback(t *testing.T) {
// Force termIsTerminal to return false so the test is deterministic
// regardless of whether the test process itself has an interactive stdin.
origTermIsTerminal := termIsTerminal
t.Cleanup(func() { termIsTerminal = origTermIsTerminal })
termIsTerminal = func() bool { return false }

// Save and restore the real openDevTty hook.
orig := openDevTty
t.Cleanup(func() { openDevTty = orig })

called := false
openDevTty = func() (*os.File, error) {
called = true
// Return a real, harmless file so the caller can call f.Close().
return os.Open(os.DevNull)
}

// With termIsTerminal stubbed to false, stdinIsTerminal must call
// openDevTty as a fallback and return true because our mock succeeds.
got := stdinIsTerminal()

if !called {
t.Error("stdinIsTerminal did not call openDevTty — /dev/tty fallback is missing (fix #154)")
}
// If openDevTty was called and succeeded, the result must be true.
if called && !got {
t.Error("openDevTty returned a file but stdinIsTerminal returned false — fix #154")
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func TestPickRootAction(t *testing.T) {
cases := []struct {
Expand Down
Loading