Files
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

397 lines
14 KiB
Python

"""Isolation tests for Root agent (Layer 0).
Each test scripts the mocked LLM response and asserts the routing /
handoff / output shape.
"""
from pathlib import Path
import pytest
from unittest.mock import MagicMock
from perfxpert.agents import root as root_module
from perfxpert.agents import schemas
from perfxpert.agents.framework import (
Agent, AgentConstructionError, HandoffPolicyViolation, ToolAllowlistViolation,
FakeProviderResponse, dispatch_tool,
)
from perfxpert.tools import tasks as tasks_tool
# -- Construction ---------------------------------------------------------
def test_root_agent_builds():
agent = root_module.build_root_agent()
assert agent.name == "Root"
assert agent.layer == 0
def test_root_tool_allowlist_size():
agent = root_module.build_root_agent()
assert len(agent.tools) <= 5
def test_root_uses_real_task_store_functions():
agent = root_module.build_root_agent()
bindings = {tool.name: tool.fn for tool in agent.tools}
assert bindings["tasks.next"] is not None
assert bindings["tasks.create"] is not None
assert bindings["tasks.update"] is not None
assert bindings["tasks.close"] is not None
assert bindings["tasks.next"] is tasks_tool.next
assert bindings["tasks.create"] is tasks_tool.create
assert bindings["tasks.update"] is tasks_tool.update
assert bindings["tasks.close"] is tasks_tool.close
def test_root_uses_source_dir_for_task_store(tmp_path: Path, monkeypatch):
source_dir = tmp_path / "project"
source_dir.mkdir()
monkeypatch.chdir(tmp_path)
def fake_run_agent(agent, input_payload, **kwargs):
dispatch_tool(agent, "tasks.create", {"title": "from-root"})
return {
"structured_output": {
"narrative": "ok",
"recommendations": [],
"primary_bottleneck": "mixed",
"warnings": [],
"metadata": {},
}
}
monkeypatch.setattr(root_module, "run_agent", fake_run_agent)
root_module.run_root(
schemas.RootInput(
user_query="track this work",
source_dir=str(source_dir),
database_path=str(source_dir / "trace.db"),
),
airgap=False,
provider="anthropic",
)
assert (source_dir / ".beads" / "tasks.db").exists()
assert not (tmp_path / ".beads" / "tasks.db").exists()
def test_root_allowed_handoffs_exactly_three():
agent = root_module.build_root_agent()
assert set(agent.allowed_handoffs) == {"analysis", "recommendation", "correctness"}
def test_root_cannot_handoff_to_specialist():
agent = root_module.build_root_agent()
with pytest.raises(HandoffPolicyViolation):
from perfxpert.agents.framework import dispatch_handoff
dispatch_handoff(agent, "compute_specialist")
def test_root_cannot_call_execution_tool():
agent = root_module.build_root_agent()
forbidden = [
"patch.apply", "patch.revert", "patch.verify_output",
"compile.build", "profile.run", "anchors.check",
]
for tool in forbidden:
assert not agent.has_tool(tool), f"Root must NOT have execution tool {tool!r}"
# -- Routing --------------------------------------------------------------
def test_root_routes_analyze_intent_to_analysis(fake_provider, monkeypatch):
"""Rule-first routing: intent=analyze → handoff to Analysis."""
fake_provider.return_value = FakeProviderResponse(
text="routed", handoff="analysis",
structured_output={
"narrative": "Routed to analysis.",
"recommendations": [],
"primary_bottleneck": "mixed",
"warnings": [], "metadata": {},
},
)
result = root_module.run_root(
schemas.RootInput(user_query="why is this kernel slow?", database_path="x.db"),
provider="anthropic",
)
assert isinstance(result, schemas.RootOutput)
def test_root_routes_verify_intent_to_correctness(fake_provider):
fake_provider.return_value = FakeProviderResponse(
text="routed", handoff="correctness",
structured_output={
"narrative": "Routed to correctness.",
"recommendations": [],
"primary_bottleneck": "mixed",
"warnings": [], "metadata": {},
},
)
result = root_module.run_root(
schemas.RootInput(user_query="did my patch help?", database_path="x.db"),
provider="anthropic",
)
assert isinstance(result, schemas.RootOutput)
def test_root_routes_optimize_intent_to_recommendation(fake_provider):
fake_provider.return_value = FakeProviderResponse(
text="routed", handoff="recommendation",
structured_output={
"narrative": "Routed to recommendation.",
"recommendations": [],
"primary_bottleneck": "mixed",
"warnings": [], "metadata": {},
},
)
result = root_module.run_root(
schemas.RootInput(user_query="suggest optimizations", database_path="x.db"),
provider="anthropic",
)
assert isinstance(result, schemas.RootOutput)
# -- Air-gap routing parity -----------------------------------------------
def test_root_routing_is_deterministic_in_airgap(monkeypatch):
"""Spec §5 invariant: air-gap routing decisions identical to LLM mode."""
monkeypatch.setenv("PERFXPERT_AIRGAP", "1")
result1 = root_module.run_root(
schemas.RootInput(user_query="why slow?", database_path="x.db"),
airgap=True,
)
result2 = root_module.run_root(
schemas.RootInput(user_query="why slow?", database_path="x.db"),
airgap=True,
)
# Deterministic: same input produces same handoff target
assert result1.metadata.get("routed_to") == result2.metadata.get("routed_to")
def test_root_airgap_uses_analysis_bottleneck_for_db_queries(monkeypatch):
"""Airgap Root should preserve deterministic Analysis classification."""
analysis_output = schemas.AnalysisOutput(
primary_bottleneck="latency",
confidence=0.75,
time_breakdown={"api_pct": 86.6},
hot_kernels=[],
counter_data_available=True,
)
monkeypatch.setattr(
"perfxpert.agents.analysis.run_analysis",
lambda *args, **kwargs: analysis_output,
)
result = root_module.run_root(
schemas.RootInput(
user_query="analyze this trace",
database_path="x.db",
),
airgap=True,
)
assert result.primary_bottleneck == "latency"
# -- Narrative assembly ---------------------------------------------------
def test_root_writes_bottleneck_narrative(fake_provider):
"""Absorbed from former Bottleneck-Narrator (review N3).
Root's narrative includes a 2-3 sentence classification explanation.
"""
fake_provider.return_value = FakeProviderResponse(
text="narrative",
structured_output={
"narrative": (
"The workload is bottlenecked on HBM bandwidth. "
"Kernel X consumes 62% of total runtime. "
"Focus optimization on memory coalescing."
),
"recommendations": [{"title": "coalesce loads", "priority": "high"}],
"primary_bottleneck": "memory_transfer",
"warnings": [], "metadata": {},
},
)
result = root_module.run_root(
schemas.RootInput(user_query="analyze", database_path="x.db"),
provider="anthropic",
)
assert len(result.narrative) > 0
assert result.primary_bottleneck == "memory_transfer"
# -- Finding #24: Root lambda stubs for tasks.* never validated ---------------
def test_root_lambdas_for_tasks_never_invoked_during_routing(monkeypatch, fake_provider):
"""Root's tasks.* are stubbed as lambdas returning None. Ensure run_root
doesn't actually invoke them in normal routing paths (Finding #24).
If it does, the lambda stubs are hiding real behavior — switch to real
tasks.* wrappers from the tasks module.
"""
from perfxpert.agents.framework import dispatch_tool as real_dispatch_tool
invocations = []
def recording_dispatch(ag, tool_name, args):
invocations.append(tool_name)
return real_dispatch_tool(ag, tool_name, args)
monkeypatch.setattr(
"perfxpert.agents.framework.dispatch_tool",
recording_dispatch,
)
fake_provider.return_value = FakeProviderResponse(
text="routed",
structured_output={
"narrative": "Routed.",
"recommendations": [],
"primary_bottleneck": "mixed",
"warnings": [],
"metadata": {},
},
)
result = root_module.run_root(
schemas.RootInput(user_query="why slow?", database_path="x.db"),
provider="anthropic",
)
assert isinstance(result, schemas.RootOutput)
tasks_invocations = [n for n in invocations if n.startswith("tasks.")]
# Root should NOT invoke tasks.* tools during a simple routing turn.
# If this assertion fails, the lambda stubs are hiding real behavior.
assert not tasks_invocations, (
f"Root invoked tasks.* tools during routing: {tasks_invocations}. "
"Switch the lambda stubs to real tasks.* wrappers from perfxpert.tools.tasks."
)
def test_root_tasks_bindings_are_real():
"""Root must wire real tasks.* wrappers — no lambda stubs may remain.
Regression guard for Finding #24.
"""
import inspect
from perfxpert.tools import tasks as tasks_mod
agent = root_module.build_root_agent()
tasks_tools = [tb for tb in agent.tools if tb.name.startswith("tasks.")]
assert len(tasks_tools) >= 1, (
"Root should declare at least one tasks.* tool per fence spec"
)
expected = {
"tasks.next": tasks_mod.next_task,
"tasks.create": tasks_mod.create,
"tasks.update": tasks_mod.update,
"tasks.close": tasks_mod.close,
}
lambda_stubs = []
for tb in tasks_tools:
try:
src = inspect.getsource(tb.fn).strip()
except (OSError, TypeError):
src = ""
if "lambda" in src and ("None" in src or ": None" in src):
lambda_stubs.append(tb.name)
if tb.name in expected:
assert tb.fn is expected[tb.name], (
f"{tb.name} should be wired to perfxpert.tools.tasks.{expected[tb.name].__name__}"
)
assert not lambda_stubs, (
f"Root still uses lambda stub(s) for: {lambda_stubs}. "
"Real perfxpert.tools.tasks wrappers must be used instead."
)
# -- Fence allowlist helper -----------------------------------------------
def _extract_fence_tool_names(fence_text):
"""Parse Tool allowlist section from a fence markdown file.
Returns set of tool names. Stops at next ## heading or EOF.
"""
in_section = False
tools = set()
for line in fence_text.splitlines():
stripped = line.strip()
if stripped.startswith("## ") and "Tool allowlist" in stripped:
in_section = True
continue
if in_section:
if stripped.startswith("## "):
break
if stripped.startswith("- "):
tool_name = stripped[2:].strip()
if tool_name:
tools.add(tool_name)
return tools
# -- Fence / allowlist alignment (design N29) ----------------------------
def test_root_fence_tools_match_allowlist():
"""Fence-declared tools must be subset of agent actual allowlist (N29)."""
from pathlib import Path
agent = root_module.build_root_agent()
allowed = {t.name for t in agent.tools}
fence_path = Path(root_module.__file__).parent / "fence" / "root.md"
fence_text = fence_path.read_text()
fence_tools = _extract_fence_tool_names(fence_text)
violations = fence_tools - allowed
assert not violations, (
f"Root fence lists tools not in agent allowlist: {violations}"
)
# -- Fence / allowlist alignment for ALL agents (design N29 full audit) --
def _agent_fence_allowlist_cases():
from pathlib import Path
from perfxpert.agents import (
analysis as analysis_mod,
recommendation as rec_mod,
correctness as cor_mod,
compute_specialist as cs_mod,
memory_specialist as ms_mod,
latency_specialist as ls_mod,
)
return [
("Root", root_module.build_root_agent,
Path(root_module.__file__).parent / "fence" / "root.md"),
("Analysis", analysis_mod.build_analysis_agent,
Path(analysis_mod.__file__).parent / "fence" / "analysis.md"),
("Recommendation", rec_mod.build_recommendation_agent,
Path(rec_mod.__file__).parent / "fence" / "recommendation.md"),
("Correctness", cor_mod.build_correctness_agent,
Path(cor_mod.__file__).parent / "fence" / "correctness.md"),
("ComputeSpecialist", cs_mod.build_compute_specialist,
Path(cs_mod.__file__).parent / "fence" / "compute_specialist.md"),
("MemorySpecialist", ms_mod.build_memory_specialist,
Path(ms_mod.__file__).parent / "fence" / "memory_specialist.md"),
("LatencySpecialist", ls_mod.build_latency_specialist,
Path(ls_mod.__file__).parent / "fence" / "latency_specialist.md"),
]
@pytest.mark.parametrize("agent_name,builder,fence_path", _agent_fence_allowlist_cases())
def test_all_agents_fence_tools_subset_of_allowlist(agent_name, builder, fence_path):
"""Every fence-listed tool must appear in code allowlist (design N29)."""
if not fence_path.exists():
pytest.skip(f"No fence file for {agent_name}: {fence_path}")
agent = builder()
allowed = {t.name for t in agent.tools}
fence_text = fence_path.read_text()
fence_tools = _extract_fence_tool_names(fence_text)
violations = fence_tools - allowed
assert not violations, (
f"{agent_name}: fence lists tools NOT in allowlist: {violations}"
)