1package cmd
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8)
9
10func TestSubcommandNoArgsNonInteractiveRunsDirectly(t *testing.T) {
11 // Not parallel: mutates package-level isStdinTerminal.
12
13 // Override stdin terminal check to simulate non-interactive environment.
14 original := isStdinTerminal
15 t.Cleanup(func() {
16 isStdinTerminal = original
17 })
18 isStdinTerminal = func() bool { return false }
19
20 // Create a config with backup paths defined so the command can run.
21 configFile := setupConfigWithBackupPaths(t)
22 setRootFlagValuesForTest(t, "home@cloud", true, configFile)
23 t.Setenv("KELD_CONFIG_FILE", "")
24 t.Setenv("KELD_DRYRUN", "")
25
26 backup := lookupSubcommand(t, "backup")
27 out, err := captureStdout(t, func() error {
28 return backup.RunE(backup, []string{})
29 })
30 // Should NOT get a bubbletea TTY error. Instead, should either:
31 // - Succeed with dry-run output (if config has paths), or
32 // - Fail with a sensible error about missing inputs.
33 if err != nil {
34 errMsg := err.Error()
35 // The old bug would produce "bubbletea: error opening TTY" here.
36 if strings.Contains(errMsg, "bubbletea") || strings.Contains(errMsg, "TTY") {
37 t.Fatalf("non-interactive mode incorrectly tried to open TTY: %v", err)
38 }
39 // A different error is acceptable (e.g., missing paths).
40 // For now we just verify no TTY error occurred.
41 t.Logf("command returned error (expected for missing paths): %v", err)
42 return
43 }
44
45 // If no error, we should see dry-run output.
46 if !strings.Contains(out, `"backup"`) {
47 t.Fatalf("expected dry-run output to contain backup command, got:\n%s", out)
48 }
49}
50
51// setupConfigWithBackupPaths creates a config file with a preset that has
52// backup paths defined, allowing the backup command to run without args.
53func setupConfigWithBackupPaths(t *testing.T) string {
54 t.Helper()
55
56 dir := t.TempDir()
57 cfg := filepath.Join(dir, "config.toml")
58 err := os.WriteFile(cfg, []byte(`
59[global]
60repo = "/repos/default"
61
62["home@"]
63tag = "home"
64
65["home@".backup]
66_arguments = "/src /dst"
67
68["@cloud"]
69repo = "/repos/cloud"
70`), 0o600)
71 if err != nil {
72 t.Fatalf("writing fixture: %v", err)
73 }
74 t.Setenv("HOME", dir)
75
76 return cfg
77}
78
79func TestSubcommandNoArgsInteractiveEntersSession(t *testing.T) {
80 // Not parallel: mutates package-level isStdinTerminal.
81
82 // Override stdin terminal check to simulate interactive terminal.
83 original := isStdinTerminal
84 t.Cleanup(func() {
85 isStdinTerminal = original
86 })
87 isStdinTerminal = func() bool { return true }
88
89 configFile := setupConfigWithBackupPaths(t)
90 setRootFlagValuesForTest(t, "", true, configFile)
91 t.Setenv("KELD_CONFIG_FILE", "")
92 t.Setenv("KELD_DRYRUN", "")
93
94 backup := lookupSubcommand(t, "backup")
95
96 // In interactive mode with no args, runInteractive is called.
97 // Since there's no real TTY attached during test, bubbletea will
98 // try to open /dev/tty and fail. We assert this behavior is happening
99 // (rather than the command running directly).
100 _, err := captureStdout(t, func() error {
101 return backup.RunE(backup, []string{})
102 })
103
104 // The expected behavior: bubbletea tries to open TTY and fails.
105 if err == nil {
106 // If it succeeded, either we're not in interactive mode or
107 // the test environment has a TTY (unlikely in CI).
108 t.Fatal("expected bubbletea TTY error when simulating interactive mode, got success")
109 }
110 errMsg := err.Error()
111 if !strings.Contains(errMsg, "TTY") && !strings.Contains(errMsg, "tty") {
112 t.Fatalf("expected bubbletea TTY error in interactive mode, got: %v", err)
113 }
114}