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:
@@ -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"
|
||||
@@ -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)
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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")
|
||||
|
||||
@@ -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),
|
||||
@@ -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(
|
||||
@@ -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
|
||||
|
||||
|
||||
+15
-38
@@ -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")
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user