working version
This commit is contained in:
@@ -1,215 +1,81 @@
|
||||
"""Tests for self-hosting flow config from dotfiles repository."""
|
||||
"""Tests for self-hosted merged YAML config loading."""
|
||||
|
||||
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"
|
||||
def mock_roots(tmp_path, monkeypatch):
|
||||
local_root = tmp_path / "local-flow"
|
||||
dotfiles_root = tmp_path / "dotfiles" / "_shared" / "flow" / ".config" / "flow"
|
||||
|
||||
config_dir.mkdir()
|
||||
dotfiles_dir.mkdir()
|
||||
local_root.mkdir(parents=True)
|
||||
dotfiles_root.mkdir(parents=True)
|
||||
|
||||
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",
|
||||
monkeypatch.setattr(paths_module, "CONFIG_DIR", local_root)
|
||||
monkeypatch.setattr(paths_module, "DOTFILES_FLOW_CONFIG", dotfiles_root)
|
||||
|
||||
return {
|
||||
"local": local_root,
|
||||
"dotfiles": dotfiles_root,
|
||||
}
|
||||
|
||||
# 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_roots):
|
||||
(mock_roots["local"] / "profiles.yaml").write_text("profiles:\n local: {os: linux}\n")
|
||||
(mock_roots["dotfiles"] / "profiles.yaml").write_text("profiles:\n dotfiles: {os: macos}\n")
|
||||
|
||||
|
||||
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")
|
||||
def test_load_manifest_fallback_to_local(mock_roots):
|
||||
(mock_roots["local"] / "profiles.yaml").write_text("profiles:\n local: {os: linux}\n")
|
||||
|
||||
# Remove dotfiles yaml file so local takes over.
|
||||
dot_yaml = mock_roots["dotfiles"] / "profiles.yaml"
|
||||
if dot_yaml.exists():
|
||||
dot_yaml.unlink()
|
||||
|
||||
# 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."""
|
||||
def test_load_manifest_empty_when_none_exist(mock_roots):
|
||||
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"
|
||||
def test_load_config_from_merged_yaml(mock_roots):
|
||||
(mock_roots["dotfiles"] / "config.yaml").write_text(
|
||||
"repository:\n"
|
||||
" dotfiles-url: git@github.com:user/dotfiles.git\n"
|
||||
"defaults:\n"
|
||||
" container-registry: registry.example.com\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
|
||||
cfg = load_config()
|
||||
assert cfg.dotfiles_url == "git@github.com:user/dotfiles.git"
|
||||
assert cfg.container_registry == "registry.example.com"
|
||||
|
||||
|
||||
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"
|
||||
)
|
||||
def test_yaml_merge_is_alphabetical_last_writer_wins(mock_roots):
|
||||
(mock_roots["local"] / "10-a.yaml").write_text("profiles:\n a: {os: linux}\n")
|
||||
(mock_roots["local"] / "20-b.yaml").write_text("profiles:\n b: {os: linux}\n")
|
||||
|
||||
# Dotfiles config doesn't exist
|
||||
config = load_config()
|
||||
assert "dotfiles-local" in config.dotfiles_url
|
||||
manifest = load_manifest(mock_roots["local"])
|
||||
assert "b" in manifest.get("profiles", {})
|
||||
assert "a" not in manifest.get("profiles", {})
|
||||
|
||||
|
||||
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_explicit_file_path_loads_single_yaml(tmp_path):
|
||||
one_file = tmp_path / "single.yaml"
|
||||
one_file.write_text("profiles:\n only: {os: linux}\n")
|
||||
|
||||
|
||||
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 == {}
|
||||
manifest = load_manifest(one_file)
|
||||
assert "only" in manifest["profiles"]
|
||||
|
||||
Reference in New Issue
Block a user