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
This commit is contained in:
@@ -4,7 +4,11 @@ from unittest.mock import MagicMock, AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from src.services.tool.instance_service import modify_compose_file, prepare_manifest_instance
|
||||
from src.services.tool.instance_service import (
|
||||
_get_repository_mount_name,
|
||||
modify_compose_file,
|
||||
prepare_manifest_instance,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -14,7 +18,7 @@ class TestModifyComposeFile:
|
||||
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'
|
||||
"services:\n app:\n image: test:latest\n volumes: []\n"
|
||||
)
|
||||
|
||||
modify_compose_file(
|
||||
@@ -32,9 +36,7 @@ class TestModifyComposeFile:
|
||||
|
||||
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'
|
||||
)
|
||||
compose_path.write_text("services:\n app:\n image: test:latest\n")
|
||||
|
||||
modify_compose_file(
|
||||
str(compose_path),
|
||||
@@ -46,11 +48,41 @@ class TestModifyComposeFile:
|
||||
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 = "my-cool-repo"
|
||||
repo.name = "src"
|
||||
repo.remote_url = "git@git.example.com:acme/headquarter.git"
|
||||
repo.path = "/data/repos/main"
|
||||
|
||||
tool_type = MagicMock()
|
||||
@@ -63,13 +95,12 @@ async def test_prepare_manifest_instance_uses_repo_name_not_workspace_dir():
|
||||
"base_image": "ubuntu:24.04",
|
||||
"interface_type": "terminal",
|
||||
"user": {"name": "user", "uid": 1001, "gid": 1001},
|
||||
"mounts": [{"name": "workspace", "target": "~/{{WORKSPACE_NAME}}", "source_type": "repo"}],
|
||||
}
|
||||
manifest_def.base_definition_id = None
|
||||
|
||||
instance = MagicMock()
|
||||
instance.id = "instance-uuid"
|
||||
instance.name = "pi-agent-my-cool-repo-abc123"
|
||||
instance.name = "pi-agent-headquarter-abc123"
|
||||
instance.repository_id = "repo-uuid"
|
||||
instance.tool_type_id = "tooltype-uuid"
|
||||
instance.port = 0
|
||||
@@ -95,16 +126,23 @@ async def test_prepare_manifest_instance_uses_repo_name_not_workspace_dir():
|
||||
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(
|
||||
(
|
||||
image_tag,
|
||||
compose_content,
|
||||
manifest,
|
||||
home_dir,
|
||||
) = await prepare_manifest_instance(
|
||||
session=session,
|
||||
instance=instance,
|
||||
instance_dir="/tmp/instance",
|
||||
@@ -116,5 +154,5 @@ async def test_prepare_manifest_instance_uses_repo_name_not_workspace_dir():
|
||||
finally:
|
||||
instance_service.subprocess.run = original_run
|
||||
|
||||
assert "WORKSPACE_NAME: my-cool-repo" in compose_content
|
||||
assert "/data/repos/main:/home/user/my-cool-repo" in compose_content
|
||||
assert "WORKSPACE_NAME: headquarter" in compose_content
|
||||
assert "/data/repos/main:/home/user/headquarter" in compose_content
|
||||
|
||||
Reference in New Issue
Block a user