Files
EvoScientist/tests/test_load_subagents.py
T
Xi Zhang f41584e10b Refactor sub-agent architecture and introduce async support (#200)
* Refactor sub-agent architecture and introduce async support

- Removed the legacy subagent.yaml file and replaced it with individual YAML files for each sub-agent in the subagents directory.
- Updated the load_subagents function to support both directory and single file layouts for loading sub-agent configurations.
- Added new langgraph_dev module for managing async sub-agent lifecycle and deployment.
- Created graphs for async sub-agents (writing-agent, data-analysis-agent) and updated langgraph.json for deployment.
- Introduced new sub-agent definitions for planner, research, debug, code, and writing agents with appropriate system prompts and configurations.
- Enhanced package data inclusion in pyproject.toml to accommodate new sub-agent YAML files.

* Refactor code for improved readability by consolidating conditional statements and formatting

* feat: enhance async sub-agent support with workspace synchronization and user feedback

- Added console status messages during async sub-agent server startup and workspace synchronization to improve user experience.
- Implemented a new WorkspaceSyncWidget for live feedback during workspace sync operations.
- Updated onboarding to reject occupied ports and ensure proper workspace handling for async sub-agents.
- Introduced locking mechanisms to manage concurrent access to langgraph dev processes and workspace states.

* feat: add async sub-agent configuration and server management functions

* feat: improve port occupation handling and log file management in start_langgraph_dev

* feat: enhance async sub-agent handling and introduce comprehensive tests

- Updated `_maybe_swap_async_subagents` to improve async sub-agent management, ensuring internal flags are stripped before handoff.
- Enhanced port management in `onboard.py` to allow reuse of occupied ports if already running by the same service.
- Introduced file locking in `manager.py` to prevent race conditions during concurrent CLI invocations.
- Added new tests for async sub-agent swapping and langgraph manager functionalities to ensure reliability and correctness.
- Updated dependencies in `pyproject.toml` to include `psutil` and `filelock`.

* fix(docs): clarify sub-agent configuration in README

* test(manager): isolate _PID_DIR + tighten reuse-path assertion

Addresses CodeRabbit review on tests/test_langgraph_manager.py:

- Patch _PID_DIR to tmp_path so the FileLock setup in
  ensure_langgraph_dev doesn't mkdir the user's real
  ~/.config/evoscientist/ dir as a test side-effect.
- Tighten "result is None or hasattr(result, 'poll')" to a strict
  "result is None" — the reuse path returns None unconditionally,
  so the OR clause was hiding potential regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(manager): clean up stale PID file when unrelated process reuses PID

* feat(tests): add validation tests for async flag in load_subagents

* fix(load_subagents): restrict to .yaml files and clarify configuration handling

* fix(load_subagents): improve error handling for non-dict specifications in YAML

* feat(onboard): add "LangGraph Port" step to onboarding process

* feat(langgraph): add concurrency configuration for langgraph dev workers

* feat(async-subagents): enhance MCP tool routing for async sub-agents

* fix(manager): update exception handling for connection errors and prevent zombie processes

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 12:45:50 +01:00

142 lines
4.0 KiB
Python

"""Tests for ``EvoScientist.utils.load_subagents``.
Focused on schema-validation paths that are easy to silently misuse from
yaml — primarily the ``async:`` flag type check that prevents quoted-string
or integer values from being misinterpreted as booleans.
"""
from __future__ import annotations
import textwrap
import pytest
from EvoScientist.utils import load_subagents
def _write_yaml(tmp_path, name: str, body: str):
"""Write ``body`` to ``tmp_path/name`` and return the directory path."""
(tmp_path / name).write_text(textwrap.dedent(body))
return tmp_path
def test_async_flag_accepts_real_bool(tmp_path):
"""``async: true`` (real yaml boolean) is accepted and carried through."""
config_path = _write_yaml(
tmp_path,
"writing.yaml",
"""
writing-agent:
description: Drafts reports
system_prompt: ""
tools: []
async: true
""",
)
subs = load_subagents(config_path, tool_registry={})
assert len(subs) == 1
assert subs[0]["name"] == "writing-agent"
assert subs[0]["_async"] is True
def test_async_flag_defaults_to_false_when_omitted(tmp_path):
"""No ``async:`` field → ``_async`` defaults to False."""
config_path = _write_yaml(
tmp_path,
"planner.yaml",
"""
planner-agent:
description: Plans experiments
system_prompt: ""
tools: []
""",
)
subs = load_subagents(config_path, tool_registry={})
assert subs[0]["_async"] is False
def test_async_flag_rejects_quoted_string(tmp_path):
"""``async: "false"`` (quoted) is a real user trap — bool("false") is True.
Without the explicit isinstance check, this would silently flip the agent
into async mode. We require the validator to fail loud instead.
"""
config_path = _write_yaml(
tmp_path,
"bad.yaml",
"""
bad-agent:
description: ""
system_prompt: ""
tools: []
async: "false"
""",
)
with pytest.raises(ValueError, match=r"'async' must be a boolean"):
load_subagents(config_path, tool_registry={})
def test_async_flag_rejects_integer(tmp_path):
"""``async: 1`` is also rejected — yaml integers are not booleans."""
config_path = _write_yaml(
tmp_path,
"bad.yaml",
"""
bad-agent:
description: ""
system_prompt: ""
tools: []
async: 1
""",
)
with pytest.raises(ValueError, match=r"'async' must be a boolean"):
load_subagents(config_path, tool_registry={})
def test_async_flag_error_includes_agent_name(tmp_path):
"""Error message must include the offending agent name for triage."""
config_path = _write_yaml(
tmp_path,
"bad.yaml",
"""
my-bad-agent:
description: ""
system_prompt: ""
tools: []
async: "yes"
""",
)
with pytest.raises(ValueError, match=r"my-bad-agent"):
load_subagents(config_path, tool_registry={})
def test_non_dict_spec_raises(tmp_path):
"""Yaml entries that aren't mappings must fail loud, not be silently dropped.
Previously ``_build_one`` had a ``if not isinstance(spec, dict): continue``
fallback that swallowed malformed entries — users would see their agent
quietly disappear with no error. Now caught during the merge loop.
"""
config_path = _write_yaml(
tmp_path,
"bad.yaml",
"""
bad-agent: 123
""",
)
with pytest.raises(ValueError, match=r"must map to a spec dict"):
load_subagents(config_path, tool_registry={})
def test_non_dict_spec_error_includes_filename_and_name(tmp_path):
"""Error must surface BOTH the offending file path and agent name."""
config_path = _write_yaml(
tmp_path,
"weird.yaml",
"""
weird-agent: "just a string"
""",
)
with pytest.raises(ValueError, match=r"weird\.yaml.*weird-agent"):
load_subagents(config_path, tool_registry={})