scooter  ·  2026-04-13

main_test.go

  1package main
  2
  3import (
  4	"bufio"
  5	"os"
  6	"os/exec"
  7	"path/filepath"
  8	"runtime"
  9	"strings"
 10	"testing"
 11
 12	"github.com/git-bug/git-bug/entities/identity"
 13	"github.com/git-bug/git-bug/repository"
 14)
 15
 16func TestPromptUser(t *testing.T) {
 17	tests := []struct {
 18		name        string
 19		input       string
 20		prompt      string
 21		expected    string
 22		expectError bool
 23	}{
 24		{
 25			name:     "simple input",
 26			input:    "Test User\n",
 27			prompt:   "Enter name: ",
 28			expected: "Test User",
 29		},
 30		{
 31			name:     "input with spaces",
 32			input:    "John Doe\n",
 33			prompt:   "Enter name: ",
 34			expected: "John Doe",
 35		},
 36		{
 37			name:     "empty input",
 38			input:    "\n",
 39			prompt:   "Enter name: ",
 40			expected: "",
 41		},
 42		{
 43			name:     "input with trailing newline",
 44			input:    "[email protected]\n",
 45			prompt:   "Enter email: ",
 46			expected: "[email protected]",
 47		},
 48	}
 49
 50	for _, tt := range tests {
 51		t.Run(tt.name, func(t *testing.T) {
 52			// Create a scanner from the test input string
 53			scanner := bufio.NewScanner(strings.NewReader(tt.input))
 54
 55			// Call promptUserWithScanner with our test scanner
 56			result, err := promptUserWithScanner(tt.prompt, scanner)
 57
 58			if tt.expectError {
 59				if err == nil {
 60					t.Errorf("expected error, got nil")
 61				}
 62				return
 63			}
 64
 65			if err != nil {
 66				t.Errorf("unexpected error: %v", err)
 67				return
 68			}
 69
 70			if result != tt.expected {
 71				t.Errorf("promptUser() = %q, want %q", result, tt.expected)
 72			}
 73		})
 74	}
 75}
 76
 77func TestDetectJJConfig(t *testing.T) {
 78	// Test case 1: No .jj directory
 79	t.Run("no_jj_directory", func(t *testing.T) {
 80		tmpDir := t.TempDir()
 81		name, email, err := detectJJConfig(tmpDir)
 82		if err != nil {
 83			t.Fatalf("unexpected error: %v", err)
 84		}
 85		if name != "" || email != "" {
 86			t.Errorf("expected empty name and email, got name=%q, email=%q", name, email)
 87		}
 88	})
 89
 90	// Test case 2: .jj directory exists (would need actual jj config in practice)
 91	t.Run("jj_directory_exists", func(t *testing.T) {
 92		// This test documents the expected behavior when .jj exists
 93		// In a real scenario, it would try to run jj commands
 94		tmpDir := t.TempDir()
 95		jjDir := filepath.Join(tmpDir, ".jj")
 96		if err := os.MkdirAll(jjDir, 0755); err != nil {
 97			t.Fatalf("failed to create .jj directory: %v", err)
 98		}
 99		// We expect the function to attempt jj config commands
100		// Since jj is not available in test env, we just verify the directory detection
101		_, _, err := detectJJConfig(tmpDir)
102		// We expect an error since jj binary won't be available
103		if err == nil {
104			t.Skip("jj not available in test environment, skipping")
105		}
106	})
107}
108
109func TestCreateIdentity(t *testing.T) {
110	// Create a temporary directory for a test git repo
111	tmpDir := t.TempDir()
112
113	// Initialize a git repo
114	initCmd := exec.Command("git", "init")
115	initCmd.Dir = tmpDir
116	if err := initCmd.Run(); err != nil {
117		t.Fatalf("failed to init git repo: %v", err)
118	}
119
120	// Test creating an identity
121	err := createIdentity(tmpDir, "Test User", "[email protected]", true)
122	if err != nil {
123		t.Fatalf("createIdentity failed: %v", err)
124	}
125
126	// Verify identity was created by checking if we can get it
127	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
128	if err != nil {
129		t.Fatalf("failed to open repo: %v", err)
130	}
131	defer repo.Close()
132
133	isSet, err := identity.IsUserIdentitySet(repo)
134	if err != nil {
135		t.Fatalf("IsUserIdentitySet failed: %v", err)
136	}
137	if !isSet {
138		t.Error("expected user identity to be set")
139	}
140
141	userIdentity, err := identity.GetUserIdentity(repo)
142	if err != nil {
143		t.Fatalf("GetUserIdentity failed: %v", err)
144	}
145
146	if userIdentity.Name() != "Test User" {
147		t.Errorf("expected name 'Test User', got %q", userIdentity.Name())
148	}
149
150	if userIdentity.Email() != "[email protected]" {
151		t.Errorf("expected email '[email protected]', got %q", userIdentity.Email())
152	}
153}
154
155func TestInitCommand_InteractiveMode(t *testing.T) {
156	// Create a temporary directory for a test git repo
157	tmpDir := t.TempDir()
158
159	// Initialize a git repo
160	initCmd := exec.Command("git", "init")
161	initCmd.Dir = tmpDir
162	if err := initCmd.Run(); err != nil {
163		t.Fatalf("failed to init git repo: %v", err)
164	}
165
166	// Add origin remote (required for bug init)
167	remoteCmd := exec.Command("git", "remote", "add", "origin", "[email protected]:test/repo.git")
168	remoteCmd.Dir = tmpDir
169	if err := remoteCmd.Run(); err != nil {
170		t.Fatalf("failed to add origin remote: %v", err)
171	}
172
173	// Test the init function with a mock input (simulating interactive mode)
174	// Since we don't have jj, we'll test the interactive path by mocking stdin
175	input := "Interactive User\[email protected]\n"
176	r, w, _ := os.Pipe()
177	go func() {
178		w.WriteString(input)
179		w.Close()
180	}()
181
182	// We need to test runInitWithReader to inject our stdin
183	err := runInitWithReader(tmpDir, r)
184	if err != nil {
185		t.Fatalf("runInitWithReader failed: %v", err)
186	}
187
188	// Verify repo was opened
189	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
190	if err != nil {
191		t.Fatalf("failed to open repo: %v", err)
192	}
193	defer repo.Close()
194
195	// Check if identities were created
196	ids, err := identity.ListLocalIds(repo)
197	if err != nil {
198		t.Fatalf("ListLocalIds failed: %v", err)
199	}
200
201	// Should have at least 2 identities (user + agent)
202	if len(ids) < 2 {
203		t.Errorf("expected at least 2 identities (user + agent), got %d", len(ids))
204	}
205
206	// Verify the user identity has the expected name and email
207	userIdentity, err := identity.GetUserIdentity(repo)
208	if err != nil {
209		t.Fatalf("GetUserIdentity failed: %v", err)
210	}
211
212	if userIdentity.Name() != "Interactive User" {
213		t.Errorf("expected user name 'Interactive User', got %q", userIdentity.Name())
214	}
215
216	if userIdentity.Email() != "[email protected]" {
217		t.Errorf("expected user email '[email protected]', got %q", userIdentity.Email())
218	}
219
220	// Verify the agent identity exists with empty email
221	// The agent identity should be one of the identities with name "agent" and empty email
222	foundAgent := false
223	for _, id := range ids {
224		i, err := identity.ReadLocal(repo, id)
225		if err != nil {
226			t.Fatalf("failed to read identity %s: %v", id, err)
227		}
228		if i.Name() == "agent" && i.Email() == "" {
229			foundAgent = true
230			break
231		}
232	}
233	if !foundAgent {
234		t.Error("expected to find an agent identity with empty email")
235	}
236}
237
238func TestParseEditorContent(t *testing.T) {
239	tests := []struct {
240		name            string
241		content         string
242		expectedTitle   string
243		expectedMessage string
244		expectError     bool
245	}{
246		{
247			name:            "simple title only",
248			content:         "Bug title\n",
249			expectedTitle:   "Bug title",
250			expectedMessage: "",
251		},
252		{
253			name:            "title with description",
254			content:         "Bug title\n\nThis is a description\nwith multiple lines\n",
255			expectedTitle:   "Bug title",
256			expectedMessage: "\nThis is a description\nwith multiple lines",
257		},
258		{
259			name:            "with comment lines",
260			content:         ";; This is a comment\nBug title\n\nDescription here\n;; Another comment",
261			expectedTitle:   "Bug title",
262			expectedMessage: "Description here",
263		},
264		{
265			name:        "empty content",
266			content:     "",
267			expectError: true,
268		},
269		{
270			name:        "only comments",
271			content:     ";; Comment 1\n;; Comment 2\n",
272			expectError: true,
273		},
274		{
275			name:            "leading empty lines",
276			content:         "\n\n  \nActual title\nDescription",
277			expectedTitle:   "Actual title",
278			expectedMessage: "Description",
279		},
280		{
281			name:            "markdown headers preserved",
282			content:         "Bug title\n\n# Header 1\n## Header 2\nContent here",
283			expectedTitle:   "Bug title",
284			expectedMessage: "\n# Header 1\n## Header 2\nContent here",
285		},
286	}
287
288	for _, tt := range tests {
289		t.Run(tt.name, func(t *testing.T) {
290			title, message, err := parseEditorContent(tt.content)
291
292			if tt.expectError {
293				if err == nil {
294					t.Errorf("expected error, got nil")
295				}
296				return
297			}
298
299			if err != nil {
300				t.Errorf("unexpected error: %v", err)
301				return
302			}
303
304			if title != tt.expectedTitle {
305				t.Errorf("title = %q, want %q", title, tt.expectedTitle)
306			}
307
308			if message != tt.expectedMessage {
309				t.Errorf("message = %q, want %q", message, tt.expectedMessage)
310			}
311		})
312	}
313}
314
315func TestParseCommentContent(t *testing.T) {
316	tests := []struct {
317		name            string
318		content         string
319		expectedMessage string
320		expectError     bool
321	}{
322		{
323			name:            "simple comment",
324			content:         "This is a comment\n",
325			expectedMessage: "This is a comment",
326		},
327		{
328			name:            "multiline comment",
329			content:         "First line\nSecond line\nThird line\n",
330			expectedMessage: "First line\nSecond line\nThird line",
331		},
332		{
333			name:            "with comment lines",
334			content:         ";; This is a comment\nActual content\n;; Another comment",
335			expectedMessage: "Actual content",
336		},
337		{
338			name:        "only comment lines",
339			content:     ";; Comment 1\n;; Comment 2\n",
340			expectError: true,
341		},
342		{
343			name:        "empty content",
344			content:     "",
345			expectError: true,
346		},
347		{
348			name:        "only whitespace",
349			content:     "   \n\n   \n",
350			expectError: true,
351		},
352		{
353			name:            "leading empty lines",
354			content:         "\n\n  \nActual content\nMore content",
355			expectedMessage: "Actual content\nMore content",
356		},
357		{
358			name:            "trailing empty lines",
359			content:         "Actual content\nMore content\n\n  \n",
360			expectedMessage: "Actual content\nMore content",
361		},
362		{
363			name:            "markdown content",
364			content:         "# Header\n\n- List item 1\n- List item 2\n\n**Bold text**",
365			expectedMessage: "# Header\n\n- List item 1\n- List item 2\n\n**Bold text**",
366		},
367		{
368			name:            "mixed comments and content",
369			content:         ";; Instructions here\n;; More instructions\n\nActual comment\n;; Hidden comment\nMore content\n;; End comment",
370			expectedMessage: "Actual comment\nMore content",
371		},
372	}
373
374	for _, tt := range tests {
375		t.Run(tt.name, func(t *testing.T) {
376			message, err := parseCommentContent(tt.content)
377
378			if tt.expectError {
379				if err == nil {
380					t.Errorf("expected error, got nil")
381				}
382				return
383			}
384
385			if err != nil {
386				t.Errorf("unexpected error: %v", err)
387				return
388			}
389
390			if message != tt.expectedMessage {
391				t.Errorf("message = %q, want %q", message, tt.expectedMessage)
392			}
393		})
394	}
395}
396
397func TestGetEditor(t *testing.T) {
398	// Save original EDITOR value
399	originalEditor := os.Getenv("EDITOR")
400	defer os.Setenv("EDITOR", originalEditor)
401
402	tests := []struct {
403		name      string
404		editorEnv string
405		check     func(t *testing.T, got string)
406	}{
407		{
408			name:      "with EDITOR set",
409			editorEnv: "vim",
410			check: func(t *testing.T, got string) {
411				if !strings.Contains(got, "vim") {
412					t.Errorf("getEditor() = %q, want to contain 'vim'", got)
413				}
414			},
415		},
416		{
417			name:      "with EDITOR set to path",
418			editorEnv: "/usr/bin/nano",
419			check: func(t *testing.T, got string) {
420				if !strings.Contains(got, "nano") {
421					t.Errorf("getEditor() = %q, want to contain 'nano'", got)
422				}
423			},
424		},
425		{
426			name:      "with EDITOR empty uses default",
427			editorEnv: "",
428			check: func(t *testing.T, got string) {
429				if got == "" {
430					t.Error("expected default editor, got empty string")
431				}
432				// Check it returns expected default based on platform
433				if runtime.GOOS == "windows" {
434					if got != "notepad" {
435						t.Errorf("expected 'notepad' on Windows, got %q", got)
436					}
437				} else {
438					if got != "vi" {
439						t.Errorf("expected 'vi' on Unix, got %q", got)
440					}
441				}
442			},
443		},
444	}
445
446	for _, tt := range tests {
447		t.Run(tt.name, func(t *testing.T) {
448			os.Setenv("EDITOR", tt.editorEnv)
449			got := getEditor()
450			tt.check(t, got)
451		})
452	}
453}