test: add comprehensive tests for config profile git mounts

- Add git mount merge function tests
- Add profile resolution tests with git mounts
- Add integration tests for CRUD with git mounts
- Add glob expansion tests (patterns, limits, repo boundary)
- Add branch checkout tests (success and failure)
- Add error handling tests for missing repos/invalid UUIDs

All 52 tests pass.
This commit is contained in:
Alex Blank
2026-05-26 22:49:12 +02:00
parent 4c11163bff
commit 0ec20b9c23
4 changed files with 433 additions and 0 deletions
@@ -13,6 +13,7 @@ from src.services.config_profile_resolver import (
_merge_files,
_merge_mounts,
_merge_runtime_hints,
_merge_git_mounts,
)
@@ -97,6 +98,39 @@ class TestMergeFunctions:
assert result["/app"].mode == "ro"
assert overrides == {"/app": "source"}
def test_merge_git_mounts_basic(self) -> None:
"""Test basic git mount merging."""
result = _merge_git_mounts(
[],
[{"repo_id": "repo1", "source_path": ".", "target_path": "/app"}],
"source",
)
assert len(result) == 1
assert result[0]["repo_id"] == "repo1"
assert result[0]["target_path"] == "/app"
def test_merge_git_mounts_override_same_repo_target(self) -> None:
"""Test that git mounts with same repo+target override."""
result = _merge_git_mounts(
[{"repo_id": "repo1", "source_path": ".", "target_path": "/app", "branch": "main"}],
[{"repo_id": "repo1", "source_path": "src", "target_path": "/app", "branch": "dev"}],
"source",
)
assert len(result) == 1
assert result[0]["source_path"] == "src"
assert result[0]["branch"] == "dev"
def test_merge_git_mounts_different_targets(self) -> None:
"""Test that git mounts with different targets are preserved."""
result = _merge_git_mounts(
[{"repo_id": "repo1", "source_path": ".", "target_path": "/app"}],
[{"repo_id": "repo2", "source_path": ".", "target_path": "/config"}],
"source",
)
assert len(result) == 2
targets = {m["target_path"] for m in result}
assert targets == {"/app", "/config"}
class TestResolveProfile:
"""Unit tests for profile resolution."""
@@ -250,6 +284,76 @@ class TestResolveProfile:
with pytest.raises(ConfigProfileCycleError):
await resolve_profile(db_session, profile_a.id)
@pytest.mark.asyncio
async def test_resolve_profile_with_git_mounts(self, db_session: AsyncSession) -> None:
"""Test resolving a profile with git mounts."""
user_id = uuid.uuid4()
profile = ConfigProfile(
id=uuid.uuid4(),
user_id=user_id,
name="with-git-mounts",
env_vars={},
files={},
git_mounts=[
{"repo_id": "repo1", "source_path": ".", "target_path": "/app"},
],
)
db_session.add(profile)
await db_session.commit()
result = await resolve_profile(db_session, profile.id)
assert len(result.git_mounts) == 1
assert result.git_mounts[0]["repo_id"] == "repo1"
assert result.git_mounts[0]["target_path"] == "/app"
@pytest.mark.asyncio
async def test_resolve_profile_with_git_mount_includes(self, db_session: AsyncSession) -> None:
"""Test resolving a profile that includes another with git mounts."""
user_id = uuid.uuid4()
# Create base profile with git mount
base = ConfigProfile(
id=uuid.uuid4(),
user_id=user_id,
name="base",
env_vars={},
files={},
git_mounts=[
{"repo_id": "repo1", "source_path": ".", "target_path": "/app"},
],
)
db_session.add(base)
# Create child profile with its own git mount
child = ConfigProfile(
id=uuid.uuid4(),
user_id=user_id,
name="child",
env_vars={},
files={},
git_mounts=[
{"repo_id": "repo2", "source_path": "config", "target_path": "/config"},
],
)
db_session.add(child)
await db_session.commit()
# Create include relationship
include = ConfigProfileInclude(
id=uuid.uuid4(),
profile_id=child.id,
included_profile_id=base.id,
order_index=0,
)
db_session.add(include)
await db_session.commit()
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"}
@pytest.mark.asyncio
async def test_resolve_profile_not_found(self, db_session: AsyncSession) -> None:
"""Test resolving a non-existent profile."""
@@ -0,0 +1,139 @@
"""Unit tests for git mount resolution in tool instances."""
import os
import tempfile
from pathlib import Path
import pytest
from src.api.tool_instances import (
_checkout_branch,
_expand_glob_source,
_resolve_single_git_mount,
)
from src.services.config_profile_resolver import ResolvedProfile
class TestExpandGlobSource:
"""Unit tests for glob pattern expansion."""
def test_no_glob_single_file(self, tmp_path: Path) -> None:
"""Test non-glob path returns single file."""
test_file = tmp_path / "test.txt"
test_file.write_text("content")
result = _expand_glob_source(str(test_file), str(tmp_path))
assert len(result) == 1
assert result[0] == str(test_file)
def test_no_glob_missing_file(self, tmp_path: Path) -> None:
"""Test non-glob missing file returns empty list."""
missing_file = tmp_path / "missing.txt"
result = _expand_glob_source(str(missing_file), str(tmp_path))
assert len(result) == 0
def test_glob_pattern(self, tmp_path: Path) -> None:
"""Test glob pattern matches files."""
(tmp_path / "file1.txt").write_text("content1")
(tmp_path / "file2.txt").write_text("content2")
(tmp_path / "other.py").write_text("code")
result = _expand_glob_source(str(tmp_path / "*.txt"), str(tmp_path))
assert len(result) == 2
assert all(f.endswith(".txt") for f in result)
def test_glob_recursive(self, tmp_path: Path) -> None:
"""Test recursive glob pattern."""
subdir = tmp_path / "subdir"
subdir.mkdir()
(subdir / "nested.txt").write_text("content")
result = _expand_glob_source(str(tmp_path / "**" / "*.txt"), str(tmp_path))
assert len(result) == 1
assert "nested.txt" in result[0]
def test_glob_limit_enforced(self, tmp_path: Path) -> None:
"""Test that glob matches are limited to prevent abuse."""
# Create more than 100 files
for i in range(105):
(tmp_path / f"file{i}.txt").write_text("content")
result = _expand_glob_source(str(tmp_path / "*.txt"), str(tmp_path))
assert len(result) == 100 # MAX_GLOB_MATCHES limit
def test_glob_escapes_repo(self, tmp_path: Path) -> None:
"""Test that glob results outside repo are filtered."""
other_dir = tmp_path.parent / "other"
other_dir.mkdir(exist_ok=True)
(other_dir / "outside.txt").write_text("content")
result = _expand_glob_source(str(tmp_path.parent / "*" / "*.txt"), str(tmp_path))
# Should only include files within tmp_path, not other_dir
assert all(r.startswith(str(tmp_path)) for r in result)
class TestCheckoutBranch:
"""Unit tests for branch checkout."""
def test_checkout_existing_branch(self, tmp_path: Path) -> None:
"""Test checking out an existing branch."""
# Initialize git repo
os.system(f"cd {tmp_path} && git init && git config user.email 'test@test.com' && git config user.name 'Test'")
(tmp_path / "file.txt").write_text("content")
os.system(f"cd {tmp_path} && git add . && git commit -m 'initial'")
os.system(f"cd {tmp_path} && git branch feature")
_checkout_branch(str(tmp_path), "feature")
# Verify we're on feature branch
result = os.popen(f"cd {tmp_path} && git branch --show-current").read().strip()
assert result == "feature"
def test_checkout_nonexistent_branch(self, tmp_path: Path) -> None:
"""Test checking out a non-existent branch raises error."""
os.system(f"cd {tmp_path} && git init && git config user.email 'test@test.com' && git config user.name 'Test'")
(tmp_path / "file.txt").write_text("content")
os.system(f"cd {tmp_path} && git add . && git commit -m 'initial'")
with pytest.raises(RuntimeError, match="Failed to checkout branch"):
_checkout_branch(str(tmp_path), "nonexistent")
class TestResolveSingleGitMount:
"""Unit tests for resolving a single git mount."""
@pytest.mark.asyncio
async def test_resolve_missing_repo(self, db_session) -> None:
"""Test that missing repo returns empty list."""
git_mount = {
"repo_id": "12345678-1234-1234-1234-123456789abc",
"source_path": ".",
"target_path": "/app",
}
result = await _resolve_single_git_mount(db_session, git_mount)
assert result == []
@pytest.mark.asyncio
async def test_resolve_missing_target_path(self, db_session) -> None:
"""Test that missing target path returns empty list."""
git_mount = {
"repo_id": "12345678-1234-1234-1234-123456789abc",
"source_path": ".",
}
result = await _resolve_single_git_mount(db_session, git_mount)
assert result == []
@pytest.mark.asyncio
async def test_resolve_invalid_repo_id(self, db_session) -> None:
"""Test that invalid repo_id returns empty list."""
git_mount = {
"repo_id": "not-a-uuid",
"source_path": ".",
"target_path": "/app",
}
result = await _resolve_single_git_mount(db_session, git_mount)
assert result == []