Enhance documentation and refactor code for clarity and functionality

- Updated README.md to clarify tool allowlist supports glob wildcards.
- Enhanced docstrings in prompts.py, utils.py, and formatter.py for better understanding of function parameters and return values.
- Refactored test cases in test_llm.py and test_onboard.py to use consistent mocking style with unittest.mock.patch.
- Improved test coverage and clarity in test_skills_manager.py and test_stream_state.py by adding descriptive comments and organizing sections.
- Adjusted tool result formatting logic in formatter.py to streamline success checks.
- Ensured backward compatibility in various modules while enhancing functionality.
This commit is contained in:
X-iZhang
2026-02-13 18:54:33 +00:00
parent b50299534f
commit 418f2d58a7
16 changed files with 299 additions and 219 deletions
+6 -15
View File
@@ -43,9 +43,6 @@ from .paths import (
_config = get_effective_config()
apply_config_to_env(_config)
# Backend mode: "sandbox" (with execute) or "filesystem" (read/write only)
BACKEND_MODE = "sandbox"
# Research limits (from config)
MAX_CONCURRENT = _config.max_concurrent
MAX_ITERATIONS = _config.max_iterations
@@ -78,18 +75,12 @@ chat_model = get_chat_model(
provider=_config.provider,
)
# Initialize workspace backend based on mode
if BACKEND_MODE == "sandbox":
_workspace_backend = CustomSandboxBackend(
root_dir=WORKSPACE_DIR,
virtual_mode=True,
timeout=300,
)
else:
_workspace_backend = FilesystemBackend(
root_dir=WORKSPACE_DIR,
virtual_mode=True,
)
# Initialize workspace backend
_workspace_backend = CustomSandboxBackend(
root_dir=WORKSPACE_DIR,
virtual_mode=True,
timeout=300,
)
# Skills backend: merge user-installed (./skills/) and system (package) skills
_skills_backend = MergedReadOnlyBackend(
+12
View File
@@ -43,6 +43,17 @@ _EXPORTS: dict[str, tuple[str, str]] = {
def __getattr__(name: str):
"""Lazily import and cache package-level attributes.
Args:
name: The attribute name to look up.
Returns:
The resolved attribute value.
Raises:
AttributeError: If the name is not in _EXPORTS.
"""
target = _EXPORTS.get(name)
if target is None:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -56,6 +67,7 @@ def __getattr__(name: str):
def __dir__() -> list[str]:
"""List available public attributes including lazy exports."""
return sorted(set(globals()) | set(_EXPORTS))
+3 -3
View File
@@ -41,7 +41,7 @@ def onboard(
Guides you through configuring API keys, model selection,
workspace settings, and agent parameters.
"""
from ..onboard import run_onboard
from ..config import run_onboard
run_onboard(skip_validation=skip_validation)
@@ -194,7 +194,7 @@ def mcp_add(
target: str = typer.Argument(..., help="Command (stdio) or URL (http/sse)"),
args: Optional[list[str]] = typer.Argument(None, help="Extra args for stdio command"),
transport: Optional[str] = typer.Option(None, "--transport", "-T", help="Transport type (default: auto-detect)"),
tools: Optional[str] = typer.Option(None, "--tools", "-t", help="Comma-separated tool allowlist"),
tools: Optional[str] = typer.Option(None, "--tools", "-t", help="Comma-separated tool allowlist (supports wildcards: *_exa, read_*)"),
expose_to: Optional[str] = typer.Option(None, "--expose-to", "-e", help="Comma-separated target agents"),
header: Optional[list[str]] = typer.Option(None, "--header", "-H", help="HTTP header as Key:Value (repeatable)"),
env: Optional[list[str]] = typer.Option(None, "--env", help="Env var as KEY=VALUE for stdio (repeatable)"),
@@ -244,7 +244,7 @@ def mcp_edit(
transport: Optional[str] = typer.Option(None, "--transport", help="New transport type"),
command: Optional[str] = typer.Option(None, "--command", help="New command (stdio)"),
url: Optional[str] = typer.Option(None, "--url", help="New URL (http/sse/websocket)"),
tools: Optional[str] = typer.Option(None, "--tools", "-t", help="Comma-separated tool allowlist ('none' to clear)"),
tools: Optional[str] = typer.Option(None, "--tools", "-t", help="Comma-separated tool allowlist, supports wildcards ('none' to clear)"),
expose_to: Optional[str] = typer.Option(None, "--expose-to", "-e", help="Comma-separated target agents ('none' to clear)"),
header: Optional[list[str]] = typer.Option(None, "--header", "-H", help="HTTP header as Key:Value (repeatable)"),
env: Optional[list[str]] = typer.Option(None, "--env", help="Env var as KEY=VALUE for stdio (repeatable)"),
+1 -1
View File
@@ -194,7 +194,7 @@ def _cmd_mcp_add(args_str: str) -> None:
console.print()
console.print("[dim]Options:[/dim]")
console.print(" --transport T Transport type (default: auto-detect)")
console.print(" --tools t1,t2 Tool allowlist")
console.print(" --tools t1,t2 Tool allowlist (supports wildcards: *_exa, read_*)")
console.print(" --expose-to a1,a2 Target agents (default: main)")
console.print(" --header Key:Value HTTP header (repeatable)")
console.print(" --env KEY=VALUE Env var for stdio (repeatable)")
+38
View File
@@ -0,0 +1,38 @@
"""Configuration package for EvoScientist.
Re-exports all public symbols from settings and onboard submodules
so that existing ``from EvoScientist.config import X`` imports continue
to work without modification.
"""
from .settings import (
get_config_dir,
get_config_path,
EvoScientistConfig,
load_config,
save_config,
reset_config,
get_config_value,
set_config_value,
list_config,
get_effective_config,
apply_config_to_env,
)
from .onboard import run_onboard
__all__ = [
# settings
"get_config_dir",
"get_config_path",
"EvoScientistConfig",
"load_config",
"save_config",
"reset_config",
"get_config_value",
"set_config_value",
"list_config",
"get_effective_config",
"apply_config_to_env",
# onboard
"run_onboard",
]
@@ -20,13 +20,13 @@ from rich.console import Console
from rich.panel import Panel
from rich.text import Text
from .config import (
from .settings import (
EvoScientistConfig,
load_config,
save_config,
get_config_path,
)
from .llm import get_models_for_provider
from ..llm import get_models_for_provider
console = Console()
@@ -919,7 +919,7 @@ def _step_skills() -> list[str]:
Returns:
List of skill sources that were selected (empty if skipped).
"""
from .paths import USER_SKILLS_DIR
from ..paths import USER_SKILLS_DIR
# Collect names of already-installed user skills
skills_dir = Path(USER_SKILLS_DIR)
@@ -975,7 +975,7 @@ def _step_skills() -> list[str]:
return []
from .tools.skills_manager import install_skill
from ..tools.skills_manager import install_skill
installed = []
for source in selected:
@@ -1070,7 +1070,7 @@ def _step_mcp_servers() -> list[str]:
Returns:
List of server names that were installed.
"""
from .mcp.client import _load_user_config, add_mcp_server
from ..mcp.client import _load_user_config, add_mcp_server
existing_config = _load_user_config()
@@ -1177,7 +1177,7 @@ def validate_imessage() -> tuple[bool, str]:
if sys.platform != "darwin":
return False, "iMessage requires macOS"
from .channels.imessage.probe import find_cli
from ..channels.imessage.probe import find_cli
cli_path = find_cli()
if not cli_path:
@@ -147,6 +147,29 @@ def reset_config() -> None:
# Config value operations
# =============================================================================
def _coerce_value(value: Any, field_type: Any) -> Any:
"""Coerce a value to the expected field type.
Args:
value: The value to coerce.
field_type: The target type (from dataclass field).
Returns:
The coerced value.
Raises:
ValueError: If the value cannot be coerced.
TypeError: If the value cannot be coerced.
"""
if field_type == "bool" or field_type is bool:
if isinstance(value, str):
return value.lower() in ("true", "1", "yes", "on")
return bool(value)
if field_type == "int" or field_type is int:
return int(value)
return str(value)
def get_config_value(key: str) -> Any:
"""Get a single configuration value.
@@ -181,15 +204,7 @@ def set_config_value(key: str, value: Any) -> bool:
field_type = field_info.type
try:
if field_type == "bool" or field_type is bool:
if isinstance(value, str):
value = value.lower() in ("true", "1", "yes", "on")
else:
value = bool(value)
elif field_type == "int" or field_type is int:
value = int(value)
elif field_type == "str" or field_type is str:
value = str(value)
value = _coerce_value(value, field_type)
except (ValueError, TypeError):
return False
@@ -250,18 +265,11 @@ def get_effective_config(cli_overrides: dict[str, Any] | None = None) -> EvoScie
for config_key, env_key in _ENV_MAPPINGS.items():
env_value = os.environ.get(env_key)
if env_value:
# Type coercion
field_info = next(f for f in fields(EvoScientistConfig) if f.name == config_key)
field_type = field_info.type
if field_type == "bool" or field_type is bool:
data[config_key] = env_value.lower() in ("true", "1", "yes", "on")
elif field_type == "int" or field_type is int:
try:
data[config_key] = int(env_value)
except ValueError:
pass
else:
data[config_key] = env_value
try:
data[config_key] = _coerce_value(env_value, field_info.type)
except (ValueError, TypeError):
pass
# Apply CLI overrides (highest priority)
if cli_overrides:
+1 -1
View File
@@ -44,7 +44,7 @@ More servers: [MCP Server Directory](https://github.com/modelcontextprotocol/ser
| `env` | No | Environment variables for subprocess |
| `url` | http/sse/ws | Server URL |
| `headers` | No | HTTP headers (e.g. auth tokens) |
| `tools` | No | Tool allowlist (omit = all tools) |
| `tools` | No | Tool allowlist with glob wildcards (omit = all tools) |
| `expose_to` | No | Target agents (default: `["main"]`) |
## Tool Routing
+9 -5
View File
@@ -265,13 +265,17 @@ Finding one with context [1]. Another insight [2].
# =============================================================================
def get_system_prompt(max_concurrent: int = 3, max_iterations: int = 3) -> str:
"""Generate the complete system prompt with configured limits."""
"""Generate the complete system prompt with configured limits.
Args:
max_concurrent: Maximum number of concurrent sub-agents.
max_iterations: Maximum number of delegation rounds.
Returns:
Combined system prompt string.
"""
delegation = DELEGATION_STRATEGY.format(
max_concurrent=max_concurrent,
max_iterations=max_iterations,
)
return EXPERIMENT_WORKFLOW + "\n" + delegation
# Default export (backward compatible)
SYSTEM_PROMPT = get_system_prompt()
+1 -5
View File
@@ -68,14 +68,10 @@ class ToolResultFormatter:
return ContentType.TEXT
def is_success(self, content: str) -> bool:
"""Check if content indicates successful execution."""
return _is_success(content)
def format(self, name: str, content: str, max_length: int = 800) -> FormattedResult:
"""Format tool result based on detected content type."""
content_type = self.detect_type(content)
success = self.is_success(content)
success = _is_success(content)
formatter_map = {
ContentType.SUCCESS: self._format_success,
+13 -6
View File
@@ -17,7 +17,14 @@ console = Console()
def format_message_content(message):
"""Convert message content to displayable string."""
"""Convert message content to displayable string.
Args:
message: A LangChain message object with content attribute.
Returns:
Formatted string representation of the message content.
"""
parts = []
tool_calls_processed = False
@@ -52,7 +59,11 @@ def format_message_content(message):
def format_messages(messages):
"""Format and display a list of messages with Rich formatting."""
"""Format and display a list of messages with Rich formatting.
Args:
messages: List of LangChain message objects to display.
"""
for m in messages:
msg_type = m.__class__.__name__.replace("Message", "")
content = format_message_content(m)
@@ -67,10 +78,6 @@ def format_messages(messages):
console.print(Panel(content, title=f"📝 {msg_type}", border_style="white"))
def format_message(messages):
"""Alias for format_messages for backward compatibility."""
return format_messages(messages)
def show_prompt(prompt_text: str, title: str = "Prompt", border_style: str = "blue"):
"""Display a prompt with rich formatting and XML tag highlighting.
+6 -6
View File
@@ -326,8 +326,8 @@ EvoSci mcp add brave-search http://localhost:8080/mcp -H "Authorization:Bearer $
# sse transport, routed to a specific agent
EvoSci mcp add my-sse http://localhost:9090/sse --transport sse -e research-agent
# With tool allowlist
EvoSci mcp add fs npx -- -y @modelcontextprotocol/server-filesystem /tmp -t read_file,write_file
# With tool allowlist (supports glob wildcards)
EvoSci mcp add fs npx -- -y @modelcontextprotocol/server-filesystem /tmp -t "read_*,write_*"
```
Or from the interactive CLI:
@@ -342,7 +342,7 @@ Or from the interactive CLI:
| Flag | Description |
|------|-------------|
| `--tools`, `-t` | Comma-separated tool allowlist (omit = all tools) |
| `--tools`, `-t` | Comma-separated tool allowlist, supports glob wildcards (omit = all tools) |
| `--expose-to`, `-e` | Comma-separated target agents (default: `main`) |
| `--header`, `-H` | HTTP header as `Key:Value` (repeatable) |
| `--env` | Env var as `KEY=VALUE` for stdio (repeatable) |
@@ -356,7 +356,7 @@ filesystem:
transport: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"]
tools: [read_file, write_file] # optional allowlist (omit = all tools)
tools: ["read_*", "write_*"] # optional allowlist with wildcards (omit = all tools)
expose_to: [main, code-agent] # optional routing (omit = ["main"])
brave-search:
@@ -395,8 +395,8 @@ Update individual fields on an existing server without re-adding it:
# Change routing
EvoSci mcp edit filesystem --expose-to main,code-agent
# Set a tool allowlist
EvoSci mcp edit filesystem --tools read_file,write_file
# Set a tool allowlist (supports glob wildcards)
EvoSci mcp edit filesystem --tools "read_*,write_*"
# Clear a tool allowlist (pass all tools)
EvoSci mcp edit filesystem --tools none
+11 -11
View File
@@ -1,6 +1,6 @@
"""Tests for EvoScientist LLM module."""
from unittest import mock
from unittest.mock import patch
from EvoScientist.llm import (
MODELS,
@@ -122,7 +122,7 @@ class TestGetModelInfo:
class TestGetChatModel:
@mock.patch("EvoScientist.llm.models.init_chat_model")
@patch("EvoScientist.llm.models.init_chat_model")
def test_uses_default_model_when_none(self, mock_init):
"""Test that get_chat_model uses default model when model=None."""
mock_init.return_value = "mock_model"
@@ -136,7 +136,7 @@ class TestGetChatModel:
assert call_kwargs["model"] == expected_model_id
assert call_kwargs["model_provider"] == expected_provider
@mock.patch("EvoScientist.llm.models.init_chat_model")
@patch("EvoScientist.llm.models.init_chat_model")
def test_resolves_short_name(self, mock_init):
"""Test that get_chat_model resolves short names correctly."""
mock_init.return_value = "mock_model"
@@ -147,7 +147,7 @@ class TestGetChatModel:
assert call_kwargs["model"] == "claude-opus-4-5-20251101"
assert call_kwargs["model_provider"] == "anthropic"
@mock.patch("EvoScientist.llm.models.init_chat_model")
@patch("EvoScientist.llm.models.init_chat_model")
def test_resolves_openai_short_name(self, mock_init):
"""Test that get_chat_model resolves OpenAI short names."""
mock_init.return_value = "mock_model"
@@ -158,7 +158,7 @@ class TestGetChatModel:
assert call_kwargs["model"] == "gpt-5-mini-2025-08-07"
assert call_kwargs["model_provider"] == "openai"
@mock.patch("EvoScientist.llm.models.init_chat_model")
@patch("EvoScientist.llm.models.init_chat_model")
def test_uses_full_model_id(self, mock_init):
"""Test that get_chat_model accepts full model IDs."""
mock_init.return_value = "mock_model"
@@ -170,7 +170,7 @@ class TestGetChatModel:
# Should infer anthropic from the model prefix
assert call_kwargs["model_provider"] == "anthropic"
@mock.patch("EvoScientist.llm.models.init_chat_model")
@patch("EvoScientist.llm.models.init_chat_model")
def test_provider_override(self, mock_init):
"""Test that provider can be overridden."""
mock_init.return_value = "mock_model"
@@ -180,7 +180,7 @@ class TestGetChatModel:
call_kwargs = mock_init.call_args[1]
assert call_kwargs["model_provider"] == "custom_provider"
@mock.patch("EvoScientist.llm.models.init_chat_model")
@patch("EvoScientist.llm.models.init_chat_model")
def test_passes_kwargs(self, mock_init):
"""Test that additional kwargs are passed through."""
mock_init.return_value = "mock_model"
@@ -191,7 +191,7 @@ class TestGetChatModel:
assert call_kwargs["temperature"] == 0.7
assert call_kwargs["max_tokens"] == 1000
@mock.patch("EvoScientist.llm.models.init_chat_model")
@patch("EvoScientist.llm.models.init_chat_model")
def test_infers_openai_from_gpt_prefix(self, mock_init):
"""Test that OpenAI is inferred from gpt- prefix."""
mock_init.return_value = "mock_model"
@@ -201,7 +201,7 @@ class TestGetChatModel:
call_kwargs = mock_init.call_args[1]
assert call_kwargs["model_provider"] == "openai"
@mock.patch("EvoScientist.llm.models.init_chat_model")
@patch("EvoScientist.llm.models.init_chat_model")
def test_infers_openai_from_o1_prefix(self, mock_init):
"""Test that OpenAI is inferred from o1 prefix."""
mock_init.return_value = "mock_model"
@@ -211,7 +211,7 @@ class TestGetChatModel:
call_kwargs = mock_init.call_args[1]
assert call_kwargs["model_provider"] == "openai"
@mock.patch("EvoScientist.llm.models.init_chat_model")
@patch("EvoScientist.llm.models.init_chat_model")
def test_infers_google_from_gemini_prefix(self, mock_init):
"""Test that google-genai is inferred from gemini prefix."""
mock_init.return_value = "mock_model"
@@ -221,7 +221,7 @@ class TestGetChatModel:
call_kwargs = mock_init.call_args[1]
assert call_kwargs["model_provider"] == "google-genai"
@mock.patch("EvoScientist.llm.models.init_chat_model")
@patch("EvoScientist.llm.models.init_chat_model")
def test_defaults_to_anthropic_for_unknown(self, mock_init):
"""Test that anthropic is default for unknown model prefixes."""
mock_init.return_value = "mock_model"
+122 -122
View File
@@ -1,11 +1,11 @@
"""Tests for EvoScientist onboarding wizard."""
import subprocess
from unittest import mock
from unittest.mock import patch, Mock, MagicMock
import pytest
from EvoScientist.onboard import (
from EvoScientist.config.onboard import (
IntegerValidator,
ChoiceValidator,
STEPS,
@@ -207,11 +207,11 @@ class TestChoiceValidator:
class TestStepProvider:
def test_returns_selected_provider(self):
"""Test that _step_provider returns selected provider."""
from EvoScientist.onboard import _step_provider
from EvoScientist.config.onboard import _step_provider
config = EvoScientistConfig()
with mock.patch("EvoScientist.onboard.questionary") as mock_q:
with patch("EvoScientist.config.onboard.questionary") as mock_q:
mock_q.select.return_value.ask.return_value = "anthropic"
result = _step_provider(config)
@@ -220,11 +220,11 @@ class TestStepProvider:
def test_raises_keyboard_interrupt_on_cancel(self):
"""Test that _step_provider raises KeyboardInterrupt on cancel."""
from EvoScientist.onboard import _step_provider
from EvoScientist.config.onboard import _step_provider
config = EvoScientistConfig()
with mock.patch("EvoScientist.onboard.questionary") as mock_q:
with patch("EvoScientist.config.onboard.questionary") as mock_q:
mock_q.select.return_value.ask.return_value = None
with pytest.raises(KeyboardInterrupt):
_step_provider(config)
@@ -233,11 +233,11 @@ class TestStepProvider:
class TestStepModel:
def test_returns_selected_model(self):
"""Test that _step_model returns selected model."""
from EvoScientist.onboard import _step_model
from EvoScientist.config.onboard import _step_model
config = EvoScientistConfig()
with mock.patch("EvoScientist.onboard.questionary") as mock_q:
with patch("EvoScientist.config.onboard.questionary") as mock_q:
mock_q.select.return_value.ask.return_value = "claude-sonnet-4-5"
result = _step_model(config, "anthropic")
@@ -245,11 +245,11 @@ class TestStepModel:
def test_raises_keyboard_interrupt_on_cancel(self):
"""Test that _step_model raises KeyboardInterrupt on cancel."""
from EvoScientist.onboard import _step_model
from EvoScientist.config.onboard import _step_model
config = EvoScientistConfig()
with mock.patch("EvoScientist.onboard.questionary") as mock_q:
with patch("EvoScientist.config.onboard.questionary") as mock_q:
mock_q.select.return_value.ask.return_value = None
with pytest.raises(KeyboardInterrupt):
_step_model(config, "anthropic")
@@ -258,11 +258,11 @@ class TestStepModel:
class TestStepWorkspace:
def test_returns_mode_and_empty_workdir(self):
"""Test workspace step with no custom directory."""
from EvoScientist.onboard import _step_workspace
from EvoScientist.config.onboard import _step_workspace
config = EvoScientistConfig()
with mock.patch("EvoScientist.onboard.questionary") as mock_q:
with patch("EvoScientist.config.onboard.questionary") as mock_q:
mock_q.select.return_value.ask.return_value = "daemon"
mock_q.confirm.return_value.ask.return_value = False # No custom dir
result = _step_workspace(config)
@@ -271,11 +271,11 @@ class TestStepWorkspace:
def test_returns_mode_and_custom_workdir(self):
"""Test workspace step with custom directory."""
from EvoScientist.onboard import _step_workspace
from EvoScientist.config.onboard import _step_workspace
config = EvoScientistConfig()
with mock.patch("EvoScientist.onboard.questionary") as mock_q:
with patch("EvoScientist.config.onboard.questionary") as mock_q:
mock_q.select.return_value.ask.return_value = "run"
mock_q.confirm.return_value.ask.return_value = True # Use custom dir
mock_q.text.return_value.ask.return_value = "/custom/path"
@@ -287,14 +287,14 @@ class TestStepWorkspace:
class TestValidateImessage:
def test_valid_when_cli_found_with_rpc(self):
"""Test validate_imessage returns valid when imsg CLI found and RPC works."""
from EvoScientist.onboard import validate_imessage
from EvoScientist.config.onboard import validate_imessage
version_result = mock.Mock(returncode=0, stdout="imsg 1.2.3")
rpc_result = mock.Mock(returncode=0)
version_result = Mock(returncode=0, stdout="imsg 1.2.3")
rpc_result = Mock(returncode=0)
with mock.patch("EvoScientist.onboard.sys") as mock_sys, \
mock.patch("EvoScientist.channels.imessage.probe.shutil") as mock_shutil, \
mock.patch("EvoScientist.onboard.subprocess") as mock_sub:
with patch("EvoScientist.config.onboard.sys") as mock_sys, \
patch("EvoScientist.channels.imessage.probe.shutil") as mock_shutil, \
patch("EvoScientist.config.onboard.subprocess") as mock_sub:
mock_sys.platform = "darwin"
mock_shutil.which.return_value = "/opt/homebrew/bin/imsg"
mock_sub.run.side_effect = [version_result, rpc_result]
@@ -306,10 +306,10 @@ class TestValidateImessage:
def test_invalid_when_cli_not_found(self):
"""Test validate_imessage returns not_installed when imsg CLI missing."""
from EvoScientist.onboard import validate_imessage
from EvoScientist.config.onboard import validate_imessage
with mock.patch("EvoScientist.onboard.sys") as mock_sys, \
mock.patch("EvoScientist.channels.imessage.probe.shutil") as mock_shutil:
with patch("EvoScientist.config.onboard.sys") as mock_sys, \
patch("EvoScientist.channels.imessage.probe.shutil") as mock_shutil:
mock_sys.platform = "darwin"
mock_shutil.which.return_value = None
valid, msg = validate_imessage()
@@ -319,9 +319,9 @@ class TestValidateImessage:
def test_invalid_on_non_macos(self):
"""Test validate_imessage returns invalid on non-macOS."""
from EvoScientist.onboard import validate_imessage
from EvoScientist.config.onboard import validate_imessage
with mock.patch("EvoScientist.onboard.sys") as mock_sys:
with patch("EvoScientist.config.onboard.sys") as mock_sys:
mock_sys.platform = "linux"
valid, msg = validate_imessage()
@@ -330,14 +330,14 @@ class TestValidateImessage:
def test_invalid_when_rpc_not_supported(self):
"""Test validate_imessage returns invalid when RPC check fails."""
from EvoScientist.onboard import validate_imessage
from EvoScientist.config.onboard import validate_imessage
version_result = mock.Mock(returncode=0, stdout="imsg 0.1.0")
rpc_result = mock.Mock(returncode=1)
version_result = Mock(returncode=0, stdout="imsg 0.1.0")
rpc_result = Mock(returncode=1)
with mock.patch("EvoScientist.onboard.sys") as mock_sys, \
mock.patch("EvoScientist.channels.imessage.probe.shutil") as mock_shutil, \
mock.patch("EvoScientist.onboard.subprocess") as mock_sub:
with patch("EvoScientist.config.onboard.sys") as mock_sys, \
patch("EvoScientist.channels.imessage.probe.shutil") as mock_shutil, \
patch("EvoScientist.config.onboard.subprocess") as mock_sub:
mock_sys.platform = "darwin"
mock_shutil.which.return_value = "/usr/local/bin/imsg"
mock_sub.run.side_effect = [version_result, rpc_result]
@@ -350,10 +350,10 @@ class TestValidateImessage:
class TestInstallImsg:
def test_install_success(self):
"""Test _install_imsg returns True on success."""
from EvoScientist.onboard import _install_imsg
from EvoScientist.config.onboard import _install_imsg
with mock.patch("EvoScientist.onboard.subprocess") as mock_sub:
mock_sub.run.return_value = mock.Mock(returncode=0)
with patch("EvoScientist.config.onboard.subprocess") as mock_sub:
mock_sub.run.return_value = Mock(returncode=0)
mock_sub.TimeoutExpired = subprocess.TimeoutExpired
result = _install_imsg()
@@ -361,10 +361,10 @@ class TestInstallImsg:
def test_install_brew_not_found(self):
"""Test _install_imsg handles missing Homebrew."""
from EvoScientist.onboard import _install_imsg
from EvoScientist.config.onboard import _install_imsg
with mock.patch("EvoScientist.onboard.subprocess") as mock_sub, \
mock.patch("EvoScientist.onboard.console"):
with patch("EvoScientist.config.onboard.subprocess") as mock_sub, \
patch("EvoScientist.config.onboard.console"):
mock_sub.run.side_effect = FileNotFoundError()
mock_sub.TimeoutExpired = subprocess.TimeoutExpired
result = _install_imsg()
@@ -373,10 +373,10 @@ class TestInstallImsg:
def test_install_failure(self):
"""Test _install_imsg returns False on non-zero exit."""
from EvoScientist.onboard import _install_imsg
from EvoScientist.config.onboard import _install_imsg
with mock.patch("EvoScientist.onboard.subprocess") as mock_sub:
mock_sub.run.return_value = mock.Mock(returncode=1)
with patch("EvoScientist.config.onboard.subprocess") as mock_sub:
mock_sub.run.return_value = Mock(returncode=1)
mock_sub.TimeoutExpired = subprocess.TimeoutExpired
result = _install_imsg()
@@ -386,32 +386,32 @@ class TestInstallImsg:
class TestSetupImessage:
def test_already_installed(self):
"""Test _setup_imessage returns True when already installed."""
from EvoScientist.onboard import _setup_imessage
from EvoScientist.config.onboard import _setup_imessage
with mock.patch("EvoScientist.onboard.validate_imessage", return_value=(True, "imsg at /bin/imsg")), \
mock.patch("EvoScientist.onboard.console"):
with patch("EvoScientist.config.onboard.validate_imessage", return_value=(True, "imsg at /bin/imsg")), \
patch("EvoScientist.config.onboard.console"):
result = _setup_imessage()
assert result is True
def test_not_macos(self):
"""Test _setup_imessage returns False on non-macOS."""
from EvoScientist.onboard import _setup_imessage
from EvoScientist.config.onboard import _setup_imessage
with mock.patch("EvoScientist.onboard.validate_imessage", return_value=(False, "iMessage requires macOS")), \
mock.patch("EvoScientist.onboard.console"):
with patch("EvoScientist.config.onboard.validate_imessage", return_value=(False, "iMessage requires macOS")), \
patch("EvoScientist.config.onboard.console"):
result = _setup_imessage()
assert result is False
def test_install_then_valid(self):
"""Test _setup_imessage installs and re-validates successfully."""
from EvoScientist.onboard import _setup_imessage
from EvoScientist.config.onboard import _setup_imessage
with mock.patch("EvoScientist.onboard.validate_imessage") as mock_val, \
mock.patch("EvoScientist.onboard._install_imsg", return_value=True), \
mock.patch("EvoScientist.onboard.questionary") as mock_q, \
mock.patch("EvoScientist.onboard.console"):
with patch("EvoScientist.config.onboard.validate_imessage") as mock_val, \
patch("EvoScientist.config.onboard._install_imsg", return_value=True), \
patch("EvoScientist.config.onboard.questionary") as mock_q, \
patch("EvoScientist.config.onboard.console"):
mock_val.side_effect = [
(False, "not_installed"), # First check
(True, "imsg at /bin/imsg"), # After install
@@ -423,11 +423,11 @@ class TestSetupImessage:
def test_user_declines_install(self):
"""Test _setup_imessage returns False when user declines install."""
from EvoScientist.onboard import _setup_imessage
from EvoScientist.config.onboard import _setup_imessage
with mock.patch("EvoScientist.onboard.validate_imessage", return_value=(False, "not_installed")), \
mock.patch("EvoScientist.onboard.questionary") as mock_q, \
mock.patch("EvoScientist.onboard.console"):
with patch("EvoScientist.config.onboard.validate_imessage", return_value=(False, "not_installed")), \
patch("EvoScientist.config.onboard.questionary") as mock_q, \
patch("EvoScientist.config.onboard.console"):
mock_q.confirm.return_value.ask.return_value = False
result = _setup_imessage()
@@ -437,10 +437,10 @@ class TestSetupImessage:
class TestStepSkills:
def test_returns_empty_when_none_selected(self):
"""Test skills step returns empty list when user selects nothing."""
from EvoScientist.onboard import _step_skills
from EvoScientist.config.onboard import _step_skills
with mock.patch("EvoScientist.onboard.questionary") as mock_q, \
mock.patch("EvoScientist.onboard.console"):
with patch("EvoScientist.config.onboard.questionary") as mock_q, \
patch("EvoScientist.config.onboard.console"):
mock_q.checkbox.return_value.ask.return_value = []
result = _step_skills()
@@ -448,13 +448,13 @@ class TestStepSkills:
def test_installs_selected_skills(self):
"""Test skills step installs selected skills and returns sources."""
from EvoScientist.onboard import _step_skills, _RECOMMENDED_SKILLS
from EvoScientist.config.onboard import _step_skills, _RECOMMENDED_SKILLS
source = _RECOMMENDED_SKILLS[0]["source"]
with mock.patch("EvoScientist.onboard.questionary") as mock_q, \
mock.patch("EvoScientist.onboard.console"), \
mock.patch("EvoScientist.tools.skills_manager.install_skill") as mock_install:
with patch("EvoScientist.config.onboard.questionary") as mock_q, \
patch("EvoScientist.config.onboard.console"), \
patch("EvoScientist.tools.skills_manager.install_skill") as mock_install:
mock_q.checkbox.return_value.ask.return_value = [source]
mock_install.return_value = {"success": True, "name": "test"}
result = _step_skills()
@@ -464,13 +464,13 @@ class TestStepSkills:
def test_handles_install_failure(self):
"""Test skills step handles installation errors gracefully."""
from EvoScientist.onboard import _step_skills, _RECOMMENDED_SKILLS
from EvoScientist.config.onboard import _step_skills, _RECOMMENDED_SKILLS
source = _RECOMMENDED_SKILLS[0]["source"]
with mock.patch("EvoScientist.onboard.questionary") as mock_q, \
mock.patch("EvoScientist.onboard.console"), \
mock.patch("EvoScientist.tools.skills_manager.install_skill") as mock_install:
with patch("EvoScientist.config.onboard.questionary") as mock_q, \
patch("EvoScientist.config.onboard.console"), \
patch("EvoScientist.tools.skills_manager.install_skill") as mock_install:
mock_q.checkbox.return_value.ask.return_value = [source]
mock_install.side_effect = Exception("network error")
result = _step_skills()
@@ -479,9 +479,9 @@ class TestStepSkills:
def test_raises_keyboard_interrupt_on_cancel(self):
"""Test skills step raises KeyboardInterrupt on cancel."""
from EvoScientist.onboard import _step_skills
from EvoScientist.config.onboard import _step_skills
with mock.patch("EvoScientist.onboard.questionary") as mock_q:
with patch("EvoScientist.config.onboard.questionary") as mock_q:
mock_q.checkbox.return_value.ask.return_value = None
with pytest.raises(KeyboardInterrupt):
_step_skills()
@@ -490,11 +490,11 @@ class TestStepSkills:
class TestStepChannels:
def test_returns_disabled_when_skip(self):
"""Test channels step returns disabled when user selects skip."""
from EvoScientist.onboard import _step_channels
from EvoScientist.config.onboard import _step_channels
config = EvoScientistConfig()
with mock.patch("EvoScientist.onboard.questionary") as mock_q:
with patch("EvoScientist.config.onboard.questionary") as mock_q:
mock_q.select.return_value.ask.return_value = "skip"
result = _step_channels(config)
@@ -502,17 +502,17 @@ class TestStepChannels:
def test_returns_enabled_when_setup_passes(self):
"""Test channels step returns enabled when setup succeeds."""
from EvoScientist.onboard import _step_channels
from EvoScientist.config.onboard import _step_channels
config = EvoScientistConfig()
select_mock_1 = mock.MagicMock()
select_mock_1 = MagicMock()
select_mock_1.ask.return_value = "imessage"
select_mock_2 = mock.MagicMock()
select_mock_2 = MagicMock()
select_mock_2.ask.return_value = True
with mock.patch("EvoScientist.onboard.questionary") as mock_q, \
mock.patch("EvoScientist.onboard._setup_imessage", return_value=True):
with patch("EvoScientist.config.onboard.questionary") as mock_q, \
patch("EvoScientist.config.onboard._setup_imessage", return_value=True):
mock_q.select.side_effect = [select_mock_1, select_mock_2]
mock_q.text.return_value.ask.return_value = ""
result = _step_channels(config)
@@ -521,17 +521,17 @@ class TestStepChannels:
def test_returns_enabled_with_senders(self):
"""Test channels step returns enabled with specific senders."""
from EvoScientist.onboard import _step_channels
from EvoScientist.config.onboard import _step_channels
config = EvoScientistConfig()
select_mock_1 = mock.MagicMock()
select_mock_1 = MagicMock()
select_mock_1.ask.return_value = "imessage"
select_mock_2 = mock.MagicMock()
select_mock_2 = MagicMock()
select_mock_2.ask.return_value = False
with mock.patch("EvoScientist.onboard.questionary") as mock_q, \
mock.patch("EvoScientist.onboard._setup_imessage", return_value=True):
with patch("EvoScientist.config.onboard.questionary") as mock_q, \
patch("EvoScientist.config.onboard._setup_imessage", return_value=True):
mock_q.select.side_effect = [select_mock_1, select_mock_2]
mock_q.text.return_value.ask.return_value = "+1234567890,+0987654321"
result = _step_channels(config)
@@ -540,12 +540,12 @@ class TestStepChannels:
def test_setup_fails_user_declines(self):
"""Test channels step returns disabled when setup fails and user declines."""
from EvoScientist.onboard import _step_channels
from EvoScientist.config.onboard import _step_channels
config = EvoScientistConfig()
with mock.patch("EvoScientist.onboard.questionary") as mock_q, \
mock.patch("EvoScientist.onboard._setup_imessage", return_value=False):
with patch("EvoScientist.config.onboard.questionary") as mock_q, \
patch("EvoScientist.config.onboard._setup_imessage", return_value=False):
mock_q.select.return_value.ask.return_value = "imessage"
mock_q.confirm.return_value.ask.return_value = False
result = _step_channels(config)
@@ -554,17 +554,17 @@ class TestStepChannels:
def test_setup_fails_user_enables_anyway(self):
"""Test channels step enables when setup fails but user confirms."""
from EvoScientist.onboard import _step_channels
from EvoScientist.config.onboard import _step_channels
config = EvoScientistConfig()
select_mock_1 = mock.MagicMock()
select_mock_1 = MagicMock()
select_mock_1.ask.return_value = "imessage"
select_mock_2 = mock.MagicMock()
select_mock_2 = MagicMock()
select_mock_2.ask.return_value = True
with mock.patch("EvoScientist.onboard.questionary") as mock_q, \
mock.patch("EvoScientist.onboard._setup_imessage", return_value=False):
with patch("EvoScientist.config.onboard.questionary") as mock_q, \
patch("EvoScientist.config.onboard._setup_imessage", return_value=False):
mock_q.select.side_effect = [select_mock_1, select_mock_2]
mock_q.confirm.return_value.ask.return_value = True
mock_q.text.return_value.ask.return_value = ""
@@ -574,11 +574,11 @@ class TestStepChannels:
def test_raises_keyboard_interrupt_on_cancel(self):
"""Test channels step raises KeyboardInterrupt on cancel."""
from EvoScientist.onboard import _step_channels
from EvoScientist.config.onboard import _step_channels
config = EvoScientistConfig()
with mock.patch("EvoScientist.onboard.questionary") as mock_q:
with patch("EvoScientist.config.onboard.questionary") as mock_q:
mock_q.select.return_value.ask.return_value = None
with pytest.raises(KeyboardInterrupt):
_step_channels(config)
@@ -587,18 +587,18 @@ class TestStepChannels:
class TestStepMcpServersNpxFailure:
def test_npx_failure_skips_npx_servers(self):
"""When _ensure_npx returns False, npx-dependent servers must be skipped."""
from EvoScientist.onboard import _step_mcp_servers, _RECOMMENDED_MCP_SERVERS
from EvoScientist.config.onboard import _step_mcp_servers, _RECOMMENDED_MCP_SERVERS
# Pick server names: one npx-based, one non-npx (URL-based)
npx_name = next(s["name"] for s in _RECOMMENDED_MCP_SERVERS if s.get("command") == "npx")
url_name = next(s["name"] for s in _RECOMMENDED_MCP_SERVERS if "url" in s)
with mock.patch("EvoScientist.onboard._checkbox_ask", return_value=[npx_name, url_name]), \
mock.patch("EvoScientist.onboard._ensure_npx", return_value=False), \
mock.patch("EvoScientist.onboard._check_npx", return_value=False), \
mock.patch("EvoScientist.mcp.client._load_user_config", return_value={}), \
mock.patch("EvoScientist.mcp.client.add_mcp_server") as mock_add, \
mock.patch("EvoScientist.onboard.console"):
with patch("EvoScientist.config.onboard._checkbox_ask", return_value=[npx_name, url_name]), \
patch("EvoScientist.config.onboard._ensure_npx", return_value=False), \
patch("EvoScientist.config.onboard._check_npx", return_value=False), \
patch("EvoScientist.mcp.client._load_user_config", return_value={}), \
patch("EvoScientist.mcp.client.add_mcp_server") as mock_add, \
patch("EvoScientist.config.onboard.console"):
result = _step_mcp_servers()
# The npx server must NOT have been added
@@ -611,17 +611,17 @@ class TestStepMcpServersNpxFailure:
def test_npx_failure_returns_empty_when_all_npx(self):
"""When all selected servers are npx-based and npx fails, return []."""
from EvoScientist.onboard import _step_mcp_servers, _RECOMMENDED_MCP_SERVERS
from EvoScientist.config.onboard import _step_mcp_servers, _RECOMMENDED_MCP_SERVERS
npx_names = [s["name"] for s in _RECOMMENDED_MCP_SERVERS if s.get("command") == "npx"]
assert len(npx_names) >= 1, "Test requires at least one npx server"
with mock.patch("EvoScientist.onboard._checkbox_ask", return_value=npx_names), \
mock.patch("EvoScientist.onboard._ensure_npx", return_value=False), \
mock.patch("EvoScientist.onboard._check_npx", return_value=False), \
mock.patch("EvoScientist.mcp.client._load_user_config", return_value={}), \
mock.patch("EvoScientist.mcp.client.add_mcp_server") as mock_add, \
mock.patch("EvoScientist.onboard.console"):
with patch("EvoScientist.config.onboard._checkbox_ask", return_value=npx_names), \
patch("EvoScientist.config.onboard._ensure_npx", return_value=False), \
patch("EvoScientist.config.onboard._check_npx", return_value=False), \
patch("EvoScientist.mcp.client._load_user_config", return_value={}), \
patch("EvoScientist.mcp.client.add_mcp_server") as mock_add, \
patch("EvoScientist.config.onboard.console"):
result = _step_mcp_servers()
assert result == []
@@ -631,11 +631,11 @@ class TestStepMcpServersNpxFailure:
class TestStepParameters:
def test_returns_parameters(self):
"""Test parameters step returns all values."""
from EvoScientist.onboard import _step_parameters
from EvoScientist.config.onboard import _step_parameters
config = EvoScientistConfig()
with mock.patch("EvoScientist.onboard.questionary") as mock_q:
with patch("EvoScientist.config.onboard.questionary") as mock_q:
mock_q.text.return_value.ask.side_effect = ["5", "3"]
mock_q.select.return_value.ask.return_value = True # show_thinking
result = _step_parameters(config)
@@ -644,11 +644,11 @@ class TestStepParameters:
def test_uses_defaults_on_empty_input(self):
"""Test that empty input uses defaults."""
from EvoScientist.onboard import _step_parameters
from EvoScientist.config.onboard import _step_parameters
config = EvoScientistConfig(max_concurrent=2, max_iterations=4, show_thinking=False)
with mock.patch("EvoScientist.onboard.questionary") as mock_q:
with patch("EvoScientist.config.onboard.questionary") as mock_q:
mock_q.text.return_value.ask.side_effect = ["", ""] # Empty inputs
mock_q.select.return_value.ask.return_value = False # show_thinking
result = _step_parameters(config)
@@ -664,12 +664,12 @@ class TestStepParameters:
class TestRunOnboard:
def test_returns_true_on_save(self):
"""Test that run_onboard returns True when config is saved."""
from EvoScientist.onboard import run_onboard
from EvoScientist.config.onboard import run_onboard
with mock.patch("EvoScientist.onboard.questionary") as mock_q, \
mock.patch("EvoScientist.onboard.load_config") as mock_load, \
mock.patch("EvoScientist.onboard.save_config") as mock_save, \
mock.patch("EvoScientist.onboard.console"):
with patch("EvoScientist.config.onboard.questionary") as mock_q, \
patch("EvoScientist.config.onboard.load_config") as mock_load, \
patch("EvoScientist.config.onboard.save_config") as mock_save, \
patch("EvoScientist.config.onboard.console"):
# Setup mock config
mock_load.return_value = EvoScientistConfig()
@@ -702,11 +702,11 @@ class TestRunOnboard:
def test_returns_false_on_cancel(self):
"""Test that run_onboard returns False when cancelled."""
from EvoScientist.onboard import run_onboard
from EvoScientist.config.onboard import run_onboard
with mock.patch("EvoScientist.onboard.questionary") as mock_q, \
mock.patch("EvoScientist.onboard.load_config") as mock_load, \
mock.patch("EvoScientist.onboard.console"):
with patch("EvoScientist.config.onboard.questionary") as mock_q, \
patch("EvoScientist.config.onboard.load_config") as mock_load, \
patch("EvoScientist.config.onboard.console"):
mock_load.return_value = EvoScientistConfig()
# First selection returns None (Ctrl+C)
@@ -718,12 +718,12 @@ class TestRunOnboard:
def test_returns_false_when_not_saving(self):
"""Test that run_onboard returns False when user declines to save."""
from EvoScientist.onboard import run_onboard
from EvoScientist.config.onboard import run_onboard
with mock.patch("EvoScientist.onboard.questionary") as mock_q, \
mock.patch("EvoScientist.onboard.load_config") as mock_load, \
mock.patch("EvoScientist.onboard.save_config") as mock_save, \
mock.patch("EvoScientist.onboard.console"):
with patch("EvoScientist.config.onboard.questionary") as mock_q, \
patch("EvoScientist.config.onboard.load_config") as mock_load, \
patch("EvoScientist.config.onboard.save_config") as mock_save, \
patch("EvoScientist.config.onboard.console"):
mock_load.return_value = EvoScientistConfig()
mock_q.select.return_value.ask.side_effect = [
+6 -6
View File
@@ -1,7 +1,7 @@
"""Tests for EvoScientist.tools.skills_manager module."""
from pathlib import Path
from unittest import mock
from unittest.mock import patch
import pytest
@@ -272,7 +272,7 @@ class TestListSkills:
"""Tests for list_skills function."""
def test_list_empty_dir(self, temp_skills_dir):
with mock.patch("EvoScientist.tools.skills_manager.USER_SKILLS_DIR", temp_skills_dir):
with patch("EvoScientist.tools.skills_manager.USER_SKILLS_DIR", temp_skills_dir):
skills = list_skills(include_system=False)
assert skills == []
@@ -280,7 +280,7 @@ class TestListSkills:
# Install a skill
install_skill(str(sample_skill_dir), str(temp_skills_dir))
with mock.patch("EvoScientist.tools.skills_manager.USER_SKILLS_DIR", temp_skills_dir):
with patch("EvoScientist.tools.skills_manager.USER_SKILLS_DIR", temp_skills_dir):
skills = list_skills(include_system=False)
assert len(skills) == 1
@@ -302,7 +302,7 @@ description: Skill number {i}
)
install_skill(str(skill_dir), str(temp_skills_dir))
with mock.patch("EvoScientist.tools.skills_manager.USER_SKILLS_DIR", temp_skills_dir):
with patch("EvoScientist.tools.skills_manager.USER_SKILLS_DIR", temp_skills_dir):
skills = list_skills(include_system=False)
assert len(skills) == 3
@@ -324,7 +324,7 @@ class TestUninstallSkill:
# Install first
install_skill(str(sample_skill_dir), str(temp_skills_dir))
with mock.patch("EvoScientist.tools.skills_manager.USER_SKILLS_DIR", temp_skills_dir):
with patch("EvoScientist.tools.skills_manager.USER_SKILLS_DIR", temp_skills_dir):
result = uninstall_skill("sample-skill")
assert result["success"] is True
@@ -334,7 +334,7 @@ class TestUninstallSkill:
assert not skill_path.exists()
def test_uninstall_nonexistent_skill(self, temp_skills_dir):
with mock.patch("EvoScientist.tools.skills_manager.USER_SKILLS_DIR", temp_skills_dir):
with patch("EvoScientist.tools.skills_manager.USER_SKILLS_DIR", temp_skills_dir):
result = uninstall_skill("nonexistent-skill")
assert result["success"] is False
+36 -12
View File
@@ -8,7 +8,9 @@ from EvoScientist.cli import (
)
# === SubAgentState ===
# =============================================================================
# SubAgentState
# =============================================================================
class TestSubAgentState:
def test_add_tool_call(self):
@@ -74,7 +76,9 @@ class TestSubAgentState:
assert result is not None
# === StreamState ===
# =============================================================================
# StreamState
# =============================================================================
class TestStreamState:
def test_handle_thinking(self):
@@ -199,7 +203,9 @@ class TestStreamState:
assert state.subagents[0].is_active is False
# === Name merging ===
# =============================================================================
# Name merging
# =============================================================================
class TestNameMerging:
def test_generic_subagent_merged(self):
@@ -259,7 +265,9 @@ class TestSubagentNameResolution:
assert state.subagents[-1].name == "sub-agent"
# === _parse_todo_items ===
# =============================================================================
# _parse_todo_items
# =============================================================================
class TestParseTodoItems:
def test_json_input(self):
@@ -285,7 +293,9 @@ class TestParseTodoItems:
assert _parse_todo_items("") is None
# === _build_todo_stats ===
# =============================================================================
# _build_todo_stats
# =============================================================================
class TestBuildTodoStats:
def test_mixed_statuses(self):
@@ -316,7 +326,9 @@ class TestBuildTodoStats:
assert "0 items" in result
# === _resolve_subagent_name ===
# =============================================================================
# _resolve_subagent_name
# =============================================================================
class TestResolveSubagentName:
def test_real_name_passthrough(self):
@@ -341,7 +353,9 @@ class TestResolveSubagentName:
assert state._resolve_subagent_name("sub-agent") == "sub-agent"
# === subagent_end fallback ===
# =============================================================================
# subagent_end fallback
# =============================================================================
class TestSubagentEndFallback:
def test_end_resolves_via_name(self):
@@ -369,7 +383,9 @@ class TestSubagentEndFallback:
assert state.subagents[1].is_active is True # b-agent still active
# === Todo capture from write_todos args ===
# =============================================================================
# Todo capture from write_todos args
# =============================================================================
class TestTodoCaptureFromArgs:
def test_capture_from_tool_call_args(self):
@@ -442,7 +458,9 @@ class TestTodoCaptureFromArgs:
assert state.todo_items == []
# === latest_text reset on tool_call ===
# =============================================================================
# latest_text reset on tool_call
# =============================================================================
class TestLatestTextReset:
def test_latest_text_accumulates(self):
@@ -462,7 +480,9 @@ class TestLatestTextReset:
assert state.response_text == "first segmentsecond segment"
# === Name merging edge cases ===
# =============================================================================
# Name merging edge cases
# =============================================================================
class TestNameMergingAdvanced:
def test_subagent_merges_into_preregistered(self):
@@ -511,7 +531,9 @@ class TestNameMergingAdvanced:
assert research_sa.tool_calls[0]["name"] == "tavily_search"
# === _parse_todo_items edge cases ===
# =============================================================================
# _parse_todo_items edge cases
# =============================================================================
class TestParseTodoItemsAdvanced:
def test_prefixed_with_update_message(self):
@@ -531,7 +553,9 @@ class TestParseTodoItemsAdvanced:
assert _parse_todo_items('["a", "b"]') is None
# === ChannelState queue mechanism ===
# =============================================================================
# ChannelState queue mechanism
# =============================================================================
class TestChannelState:
"""Tests for _ChannelState queue-based communication."""