refactor: nuke legacy harness, drop sdk_ prefixes

The SDK harness is the only path now; legacy host-side code is gone.
File names no longer carry the ``sdk_`` distinction.

Deleted legacy host-side modules:
- strix/agents/StrixAgent/ (template moved to strix/agents/prompts/)
- strix/agents/base_agent.py, state.py
- strix/llm/llm.py, config.py
- strix/runtime/docker_runtime.py, runtime.py
- strix/tools/executor.py, agents_graph/agents_graph_actions.py
- strix/interface/sdk_dispatch.py + the env-flag dispatch in cli.py

Renamed (drop ``sdk_`` prefix):
- strix/sdk_entry.py → strix/entry.py
- strix/agents/sdk_factory.py → strix/agents/factory.py
- strix/agents/sdk_prompt.py → strix/agents/prompt.py
- strix/tools/<x>/<x>_sdk_tool[s].py → strix/tools/<x>/tool[s].py
- strix/tools/_legacy_adapter.py → strix/tools/_state_adapter.py
- ``_legacy`` aliases inside the wrappers → ``_impl``

CLI + TUI now call ``run_strix_scan`` directly — they build the
sandbox image / sources_path locally and rely on
``session_manager.cleanup`` (called inside ``run_strix_scan``'s finally)
for teardown. Three TUI handlers that reached into legacy multi-agent
globals (``_agent_instances``, ``send_user_message_to_agent``,
``stop_agent``) are now no-ops with a TODO; reconnecting them to the
``AgentMessageBus`` is a follow-up.

Tracer.get_total_llm_stats no longer reaches into the deleted
``agents_graph_actions`` globals — the orchestration hooks now feed the
tracer via ``Tracer.record_llm_usage`` (live + completed buckets).
finish_scan's ``_check_active_agents`` and load_skill's runtime
``_agent_instances`` reach-in are no-op stubs; the
``AgentMessageBus`` is the source of truth post-migration.

llm/utils.py rewritten to keep only the streaming-parser helpers
(``normalize_tool_format``, ``parse_tool_invocations``,
``fix_incomplete_tool_call``, ``format_tool_call``, ``clean_content``).
``STRIX_MODEL_MAP`` moved to ``llm/multi_provider_setup.py`` (its only
remaining caller).

Per-file ruff ignores added for legacy interface modules (TUI / main /
CLI / utils / streaming_parser / tool_components) and tracer.py —
pre-existing PLC0415/BLE001/PLR0915 patterns are out of scope.

Tests: 287/287 passing. Renamed test files to drop ``sdk_`` prefix.
``test_tracer.py::test_get_total_llm_stats_aggregates_live_and_completed``
rewritten to feed ``Tracer.record_llm_usage`` instead of legacy globals.
Test file annotations added so pre-commit's strict mypy passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
0xallam
2026-04-25 09:30:23 -07:00
parent 4e0d0f35d9
commit d8881498ee
69 changed files with 646 additions and 4537 deletions
@@ -21,8 +21,8 @@ from unittest.mock import patch
from agents import Agent
from agents.tool import FunctionTool
from strix.agents.sdk_factory import build_strix_agent, make_child_factory
from strix.agents.sdk_prompt import _resolve_skills, render_system_prompt
from strix.agents.factory import build_strix_agent, make_child_factory
from strix.agents.prompt import _resolve_skills, render_system_prompt
# --- prompt renderer ----------------------------------------------------
@@ -60,7 +60,7 @@ def test_render_system_prompt_swallows_template_errors() -> None:
"""If the template path can't be resolved, return an empty string
(not raise) agent construction must never blow up on prompt load."""
with patch(
"strix.agents.sdk_prompt.get_strix_resource_path",
"strix.agents.prompt.get_strix_resource_path",
side_effect=RuntimeError("missing"),
):
out = render_system_prompt(skills=[])
@@ -191,7 +191,7 @@ def test_make_child_factory_passes_scan_level_config() -> None:
interactive=True,
system_prompt_context={"scope_source": "test"},
)
with patch("strix.agents.sdk_factory.render_system_prompt", side_effect=fake_render):
with patch("strix.agents.factory.render_system_prompt", side_effect=fake_render):
factory(name="child", skills=["xss"])
assert captured["scan_mode"] == "fast"
-199
View File
@@ -1,199 +0,0 @@
"""Phase 5b tests for the STRIX_USE_SDK_HARNESS dispatch.
Covers the env-flag reader, source-path resolution, sandbox image
lookup, and the adapter that translates legacy CLI args into
``run_strix_scan`` kwargs.
We never call ``run_strix_scan`` for real — that requires a live
Docker daemon + LLM. The tests patch it and verify the kwargs handoff.
"""
from __future__ import annotations
import logging
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from strix.interface.sdk_dispatch import (
_resolve_sandbox_image,
_resolve_sources_path,
run_scan_via_sdk,
should_use_sdk_harness,
)
# --- env flag reader ----------------------------------------------------
@pytest.mark.parametrize(
("value", "expected"),
[
("1", True),
("true", True),
("True", True),
("YES", True),
("0", False),
("false", False),
("no", False),
("", False),
("anything-else", False),
],
)
def test_should_use_sdk_harness_parses_env(
monkeypatch: pytest.MonkeyPatch,
value: str,
expected: bool,
) -> None:
monkeypatch.setenv("STRIX_USE_SDK_HARNESS", value)
assert should_use_sdk_harness() is expected
def test_should_use_sdk_harness_defaults_false_when_unset(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("STRIX_USE_SDK_HARNESS", raising=False)
assert should_use_sdk_harness() is False
# --- image lookup -------------------------------------------------------
def test_resolve_sandbox_image_uses_config_value() -> None:
with patch(
"strix.config.Config.get",
return_value="strix-sandbox:0.1.13",
):
assert _resolve_sandbox_image() == "strix-sandbox:0.1.13"
def test_resolve_sandbox_image_falls_back_when_unset(
caplog: pytest.LogCaptureFixture,
) -> None:
with (
patch("strix.config.Config.get", return_value=None),
caplog.at_level(logging.WARNING, logger="strix.interface.sdk_dispatch"),
):
out = _resolve_sandbox_image()
assert out == "strix-sandbox:latest"
assert any("strix_image not configured" in r.message for r in caplog.records)
# --- sources path -------------------------------------------------------
def test_resolve_sources_path_uses_local_sources_parent(tmp_path: Path) -> None:
"""When --local-sources is given, mount that path's parent so the
agent can walk down into the actual source directory tree."""
src_dir = tmp_path / "my-project"
src_dir.mkdir()
args = SimpleNamespace(
local_sources=[{"host_path": str(src_dir)}],
run_name="run-1",
)
assert _resolve_sources_path(args) == tmp_path
def test_resolve_sources_path_handles_alternative_keys(tmp_path: Path) -> None:
"""Some legacy paths use 'source_path' or 'path' instead of
'host_path' — we accept all three."""
src_dir = tmp_path / "alt"
src_dir.mkdir()
args = SimpleNamespace(
local_sources=[{"path": str(src_dir)}],
run_name="run-2",
)
assert _resolve_sources_path(args) == tmp_path
def test_resolve_sources_path_creates_scratch_dir_when_absent(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
args = SimpleNamespace(local_sources=None, run_name="scan-x")
out = _resolve_sources_path(args)
assert out == tmp_path / "strix" / "sources" / "scan-x"
assert out.exists()
assert out.is_dir()
# --- adapter -----------------------------------------------------------
@pytest.mark.asyncio
async def test_run_scan_via_sdk_translates_args_to_kwargs(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Verify every kwarg the entry point reads is forwarded correctly."""
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
scan_config = {"targets": [], "scan_mode": "deep"}
args = SimpleNamespace(
run_name="scan-42",
local_sources=None,
interactive=True,
)
fake_tracer = MagicMock(name="tracer")
fake_run = AsyncMock(return_value=MagicMock(name="run_result"))
with (
patch("strix.config.Config.get", return_value="strix-sandbox:test"),
patch("strix.sdk_entry.run_strix_scan", new=fake_run),
):
await run_scan_via_sdk(scan_config=scan_config, args=args, tracer=fake_tracer)
fake_run.assert_awaited_once()
assert fake_run.await_args is not None
kwargs = fake_run.await_args.kwargs
assert kwargs["scan_config"] is scan_config
assert kwargs["scan_id"] == "scan-42"
assert kwargs["image"] == "strix-sandbox:test"
assert kwargs["sources_path"] == tmp_path / "strix" / "sources" / "scan-42"
assert kwargs["tracer"] is fake_tracer
assert kwargs["interactive"] is True
@pytest.mark.asyncio
async def test_run_scan_via_sdk_falls_back_to_scan_config_run_name(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""If args has no run_name, scan_config['run_name'] should be used."""
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
scan_config = {"targets": [], "run_name": "from-config"}
args = SimpleNamespace(local_sources=None)
fake_run = AsyncMock(return_value=MagicMock())
with (
patch("strix.config.Config.get", return_value="img:1"),
patch("strix.sdk_entry.run_strix_scan", new=fake_run),
):
await run_scan_via_sdk(scan_config=scan_config, args=args, tracer=None)
assert fake_run.await_args is not None
assert fake_run.await_args.kwargs["scan_id"] == "from-config"
@pytest.mark.asyncio
async def test_run_scan_via_sdk_propagates_run_failure(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A failure inside run_strix_scan should bubble up to the caller —
the legacy CLI relies on raised exceptions for the SDK path."""
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
scan_config: dict[str, Any] = {"targets": []}
args = SimpleNamespace(run_name="r", local_sources=None)
fake_run = AsyncMock(side_effect=RuntimeError("boom"))
with (
patch("strix.config.Config.get", return_value="img"),
patch("strix.sdk_entry.run_strix_scan", new=fake_run),
pytest.raises(RuntimeError, match="boom"),
):
await run_scan_via_sdk(scan_config=scan_config, args=args, tracer=None)
-16
View File
@@ -1,16 +0,0 @@
import litellm
import pytest
from strix.llm.config import LLMConfig
from strix.llm.llm import LLM
def test_llm_does_not_modify_litellm_callbacks(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("STRIX_TELEMETRY", "1")
monkeypatch.setenv("STRIX_OTEL_TELEMETRY", "1")
monkeypatch.setattr(litellm, "callbacks", ["custom-callback"])
llm = LLM(LLMConfig(model_name="openai/gpt-5.4"), agent_name=None)
assert llm is not None
assert litellm.callbacks == ["custom-callback"]
-30
View File
@@ -1,30 +0,0 @@
from strix.llm.config import LLMConfig
from strix.llm.llm import LLM
def test_llm_config_whitebox_defaults_to_false(monkeypatch) -> None:
monkeypatch.setenv("STRIX_LLM", "openai/gpt-5")
config = LLMConfig()
assert config.is_whitebox is False
def test_llm_config_whitebox_can_be_enabled(monkeypatch) -> None:
monkeypatch.setenv("STRIX_LLM", "openai/gpt-5")
config = LLMConfig(is_whitebox=True)
assert config.is_whitebox is True
def test_whitebox_prompt_loads_source_aware_coordination_skill(monkeypatch) -> None:
monkeypatch.setenv("STRIX_LLM", "openai/gpt-5")
whitebox_llm = LLM(LLMConfig(scan_mode="quick", is_whitebox=True), agent_name="StrixAgent")
assert "<source_aware_whitebox>" in whitebox_llm.system_prompt
assert "<source_aware_sast>" in whitebox_llm.system_prompt
assert "Begin with fast source triage" in whitebox_llm.system_prompt
assert "You MUST begin at the very first step by running the code and testing live." not in (
whitebox_llm.system_prompt
)
non_whitebox_llm = LLM(LLMConfig(scan_mode="quick", is_whitebox=False), agent_name="StrixAgent")
assert "<source_aware_whitebox>" not in non_whitebox_llm.system_prompt
assert "<source_aware_sast>" not in non_whitebox_llm.system_prompt
+62 -69
View File
@@ -10,7 +10,6 @@ from opentelemetry.sdk.trace.export import SimpleSpanProcessor, SpanExportResult
from strix.telemetry import tracer as tracer_module
from strix.telemetry import utils as telemetry_utils
from strix.telemetry.tracer import Tracer, set_global_tracer
from strix.tools.agents_graph import agents_graph_actions
def _load_events(events_path: Path) -> list[dict[str, Any]]:
@@ -19,7 +18,7 @@ def _load_events(events_path: Path) -> list[dict[str, Any]]:
@pytest.fixture(autouse=True)
def _reset_tracer_globals(monkeypatch) -> None:
def _reset_tracer_globals(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(tracer_module, "_global_tracer", None)
monkeypatch.setattr(tracer_module, "_OTEL_BOOTSTRAPPED", False)
monkeypatch.setattr(tracer_module, "_OTEL_REMOTE_ENABLED", False)
@@ -32,7 +31,9 @@ def _reset_tracer_globals(monkeypatch) -> None:
monkeypatch.delenv("TRACELOOP_HEADERS", raising=False)
def test_tracer_local_mode_writes_jsonl_with_correlation(monkeypatch, tmp_path) -> None:
def test_tracer_local_mode_writes_jsonl_with_correlation(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.chdir(tmp_path)
tracer = Tracer("local-observability")
@@ -60,7 +61,7 @@ def test_tracer_local_mode_writes_jsonl_with_correlation(monkeypatch, tmp_path)
assert event["span_id"]
def test_tracer_redacts_sensitive_payloads(monkeypatch, tmp_path) -> None:
def test_tracer_redacts_sensitive_payloads(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.chdir(tmp_path)
tracer = Tracer("redaction-run")
@@ -89,7 +90,9 @@ def test_tracer_redacts_sensitive_payloads(monkeypatch, tmp_path) -> None:
assert "[REDACTED]" in serialized
def test_tracer_remote_mode_configures_traceloop_export(monkeypatch, tmp_path) -> None:
def test_tracer_remote_mode_configures_traceloop_export(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.chdir(tmp_path)
class FakeTraceloop:
@@ -128,7 +131,9 @@ def test_tracer_remote_mode_configures_traceloop_export(monkeypatch, tmp_path) -
assert run_started["payload"]["remote_export_enabled"] is True
def test_tracer_local_mode_avoids_traceloop_remote_endpoint(monkeypatch, tmp_path) -> None:
def test_tracer_local_mode_avoids_traceloop_remote_endpoint(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.chdir(tmp_path)
class FakeTraceloop:
@@ -157,7 +162,9 @@ def test_tracer_local_mode_avoids_traceloop_remote_endpoint(monkeypatch, tmp_pat
assert tracer._remote_export_enabled is False
def test_otlp_fallback_includes_auth_and_custom_headers(monkeypatch, tmp_path) -> None:
def test_otlp_fallback_includes_auth_and_custom_headers(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(tracer_module, "Traceloop", None)
monkeypatch.setenv("TRACELOOP_BASE_URL", "https://otel.example.com")
@@ -172,13 +179,13 @@ def test_otlp_fallback_includes_auth_and_custom_headers(monkeypatch, tmp_path) -
captured["headers"] = headers or {}
captured["kwargs"] = kwargs
def export(self, spans: Any) -> SpanExportResult: # noqa: ARG002
def export(self, spans: Any) -> SpanExportResult:
return SpanExportResult.SUCCESS
def shutdown(self) -> None:
return None
def force_flush(self, timeout_millis: int = 30_000) -> bool: # noqa: ARG002
def force_flush(self, timeout_millis: int = 30_000) -> bool:
return True
fake_module = types.ModuleType("opentelemetry.exporter.otlp.proto.http.trace_exporter")
@@ -199,7 +206,8 @@ def test_otlp_fallback_includes_auth_and_custom_headers(monkeypatch, tmp_path) -
def test_traceloop_init_failure_does_not_mark_bootstrapped_on_provider_failure(
monkeypatch, tmp_path
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
monkeypatch.chdir(tmp_path)
@@ -226,7 +234,7 @@ def test_traceloop_init_failure_does_not_mark_bootstrapped_on_provider_failure(
assert tracer._remote_export_enabled is False
def test_run_completed_event_emitted_once(monkeypatch, tmp_path) -> None:
def test_run_completed_event_emitted_once(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.chdir(tmp_path)
tracer = Tracer("single-complete")
@@ -240,7 +248,9 @@ def test_run_completed_event_emitted_once(monkeypatch, tmp_path) -> None:
assert len(run_completed) == 1
def test_events_with_agent_id_include_agent_name(monkeypatch, tmp_path) -> None:
def test_events_with_agent_id_include_agent_name(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.chdir(tmp_path)
tracer = Tracer("agent-name-enrichment")
@@ -256,61 +266,32 @@ def test_events_with_agent_id_include_agent_name(monkeypatch, tmp_path) -> None:
assert chat_event["actor"]["agent_name"] == "Root Agent"
def test_get_total_llm_stats_includes_completed_subagents(monkeypatch, tmp_path) -> None:
def test_get_total_llm_stats_aggregates_live_and_completed(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.chdir(tmp_path)
class DummyStats:
def __init__(
self,
*,
input_tokens: int,
output_tokens: int,
cached_tokens: int,
cost: float,
requests: int,
) -> None:
self.input_tokens = input_tokens
self.output_tokens = output_tokens
self.cached_tokens = cached_tokens
self.cost = cost
self.requests = requests
class DummyLLM:
def __init__(self, stats: DummyStats) -> None:
self._total_stats = stats
class DummyAgent:
def __init__(self, stats: DummyStats) -> None:
self.llm = DummyLLM(stats)
tracer = Tracer("cost-rollup")
set_global_tracer(tracer)
monkeypatch.setattr(
agents_graph_actions,
"_agent_instances",
{
"root-agent": DummyAgent(
DummyStats(
input_tokens=1_000,
output_tokens=250,
cached_tokens=100,
cost=0.12345,
requests=2,
)
)
},
# Live agent (still running).
tracer.record_llm_usage(
agent_id="root-agent",
input_tokens=1_000,
output_tokens=250,
cached_tokens=100,
cost=0.12345,
requests=2,
bucket="live",
)
monkeypatch.setattr(
agents_graph_actions,
"_completed_agent_llm_totals",
{
"input_tokens": 2_000,
"output_tokens": 500,
"cached_tokens": 400,
"cost": 0.54321,
"requests": 3,
},
# Completed agents (finalized — moved by on_agent_end hook).
tracer.record_llm_usage(
agent_id="child-1",
input_tokens=2_000,
output_tokens=500,
cached_tokens=400,
cost=0.54321,
requests=3,
bucket="completed",
)
stats = tracer.get_total_llm_stats()
@@ -325,7 +306,9 @@ def test_get_total_llm_stats_includes_completed_subagents(monkeypatch, tmp_path)
assert stats["total_tokens"] == 3_750
def test_run_metadata_is_only_on_run_lifecycle_events(monkeypatch, tmp_path) -> None:
def test_run_metadata_is_only_on_run_lifecycle_events(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.chdir(tmp_path)
tracer = Tracer("metadata-scope")
@@ -345,7 +328,7 @@ def test_run_metadata_is_only_on_run_lifecycle_events(monkeypatch, tmp_path) ->
assert "run_metadata" not in chat_event
def test_set_run_name_resets_cached_paths(monkeypatch, tmp_path) -> None:
def test_set_run_name_resets_cached_paths(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.chdir(tmp_path)
tracer = Tracer()
@@ -364,7 +347,9 @@ def test_set_run_name_resets_cached_paths(monkeypatch, tmp_path) -> None:
assert any(event["event_type"] == "chat.message" for event in events)
def test_set_run_name_resets_run_completed_flag(monkeypatch, tmp_path) -> None:
def test_set_run_name_resets_run_completed_flag(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.chdir(tmp_path)
tracer = Tracer()
@@ -382,7 +367,9 @@ def test_set_run_name_resets_run_completed_flag(monkeypatch, tmp_path) -> None:
assert len(run_completed) == 1
def test_set_run_name_updates_traceloop_association_properties(monkeypatch, tmp_path) -> None:
def test_set_run_name_updates_traceloop_association_properties(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.chdir(tmp_path)
class FakeTraceloop:
@@ -407,7 +394,9 @@ def test_set_run_name_updates_traceloop_association_properties(monkeypatch, tmp_
assert FakeTraceloop.associations[-1]["run_name"] == "renamed-run"
def test_events_write_locks_are_scoped_by_events_file(monkeypatch, tmp_path) -> None:
def test_events_write_locks_are_scoped_by_events_file(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setenv("STRIX_TELEMETRY", "0")
@@ -422,7 +411,9 @@ def test_events_write_locks_are_scoped_by_events_file(monkeypatch, tmp_path) ->
assert lock_a_from_one is not lock_b
def test_tracer_skips_jsonl_when_telemetry_disabled(monkeypatch, tmp_path) -> None:
def test_tracer_skips_jsonl_when_telemetry_disabled(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setenv("STRIX_TELEMETRY", "0")
@@ -435,7 +426,9 @@ def test_tracer_skips_jsonl_when_telemetry_disabled(monkeypatch, tmp_path) -> No
assert not events_path.exists()
def test_tracer_otel_flag_overrides_global_telemetry(monkeypatch, tmp_path) -> None:
def test_tracer_otel_flag_overrides_global_telemetry(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setenv("STRIX_TELEMETRY", "0")
monkeypatch.setenv("STRIX_OTEL_TELEMETRY", "1")
+21 -21
View File
@@ -24,8 +24,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from strix.entry import _build_root_task, _build_scope_context, run_strix_scan
from strix.orchestration.bus import AgentMessageBus
from strix.sdk_entry import _build_root_task, _build_scope_context, run_strix_scan
# --- helpers ------------------------------------------------------------
@@ -140,18 +140,18 @@ async def test_run_strix_scan_wires_context_and_calls_runner(tmp_path: Path) ->
with (
patch(
"strix.sdk_entry.session_manager.create_or_reuse",
"strix.entry.session_manager.create_or_reuse",
new=AsyncMock(return_value=bundle),
) as create_mock,
patch(
"strix.sdk_entry.session_manager.cleanup",
"strix.entry.session_manager.cleanup",
new=AsyncMock(),
) as cleanup_mock,
patch("strix.sdk_entry.Runner.run", side_effect=fake_runner_run) as runner_mock,
patch("strix.entry.Runner.run", side_effect=fake_runner_run) as runner_mock,
# Stub the factory to avoid rendering the 158k-char prompt for
# every test (it's covered by sdk_prompt tests).
patch(
"strix.sdk_entry.build_strix_agent",
"strix.entry.build_strix_agent",
return_value=MagicMock(name="root_agent"),
) as factory_mock,
):
@@ -200,18 +200,18 @@ async def test_run_strix_scan_cleans_up_on_runner_failure(tmp_path: Path) -> Non
with (
patch(
"strix.sdk_entry.session_manager.create_or_reuse",
"strix.entry.session_manager.create_or_reuse",
new=AsyncMock(return_value=bundle),
),
patch(
"strix.sdk_entry.session_manager.cleanup",
"strix.entry.session_manager.cleanup",
new=AsyncMock(),
) as cleanup_mock,
patch(
"strix.sdk_entry.Runner.run",
"strix.entry.Runner.run",
side_effect=RuntimeError("simulated LLM blow-up"),
),
patch("strix.sdk_entry.build_strix_agent", return_value=MagicMock()),
patch("strix.entry.build_strix_agent", return_value=MagicMock()),
pytest.raises(RuntimeError, match="simulated LLM"),
):
await run_strix_scan(
@@ -233,15 +233,15 @@ async def test_run_strix_scan_skips_cleanup_when_disabled(tmp_path: Path) -> Non
with (
patch(
"strix.sdk_entry.session_manager.create_or_reuse",
"strix.entry.session_manager.create_or_reuse",
new=AsyncMock(return_value=bundle),
),
patch(
"strix.sdk_entry.session_manager.cleanup",
"strix.entry.session_manager.cleanup",
new=AsyncMock(),
) as cleanup_mock,
patch("strix.sdk_entry.Runner.run", side_effect=fake_runner_run),
patch("strix.sdk_entry.build_strix_agent", return_value=MagicMock()),
patch("strix.entry.Runner.run", side_effect=fake_runner_run),
patch("strix.entry.build_strix_agent", return_value=MagicMock()),
):
await run_strix_scan(
scan_config=_scan_config(),
@@ -267,12 +267,12 @@ async def test_run_strix_scan_auto_generates_scan_id(tmp_path: Path) -> None:
with (
patch(
"strix.sdk_entry.session_manager.create_or_reuse",
"strix.entry.session_manager.create_or_reuse",
new=AsyncMock(side_effect=fake_create),
),
patch("strix.sdk_entry.session_manager.cleanup", new=AsyncMock()),
patch("strix.sdk_entry.Runner.run", new=AsyncMock(return_value=MagicMock())),
patch("strix.sdk_entry.build_strix_agent", return_value=MagicMock()),
patch("strix.entry.session_manager.cleanup", new=AsyncMock()),
patch("strix.entry.Runner.run", new=AsyncMock(return_value=MagicMock())),
patch("strix.entry.build_strix_agent", return_value=MagicMock()),
):
await run_strix_scan(
scan_config=_scan_config(),
@@ -300,12 +300,12 @@ async def test_run_strix_scan_passes_scan_level_config_into_factory(
with (
patch(
"strix.sdk_entry.session_manager.create_or_reuse",
"strix.entry.session_manager.create_or_reuse",
new=AsyncMock(return_value=bundle),
),
patch("strix.sdk_entry.session_manager.cleanup", new=AsyncMock()),
patch("strix.sdk_entry.Runner.run", new=AsyncMock(return_value=MagicMock())),
patch("strix.sdk_entry.build_strix_agent", side_effect=fake_factory),
patch("strix.entry.session_manager.cleanup", new=AsyncMock()),
patch("strix.entry.Runner.run", new=AsyncMock(return_value=MagicMock())),
patch("strix.entry.build_strix_agent", side_effect=fake_factory),
):
await run_strix_scan(
scan_config=_scan_config(scan_mode="fast", is_whitebox=True),
-291
View File
@@ -1,291 +0,0 @@
from types import SimpleNamespace
import strix.agents as agents_module
from strix.llm.config import LLMConfig
from strix.tools.agents_graph import agents_graph_actions
def _reset_agent_graph_state() -> None:
agents_graph_actions._agent_graph["nodes"].clear()
agents_graph_actions._agent_graph["edges"].clear()
agents_graph_actions._agent_messages.clear()
agents_graph_actions._running_agents.clear()
agents_graph_actions._agent_instances.clear()
agents_graph_actions._completed_agent_llm_totals.clear()
agents_graph_actions._completed_agent_llm_totals.update(
agents_graph_actions._empty_llm_stats_totals()
)
agents_graph_actions._agent_states.clear()
def test_create_agent_inherits_parent_whitebox_flag(monkeypatch) -> None:
monkeypatch.setenv("STRIX_LLM", "openai/gpt-5")
_reset_agent_graph_state()
parent_id = "parent-agent"
parent_llm = LLMConfig(timeout=123, scan_mode="standard", is_whitebox=True)
agents_graph_actions._agent_instances[parent_id] = SimpleNamespace(
llm_config=parent_llm,
non_interactive=True,
)
captured_config: dict[str, object] = {}
class FakeStrixAgent:
def __init__(self, config: dict[str, object]):
captured_config["agent_config"] = config
class FakeThread:
def __init__(self, target, args, daemon, name):
self.target = target
self.args = args
self.daemon = daemon
self.name = name
def start(self) -> None:
return None
monkeypatch.setattr(agents_module, "StrixAgent", FakeStrixAgent)
monkeypatch.setattr(agents_graph_actions.threading, "Thread", FakeThread)
agent_state = SimpleNamespace(
agent_id=parent_id,
get_conversation_history=list,
)
result = agents_graph_actions.create_agent(
agent_state=agent_state,
task="source-aware child task",
name="SourceAwareChild",
inherit_context=False,
)
assert result["success"] is True
llm_config = captured_config["agent_config"]["llm_config"]
assert isinstance(llm_config, LLMConfig)
assert llm_config.timeout == 123
assert llm_config.scan_mode == "standard"
assert llm_config.is_whitebox is True
child_task = captured_config["agent_config"]["state"].task
assert "White-box execution guidance (recommended when source is available):" in child_task
assert "mandatory" not in child_task.lower()
def test_delegation_prompt_includes_wiki_memory_instruction_in_whitebox(monkeypatch) -> None:
monkeypatch.setenv("STRIX_LLM", "openai/gpt-5")
_reset_agent_graph_state()
parent_id = "parent-1"
child_id = "child-1"
agents_graph_actions._agent_graph["nodes"][parent_id] = {"name": "Parent", "status": "running"}
agents_graph_actions._agent_graph["nodes"][child_id] = {"name": "Child", "status": "running"}
class FakeState:
def __init__(self) -> None:
self.agent_id = child_id
self.agent_name = "Child"
self.parent_id = parent_id
self.task = "analyze source risks"
self.stop_requested = False
self.messages: list[tuple[str, str]] = []
def add_message(self, role: str, content: str) -> None:
self.messages.append((role, content))
def model_dump(self) -> dict[str, str]:
return {"agent_id": self.agent_id}
class FakeAgent:
def __init__(self) -> None:
self.llm_config = LLMConfig(is_whitebox=True)
async def agent_loop(self, _task: str) -> dict[str, bool]:
return {"ok": True}
state = FakeState()
agent = FakeAgent()
agents_graph_actions._agent_instances[child_id] = agent
result = agents_graph_actions._run_agent_in_thread(agent, state, inherited_messages=[])
assert result["result"] == {"ok": True}
task_messages = [msg for role, msg in state.messages if role == "user"]
assert task_messages
assert 'list_notes(category="wiki")' in task_messages[-1]
assert "get_note(note_id=...)" in task_messages[-1]
assert "Before agent_finish" in task_messages[-1]
def test_agent_finish_appends_wiki_update_for_whitebox(monkeypatch) -> None:
monkeypatch.setenv("STRIX_LLM", "openai/gpt-5")
_reset_agent_graph_state()
parent_id = "parent-2"
child_id = "child-2"
agents_graph_actions._agent_graph["nodes"][parent_id] = {
"name": "Parent",
"task": "parent task",
"status": "running",
"parent_id": None,
}
agents_graph_actions._agent_graph["nodes"][child_id] = {
"name": "Child",
"task": "child task",
"status": "running",
"parent_id": parent_id,
}
agents_graph_actions._agent_instances[child_id] = SimpleNamespace(
llm_config=LLMConfig(is_whitebox=True)
)
captured: dict[str, str] = {}
def fake_list_notes(category=None):
assert category == "wiki"
return {
"success": True,
"notes": [{"note_id": "wiki-note-1", "content": "Existing wiki content"}],
"total_count": 1,
}
captured_get: dict[str, str] = {}
def fake_get_note(note_id: str):
captured_get["note_id"] = note_id
return {
"success": True,
"note": {
"note_id": note_id,
"title": "Repo Wiki",
"content": "Existing wiki content",
},
}
def fake_append_note_content(note_id: str, delta: str):
captured["note_id"] = note_id
captured["delta"] = delta
return {"success": True, "note_id": note_id}
monkeypatch.setattr("strix.tools.notes.notes_actions.list_notes", fake_list_notes)
monkeypatch.setattr("strix.tools.notes.notes_actions.get_note", fake_get_note)
monkeypatch.setattr("strix.tools.notes.notes_actions.append_note_content", fake_append_note_content)
state = SimpleNamespace(agent_id=child_id, parent_id=parent_id)
result = agents_graph_actions.agent_finish(
agent_state=state,
result_summary="AST pass completed",
findings=["Found route sink candidate"],
success=True,
final_recommendations=["Validate sink with dynamic PoC"],
)
assert result["agent_completed"] is True
assert captured_get["note_id"] == "wiki-note-1"
assert captured["note_id"] == "wiki-note-1"
assert "Agent Update: Child" in captured["delta"]
assert "AST pass completed" in captured["delta"]
def test_run_agent_in_thread_injects_shared_wiki_context_in_whitebox(monkeypatch) -> None:
monkeypatch.setenv("STRIX_LLM", "openai/gpt-5")
_reset_agent_graph_state()
parent_id = "parent-3"
child_id = "child-3"
agents_graph_actions._agent_graph["nodes"][parent_id] = {"name": "Parent", "status": "running"}
agents_graph_actions._agent_graph["nodes"][child_id] = {"name": "Child", "status": "running"}
class FakeState:
def __init__(self) -> None:
self.agent_id = child_id
self.agent_name = "Child"
self.parent_id = parent_id
self.task = "map source"
self.stop_requested = False
self.messages: list[tuple[str, str]] = []
def add_message(self, role: str, content: str) -> None:
self.messages.append((role, content))
def model_dump(self) -> dict[str, str]:
return {"agent_id": self.agent_id}
class FakeAgent:
def __init__(self) -> None:
self.llm_config = LLMConfig(is_whitebox=True)
async def agent_loop(self, _task: str) -> dict[str, bool]:
return {"ok": True}
captured_get: dict[str, str] = {}
def fake_list_notes(category=None):
assert category == "wiki"
return {
"success": True,
"notes": [{"note_id": "wiki-ctx-1"}],
"total_count": 1,
}
def fake_get_note(note_id: str):
captured_get["note_id"] = note_id
return {
"success": True,
"note": {
"note_id": note_id,
"title": "Shared Repo Wiki",
"content": "Architecture: server/client split",
},
}
monkeypatch.setattr("strix.tools.notes.notes_actions.list_notes", fake_list_notes)
monkeypatch.setattr("strix.tools.notes.notes_actions.get_note", fake_get_note)
state = FakeState()
agent = FakeAgent()
agents_graph_actions._agent_instances[child_id] = agent
result = agents_graph_actions._run_agent_in_thread(agent, state, inherited_messages=[])
assert result["result"] == {"ok": True}
assert captured_get["note_id"] == "wiki-ctx-1"
user_messages = [content for role, content in state.messages if role == "user"]
assert user_messages
assert "<shared_repo_wiki" in user_messages[0]
assert "Architecture: server/client split" in user_messages[0]
def test_load_primary_wiki_note_prefers_repo_tag_match(monkeypatch) -> None:
selected_note_ids: list[str] = []
def fake_list_notes(category=None):
assert category == "wiki"
return {
"success": True,
"notes": [
{"note_id": "wiki-other", "tags": ["repo:other"]},
{"note_id": "wiki-target", "tags": ["repo:appsmith"]},
],
"total_count": 2,
}
def fake_get_note(note_id: str):
selected_note_ids.append(note_id)
return {
"success": True,
"note": {"note_id": note_id, "title": "Repo Wiki", "content": "content"},
}
monkeypatch.setattr("strix.tools.notes.notes_actions.list_notes", fake_list_notes)
monkeypatch.setattr("strix.tools.notes.notes_actions.get_note", fake_get_note)
agent_state = SimpleNamespace(
task="analyze /workspace/appsmith",
context={"whitebox_repo_tags": ["repo:appsmith"]},
)
note = agents_graph_actions._load_primary_wiki_note(agent_state)
assert note is not None
assert note["note_id"] == "wiki-target"
assert selected_note_ids == ["wiki-target"]
@@ -29,7 +29,7 @@ import pytest
from agents.tool import FunctionTool
from strix.orchestration.bus import AgentMessageBus
from strix.tools.agents_graph.agents_graph_sdk_tools import (
from strix.tools.agents_graph.tools import (
agent_finish,
agent_status,
create_agent,
@@ -289,7 +289,7 @@ async def test_create_agent_spawns_and_registers_child() -> None:
)
with patch(
"strix.tools.agents_graph.agents_graph_sdk_tools.Runner.run",
"strix.tools.agents_graph.tools.Runner.run",
side_effect=fake_runner_run,
):
out = await _invoke(
@@ -361,7 +361,7 @@ async def test_create_agent_inherits_parent_history() -> None:
)
with patch(
"strix.tools.agents_graph.agents_graph_sdk_tools.Runner.run",
"strix.tools.agents_graph.tools.Runner.run",
side_effect=fake_runner_run,
):
await _invoke(
@@ -493,7 +493,7 @@ async def test_create_agent_spawn_is_cancelable_via_bus() -> None:
runner_mock = AsyncMock(side_effect=slow_runner_run)
with patch(
"strix.tools.agents_graph.agents_graph_sdk_tools.Runner.run",
"strix.tools.agents_graph.tools.Runner.run",
new=runner_mock,
):
out = await _invoke(
-139
View File
@@ -1,139 +0,0 @@
from typing import Any
from strix.tools.agents_graph import agents_graph_actions
from strix.tools.load_skill import load_skill_actions
class _DummyLLM:
def __init__(self, initial_skills: list[str] | None = None) -> None:
self.loaded: set[str] = set(initial_skills or [])
def add_skills(self, skill_names: list[str]) -> list[str]:
newly_loaded = [skill for skill in skill_names if skill not in self.loaded]
self.loaded.update(newly_loaded)
return newly_loaded
class _DummyAgent:
def __init__(self, initial_skills: list[str] | None = None) -> None:
self.llm = _DummyLLM(initial_skills)
class _DummyAgentState:
def __init__(self, agent_id: str) -> None:
self.agent_id = agent_id
self.context: dict[str, Any] = {}
def update_context(self, key: str, value: Any) -> None:
self.context[key] = value
def test_load_skill_success_and_context_update() -> None:
instances = agents_graph_actions.__dict__["_agent_instances"]
original_instances = dict(instances)
try:
state = _DummyAgentState("agent_test_load_skill_success")
instances.clear()
instances[state.agent_id] = _DummyAgent()
result = load_skill_actions.load_skill(state, "ffuf,xss")
assert result["success"] is True
assert result["loaded_skills"] == ["ffuf", "xss"]
assert result["newly_loaded_skills"] == ["ffuf", "xss"]
assert state.context["loaded_skills"] == ["ffuf", "xss"]
finally:
instances.clear()
instances.update(original_instances)
def test_load_skill_uses_same_plain_skill_format_as_create_agent() -> None:
instances = agents_graph_actions.__dict__["_agent_instances"]
original_instances = dict(instances)
try:
state = _DummyAgentState("agent_test_load_skill_short_name")
instances.clear()
instances[state.agent_id] = _DummyAgent()
result = load_skill_actions.load_skill(state, "nmap")
assert result["success"] is True
assert result["loaded_skills"] == ["nmap"]
assert result["newly_loaded_skills"] == ["nmap"]
assert state.context["loaded_skills"] == ["nmap"]
finally:
instances.clear()
instances.update(original_instances)
def test_load_skill_invalid_skill_returns_error() -> None:
instances = agents_graph_actions.__dict__["_agent_instances"]
original_instances = dict(instances)
try:
state = _DummyAgentState("agent_test_load_skill_invalid")
instances.clear()
instances[state.agent_id] = _DummyAgent()
result = load_skill_actions.load_skill(state, "definitely_not_a_real_skill")
assert result["success"] is False
assert "Invalid skills" in result["error"]
assert "Available skills" in result["error"]
finally:
instances.clear()
instances.update(original_instances)
def test_load_skill_rejects_more_than_five_skills() -> None:
instances = agents_graph_actions.__dict__["_agent_instances"]
original_instances = dict(instances)
try:
state = _DummyAgentState("agent_test_load_skill_too_many")
instances.clear()
instances[state.agent_id] = _DummyAgent()
result = load_skill_actions.load_skill(state, "a,b,c,d,e,f")
assert result["success"] is False
assert result["error"] == (
"Cannot specify more than 5 skills for an agent (use comma-separated format)"
)
finally:
instances.clear()
instances.update(original_instances)
def test_load_skill_missing_agent_instance_returns_error() -> None:
instances = agents_graph_actions.__dict__["_agent_instances"]
original_instances = dict(instances)
try:
state = _DummyAgentState("agent_test_load_skill_missing_instance")
instances.clear()
result = load_skill_actions.load_skill(state, "httpx")
assert result["success"] is False
assert "running agent instance" in result["error"]
finally:
instances.clear()
instances.update(original_instances)
def test_load_skill_does_not_reload_skill_already_present_from_agent_creation() -> None:
instances = agents_graph_actions.__dict__["_agent_instances"]
original_instances = dict(instances)
try:
state = _DummyAgentState("agent_test_load_skill_existing_config_skill")
instances.clear()
instances[state.agent_id] = _DummyAgent(["xss"])
result = load_skill_actions.load_skill(state, "xss,sql_injection")
assert result["success"] is True
assert result["loaded_skills"] == ["xss", "sql_injection"]
assert result["newly_loaded_skills"] == ["sql_injection"]
assert result["already_loaded_skills"] == ["xss"]
assert state.context["loaded_skills"] == ["sql_injection", "xss"]
finally:
instances.clear()
instances.update(original_instances)
@@ -20,16 +20,16 @@ from unittest.mock import patch
import pytest
from agents.tool import FunctionTool
from strix.tools.notes import notes_actions as _notes_legacy
from strix.tools.notes.notes_sdk_tools import (
from strix.tools.notes import notes_actions as _notes_impl
from strix.tools.notes.tools import (
create_note,
delete_note,
get_note,
list_notes,
update_note,
)
from strix.tools.thinking.thinking_sdk_tools import think
from strix.tools.todo.todo_sdk_tools import (
from strix.tools.thinking.tool import think
from strix.tools.todo.tools import (
create_todo,
delete_todo,
list_todos,
@@ -190,10 +190,10 @@ def notes_run_dir(tmp_path: Path) -> Iterator[Path]:
"""Point the legacy notes module at a fresh run dir per test."""
run_dir = tmp_path / "strix_runs" / "test"
run_dir.mkdir(parents=True)
_notes_legacy._notes_storage.clear()
_notes_legacy._loaded_notes_run_dir = None
_notes_impl._notes_storage.clear()
_notes_impl._loaded_notes_run_dir = None
with patch.object(_notes_legacy, "_get_run_dir", return_value=run_dir):
with patch.object(_notes_impl, "_get_run_dir", return_value=run_dir):
yield run_dir
@@ -12,7 +12,6 @@ sandbox-bound tools route through ``post_to_sandbox``.
from __future__ import annotations
import json
from collections.abc import Iterator
from dataclasses import dataclass, field
from typing import Any
from unittest.mock import patch
@@ -20,15 +19,15 @@ from unittest.mock import patch
import pytest
from agents.tool import FunctionTool
from strix.tools.file_edit.file_edit_sdk_tools import (
from strix.tools.file_edit.tools import (
list_files,
search_files,
str_replace_editor,
)
from strix.tools.finish.finish_sdk_tool import finish_scan
from strix.tools.load_skill.load_skill_sdk_tool import load_skill
from strix.tools.reporting.reporting_sdk_tools import create_vulnerability_report
from strix.tools.web_search.web_search_sdk_tool import web_search
from strix.tools.finish.tool import finish_scan
from strix.tools.load_skill.tool import load_skill
from strix.tools.reporting.tool import create_vulnerability_report
from strix.tools.web_search.tool import web_search
@dataclass
@@ -93,7 +92,7 @@ async def test_web_search_no_api_key_returns_structured_error(
@pytest.mark.asyncio
async def test_web_search_delegates_to_legacy(monkeypatch: pytest.MonkeyPatch) -> None:
async def test_web_search_delegates_to_impl(monkeypatch: pytest.MonkeyPatch) -> None:
"""Legacy ``web_search`` returns dict; wrapper JSON-encodes it."""
monkeypatch.setenv("PERPLEXITY_API_KEY", "fake-key")
@@ -104,7 +103,7 @@ async def test_web_search_delegates_to_legacy(monkeypatch: pytest.MonkeyPatch) -
"message": "Web search completed successfully",
}
with patch(
"strix.tools.web_search.web_search_sdk_tool._legacy.web_search",
"strix.tools.web_search.tool._impl.web_search",
return_value=fake_result,
) as legacy:
out = await _invoke(web_search, _ctx_for(), query="xss techniques")
@@ -121,7 +120,7 @@ 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.file_edit_sdk_tools.post_to_sandbox",
"strix.tools.file_edit.tools.post_to_sandbox",
return_value=fake_response,
) as dispatch:
out = await _invoke(
@@ -147,7 +146,7 @@ async def test_str_replace_editor_routes_to_sandbox() -> None:
async def test_list_files_routes_to_sandbox() -> None:
fake_response = {"result": {"files": ["a.py"], "directories": []}}
with patch(
"strix.tools.file_edit.file_edit_sdk_tools.post_to_sandbox",
"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)
@@ -162,7 +161,7 @@ async def test_list_files_routes_to_sandbox() -> None:
async def test_search_files_routes_to_sandbox() -> None:
fake_response = {"result": {"output": "src/foo.py:1:match"}}
with patch(
"strix.tools.file_edit.file_edit_sdk_tools.post_to_sandbox",
"strix.tools.file_edit.tools.post_to_sandbox",
return_value=fake_response,
) as dispatch:
out = await _invoke(
@@ -204,7 +203,7 @@ async def test_create_vulnerability_report_validates_required_fields() -> None:
@pytest.mark.asyncio
async def test_create_vulnerability_report_delegates_to_legacy() -> None:
async def test_create_vulnerability_report_delegates_to_impl() -> None:
"""Verify the wrapper passes all params through to the legacy function."""
fake_result = {
"success": True,
@@ -214,7 +213,7 @@ async def test_create_vulnerability_report_delegates_to_legacy() -> None:
"cvss_score": 7.5,
}
with patch(
"strix.tools.reporting.reporting_sdk_tools._legacy.create_vulnerability_report",
"strix.tools.reporting.tool._impl.create_vulnerability_report",
return_value=fake_result,
) as legacy:
out = await _invoke(
@@ -255,7 +254,7 @@ async def test_load_skill_passes_adapter_with_agent_id() -> None:
return {"success": True, "loaded_skills": ["recon"]}
with patch(
"strix.tools.load_skill.load_skill_sdk_tool._legacy.load_skill",
"strix.tools.load_skill.tool._impl.load_skill",
side_effect=fake_legacy,
):
out = await _invoke(load_skill, _ctx_for("agent-XYZ"), skills="recon")
@@ -276,28 +275,7 @@ async def test_load_skill_with_empty_input() -> None:
# --- finish_scan ---------------------------------------------------------
@pytest.fixture
def isolated_agent_graph() -> Iterator[None]:
"""Clear the legacy agent-graph globals so finish_scan sees an empty world.
The legacy ``_check_active_agents`` reads ``_agent_graph["nodes"]`` and
returns an "agents still active" error if any non-self agent is in
state ``running`` or ``stopping``. Tests in other modules (legacy
multi-agent tests) populate this dict; without isolation they bleed
into our validation tests and mask the field-validation path.
"""
from strix.tools.agents_graph import agents_graph_actions
saved_nodes = agents_graph_actions._agent_graph.get("nodes", {}).copy()
agents_graph_actions._agent_graph["nodes"] = {}
try:
yield
finally:
agents_graph_actions._agent_graph["nodes"] = saved_nodes
@pytest.mark.asyncio
@pytest.mark.usefixtures("isolated_agent_graph")
async def test_finish_scan_validates_empty_fields() -> None:
"""Legacy validation: every section must be non-empty."""
out = await _invoke(
@@ -313,8 +291,7 @@ async def test_finish_scan_validates_empty_fields() -> None:
@pytest.mark.asyncio
@pytest.mark.usefixtures("isolated_agent_graph")
async def test_finish_scan_delegates_to_legacy() -> None:
async def test_finish_scan_delegates_to_impl() -> None:
"""Wrapper must pass the legacy adapter and the four sections through."""
fake_result = {
"success": True,
@@ -323,7 +300,7 @@ async def test_finish_scan_delegates_to_legacy() -> None:
"vulnerabilities_found": 3,
}
with patch(
"strix.tools.finish.finish_sdk_tool._legacy.finish_scan",
"strix.tools.finish.tool._impl.finish_scan",
return_value=fake_result,
) as legacy:
out = await _invoke(
@@ -27,8 +27,8 @@ from unittest.mock import patch
import pytest
from agents.tool import FunctionTool
from strix.tools.browser.browser_sdk_tool import browser_action
from strix.tools.proxy.proxy_sdk_tools import (
from strix.tools.browser.tool import browser_action
from strix.tools.proxy.tools import (
list_requests,
list_sitemap,
repeat_request,
@@ -37,8 +37,8 @@ from strix.tools.proxy.proxy_sdk_tools import (
view_request,
view_sitemap_entry,
)
from strix.tools.python.python_sdk_tool import python_action
from strix.tools.terminal.terminal_sdk_tool import terminal_execute
from strix.tools.python.tool import python_action
from strix.tools.terminal.tool import terminal_execute
_ALL_SANDBOX_TOOLS = (
@@ -119,7 +119,7 @@ async def test_browser_action_dispatches_full_payload() -> None:
(the in-container handler distinguishes ``None`` from missing)."""
fake = {"result": {"screenshot": "data:image/png;base64,..."}}
with patch(
"strix.tools.browser.browser_sdk_tool.post_to_sandbox",
"strix.tools.browser.tool.post_to_sandbox",
return_value=fake,
) as dispatch:
out = await _invoke(
@@ -158,7 +158,7 @@ async def test_browser_action_dispatches_full_payload() -> None:
async def test_terminal_execute_dispatches() -> None:
fake = {"result": {"content": "hello\n", "exit_code": 0}}
with patch(
"strix.tools.terminal.terminal_sdk_tool.post_to_sandbox",
"strix.tools.terminal.tool.post_to_sandbox",
return_value=fake,
) as dispatch:
out = await _invoke(
@@ -185,7 +185,7 @@ async def test_terminal_execute_dispatches() -> None:
async def test_python_action_dispatches() -> None:
fake = {"result": {"stdout": "42\n", "is_running": False}}
with patch(
"strix.tools.python.python_sdk_tool.post_to_sandbox",
"strix.tools.python.tool.post_to_sandbox",
return_value=fake,
) as dispatch:
out = await _invoke(
@@ -214,7 +214,7 @@ async def test_python_action_dispatches() -> None:
async def test_list_requests_forwards_full_query() -> None:
fake: dict[str, Any] = {"result": {"requests": []}}
with patch(
"strix.tools.proxy.proxy_sdk_tools.post_to_sandbox",
"strix.tools.proxy.tools.post_to_sandbox",
return_value=fake,
) as dispatch:
await _invoke(
@@ -241,7 +241,7 @@ async def test_list_requests_forwards_full_query() -> None:
async def test_view_request_dispatches() -> None:
fake = {"result": {"raw": "GET / HTTP/1.1..."}}
with patch(
"strix.tools.proxy.proxy_sdk_tools.post_to_sandbox",
"strix.tools.proxy.tools.post_to_sandbox",
return_value=fake,
) as dispatch:
await _invoke(
@@ -264,7 +264,7 @@ async def test_send_request_normalizes_missing_headers() -> None:
"""Legacy schema treats omitted ``headers`` as ``{}``; the wrapper must too."""
fake = {"result": {"status": 200}}
with patch(
"strix.tools.proxy.proxy_sdk_tools.post_to_sandbox",
"strix.tools.proxy.tools.post_to_sandbox",
return_value=fake,
) as dispatch:
await _invoke(
@@ -286,7 +286,7 @@ async def test_send_request_normalizes_missing_headers() -> None:
async def test_repeat_request_normalizes_missing_modifications() -> None:
fake = {"result": {"status": 200}}
with patch(
"strix.tools.proxy.proxy_sdk_tools.post_to_sandbox",
"strix.tools.proxy.tools.post_to_sandbox",
return_value=fake,
) as dispatch:
await _invoke(repeat_request, _ctx_for(), request_id="req-1")
@@ -300,7 +300,7 @@ async def test_repeat_request_normalizes_missing_modifications() -> None:
async def test_scope_rules_dispatches() -> None:
fake = {"result": {"scope_id": "s-1"}}
with patch(
"strix.tools.proxy.proxy_sdk_tools.post_to_sandbox",
"strix.tools.proxy.tools.post_to_sandbox",
return_value=fake,
) as dispatch:
await _invoke(
@@ -323,7 +323,7 @@ async def test_scope_rules_dispatches() -> None:
async def test_list_sitemap_defaults() -> None:
fake: dict[str, Any] = {"result": {"entries": []}}
with patch(
"strix.tools.proxy.proxy_sdk_tools.post_to_sandbox",
"strix.tools.proxy.tools.post_to_sandbox",
return_value=fake,
) as dispatch:
await _invoke(list_sitemap, _ctx_for())
@@ -338,7 +338,7 @@ async def test_list_sitemap_defaults() -> None:
async def test_view_sitemap_entry_dispatches() -> None:
fake = {"result": {"entry_id": "e-1"}}
with patch(
"strix.tools.proxy.proxy_sdk_tools.post_to_sandbox",
"strix.tools.proxy.tools.post_to_sandbox",
return_value=fake,
) as dispatch:
await _invoke(view_sitemap_entry, _ctx_for(), entry_id="e-1")
+7 -2
View File
@@ -21,9 +21,15 @@ def _reload_tools_module() -> ModuleType:
return importlib.import_module("strix.tools")
def test_non_sandbox_registers_agents_graph_but_not_browser_or_web_search_when_disabled(
def test_non_sandbox_skips_browser_and_web_search_when_disabled(
monkeypatch: Any,
) -> None:
"""Browser registration is gated on STRIX_DISABLE_BROWSER and
web_search on PERPLEXITY_API_KEY; both should stay out of the
in-container ``register_tool`` registry when their gates are off.
Agents_graph is no longer in this registry — those tools are SDK
function tools (host-side only), not in-container tools.
"""
monkeypatch.setenv("STRIX_SANDBOX_MODE", "false")
monkeypatch.setenv("STRIX_DISABLE_BROWSER", "true")
monkeypatch.delenv("PERPLEXITY_API_KEY", raising=False)
@@ -32,7 +38,6 @@ def test_non_sandbox_registers_agents_graph_but_not_browser_or_web_search_when_d
tools = _reload_tools_module()
names = set(tools.get_tool_names())
assert "create_agent" in names
assert "browser_action" not in names
assert "web_search" not in names