fix(config-profiles): synchronize shared mount working copies

Use canonical profile files and writable Git working copies so editor and container changes share one source. Require confirmation before destructive Git refreshes and overlay profile files without composite snapshots.
This commit is contained in:
2026-07-22 11:23:19 +02:00
parent 0d6c1926ae
commit 2247ec47c9
14 changed files with 285 additions and 159 deletions
+48 -27
View File
@@ -15,6 +15,7 @@ from src.services.tool.instance_service import (
clone_git_repo,
modify_compose_file,
prepare_manifest_instance,
pull_repository_updates,
)
@@ -97,6 +98,37 @@ class TestCloneGitRepo:
clone.assert_not_called()
pull.assert_called_once_with(str(repo_path), remote_url)
def test_refresh_resets_writable_working_copy_to_remote_branch(
self, monkeypatch
) -> None:
from src.services.tool import instance_service
results = [
MagicMock(returncode=0, stdout="", stderr=""),
MagicMock(returncode=0, stdout="main\n", stderr=""),
MagicMock(returncode=0, stdout="", stderr=""),
]
run = MagicMock(side_effect=results)
monkeypatch.setattr(instance_service.subprocess, "run", run)
pull_repository_updates("/work/profile-git", "https://example.test/repo.git")
assert run.call_args_list[0].args[0] == [
"git",
"-C",
"/work/profile-git",
"fetch",
"origin",
]
assert run.call_args_list[2].args[0] == [
"git",
"-C",
"/work/profile-git",
"reset",
"--hard",
"origin/main",
]
def test_replaces_incomplete_clone_before_retry(
self, monkeypatch, tmp_path
) -> None:
@@ -191,17 +223,12 @@ class TestStackProfileMountsWithGitMounts:
str(instance_dir),
)
assert len(result) == 1
composite = result[0]
assert composite["target"] == "/home/user/.pi"
assert composite["source"].startswith(str(instance_dir))
assert not (tmp_path / "git" / "repo-clone" / "settings.json").exists()
assert (
tmp_path / "git" / "repo-clone" / "existing.txt"
).read_text() == "from git"
assert (tmp_path / "instance" / "mounts" / "composites").exists()
assert (Path(composite["source"]) / "existing.txt").read_text() == "from git"
assert (Path(composite["source"]) / "settings.json").read_text() == "{}"
assert len(result) == 2
assert result[0]["source"] == str(git_source)
assert result[0]["target"] == "/home/user/.pi"
assert result[1]["source"].endswith("/settings.json")
assert result[1]["target"] == "/home/user/.pi/settings.json"
assert not (git_source / "settings.json").exists()
def test_descendant_profile_mount_extends_git_root(self, tmp_path) -> None:
"""Nested targets are composed at the Git root, preserving siblings."""
@@ -223,11 +250,9 @@ class TestStackProfileMountsWithGitMounts:
str(instance_dir),
)
assert len(result) == 1
composite = Path(result[0]["source"])
assert result[0]["target"] == "/home/user/.pi"
assert (composite / "README").read_text() == "repo"
assert (composite / "agent" / "settings.json").read_text() == "x"
assert len(result) == 2
assert result[0]["source"] == str(git_source)
assert result[1]["target"] == "/home/user/.pi/agent/settings.json"
assert not (git_source / "agent").exists()
def test_file_profile_mount_extends_git_root(self, tmp_path) -> None:
@@ -254,11 +279,9 @@ class TestStackProfileMountsWithGitMounts:
str(instance_dir),
)
assert len(result) == 1
composite = Path(result[0]["source"])
assert result[0]["target"] == "/home/user/.pi"
assert (composite / "README").read_text() == "repo"
assert (composite / "settings.json").read_text() == "{}"
assert len(result) == 2
assert result[0]["source"] == str(git_source)
assert result[1]["target"] == "/home/user/.pi/settings.json"
def test_parent_profile_mount_extends_nested_git_mount(self, tmp_path) -> None:
"""A profile parent mount keeps Git content and its own sibling files."""
@@ -279,11 +302,9 @@ class TestStackProfileMountsWithGitMounts:
str(instance_dir),
)
assert len(result) == 1
composite = Path(result[0]["source"])
assert result[0]["target"] == "/home/user"
assert (composite / "config.toml").read_text() == "profile"
assert (composite / ".pi" / "plugin.toml").read_text() == "git"
assert len(result) == 2
assert result[0]["source"] == str(git_source)
assert result[1]["target"] == "/home/user/config.toml"
def test_non_overlapping_mounts_remain_separate(self, tmp_path) -> None:
"""Unrelated profile and Git mounts retain their independent sources."""
@@ -303,7 +324,7 @@ class TestStackProfileMountsWithGitMounts:
_stack_profile_mounts_with_git_mounts(
profile_mounts, git_mounts, str(instance_dir)
)
== profile_mounts + git_mounts
== git_mounts + profile_mounts
)