package editor import ( "testing" "time" "pad/internal/io/pool/mock" ) // TestEvictDirtyChunk_LosesEdits verifies that editing a chunk and then // evicting it does NOT lose the modification. Dirty chunks are protected // from eviction even when far from the cursor. func TestEvictDirtyChunk_LosesEdits(t *testing.T) { mockFS := mock.NewFileSystem() filename := "/test.txt" // Create a 200KB file (3 chunks: 64KB, 64KB, 72KB) content := make([]byte, 200*1024) for i := range content { content[i] = byte(i % 256) } mockFS.AddFile(filename, content, time.Now()) cb := NewChunkedBuffer(filename, DefaultChunkSize, mockFS, "") cb.SetFileSize(200 * 1024) cb.LoadChunk(0) // 1. Insert 100 bytes at position 0, modifying chunk 0 insertText := "MODIFIED" cb.Insert(0, insertText) // Verify the chunk grew if len(cb.chunks[0]) != DefaultChunkSize+len(insertText) { t.Fatalf("expected chunk 0 to grow to %d, got %d", DefaultChunkSize+len(insertText), len(cb.chunks[0])) } // 2. Simulate scrolling to chunk 2, which would normally trigger eviction of chunk 0 cb.EvictFarChunks(2*DefaultChunkSize, 1) // Chunk 0 should NOT be evicted because it's dirty if _, ok := cb.chunks[0]; !ok { t.Fatal("expected dirty chunk 0 to survive eviction") } // 3. Reconstruct the full file — the edit at position 0 must survive full, err := cb.FullContent() if err != nil { t.Fatalf("unexpected error: %v", err) } // The first len(insertText) bytes must be the inserted text if full[:len(insertText)] != insertText { t.Errorf("expected first %d bytes to be %q, got %q", len(insertText), insertText, full[:len(insertText)]) } } // TestEvictDirtyChunk_DeleteLosesEdits verifies that a deletion in a chunk // survives eviction. Dirty chunks are never evicted. func TestEvictDirtyChunk_DeleteLosesEdits(t *testing.T) { mockFS := mock.NewFileSystem() filename := "/test.txt" // Create a 128KB file (2 chunks of 64KB) content := make([]byte, 128*1024) for i := range content { content[i] = byte(i % 256) } mockFS.AddFile(filename, content, time.Now()) cb := NewChunkedBuffer(filename, DefaultChunkSize, mockFS, "") cb.SetFileSize(128 * 1024) cb.LoadChunk(0) cb.LoadChunk(1) // 1. Delete 100 bytes at position 127000 (in chunk 1) cb.Delete(127000, 100) // Verify chunk 1 shrunk if len(cb.chunks[1]) != DefaultChunkSize-100 { t.Fatalf("expected chunk 1 to shrink to %d, got %d", DefaultChunkSize-100, len(cb.chunks[1])) } // 2. Evict — chunk 1 should NOT be evicted because it's dirty cb.EvictFarChunks(0, 1) if _, ok := cb.chunks[1]; !ok { t.Fatal("expected dirty chunk 1 to survive eviction") } // 3. Reconstruct — the deletion must survive full, err := cb.FullContent() if err != nil { t.Fatalf("unexpected error: %v", err) } expectedLen := 128*1024 - 100 if len(full) != expectedLen { t.Errorf("expected file length %d after deletion, got %d", expectedLen, len(full)) } } // TestEvictCleanChunk_AllowsEviction verifies that chunks WITHOUT edits // CAN be evicted (i.e., dirty-chunk tracking doesn't prevent normal eviction). func TestEvictCleanChunk_AllowsEviction(t *testing.T) { mockFS := mock.NewFileSystem() filename := "/test.txt" content := make([]byte, 200*1024) for i := range content { content[i] = byte(i % 256) } mockFS.AddFile(filename, content, time.Now()) cb := NewChunkedBuffer(filename, DefaultChunkSize, mockFS, "") cb.SetFileSize(200 * 1024) cb.LoadChunk(0) cb.LoadChunk(1) cb.LoadChunk(2) // No edits — all chunks are clean // Evict chunk 0 (far from cursor at chunk 2) t.Logf("DEBUG: chunks before eviction: %v", cb.chunks) cb.EvictFarChunks(2*DefaultChunkSize, 1) t.Logf("DEBUG: chunks after eviction: %v", cb.chunks) // Chunk 0 should be evicted (it's clean) if _, evicted := cb.chunks[0]; evicted { t.Fatalf("expected clean chunk 0 to be evicted, but it still exists (chunks=%v)", cb.chunks) } // But chunks 1 and 2 should remain if _, ok := cb.chunks[1]; !ok { t.Fatal("expected chunk 1 to remain") } if _, ok := cb.chunks[2]; !ok { t.Fatal("expected chunk 2 to remain") } } // TestFullContent_AfterEvictDirtyChunk_ReReadsFromDisk verifies that when a dirty // chunk is somehow evicted (e.g., by a bug), FullContent re-reads the OLD disk // content — confirming the data loss scenario. This test documents the bug before // it is fixed. func TestFullContent_AfterEvictDirtyChunk_ReReadsFromDisk(t *testing.T) { mockFS := mock.NewFileSystem() filename := "/test.txt" // Create a small 100-byte file (1 chunk) content := []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") mockFS.AddFile(filename, content, time.Now()) cb := NewChunkedBuffer(filename, DefaultChunkSize, mockFS, "") cb.SetFileSize(100) cb.LoadChunk(0) // Insert at position 0 — modifies chunk 0 in memory cb.Insert(0, "X") // The in-memory chunk 0 should now be 101 bytes starting with "X" t.Logf("DEBUG: chunk 0 len=%d, first byte=%q, dirtyChunks=%v", len(cb.chunks[0]), cb.chunks[0][0], cb.dirtyChunks) if cb.chunks[0][0] != 'X' { t.Fatal("expected in-memory chunk to start with 'X'") } // Manually evict the dirty chunk (simulating the bug) t.Logf("DEBUG: before delete, chunks=%v", cb.chunks) delete(cb.chunks, 0) t.Logf("DEBUG: after delete, chunks=%v", cb.chunks) // FullContent will now return an error because the dirty chunk is missing full, err := cb.FullContent() if err == nil { t.Errorf("expected error due to missing dirty chunk, got nil, full content: %q", full) } else { t.Logf("DEBUG: caught expected error: %v", err) } }