diff --git a/cmd/root.go b/cmd/root.go index 9750d9bf6fc1c3b471408996482347a7c195779d..1cae4d1c4c20d50e0e474f5fff1e3b9a759574ad 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -44,6 +44,11 @@ var rootCmd = &cobra.Command{ TraverseChildren: true, RunE: func(cmd *cobra.Command, _ []string) error { + // Bare keld with no subcommand requires a tty. + if !isInteractive() { + return fmt.Errorf("keld: no subcommand specified; non-interactive mode requires a subcommand (e.g. 'keld backup')") + } + commandName, preset, overrides, err := runInteractive("") if err != nil { return err diff --git a/cmd/root_noninteractive_test.go b/cmd/root_noninteractive_test.go new file mode 100644 index 0000000000000000000000000000000000000000..dc2409e22b120d559635870cfb2feaccdb533e63 --- /dev/null +++ b/cmd/root_noninteractive_test.go @@ -0,0 +1,38 @@ +package cmd + +import ( + "strings" + "testing" +) + +func TestRootRunNonInteractiveRequiresSubcommand(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 } + + // Reset root command flags to defaults. + setRootFlagValuesForTest(t, "", false, "") + t.Setenv("KELD_CONFIG_FILE", "") + t.Setenv("KELD_DRYRUN", "") + + err := rootCmd.RunE(rootCmd, nil) + if err == nil { + t.Fatal("expected error for bare keld in non-interactive mode, got nil") + } + + errMsg := err.Error() + // Should NOT be a bubbletea TTY error. + if strings.Contains(errMsg, "bubbletea") || strings.Contains(errMsg, "TTY") { + t.Fatalf("non-interactive mode incorrectly tried to open TTY: %v", err) + } + + // Should mention missing subcommand. + if !strings.Contains(errMsg, "subcommand") { + t.Fatalf("expected error to mention subcommand, got: %v", err) + } +} \ No newline at end of file