feat: config profile multi-repo mounts
- Add mappings array support to git_mount entries - Clone repository once per git_mount entry, mount multiple subdirectories - Normalize legacy source_path+target_path to mappings on read - Update _merge_git_mounts to dedup by (remote_url, branch) and concatenate mappings - Add _normalize_git_mount, _clone_git_repo, _resolve_git_mount_mappings helpers - Update GitMountItem Pydantic model with GitMountMapping and model_validator - Update frontend GitMountEditor component with mappings UI - Auto-convert legacy git mount entries to mappings format on load - Add 15 backend unit tests for normalization, resolution, and glob expansion - Update existing config profile resolver tests for new merge behavior Quality gates: pytest 167 passed, frontend typecheck clean Addresses: config-profile-multi-repo-mounts
This commit is contained in:
@@ -97,7 +97,7 @@ class TestMergeFunctions:
|
||||
assert overrides == {"/app": "source"}
|
||||
|
||||
def test_merge_git_mounts_basic(self) -> None:
|
||||
"""Test basic git mount merging."""
|
||||
"""Test basic git mount merging normalizes to mappings format."""
|
||||
result = _merge_git_mounts(
|
||||
[],
|
||||
[{"remote_url": "https://github.com/user/repo1.git", "source_path": ".", "target_path": "/app"}],
|
||||
@@ -105,29 +105,54 @@ class TestMergeFunctions:
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert result[0]["remote_url"] == "https://github.com/user/repo1.git"
|
||||
assert result[0]["target_path"] == "/app"
|
||||
assert "mappings" in result[0]
|
||||
assert result[0]["mappings"] == [{"source_path": ".", "target_path": "/app"}]
|
||||
|
||||
def test_merge_git_mounts_override_same_repo_target(self) -> None:
|
||||
"""Test that git mounts with same repo+target override."""
|
||||
def test_merge_git_mounts_concatenate_same_repo_branch(self) -> None:
|
||||
"""Test that git mounts with same repo+branch concatenate mappings."""
|
||||
result = _merge_git_mounts(
|
||||
[{"remote_url": "https://github.com/user/repo1.git", "source_path": ".", "target_path": "/app", "branch": "main"}],
|
||||
[{"remote_url": "https://github.com/user/repo1.git", "source_path": "src", "target_path": "/app", "branch": "dev"}],
|
||||
[{"remote_url": "https://github.com/user/repo1.git", "source_path": "src", "target_path": "/src", "branch": "main"}],
|
||||
"source",
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert result[0]["source_path"] == "src"
|
||||
assert result[0]["branch"] == "dev"
|
||||
assert result[0]["branch"] == "main"
|
||||
mappings: list[dict[str, str]] = result[0]["mappings"]
|
||||
assert len(mappings) == 2
|
||||
assert {"source_path": ".", "target_path": "/app"} in mappings
|
||||
assert {"source_path": "src", "target_path": "/src"} in mappings
|
||||
|
||||
def test_merge_git_mounts_different_targets(self) -> None:
|
||||
"""Test that git mounts with different targets are preserved."""
|
||||
def test_merge_git_mounts_dedup_same_mapping(self) -> None:
|
||||
"""Test that duplicate mappings are deduplicated."""
|
||||
result = _merge_git_mounts(
|
||||
[{"remote_url": "https://github.com/user/repo1.git", "source_path": ".", "target_path": "/app", "branch": "main"}],
|
||||
[{"remote_url": "https://github.com/user/repo1.git", "source_path": ".", "target_path": "/app", "branch": "main"}],
|
||||
"source",
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert len(result[0]["mappings"]) == 1
|
||||
|
||||
def test_merge_git_mounts_different_repos(self) -> None:
|
||||
"""Test that git mounts with different repos are preserved."""
|
||||
result = _merge_git_mounts(
|
||||
[{"remote_url": "https://github.com/user/repo1.git", "source_path": ".", "target_path": "/app"}],
|
||||
[{"remote_url": "https://github.com/user/repo2.git", "source_path": ".", "target_path": "/config"}],
|
||||
"source",
|
||||
)
|
||||
assert len(result) == 2
|
||||
targets = {m["target_path"] for m in result}
|
||||
assert targets == {"/app", "/config"}
|
||||
urls = {m["remote_url"] for m in result}
|
||||
assert urls == {"https://github.com/user/repo1.git", "https://github.com/user/repo2.git"}
|
||||
|
||||
def test_merge_git_mounts_different_branches(self) -> None:
|
||||
"""Test that same repo with different branches are kept separate."""
|
||||
result = _merge_git_mounts(
|
||||
[{"remote_url": "https://github.com/user/repo1.git", "source_path": ".", "target_path": "/app", "branch": "main"}],
|
||||
[{"remote_url": "https://github.com/user/repo1.git", "source_path": ".", "target_path": "/app", "branch": "dev"}],
|
||||
"source",
|
||||
)
|
||||
assert len(result) == 2
|
||||
branches = {m.get("branch") for m in result}
|
||||
assert branches == {"main", "dev"}
|
||||
|
||||
|
||||
class TestResolveProfile:
|
||||
@@ -284,7 +309,7 @@ class TestResolveProfile:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_profile_with_git_mounts(self, db_session: AsyncSession) -> None:
|
||||
"""Test resolving a profile with git mounts."""
|
||||
"""Test resolving a profile with git mounts normalizes to mappings."""
|
||||
user_id = uuid.uuid4()
|
||||
|
||||
profile = ConfigProfile(
|
||||
@@ -303,7 +328,8 @@ class TestResolveProfile:
|
||||
result = await resolve_profile(db_session, profile.id)
|
||||
assert len(result.git_mounts) == 1
|
||||
assert result.git_mounts[0]["remote_url"] == "https://github.com/user/repo1.git"
|
||||
assert result.git_mounts[0]["target_path"] == "/app"
|
||||
assert "mappings" in result.git_mounts[0]
|
||||
assert result.git_mounts[0]["mappings"] == [{"source_path": ".", "target_path": "/app"}]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_profile_with_git_mount_includes(self, db_session: AsyncSession) -> None:
|
||||
@@ -349,8 +375,10 @@ class TestResolveProfile:
|
||||
|
||||
result = await resolve_profile(db_session, child.id)
|
||||
assert len(result.git_mounts) == 2
|
||||
targets = {m["target_path"] for m in result.git_mounts}
|
||||
assert targets == {"/app", "/config"}
|
||||
urls = {m["remote_url"] for m in result.git_mounts}
|
||||
assert urls == {"https://github.com/user/repo1.git", "https://github.com/user/repo2.git"}
|
||||
for m in result.git_mounts:
|
||||
assert "mappings" in m
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_profile_not_found(self, db_session: AsyncSession) -> None:
|
||||
|
||||
Reference in New Issue
Block a user