working version

This commit is contained in:
2026-02-13 12:15:46 +02:00
parent 1217337fbb
commit 6cff65f288
37 changed files with 2232 additions and 1872 deletions

View File

@@ -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"]