@@ -48,8 +48,8 @@ func registerSubcommands() {
// When invoked with no flags or arguments and the command
// is wrapped (has TUI screens), enter the interactive
// session with the command pre-selected, skipping the
- // menu screen.
- if len(args) == 0 && isWrappedCommand(commandName) {
+ // menu screen. Only do this when running interactively.
+ if len(args) == 0 && isWrappedCommand(commandName) && isInteractive() {
cmdName, preset, overrides, err := runInteractive(commandName)
if err != nil {
return err
@@ -0,0 +1,115 @@
+package cmd
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+func TestSubcommandNoArgsNonInteractiveRunsDirectly(t *testing.T) {
+ // Not parallel: mutates package-level isStdinTerminal.
+
+ // Override stdin terminal check to simulate non-interactive environment.
+ original := isStdinTerminal
+ t.Cleanup(func() {
+ isStdinTerminal = original
+ })
+ isStdinTerminal = func() bool { return false }
+
+ // Create a config with backup paths defined so the command can run.
+ configFile := setupConfigWithBackupPaths(t)
+ setRootFlagValuesForTest(t, "home@cloud", true, configFile)
+ t.Setenv("KELD_CONFIG_FILE", "")
+ t.Setenv("KELD_DRYRUN", "")
+
+ backup := lookupSubcommand(t, "backup")
+ out, err := captureStdout(t, func() error {
+ return backup.RunE(backup, []string{})
+ })
+
+ // Should NOT get a bubbletea TTY error. Instead, should either:
+ // - Succeed with dry-run output (if config has paths), or
+ // - Fail with a sensible error about missing inputs.
+ if err != nil {
+ errMsg := err.Error()
+ // The old bug would produce "bubbletea: error opening TTY" here.
+ if strings.Contains(errMsg, "bubbletea") || strings.Contains(errMsg, "TTY") {
+ t.Fatalf("non-interactive mode incorrectly tried to open TTY: %v", err)
+ }
+ // A different error is acceptable (e.g., missing paths).
+ // For now we just verify no TTY error occurred.
+ t.Logf("command returned error (expected for missing paths): %v", err)
+ return
+ }
+
+ // If no error, we should see dry-run output.
+ if !strings.Contains(out, `"backup"`) {
+ t.Fatalf("expected dry-run output to contain backup command, got:\n%s", out)
+ }
+}
+
+// setupConfigWithBackupPaths creates a config file with a preset that has
+// backup paths defined, allowing the backup command to run without args.
+func setupConfigWithBackupPaths(t *testing.T) string {
+ t.Helper()
+
+ dir := t.TempDir()
+ cfg := filepath.Join(dir, "config.toml")
+ err := os.WriteFile(cfg, []byte(`
+[global]
+repo = "/repos/default"
+
+["home@"]
+tag = "home"
+
+["home@".backup]
+_arguments = "/src /dst"
+
+["@cloud"]
+repo = "/repos/cloud"
+`), 0o600)
+ if err != nil {
+ t.Fatalf("writing fixture: %v", err)
+ }
+ t.Setenv("HOME", dir)
+
+ return cfg
+}
+
+func TestSubcommandNoArgsInteractiveEntersSession(t *testing.T) {
+ // Not parallel: mutates package-level isStdinTerminal.
+
+ // Override stdin terminal check to simulate interactive terminal.
+ original := isStdinTerminal
+ t.Cleanup(func() {
+ isStdinTerminal = original
+ })
+ isStdinTerminal = func() bool { return true }
+
+ configFile := setupConfigWithBackupPaths(t)
+ setRootFlagValuesForTest(t, "", true, configFile)
+ t.Setenv("KELD_CONFIG_FILE", "")
+ t.Setenv("KELD_DRYRUN", "")
+
+ backup := lookupSubcommand(t, "backup")
+
+ // In interactive mode with no args, runInteractive is called.
+ // Since there's no real TTY attached during test, bubbletea will
+ // try to open /dev/tty and fail. We assert this behavior is happening
+ // (rather than the command running directly).
+ _, err := captureStdout(t, func() error {
+ return backup.RunE(backup, []string{})
+ })
+
+ // The expected behavior: bubbletea tries to open TTY and fails.
+ if err == nil {
+ // If it succeeded, either we're not in interactive mode or
+ // the test environment has a TTY (unlikely in CI).
+ t.Fatal("expected bubbletea TTY error when simulating interactive mode, got success")
+ }
+ errMsg := err.Error()
+ if !strings.Contains(errMsg, "TTY") && !strings.Contains(errMsg, "tty") {
+ t.Fatalf("expected bubbletea TTY error in interactive mode, got: %v", err)
+ }
+}