mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(toolchain): provision against the target interpreter + runnability smoke
When toolchain matching is enabled, install_dev_deps resolves the target's
Python and provisions the venv against it (uv sync --extra dev --python <v>,
uv auto-downloads), then runs a runnability smoke (uv run pytest --collect-only)
and records {python, status} in a .git/.roboco-toolchain marker the gates read.
Precision over recall: only a pytest collection error (the interpreter-mismatch
signature) reports 'broken'; pytest-absent / timeout report 'unknown' so a gate
never blocks on an inconclusive smoke. Flag off => provisioning is unchanged.
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
"""Toolchain-matched provisioning: workspace gets the TARGET's Python.
|
||||
|
||||
When ``toolchain_match_enabled`` is on and the target declares a Python version,
|
||||
``install_dev_deps`` provisions the venv against that interpreter (``uv ...
|
||||
--python <v>``), runs a runnability smoke, and records the result in a workspace
|
||||
marker the gates read. When off, provisioning is exactly today's behavior.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from roboco.services.workspace import (
|
||||
WorkspaceService,
|
||||
_detect_dep_commands,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
_PY = '[project]\nname = "x"\nrequires-python = ">=3.14,<3.15"\n'
|
||||
|
||||
|
||||
def _service() -> WorkspaceService:
|
||||
session = MagicMock()
|
||||
session.execute = AsyncMock()
|
||||
return WorkspaceService(session)
|
||||
|
||||
|
||||
def _make_workspace(tmp_path: Path) -> Path:
|
||||
workspace = tmp_path / "roboco" / "backend" / "be-dev-1"
|
||||
(workspace / ".git").mkdir(parents=True)
|
||||
return workspace
|
||||
|
||||
|
||||
def test_detect_dep_commands_injects_target_python(tmp_path: Path) -> None:
|
||||
ws = _make_workspace(tmp_path)
|
||||
(ws / "pyproject.toml").write_text(_PY)
|
||||
commands = _detect_dep_commands(ws, target_python="3.14")
|
||||
assert commands[0][1] == ["uv", "sync", "--extra", "dev", "--python", "3.14"]
|
||||
|
||||
|
||||
def test_detect_dep_commands_no_python_is_unchanged(tmp_path: Path) -> None:
|
||||
ws = _make_workspace(tmp_path)
|
||||
(ws / "pyproject.toml").write_text(_PY)
|
||||
commands = _detect_dep_commands(ws)
|
||||
assert commands[0][1] == ["uv", "sync", "--extra", "dev"]
|
||||
|
||||
|
||||
def _fake_run_factory(captured: list[list[str]], *, collect_rc: int):
|
||||
def _fake_run(argv, **_kw) -> subprocess.CompletedProcess[str]:
|
||||
captured.append(argv)
|
||||
rc = collect_rc if "--collect-only" in argv else 0
|
||||
return subprocess.CompletedProcess(argv, returncode=rc, stdout="", stderr="")
|
||||
|
||||
return _fake_run
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_provisions_target_python_and_records_ok(tmp_path: Path) -> None:
|
||||
ws = _make_workspace(tmp_path)
|
||||
(ws / "pyproject.toml").write_text(_PY)
|
||||
(ws / "uv.lock").write_text("version = 1\n")
|
||||
svc = _service()
|
||||
captured: list[list[str]] = []
|
||||
|
||||
with (
|
||||
patch("roboco.services.workspace.settings.toolchain_match_enabled", True),
|
||||
patch(
|
||||
"roboco.services.workspace.subprocess.run",
|
||||
side_effect=_fake_run_factory(captured, collect_rc=0),
|
||||
),
|
||||
patch("roboco.services.workspace._ensure_agent_owned"),
|
||||
):
|
||||
await svc.install_dev_deps(ws)
|
||||
|
||||
# provisioned against 3.14 + ran the collect smoke
|
||||
assert ["uv", "sync", "--extra", "dev", "--python", "3.14"] in captured
|
||||
assert any("--collect-only" in argv for argv in captured)
|
||||
assert svc.read_toolchain_status(ws) == ("3.14", "ok")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_records_broken_on_collection_error(tmp_path: Path) -> None:
|
||||
ws = _make_workspace(tmp_path)
|
||||
(ws / "pyproject.toml").write_text(_PY)
|
||||
(ws / "uv.lock").write_text("version = 1\n")
|
||||
svc = _service()
|
||||
captured: list[list[str]] = []
|
||||
|
||||
with (
|
||||
patch("roboco.services.workspace.settings.toolchain_match_enabled", True),
|
||||
patch(
|
||||
"roboco.services.workspace.subprocess.run",
|
||||
side_effect=_fake_run_factory(
|
||||
captured, collect_rc=2
|
||||
), # pytest collect error
|
||||
),
|
||||
patch("roboco.services.workspace._ensure_agent_owned"),
|
||||
):
|
||||
await svc.install_dev_deps(ws)
|
||||
|
||||
assert svc.read_toolchain_status(ws) == ("3.14", "broken")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_flag_off_is_unchanged_no_marker(tmp_path: Path) -> None:
|
||||
ws = _make_workspace(tmp_path)
|
||||
(ws / "pyproject.toml").write_text(_PY)
|
||||
(ws / "uv.lock").write_text("version = 1\n")
|
||||
svc = _service()
|
||||
captured: list[list[str]] = []
|
||||
|
||||
with (
|
||||
patch("roboco.services.workspace.settings.toolchain_match_enabled", False),
|
||||
patch(
|
||||
"roboco.services.workspace.subprocess.run",
|
||||
side_effect=_fake_run_factory(captured, collect_rc=0),
|
||||
),
|
||||
patch("roboco.services.workspace._ensure_agent_owned"),
|
||||
):
|
||||
await svc.install_dev_deps(ws)
|
||||
|
||||
assert ["uv", "sync", "--extra", "dev"] in captured
|
||||
assert not any("--python" in argv for argv in captured)
|
||||
assert not any("--collect-only" in argv for argv in captured)
|
||||
assert svc.read_toolchain_status(ws) == (None, None)
|
||||
Reference in New Issue
Block a user