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"