Files
roboco/tests/unit/runtime/test_remove_container_slug_guard.py
T

75 lines
2.4 KiB
Python
Raw Normal View History

2026-06-29 05:38:21 +02:00
"""_remove_container validates the slug before building the log-dir path.
The slug (``container_name`` with the ``roboco-agent-`` prefix stripped)
flows into ``Path("/data/logs/agents") / slug`` and ``mkdir(parents=True)``.
A traversal-shaped slug (``../../etc``) would mkdir outside the logs root.
``spawn_agent`` already validates the agent_id the container name is derived
from, so this is defense-in-depth — but the guard must fire before the join.
"""
from __future__ import annotations
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from roboco.runtime.orchestrator import AgentOrchestrator
def _make_orchestrator() -> AgentOrchestrator:
with patch.object(AgentOrchestrator, "__init__", return_value=None):
orch = AgentOrchestrator.__new__(AgentOrchestrator)
orch._instances = {}
orch._lock = MagicMock()
return orch
class _FakeProc:
async def wait(self) -> int:
return 0
@pytest.mark.asyncio
async def test_remove_container_skips_log_dump_on_traversal_slug(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A traversal-shaped slug must NOT mkdir outside /data/logs/agents."""
orch = _make_orchestrator()
mkdir_paths: list[str] = []
def _fake_mkdir(self: Path, *_a: object, **_kw: object) -> None:
mkdir_paths.append(str(self))
monkeypatch.setattr(Path, "mkdir", _fake_mkdir)
monkeypatch.setattr(
"asyncio.create_subprocess_exec", AsyncMock(return_value=_FakeProc())
)
# No exception propagates: log-dump is best-effort; removal still runs.
await orch._remove_container("roboco-agent-../../etc")
assert mkdir_paths == [], f"traversal slug must not reach mkdir; got {mkdir_paths}"
@pytest.mark.asyncio
async def test_remove_container_normal_slug_dumps_logs(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A normal slug passes the guard and reaches the log-dir mkdir."""
orch = _make_orchestrator()
mkdir_paths: list[str] = []
def _fake_mkdir(self: Path, *_a: object, **_kw: object) -> None:
mkdir_paths.append(str(self))
monkeypatch.setattr(Path, "mkdir", _fake_mkdir)
monkeypatch.setattr(
"asyncio.create_subprocess_exec", AsyncMock(return_value=_FakeProc())
)
await orch._remove_container("roboco-agent-be-dev-1")
assert any(p.endswith("/data/logs/agents/be-dev-1") for p in mkdir_paths)