Newer
Older
navi-1 / tests / unit / tools / test_spawn_agent.py
import pytest

from navi.core.session import InMemorySessionStore
from navi.core.subagent_runner import SubAgentOutcome
from navi.tools._internal.base import ToolContext
from navi.tools.spawn_agent import SpawnAgentTool
from tests.conftest_factory import (
    FakeLLMBackend,
    make_profile_registry,
    make_registry_with_tools,
)
from navi.core.registry import BackendRegistry


@pytest.fixture
def spawn_tool():
    profiles = make_profile_registry()
    tools = make_registry_with_tools()
    backends = BackendRegistry()
    backends.register("ollama", FakeLLMBackend(responses=["done"]))
    store = InMemorySessionStore()
    tool = SpawnAgentTool(profiles, tools, backends, store)
    return tool, profiles, store


@pytest.mark.anyio
async def test_spawn_agent_uses_explicit_profile(monkeypatch, spawn_tool):
    tool, _, _ = spawn_tool
    captured = {}

    async def fake_run_ephemeral(self, **kwargs):
        captured.update(kwargs)
        return SubAgentOutcome(text="developer result", status="ok")

    monkeypatch.setattr("navi.core.agent.Agent.run_ephemeral", fake_run_ephemeral)

    result = await tool.execute({
        "task": "inspect code",
        "profile_id": "developer",
    }, ctx=ToolContext())

    assert result.success is True
    assert captured["profile_id"] == "developer"
    assert "developer result" in result.output


@pytest.mark.anyio
async def test_spawn_agent_defaults_to_parent_profile(monkeypatch, spawn_tool):
    tool, _, store = spawn_tool
    session = await store.create("secretary")
    captured = {}

    async def fake_run_ephemeral(self, **kwargs):
        captured.update(kwargs)
        return SubAgentOutcome(text="secretary result", status="ok")

    monkeypatch.setattr("navi.core.agent.Agent.run_ephemeral", fake_run_ephemeral)

    result = await tool.execute({"task": "research this"}, ctx=ToolContext(session_id=session.id))

    assert result.success is True
    assert captured["profile_id"] == "secretary"


@pytest.mark.anyio
async def test_spawn_agent_rejects_unknown_profile(spawn_tool):
    tool, _, _ = spawn_tool

    result = await tool.execute({
        "task": "do work",
        "profile_id": "missing_profile",
    }, ctx=ToolContext())

    assert result.success is False
    assert result.error == "unknown_profile:missing_profile"
    assert "Available profiles" in result.output


@pytest.mark.anyio
async def test_schema_declares_background_param(spawn_tool):
    """ะค2: the tool schema exposes background=true for detached sub-agents."""
    tool, _, _ = spawn_tool
    background = tool.parameters["properties"]["background"]
    assert background["type"] == "boolean"
    assert "task_id" in background["description"]
    # the default remains synchronous
    assert "synchronous" in tool.description.lower()


class TestResultRendering:
    """How the run ended must reach the parent, the result must stay bounded,
    and a bad argument must not become an exception."""

    @pytest.mark.anyio
    @pytest.mark.parametrize(
        ("status", "label"),
        [
            ("timeout", "timed out"),
            ("max_iterations", "iteration limit"),
            ("thinking_stall", "stalled"),
            ("user_stop", "stopped by the user"),
            ("context_overflow", "ran out of context"),
        ],
    )
    async def test_partial_status_gets_its_own_header(
        self, monkeypatch, spawn_tool, status, label
    ):
        tool, _, _ = spawn_tool

        async def fake_run_ephemeral(self, **kwargs):
            return SubAgentOutcome(text=f"[Sub-agent stopped: {status}]\n\npartial", status=status)

        monkeypatch.setattr("navi.core.agent.Agent.run_ephemeral", fake_run_ephemeral)
        result = await tool.execute({"task": "x"}, ctx=ToolContext())

        assert result.success is False
        assert result.metadata["status"] == status
        assert label in result.output
        # The bug this replaces: every short run was reported as an iteration limit.
        if status != "max_iterations":
            assert "iteration limit" not in result.output.lower()

    @pytest.mark.anyio
    async def test_long_result_is_truncated_keeping_head_and_tail(self, monkeypatch, spawn_tool):
        tool, _, _ = spawn_tool

        async def fake_run_ephemeral(self, **kwargs):
            return SubAgentOutcome(text="HEAD" + "x" * 20_000 + "TAIL", status="ok")

        monkeypatch.setattr("navi.core.agent.Agent.run_ephemeral", fake_run_ephemeral)
        result = await tool.execute({"task": "x"}, ctx=ToolContext())

        assert "HEAD" in result.output
        assert "TAIL" in result.output
        assert "truncated from the middle" in result.output
        assert len(result.output) < 20_000

    @pytest.mark.anyio
    async def test_empty_task_is_a_clean_failure(self, spawn_tool):
        tool, _, _ = spawn_tool
        result = await tool.execute({"task": "   "}, ctx=ToolContext())
        assert result.success is False
        assert result.error == "missing_task"

    @pytest.mark.anyio
    async def test_bad_max_iterations_is_a_clean_failure(self, spawn_tool):
        tool, _, _ = spawn_tool
        result = await tool.execute(
            {"task": "x", "max_iterations": "many"}, ctx=ToolContext()
        )
        assert result.success is False
        assert result.error == "invalid_max_iterations"