package core import ( "fmt" "os" "os/exec" "path/filepath" "strings" "testing" "github.com/stretchr/testify/suite" ) type CoreTestSuite struct { suite.Suite tempDir string originalDir string lnk *Lnk } func (suite *CoreTestSuite) SetupTest() { // Create temporary directory for each test tempDir, err := os.MkdirTemp("", "lnk-test-*") suite.Require().NoError(err) suite.tempDir = tempDir // Change to temp directory originalDir, err := os.Getwd() suite.Require().NoError(err) suite.originalDir = originalDir err = os.Chdir(tempDir) suite.Require().NoError(err) // Set HOME to temp directory for consistent relative path calculation suite.T().Setenv("HOME", tempDir) // Set XDG_CONFIG_HOME to temp directory suite.T().Setenv("XDG_CONFIG_HOME", tempDir) // Initialize Lnk instance suite.lnk = NewLnk() } func (suite *CoreTestSuite) TearDownTest() { // Return to original directory err := os.Chdir(suite.originalDir) suite.Require().NoError(err) // Clean up temp directory err = os.RemoveAll(suite.tempDir) suite.Require().NoError(err) } // Test core initialization functionality func (suite *CoreTestSuite) TestCoreInit() { err := suite.lnk.Init() suite.Require().NoError(err) // Check that the lnk directory was created lnkDir := filepath.Join(suite.tempDir, "lnk") suite.DirExists(lnkDir) // Check that Git repo was initialized gitDir := filepath.Join(lnkDir, ".git") suite.DirExists(gitDir) } // Test core add/remove functionality with files func (suite *CoreTestSuite) TestCoreFileOperations() { // Initialize first err := suite.lnk.Init() suite.Require().NoError(err) // Create a test file testFile := filepath.Join(suite.tempDir, ".bashrc") content := "export PATH=$PATH:/usr/local/bin" err = os.WriteFile(testFile, []byte(content), 0644) suite.Require().NoError(err) // Add the file err = suite.lnk.Add(testFile) suite.Require().NoError(err) // Verify symlink and repo file info, err := os.Lstat(testFile) suite.Require().NoError(err) suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink) // The repository file will preserve the directory structure lnkDir := filepath.Join(suite.tempDir, "lnk") // Find the .bashrc file in the repository (it should be at the relative path from HOME) repoFile := filepath.Join(lnkDir, ".bashrc") suite.FileExists(repoFile) // Verify content is preserved repoContent, err := os.ReadFile(repoFile) suite.Require().NoError(err) suite.Equal(content, string(repoContent)) // Test remove err = suite.lnk.Remove(testFile) suite.Require().NoError(err) // Verify symlink is gone and regular file is restored info, err = os.Lstat(testFile) suite.Require().NoError(err) suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink) // Not a symlink // Verify content is preserved restoredContent, err := os.ReadFile(testFile) suite.Require().NoError(err) suite.Equal(content, string(restoredContent)) } // Test core add/remove functionality with directories func (suite *CoreTestSuite) TestCoreDirectoryOperations() { err := suite.lnk.Init() suite.Require().NoError(err) // Create a directory with files testDir := filepath.Join(suite.tempDir, "testdir") err = os.MkdirAll(testDir, 0755) suite.Require().NoError(err) testFile := filepath.Join(testDir, "config.txt") content := "test config" err = os.WriteFile(testFile, []byte(content), 0644) suite.Require().NoError(err) // Add the directory err = suite.lnk.Add(testDir) suite.Require().NoError(err) // Verify directory is now a symlink info, err := os.Lstat(testDir) suite.Require().NoError(err) suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink) // Check that the repository directory preserves the structure lnkDir := filepath.Join(suite.tempDir, "lnk") // The directory should be at the relative path from HOME repoDir := filepath.Join(lnkDir, "testdir") suite.DirExists(repoDir) // Remove the directory err = suite.lnk.Remove(testDir) suite.Require().NoError(err) // Verify symlink is gone and regular directory is restored info, err = os.Lstat(testDir) suite.Require().NoError(err) suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink) // Not a symlink suite.True(info.IsDir()) // Is a directory // Verify content is preserved restoredContent, err := os.ReadFile(testFile) suite.Require().NoError(err) suite.Equal(content, string(restoredContent)) } // Test .lnk file tracking functionality func (suite *CoreTestSuite) TestLnkFileTracking() { err := suite.lnk.Init() suite.Require().NoError(err) // Add multiple items testFile := filepath.Join(suite.tempDir, ".bashrc") err = os.WriteFile(testFile, []byte("export PATH=/usr/local/bin:$PATH"), 0644) suite.Require().NoError(err) err = suite.lnk.Add(testFile) suite.Require().NoError(err) testDir := filepath.Join(suite.tempDir, ".ssh") err = os.MkdirAll(testDir, 0700) suite.Require().NoError(err) configFile := filepath.Join(testDir, "config") err = os.WriteFile(configFile, []byte("Host example.com"), 0600) suite.Require().NoError(err) err = suite.lnk.Add(testDir) suite.Require().NoError(err) // Check .lnk file contains both entries lnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk") suite.FileExists(lnkFile) lnkContent, err := os.ReadFile(lnkFile) suite.Require().NoError(err) lines := strings.Split(strings.TrimSpace(string(lnkContent)), "\n") suite.Len(lines, 2) // The .lnk file now contains relative paths, not basenames // Check that the content contains references to .bashrc and .ssh content := string(lnkContent) suite.Contains(content, ".bashrc", ".lnk file should contain reference to .bashrc") suite.Contains(content, ".ssh", ".lnk file should contain reference to .ssh") // Remove one item and verify tracking is updated err = suite.lnk.Remove(testFile) suite.Require().NoError(err) lnkContent, err = os.ReadFile(lnkFile) suite.Require().NoError(err) lines = strings.Split(strings.TrimSpace(string(lnkContent)), "\n") suite.Len(lines, 1) content = string(lnkContent) suite.Contains(content, ".ssh", ".lnk file should still contain reference to .ssh") suite.NotContains(content, ".bashrc", ".lnk file should not contain reference to .bashrc after removal") } // Test XDG_CONFIG_HOME fallback func (suite *CoreTestSuite) TestXDGConfigHomeFallback() { // Test fallback to ~/.config/lnk when XDG_CONFIG_HOME is not set suite.T().Setenv("XDG_CONFIG_HOME", "") homeDir := filepath.Join(suite.tempDir, "home") err := os.MkdirAll(homeDir, 0755) suite.Require().NoError(err) suite.T().Setenv("HOME", homeDir) lnk := NewLnk() err = lnk.Init() suite.Require().NoError(err) // Check that the lnk directory was created under ~/.config/lnk expectedDir := filepath.Join(homeDir, ".config", "lnk") suite.DirExists(expectedDir) } // Test symlink restoration (pull functionality) func (suite *CoreTestSuite) TestSymlinkRestoration() { _ = suite.lnk.Init() // Create a file in the repo directly (simulating a pull) repoFile := filepath.Join(suite.tempDir, "lnk", ".bashrc") content := "export PATH=$PATH:/usr/local/bin" err := os.WriteFile(repoFile, []byte(content), 0644) suite.Require().NoError(err) // Create .lnk file to track it lnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk") err = os.WriteFile(lnkFile, []byte(".bashrc\n"), 0644) suite.Require().NoError(err) // Get home directory for the test homeDir, err := os.UserHomeDir() suite.Require().NoError(err) targetFile := filepath.Join(homeDir, ".bashrc") // Clean up the test file after the test defer func() { _ = os.Remove(targetFile) }() // Test symlink restoration restored, err := suite.lnk.RestoreSymlinks() suite.Require().NoError(err) // Should have restored the symlink suite.Len(restored, 1) suite.Equal(".bashrc", restored[0]) // Check that file is now a symlink info, err := os.Lstat(targetFile) suite.NoError(err) suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink) } // Test error conditions func (suite *CoreTestSuite) TestErrorConditions() { // Test add nonexistent file err := suite.lnk.Init() suite.Require().NoError(err) err = suite.lnk.Add("/nonexistent/file") suite.Error(err) suite.Contains(err.Error(), "File or directory not found") // Test remove unmanaged file testFile := filepath.Join(suite.tempDir, ".regularfile") err = os.WriteFile(testFile, []byte("content"), 0644) suite.Require().NoError(err) err = suite.lnk.Remove(testFile) suite.Error(err) suite.Contains(err.Error(), "File is not managed by lnk") // Test status without remote _, err = suite.lnk.Status() suite.Error(err) suite.Contains(err.Error(), "No remote repository is configured") } // Test git operations func (suite *CoreTestSuite) TestGitOperations() { err := suite.lnk.Init() suite.Require().NoError(err) // Add a file to create a commit testFile := filepath.Join(suite.tempDir, ".bashrc") content := "export PATH=$PATH:/usr/local/bin" err = os.WriteFile(testFile, []byte(content), 0644) suite.Require().NoError(err) err = suite.lnk.Add(testFile) suite.Require().NoError(err) // Check that Git commit was made commits, err := suite.lnk.GetCommits() suite.Require().NoError(err) suite.Len(commits, 1) suite.Contains(commits[0], "lnk: added .bashrc") // Test add remote err = suite.lnk.AddRemote("origin", "https://github.com/test/dotfiles.git") suite.Require().NoError(err) // Test status with remote status, err := suite.lnk.Status() suite.Require().NoError(err) suite.Equal(1, status.Ahead) suite.Equal(0, status.Behind) } // Test edge case: files with same basename from different directories should be handled properly func (suite *CoreTestSuite) TestSameBasenameFilesOverwrite() { err := suite.lnk.Init() suite.Require().NoError(err) // Create two directories with files having the same basename dirA := filepath.Join(suite.tempDir, "a") dirB := filepath.Join(suite.tempDir, "b") err = os.MkdirAll(dirA, 0755) suite.Require().NoError(err) err = os.MkdirAll(dirB, 0755) suite.Require().NoError(err) // Create files with same basename but different content fileA := filepath.Join(dirA, "config.json") fileB := filepath.Join(dirB, "config.json") contentA := `{"name": "config_a"}` contentB := `{"name": "config_b"}` err = os.WriteFile(fileA, []byte(contentA), 0644) suite.Require().NoError(err) err = os.WriteFile(fileB, []byte(contentB), 0644) suite.Require().NoError(err) // Add first file err = suite.lnk.Add(fileA) suite.Require().NoError(err) // Verify first file is managed correctly and preserves content info, err := os.Lstat(fileA) suite.Require().NoError(err) suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink) symlinkContentA, err := os.ReadFile(fileA) suite.Require().NoError(err) suite.Equal(contentA, string(symlinkContentA), "First file should preserve its original content") // Add second file - this should work without overwriting the first err = suite.lnk.Add(fileB) suite.Require().NoError(err) // Verify second file is managed info, err = os.Lstat(fileB) suite.Require().NoError(err) suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink) // CORRECT BEHAVIOR: Both files should preserve their original content symlinkContentA, err = os.ReadFile(fileA) suite.Require().NoError(err) symlinkContentB, err := os.ReadFile(fileB) suite.Require().NoError(err) suite.Equal(contentA, string(symlinkContentA), "First file should keep its original content") suite.Equal(contentB, string(symlinkContentB), "Second file should keep its original content") // Both files should be removable independently err = suite.lnk.Remove(fileA) suite.Require().NoError(err, "First file should be removable") // First file should be restored with correct content info, err = os.Lstat(fileA) suite.Require().NoError(err) suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink) // Not a symlink anymore restoredContentA, err := os.ReadFile(fileA) suite.Require().NoError(err) suite.Equal(contentA, string(restoredContentA), "Restored file should have original content") // Second file should still be manageable and removable err = suite.lnk.Remove(fileB) suite.Require().NoError(err, "Second file should also be removable without errors") // Second file should be restored with correct content info, err = os.Lstat(fileB) suite.Require().NoError(err) suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink) // Not a symlink anymore restoredContentB, err := os.ReadFile(fileB) suite.Require().NoError(err) suite.Equal(contentB, string(restoredContentB), "Second restored file should have original content") } // Test another variant: adding files with same basename should work correctly func (suite *CoreTestSuite) TestSameBasenameSequentialAdd() { err := suite.lnk.Init() suite.Require().NoError(err) // Create subdirectories in different locations configDir := filepath.Join(suite.tempDir, "config") backupDir := filepath.Join(suite.tempDir, "backup") err = os.MkdirAll(configDir, 0755) suite.Require().NoError(err) err = os.MkdirAll(backupDir, 0755) suite.Require().NoError(err) // Create files with same basename (.bashrc) configBashrc := filepath.Join(configDir, ".bashrc") backupBashrc := filepath.Join(backupDir, ".bashrc") originalContent := "export PATH=/usr/local/bin:$PATH" backupContent := "export PATH=/opt/bin:$PATH" err = os.WriteFile(configBashrc, []byte(originalContent), 0644) suite.Require().NoError(err) err = os.WriteFile(backupBashrc, []byte(backupContent), 0644) suite.Require().NoError(err) // Add first .bashrc err = suite.lnk.Add(configBashrc) suite.Require().NoError(err) // Add second .bashrc - should work without overwriting the first err = suite.lnk.Add(backupBashrc) suite.Require().NoError(err) // Check .lnk tracking file should track both properly lnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk") lnkContent, err := os.ReadFile(lnkFile) suite.Require().NoError(err) // Both entries should be tracked and distinguishable content := string(lnkContent) suite.Contains(content, ".bashrc", "Both .bashrc files should be tracked") // Both files should maintain their distinct content content1, err := os.ReadFile(configBashrc) suite.Require().NoError(err) content2, err := os.ReadFile(backupBashrc) suite.Require().NoError(err) suite.Equal(originalContent, string(content1), "First file should keep original content") suite.Equal(backupContent, string(content2), "Second file should keep its distinct content") // Both should be removable independently err = suite.lnk.Remove(configBashrc) suite.Require().NoError(err, "First .bashrc should be removable") err = suite.lnk.Remove(backupBashrc) suite.Require().NoError(err, "Second .bashrc should be removable") } // Test dirty repository status detection func (suite *CoreTestSuite) TestStatusDetectsDirtyRepo() { err := suite.lnk.Init() suite.Require().NoError(err) // Add and commit a file testFile := filepath.Join(suite.tempDir, "a") err = os.WriteFile(testFile, []byte("abc"), 0644) suite.Require().NoError(err) err = suite.lnk.Add(testFile) suite.Require().NoError(err) // Add a remote so status works err = suite.lnk.AddRemote("origin", "https://github.com/test/dotfiles.git") suite.Require().NoError(err) // Check status - should be clean but ahead of remote status, err := suite.lnk.Status() suite.Require().NoError(err) suite.Equal(1, status.Ahead) suite.Equal(0, status.Behind) suite.False(status.Dirty, "Repository should not be dirty after commit") // Now edit the managed file (simulating the issue scenario) err = os.WriteFile(testFile, []byte("def"), 0644) suite.Require().NoError(err) // Check status again - should detect dirty state status, err = suite.lnk.Status() suite.Require().NoError(err) suite.Equal(1, status.Ahead) suite.Equal(0, status.Behind) suite.True(status.Dirty, "Repository should be dirty after editing managed file") } // Test list functionality func (suite *CoreTestSuite) TestListManagedItems() { // Test list without init - should fail _, err := suite.lnk.List() suite.Error(err) suite.Contains(err.Error(), "Lnk repository not initialized") // Initialize repository err = suite.lnk.Init() suite.Require().NoError(err) // Test list with no managed files items, err := suite.lnk.List() suite.Require().NoError(err) suite.Empty(items) // Add a file testFile := filepath.Join(suite.tempDir, ".bashrc") content := "export PATH=$PATH:/usr/local/bin" err = os.WriteFile(testFile, []byte(content), 0644) suite.Require().NoError(err) err = suite.lnk.Add(testFile) suite.Require().NoError(err) // Test list with one managed file items, err = suite.lnk.List() suite.Require().NoError(err) suite.Len(items, 1) suite.Contains(items[0], ".bashrc") // Add a directory testDir := filepath.Join(suite.tempDir, ".config") err = os.MkdirAll(testDir, 0755) suite.Require().NoError(err) configFile := filepath.Join(testDir, "app.conf") err = os.WriteFile(configFile, []byte("setting=value"), 0644) suite.Require().NoError(err) err = suite.lnk.Add(testDir) suite.Require().NoError(err) // Test list with multiple managed items items, err = suite.lnk.List() suite.Require().NoError(err) suite.Len(items, 2) // Check that both items are present found := make(map[string]bool) for _, item := range items { if strings.Contains(item, ".bashrc") { found[".bashrc"] = true } if strings.Contains(item, ".config") { found[".config"] = true } } suite.True(found[".bashrc"], "Should contain .bashrc") suite.True(found[".config"], "Should contain .config") // Remove one item and verify list is updated err = suite.lnk.Remove(testFile) suite.Require().NoError(err) items, err = suite.lnk.List() suite.Require().NoError(err) suite.Len(items, 1) suite.Contains(items[0], ".config") } // Test multihost functionality func (suite *CoreTestSuite) TestMultihostFileOperations() { err := suite.lnk.Init() suite.Require().NoError(err) // Create test files for different hosts testFile1 := filepath.Join(suite.tempDir, ".bashrc") content1 := "export PATH=$PATH:/usr/local/bin" err = os.WriteFile(testFile1, []byte(content1), 0644) suite.Require().NoError(err) testFile2 := filepath.Join(suite.tempDir, ".vimrc") content2 := "set number" err = os.WriteFile(testFile2, []byte(content2), 0644) suite.Require().NoError(err) // Add file to common configuration commonLnk := NewLnk() err = commonLnk.Add(testFile1) suite.Require().NoError(err) // Add file to host-specific configuration hostLnk := NewLnk(WithHost("workstation")) err = hostLnk.Add(testFile2) suite.Require().NoError(err) // Verify both files are symlinks info1, err := os.Lstat(testFile1) suite.Require().NoError(err) suite.Equal(os.ModeSymlink, info1.Mode()&os.ModeSymlink) info2, err := os.Lstat(testFile2) suite.Require().NoError(err) suite.Equal(os.ModeSymlink, info2.Mode()&os.ModeSymlink) // Verify common configuration tracking commonItems, err := commonLnk.List() suite.Require().NoError(err) suite.Len(commonItems, 1) suite.Contains(commonItems[0], ".bashrc") // Verify host-specific configuration tracking hostItems, err := hostLnk.List() suite.Require().NoError(err) suite.Len(hostItems, 1) suite.Contains(hostItems[0], ".vimrc") // Verify files are stored in correct locations lnkDir := filepath.Join(suite.tempDir, "lnk") // Common file should be in root commonFile := filepath.Join(lnkDir, ".lnk") suite.FileExists(commonFile) // Host-specific file should be in host subdirectory hostDir := filepath.Join(lnkDir, "workstation.lnk") suite.DirExists(hostDir) hostTrackingFile := filepath.Join(lnkDir, ".lnk.workstation") suite.FileExists(hostTrackingFile) // Test removal err = commonLnk.Remove(testFile1) suite.Require().NoError(err) err = hostLnk.Remove(testFile2) suite.Require().NoError(err) // Verify files are restored info1, err = os.Lstat(testFile1) suite.Require().NoError(err) suite.Equal(os.FileMode(0), info1.Mode()&os.ModeSymlink) info2, err = os.Lstat(testFile2) suite.Require().NoError(err) suite.Equal(os.FileMode(0), info2.Mode()&os.ModeSymlink) } // Test hostname detection func (suite *CoreTestSuite) TestHostnameDetection() { hostname, err := GetCurrentHostname() suite.NoError(err) suite.NotEmpty(hostname) } // Test host-specific symlink restoration func (suite *CoreTestSuite) TestMultihostSymlinkRestoration() { err := suite.lnk.Init() suite.Require().NoError(err) // Create files directly in host-specific storage (simulating a pull) hostLnk := NewLnk(WithHost("testhost")) // Ensure host storage directory exists hostStoragePath := hostLnk.getHostStoragePath() err = os.MkdirAll(hostStoragePath, 0755) suite.Require().NoError(err) // Create a file in host storage repoFile := filepath.Join(hostStoragePath, ".bashrc") content := "export HOST=testhost" err = os.WriteFile(repoFile, []byte(content), 0644) suite.Require().NoError(err) // Create host tracking file trackingFile := filepath.Join(suite.tempDir, "lnk", ".lnk.testhost") err = os.WriteFile(trackingFile, []byte(".bashrc\n"), 0644) suite.Require().NoError(err) // Get home directory for the test homeDir, err := os.UserHomeDir() suite.Require().NoError(err) targetFile := filepath.Join(homeDir, ".bashrc") // Clean up the test file after the test defer func() { _ = os.Remove(targetFile) }() // Test symlink restoration restored, err := hostLnk.RestoreSymlinks() suite.Require().NoError(err) // Should have restored the symlink suite.Len(restored, 1) suite.Equal(".bashrc", restored[0]) // Check that file is now a symlink info, err := os.Lstat(targetFile) suite.NoError(err) suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink) } // Test that common and host-specific configurations don't interfere func (suite *CoreTestSuite) TestMultihostIsolation() { err := suite.lnk.Init() suite.Require().NoError(err) // Create same file for common and host-specific testFile := filepath.Join(suite.tempDir, ".gitconfig") commonContent := "[user]\n\tname = Common User" err = os.WriteFile(testFile, []byte(commonContent), 0644) suite.Require().NoError(err) // Add to common commonLnk := NewLnk() err = commonLnk.Add(testFile) suite.Require().NoError(err) // Remove and recreate with different content err = commonLnk.Remove(testFile) suite.Require().NoError(err) hostContent := "[user]\n\tname = Work User" err = os.WriteFile(testFile, []byte(hostContent), 0644) suite.Require().NoError(err) // Add to host-specific hostLnk := NewLnk(WithHost("work")) err = hostLnk.Add(testFile) suite.Require().NoError(err) // Verify tracking files are separate commonItems, err := commonLnk.List() suite.Require().NoError(err) suite.Len(commonItems, 0) // Should be empty after removal hostItems, err := hostLnk.List() suite.Require().NoError(err) suite.Len(hostItems, 1) suite.Contains(hostItems[0], ".gitconfig") // Verify content is correct symlinkContent, err := os.ReadFile(testFile) suite.Require().NoError(err) suite.Equal(hostContent, string(symlinkContent)) } // Test bootstrap script detection func (suite *CoreTestSuite) TestFindBootstrapScript() { err := suite.lnk.Init() suite.Require().NoError(err) // Test with no bootstrap script scriptPath, err := suite.lnk.FindBootstrapScript() suite.NoError(err) suite.Empty(scriptPath) // Test with bootstrap.sh bootstrapScript := filepath.Join(suite.tempDir, "lnk", "bootstrap.sh") err = os.WriteFile(bootstrapScript, []byte("#!/bin/bash\necho 'test'"), 0644) suite.Require().NoError(err) scriptPath, err = suite.lnk.FindBootstrapScript() suite.NoError(err) suite.Equal("bootstrap.sh", scriptPath) } // Test bootstrap script execution func (suite *CoreTestSuite) TestRunBootstrapScript() { err := suite.lnk.Init() suite.Require().NoError(err) // Create a test script that creates a marker file bootstrapScript := filepath.Join(suite.tempDir, "lnk", "test.sh") markerFile := filepath.Join(suite.tempDir, "lnk", "bootstrap-executed.txt") scriptContent := fmt.Sprintf("#!/bin/bash\ntouch %s\necho 'Bootstrap executed'", markerFile) err = os.WriteFile(bootstrapScript, []byte(scriptContent), 0755) suite.Require().NoError(err) // Run the bootstrap script err = suite.lnk.RunBootstrapScript("test.sh") suite.NoError(err) // Verify the marker file was created suite.FileExists(markerFile) } // Test bootstrap script execution with error func (suite *CoreTestSuite) TestRunBootstrapScriptWithError() { err := suite.lnk.Init() suite.Require().NoError(err) // Create a script that will fail bootstrapScript := filepath.Join(suite.tempDir, "lnk", "failing.sh") scriptContent := "#!/bin/bash\nexit 1" err = os.WriteFile(bootstrapScript, []byte(scriptContent), 0755) suite.Require().NoError(err) // Run the bootstrap script - should fail err = suite.lnk.RunBootstrapScript("failing.sh") suite.Error(err) suite.Contains(err.Error(), "Bootstrap script failed") } // Test running bootstrap on non-existent script func (suite *CoreTestSuite) TestRunBootstrapScriptNotFound() { err := suite.lnk.Init() suite.Require().NoError(err) // Try to run non-existent script err = suite.lnk.RunBootstrapScript("nonexistent.sh") suite.Error(err) suite.Contains(err.Error(), "Bootstrap script not found") } func (suite *CoreTestSuite) TestAddMultiple() { err := suite.lnk.Init() suite.Require().NoError(err) // Create multiple test files file1 := filepath.Join(suite.tempDir, "file1.txt") file2 := filepath.Join(suite.tempDir, "file2.txt") file3 := filepath.Join(suite.tempDir, "file3.txt") content1 := "content1" content2 := "content2" content3 := "content3" err = os.WriteFile(file1, []byte(content1), 0644) suite.Require().NoError(err) err = os.WriteFile(file2, []byte(content2), 0644) suite.Require().NoError(err) err = os.WriteFile(file3, []byte(content3), 0644) suite.Require().NoError(err) // Test AddMultiple method - should succeed paths := []string{file1, file2, file3} err = suite.lnk.AddMultiple(paths) suite.NoError(err, "AddMultiple should succeed") // Verify all files are now symlinks for _, file := range paths { info, err := os.Lstat(file) suite.NoError(err) suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink, "File should be a symlink: %s", file) } // Verify all files exist in storage lnkDir := filepath.Join(suite.tempDir, "lnk") suite.FileExists(filepath.Join(lnkDir, "file1.txt")) suite.FileExists(filepath.Join(lnkDir, "file2.txt")) suite.FileExists(filepath.Join(lnkDir, "file3.txt")) // Verify .lnk file contains all entries lnkFile := filepath.Join(lnkDir, ".lnk") lnkContent, err := os.ReadFile(lnkFile) suite.NoError(err) suite.Equal("file1.txt\nfile2.txt\nfile3.txt\n", string(lnkContent)) // Verify Git commit was created commits, err := suite.lnk.GetCommits() suite.NoError(err) suite.T().Logf("Commits: %v", commits) // Should have at least 1 commit for the batch add suite.GreaterOrEqual(len(commits), 1) // The most recent commit should mention multiple files suite.Contains(commits[0], "added 3 files") } func (suite *CoreTestSuite) TestAddMultipleWithConflicts() { err := suite.lnk.Init() suite.Require().NoError(err) // Create test files file1 := filepath.Join(suite.tempDir, "file1.txt") file2 := filepath.Join(suite.tempDir, "file2.txt") file3 := filepath.Join(suite.tempDir, "file3.txt") err = os.WriteFile(file1, []byte("content1"), 0644) suite.Require().NoError(err) err = os.WriteFile(file2, []byte("content2"), 0644) suite.Require().NoError(err) err = os.WriteFile(file3, []byte("content3"), 0644) suite.Require().NoError(err) // Add file2 individually first err = suite.lnk.Add(file2) suite.Require().NoError(err) // Now try to add all three - should fail due to conflict with file2 paths := []string{file1, file2, file3} err = suite.lnk.AddMultiple(paths) suite.Error(err, "AddMultiple should fail due to conflict") suite.Contains(err.Error(), "already managed") // Verify no partial changes were made // file1 and file3 should still be regular files, not symlinks info1, err := os.Lstat(file1) suite.NoError(err) suite.Equal(os.FileMode(0), info1.Mode()&os.ModeSymlink, "file1 should not be a symlink") info3, err := os.Lstat(file3) suite.NoError(err) suite.Equal(os.FileMode(0), info3.Mode()&os.ModeSymlink, "file3 should not be a symlink") // file2 should still be managed (was added before) info2, err := os.Lstat(file2) suite.NoError(err) suite.Equal(os.ModeSymlink, info2.Mode()&os.ModeSymlink, "file2 should remain a symlink") } func (suite *CoreTestSuite) TestAddMultipleRollback() { err := suite.lnk.Init() suite.Require().NoError(err) // Create test files - one will be invalid to force rollback file1 := filepath.Join(suite.tempDir, "file1.txt") file2 := filepath.Join(suite.tempDir, "nonexistent.txt") // This doesn't exist file3 := filepath.Join(suite.tempDir, "file3.txt") err = os.WriteFile(file1, []byte("content1"), 0644) suite.Require().NoError(err) err = os.WriteFile(file3, []byte("content3"), 0644) suite.Require().NoError(err) // Note: file2 is intentionally not created // Try to add all files - should fail and rollback paths := []string{file1, file2, file3} err = suite.lnk.AddMultiple(paths) suite.Error(err, "AddMultiple should fail due to nonexistent file") // Verify rollback - no files should be symlinks info1, err := os.Lstat(file1) suite.NoError(err) suite.Equal(os.FileMode(0), info1.Mode()&os.ModeSymlink, "file1 should not be a symlink after rollback") info3, err := os.Lstat(file3) suite.NoError(err) suite.Equal(os.FileMode(0), info3.Mode()&os.ModeSymlink, "file3 should not be a symlink after rollback") // Verify no files in storage lnkDir := filepath.Join(suite.tempDir, "lnk") suite.NoFileExists(filepath.Join(lnkDir, "file1.txt")) suite.NoFileExists(filepath.Join(lnkDir, "file3.txt")) // Verify .lnk file is empty or doesn't contain these files lnkFile := filepath.Join(lnkDir, ".lnk") if _, err := os.Stat(lnkFile); err == nil { lnkContent, err := os.ReadFile(lnkFile) suite.NoError(err) content := string(lnkContent) suite.NotContains(content, "file1.txt") suite.NotContains(content, "file3.txt") } } func (suite *CoreTestSuite) TestValidateMultiplePaths() { err := suite.lnk.Init() suite.Require().NoError(err) // Create a mix of valid and invalid paths validFile := filepath.Join(suite.tempDir, "valid.txt") err = os.WriteFile(validFile, []byte("content"), 0644) suite.Require().NoError(err) nonexistentFile := filepath.Join(suite.tempDir, "nonexistent.txt") // Don't create this file // Create a valid directory validDir := filepath.Join(suite.tempDir, "validdir") err = os.MkdirAll(validDir, 0755) suite.Require().NoError(err) // Test validation fails early with detailed error paths := []string{validFile, nonexistentFile, validDir} err = suite.lnk.AddMultiple(paths) suite.Error(err, "Should fail due to nonexistent file") suite.Contains(err.Error(), "validation failed") suite.Contains(err.Error(), "nonexistent.txt") // Verify no partial changes were made info, err := os.Lstat(validFile) suite.NoError(err) suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink, "valid file should not be a symlink") info, err = os.Lstat(validDir) suite.NoError(err) suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink, "valid directory should not be a symlink") } func (suite *CoreTestSuite) TestAtomicRollbackOnFailure() { err := suite.lnk.Init() suite.Require().NoError(err) // Create test files file1 := filepath.Join(suite.tempDir, "file1.txt") file2 := filepath.Join(suite.tempDir, "file2.txt") file3 := filepath.Join(suite.tempDir, "file3.txt") content1 := "original content 1" content2 := "original content 2" content3 := "original content 3" err = os.WriteFile(file1, []byte(content1), 0644) suite.Require().NoError(err) err = os.WriteFile(file2, []byte(content2), 0644) suite.Require().NoError(err) err = os.WriteFile(file3, []byte(content3), 0644) suite.Require().NoError(err) // Add file2 individually first to create a conflict err = suite.lnk.Add(file2) suite.Require().NoError(err) // Store original states info1Before, err := os.Lstat(file1) suite.Require().NoError(err) info3Before, err := os.Lstat(file3) suite.Require().NoError(err) // Try to add all files - should fail and rollback completely paths := []string{file1, file2, file3} err = suite.lnk.AddMultiple(paths) suite.Error(err, "Should fail due to conflict with file2") // Verify complete rollback info1After, err := os.Lstat(file1) suite.NoError(err) suite.Equal(info1Before.Mode(), info1After.Mode(), "file1 mode should be unchanged") info3After, err := os.Lstat(file3) suite.NoError(err) suite.Equal(info3Before.Mode(), info3After.Mode(), "file3 mode should be unchanged") // Verify original contents are preserved content1After, err := os.ReadFile(file1) suite.NoError(err) suite.Equal(content1, string(content1After), "file1 content should be preserved") content3After, err := os.ReadFile(file3) suite.NoError(err) suite.Equal(content3, string(content3After), "file3 content should be preserved") // file2 should still be managed (was added before) info2, err := os.Lstat(file2) suite.NoError(err) suite.Equal(os.ModeSymlink, info2.Mode()&os.ModeSymlink, "file2 should remain a symlink") } func (suite *CoreTestSuite) TestDetailedErrorMessages() { err := suite.lnk.Init() suite.Require().NoError(err) // Test with multiple types of errors validFile := filepath.Join(suite.tempDir, "valid.txt") err = os.WriteFile(validFile, []byte("content"), 0644) suite.Require().NoError(err) nonexistentFile := filepath.Join(suite.tempDir, "does-not-exist.txt") alreadyManagedFile := filepath.Join(suite.tempDir, "already-managed.txt") err = os.WriteFile(alreadyManagedFile, []byte("managed"), 0644) suite.Require().NoError(err) // Add one file first to create conflict err = suite.lnk.Add(alreadyManagedFile) suite.Require().NoError(err) // Test with nonexistent file paths := []string{validFile, nonexistentFile} err = suite.lnk.AddMultiple(paths) suite.Error(err, "Should fail due to nonexistent file") suite.Contains(err.Error(), "validation failed", "Error should mention validation failure") suite.Contains(err.Error(), "does-not-exist.txt", "Error should include specific filename") // Test with already managed file paths = []string{validFile, alreadyManagedFile} err = suite.lnk.AddMultiple(paths) suite.Error(err, "Should fail due to already managed file") suite.Contains(err.Error(), "already managed", "Error should mention already managed") suite.Contains(err.Error(), "already-managed.txt", "Error should include specific filename") } // Task 2.2: Directory Walking Logic Tests func (suite *CoreTestSuite) TestWalkDirectory() { // Initialize lnk repository err := suite.lnk.Init() suite.Require().NoError(err) // Create nested directory structure configDir := filepath.Join(suite.tempDir, ".config", "myapp") err = os.MkdirAll(configDir, 0755) suite.Require().NoError(err) themeDir := filepath.Join(configDir, "themes") err = os.MkdirAll(themeDir, 0755) suite.Require().NoError(err) // Create files in different levels file1 := filepath.Join(configDir, "config.json") file2 := filepath.Join(configDir, "settings.json") file3 := filepath.Join(themeDir, "dark.json") file4 := filepath.Join(themeDir, "light.json") suite.Require().NoError(os.WriteFile(file1, []byte("config"), 0644)) suite.Require().NoError(os.WriteFile(file2, []byte("settings"), 0644)) suite.Require().NoError(os.WriteFile(file3, []byte("dark theme"), 0644)) suite.Require().NoError(os.WriteFile(file4, []byte("light theme"), 0644)) // Call walkDirectory method (which doesn't exist yet) files, err := suite.lnk.walkDirectory(configDir) suite.Require().NoError(err, "walkDirectory should succeed") // Should find all 4 files suite.Len(files, 4, "Should find all files in nested structure") // Check that all expected files are found (order may vary) expectedFiles := []string{file1, file2, file3, file4} for _, expectedFile := range expectedFiles { suite.Contains(files, expectedFile, "Should include file %s", expectedFile) } } func (suite *CoreTestSuite) TestWalkDirectoryIncludesHiddenFiles() { // Initialize lnk repository err := suite.lnk.Init() suite.Require().NoError(err) // Create directory with hidden files and directories testDir := filepath.Join(suite.tempDir, "test-hidden") err = os.MkdirAll(testDir, 0755) suite.Require().NoError(err) hiddenDir := filepath.Join(testDir, ".hidden") err = os.MkdirAll(hiddenDir, 0755) suite.Require().NoError(err) // Create regular and hidden files regularFile := filepath.Join(testDir, "regular.txt") hiddenFile := filepath.Join(testDir, ".hidden-file") hiddenDirFile := filepath.Join(hiddenDir, "file-in-hidden.txt") suite.Require().NoError(os.WriteFile(regularFile, []byte("regular"), 0644)) suite.Require().NoError(os.WriteFile(hiddenFile, []byte("hidden"), 0644)) suite.Require().NoError(os.WriteFile(hiddenDirFile, []byte("in hidden dir"), 0644)) // Call walkDirectory method files, err := suite.lnk.walkDirectory(testDir) suite.Require().NoError(err, "walkDirectory should succeed with hidden files") // Should find all files including hidden ones suite.Len(files, 3, "Should find all files including hidden ones") suite.Contains(files, regularFile, "Should include regular file") suite.Contains(files, hiddenFile, "Should include hidden file") suite.Contains(files, hiddenDirFile, "Should include file in hidden directory") } func (suite *CoreTestSuite) TestWalkDirectorySymlinkHandling() { // Initialize lnk repository err := suite.lnk.Init() suite.Require().NoError(err) // Create directory structure testDir := filepath.Join(suite.tempDir, "test-symlinks") err = os.MkdirAll(testDir, 0755) suite.Require().NoError(err) // Create a regular file regularFile := filepath.Join(testDir, "regular.txt") suite.Require().NoError(os.WriteFile(regularFile, []byte("regular"), 0644)) // Create a symlink to the regular file symlinkFile := filepath.Join(testDir, "link-to-regular.txt") err = os.Symlink(regularFile, symlinkFile) suite.Require().NoError(err) // Call walkDirectory method files, err := suite.lnk.walkDirectory(testDir) suite.Require().NoError(err, "walkDirectory should handle symlinks") // Should include both regular file and properly handle symlink // (exact behavior depends on implementation - could include symlink as file) suite.GreaterOrEqual(len(files), 1, "Should find at least the regular file") suite.Contains(files, regularFile, "Should include regular file") // The symlink handling behavior will be defined in implementation // For now, we just ensure no errors occur } func (suite *CoreTestSuite) TestWalkDirectoryEmptyDirs() { // Initialize lnk repository err := suite.lnk.Init() suite.Require().NoError(err) // Create directory structure with empty directories testDir := filepath.Join(suite.tempDir, "test-empty") err = os.MkdirAll(testDir, 0755) suite.Require().NoError(err) // Create empty subdirectories emptyDir1 := filepath.Join(testDir, "empty1") emptyDir2 := filepath.Join(testDir, "empty2") err = os.MkdirAll(emptyDir1, 0755) suite.Require().NoError(err) err = os.MkdirAll(emptyDir2, 0755) suite.Require().NoError(err) // Create one file in a non-empty directory nonEmptyDir := filepath.Join(testDir, "non-empty") err = os.MkdirAll(nonEmptyDir, 0755) suite.Require().NoError(err) testFile := filepath.Join(nonEmptyDir, "test.txt") suite.Require().NoError(os.WriteFile(testFile, []byte("content"), 0644)) // Call walkDirectory method files, err := suite.lnk.walkDirectory(testDir) suite.Require().NoError(err, "walkDirectory should skip empty directories") // Should only find the one file, not empty directories suite.Len(files, 1, "Should only find files, not empty directories") suite.Contains(files, testFile, "Should include the actual file") } // Task 2.3: Progress Indication System Tests func (suite *CoreTestSuite) TestProgressReporting() { // Initialize lnk repository err := suite.lnk.Init() suite.Require().NoError(err) // Create directory with multiple files to test progress reporting testDir := filepath.Join(suite.tempDir, "progress-test") err = os.MkdirAll(testDir, 0755) suite.Require().NoError(err) // Create 15 files to exceed threshold expectedFiles := 15 for i := 0; i < expectedFiles; i++ { file := filepath.Join(testDir, fmt.Sprintf("file%d.txt", i)) suite.Require().NoError(os.WriteFile(file, []byte(fmt.Sprintf("content %d", i)), 0644)) } // Track progress calls var progressCalls []struct { Current int Total int CurrentFile string } progressCallback := func(current, total int, currentFile string) { progressCalls = append(progressCalls, struct { Current int Total int CurrentFile string }{ Current: current, Total: total, CurrentFile: currentFile, }) } // Call AddRecursiveWithProgress method (which doesn't exist yet) err = suite.lnk.AddRecursiveWithProgress([]string{testDir}, progressCallback) suite.Require().NoError(err, "AddRecursiveWithProgress should succeed") // Verify progress was reported suite.Greater(len(progressCalls), 0, "Progress callback should be called") suite.Equal(expectedFiles, len(progressCalls), "Should have progress calls for each file") // Verify progress order and totals for i, call := range progressCalls { suite.Equal(i+1, call.Current, "Current count should increment") suite.Equal(expectedFiles, call.Total, "Total should be consistent") suite.NotEmpty(call.CurrentFile, "CurrentFile should be provided") } } func (suite *CoreTestSuite) TestProgressThreshold() { // Initialize lnk repository err := suite.lnk.Init() suite.Require().NoError(err) // Test with few files (under threshold) smallDir := filepath.Join(suite.tempDir, "small-test") err = os.MkdirAll(smallDir, 0755) suite.Require().NoError(err) // Create only 5 files (under 10 threshold) for i := 0; i < 5; i++ { file := filepath.Join(smallDir, fmt.Sprintf("small%d.txt", i)) suite.Require().NoError(os.WriteFile(file, []byte(fmt.Sprintf("content %d", i)), 0644)) } // Track progress calls for small operation smallProgressCalls := 0 smallCallback := func(current, total int, currentFile string) { smallProgressCalls++ } err = suite.lnk.AddRecursiveWithProgress([]string{smallDir}, smallCallback) suite.Require().NoError(err, "AddRecursiveWithProgress should succeed for small operation") // Should NOT call progress for small operations suite.Equal(0, smallProgressCalls, "Progress should not be called for operations under threshold") // Test with many files (over threshold) largeDir := filepath.Join(suite.tempDir, "large-test") err = os.MkdirAll(largeDir, 0755) suite.Require().NoError(err) // Create 15 files (over 10 threshold) for i := 0; i < 15; i++ { file := filepath.Join(largeDir, fmt.Sprintf("large%d.txt", i)) suite.Require().NoError(os.WriteFile(file, []byte(fmt.Sprintf("content %d", i)), 0644)) } // Track progress calls for large operation largeProgressCalls := 0 largeCallback := func(current, total int, currentFile string) { largeProgressCalls++ } err = suite.lnk.AddRecursiveWithProgress([]string{largeDir}, largeCallback) suite.Require().NoError(err, "AddRecursiveWithProgress should succeed for large operation") // Should call progress for large operations suite.Equal(15, largeProgressCalls, "Progress should be called for operations over threshold") } // Task 3.1: Dry-Run Mode Core Tests func (suite *CoreTestSuite) TestPreviewAdd() { // Initialize lnk repository err := suite.lnk.Init() suite.Require().NoError(err) // Create test files testFile1 := filepath.Join(suite.tempDir, "test1.txt") testFile2 := filepath.Join(suite.tempDir, "test2.txt") suite.Require().NoError(os.WriteFile(testFile1, []byte("content1"), 0644)) suite.Require().NoError(os.WriteFile(testFile2, []byte("content2"), 0644)) // Test PreviewAdd for multiple files files, err := suite.lnk.PreviewAdd([]string{testFile1, testFile2}, false) suite.Require().NoError(err, "PreviewAdd should succeed") // Should return both files suite.Len(files, 2, "Should preview both files") suite.Contains(files, testFile1, "Should include first file") suite.Contains(files, testFile2, "Should include second file") // Verify no actual changes were made (files should still be regular files) info, err := os.Lstat(testFile1) suite.NoError(err) suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink, "File should not be symlink after preview") info, err = os.Lstat(testFile2) suite.NoError(err) suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink, "File should not be symlink after preview") } func (suite *CoreTestSuite) TestPreviewAddRecursive() { // Initialize lnk repository err := suite.lnk.Init() suite.Require().NoError(err) // Create directory structure configDir := filepath.Join(suite.tempDir, ".config", "test-app") err = os.MkdirAll(configDir, 0755) suite.Require().NoError(err) // Create files in directory expectedFiles := 5 var createdFiles []string for i := 1; i <= expectedFiles; i++ { file := filepath.Join(configDir, fmt.Sprintf("config%d.json", i)) suite.Require().NoError(os.WriteFile(file, []byte(fmt.Sprintf("config %d", i)), 0644)) createdFiles = append(createdFiles, file) } // Test PreviewAdd with recursive files, err := suite.lnk.PreviewAdd([]string{configDir}, true) suite.Require().NoError(err, "PreviewAdd recursive should succeed") // Should return all files in directory suite.Len(files, expectedFiles, "Should preview all files in directory") // Check that all created files are included for _, createdFile := range createdFiles { suite.Contains(files, createdFile, "Should include file %s", createdFile) } // Verify no actual changes were made for _, createdFile := range createdFiles { info, err := os.Lstat(createdFile) suite.NoError(err) suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink, "File should not be symlink after preview") } } func (suite *CoreTestSuite) TestPreviewAddValidation() { // Initialize lnk repository err := suite.lnk.Init() suite.Require().NoError(err) // Test with nonexistent file nonexistentFile := filepath.Join(suite.tempDir, "nonexistent.txt") _, err = suite.lnk.PreviewAdd([]string{nonexistentFile}, false) suite.Error(err, "PreviewAdd should fail for nonexistent file") suite.Contains(err.Error(), "failed to stat", "Error should mention stat failure") // Create and add a file first testFile := filepath.Join(suite.tempDir, "test.txt") suite.Require().NoError(os.WriteFile(testFile, []byte("content"), 0644)) err = suite.lnk.Add(testFile) suite.Require().NoError(err) // Test preview with already managed file _, err = suite.lnk.PreviewAdd([]string{testFile}, false) suite.Error(err, "PreviewAdd should fail for already managed file") suite.Contains(err.Error(), "already managed", "Error should mention already managed") } // Task 1.1: Tests for HasUserContent() method func (suite *CoreTestSuite) TestHasUserContent_WithCommonTracker_ReturnsTrue() { // Initialize lnk repository err := suite.lnk.Init() suite.Require().NoError(err) // Create .lnk file to simulate existing content lnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk") err = os.WriteFile(lnkFile, []byte(".bashrc\n"), 0644) suite.Require().NoError(err) // Call HasUserContent() hasContent := suite.lnk.HasUserContent() suite.True(hasContent, "Should detect common tracker file") } func (suite *CoreTestSuite) TestHasUserContent_WithHostTracker_ReturnsTrue() { // Initialize lnk repository err := suite.lnk.Init() suite.Require().NoError(err) // Create host-specific lnk instance hostLnk := NewLnk(WithHost("testhost")) // Create .lnk.hostname file to simulate host-specific content lnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk.testhost") err = os.WriteFile(lnkFile, []byte(".vimrc\n"), 0644) suite.Require().NoError(err) // Call HasUserContent() hasContent := hostLnk.HasUserContent() suite.True(hasContent, "Should detect host-specific tracker file") } func (suite *CoreTestSuite) TestHasUserContent_WithBothTrackers_ReturnsTrue() { // Initialize lnk repository err := suite.lnk.Init() suite.Require().NoError(err) // Create both common and host-specific tracker files commonLnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk") err = os.WriteFile(commonLnkFile, []byte(".bashrc\n"), 0644) suite.Require().NoError(err) hostLnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk.testhost") err = os.WriteFile(hostLnkFile, []byte(".vimrc\n"), 0644) suite.Require().NoError(err) // Test with common instance hasContent := suite.lnk.HasUserContent() suite.True(hasContent, "Should detect common tracker file") // Test with host-specific instance hostLnk := NewLnk(WithHost("testhost")) hasContent = hostLnk.HasUserContent() suite.True(hasContent, "Should detect host-specific tracker file") } func (suite *CoreTestSuite) TestHasUserContent_EmptyDirectory_ReturnsFalse() { // Initialize lnk repository err := suite.lnk.Init() suite.Require().NoError(err) // Call HasUserContent() on empty repository hasContent := suite.lnk.HasUserContent() suite.False(hasContent, "Should return false for empty repository") } func (suite *CoreTestSuite) TestHasUserContent_NonTrackerFiles_ReturnsFalse() { // Initialize lnk repository err := suite.lnk.Init() suite.Require().NoError(err) // Create non-tracker files randomFile := filepath.Join(suite.tempDir, "lnk", "random.txt") err = os.WriteFile(randomFile, []byte("some content"), 0644) suite.Require().NoError(err) configFile := filepath.Join(suite.tempDir, "lnk", ".gitignore") err = os.WriteFile(configFile, []byte("*.log"), 0644) suite.Require().NoError(err) // Call HasUserContent() hasContent := suite.lnk.HasUserContent() suite.False(hasContent, "Should return false when only non-tracker files exist") } // Task 2.1: Tests for enhanced InitWithRemote() safety check func (suite *CoreTestSuite) TestInitWithRemote_HasUserContent_ReturnsError() { // Initialize and add content first err := suite.lnk.Init() suite.Require().NoError(err) // Create .lnk file to simulate existing content lnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk") err = os.WriteFile(lnkFile, []byte(".bashrc\n"), 0644) suite.Require().NoError(err) // Try InitWithRemote - should fail err = suite.lnk.InitWithRemote("https://github.com/test/dotfiles.git") suite.Error(err, "Should fail when user content exists") suite.Contains(err.Error(), "already contains managed files") suite.Contains(err.Error(), "lnk pull") // Verify .lnk file still exists (no deletion occurred) suite.FileExists(lnkFile) } func (suite *CoreTestSuite) TestInitWithRemote_EmptyDirectory_Success() { // Create a dummy remote directory for testing remoteDir := filepath.Join(suite.tempDir, "remote") err := os.MkdirAll(remoteDir, 0755) suite.Require().NoError(err) // Initialize a bare git repository as remote cmd := exec.Command("git", "init", "--bare") cmd.Dir = remoteDir err = cmd.Run() suite.Require().NoError(err) // InitWithRemote should succeed on empty directory err = suite.lnk.InitWithRemote(remoteDir) suite.NoError(err, "Should succeed when no user content exists") // Verify repository was cloned lnkDir := filepath.Join(suite.tempDir, "lnk") suite.DirExists(lnkDir) gitDir := filepath.Join(lnkDir, ".git") suite.DirExists(gitDir) } func (suite *CoreTestSuite) TestInitWithRemote_NoRemoteURL_BypassesSafetyCheck() { // Initialize and add content first err := suite.lnk.Init() suite.Require().NoError(err) // Create .lnk file to simulate existing content lnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk") err = os.WriteFile(lnkFile, []byte(".bashrc\n"), 0644) suite.Require().NoError(err) // InitWithRemote with empty URL should bypass safety check (this is local init) err = suite.lnk.InitWithRemote("") suite.NoError(err, "Should bypass safety check when no remote URL provided") } func (suite *CoreTestSuite) TestInitWithRemote_ErrorMessage_ContainsSuggestedCommand() { // Initialize and add content first err := suite.lnk.Init() suite.Require().NoError(err) // Create host-specific content hostLnk := NewLnk(WithHost("testhost")) hostLnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk.testhost") err = os.WriteFile(hostLnkFile, []byte(".vimrc\n"), 0644) suite.Require().NoError(err) // Try InitWithRemote - should fail with helpful message err = hostLnk.InitWithRemote("https://github.com/test/dotfiles.git") suite.Error(err, "Should fail when user content exists") suite.Contains(err.Error(), "lnk pull", "Should suggest pull command") suite.Contains(err.Error(), "instead of", "Should explain alternative") } func TestCoreSuite(t *testing.T) { suite.Run(t, new(CoreTestSuite)) }