package rig import ( "os" "path/filepath" "testing" ) func TestCopyOverlay_NoOverlayDirectory(t *testing.T) { tmpDir := t.TempDir() destDir := t.TempDir() // No overlay directory exists err := CopyOverlay(tmpDir, destDir) if err != nil { t.Errorf("CopyOverlay() with no overlay directory should return nil, got %v", err) } } func TestCopyOverlay_CopiesFiles(t *testing.T) { rigDir := t.TempDir() destDir := t.TempDir() // Create overlay directory with test files overlayDir := filepath.Join(rigDir, ".runtime", "overlay") if err := os.MkdirAll(overlayDir, 0755); err != nil { t.Fatalf("Failed to create overlay dir: %v", err) } // Create test files testFile1 := filepath.Join(overlayDir, "test1.txt") testFile2 := filepath.Join(overlayDir, "test2.txt") if err := os.WriteFile(testFile1, []byte("content1"), 0644); err != nil { t.Fatalf("Failed to create test file: %v", err) } if err := os.WriteFile(testFile2, []byte("content2"), 0644); err != nil { t.Fatalf("Failed to create test file: %v", err) } // Copy overlay err := CopyOverlay(rigDir, destDir) if err != nil { t.Fatalf("CopyOverlay() error = %v", err) } // Verify files were copied destFile1 := filepath.Join(destDir, "test1.txt") destFile2 := filepath.Join(destDir, "test2.txt") content1, err := os.ReadFile(destFile1) if err != nil { t.Errorf("File test1.txt was not copied: %v", err) } if string(content1) != "content1" { t.Errorf("test1.txt content = %q, want %q", string(content1), "content1") } content2, err := os.ReadFile(destFile2) if err != nil { t.Errorf("File test2.txt was not copied: %v", err) } if string(content2) != "content2" { t.Errorf("test2.txt content = %q, want %q", string(content2), "content2") } } func TestCopyOverlay_PreservesPermissions(t *testing.T) { rigDir := t.TempDir() destDir := t.TempDir() // Create overlay directory with a file overlayDir := filepath.Join(rigDir, ".runtime", "overlay") if err := os.MkdirAll(overlayDir, 0755); err != nil { t.Fatalf("Failed to create overlay dir: %v", err) } testFile := filepath.Join(overlayDir, "test.txt") if err := os.WriteFile(testFile, []byte("content"), 0755); err != nil { t.Fatalf("Failed to create test file: %v", err) } // Copy overlay err := CopyOverlay(rigDir, destDir) if err != nil { t.Fatalf("CopyOverlay() error = %v", err) } // Verify permissions were preserved srcInfo, _ := os.Stat(testFile) destInfo, err := os.Stat(filepath.Join(destDir, "test.txt")) if err != nil { t.Fatalf("Failed to stat destination file: %v", err) } if srcInfo.Mode().Perm() != destInfo.Mode().Perm() { t.Errorf("Permissions not preserved: src=%v, dest=%v", srcInfo.Mode(), destInfo.Mode()) } } func TestCopyOverlay_SkipsSubdirectories(t *testing.T) { rigDir := t.TempDir() destDir := t.TempDir() // Create overlay directory with a subdirectory overlayDir := filepath.Join(rigDir, ".runtime", "overlay") if err := os.MkdirAll(overlayDir, 0755); err != nil { t.Fatalf("Failed to create overlay dir: %v", err) } // Create a subdirectory subDir := filepath.Join(overlayDir, "subdir") if err := os.MkdirAll(subDir, 0755); err != nil { t.Fatalf("Failed to create subdirectory: %v", err) } // Create a file in the overlay root testFile := filepath.Join(overlayDir, "test.txt") if err := os.WriteFile(testFile, []byte("content"), 0644); err != nil { t.Fatalf("Failed to create test file: %v", err) } // Create a file in the subdirectory subFile := filepath.Join(subDir, "sub.txt") if err := os.WriteFile(subFile, []byte("subcontent"), 0644); err != nil { t.Fatalf("Failed to create sub file: %v", err) } // Copy overlay err := CopyOverlay(rigDir, destDir) if err != nil { t.Fatalf("CopyOverlay() error = %v", err) } // Verify root file was copied if _, err := os.Stat(filepath.Join(destDir, "test.txt")); err != nil { t.Error("Root file should be copied") } // Verify subdirectory was NOT copied if _, err := os.Stat(filepath.Join(destDir, "subdir")); err == nil { t.Error("Subdirectory should not be copied") } if _, err := os.Stat(filepath.Join(destDir, "subdir", "sub.txt")); err == nil { t.Error("File in subdirectory should not be copied") } } func TestCopyOverlay_EmptyOverlay(t *testing.T) { rigDir := t.TempDir() destDir := t.TempDir() // Create empty overlay directory overlayDir := filepath.Join(rigDir, ".runtime", "overlay") if err := os.MkdirAll(overlayDir, 0755); err != nil { t.Fatalf("Failed to create overlay dir: %v", err) } // Copy overlay err := CopyOverlay(rigDir, destDir) if err != nil { t.Fatalf("CopyOverlay() error = %v", err) } // Should succeed without errors } func TestCopyOverlay_OverwritesExisting(t *testing.T) { rigDir := t.TempDir() destDir := t.TempDir() // Create overlay directory with test file overlayDir := filepath.Join(rigDir, ".runtime", "overlay") if err := os.MkdirAll(overlayDir, 0755); err != nil { t.Fatalf("Failed to create overlay dir: %v", err) } testFile := filepath.Join(overlayDir, "test.txt") if err := os.WriteFile(testFile, []byte("new content"), 0644); err != nil { t.Fatalf("Failed to create test file: %v", err) } // Create existing file in destination with different content destFile := filepath.Join(destDir, "test.txt") if err := os.WriteFile(destFile, []byte("old content"), 0644); err != nil { t.Fatalf("Failed to create dest file: %v", err) } // Copy overlay err := CopyOverlay(rigDir, destDir) if err != nil { t.Fatalf("CopyOverlay() error = %v", err) } // Verify file was overwritten content, err := os.ReadFile(destFile) if err != nil { t.Fatalf("Failed to read dest file: %v", err) } if string(content) != "new content" { t.Errorf("File content = %q, want %q", string(content), "new content") } } func TestCopyFilePreserveMode(t *testing.T) { tmpDir := t.TempDir() // Create source file srcFile := filepath.Join(tmpDir, "src.txt") if err := os.WriteFile(srcFile, []byte("test content"), 0644); err != nil { t.Fatalf("Failed to create src file: %v", err) } // Copy file dstFile := filepath.Join(tmpDir, "dst.txt") err := copyFilePreserveMode(srcFile, dstFile) if err != nil { t.Fatalf("copyFilePreserveMode() error = %v", err) } // Verify content content, err := os.ReadFile(dstFile) if err != nil { t.Errorf("Failed to read dst file: %v", err) } if string(content) != "test content" { t.Errorf("Content = %q, want %q", string(content), "test content") } // Verify permissions srcInfo, _ := os.Stat(srcFile) dstInfo, err := os.Stat(dstFile) if err != nil { t.Fatalf("Failed to stat dst file: %v", err) } if srcInfo.Mode().Perm() != dstInfo.Mode().Perm() { t.Errorf("Permissions not preserved: src=%v, dest=%v", srcInfo.Mode(), dstInfo.Mode()) } } func TestCopyFilePreserveMode_NonexistentSource(t *testing.T) { tmpDir := t.TempDir() srcFile := filepath.Join(tmpDir, "nonexistent.txt") dstFile := filepath.Join(tmpDir, "dst.txt") err := copyFilePreserveMode(srcFile, dstFile) if err == nil { t.Error("copyFilePreserveMode() with nonexistent source should return error") } } func TestEnsureGitignorePatterns_CreatesNewFile(t *testing.T) { tmpDir := t.TempDir() err := EnsureGitignorePatterns(tmpDir) if err != nil { t.Fatalf("EnsureGitignorePatterns() error = %v", err) } content, err := os.ReadFile(filepath.Join(tmpDir, ".gitignore")) if err != nil { t.Fatalf("Failed to read .gitignore: %v", err) } // Check all required patterns are present patterns := []string{".runtime/", ".claude/", ".beads/", ".logs/"} for _, pattern := range patterns { if !containsLine(string(content), pattern) { t.Errorf(".gitignore missing pattern %q", pattern) } } } func TestEnsureGitignorePatterns_AppendsToExisting(t *testing.T) { tmpDir := t.TempDir() // Create existing .gitignore with some content existing := "node_modules/\n*.log\n" if err := os.WriteFile(filepath.Join(tmpDir, ".gitignore"), []byte(existing), 0644); err != nil { t.Fatalf("Failed to create .gitignore: %v", err) } err := EnsureGitignorePatterns(tmpDir) if err != nil { t.Fatalf("EnsureGitignorePatterns() error = %v", err) } content, err := os.ReadFile(filepath.Join(tmpDir, ".gitignore")) if err != nil { t.Fatalf("Failed to read .gitignore: %v", err) } // Should preserve existing content if !containsLine(string(content), "node_modules/") { t.Error("Existing pattern node_modules/ was removed") } // Should add header if !containsLine(string(content), "# Gas Town (added by gt)") { t.Error("Missing Gas Town header comment") } // Should add required patterns patterns := []string{".runtime/", ".claude/", ".beads/", ".logs/"} for _, pattern := range patterns { if !containsLine(string(content), pattern) { t.Errorf(".gitignore missing pattern %q", pattern) } } } func TestEnsureGitignorePatterns_SkipsExistingPatterns(t *testing.T) { tmpDir := t.TempDir() // Create existing .gitignore with some Gas Town patterns already existing := ".runtime/\n.claude/\n" if err := os.WriteFile(filepath.Join(tmpDir, ".gitignore"), []byte(existing), 0644); err != nil { t.Fatalf("Failed to create .gitignore: %v", err) } err := EnsureGitignorePatterns(tmpDir) if err != nil { t.Fatalf("EnsureGitignorePatterns() error = %v", err) } content, err := os.ReadFile(filepath.Join(tmpDir, ".gitignore")) if err != nil { t.Fatalf("Failed to read .gitignore: %v", err) } // Should not duplicate existing patterns count := countOccurrences(string(content), ".runtime/") if count != 1 { t.Errorf(".runtime/ appears %d times, expected 1", count) } // Should add missing patterns if !containsLine(string(content), ".beads/") { t.Error(".gitignore missing pattern .beads/") } if !containsLine(string(content), ".logs/") { t.Error(".gitignore missing pattern .logs/") } } func TestEnsureGitignorePatterns_RecognizesVariants(t *testing.T) { tmpDir := t.TempDir() // Create existing .gitignore with variant patterns (without trailing slash) existing := ".runtime\n/.claude\n" if err := os.WriteFile(filepath.Join(tmpDir, ".gitignore"), []byte(existing), 0644); err != nil { t.Fatalf("Failed to create .gitignore: %v", err) } err := EnsureGitignorePatterns(tmpDir) if err != nil { t.Fatalf("EnsureGitignorePatterns() error = %v", err) } content, err := os.ReadFile(filepath.Join(tmpDir, ".gitignore")) if err != nil { t.Fatalf("Failed to read .gitignore: %v", err) } // Should recognize variants and not add duplicates // .runtime (no slash) should count as .runtime/ if containsLine(string(content), ".runtime/") && containsLine(string(content), ".runtime") { // Only one should be present unless they're the same line runtimeCount := countOccurrences(string(content), ".runtime") if runtimeCount > 1 { t.Errorf(".runtime appears %d times (variant detection failed)", runtimeCount) } } } func TestEnsureGitignorePatterns_AllPatternsPresent(t *testing.T) { tmpDir := t.TempDir() // Create existing .gitignore with all required patterns existing := ".runtime/\n.claude/\n.beads/\n.logs/\n" if err := os.WriteFile(filepath.Join(tmpDir, ".gitignore"), []byte(existing), 0644); err != nil { t.Fatalf("Failed to create .gitignore: %v", err) } err := EnsureGitignorePatterns(tmpDir) if err != nil { t.Fatalf("EnsureGitignorePatterns() error = %v", err) } content, err := os.ReadFile(filepath.Join(tmpDir, ".gitignore")) if err != nil { t.Fatalf("Failed to read .gitignore: %v", err) } // File should be unchanged (no header added) if containsLine(string(content), "# Gas Town") { t.Error("Should not add header when all patterns already present") } // Content should match original if string(content) != existing { t.Errorf("File was modified when it shouldn't be.\nGot: %q\nWant: %q", string(content), existing) } } // Helper functions func containsLine(content, pattern string) bool { for _, line := range splitLines(content) { if line == pattern { return true } } return false } func countOccurrences(content, pattern string) int { count := 0 for _, line := range splitLines(content) { if line == pattern { count++ } } return count } func splitLines(content string) []string { var lines []string start := 0 for i, c := range content { if c == '\n' { lines = append(lines, content[start:i]) start = i + 1 } } if start < len(content) { lines = append(lines, content[start:]) } return lines }