"""Tests for self-hosting flow config from dotfiles repository.""" from pathlib import Path from unittest.mock import patch import pytest import yaml from flow.core import paths as paths_module from flow.core.config import load_config, load_manifest @pytest.fixture def mock_paths(tmp_path, monkeypatch): """Mock path constants for testing.""" config_dir = tmp_path / "config" dotfiles_dir = tmp_path / "dotfiles" config_dir.mkdir() dotfiles_dir.mkdir() test_paths = { "config_dir": config_dir, "dotfiles_dir": dotfiles_dir, "local_config": config_dir / "config", "local_manifest": config_dir / "manifest.yaml", "dotfiles_config": dotfiles_dir / "flow" / ".config" / "flow" / "config", "dotfiles_manifest": dotfiles_dir / "flow" / ".config" / "flow" / "manifest.yaml", } # Patch at the paths module level monkeypatch.setattr(paths_module, "CONFIG_FILE", test_paths["local_config"]) monkeypatch.setattr(paths_module, "MANIFEST_FILE", test_paths["local_manifest"]) monkeypatch.setattr(paths_module, "DOTFILES_CONFIG", test_paths["dotfiles_config"]) monkeypatch.setattr(paths_module, "DOTFILES_MANIFEST", test_paths["dotfiles_manifest"]) return test_paths def test_load_manifest_priority_dotfiles_first(mock_paths): """Test that dotfiles manifest takes priority over local.""" # Create both manifests local_manifest = mock_paths["local_manifest"] dotfiles_manifest = mock_paths["dotfiles_manifest"] local_manifest.write_text("profiles:\n local:\n os: linux") dotfiles_manifest.parent.mkdir(parents=True) dotfiles_manifest.write_text("profiles:\n dotfiles:\n os: macos") # Should load from dotfiles manifest = load_manifest() assert "dotfiles" in manifest.get("profiles", {}) assert "local" not in manifest.get("profiles", {}) def test_load_manifest_fallback_to_local(mock_paths): """Test fallback to local manifest when dotfiles doesn't exist.""" local_manifest = mock_paths["local_manifest"] local_manifest.write_text("profiles:\n local:\n os: linux") # Dotfiles manifest doesn't exist manifest = load_manifest() assert "local" in manifest.get("profiles", {}) def test_load_manifest_empty_when_none_exist(mock_paths): """Test empty dict returned when no manifests exist.""" manifest = load_manifest() assert manifest == {} def test_load_config_priority_dotfiles_first(mock_paths): """Test that dotfiles config takes priority over local.""" local_config = mock_paths["local_config"] dotfiles_config = mock_paths["dotfiles_config"] # Create local config local_config.write_text( "[repository]\n" "dotfiles_url = https://github.com/user/dotfiles-local.git\n" ) # Create dotfiles config dotfiles_config.parent.mkdir(parents=True) dotfiles_config.write_text( "[repository]\n" "dotfiles_url = https://github.com/user/dotfiles-from-repo.git\n" ) # Should load from dotfiles config = load_config() assert "dotfiles-from-repo" in config.dotfiles_url def test_load_config_fallback_to_local(mock_paths): """Test fallback to local config when dotfiles doesn't exist.""" local_config = mock_paths["local_config"] local_config.write_text( "[repository]\n" "dotfiles_url = https://github.com/user/dotfiles-local.git\n" ) # Dotfiles config doesn't exist config = load_config() assert "dotfiles-local" in config.dotfiles_url def test_load_config_empty_when_none_exist(mock_paths): """Test default config returned when no configs exist.""" config = load_config() assert config.dotfiles_url == "" assert config.dotfiles_branch == "main" def test_self_hosting_workflow(tmp_path, monkeypatch): """Test complete self-hosting workflow. Simulates: 1. User has dotfiles repo with flow config 2. Flow links its own config from dotfiles 3. Flow reads from self-hosted location """ # Setup paths home = tmp_path / "home" dotfiles = tmp_path / "dotfiles" home.mkdir() dotfiles.mkdir() # Create flow package in dotfiles flow_pkg = dotfiles / "flow" / ".config" / "flow" flow_pkg.mkdir(parents=True) # Create manifest in dotfiles manifest_content = { "profiles": { "test-env": { "os": "linux", "packages": {"standard": ["git", "vim"]}, } } } (flow_pkg / "manifest.yaml").write_text(yaml.dump(manifest_content)) # Create config in dotfiles (flow_pkg / "config").write_text( "[repository]\n" "dotfiles_url = https://github.com/user/dotfiles.git\n" ) # Mock paths to use our temp directories monkeypatch.setattr(paths_module, "DOTFILES_MANIFEST", flow_pkg / "manifest.yaml") monkeypatch.setattr(paths_module, "DOTFILES_CONFIG", flow_pkg / "config") monkeypatch.setattr(paths_module, "MANIFEST_FILE", home / ".config" / "devflow" / "manifest.yaml") monkeypatch.setattr(paths_module, "CONFIG_FILE", home / ".config" / "devflow" / "config") # Load config and manifest - should come from dotfiles manifest = load_manifest() config = load_config() assert "test-env" in manifest.get("profiles", {}) assert "github.com/user/dotfiles.git" in config.dotfiles_url def test_manifest_cascade_with_symlink(tmp_path, monkeypatch): """Test that loading works correctly when symlink is used.""" # Setup dotfiles = tmp_path / "dotfiles" home_config = tmp_path / "home" / ".config" / "flow" flow_pkg = dotfiles / "flow" / ".config" / "flow" flow_pkg.mkdir(parents=True) home_config.mkdir(parents=True) # Create manifest in dotfiles manifest_content = {"profiles": {"from-dotfiles": {"os": "linux"}}} (flow_pkg / "manifest.yaml").write_text(yaml.dump(manifest_content)) # Create symlink from home config to dotfiles manifest_link = home_config / "manifest.yaml" manifest_link.symlink_to(flow_pkg / "manifest.yaml") # Mock paths monkeypatch.setattr(paths_module, "DOTFILES_MANIFEST", flow_pkg / "manifest.yaml") monkeypatch.setattr(paths_module, "MANIFEST_FILE", manifest_link) # Load - should work through symlink manifest = load_manifest() assert "from-dotfiles" in manifest.get("profiles", {}) def test_config_priority_documentation(mock_paths): """Document the config loading priority for users.""" # This test serves as documentation of the cascade behavior # Priority 1: Dotfiles repo (self-hosted) dotfiles_manifest = mock_paths["dotfiles_manifest"] dotfiles_manifest.parent.mkdir(parents=True) dotfiles_manifest.write_text("profiles:\n priority-1: {}") manifest = load_manifest() assert "priority-1" in manifest.get("profiles", {}) # If we remove dotfiles, falls back to Priority 2: Local override dotfiles_manifest.unlink() local_manifest = mock_paths["local_manifest"] local_manifest.write_text("profiles:\n priority-2: {}") manifest = load_manifest() assert "priority-2" in manifest.get("profiles", {}) # If neither exists, Priority 3: Empty fallback local_manifest.unlink() manifest = load_manifest() assert manifest == {}