Files
headquarter/apps/api/tests/unit/test_instance_service.py
T
Developer 6e33e8e4e9 fix: remove explicit repo mount from pi-agent manifest and derive workspace name from remote URL
The pi-agent manifest still declared an explicit repo mount with
{{WORKSPACE_NAME}}, making the mount target dependent on tool config. The
instance service now synthesizes the repo mount, so the manifest no longer
needs the explicit mount.

- Add Alembic migration 2026_06_15_090500 to remove the source_type: repo
  mount from the built-in pi-agent manifest
- Add _get_repository_mount_name() helper to derive the workspace directory
  name from the repository remote URL (matching git clone behavior) and
  fall back to the user-provided repository name
- Use the helper for WORKSPACE_NAME/REPO_NAME in manifest, legacy dockerfile,
  and legacy compose template paths
- Update unit tests for the new migration and helper

Quality gates:
- pytest tests/unit: 218 passed
- ruff: clean on changed files
- mypy: clean on changed files
- alembic heads: single head
2026-06-15 09:10:05 +00:00

159 lines
4.9 KiB
Python

"""Unit tests for the tool instance service."""
from unittest.mock import MagicMock, AsyncMock
import pytest
from src.services.tool.instance_service import (
_get_repository_mount_name,
modify_compose_file,
prepare_manifest_instance,
)
@pytest.mark.unit
class TestModifyComposeFile:
"""Tests for modify_compose_file home-directory expansion."""
def test_extra_volumes_expand_home_dir(self, tmp_path):
compose_path = tmp_path / "docker-compose.yml"
compose_path.write_text(
"services:\n app:\n image: test:latest\n volumes: []\n"
)
modify_compose_file(
str(compose_path),
extra_volumes=[
{"source": "/host/config", "target": "~/.config", "type": "bind"},
{"source": "/host/code", "target": "$HOME/code", "type": "bind"},
],
home_dir="/home/user",
)
content = compose_path.read_text()
assert "/host/config:/home/user/.config" in content
assert "/host/code:/home/user/code" in content
def test_working_directory_expands_home_dir(self, tmp_path):
compose_path = tmp_path / "docker-compose.yml"
compose_path.write_text("services:\n app:\n image: test:latest\n")
modify_compose_file(
str(compose_path),
working_directory="~/workspace",
home_dir="/home/user",
)
content = compose_path.read_text()
assert "working_dir: /home/user/workspace" in content
@pytest.mark.unit
class TestGetRepositoryMountName:
"""Tests for _get_repository_mount_name."""
def test_prefers_remote_url_name_over_user_provided_name(self):
repo = MagicMock()
repo.name = "src"
repo.remote_url = "git@git.example.com:acme/headquarter.git"
assert _get_repository_mount_name(repo) == "headquarter"
def test_parses_browser_url_to_repo_name(self):
repo = MagicMock()
repo.name = "src"
repo.remote_url = "https://github.com/acme/headquarter/tree/main"
assert _get_repository_mount_name(repo) == "headquarter"
def test_falls_back_to_repo_name_when_remote_url_missing(self):
repo = MagicMock()
repo.name = "my-cool-repo"
repo.remote_url = None
assert _get_repository_mount_name(repo) == "my-cool-repo"
def test_falls_back_to_repo_name_for_unparseable_url(self):
repo = MagicMock()
repo.name = "my-cool-repo"
repo.remote_url = ""
assert _get_repository_mount_name(repo) == "my-cool-repo"
@pytest.mark.unit
async def test_prepare_manifest_instance_uses_repo_name_not_workspace_dir():
"""WORKSPACE_NAME must be the repository name, not the workspace path basename."""
repo = MagicMock()
repo.name = "src"
repo.remote_url = "git@git.example.com:acme/headquarter.git"
repo.path = "/data/repos/main"
tool_type = MagicMock()
tool_type.name = "pi-agent"
tool_type.manifest_id = "manifest-uuid"
manifest_def = MagicMock()
manifest_def.id = "manifest-uuid"
manifest_def.manifest = {
"base_image": "ubuntu:24.04",
"interface_type": "terminal",
"user": {"name": "user", "uid": 1001, "gid": 1001},
}
manifest_def.base_definition_id = None
instance = MagicMock()
instance.id = "instance-uuid"
instance.name = "pi-agent-headquarter-abc123"
instance.repository_id = "repo-uuid"
instance.tool_type_id = "tooltype-uuid"
instance.port = 0
instance.selected_config_profile_id = None
instance.workspace_id = None
session = AsyncMock()
async def session_get(model, obj_id):
if model.__name__ == "ToolType":
return tool_type
if model.__name__ == "ToolDefinitionManifest":
return manifest_def
if model.__name__ == "GitRepository":
return repo
return None
session.get.side_effect = session_get
# Patch docker images check to report the image already exists so we skip
# the actual Docker build.
import subprocess
from src.services.tool import instance_service
original_run = subprocess.run
def fake_run(cmd, **kwargs):
class Result:
returncode = 0
stdout = "image-id"
stderr = ""
return Result()
instance_service.subprocess.run = fake_run
try:
(
image_tag,
compose_content,
manifest,
home_dir,
) = await prepare_manifest_instance(
session=session,
instance=instance,
instance_dir="/tmp/instance",
repo_path="/data/repos/main",
env_vars={},
extra_volumes=[],
working_directory=None,
)
finally:
instance_service.subprocess.run = original_run
assert "WORKSPACE_NAME: headquarter" in compose_content
assert "/data/repos/main:/home/user/headquarter" in compose_content