1package modes
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8
9 "github.com/patriceckhart/zot/packages/core"
10 "github.com/patriceckhart/zot/packages/tui"
11)
12
13func TestSessionDialogDeleteRequiresConfirmation(t *testing.T) {
14 d := newSessionDialog()
15 d.active = true
16 d.sessions = []core.SessionSummary{{Path: "/sessions/one.jsonl"}}
17
18 if act := d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'd'}); act.Delete {
19 t.Fatal("delete hotkey immediately returned a delete action")
20 }
21 if !d.deleting {
22 t.Fatal("delete hotkey did not open confirmation")
23 }
24 if act := d.HandleKey(tui.Key{Kind: tui.KeyEnter}); act.Delete {
25 t.Fatal("enter confirmed deletion; want safe default cancellation")
26 }
27 if d.deleting {
28 t.Fatal("enter did not close deletion confirmation")
29 }
30
31 d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'd'})
32 act := d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'y'})
33 if !act.Delete || act.Path != "/sessions/one.jsonl" {
34 t.Fatalf("confirmed action = %+v, want delete for selected path", act)
35 }
36 if len(d.sessions) != 1 {
37 t.Fatal("dialog removed row before the host confirmed filesystem deletion")
38 }
39}
40
41func TestSessionDialogDeleteConfirmationRender(t *testing.T) {
42 d := newSessionDialog()
43 d.active = true
44 d.sessions = []core.SessionSummary{{Path: "/sessions/one.jsonl", Title: "keep or delete"}}
45 d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'd'})
46
47 plain := strings.Join(d.Render(tui.Theme{}, 100), "\n")
48 for _, want := range []string{"permanently delete", "keep or delete", "press y to delete"} {
49 if !strings.Contains(plain, want) {
50 t.Fatalf("confirmation render missing %q:\n%s", want, plain)
51 }
52 }
53}
54
55func TestSessionDialogRemoveKeepsCursorValid(t *testing.T) {
56 tests := []struct {
57 name string
58 cursor int
59 removePath string
60 wantCursor int
61 wantPath string
62 }{
63 {name: "first", cursor: 0, removePath: "a", wantCursor: 0, wantPath: "b"},
64 {name: "middle", cursor: 1, removePath: "b", wantCursor: 1, wantPath: "c"},
65 {name: "last", cursor: 2, removePath: "c", wantCursor: 1, wantPath: "b"},
66 }
67 for _, tt := range tests {
68 t.Run(tt.name, func(t *testing.T) {
69 d := &sessionDialog{
70 sessions: []core.SessionSummary{{Path: "a"}, {Path: "b"}, {Path: "c"}},
71 cursor: tt.cursor,
72 }
73 d.Remove(tt.removePath)
74 if d.cursor != tt.wantCursor {
75 t.Fatalf("cursor = %d, want %d", d.cursor, tt.wantCursor)
76 }
77 if got := d.sessions[d.cursor].Path; got != tt.wantPath {
78 t.Fatalf("selected path = %q, want %q", got, tt.wantPath)
79 }
80 })
81 }
82
83 d := &sessionDialog{sessions: []core.SessionSummary{{Path: "only"}}}
84 d.Remove("only")
85 if len(d.sessions) != 0 || d.cursor != 0 {
86 t.Fatalf("empty dialog has len %d and cursor %d, want 0 and 0", len(d.sessions), d.cursor)
87 }
88}
89
90func TestApplySessionDeletionRemovesFileAndRow(t *testing.T) {
91 path := filepath.Join(t.TempDir(), "session.jsonl")
92 if err := os.WriteFile(path, []byte("session"), 0o600); err != nil {
93 t.Fatal(err)
94 }
95 i := &Interactive{
96 cfg: InteractiveConfig{CurrentSessionPath: func() string { return "" }},
97 sessionDialog: &sessionDialog{active: true, sessions: []core.SessionSummary{{Path: path}}},
98 }
99
100 i.applySessionDeletion(path)
101
102 if _, err := os.Stat(path); !os.IsNotExist(err) {
103 t.Fatalf("deleted session still exists: %v", err)
104 }
105 if len(i.sessionDialog.sessions) != 0 {
106 t.Fatal("deleted session remains in picker")
107 }
108 if !strings.Contains(i.statusOK, "deleted session") || i.statusErr != "" {
109 t.Fatalf("statusOK = %q, statusErr = %q", i.statusOK, i.statusErr)
110 }
111}
112
113func TestApplySessionDeletionProtectsActiveSession(t *testing.T) {
114 path := filepath.Join(t.TempDir(), "session.jsonl")
115 if err := os.WriteFile(path, []byte("session"), 0o600); err != nil {
116 t.Fatal(err)
117 }
118 i := &Interactive{
119 cfg: InteractiveConfig{CurrentSessionPath: func() string { return path }},
120 sessionDialog: &sessionDialog{active: true, sessions: []core.SessionSummary{{Path: path}}},
121 }
122
123 i.applySessionDeletion(path)
124
125 if _, err := os.Stat(path); err != nil {
126 t.Fatalf("active session was deleted: %v", err)
127 }
128 if len(i.sessionDialog.sessions) != 1 {
129 t.Fatal("active session was removed from picker")
130 }
131 if !strings.Contains(i.statusErr, "active session") {
132 t.Fatalf("statusErr = %q, want active-session explanation", i.statusErr)
133 }
134}
135
136func TestApplySessionDeletionRequiresCurrentSessionWiring(t *testing.T) {
137 path := filepath.Join(t.TempDir(), "session.jsonl")
138 if err := os.WriteFile(path, []byte("session"), 0o600); err != nil {
139 t.Fatal(err)
140 }
141 i := &Interactive{
142 sessionDialog: &sessionDialog{active: true, sessions: []core.SessionSummary{{Path: path}}},
143 }
144
145 i.applySessionDeletion(path)
146
147 if _, err := os.Stat(path); err != nil {
148 t.Fatalf("session was deleted without active-session wiring: %v", err)
149 }
150 if !strings.Contains(i.statusErr, "not wired") {
151 t.Fatalf("statusErr = %q, want wiring error", i.statusErr)
152 }
153}
154
155func TestApplySessionDeletionPreservesRowOnError(t *testing.T) {
156 path := filepath.Join(t.TempDir(), "missing.jsonl")
157 i := &Interactive{
158 cfg: InteractiveConfig{CurrentSessionPath: func() string { return "" }},
159 sessionDialog: &sessionDialog{active: true, sessions: []core.SessionSummary{{Path: path}}},
160 }
161
162 i.applySessionDeletion(path)
163
164 if len(i.sessionDialog.sessions) != 1 {
165 t.Fatal("failed deletion removed session from picker")
166 }
167 if !strings.Contains(i.statusErr, "delete session") {
168 t.Fatalf("statusErr = %q, want deletion error", i.statusErr)
169 }
170}