mirror of
https://github.com/pydantic/pydantic-ai-harness.git
synced 2026-07-21 10:55:35 +00:00
Harden macroscope spawn failures and cover agent-spec loading
Two consistency fixes from an audit against the sibling capabilities: - A binary that passes the `shutil.which` check can still fail to exec (lost +x, bad interpreter, a race). Surface that `OSError` to the model as a retryable setup error instead of letting it crash the run, matching how the other failure paths (missing binary, timeout, no review id) already behave. - Add an agent-spec roundtrip test so the `custom_capability_types=[Macroscope]` loading the docs document stays verified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
31a34ebae0
commit
eb556ece56
@@ -159,13 +159,19 @@ class MacroscopeToolset(FunctionToolset[AgentDepsT]):
|
||||
Spawns the CLI in its own session so a hung review can be killed by process
|
||||
group when it exceeds `timeout`, rather than leaking a background process.
|
||||
"""
|
||||
proc = await anyio.open_process(
|
||||
args,
|
||||
cwd=self._cwd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
start_new_session=True,
|
||||
)
|
||||
try:
|
||||
proc = await anyio.open_process(
|
||||
args,
|
||||
cwd=self._cwd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
start_new_session=True,
|
||||
)
|
||||
except OSError as e:
|
||||
# `shutil.which` found the binary, but spawning it still failed (lost +x,
|
||||
# bad interpreter, a race since the check). Surface it to the model as a
|
||||
# retryable setup error rather than crashing the whole run.
|
||||
raise ModelRetry(f'Failed to launch the macroscope CLI ({self._command!r}): {e}') from e
|
||||
stdout_chunks: list[bytes] = []
|
||||
stderr_chunks: list[bytes] = []
|
||||
try:
|
||||
|
||||
@@ -7,6 +7,7 @@ import time
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
|
||||
import anyio
|
||||
import pytest
|
||||
from pydantic_ai import Agent
|
||||
from pydantic_ai.exceptions import ModelRetry
|
||||
@@ -139,6 +140,17 @@ class TestRunReview:
|
||||
await _toolset(command, tmp_path, base=None).run_macroscope_review()
|
||||
assert _recorded_args(command) == ['codereview', '--raw']
|
||||
|
||||
async def test_spawn_failure_raises_model_retry(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Binary passes the `which` check but fails to exec (lost +x, bad interpreter, TOCTOU).
|
||||
command = _fake_cli(tmp_path, ['review_id=rev-6', 'issue_status=completed'])
|
||||
|
||||
async def _boom(*args: object, **kwargs: object) -> object:
|
||||
raise PermissionError('exec denied')
|
||||
|
||||
monkeypatch.setattr(anyio, 'open_process', _boom)
|
||||
with pytest.raises(ModelRetry, match='Failed to launch'):
|
||||
await _toolset(command, tmp_path).run_macroscope_review()
|
||||
|
||||
|
||||
class TestCapability:
|
||||
def test_default_instructions_mention_validation(self) -> None:
|
||||
@@ -153,6 +165,17 @@ class TestCapability:
|
||||
def test_empty_guidance_disables_instructions(self) -> None:
|
||||
assert Macroscope(guidance='').get_instructions() is None
|
||||
|
||||
def test_agent_spec_roundtrip(self) -> None:
|
||||
# The docs promise Macroscope loads from an agent spec via `custom_capability_types`.
|
||||
cap = Macroscope.from_spec(base='release', timeout=900.0)
|
||||
assert isinstance(cap, Macroscope)
|
||||
assert (cap.base, cap.timeout) == ('release', 900.0)
|
||||
agent = Agent.from_spec(
|
||||
{'model': 'test', 'capabilities': [{'Macroscope': {'base': 'main'}}]},
|
||||
custom_capability_types=[Macroscope],
|
||||
)
|
||||
assert isinstance(agent, Agent)
|
||||
|
||||
async def test_tool_runs_through_agent(self, tmp_path: Path) -> None:
|
||||
command = _fake_cli(tmp_path, ['review_id=rev-9', _ISSUE_LINE, 'issue_status=completed'])
|
||||
agent = Agent(TestModel(), capabilities=[Macroscope(command=command, cwd=tmp_path, base='main')])
|
||||
|
||||
Reference in New Issue
Block a user