package command import ( "os" "path/filepath" "strings" "testing" "git.gophernest.net/azpect/TextEditor/internal/action" "git.gophernest.net/azpect/TextEditor/internal/core" tea "github.com/charmbracelet/bubbletea" ) // ================================================== // Mock Model Implementation // ================================================== type mockModel struct { windows []*core.Window activeWindow *core.Window buffers []*core.Buffer settings core.EditorSettings mode core.Mode registers map[rune]core.Register insertKeys []string command string commandCursor int commandOutput *core.CommandOutput lastFind core.LastFindCommand } func newMockModel() *mockModel { buf := core.NewBufferBuilder(). WithLines([]string{""}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithHeight(24). WithWidth(80). Build() return &mockModel{ windows: []*core.Window{&win}, activeWindow: &win, buffers: []*core.Buffer{&buf}, settings: core.NewDefaultSettings(), mode: core.NormalMode, registers: core.DefaultRegisters(), } } func newMockModelWithBuffer(buf *core.Buffer) *mockModel { win := core.NewWindowBuilder(). WithBuffer(buf). WithHeight(24). WithWidth(80). Build() return &mockModel{ windows: []*core.Window{&win}, activeWindow: &win, buffers: []*core.Buffer{buf}, settings: core.NewDefaultSettings(), mode: core.NormalMode, registers: core.DefaultRegisters(), } } // Core Data Access func (m *mockModel) Windows() []*core.Window { return m.windows } func (m *mockModel) ActiveWindow() *core.Window { return m.activeWindow } func (m *mockModel) Buffers() []*core.Buffer { return m.buffers } func (m *mockModel) SetBuffers(bufs []*core.Buffer) { m.buffers = bufs } func (m *mockModel) ActiveBuffer() *core.Buffer { return m.activeWindow.Buffer } // Insert Mode State func (m *mockModel) InsertKeys() []string { return m.insertKeys } func (m *mockModel) SetInsertKeys(keys []string) { m.insertKeys = keys } func (m *mockModel) SetInsertRecording(count int, action action.Action) {} func (m *mockModel) ExitInsertMode() {} func (m *mockModel) SetLastFind(char string, forward, inclusive bool) { m.lastFind = core.LastFindCommand{Char: char, Forward: forward, Inclusive: inclusive} } func (m *mockModel) GetLastFind() *core.LastFindCommand { return &m.lastFind } // Command Mode State func (m *mockModel) Command() string { return m.command } func (m *mockModel) SetCommand(cmd string) { m.command = cmd } func (m *mockModel) CommandCursor() int { return m.commandCursor } func (m *mockModel) SetCommandCursor(cur int) { m.commandCursor = cur } func (m *mockModel) CommandOutput() *core.CommandOutput { return m.commandOutput } func (m *mockModel) SetCommandOutput(out *core.CommandOutput) { m.commandOutput = out } // Editor-wide State func (m *mockModel) Mode() core.Mode { return m.mode } func (m *mockModel) SetMode(mode core.Mode) { m.mode = mode } func (m *mockModel) Settings() core.EditorSettings { return m.settings } func (m *mockModel) SetSettings(s core.EditorSettings) { m.settings = s } // Registers func (m *mockModel) Registers() map[rune]core.Register { return m.registers } func (m *mockModel) GetRegister(name rune) (core.Register, bool) { reg, ok := m.registers[name] return reg, ok } func (m *mockModel) SetRegister(name rune, t core.RegisterType, cnt []string) error { m.registers[name] = core.Register{Type: t, Content: cnt} return nil } func (m *mockModel) UpdateDefaultRegister(t core.RegisterType, cnt []string) { m.registers['"'] = core.Register{Type: t, Content: cnt} } // ================================================== // cmdWrite Tests // ================================================== func TestCmdWrite(t *testing.T) { t.Run("writes buffer content to file", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "test.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines([]string{"line 1", "line 2", "line 3"}). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } content, err := os.ReadFile(filename) if err != nil { t.Fatalf("file not written: %v", err) } expected := "line 1\nline 2\nline 3\n" if string(content) != expected { t.Errorf("got %q, want %q", string(content), expected) } }) t.Run("writes to argument filename instead of buffer filename", func(t *testing.T) { tmpDir := t.TempDir() originalFile := filepath.Join(tmpDir, "original.txt") newFile := filepath.Join(tmpDir, "new.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(originalFile). WithLines([]string{"content"}). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{newFile}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Verify new file was created if _, err := os.Stat(newFile); os.IsNotExist(err) { t.Error("new file not created") } // Verify original file was NOT created if _, err := os.Stat(originalFile); !os.IsNotExist(err) { t.Error("original file should not exist") } content, _ := os.ReadFile(newFile) expected := "content\n" if string(content) != expected { t.Errorf("got %q, want %q", string(content), expected) } }) t.Run("truncates existing file when overwriting", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "test.txt") // Write longer content first originalContent := "this is very long original content\nwith multiple lines\nand more text\n" os.WriteFile(filename, []byte(originalContent), 0644) // Create buffer with shorter content buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines([]string{"short"}). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } content, _ := os.ReadFile(filename) expected := "short\n" if string(content) != expected { t.Errorf("file not truncated properly: got %q, want %q", string(content), expected) } }) t.Run("refuses to write readonly buffer", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "test.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines([]string{"content"}). ReadOnly(). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for readonly buffer") } if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "readonly") { t.Errorf("error should mention readonly: %v", m.commandOutput.Lines) } // Verify file was NOT created if _, err := os.Stat(filename); !os.IsNotExist(err) { t.Error("file should not be created for readonly buffer") } }) t.Run("refuses to write scratch buffer", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "test.txt") buf := core.NewBufferBuilder(). WithType(core.ScatchBuffer). WithFilename(filename). WithLines([]string{"content"}). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for scratch buffer") } if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "ScratchBuffer") { t.Errorf("error should mention ScratchBuffer: %v", m.commandOutput.Lines) } }) t.Run("errors when no filename and buffer has no filename", func(t *testing.T) { buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(""). WithLines([]string{"content"}). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error when no filename available") } if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "no file name") { t.Errorf("error should mention no file name: %v", m.commandOutput.Lines) } }) t.Run("errors on invalid path", func(t *testing.T) { buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename("/invalid/nonexistent/path/file.txt"). WithLines([]string{"content"}). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for invalid path") } }) t.Run("writes empty buffer", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "empty.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines([]string{}). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } content, err := os.ReadFile(filename) if err != nil { t.Fatalf("file not created: %v", err) } // Empty buffer should produce empty file if string(content) != "" { t.Errorf("expected empty file, got %q", string(content)) } }) t.Run("writes buffer with empty lines", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "test.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines([]string{"line 1", "", "line 3", ""}). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } content, _ := os.ReadFile(filename) expected := "line 1\n\nline 3\n\n" if string(content) != expected { t.Errorf("got %q, want %q", string(content), expected) } }) t.Run("writes buffer with special characters", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "special.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines([]string{ "hello\tworld", "unicode: \u00e9\u00e8\u00ea", "symbols: !@#$%^&*()", }). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } content, _ := os.ReadFile(filename) expected := "hello\tworld\nunicode: \u00e9\u00e8\u00ea\nsymbols: !@#$%^&*()\n" if string(content) != expected { t.Errorf("got %q, want %q", string(content), expected) } }) t.Run("writes buffer with unicode content", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "unicode.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines([]string{ "Japanese: \u3053\u3093\u306b\u3061\u306f", "Chinese: \u4f60\u597d", "Emoji: \U0001F600\U0001F389\U0001F680", "Russian: \u041f\u0440\u0438\u0432\u0435\u0442", }). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } content, _ := os.ReadFile(filename) lines := strings.Split(strings.TrimSuffix(string(content), "\n"), "\n") if len(lines) != 4 { t.Errorf("expected 4 lines, got %d", len(lines)) } if !strings.Contains(lines[0], "\u3053\u3093\u306b\u3061\u306f") { t.Errorf("Japanese content not preserved: %q", lines[0]) } }) t.Run("sets correct output message", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "test.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines([]string{"line 1", "line 2"}). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{}, false) if m.commandOutput == nil || len(m.commandOutput.Lines) == 0 { t.Error("expected output message") } // Should contain filename and line count if !strings.Contains(m.commandOutput.Lines[0], filename) { t.Errorf("output should contain filename: %q", m.commandOutput.Lines[0]) } if !strings.Contains(m.commandOutput.Lines[0], "2L") { t.Errorf("output should contain line count: %q", m.commandOutput.Lines[0]) } }) t.Run("writes large file", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "large.txt") // Create buffer with many lines lines := make([]string, 10000) for i := range lines { lines[i] = strings.Repeat("x", 100) } buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines(lines). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } info, err := os.Stat(filename) if err != nil { t.Fatalf("file not created: %v", err) } // Each line is 100 chars + newline = 101 bytes * 10000 lines expectedSize := int64(101 * 10000) if info.Size() != expectedSize { t.Errorf("expected size %d, got %d", expectedSize, info.Size()) } }) t.Run("writes to file with spaces in path", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "file with spaces.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines([]string{"content"}). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if _, err := os.Stat(filename); os.IsNotExist(err) { t.Error("file with spaces not created") } }) t.Run("creates parent directories via argument", func(t *testing.T) { tmpDir := t.TempDir() // Note: cmdWrite doesn't create parent dirs, so this should fail filename := filepath.Join(tmpDir, "nonexistent", "nested", "file.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename("original.txt"). WithLines([]string{"content"}). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{filename}, false) // Current implementation doesn't create parent dirs, so expect error if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error when parent directories don't exist") } }) t.Run("clears modified flag after successful write", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "test.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines([]string{"content"}). Modified(). Build() m := newMockModelWithBuffer(&buf) if !m.ActiveBuffer().Modified { t.Fatal("precondition: buffer should be modified before write") } cmdWrite(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if m.ActiveBuffer().Modified { t.Error("buffer should not be modified after successful write") } }) t.Run("clears modified flag even when writing to different file", func(t *testing.T) { tmpDir := t.TempDir() originalFile := filepath.Join(tmpDir, "original.txt") newFile := filepath.Join(tmpDir, "copy.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(originalFile). WithLines([]string{"content"}). Modified(). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{newFile}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Modified flag is cleared even when writing to a different file if m.ActiveBuffer().Modified { t.Error("buffer modified flag should be cleared after write") } // Buffer filename should NOT change (vim behavior) if m.ActiveBuffer().Filename != originalFile { t.Errorf("buffer filename should remain %q, got %q", originalFile, m.ActiveBuffer().Filename) } }) t.Run("does not clear modified flag on write error", func(t *testing.T) { buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename("/invalid/nonexistent/path/file.txt"). WithLines([]string{"content"}). Modified(). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Fatal("expected error for invalid path") } // Modified flag should remain true on error if !m.ActiveBuffer().Modified { t.Error("buffer should still be modified after failed write") } }) } // ================================================== // cmdEdit Tests // ================================================== func TestCmdEdit(t *testing.T) { t.Run("errors when no argument provided", func(t *testing.T) { m := newMockModel() cmdEdit(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error when no argument") } if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "requires an argument") { t.Errorf("error should mention requires argument: %v", m.commandOutput.Lines) } }) t.Run("creates new buffer for nonexistent file", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "newfile.txt") m := newMockModel() initialBufferCount := len(m.buffers) cmdEdit(m, []string{filename}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Should have added a new buffer if len(m.buffers) != initialBufferCount+1 { t.Errorf("expected %d buffers, got %d", initialBufferCount+1, len(m.buffers)) } // Active window should point to new buffer if m.activeWindow.Buffer.Filename != filename { t.Errorf("active buffer filename = %q, want %q", m.activeWindow.Buffer.Filename, filename) } // New buffer should be empty if m.activeWindow.Buffer.LineCount() != 1 || m.activeWindow.Buffer.Line(0) != "" { t.Errorf("new buffer should have one empty line") } // Buffer should be FileBuffer type if m.activeWindow.Buffer.Type != core.FileBuffer { t.Errorf("buffer type = %v, want FileBuffer", m.activeWindow.Buffer.Type) } }) t.Run("loads existing file content", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "existing.txt") // Create file with content content := "line 1\nline 2\nline 3\n" os.WriteFile(filename, []byte(content), 0644) m := newMockModel() cmdEdit(m, []string{filename}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } buf := m.activeWindow.Buffer if buf.LineCount() != 3 { t.Errorf("expected 3 lines, got %d", buf.LineCount()) } if buf.Line(0) != "line 1" { t.Errorf("line 0 = %q, want %q", buf.Line(0), "line 1") } if buf.Line(1) != "line 2" { t.Errorf("line 1 = %q, want %q", buf.Line(1), "line 2") } if buf.Line(2) != "line 3" { t.Errorf("line 2 = %q, want %q", buf.Line(2), "line 3") } }) t.Run("loads file without trailing newline", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "notrailing.txt") // File without trailing newline content := "line 1\nline 2" os.WriteFile(filename, []byte(content), 0644) m := newMockModel() cmdEdit(m, []string{filename}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } buf := m.activeWindow.Buffer if buf.LineCount() != 2 { t.Errorf("expected 2 lines, got %d", buf.LineCount()) } }) t.Run("loads empty file", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "empty.txt") os.WriteFile(filename, []byte(""), 0644) m := newMockModel() cmdEdit(m, []string{filename}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } buf := m.activeWindow.Buffer // Empty file should have 0 lines (scanner produces no lines) if buf.LineCount() != 0 { t.Errorf("expected 0 lines for empty file, got %d", buf.LineCount()) } }) t.Run("sets correct filetype from extension", func(t *testing.T) { tmpDir := t.TempDir() tests := []struct { filename string expected string }{ {"test.go", ".go"}, {"test.py", ".py"}, {"test.js", ".js"}, {"test.txt", ".txt"}, {"Makefile", ""}, {"test.tar.gz", ".gz"}, } for _, tt := range tests { t.Run(tt.filename, func(t *testing.T) { filename := filepath.Join(tmpDir, tt.filename) os.WriteFile(filename, []byte("content"), 0644) m := newMockModel() cmdEdit(m, []string{filename}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if m.activeWindow.Buffer.Filetype != tt.expected { t.Errorf("filetype = %q, want %q", m.activeWindow.Buffer.Filetype, tt.expected) } }) } }) t.Run("converts tabs to spaces based on tabstop", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "tabs.txt") // File with tabs content := "\tindented\n\t\tdouble" os.WriteFile(filename, []byte(content), 0644) m := newMockModel() m.settings.TabStop = 4 cmdEdit(m, []string{filename}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } buf := m.activeWindow.Buffer // Tab should be converted to 4 spaces expected0 := " indented" if buf.Line(0) != expected0 { t.Errorf("line 0 = %q, want %q", buf.Line(0), expected0) } expected1 := " double" if buf.Line(1) != expected1 { t.Errorf("line 1 = %q, want %q", buf.Line(1), expected1) } }) t.Run("handles different tabstop values", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "tabs.txt") content := "\ttest" os.WriteFile(filename, []byte(content), 0644) tests := []struct { tabstop int expected string }{ {2, " test"}, {4, " test"}, {8, " test"}, } for _, tt := range tests { t.Run(string(rune('0'+tt.tabstop)), func(t *testing.T) { m := newMockModel() m.settings.TabStop = tt.tabstop cmdEdit(m, []string{filename}, false) if m.activeWindow.Buffer.Line(0) != tt.expected { t.Errorf("with tabstop=%d: got %q, want %q", tt.tabstop, m.activeWindow.Buffer.Line(0), tt.expected) } }) } }) t.Run("loads file with unicode content", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "unicode.txt") content := "Hello \u4e16\u754c\n\u3053\u3093\u306b\u3061\u306f\nEmoji: \U0001F600\n" os.WriteFile(filename, []byte(content), 0644) m := newMockModel() cmdEdit(m, []string{filename}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } buf := m.activeWindow.Buffer if buf.LineCount() != 3 { t.Errorf("expected 3 lines, got %d", buf.LineCount()) } if !strings.Contains(buf.Line(0), "\u4e16\u754c") { t.Errorf("unicode not preserved in line 0: %q", buf.Line(0)) } }) t.Run("loads large file", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "large.txt") // Create large file var content strings.Builder for range 10000 { content.WriteString(strings.Repeat("x", 100)) content.WriteString("\n") } os.WriteFile(filename, []byte(content.String()), 0644) m := newMockModel() cmdEdit(m, []string{filename}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } buf := m.activeWindow.Buffer if buf.LineCount() != 10000 { t.Errorf("expected 10000 lines, got %d", buf.LineCount()) } }) t.Run("sets buffer as loaded and listed", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "test.txt") os.WriteFile(filename, []byte("content"), 0644) m := newMockModel() cmdEdit(m, []string{filename}, false) buf := m.activeWindow.Buffer if !buf.Loaded { t.Error("buffer should be marked as loaded") } if !buf.Listed { t.Error("buffer should be marked as listed") } }) t.Run("new file buffer is loaded and listed", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "newfile.txt") m := newMockModel() cmdEdit(m, []string{filename}, false) buf := m.activeWindow.Buffer if !buf.Loaded { t.Error("new buffer should be marked as loaded") } if !buf.Listed { t.Error("new buffer should be marked as listed") } }) t.Run("errors on permission denied", func(t *testing.T) { if os.Getuid() == 0 { t.Skip("test requires non-root user") } tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "noperm.txt") // Create file then remove read permission os.WriteFile(filename, []byte("content"), 0644) os.Chmod(filename, 0000) defer os.Chmod(filename, 0644) // Cleanup m := newMockModel() cmdEdit(m, []string{filename}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for permission denied") } }) t.Run("handles file with long lines", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "longlines.txt") // Use 50000 chars - within bufio.Scanner's default 64KB limit longLine := strings.Repeat("x", 50000) content := longLine + "\nshort\n" + longLine os.WriteFile(filename, []byte(content), 0644) m := newMockModel() cmdEdit(m, []string{filename}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } buf := m.activeWindow.Buffer if len(buf.Line(0)) != 50000 { t.Errorf("long line not preserved: got %d chars", len(buf.Line(0))) } }) t.Run("bufio.Scanner limitation with extremely long lines", func(t *testing.T) { // NOTE: This documents a known limitation // bufio.Scanner has a default max token size of ~64KB // Lines longer than this will cause scanner.Scan() to return false // with scanner.Err() returning bufio.ErrTooLong // // A future improvement would be to use bufio.Reader.ReadString // or increase scanner buffer size with scanner.Buffer() tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "verylonglines.txt") // Create line exceeding 64KB limit veryLongLine := strings.Repeat("x", 100000) os.WriteFile(filename, []byte(veryLongLine+"\n"), 0644) m := newMockModel() cmdEdit(m, []string{filename}, false) // Currently this will result in 0 lines loaded due to scanner limitation // This test documents the current behavior buf := m.activeWindow.Buffer if buf.LineCount() > 0 && len(buf.Line(0)) == 100000 { t.Log("Scanner handled very long line - limitation may be fixed") } else { t.Log("Known limitation: bufio.Scanner cannot handle lines > 64KB") } }) t.Run("strips carriage return from CRLF line endings", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "crlf.txt") // Windows-style line endings content := "line 1\r\nline 2\r\nline 3\r\n" os.WriteFile(filename, []byte(content), 0644) m := newMockModel() cmdEdit(m, []string{filename}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } buf := m.activeWindow.Buffer if buf.LineCount() != 3 { t.Errorf("expected 3 lines, got %d", buf.LineCount()) } // Verify \r is stripped from all lines for i := 0; i < buf.LineCount(); i++ { line := buf.Line(i) if strings.HasSuffix(line, "\r") { t.Errorf("line %d has trailing \\r: %q", i, line) } if strings.Contains(line, "\r") { t.Errorf("line %d contains \\r: %q", i, line) } } // Verify content is correct if buf.Line(0) != "line 1" { t.Errorf("line 0 = %q, want %q", buf.Line(0), "line 1") } if buf.Line(1) != "line 2" { t.Errorf("line 1 = %q, want %q", buf.Line(1), "line 2") } }) t.Run("handles mixed line endings", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "mixed.txt") // Mix of Unix (\n) and Windows (\r\n) line endings content := "unix line\nwindows line\r\nunix again\n" os.WriteFile(filename, []byte(content), 0644) m := newMockModel() cmdEdit(m, []string{filename}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } buf := m.activeWindow.Buffer if buf.LineCount() != 3 { t.Errorf("expected 3 lines, got %d", buf.LineCount()) } // All lines should be clean (no \r) for i := 0; i < buf.LineCount(); i++ { if strings.Contains(buf.Line(i), "\r") { t.Errorf("line %d contains \\r: %q", i, buf.Line(i)) } } }) t.Run("does not panic when editing nonexistent file", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "does_not_exist.txt") m := newMockModel() // This should NOT panic (tests the nil pointer fix) cmdEdit(m, []string{filename}, false) // Should succeed - creates new buffer if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Buffer should be created with the filename if m.activeWindow.Buffer.Filename != filename { t.Errorf("buffer filename = %q, want %q", m.activeWindow.Buffer.Filename, filename) } }) t.Run("adds buffer to buffer list", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "test.txt") os.WriteFile(filename, []byte("content"), 0644) m := newMockModel() initialCount := len(m.buffers) cmdEdit(m, []string{filename}, false) if len(m.buffers) != initialCount+1 { t.Errorf("buffer not added: expected %d, got %d", initialCount+1, len(m.buffers)) } // Find the new buffer in the list found := false for _, buf := range m.buffers { if buf.Filename == filename { found = true break } } if !found { t.Error("new buffer not found in buffer list") } }) t.Run("handles path with spaces", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "file with spaces.txt") os.WriteFile(filename, []byte("content"), 0644) m := newMockModel() cmdEdit(m, []string{filename}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if m.activeWindow.Buffer.Filename != filename { t.Errorf("filename = %q, want %q", m.activeWindow.Buffer.Filename, filename) } }) t.Run("switches to existing buffer instead of creating duplicate", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "existing.txt") os.WriteFile(filename, []byte("original content"), 0644) m := newMockModel() initialBufferCount := len(m.buffers) // First edit - loads the file cmdEdit(m, []string{filename}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error on first edit: %v", m.commandOutput.Lines) } if len(m.buffers) != initialBufferCount+1 { t.Errorf("expected %d buffers after first edit, got %d", initialBufferCount+1, len(m.buffers)) } firstBuffer := m.activeWindow.Buffer if firstBuffer.Filename != filename { t.Fatalf("first buffer filename = %q, want %q", firstBuffer.Filename, filename) } // Modify the buffer to verify we get the same instance firstBuffer.SetLine(0, "modified content") bufferId := firstBuffer.Id // Second edit - should switch to existing buffer, not create new one cmdEdit(m, []string{filename}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error on second edit: %v", m.commandOutput.Lines) } // Should not have created a new buffer if len(m.buffers) != initialBufferCount+1 { t.Errorf("expected buffer count to remain %d, got %d (created duplicate buffer)", initialBufferCount+1, len(m.buffers)) } // Should be the same buffer instance if m.activeWindow.Buffer.Id != bufferId { t.Errorf("expected to switch to existing buffer (ID %d), got different buffer (ID %d)", bufferId, m.activeWindow.Buffer.Id) } // Should have our modifications, not reload from disk if m.activeWindow.Buffer.Line(0) != "modified content" { t.Errorf("expected modified buffer content %q, got %q (buffer was reloaded from disk)", "modified content", m.activeWindow.Buffer.Line(0)) } }) t.Run("switches to existing buffer even if file was modified on disk", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "file.txt") os.WriteFile(filename, []byte("version 1"), 0644) m := newMockModel() // First edit cmdEdit(m, []string{filename}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } bufferId := m.activeWindow.Buffer.Id originalLine := m.activeWindow.Buffer.Line(0) // Modify file on disk os.WriteFile(filename, []byte("version 2 - changed on disk"), 0644) // Second edit - should switch to buffer, not reload from disk cmdEdit(m, []string{filename}, false) // Should still be the same buffer if m.activeWindow.Buffer.Id != bufferId { t.Errorf("expected same buffer ID %d, got %d", bufferId, m.activeWindow.Buffer.Id) } // Should have old content, not reload if m.activeWindow.Buffer.Line(0) != originalLine { t.Errorf("buffer was reloaded from disk, expected %q, got %q", originalLine, m.activeWindow.Buffer.Line(0)) } // Note: Real vim would warn about file change, but for now we just switch to buffer }) t.Run("creates new buffer for different file even if similar name", func(t *testing.T) { tmpDir := t.TempDir() file1 := filepath.Join(tmpDir, "test.txt") file2 := filepath.Join(tmpDir, "test2.txt") os.WriteFile(file1, []byte("file 1"), 0644) os.WriteFile(file2, []byte("file 2"), 0644) m := newMockModel() cmdEdit(m, []string{file1}, false) bufferCount := len(m.buffers) cmdEdit(m, []string{file2}, false) // Should have created a new buffer for different file if len(m.buffers) != bufferCount+1 { t.Errorf("expected new buffer for different file, buffer count = %d", len(m.buffers)) } if m.activeWindow.Buffer.Filename != file2 { t.Errorf("active buffer filename = %q, want %q", m.activeWindow.Buffer.Filename, file2) } }) t.Run("matches buffer by absolute path", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "file.txt") os.WriteFile(filename, []byte("content"), 0644) m := newMockModel() // First edit with absolute path cmdEdit(m, []string{filename}, false) bufferId := m.activeWindow.Buffer.Id bufferCount := len(m.buffers) // Second edit with same absolute path - should match cmdEdit(m, []string{filename}, false) if len(m.buffers) != bufferCount { t.Error("created duplicate buffer for same absolute path") } if m.activeWindow.Buffer.Id != bufferId { t.Error("did not switch to existing buffer") } }) } // ================================================== // Round-trip Tests (Edit then Write) // ================================================== func TestEditWriteRoundTrip(t *testing.T) { t.Run("edit then write preserves content", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "roundtrip.txt") original := "line 1\nline 2\nline 3\n" os.WriteFile(filename, []byte(original), 0644) m := newMockModel() // Edit cmdEdit(m, []string{filename}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("edit error: %v", m.commandOutput.Lines) } // Write cmdWrite(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("write error: %v", m.commandOutput.Lines) } // Read back content, _ := os.ReadFile(filename) if string(content) != original { t.Errorf("content not preserved:\ngot: %q\nwant: %q", string(content), original) } }) t.Run("edit new file then write creates file", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "newfile.txt") m := newMockModel() // Edit (creates new buffer) cmdEdit(m, []string{filename}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("edit error: %v", m.commandOutput.Lines) } // Add content to buffer m.activeWindow.Buffer.InsertLine(0, "hello world") // Write cmdWrite(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("write error: %v", m.commandOutput.Lines) } // Verify file exists content, err := os.ReadFile(filename) if err != nil { t.Fatalf("file not created: %v", err) } expected := "hello world\n\n" // Original empty line + inserted line if string(content) != expected { t.Errorf("got %q, want %q", string(content), expected) } }) t.Run("edit modifies write to different file", func(t *testing.T) { tmpDir := t.TempDir() original := filepath.Join(tmpDir, "original.txt") newFile := filepath.Join(tmpDir, "copy.txt") os.WriteFile(original, []byte("content\n"), 0644) m := newMockModel() // Edit original cmdEdit(m, []string{original}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("edit error: %v", m.commandOutput.Lines) } // Modify m.activeWindow.Buffer.SetLine(0, "modified content") // Write to new file cmdWrite(m, []string{newFile}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("write error: %v", m.commandOutput.Lines) } // Verify new file has modified content content, _ := os.ReadFile(newFile) if string(content) != "modified content\n" { t.Errorf("new file content = %q", string(content)) } // Verify original is unchanged origContent, _ := os.ReadFile(original) if string(origContent) != "content\n" { t.Errorf("original was modified: %q", string(origContent)) } }) } // ================================================== // cmdQuit Tests // ================================================== // // Expected Vim behavior for quit commands with readonly buffers: // // :quit / :q // - Quits if no buffers are modified (readonly or not) // - Errors if any buffer is modified (readonly or not) // // :quitall / :qa // - Same as :quit but for all windows // // :write / :w // - Errors if buffer is readonly (even if not modified) // // :wall / :wa // - Only writes MODIFIED buffers // - Skips unmodified buffers (including readonly ones) // - Errors if any MODIFIED buffer is readonly // // :wq // - Writes current buffer and quits // - Errors if buffer is readonly AND modified // - If buffer is NOT modified, just quits (no write needed) // // :wqall / :wqa // - Writes all MODIFIED buffers and quits // - Skips unmodified buffers (including readonly ones) // - Errors if any MODIFIED buffer is readonly // // Force variants (with !) override readonly protection // ================================================== func TestCmdQuit(t *testing.T) { t.Run("quits when no buffers are modified", func(t *testing.T) { buf := core.NewBufferBuilder(). WithFilename("test.txt"). WithLines([]string{"content"}). Build() m := newMockModelWithBuffer(&buf) cmd := cmdQuit(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { t.Fatal("expected quit command, got nil") } // Execute the command and verify it returns quit message msg := cmd() if _, ok := msg.(tea.QuitMsg); !ok { t.Errorf("expected tea.QuitMsg, got %T", msg) } }) t.Run("errors when buffer is modified", func(t *testing.T) { buf := core.NewBufferBuilder(). WithFilename("unsaved.txt"). WithLines([]string{"unsaved content"}). Modified(). Build() m := newMockModelWithBuffer(&buf) cmd := cmdQuit(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for modified buffer") } if cmd != nil { t.Error("should not return quit command when buffer is modified") } }) t.Run("error message includes filename of modified buffer", func(t *testing.T) { buf := core.NewBufferBuilder(). WithFilename("important_file.txt"). WithLines([]string{"content"}). Modified(). Build() m := newMockModelWithBuffer(&buf) cmdQuit(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Fatal("expected error") } if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "important_file.txt") { t.Errorf("error should mention filename: %v", m.commandOutput.Lines) } }) t.Run("errors when any buffer in list is modified", func(t *testing.T) { // Active buffer is not modified activeBuf := core.NewBufferBuilder(). WithFilename("active.txt"). WithLines([]string{"content"}). Build() activeBuf.Modified = false // Another buffer IS modified modifiedBuf := core.NewBufferBuilder(). WithFilename("modified.txt"). WithLines([]string{"unsaved"}). Build() modifiedBuf.Modified = true m := newMockModelWithBuffer(&activeBuf) m.buffers = append(m.buffers, &modifiedBuf) cmd := cmdQuit(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error when any buffer is modified") } if cmd != nil { t.Error("should not return quit command") } }) t.Run("switches to modified buffer when quitting fails", func(t *testing.T) { activeBuf := core.NewBufferBuilder(). WithFilename("active.txt"). WithLines([]string{"content"}). Build() activeBuf.Modified = false modifiedBuf := core.NewBufferBuilder(). WithFilename("modified.txt"). WithLines([]string{"unsaved"}). Build() modifiedBuf.Modified = true m := newMockModelWithBuffer(&activeBuf) m.buffers = append(m.buffers, &modifiedBuf) cmdQuit(m, []string{}, false) // Should switch active window to show the modified buffer if m.activeWindow.Buffer.Filename != "modified.txt" { t.Errorf("should switch to modified buffer, got %q", m.activeWindow.Buffer.Filename) } }) t.Run("handles buffer with no filename", func(t *testing.T) { buf := core.NewBufferBuilder(). WithFilename(""). WithLines([]string{"unsaved content"}). Modified(). Build() m := newMockModelWithBuffer(&buf) cmdQuit(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for modified buffer without filename") } // Error message should still be meaningful if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "unsaved") { t.Logf("Note: error message for unnamed buffer: %v", m.commandOutput.Lines) } }) t.Run("quits with multiple unmodified buffers", func(t *testing.T) { buf1 := core.NewBufferBuilder(). WithFilename("file1.txt"). WithLines([]string{"content"}). Build() buf1.Modified = false buf2 := core.NewBufferBuilder(). WithFilename("file2.txt"). WithLines([]string{"content"}). Build() buf2.Modified = false buf3 := core.NewBufferBuilder(). WithFilename("file3.txt"). WithLines([]string{"content"}). Build() buf3.Modified = false m := newMockModelWithBuffer(&buf1) m.buffers = append(m.buffers, &buf2, &buf3) cmd := cmdQuit(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { t.Error("expected quit command") } }) } // ================================================== // cmdQuitAll Tests // ================================================== func TestCmdQuitAll(t *testing.T) { t.Run("quits when no buffers are modified", func(t *testing.T) { buf := core.NewBufferBuilder(). WithFilename("test.txt"). WithLines([]string{"content"}). Build() m := newMockModelWithBuffer(&buf) cmd := cmdQuitAll(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { t.Fatal("expected quit command, got nil") } msg := cmd() if _, ok := msg.(tea.QuitMsg); !ok { t.Errorf("expected tea.QuitMsg, got %T", msg) } }) t.Run("errors when any buffer is modified", func(t *testing.T) { buf1 := core.NewBufferBuilder(). WithFilename("saved.txt"). WithLines([]string{"content"}). Build() buf1.Modified = false buf2 := core.NewBufferBuilder(). WithFilename("unsaved.txt"). WithLines([]string{"modified content"}). Build() buf2.Modified = true m := newMockModelWithBuffer(&buf1) m.buffers = append(m.buffers, &buf2) cmd := cmdQuitAll(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error when any buffer is modified") } if cmd != nil { t.Error("should not return quit command") } }) t.Run("quits with multiple unmodified buffers", func(t *testing.T) { buf1 := core.NewBufferBuilder(). WithFilename("file1.txt"). Build() buf1.Modified = false buf2 := core.NewBufferBuilder(). WithFilename("file2.txt"). Build() buf2.Modified = false m := newMockModelWithBuffer(&buf1) m.buffers = append(m.buffers, &buf2) cmd := cmdQuitAll(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { t.Error("expected quit command") } }) t.Run("error mentions which buffer is modified", func(t *testing.T) { buf := core.NewBufferBuilder(). WithFilename("important_document.txt"). WithLines([]string{"content"}). Modified(). Build() m := newMockModelWithBuffer(&buf) cmdQuitAll(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Fatal("expected error") } if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "important_document.txt") { t.Errorf("error should mention filename: %v", m.commandOutput.Lines) } }) t.Run("reports first modified buffer found", func(t *testing.T) { buf1 := core.NewBufferBuilder(). WithFilename("first_modified.txt"). Build() buf1.Modified = true buf2 := core.NewBufferBuilder(). WithFilename("second_modified.txt"). Build() buf2.Modified = true m := newMockModelWithBuffer(&buf1) m.buffers = append(m.buffers, &buf2) cmdQuitAll(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Fatal("expected error") } // Should report first modified buffer if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "first_modified.txt") { t.Errorf("should report first modified buffer: %v", m.commandOutput.Lines) } }) } // ================================================== // cmdQuitForce Tests (q!) // ================================================== func TestCmdQuitForce(t *testing.T) { t.Run("quits with modified buffer", func(t *testing.T) { buf := core.NewBufferBuilder(). WithFilename("unsaved.txt"). WithLines([]string{"unsaved content"}). Modified(). Build() m := newMockModelWithBuffer(&buf) cmd := cmdQuit(m, []string{}, true) // Should NOT error if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Should return quit command if cmd == nil { t.Fatal("expected quit command, got nil") } msg := cmd() if _, ok := msg.(tea.QuitMsg); !ok { t.Errorf("expected tea.QuitMsg, got %T", msg) } }) t.Run("quits with readonly modified buffer", func(t *testing.T) { buf := core.NewBufferBuilder(). WithFilename("readonly.txt"). WithLines([]string{"modified content"}). ReadOnly(). Modified(). Build() m := newMockModelWithBuffer(&buf) cmd := cmdQuit(m, []string{}, true) // Force quit should work even with readonly modified buffer if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { t.Fatal("expected quit command") } msg := cmd() if _, ok := msg.(tea.QuitMsg); !ok { t.Errorf("expected tea.QuitMsg, got %T", msg) } }) t.Run("quits with multiple modified buffers", func(t *testing.T) { buf1 := core.NewBufferBuilder(). WithFilename("file1.txt"). WithLines([]string{"unsaved 1"}). Modified(). Build() buf2 := core.NewBufferBuilder(). WithFilename("file2.txt"). WithLines([]string{"unsaved 2"}). Modified(). Build() buf3 := core.NewBufferBuilder(). WithFilename("file3.txt"). WithLines([]string{"unsaved 3"}). Modified(). Build() m := newMockModelWithBuffer(&buf1) m.buffers = append(m.buffers, &buf2, &buf3) cmd := cmdQuit(m, []string{}, true) // Should quit regardless of multiple modified buffers if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { t.Fatal("expected quit command") } msg := cmd() if _, ok := msg.(tea.QuitMsg); !ok { t.Errorf("expected tea.QuitMsg, got %T", msg) } }) t.Run("quits with unmodified buffers", func(t *testing.T) { buf := core.NewBufferBuilder(). WithFilename("test.txt"). WithLines([]string{"content"}). Build() m := newMockModelWithBuffer(&buf) cmd := cmdQuit(m, []string{}, false) // Should work with unmodified buffers too if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { t.Fatal("expected quit command") } msg := cmd() if _, ok := msg.(tea.QuitMsg); !ok { t.Errorf("expected tea.QuitMsg, got %T", msg) } }) t.Run("quits with buffer without filename", func(t *testing.T) { buf := core.NewBufferBuilder(). WithFilename(""). WithLines([]string{"unsaved content"}). Modified(). Build() m := newMockModelWithBuffer(&buf) cmd := cmdQuit(m, []string{}, true) // Force quit works even without filename if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { t.Fatal("expected quit command") } msg := cmd() if _, ok := msg.(tea.QuitMsg); !ok { t.Errorf("expected tea.QuitMsg, got %T", msg) } }) t.Run("quits with scratch buffer", func(t *testing.T) { buf := core.NewBufferBuilder(). WithType(core.ScatchBuffer). WithFilename("scratch"). WithLines([]string{"content"}). Modified(). Build() m := newMockModelWithBuffer(&buf) cmd := cmdQuit(m, []string{}, true) // Force quit works with scratch buffers if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { t.Fatal("expected quit command") } msg := cmd() if _, ok := msg.(tea.QuitMsg); !ok { t.Errorf("expected tea.QuitMsg, got %T", msg) } }) t.Run("quits with mix of modified and unmodified buffers", func(t *testing.T) { modifiedBuf := core.NewBufferBuilder(). WithFilename("modified.txt"). WithLines([]string{"unsaved"}). Modified(). Build() unmodifiedBuf := core.NewBufferBuilder(). WithFilename("unmodified.txt"). WithLines([]string{"saved"}). Build() readonlyModifiedBuf := core.NewBufferBuilder(). WithFilename("readonly.txt"). WithLines([]string{"readonly unsaved"}). ReadOnly(). Modified(). Build() m := newMockModelWithBuffer(&modifiedBuf) m.buffers = append(m.buffers, &unmodifiedBuf, &readonlyModifiedBuf) cmd := cmdQuit(m, []string{}, true) // Should quit regardless of buffer states if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { t.Fatal("expected quit command") } msg := cmd() if _, ok := msg.(tea.QuitMsg); !ok { t.Errorf("expected tea.QuitMsg, got %T", msg) } }) t.Run("does not switch buffers", func(t *testing.T) { activeBuf := core.NewBufferBuilder(). WithFilename("active.txt"). WithLines([]string{"active"}). Build() modifiedBuf := core.NewBufferBuilder(). WithFilename("modified.txt"). WithLines([]string{"unsaved"}). Modified(). Build() m := newMockModelWithBuffer(&activeBuf) m.buffers = append(m.buffers, &modifiedBuf) // Remember active buffer activeFilename := m.activeWindow.Buffer.Filename cmdQuit(m, []string{}, true) // Should NOT switch to modified buffer (unlike regular quit) if m.activeWindow.Buffer.Filename != activeFilename { t.Errorf("should not switch buffers, got %q", m.activeWindow.Buffer.Filename) } }) } // ================================================== // cmdQuitAllForce Tests (qall! / qa!) // ================================================== func TestCmdQuitAllForce(t *testing.T) { t.Run("quits with all buffers modified", func(t *testing.T) { buf1 := core.NewBufferBuilder(). WithFilename("file1.txt"). WithLines([]string{"unsaved 1"}). Modified(). Build() buf2 := core.NewBufferBuilder(). WithFilename("file2.txt"). WithLines([]string{"unsaved 2"}). Modified(). Build() m := newMockModelWithBuffer(&buf1) m.buffers = append(m.buffers, &buf2) cmd := cmdQuitAll(m, []string{}, true) // Should quit even with all buffers modified if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { t.Fatal("expected quit command, got nil") } msg := cmd() if _, ok := msg.(tea.QuitMsg); !ok { t.Errorf("expected tea.QuitMsg, got %T", msg) } }) t.Run("quits with mix of modified and unmodified", func(t *testing.T) { buf1 := core.NewBufferBuilder(). WithFilename("saved.txt"). WithLines([]string{"content"}). Build() buf2 := core.NewBufferBuilder(). WithFilename("unsaved.txt"). WithLines([]string{"modified content"}). Modified(). Build() m := newMockModelWithBuffer(&buf1) m.buffers = append(m.buffers, &buf2) cmd := cmdQuitAll(m, []string{}, true) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { t.Fatal("expected quit command") } msg := cmd() if _, ok := msg.(tea.QuitMsg); !ok { t.Errorf("expected tea.QuitMsg, got %T", msg) } }) t.Run("quits with readonly buffers", func(t *testing.T) { buf1 := core.NewBufferBuilder(). WithFilename("readonly1.txt"). WithLines([]string{"content"}). ReadOnly(). Modified(). Build() buf2 := core.NewBufferBuilder(). WithFilename("readonly2.txt"). WithLines([]string{"content"}). ReadOnly(). Build() m := newMockModelWithBuffer(&buf1) m.buffers = append(m.buffers, &buf2) cmd := cmdQuitAll(m, []string{}, true) // Force quit should work with readonly buffers if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { t.Fatal("expected quit command") } msg := cmd() if _, ok := msg.(tea.QuitMsg); !ok { t.Errorf("expected tea.QuitMsg, got %T", msg) } }) t.Run("quits with scratch buffers", func(t *testing.T) { buf1 := core.NewBufferBuilder(). WithType(core.ScatchBuffer). WithFilename("scratch1"). WithLines([]string{"content"}). Modified(). Build() buf2 := core.NewBufferBuilder(). WithType(core.ScatchBuffer). WithFilename("scratch2"). WithLines([]string{"content"}). Build() m := newMockModelWithBuffer(&buf1) m.buffers = append(m.buffers, &buf2) cmd := cmdQuitAll(m, []string{}, true) // Should quit with scratch buffers if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { t.Fatal("expected quit command") } msg := cmd() if _, ok := msg.(tea.QuitMsg); !ok { t.Errorf("expected tea.QuitMsg, got %T", msg) } }) t.Run("quits with buffers without filenames", func(t *testing.T) { buf1 := core.NewBufferBuilder(). WithFilename(""). WithLines([]string{"unsaved 1"}). Modified(). Build() buf2 := core.NewBufferBuilder(). WithFilename(""). WithLines([]string{"unsaved 2"}). Modified(). Build() m := newMockModelWithBuffer(&buf1) m.buffers = append(m.buffers, &buf2) cmd := cmdQuitAll(m, []string{}, true) // Force quit works even without filenames if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { t.Fatal("expected quit command") } msg := cmd() if _, ok := msg.(tea.QuitMsg); !ok { t.Errorf("expected tea.QuitMsg, got %T", msg) } }) t.Run("quits with no buffers", func(t *testing.T) { m := newMockModel() // Clear buffers m.buffers = []*core.Buffer{} cmd := cmdQuitAll(m, []string{}, true) // Should quit even with no buffers if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { t.Fatal("expected quit command") } msg := cmd() if _, ok := msg.(tea.QuitMsg); !ok { t.Errorf("expected tea.QuitMsg, got %T", msg) } }) t.Run("quits with complex buffer states", func(t *testing.T) { // Mix of all different buffer states buf1 := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename("file.txt"). WithLines([]string{"modified file"}). Modified(). Build() buf2 := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename("readonly.txt"). WithLines([]string{"readonly modified"}). ReadOnly(). Modified(). Build() buf3 := core.NewBufferBuilder(). WithType(core.ScatchBuffer). WithFilename("scratch"). WithLines([]string{"scratch modified"}). Modified(). Build() buf4 := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(""). WithLines([]string{"no name"}). Modified(). Build() buf5 := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename("saved.txt"). WithLines([]string{"saved content"}). Build() m := newMockModelWithBuffer(&buf1) m.buffers = append(m.buffers, &buf2, &buf3, &buf4, &buf5) cmd := cmdQuitAll(m, []string{}, true) // Force quit should work regardless of any buffer state if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { t.Fatal("expected quit command") } msg := cmd() if _, ok := msg.(tea.QuitMsg); !ok { t.Errorf("expected tea.QuitMsg, got %T", msg) } }) t.Run("quits with all buffers unmodified", func(t *testing.T) { buf1 := core.NewBufferBuilder(). WithFilename("file1.txt"). WithLines([]string{"content"}). Build() buf2 := core.NewBufferBuilder(). WithFilename("file2.txt"). WithLines([]string{"content"}). Build() m := newMockModelWithBuffer(&buf1) m.buffers = append(m.buffers, &buf2) cmd := cmdQuitAll(m, []string{}, true) // Should work with unmodified buffers too if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { t.Fatal("expected quit command") } msg := cmd() if _, ok := msg.(tea.QuitMsg); !ok { t.Errorf("expected tea.QuitMsg, got %T", msg) } }) } // ================================================== // cmdWriteAll Tests // ================================================== func TestCmdWriteAll(t *testing.T) { t.Run("writes all modified buffers", func(t *testing.T) { tmpDir := t.TempDir() file1 := filepath.Join(tmpDir, "file1.txt") file2 := filepath.Join(tmpDir, "file2.txt") buf1 := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(file1). WithLines([]string{"content 1"}). Build() buf1.Modified = true buf2 := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(file2). WithLines([]string{"content 2"}). Build() buf2.Modified = true m := newMockModelWithBuffer(&buf1) m.buffers = append(m.buffers, &buf2) cmdWriteAll(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Verify both files were written content1, err := os.ReadFile(file1) if err != nil { t.Errorf("file1 not written: %v", err) } else if string(content1) != "content 1\n" { t.Errorf("file1 content = %q", string(content1)) } content2, err := os.ReadFile(file2) if err != nil { t.Errorf("file2 not written: %v", err) } else if string(content2) != "content 2\n" { t.Errorf("file2 content = %q", string(content2)) } }) t.Run("skips unmodified buffers", func(t *testing.T) { tmpDir := t.TempDir() modifiedFile := filepath.Join(tmpDir, "modified.txt") unmodifiedFile := filepath.Join(tmpDir, "unmodified.txt") modifiedBuf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(modifiedFile). WithLines([]string{"new content"}). Build() modifiedBuf.Modified = true unmodifiedBuf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(unmodifiedFile). WithLines([]string{"old content"}). Build() unmodifiedBuf.Modified = false m := newMockModelWithBuffer(&modifiedBuf) m.buffers = append(m.buffers, &unmodifiedBuf) cmdWriteAll(m, []string{}, false) // Modified file should be written if _, err := os.Stat(modifiedFile); os.IsNotExist(err) { t.Error("modified file should be written") } // Unmodified file should NOT be written (doesn't exist) if _, err := os.Stat(unmodifiedFile); !os.IsNotExist(err) { t.Error("unmodified file should not be written") } }) t.Run("clears modified flag on written buffers", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "test.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines([]string{"content"}). Modified(). Build() m := newMockModelWithBuffer(&buf) cmdWriteAll(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if buf.Modified { t.Error("modified flag should be cleared after write") } }) t.Run("errors on modified readonly buffer", func(t *testing.T) { buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename("readonly.txt"). WithLines([]string{"content"}). ReadOnly(). Modified(). Build() m := newMockModelWithBuffer(&buf) cmdWriteAll(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Fatal("expected error for modified readonly buffer") } if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "readonly") { t.Errorf("error should mention readonly: %v", m.commandOutput.Lines) } }) t.Run("skips unmodified readonly buffer", func(t *testing.T) { tmpDir := t.TempDir() writableFile := filepath.Join(tmpDir, "writable.txt") writableBuf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(writableFile). WithLines([]string{"writable content"}). Modified(). Build() // Readonly but NOT modified - should be skipped readonlyBuf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename("readonly.txt"). WithLines([]string{"readonly content"}). ReadOnly(). Build() // NOT modified! m := newMockModelWithBuffer(&writableBuf) m.buffers = append(m.buffers, &readonlyBuf) cmdWriteAll(m, []string{}, false) // Should succeed - readonly buffer not modified, so skip it if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("should not error for unmodified readonly buffer: %v", m.commandOutput.Lines) } // Writable file should be written content, _ := os.ReadFile(writableFile) if string(content) != "writable content\n" { t.Errorf("writable buffer not written: %q", string(content)) } }) t.Run("errors on scratch buffer", func(t *testing.T) { buf := core.NewBufferBuilder(). WithType(core.ScatchBuffer). WithFilename("scratch"). WithLines([]string{"content"}). Modified(). Build() m := newMockModelWithBuffer(&buf) cmdWriteAll(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for scratch buffer") } }) t.Run("errors on buffer with no filename", func(t *testing.T) { buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(""). WithLines([]string{"content"}). Modified(). Build() m := newMockModelWithBuffer(&buf) cmdWriteAll(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for buffer without filename") } if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "no file name") || !strings.Contains(strings.ToLower(strings.Join(m.commandOutput.Lines, " ")), "name") { t.Logf("error message: %v", m.commandOutput.Lines) } }) t.Run("succeeds with no modified buffers", func(t *testing.T) { buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename("unchanged.txt"). WithLines([]string{"content"}). Build() m := newMockModelWithBuffer(&buf) cmdWriteAll(m, []string{}, false) // Should succeed even with nothing to write if m.commandOutput != nil && m.commandOutput.IsError { t.Errorf("unexpected error: %v", m.commandOutput.Lines) } }) t.Run("reports number of buffers written", func(t *testing.T) { tmpDir := t.TempDir() file1 := filepath.Join(tmpDir, "file1.txt") file2 := filepath.Join(tmpDir, "file2.txt") buf1 := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(file1). WithLines([]string{"content"}). Build() buf1.Modified = true buf2 := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(file2). WithLines([]string{"content"}). Build() buf2.Modified = true m := newMockModelWithBuffer(&buf1) m.buffers = append(m.buffers, &buf2) cmdWriteAll(m, []string{}, false) // Should have some output indicating what was written if m.commandOutput == nil || len(m.commandOutput.Lines) == 0 { t.Log("Note: cmdWriteAll doesn't set output message") } else if !strings.Contains(m.commandOutput.Lines[0], "2") { t.Logf("Output message: %q", m.commandOutput.Lines[0]) } }) t.Run("stops on first error", func(t *testing.T) { tmpDir := t.TempDir() validFile := filepath.Join(tmpDir, "valid.txt") // First buffer - valid validBuf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(validFile). WithLines([]string{"content"}). Build() validBuf.Modified = true // Second buffer - invalid (no filename) invalidBuf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(""). WithLines([]string{"content"}). Build() invalidBuf.Modified = true m := newMockModelWithBuffer(&invalidBuf) // Invalid first m.buffers = append(m.buffers, &validBuf) cmdWriteAll(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error") } // Valid file might or might not be written depending on implementation // (stops on first error vs continues and reports all errors) }) } // ================================================== // cmdWriteQuit Tests // ================================================== func TestCmdWriteQuit(t *testing.T) { t.Run("writes buffer and quits", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "test.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines([]string{"content to save"}). Build() m := newMockModelWithBuffer(&buf) cmd := cmdWriteQuit(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Verify file was written content, err := os.ReadFile(filename) if err != nil { t.Errorf("file not written: %v", err) } else if string(content) != "content to save\n" { t.Errorf("file content = %q", string(content)) } // Verify quit command returned if cmd == nil { t.Fatal("expected quit command") } msg := cmd() if _, ok := msg.(tea.QuitMsg); !ok { t.Errorf("expected tea.QuitMsg, got %T", msg) } }) t.Run("errors on readonly buffer", func(t *testing.T) { buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename("readonly.txt"). WithLines([]string{"content"}). ReadOnly(). Build() m := newMockModelWithBuffer(&buf) cmd := cmdWriteQuit(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for readonly buffer") } if cmd != nil { t.Error("should not return quit command on error") } }) t.Run("errors on scratch buffer", func(t *testing.T) { buf := core.NewBufferBuilder(). WithType(core.ScatchBuffer). WithFilename("scratch"). WithLines([]string{"content"}). Build() m := newMockModelWithBuffer(&buf) cmd := cmdWriteQuit(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for scratch buffer") } if cmd != nil { t.Error("should not return quit command on error") } }) t.Run("errors on buffer with no filename", func(t *testing.T) { buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(""). WithLines([]string{"content"}). Build() m := newMockModelWithBuffer(&buf) cmd := cmdWriteQuit(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for buffer without filename") } if cmd != nil { t.Error("should not return quit command on error") } }) t.Run("errors on invalid path", func(t *testing.T) { buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename("/invalid/nonexistent/path/file.txt"). WithLines([]string{"content"}). Build() m := newMockModelWithBuffer(&buf) cmd := cmdWriteQuit(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for invalid path") } if cmd != nil { t.Error("should not return quit command on error") } }) t.Run("writes to argument filename if provided", func(t *testing.T) { tmpDir := t.TempDir() originalFile := filepath.Join(tmpDir, "original.txt") newFile := filepath.Join(tmpDir, "new.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(originalFile). WithLines([]string{"content"}). Build() m := newMockModelWithBuffer(&buf) cmd := cmdWriteQuit(m, []string{newFile}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // New file should exist if _, err := os.Stat(newFile); os.IsNotExist(err) { t.Error("new file not created") } // Original should not exist if _, err := os.Stat(originalFile); !os.IsNotExist(err) { t.Error("original file should not exist") } if cmd == nil { t.Error("expected quit command") } }) t.Run("clears modified flag before quit", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "test.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines([]string{"content"}). Modified(). Build() m := newMockModelWithBuffer(&buf) cmdWriteQuit(m, []string{}, false) if buf.Modified { t.Error("modified flag should be cleared") } }) } // ================================================== // cmdWriteQuitAll Tests (wqall / wqa / xa) // ================================================== func TestCmdWriteQuitAll(t *testing.T) { // Note: This command may not be implemented yet // These tests define expected behavior t.Run("writes all buffers and quits", func(t *testing.T) { tmpDir := t.TempDir() file1 := filepath.Join(tmpDir, "file1.txt") file2 := filepath.Join(tmpDir, "file2.txt") buf1 := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(file1). WithLines([]string{"content 1"}). Build() buf1.Modified = true buf2 := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(file2). WithLines([]string{"content 2"}). Build() buf2.Modified = true m := newMockModelWithBuffer(&buf1) m.buffers = append(m.buffers, &buf2) cmd := cmdWriteQuitAll(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Verify files were written content1, _ := os.ReadFile(file1) if string(content1) != "content 1\n" { t.Errorf("file1 content = %q", string(content1)) } content2, _ := os.ReadFile(file2) if string(content2) != "content 2\n" { t.Errorf("file2 content = %q", string(content2)) } // Verify quit command if cmd == nil { t.Fatal("expected quit command") } msg := cmd() if _, ok := msg.(tea.QuitMsg); !ok { t.Errorf("expected tea.QuitMsg, got %T", msg) } }) t.Run("skips unmodified buffers", func(t *testing.T) { tmpDir := t.TempDir() modifiedFile := filepath.Join(tmpDir, "modified.txt") unmodifiedFile := filepath.Join(tmpDir, "unmodified.txt") modifiedBuf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(modifiedFile). WithLines([]string{"new content"}). Build() modifiedBuf.Modified = true unmodifiedBuf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(unmodifiedFile). WithLines([]string{"unchanged"}). Build() unmodifiedBuf.Modified = false m := newMockModelWithBuffer(&modifiedBuf) m.buffers = append(m.buffers, &unmodifiedBuf) cmd := cmdWriteQuitAll(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Should still quit even if some buffers unmodified if cmd == nil { t.Error("expected quit command") } }) t.Run("errors if readonly buffer is modified", func(t *testing.T) { tmpDir := t.TempDir() validFile := filepath.Join(tmpDir, "valid.txt") validBuf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(validFile). WithLines([]string{"content"}). Modified(). Build() // Readonly buffer that IS modified - should cause error readonlyBuf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename("readonly.txt"). WithLines([]string{"content"}). ReadOnly(). Modified(). Build() m := newMockModelWithBuffer(&validBuf) m.buffers = append(m.buffers, &readonlyBuf) cmd := cmdWriteQuitAll(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for modified readonly buffer") } if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "readonly") { t.Errorf("error should mention readonly: %v", m.commandOutput.Lines) } if cmd != nil { t.Error("should not quit when readonly buffer has unsaved changes") } }) t.Run("quits with readonly buffer that is NOT modified", func(t *testing.T) { tmpDir := t.TempDir() writableFile := filepath.Join(tmpDir, "writable.txt") writableBuf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(writableFile). WithLines([]string{"modified content"}). Modified(). Build() // Readonly buffer that is NOT modified - should be skipped, not prevent quit readonlyBuf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename("readonly.txt"). WithLines([]string{"unchanged content"}). ReadOnly(). Build() // NOT modified - this is the key m := newMockModelWithBuffer(&writableBuf) m.buffers = append(m.buffers, &readonlyBuf) cmd := cmdWriteQuitAll(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("should not error for unmodified readonly buffer: %v", m.commandOutput.Lines) } // Should write the modified writable buffer content, _ := os.ReadFile(writableFile) if string(content) != "modified content\n" { t.Errorf("writable buffer not written: %q", string(content)) } // Should return quit command if cmd == nil { t.Error("expected quit command (readonly buffer not modified)") } }) t.Run("errors if any buffer has no filename", func(t *testing.T) { noNameBuf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(""). WithLines([]string{"content"}). Build() noNameBuf.Modified = true m := newMockModelWithBuffer(&noNameBuf) cmd := cmdWriteQuitAll(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for buffer without filename") } if cmd != nil { t.Error("should not quit when write fails") } }) t.Run("quits with no modified buffers", func(t *testing.T) { buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename("unchanged.txt"). WithLines([]string{"content"}). Build() m := newMockModelWithBuffer(&buf) cmd := cmdWriteQuitAll(m, []string{}, false) // Should still quit even if nothing to write if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { t.Error("expected quit command") } }) t.Run("clears modified flags on all buffers", func(t *testing.T) { tmpDir := t.TempDir() file1 := filepath.Join(tmpDir, "file1.txt") file2 := filepath.Join(tmpDir, "file2.txt") buf1 := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(file1). WithLines([]string{"content"}). Build() buf1.Modified = true buf2 := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(file2). WithLines([]string{"content"}). Build() buf2.Modified = true m := newMockModelWithBuffer(&buf1) m.buffers = append(m.buffers, &buf2) cmdWriteQuitAll(m, []string{}, false) if buf1.Modified { t.Error("buf1 modified flag should be cleared") } if buf2.Modified { t.Error("buf2 modified flag should be cleared") } }) } // ================================================== // Force Write Tests (w!) // ================================================== func TestCmdWriteForce(t *testing.T) { t.Run("force writes readonly buffer to its own file", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "readonly.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines([]string{"forced content"}). ReadOnly(). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{}, true) // Force should bypass readonly check if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Verify file was written content, err := os.ReadFile(filename) if err != nil { t.Fatalf("file not written: %v", err) } expected := "forced content\n" if string(content) != expected { t.Errorf("got %q, want %q", string(content), expected) } }) t.Run("force writes readonly buffer to argument file", func(t *testing.T) { tmpDir := t.TempDir() originalFile := filepath.Join(tmpDir, "readonly_original.txt") newFile := filepath.Join(tmpDir, "new_destination.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(originalFile). WithLines([]string{"readonly content"}). ReadOnly(). Modified(). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{newFile}, true) // Should succeed with force if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Verify new file was created content, err := os.ReadFile(newFile) if err != nil { t.Fatalf("new file not created: %v", err) } expected := "readonly content\n" if string(content) != expected { t.Errorf("got %q, want %q", string(content), expected) } // Original should not exist (never written) if _, err := os.Stat(originalFile); !os.IsNotExist(err) { t.Error("original file should not exist") } }) t.Run("force writes scratch buffer to argument file", func(t *testing.T) { tmpDir := t.TempDir() targetFile := filepath.Join(tmpDir, "from_scratch.txt") buf := core.NewBufferBuilder(). WithType(core.ScatchBuffer). WithFilename("scratch"). WithLines([]string{"scratch content", "line 2"}). Modified(). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{targetFile}, true) // Force with filename should work for scratch buffer if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Verify file was written content, err := os.ReadFile(targetFile) if err != nil { t.Fatalf("file not written: %v", err) } expected := "scratch content\nline 2\n" if string(content) != expected { t.Errorf("got %q, want %q", string(content), expected) } }) t.Run("force writes readonly scratch buffer to argument file", func(t *testing.T) { tmpDir := t.TempDir() targetFile := filepath.Join(tmpDir, "from_readonly_scratch.txt") // Readonly scratch buffer - double protection buf := core.NewBufferBuilder(). WithType(core.ScatchBuffer). WithFilename("scratch"). WithLines([]string{"protected content"}). ReadOnly(). Modified(). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{targetFile}, true) // Force should bypass BOTH readonly AND scratch checks if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error with force: %v", m.commandOutput.Lines) } // Verify file was written content, err := os.ReadFile(targetFile) if err != nil { t.Fatalf("file not written: %v", err) } expected := "protected content\n" if string(content) != expected { t.Errorf("got %q, want %q", string(content), expected) } // Verify modified flag was cleared if buf.Modified { t.Error("modified flag should be cleared after successful write") } }) t.Run("force write scratch buffer without filename still errors", func(t *testing.T) { buf := core.NewBufferBuilder(). WithType(core.ScatchBuffer). WithFilename(""). WithLines([]string{"content"}). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{}, true) // Force doesn't help if there's no filename to write to if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error when no filename provided") } if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "no file name") { t.Errorf("error should mention no file name: %v", m.commandOutput.Lines) } }) t.Run("force write to invalid path still errors", func(t *testing.T) { buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename("/invalid/nonexistent/path/file.txt"). WithLines([]string{"content"}). ReadOnly(). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{}, true) // Force bypasses buffer checks but not OS-level checks if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for invalid path even with force") } }) t.Run("force write without force flag still errors on readonly", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "readonly.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines([]string{"content"}). ReadOnly(). Build() m := newMockModelWithBuffer(&buf) // force=false should still prevent write cmdWrite(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for readonly without force flag") } if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "readonly") { t.Errorf("error should mention readonly: %v", m.commandOutput.Lines) } }) t.Run("force write clears modified flag even for readonly buffer", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "test.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines([]string{"content"}). ReadOnly(). Modified(). Build() m := newMockModelWithBuffer(&buf) if !buf.Modified { t.Fatal("precondition: buffer should be modified") } cmdWrite(m, []string{}, true) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Modified flag should be cleared if buf.Modified { t.Error("modified flag should be cleared after force write") } }) t.Run("force write preserves buffer type", func(t *testing.T) { tmpDir := t.TempDir() targetFile := filepath.Join(tmpDir, "output.txt") buf := core.NewBufferBuilder(). WithType(core.ScatchBuffer). WithFilename("scratch"). WithLines([]string{"content"}). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{targetFile}, true) // Buffer type should remain ScratchBuffer if buf.Type != core.ScatchBuffer { t.Errorf("buffer type changed to %v, should remain ScatchBuffer", buf.Type) } // Filename should NOT change (Vim behavior) if buf.Filename != "scratch" { t.Errorf("buffer filename changed to %q, should remain 'scratch'", buf.Filename) } }) t.Run("force write with empty scratch buffer", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "empty.txt") buf := core.NewBufferBuilder(). WithType(core.ScatchBuffer). WithFilename(""). WithLines([]string{}). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{filename}, true) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Should create empty file content, err := os.ReadFile(filename) if err != nil { t.Fatalf("file not created: %v", err) } if string(content) != "" { t.Errorf("expected empty file, got %q", string(content)) } }) } // ================================================== // Force WriteAll Tests (wall!) // ================================================== func TestCmdWriteAllForce(t *testing.T) { t.Run("force writes all modified readonly buffers", func(t *testing.T) { tmpDir := t.TempDir() file1 := filepath.Join(tmpDir, "readonly1.txt") file2 := filepath.Join(tmpDir, "readonly2.txt") buf1 := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(file1). WithLines([]string{"content 1"}). ReadOnly(). Modified(). Build() buf2 := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(file2). WithLines([]string{"content 2"}). ReadOnly(). Modified(). Build() m := newMockModelWithBuffer(&buf1) m.buffers = append(m.buffers, &buf2) cmdWriteAll(m, []string{}, true) // Should succeed with force if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Both files should be written content1, err := os.ReadFile(file1) if err != nil { t.Errorf("file1 not written: %v", err) } else if string(content1) != "content 1\n" { t.Errorf("file1 content = %q", string(content1)) } content2, err := os.ReadFile(file2) if err != nil { t.Errorf("file2 not written: %v", err) } else if string(content2) != "content 2\n" { t.Errorf("file2 content = %q", string(content2)) } }) t.Run("force writeall still errors on scratch buffer without filename argument", func(t *testing.T) { buf := core.NewBufferBuilder(). WithType(core.ScatchBuffer). WithFilename(""). WithLines([]string{"content"}). Modified(). Build() m := newMockModelWithBuffer(&buf) cmdWriteAll(m, []string{}, true) // Force can bypass scratch type check, but if buffer has no filename // it should still error with "no file name provided" if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for scratch buffer without filename") } }) t.Run("force writeall with mix of protected buffer types", func(t *testing.T) { tmpDir := t.TempDir() readonlyFile := filepath.Join(tmpDir, "readonly.txt") normalFile := filepath.Join(tmpDir, "normal.txt") readonlyBuf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(readonlyFile). WithLines([]string{"readonly content"}). ReadOnly(). Modified(). Build() normalBuf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(normalFile). WithLines([]string{"normal content"}). Modified(). Build() unmodifiedBuf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filepath.Join(tmpDir, "unmodified.txt")). WithLines([]string{"unchanged"}). Build() m := newMockModelWithBuffer(&readonlyBuf) m.buffers = append(m.buffers, &normalBuf, &unmodifiedBuf) cmdWriteAll(m, []string{}, true) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Both modified files should be written if _, err := os.Stat(readonlyFile); os.IsNotExist(err) { t.Error("readonly file should be written with force") } if _, err := os.Stat(normalFile); os.IsNotExist(err) { t.Error("normal file should be written") } // Unmodified should be skipped (doesn't exist) unmodifiedFile := filepath.Join(tmpDir, "unmodified.txt") if _, err := os.Stat(unmodifiedFile); !os.IsNotExist(err) { t.Error("unmodified file should not be written") } }) t.Run("force writeall clears modified flags", func(t *testing.T) { tmpDir := t.TempDir() file1 := filepath.Join(tmpDir, "file1.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(file1). WithLines([]string{"content"}). ReadOnly(). Modified(). Build() m := newMockModelWithBuffer(&buf) cmdWriteAll(m, []string{}, true) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if buf.Modified { t.Error("modified flag should be cleared") } }) } // ================================================== // Force WriteQuit Tests (wq!) // ================================================== func TestCmdWriteQuitForce(t *testing.T) { t.Run("force write-quit with readonly buffer", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "readonly.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines([]string{"content"}). ReadOnly(). Modified(). Build() m := newMockModelWithBuffer(&buf) cmd := cmdWriteQuit(m, []string{}, true) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // File should be written content, err := os.ReadFile(filename) if err != nil { t.Errorf("file not written: %v", err) } else if string(content) != "content\n" { t.Errorf("content = %q", string(content)) } // Should quit if cmd == nil { t.Fatal("expected quit command") } msg := cmd() if _, ok := msg.(tea.QuitMsg); !ok { t.Errorf("expected tea.QuitMsg, got %T", msg) } }) t.Run("force write-quit with scratch buffer and filename arg", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "output.txt") buf := core.NewBufferBuilder(). WithType(core.ScatchBuffer). WithFilename("scratch"). WithLines([]string{"scratch data"}). Modified(). Build() m := newMockModelWithBuffer(&buf) cmd := cmdWriteQuit(m, []string{filename}, true) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // File should be written content, err := os.ReadFile(filename) if err != nil { t.Errorf("file not written: %v", err) } else if string(content) != "scratch data\n" { t.Errorf("content = %q", string(content)) } if cmd == nil { t.Error("expected quit command") } }) t.Run("force write-quit without filename on scratch buffer still errors", func(t *testing.T) { buf := core.NewBufferBuilder(). WithType(core.ScatchBuffer). WithFilename(""). WithLines([]string{"content"}). Build() m := newMockModelWithBuffer(&buf) cmd := cmdWriteQuit(m, []string{}, true) // Can't write without a filename even with force if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error when no filename") } if cmd != nil { t.Error("should not quit on error") } }) } // ================================================== // Force WriteQuitAll Tests (wqall! / wqa! / xa!) // ================================================== func TestCmdWriteQuitAllForce(t *testing.T) { t.Run("force write-quit-all with readonly buffers", func(t *testing.T) { tmpDir := t.TempDir() file1 := filepath.Join(tmpDir, "readonly1.txt") file2 := filepath.Join(tmpDir, "readonly2.txt") buf1 := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(file1). WithLines([]string{"content 1"}). ReadOnly(). Modified(). Build() buf2 := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(file2). WithLines([]string{"content 2"}). ReadOnly(). Modified(). Build() m := newMockModelWithBuffer(&buf1) m.buffers = append(m.buffers, &buf2) cmd := cmdWriteQuitAll(m, []string{}, true) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Both files should be written content1, _ := os.ReadFile(file1) if string(content1) != "content 1\n" { t.Errorf("file1 content = %q", string(content1)) } content2, _ := os.ReadFile(file2) if string(content2) != "content 2\n" { t.Errorf("file2 content = %q", string(content2)) } // Should quit if cmd == nil { t.Fatal("expected quit command") } msg := cmd() if _, ok := msg.(tea.QuitMsg); !ok { t.Errorf("expected tea.QuitMsg, got %T", msg) } }) t.Run("force write-quit-all with mix of buffer types", func(t *testing.T) { tmpDir := t.TempDir() readonlyFile := filepath.Join(tmpDir, "readonly.txt") normalFile := filepath.Join(tmpDir, "normal.txt") readonlyBuf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(readonlyFile). WithLines([]string{"readonly"}). ReadOnly(). Modified(). Build() normalBuf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(normalFile). WithLines([]string{"normal"}). Modified(). Build() unmodifiedBuf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename("unmodified.txt"). WithLines([]string{"unchanged"}). Build() m := newMockModelWithBuffer(&readonlyBuf) m.buffers = append(m.buffers, &normalBuf, &unmodifiedBuf) cmd := cmdWriteQuitAll(m, []string{}, true) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Modified buffers should be written if _, err := os.Stat(readonlyFile); os.IsNotExist(err) { t.Error("readonly file should be written") } if _, err := os.Stat(normalFile); os.IsNotExist(err) { t.Error("normal file should be written") } if cmd == nil { t.Error("expected quit command") } }) t.Run("force write-quit-all still errors on buffer without filename", func(t *testing.T) { tmpDir := t.TempDir() validFile := filepath.Join(tmpDir, "valid.txt") validBuf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(validFile). WithLines([]string{"content"}). Modified(). Build() noNameBuf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(""). WithLines([]string{"content"}). Modified(). Build() m := newMockModelWithBuffer(&validBuf) m.buffers = append(m.buffers, &noNameBuf) cmd := cmdWriteQuitAll(m, []string{}, true) // Force can't help when there's no filename if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for buffer without filename") } if cmd != nil { t.Error("should not quit when write fails") } }) t.Run("force write-quit-all clears all modified flags", func(t *testing.T) { tmpDir := t.TempDir() file1 := filepath.Join(tmpDir, "file1.txt") file2 := filepath.Join(tmpDir, "file2.txt") buf1 := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(file1). WithLines([]string{"content"}). ReadOnly(). Modified(). Build() buf2 := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(file2). WithLines([]string{"content"}). Modified(). Build() m := newMockModelWithBuffer(&buf1) m.buffers = append(m.buffers, &buf2) cmdWriteQuitAll(m, []string{}, true) if buf1.Modified { t.Error("buf1 modified flag should be cleared") } if buf2.Modified { t.Error("buf2 modified flag should be cleared") } }) } // ================================================== // Edge Case Tests // ================================================== func TestEdgeCases(t *testing.T) { t.Run("write to file that becomes readonly during editing", func(t *testing.T) { if os.Getuid() == 0 { t.Skip("test requires non-root user") } tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "test.txt") // Create file os.WriteFile(filename, []byte("original"), 0644) buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines([]string{"modified content"}). Modified(). Build() // Make file readonly on filesystem (different from buffer readonly flag) os.Chmod(filename, 0444) defer os.Chmod(filename, 0644) // Cleanup m := newMockModelWithBuffer(&buf) // Regular write should fail (OS permission denied) cmdWrite(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for OS-level readonly file") } }) t.Run("write unmodified readonly buffer is allowed with force", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "readonly.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines([]string{"content"}). ReadOnly(). Build() buf.Modified = false m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{}, true) // Force write should work even for unmodified readonly if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } content, _ := os.ReadFile(filename) if string(content) != "content\n" { t.Errorf("file not written correctly") } }) t.Run("buffer with both readonly and modified flags", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "test.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines([]string{"edited readonly content"}). ReadOnly(). Modified(). Build() m := newMockModelWithBuffer(&buf) // Without force - should error cmdWrite(m, []string{}, false) if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error without force") } // With force - should succeed m.commandOutput = nil cmdWrite(m, []string{}, true) if m.commandOutput != nil && m.commandOutput.IsError { t.Errorf("force write failed: %v", m.commandOutput.Lines) } // Modified flag should be cleared if buf.Modified { t.Error("modified flag should be cleared") } }) t.Run("write buffer with very long filename", func(t *testing.T) { tmpDir := t.TempDir() // Create a long but valid filename (255 chars is typical limit) longName := strings.Repeat("a", 200) + ".txt" filename := filepath.Join(tmpDir, longName) buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines([]string{"content"}). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Verify file exists if _, err := os.Stat(filename); os.IsNotExist(err) { t.Error("file with long name not created") } }) t.Run("write buffer with unicode in filename", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "テスト_файл_测试.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines([]string{"unicode filename test"}). Build() m := newMockModelWithBuffer(&buf) cmdWrite(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Verify file exists and content is correct content, err := os.ReadFile(filename) if err != nil { t.Fatalf("file with unicode name not created: %v", err) } if string(content) != "unicode filename test\n" { t.Errorf("content = %q", string(content)) } }) t.Run("multiple sequential writes to same file", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "multi_write.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines([]string{"version 1"}). Build() m := newMockModelWithBuffer(&buf) // First write cmdWrite(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("first write failed: %v", m.commandOutput.Lines) } // Modify buffer buf.SetLine(0, "version 2") buf.SetModified(true) // Second write cmdWrite(m, []string{}, false) if m.commandOutput != nil && m.commandOutput.IsError { t.Fatalf("second write failed: %v", m.commandOutput.Lines) } // Verify final content content, _ := os.ReadFile(filename) if string(content) != "version 2\n" { t.Errorf("final content = %q", string(content)) } }) t.Run("force quit does not write", func(t *testing.T) { tmpDir := t.TempDir() filename := filepath.Join(tmpDir, "not_written.txt") buf := core.NewBufferBuilder(). WithType(core.FileBuffer). WithFilename(filename). WithLines([]string{"unsaved content"}). Modified(). Build() m := newMockModelWithBuffer(&buf) cmd := cmdQuit(m, []string{}, true) // Should quit without error if m.commandOutput != nil && m.commandOutput.IsError { t.Errorf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { t.Fatal("expected quit command") } // File should NOT be written if _, err := os.Stat(filename); !os.IsNotExist(err) { t.Error("file should not be written with force quit") } // Buffer should still be modified (not saved) if !buf.Modified { t.Error("buffer should still be modified (force quit doesn't save)") } }) }