mirror of
https://github.com/usestrix/strix.git
synced 2026-08-18 17:52:32 +02:00
Argument parser: - Delete ``strix/tools/argument_parser.py`` and its tests. The SDK validates and types tool arguments via Pydantic before they hit our wrappers, and the in-container tool server receives JSON-typed kwargs over the wire. The string-coercion belt-and-suspenders is no longer pulling its weight. XML → JSON / typed structures: - ``create_vulnerability_report``: ``cvss_breakdown`` is now a ``dict[str, str]`` of the 8 metrics; ``code_locations`` is a ``list[dict]``. No more XML parsing in the tool or the renderer. - ``check_duplicate``: the dedup judge now emits a single JSON object instead of an ``<dedupe_result>`` block. Strict JSON parser handles optional code-fence wrappers. - ``agent_finish``: completion report posted to the parent inbox is a JSON object (``kind``, ``from``, ``agent_id``, ``success``, ``summary``, ``findings``, ``recommendations``) rather than a hand-rolled ``<agent_completion_report>`` XML envelope. - ``create_agent``: identity preamble + inherited-context markers are plain bracketed labels rather than ``<agent_delegation>`` / ``<inherited_context_from_parent>`` envelopes. - ``inject_messages_filter``: peer messages get a ``[Message from agent <id> | type=... | priority=...]`` header line instead of an ``<inter_agent_message>`` envelope. - Crash + system-warning messages: bracketed labels, no XML. - System prompt: the inter-agent block now describes the new header format and drops the "never echo XML envelope" rule. - ``strix/llm/utils.py``: deleted. ``clean_content`` collapsed into a one-line blank-line normalizer in the agent-message renderer (the XML envelope scrub had nothing left to scrub). Tests updated to match the new shapes.
306 lines
9.1 KiB
Python
306 lines
9.1 KiB
Python
"""Smoke tests for the remaining local SDK tool wrappers.
|
|
|
|
Covers: web_search, file_edit (str_replace_editor + list_files +
|
|
search_files), reporting (create_vulnerability_report), finish_scan.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from agents.tool import FunctionTool
|
|
|
|
from strix.tools.file_edit.tools import (
|
|
list_files,
|
|
search_files,
|
|
str_replace_editor,
|
|
)
|
|
from strix.tools.finish.tool import finish_scan
|
|
from strix.tools.reporting.tool import create_vulnerability_report
|
|
from strix.tools.web_search.tool import web_search
|
|
|
|
|
|
@dataclass
|
|
class _Ctx:
|
|
context: dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
def _ctx_for(agent_id: str = "test-agent") -> _Ctx:
|
|
return _Ctx(
|
|
context={
|
|
"agent_id": agent_id,
|
|
"tool_server_host_port": 12345,
|
|
"sandbox_token": "test-token",
|
|
},
|
|
)
|
|
|
|
|
|
async def _invoke(tool: FunctionTool, ctx: _Ctx, **kwargs: Any) -> dict[str, Any]:
|
|
from agents.tool_context import ToolContext
|
|
|
|
tool_ctx = ToolContext(
|
|
context=ctx.context,
|
|
usage=None,
|
|
tool_name=tool.name,
|
|
tool_call_id="test-call-id",
|
|
tool_arguments=json.dumps(kwargs),
|
|
)
|
|
result = await tool.on_invoke_tool(tool_ctx, json.dumps(kwargs))
|
|
assert isinstance(result, str)
|
|
decoded = json.loads(result)
|
|
assert isinstance(decoded, dict)
|
|
return decoded
|
|
|
|
|
|
def test_all_remaining_tools_are_function_tools() -> None:
|
|
for tool in (
|
|
web_search,
|
|
str_replace_editor,
|
|
list_files,
|
|
search_files,
|
|
create_vulnerability_report,
|
|
finish_scan,
|
|
):
|
|
assert isinstance(tool, FunctionTool)
|
|
|
|
|
|
# --- web_search -----------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_web_search_no_api_key_returns_structured_error(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""The legacy function returns a structured error when the env var is
|
|
missing — verify the wrapper passes that through verbatim."""
|
|
monkeypatch.delenv("PERPLEXITY_API_KEY", raising=False)
|
|
|
|
out = await _invoke(web_search, _ctx_for(), query="anything")
|
|
assert out["success"] is False
|
|
assert "PERPLEXITY_API_KEY" in out["message"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_web_search_delegates_to_perplexity(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""The wrapper invokes the Perplexity HTTP path on a thread."""
|
|
monkeypatch.setenv("PERPLEXITY_API_KEY", "fake-key")
|
|
|
|
fake_result = {
|
|
"success": True,
|
|
"query": "xss techniques",
|
|
"content": "Reflected XSS payload examples...",
|
|
"message": "Web search completed successfully",
|
|
}
|
|
with patch(
|
|
"strix.tools.web_search.tool._do_search",
|
|
return_value=fake_result,
|
|
) as do_search:
|
|
out = await _invoke(web_search, _ctx_for(), query="xss techniques")
|
|
|
|
assert out == fake_result
|
|
do_search.assert_called_once_with("xss techniques")
|
|
|
|
|
|
# --- file_edit (sandbox-bound) -------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_str_replace_editor_routes_to_sandbox() -> None:
|
|
"""file_edit tools must POST to the in-sandbox tool server, not run locally."""
|
|
fake_response = {"result": {"content": "file viewed"}}
|
|
with patch(
|
|
"strix.tools.file_edit.tools.post_to_sandbox",
|
|
return_value=fake_response,
|
|
) as dispatch:
|
|
out = await _invoke(
|
|
str_replace_editor,
|
|
_ctx_for(),
|
|
command="view",
|
|
path="src/foo.py",
|
|
)
|
|
|
|
assert out == fake_response
|
|
assert dispatch.call_count == 1
|
|
# post_to_sandbox is called positionally as (ctx, tool_name, kwargs).
|
|
args, _ = dispatch.call_args
|
|
assert args[1] == "str_replace_editor"
|
|
assert args[2]["command"] == "view"
|
|
assert args[2]["path"] == "src/foo.py"
|
|
# All optional file-edit params are forwarded as None (parity with legacy schema).
|
|
assert args[2]["file_text"] is None
|
|
assert args[2]["old_str"] is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_files_routes_to_sandbox() -> None:
|
|
fake_response = {"result": {"files": ["a.py"], "directories": []}}
|
|
with patch(
|
|
"strix.tools.file_edit.tools.post_to_sandbox",
|
|
return_value=fake_response,
|
|
) as dispatch:
|
|
out = await _invoke(list_files, _ctx_for(), path="src", recursive=True)
|
|
|
|
assert out == fake_response
|
|
args, _ = dispatch.call_args
|
|
assert args[1] == "list_files"
|
|
assert args[2] == {"path": "src", "recursive": True}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_files_routes_to_sandbox() -> None:
|
|
fake_response = {"result": {"output": "src/foo.py:1:match"}}
|
|
with patch(
|
|
"strix.tools.file_edit.tools.post_to_sandbox",
|
|
return_value=fake_response,
|
|
) as dispatch:
|
|
out = await _invoke(
|
|
search_files,
|
|
_ctx_for(),
|
|
path="src",
|
|
regex="TODO",
|
|
file_pattern="*.py",
|
|
)
|
|
|
|
assert out == fake_response
|
|
args, _ = dispatch.call_args
|
|
assert args[1] == "search_files"
|
|
assert args[2] == {"path": "src", "regex": "TODO", "file_pattern": "*.py"}
|
|
|
|
|
|
# --- reporting -----------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_vulnerability_report_validates_required_fields() -> None:
|
|
"""Empty required fields should be rejected by the validator."""
|
|
out = await _invoke(
|
|
create_vulnerability_report,
|
|
_ctx_for(),
|
|
title="", # empty -> validation error
|
|
description="d",
|
|
impact="i",
|
|
target="t",
|
|
technical_analysis="ta",
|
|
poc_description="pd",
|
|
poc_script_code="curl ...",
|
|
remediation_steps="rs",
|
|
cvss_breakdown={"attack_vector": "N"},
|
|
)
|
|
assert out["success"] is False
|
|
assert "errors" in out
|
|
assert any("Title" in e for e in out["errors"])
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_vulnerability_report_delegates_to_impl() -> None:
|
|
"""Verify the wrapper threads its kwargs through to the implementation."""
|
|
fake_result = {
|
|
"success": True,
|
|
"message": "Vulnerability report 'X' created successfully",
|
|
"report_id": "abc123",
|
|
"severity": "high",
|
|
"cvss_score": 7.5,
|
|
}
|
|
with patch(
|
|
"strix.tools.reporting.tool._do_create",
|
|
return_value=fake_result,
|
|
) as do_create:
|
|
out = await _invoke(
|
|
create_vulnerability_report,
|
|
_ctx_for(),
|
|
title="t",
|
|
description="d",
|
|
impact="i",
|
|
target="tg",
|
|
technical_analysis="ta",
|
|
poc_description="pd",
|
|
poc_script_code="pc",
|
|
remediation_steps="rs",
|
|
cvss_breakdown={"attack_vector": "N"},
|
|
cve="CVE-2024-12345",
|
|
)
|
|
|
|
assert out == fake_result
|
|
kwargs = do_create.call_args.kwargs
|
|
assert kwargs["title"] == "t"
|
|
assert kwargs["cve"] == "CVE-2024-12345"
|
|
# Optional params we didn't pass should still be forwarded as None.
|
|
assert kwargs["endpoint"] is None
|
|
assert kwargs["method"] is None
|
|
|
|
|
|
# --- finish_scan ---------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_finish_scan_validates_empty_fields() -> None:
|
|
"""Legacy validation: every section must be non-empty."""
|
|
out = await _invoke(
|
|
finish_scan,
|
|
_ctx_for(),
|
|
executive_summary="",
|
|
methodology="m",
|
|
technical_analysis="ta",
|
|
recommendations="r",
|
|
)
|
|
assert out["success"] is False
|
|
assert any("Executive summary" in e for e in out["errors"])
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_finish_scan_rejects_subagent() -> None:
|
|
"""A subagent (parent_id is set) must not be able to finish the scan."""
|
|
ctx = _Ctx(
|
|
context={
|
|
"agent_id": "child-1",
|
|
"parent_id": "root-1",
|
|
"tool_server_host_port": 12345,
|
|
"sandbox_token": "test-token",
|
|
},
|
|
)
|
|
out = await _invoke(
|
|
finish_scan,
|
|
ctx,
|
|
executive_summary="es",
|
|
methodology="m",
|
|
technical_analysis="ta",
|
|
recommendations="r",
|
|
)
|
|
assert out["success"] is False
|
|
assert out["error"] == "finish_scan_wrong_agent"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_finish_scan_persists_via_tracer() -> None:
|
|
"""When a global tracer exists, finish_scan should write the four sections."""
|
|
from unittest.mock import MagicMock
|
|
|
|
fake_tracer = MagicMock()
|
|
fake_tracer.vulnerability_reports = [{}, {}, {}]
|
|
|
|
with patch(
|
|
"strix.telemetry.tracer.get_global_tracer",
|
|
return_value=fake_tracer,
|
|
):
|
|
out = await _invoke(
|
|
finish_scan,
|
|
_ctx_for("root-agent"),
|
|
executive_summary="es",
|
|
methodology="m",
|
|
technical_analysis="ta",
|
|
recommendations="r",
|
|
)
|
|
|
|
assert out["success"] is True
|
|
assert out["vulnerabilities_found"] == 3
|
|
fake_tracer.update_scan_final_fields.assert_called_once_with(
|
|
executive_summary="es",
|
|
methodology="m",
|
|
technical_analysis="ta",
|
|
recommendations="r",
|
|
)
|