refactor-1
This commit is contained in:
@@ -62,12 +62,12 @@ def test_resolve_package_manager_explicit_value(ctx):
|
||||
|
||||
|
||||
def test_resolve_package_manager_linux_auto_apt(monkeypatch, ctx):
|
||||
monkeypatch.setattr("flow.commands.bootstrap.shutil.which", lambda name: "/usr/bin/apt" if name == "apt" else None)
|
||||
monkeypatch.setattr("flow.services.bootstrap.shutil.which", lambda name: "/usr/bin/apt" if name == "apt" else None)
|
||||
assert _resolve_package_manager(ctx, {"os": "linux"}) == "apt"
|
||||
|
||||
|
||||
def test_resolve_package_manager_linux_auto_dnf(monkeypatch, ctx):
|
||||
monkeypatch.setattr("flow.commands.bootstrap.shutil.which", lambda name: "/usr/bin/dnf" if name == "dnf" else None)
|
||||
monkeypatch.setattr("flow.services.bootstrap.shutil.which", lambda name: "/usr/bin/dnf" if name == "dnf" else None)
|
||||
assert _resolve_package_manager(ctx, {"os": "linux"}) == "dnf"
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ class _FakeResponse:
|
||||
|
||||
def _patch_binary_download(monkeypatch, after_unpack=None):
|
||||
monkeypatch.setattr(
|
||||
"flow.commands.bootstrap.urllib.request.urlopen",
|
||||
"flow.services.bootstrap.urllib.request.urlopen",
|
||||
lambda *args, **kwargs: _FakeResponse(),
|
||||
)
|
||||
|
||||
@@ -168,7 +168,7 @@ def _patch_binary_download(monkeypatch, after_unpack=None):
|
||||
if after_unpack:
|
||||
after_unpack(extracted)
|
||||
|
||||
monkeypatch.setattr("flow.commands.bootstrap.shutil.unpack_archive", _fake_unpack)
|
||||
monkeypatch.setattr("flow.services.bootstrap.shutil.unpack_archive", _fake_unpack)
|
||||
|
||||
|
||||
def test_install_binary_package_rejects_absolute_declared_path(monkeypatch, tmp_path, ctx):
|
||||
@@ -177,7 +177,7 @@ def test_install_binary_package_rejects_absolute_declared_path(monkeypatch, tmp_
|
||||
|
||||
_patch_binary_download(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
"flow.commands.bootstrap._copy_install_item",
|
||||
"flow.services.bootstrap._copy_install_item",
|
||||
lambda *args, **kwargs: pytest.fail("_copy_install_item should not be called"),
|
||||
)
|
||||
|
||||
@@ -199,7 +199,7 @@ def test_install_binary_package_rejects_parent_traversal_declared_path(monkeypat
|
||||
|
||||
_patch_binary_download(monkeypatch, after_unpack=_after_unpack)
|
||||
monkeypatch.setattr(
|
||||
"flow.commands.bootstrap._copy_install_item",
|
||||
"flow.services.bootstrap._copy_install_item",
|
||||
lambda *args, **kwargs: pytest.fail("_copy_install_item should not be called"),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Tests for flow.commands.dotfiles discovery and path resolution."""
|
||||
"""Tests for flow.services.dotfiles discovery and path resolution."""
|
||||
|
||||
import pytest
|
||||
|
||||
from flow.commands.dotfiles import _collect_home_specs, _discover_packages, _resolve_edit_target, _walk_package
|
||||
from flow.services.dotfiles import _collect_home_specs, _discover_packages, _resolve_edit_target, _walk_package
|
||||
from flow.core.config import AppConfig, FlowContext
|
||||
from flow.core.console import ConsoleLogger
|
||||
from flow.core.platform import PlatformInfo
|
||||
|
||||
@@ -15,12 +15,12 @@ import pytest
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _docker_available() -> bool:
|
||||
if shutil.which("docker") is None:
|
||||
def _runtime_available(runtime: str) -> bool:
|
||||
if shutil.which(runtime) is None:
|
||||
return False
|
||||
|
||||
result = subprocess.run(
|
||||
["docker", "info"],
|
||||
[runtime, "info"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
@@ -28,16 +28,36 @@ def _docker_available() -> bool:
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def _require_container_e2e() -> None:
|
||||
def _container_runtime() -> str | None:
|
||||
preferred = os.environ.get("FLOW_E2E_CONTAINER_RUNTIME")
|
||||
candidates = [preferred] if preferred else ["podman", "docker"]
|
||||
|
||||
for runtime in candidates:
|
||||
if not runtime:
|
||||
continue
|
||||
if _runtime_available(runtime):
|
||||
return runtime
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _require_container_e2e() -> str:
|
||||
if os.environ.get("FLOW_RUN_E2E_CONTAINER") != "1":
|
||||
pytest.skip("Set FLOW_RUN_E2E_CONTAINER=1 to run container e2e tests")
|
||||
if not _docker_available():
|
||||
pytest.skip("Docker is required for container e2e tests")
|
||||
runtime = _container_runtime()
|
||||
if runtime is None:
|
||||
pytest.skip("Podman or Docker is required for container e2e tests")
|
||||
return runtime
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def e2e_image(tmp_path_factory):
|
||||
_require_container_e2e()
|
||||
def e2e_runtime():
|
||||
return _require_container_e2e()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def e2e_image(tmp_path_factory, e2e_runtime):
|
||||
runtime = e2e_runtime
|
||||
|
||||
context_dir = tmp_path_factory.mktemp("flow-e2e-docker-context")
|
||||
dockerfile = context_dir / "Dockerfile"
|
||||
@@ -53,7 +73,7 @@ def e2e_image(tmp_path_factory):
|
||||
|
||||
tag = f"flow-e2e-{uuid.uuid4().hex[:10]}"
|
||||
subprocess.run(
|
||||
["docker", "build", "-t", tag, str(context_dir)],
|
||||
[runtime, "build", "-t", tag, str(context_dir)],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -62,13 +82,13 @@ def e2e_image(tmp_path_factory):
|
||||
try:
|
||||
yield tag
|
||||
finally:
|
||||
subprocess.run(["docker", "rmi", "-f", tag], capture_output=True, text=True, check=False)
|
||||
subprocess.run([runtime, "rmi", "-f", tag], capture_output=True, text=True, check=False)
|
||||
|
||||
|
||||
def _run_in_container(image_tag: str, script: str) -> subprocess.CompletedProcess:
|
||||
def _run_in_container(runtime: str, image_tag: str, script: str) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(
|
||||
[
|
||||
"docker",
|
||||
runtime,
|
||||
"run",
|
||||
"--rm",
|
||||
"-v",
|
||||
@@ -89,7 +109,7 @@ def _assert_ok(run: subprocess.CompletedProcess) -> None:
|
||||
raise AssertionError(f"Container e2e failed:\nSTDOUT:\n{run.stdout}\nSTDERR:\n{run.stderr}")
|
||||
|
||||
|
||||
def test_e2e_link_and_undo_with_root_targets(e2e_image):
|
||||
def test_e2e_link_and_undo_with_root_targets(e2e_runtime, e2e_image):
|
||||
script = r"""
|
||||
set -euo pipefail
|
||||
export HOME=/home/flow
|
||||
@@ -116,10 +136,10 @@ test ! -L "$HOME/.zshrc"
|
||||
grep -q '^# before$' "$HOME/.zshrc"
|
||||
test ! -e /tmp/flow-e2e-root-target
|
||||
"""
|
||||
_assert_ok(_run_in_container(e2e_image, script))
|
||||
_assert_ok(_run_in_container(e2e_runtime, e2e_image, script))
|
||||
|
||||
|
||||
def test_e2e_dry_run_force_is_read_only_in_both_flag_orders(e2e_image):
|
||||
def test_e2e_dry_run_force_is_read_only_in_both_flag_orders(e2e_runtime, e2e_image):
|
||||
script = r"""
|
||||
set -euo pipefail
|
||||
export HOME=/home/flow
|
||||
@@ -150,10 +170,10 @@ assert "last_transaction" not in data, data
|
||||
PY
|
||||
fi
|
||||
"""
|
||||
_assert_ok(_run_in_container(e2e_image, script))
|
||||
_assert_ok(_run_in_container(e2e_runtime, e2e_image, script))
|
||||
|
||||
|
||||
def test_e2e_unmanaged_conflict_without_force_is_non_destructive(e2e_image):
|
||||
def test_e2e_unmanaged_conflict_without_force_is_non_destructive(e2e_runtime, e2e_image):
|
||||
script = r"""
|
||||
set -euo pipefail
|
||||
export HOME=/home/flow
|
||||
@@ -177,10 +197,10 @@ test -f "$HOME/.zshrc"
|
||||
test ! -L "$HOME/.zshrc"
|
||||
grep -q '^# user-file$' "$HOME/.zshrc"
|
||||
"""
|
||||
_assert_ok(_run_in_container(e2e_image, script))
|
||||
_assert_ok(_run_in_container(e2e_runtime, e2e_image, script))
|
||||
|
||||
|
||||
def test_e2e_managed_drift_requires_force(e2e_image):
|
||||
def test_e2e_managed_drift_requires_force(e2e_runtime, e2e_image):
|
||||
script = r"""
|
||||
set -euo pipefail
|
||||
export HOME=/home/flow
|
||||
@@ -208,10 +228,10 @@ test -f "$HOME/.zshrc"
|
||||
test ! -L "$HOME/.zshrc"
|
||||
grep -q '^# drifted-manual$' "$HOME/.zshrc"
|
||||
"""
|
||||
_assert_ok(_run_in_container(e2e_image, script))
|
||||
_assert_ok(_run_in_container(e2e_runtime, e2e_image, script))
|
||||
|
||||
|
||||
def test_e2e_directory_conflict_is_atomic_even_with_force(e2e_image):
|
||||
def test_e2e_directory_conflict_is_atomic_even_with_force(e2e_runtime, e2e_image):
|
||||
script = r"""
|
||||
set -euo pipefail
|
||||
export HOME=/home/flow
|
||||
@@ -236,10 +256,10 @@ test "$rc" -ne 0
|
||||
test -d "$HOME/.zshrc"
|
||||
test ! -e "$HOME/.gitconfig"
|
||||
"""
|
||||
_assert_ok(_run_in_container(e2e_image, script))
|
||||
_assert_ok(_run_in_container(e2e_runtime, e2e_image, script))
|
||||
|
||||
|
||||
def test_e2e_undo_after_failed_followup_link_restores_last_transaction(e2e_image):
|
||||
def test_e2e_undo_after_failed_followup_link_restores_last_transaction(e2e_runtime, e2e_image):
|
||||
script = r"""
|
||||
set -euo pipefail
|
||||
export HOME=/home/flow
|
||||
@@ -273,4 +293,4 @@ test -f "$HOME/.a"
|
||||
test ! -L "$HOME/.a"
|
||||
grep -q '^# pre-a$' "$HOME/.a"
|
||||
"""
|
||||
_assert_ok(_run_in_container(e2e_image, script))
|
||||
_assert_ok(_run_in_container(e2e_runtime, e2e_image, script))
|
||||
|
||||
@@ -7,7 +7,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from flow.commands.dotfiles import (
|
||||
from flow.services.dotfiles import (
|
||||
LinkSpec,
|
||||
_collect_home_specs,
|
||||
_list_profiles,
|
||||
@@ -16,6 +16,7 @@ from flow.commands.dotfiles import (
|
||||
_pull_requires_ack,
|
||||
_resolved_package_source,
|
||||
_run_sudo,
|
||||
run_relink,
|
||||
run_undo,
|
||||
_save_link_specs_to_state,
|
||||
_sync_to_desired,
|
||||
@@ -95,7 +96,7 @@ def test_collect_home_specs_skip_root_marker(tmp_path):
|
||||
|
||||
def test_state_round_trip(tmp_path, monkeypatch):
|
||||
state_file = tmp_path / "linked.json"
|
||||
monkeypatch.setattr("flow.commands.dotfiles.LINKED_STATE", state_file)
|
||||
monkeypatch.setattr("flow.services.dotfiles.LINKED_STATE", state_file)
|
||||
|
||||
specs = {
|
||||
Path("/home/user/.gitconfig"): LinkSpec(
|
||||
@@ -113,7 +114,7 @@ def test_state_round_trip(tmp_path, monkeypatch):
|
||||
|
||||
def test_state_old_format_rejected(tmp_path, monkeypatch):
|
||||
state_file = tmp_path / "linked.json"
|
||||
monkeypatch.setattr("flow.commands.dotfiles.LINKED_STATE", state_file)
|
||||
monkeypatch.setattr("flow.services.dotfiles.LINKED_STATE", state_file)
|
||||
state_file.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
@@ -131,24 +132,24 @@ def test_state_old_format_rejected(tmp_path, monkeypatch):
|
||||
|
||||
|
||||
def test_module_source_requires_sync(tmp_path):
|
||||
package_dir = tmp_path / "_shared" / "nvim"
|
||||
package_dir.mkdir(parents=True)
|
||||
(package_dir / "_module.yaml").write_text(
|
||||
package_root = tmp_path / "_shared" / "nvim"
|
||||
module_mount = package_root / ".config" / "nvim"
|
||||
module_mount.mkdir(parents=True)
|
||||
(module_mount / "_module.yaml").write_text(
|
||||
"source: github:dummy/example\n"
|
||||
"ref:\n"
|
||||
" branch: main\n"
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="Run 'flow dotfiles sync' first"):
|
||||
_resolved_package_source(_ctx(), "_shared/nvim", package_dir)
|
||||
_resolved_package_source(_ctx(), "_shared/nvim", package_root)
|
||||
|
||||
|
||||
def test_sync_modules_populates_cache_and_resolves_source(tmp_path, monkeypatch):
|
||||
module_src = tmp_path / "module-src"
|
||||
module_src.mkdir()
|
||||
subprocess.run(["git", "init", "-b", "main", str(module_src)], check=True)
|
||||
(module_src / ".config" / "nvim").mkdir(parents=True)
|
||||
(module_src / ".config" / "nvim" / "init.lua").write_text("-- module")
|
||||
(module_src / "init.lua").write_text("-- module")
|
||||
subprocess.run(["git", "-C", str(module_src), "add", "."], check=True)
|
||||
subprocess.run(
|
||||
[
|
||||
@@ -167,30 +168,30 @@ def test_sync_modules_populates_cache_and_resolves_source(tmp_path, monkeypatch)
|
||||
)
|
||||
|
||||
dotfiles = tmp_path / "dotfiles"
|
||||
package_dir = dotfiles / "_shared" / "nvim"
|
||||
package_dir.mkdir(parents=True)
|
||||
(package_dir / "_module.yaml").write_text(
|
||||
package_root = dotfiles / "_shared" / "nvim"
|
||||
module_mount = package_root / ".config" / "nvim"
|
||||
module_mount.mkdir(parents=True)
|
||||
(module_mount / "_module.yaml").write_text(
|
||||
f"source: {module_src}\n"
|
||||
"ref:\n"
|
||||
" branch: main\n"
|
||||
)
|
||||
(package_dir / "notes.txt").write_text("ignore me")
|
||||
(package_root / "notes.txt").write_text("ignore me")
|
||||
|
||||
monkeypatch.setattr("flow.commands.dotfiles.DOTFILES_DIR", dotfiles)
|
||||
monkeypatch.setattr("flow.commands.dotfiles.MODULES_DIR", tmp_path / "modules")
|
||||
monkeypatch.setattr("flow.services.dotfiles.DOTFILES_DIR", dotfiles)
|
||||
monkeypatch.setattr("flow.services.dotfiles.MODULES_DIR", tmp_path / "modules")
|
||||
|
||||
_sync_modules(_ctx(), verbose=False)
|
||||
resolved = _resolved_package_source(_ctx(), "_shared/nvim", package_dir)
|
||||
resolved = _resolved_package_source(_ctx(), "_shared/nvim", package_root)
|
||||
|
||||
assert (resolved / ".config" / "nvim" / "init.lua").exists()
|
||||
assert (resolved / "init.lua").exists()
|
||||
|
||||
|
||||
def test_module_backed_link_specs_exclude_git_internals(tmp_path, monkeypatch):
|
||||
module_src = tmp_path / "module-src"
|
||||
module_src.mkdir()
|
||||
subprocess.run(["git", "init", "-b", "main", str(module_src)], check=True)
|
||||
(module_src / ".config" / "nvim").mkdir(parents=True)
|
||||
(module_src / ".config" / "nvim" / "init.lua").write_text("-- module")
|
||||
(module_src / "init.lua").write_text("-- module")
|
||||
subprocess.run(["git", "-C", str(module_src), "add", "."], check=True)
|
||||
subprocess.run(
|
||||
[
|
||||
@@ -209,16 +210,17 @@ def test_module_backed_link_specs_exclude_git_internals(tmp_path, monkeypatch):
|
||||
)
|
||||
|
||||
dotfiles = tmp_path / "dotfiles"
|
||||
package_dir = dotfiles / "_shared" / "nvim"
|
||||
package_dir.mkdir(parents=True)
|
||||
(package_dir / "_module.yaml").write_text(
|
||||
package_root = dotfiles / "_shared" / "nvim"
|
||||
module_mount = package_root / ".config" / "nvim"
|
||||
module_mount.mkdir(parents=True)
|
||||
(module_mount / "_module.yaml").write_text(
|
||||
f"source: {module_src}\n"
|
||||
"ref:\n"
|
||||
" branch: main\n"
|
||||
)
|
||||
|
||||
monkeypatch.setattr("flow.commands.dotfiles.DOTFILES_DIR", dotfiles)
|
||||
monkeypatch.setattr("flow.commands.dotfiles.MODULES_DIR", tmp_path / "modules")
|
||||
monkeypatch.setattr("flow.services.dotfiles.DOTFILES_DIR", dotfiles)
|
||||
monkeypatch.setattr("flow.services.dotfiles.MODULES_DIR", tmp_path / "modules")
|
||||
|
||||
_sync_modules(_ctx(), verbose=False)
|
||||
|
||||
@@ -234,8 +236,7 @@ def test_sync_modules_resolves_relative_source_independent_of_cwd(tmp_path, monk
|
||||
module_src = tmp_path / "module-src"
|
||||
module_src.mkdir()
|
||||
subprocess.run(["git", "init", "-b", "main", str(module_src)], check=True)
|
||||
(module_src / ".config" / "nvim").mkdir(parents=True)
|
||||
(module_src / ".config" / "nvim" / "init.lua").write_text("-- module")
|
||||
(module_src / "init.lua").write_text("-- module")
|
||||
subprocess.run(["git", "-C", str(module_src), "add", "."], check=True)
|
||||
subprocess.run(
|
||||
[
|
||||
@@ -254,10 +255,11 @@ def test_sync_modules_resolves_relative_source_independent_of_cwd(tmp_path, monk
|
||||
)
|
||||
|
||||
dotfiles = tmp_path / "dotfiles"
|
||||
package_dir = dotfiles / "_shared" / "nvim"
|
||||
package_dir.mkdir(parents=True)
|
||||
relative_source = Path("../../../module-src")
|
||||
(package_dir / "_module.yaml").write_text(
|
||||
package_root = dotfiles / "_shared" / "nvim"
|
||||
module_mount = package_root / ".config" / "nvim"
|
||||
module_mount.mkdir(parents=True)
|
||||
relative_source = Path("../../../../../module-src")
|
||||
(module_mount / "_module.yaml").write_text(
|
||||
f"source: {relative_source}\n"
|
||||
"ref:\n"
|
||||
" branch: main\n"
|
||||
@@ -266,13 +268,61 @@ def test_sync_modules_resolves_relative_source_independent_of_cwd(tmp_path, monk
|
||||
unrelated_cwd = tmp_path / "unrelated-cwd"
|
||||
unrelated_cwd.mkdir()
|
||||
monkeypatch.chdir(unrelated_cwd)
|
||||
monkeypatch.setattr("flow.commands.dotfiles.DOTFILES_DIR", dotfiles)
|
||||
monkeypatch.setattr("flow.commands.dotfiles.MODULES_DIR", tmp_path / "modules")
|
||||
monkeypatch.setattr("flow.services.dotfiles.DOTFILES_DIR", dotfiles)
|
||||
monkeypatch.setattr("flow.services.dotfiles.MODULES_DIR", tmp_path / "modules")
|
||||
|
||||
_sync_modules(_ctx(), verbose=False)
|
||||
resolved = _resolved_package_source(_ctx(), "_shared/nvim", package_dir)
|
||||
resolved = _resolved_package_source(_ctx(), "_shared/nvim", package_root)
|
||||
|
||||
assert (resolved / ".config" / "nvim" / "init.lua").exists()
|
||||
assert (resolved / "init.lua").exists()
|
||||
|
||||
|
||||
def test_module_mount_inherits_directory_path(tmp_path, monkeypatch):
|
||||
module_src = tmp_path / "module-src"
|
||||
module_src.mkdir()
|
||||
subprocess.run(["git", "init", "-b", "main", str(module_src)], check=True)
|
||||
(module_src / "init.lua").write_text("-- module")
|
||||
(module_src / "lua").mkdir()
|
||||
(module_src / "lua" / "config.lua").write_text("-- module")
|
||||
subprocess.run(["git", "-C", str(module_src), "add", "."], check=True)
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
str(module_src),
|
||||
"-c",
|
||||
"user.name=Flow Test",
|
||||
"-c",
|
||||
"user.email=flow-test@example.com",
|
||||
"commit",
|
||||
"-m",
|
||||
"init module",
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
dotfiles = tmp_path / "dotfiles"
|
||||
package_root = dotfiles / "_shared" / "nvim"
|
||||
module_mount = package_root / ".config" / "nvim"
|
||||
module_mount.mkdir(parents=True)
|
||||
(module_mount / "_module.yaml").write_text(
|
||||
f"source: {module_src}\n"
|
||||
"ref:\n"
|
||||
" branch: main\n"
|
||||
)
|
||||
|
||||
monkeypatch.setattr("flow.services.dotfiles.DOTFILES_DIR", dotfiles)
|
||||
monkeypatch.setattr("flow.services.dotfiles.MODULES_DIR", tmp_path / "modules")
|
||||
_sync_modules(_ctx(), verbose=False)
|
||||
|
||||
home = tmp_path / "home"
|
||||
home.mkdir()
|
||||
specs = _collect_home_specs(_ctx(), dotfiles, home, None, set(), None)
|
||||
|
||||
assert home / ".config" / "nvim" / "init.lua" in specs
|
||||
assert home / ".config" / "nvim" / "lua" / "config.lua" in specs
|
||||
assert home / "init.lua" not in specs
|
||||
assert home / "lua" / "config.lua" not in specs
|
||||
|
||||
|
||||
def test_pull_requires_ack_only_on_real_updates():
|
||||
@@ -280,10 +330,29 @@ def test_pull_requires_ack_only_on_real_updates():
|
||||
assert _pull_requires_ack("Updating 123..456\n", "") is True
|
||||
|
||||
|
||||
def test_run_relink_uses_transactional_link_path(monkeypatch):
|
||||
calls = []
|
||||
|
||||
monkeypatch.setattr("flow.services.dotfiles._ensure_flow_dir", lambda _ctx: None)
|
||||
monkeypatch.setattr(
|
||||
"flow.services.dotfiles.run_unlink",
|
||||
lambda _ctx, _args: (_ for _ in ()).throw(AssertionError("run_unlink must not be used")),
|
||||
)
|
||||
|
||||
def _fake_run_link(_ctx, args):
|
||||
calls.append((args.packages, args.profile, args.copy, args.force, args.dry_run))
|
||||
|
||||
monkeypatch.setattr("flow.services.dotfiles.run_link", _fake_run_link)
|
||||
|
||||
run_relink(_ctx(), Namespace(packages=["git"], profile="work"))
|
||||
|
||||
assert calls == [(["git"], "work", False, False, False)]
|
||||
|
||||
|
||||
def test_sync_to_desired_dry_run_force_is_read_only(tmp_path, monkeypatch):
|
||||
state_file = tmp_path / "linked.json"
|
||||
monkeypatch.setattr("flow.commands.dotfiles.LINKED_STATE", state_file)
|
||||
monkeypatch.setattr("flow.commands.dotfiles._is_in_home", lambda _path, _home: True)
|
||||
monkeypatch.setattr("flow.services.dotfiles.LINKED_STATE", state_file)
|
||||
monkeypatch.setattr("flow.services.dotfiles._is_in_home", lambda _path, _home: True)
|
||||
|
||||
source = tmp_path / "source" / ".zshrc"
|
||||
source.parent.mkdir(parents=True)
|
||||
@@ -317,8 +386,8 @@ def test_sync_to_desired_dry_run_force_is_read_only(tmp_path, monkeypatch):
|
||||
|
||||
def test_sync_to_desired_force_fails_before_any_writes_on_directory_conflict(tmp_path, monkeypatch):
|
||||
state_file = tmp_path / "linked.json"
|
||||
monkeypatch.setattr("flow.commands.dotfiles.LINKED_STATE", state_file)
|
||||
monkeypatch.setattr("flow.commands.dotfiles._is_in_home", lambda _path, _home: True)
|
||||
monkeypatch.setattr("flow.services.dotfiles.LINKED_STATE", state_file)
|
||||
monkeypatch.setattr("flow.services.dotfiles._is_in_home", lambda _path, _home: True)
|
||||
|
||||
source_root = tmp_path / "source"
|
||||
source_root.mkdir()
|
||||
@@ -354,9 +423,9 @@ def test_sync_to_desired_force_fails_before_any_writes_on_directory_conflict(tmp
|
||||
|
||||
def test_undo_restores_previous_file_and_link_state(tmp_path, monkeypatch):
|
||||
state_file = tmp_path / "linked.json"
|
||||
monkeypatch.setattr("flow.commands.dotfiles.LINKED_STATE", state_file)
|
||||
monkeypatch.setattr("flow.commands.dotfiles.LINK_BACKUP_DIR", tmp_path / "link-backups")
|
||||
monkeypatch.setattr("flow.commands.dotfiles._is_in_home", lambda _path, _home: True)
|
||||
monkeypatch.setattr("flow.services.dotfiles.LINKED_STATE", state_file)
|
||||
monkeypatch.setattr("flow.services.dotfiles.LINK_BACKUP_DIR", tmp_path / "link-backups")
|
||||
monkeypatch.setattr("flow.services.dotfiles._is_in_home", lambda _path, _home: True)
|
||||
|
||||
source = tmp_path / "source" / ".zshrc"
|
||||
source.parent.mkdir(parents=True)
|
||||
@@ -403,9 +472,9 @@ def test_undo_restores_previous_file_and_link_state(tmp_path, monkeypatch):
|
||||
|
||||
def test_sync_to_desired_persists_incomplete_transaction_on_failure(tmp_path, monkeypatch):
|
||||
state_file = tmp_path / "linked.json"
|
||||
monkeypatch.setattr("flow.commands.dotfiles.LINKED_STATE", state_file)
|
||||
monkeypatch.setattr("flow.commands.dotfiles.LINK_BACKUP_DIR", tmp_path / "link-backups")
|
||||
monkeypatch.setattr("flow.commands.dotfiles._is_in_home", lambda _path, _home: True)
|
||||
monkeypatch.setattr("flow.services.dotfiles.LINKED_STATE", state_file)
|
||||
monkeypatch.setattr("flow.services.dotfiles.LINK_BACKUP_DIR", tmp_path / "link-backups")
|
||||
monkeypatch.setattr("flow.services.dotfiles._is_in_home", lambda _path, _home: True)
|
||||
|
||||
source = tmp_path / "source"
|
||||
source.mkdir()
|
||||
@@ -435,7 +504,7 @@ def test_sync_to_desired_persists_incomplete_transaction_on_failure(tmp_path, mo
|
||||
spec.target.symlink_to(spec.source)
|
||||
return True
|
||||
|
||||
monkeypatch.setattr("flow.commands.dotfiles._apply_link_spec", _failing_apply)
|
||||
monkeypatch.setattr("flow.services.dotfiles._apply_link_spec", _failing_apply)
|
||||
|
||||
with pytest.raises(RuntimeError, match="simulated failure"):
|
||||
_sync_to_desired(
|
||||
@@ -463,8 +532,8 @@ def test_sync_to_desired_persists_incomplete_transaction_on_failure(tmp_path, mo
|
||||
|
||||
def test_sync_to_desired_requires_force_to_remove_modified_managed_target(tmp_path, monkeypatch):
|
||||
state_file = tmp_path / "linked.json"
|
||||
monkeypatch.setattr("flow.commands.dotfiles.LINKED_STATE", state_file)
|
||||
monkeypatch.setattr("flow.commands.dotfiles._is_in_home", lambda _path, _home: True)
|
||||
monkeypatch.setattr("flow.services.dotfiles.LINKED_STATE", state_file)
|
||||
monkeypatch.setattr("flow.services.dotfiles._is_in_home", lambda _path, _home: True)
|
||||
|
||||
source = tmp_path / "source" / ".old"
|
||||
source.parent.mkdir(parents=True)
|
||||
@@ -501,8 +570,8 @@ def test_sync_to_desired_requires_force_to_remove_modified_managed_target(tmp_pa
|
||||
|
||||
def test_sync_to_desired_requires_force_to_replace_modified_managed_target(tmp_path, monkeypatch):
|
||||
state_file = tmp_path / "linked.json"
|
||||
monkeypatch.setattr("flow.commands.dotfiles.LINKED_STATE", state_file)
|
||||
monkeypatch.setattr("flow.commands.dotfiles._is_in_home", lambda _path, _home: True)
|
||||
monkeypatch.setattr("flow.services.dotfiles.LINKED_STATE", state_file)
|
||||
monkeypatch.setattr("flow.services.dotfiles._is_in_home", lambda _path, _home: True)
|
||||
|
||||
old_source = tmp_path / "source" / ".old"
|
||||
new_source = tmp_path / "source" / ".new"
|
||||
@@ -548,6 +617,6 @@ def test_sync_to_desired_requires_force_to_replace_modified_managed_target(tmp_p
|
||||
|
||||
|
||||
def test_run_sudo_errors_when_binary_missing(monkeypatch):
|
||||
monkeypatch.setattr("flow.commands.dotfiles.shutil.which", lambda _name: None)
|
||||
monkeypatch.setattr("flow.services.dotfiles.shutil.which", lambda _name: None)
|
||||
with pytest.raises(RuntimeError, match="sudo is required"):
|
||||
_run_sudo(["true"], dry_run=False)
|
||||
|
||||
Reference in New Issue
Block a user