بنقرة واحدة
python-testing
Deep pytest guide for agent-core: fixtures, mocking, async tests, and TDD workflow.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Deep pytest guide for agent-core: fixtures, mocking, async tests, and TDD workflow.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Runtime extension 验证规范 — 验证 harness package 中 tools、rails、skills 是否能真实热加载并可运行
实现阶段主操作手册 — 指导 agent 完成改码与局部验证,并把提交留给独立 commit phase
扩展实现阶段 — 在 worktree 中生成运行时扩展代码
扩展方案设计 — 将能力缺口转化为 ExtensionDesign 结构
Generates a multimodal Skill markdown file from a source URL. Use this Skill when a user wants to turn a web tutorial, product support article, or software guide into a reusable agent Skill with concise steps and embedded screenshots.
Comprehensive step-by-step workflow for navigating the Booking.com mobile site to search for destinations, select dates/occupancy, browse results, and extract detailed hotel information including pricing and facilities.
| name | python-testing |
| description | Deep pytest guide for agent-core: fixtures, mocking, async tests, and TDD workflow. |
Comprehensive pytest patterns for agent-core. This skill extends
.claude/rules/python/testing.md and .claude/rules/testing.md.
Write tests before implementation. Follow the red-green-refactor cycle:
For agent-core, this means:
# RED: Write the test first
class TestAbilityManager:
@pytest.mark.asyncio
async def test_registers_tool(self):
manager = AbilityManager()
card = ToolCard(name="echo", description="echo input", parameters={})
await manager.register_tool(card, echo_handler)
assert "echo" in manager.list_tools()
# GREEN: Minimal implementation
# ...
# REFACTOR: Clean up, add edge cases
@pytest.mark.asyncio
async def test_raises_on_duplicate_registration(self):
manager = AbilityManager()
card = ToolCard(name="echo", description="echo input", parameters={})
await manager.register_tool(card, echo_handler)
with pytest.raises(ConfigurationError, match="already registered"):
await manager.register_tool(card, echo_handler)
Define fixtures in tests/conftest.py for project-wide fixtures, or in
tests/unit_tests/<module>/conftest.py for module-specific fixtures.
# tests/conftest.py
import pytest
from unittest import mock
@pytest.fixture
def mock_llm_client():
"""Provides a mocked LLM client for all tests."""
with mock.patch("openjiuwen.core.foundation.LLMClient") as cls:
instance = mock.MagicMock()
instance.call.return_value = '{"result": "ok"}'
cls.return_value = instance
yield instance
@pytest.fixture(scope="module")
def sample_agent_card():
"""Module-scoped: created once per test module."""
return AgentCard(id="test-agent", name="TestAgent", version="1.0")
@pytest.fixture
def temp_workspace(tmp_path):
"""Provides a clean temporary directory for each test."""
workspace = tmp_path / "workspace"
workspace.mkdir()
yield workspace
# Cleanup happens automatically via tmp_path
@pytest.fixture(params=["haiku", "sonnet", "opus"])
def llm_model(request: pytest.FixtureRequest):
return request.param
async def test_llm_client(llm_model: str):
client = LLMClient(model=llm_model)
result = await client.call("hello")
assert result is not None
@pytest.fixture
def make_tool_card():
"""Factory fixture: creates ToolCards with defaults."""
def _make(name: str = "test-tool", **kwargs) -> ToolCard:
defaults = {
"description": "a test tool",
"parameters": {},
"version": "1.0",
}
defaults.update(kwargs)
return ToolCard(name=name, **defaults)
return _make
async def test_registers_custom_tool(make_tool_card):
card = make_tool_card(name="custom")
manager = AbilityManager()
await manager.register_tool(card, handler)
assert "custom" in manager.list_tools()
Use autouse sparingly — only for global setup that must happen for every
test in the scope:
@pytest.fixture(autouse=True)
def reset_global_state():
"""Reset global state before each test to ensure isolation."""
original = Runner.resource_mgr
Runner.resource_mgr = ResourceManager()
yield
Runner.resource_mgr = original
In pyproject.toml:
[tool.pytest.ini_options]
markers = [
"unit: fast, deterministic tests with no I/O",
"integration: tests requiring external services",
"slow: long-running tests (skipped by default)",
"e2e: end-to-end workflow tests",
]
# Run only fast unit tests (skip slow + integration)
pytest -m "unit"
# Run unit + integration, skip slow
pytest -m "not slow"
# Run everything including slow tests
pytest -m ""
# Run only e2e tests
pytest -m "e2e"
Use monkeypatch for simple cases — prefer it over @patch decorators:
def test_reads_config(monkeypatch, tmp_path):
config_file = tmp_path / "config.json"
config_file.write_text('{"timeout": 30}')
monkeypatch.setattr(
"openjiuwen.core.config.CONFIG_PATH",
config_file
)
config = load_config()
assert config.timeout == 30
Use @patch when mocking class methods or when the mock needs to be
accessed inside the test:
from unittest import mock
@mock.patch("openjiuwen.harness.DeepAgent._run_step", new_callable=mock.AsyncMock)
async def test_delegates_to_subagent(mock_run_step):
mock_run_step.return_value = StepResult(
status="success",
subagent_called=True,
)
agent = DeepAgent()
result = await agent.run("complex task")
assert result.subagent_called
mock_run_step.assert_called_once()
Use autospec=True to automatically match the signature of the real method:
with mock.patch(
"openjiuwen.core.foundation.LLMClient.call",
autospec=True
) as mock_call:
mock_call.return_value = '{"choices": [{"message": {"content": "ok"}}]}'
# mock_call.assert_called_once_with("prompt") # Enforces correct signature
For async methods:
from unittest import mock
with mock.patch(
"openjiuwen.harness.DeepAgent._run_step",
new_callable=mock.AsyncMock
) as mock_step:
mock_step.return_value = StepResult(status="success")
result = await agent.run("test")
assert result.status == "success"
from unittest import mock
def test_sandbox_executes_within_scope():
with mock.patch(
"openjiuwen.core.sys_operation.sandbox.SandboxedRunner.run"
) as mock_run:
mock_run.return_value = CommandResult(stdout="ok", stderr="", code=0)
result = sandbox.run("echo hello")
assert result.stdout == "ok"
mock_run.assert_called_once_with("echo hello")
In pyproject.toml:
[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
@pytest.mark.asyncio
async def test_ability_manager_registers_tool():
manager = AbilityManager()
card = ToolCard(name="test", description="test tool", parameters={})
await manager.register_tool(card, handler)
tools = manager.list_tools()
assert "test" in tools
Use unittest.IsolatedAsyncioTestCase for test classes that need strict
per-test isolation:
from unittest import IsolatedAsyncioTestCase
class TestRunner(IsolatedAsyncioTestCase):
async def asyncSetUp(self) -> None:
self.runner = await create_runner()
self.runner.resource_mgr.clear()
async def asyncTearDown(self) -> None:
await self.runner.shutdown()
async def test_it_executes_task(self) -> None:
result = await self.runner.execute("test task")
self.assertEqual(result.status, "success")
@pytest.mark.asyncio
async def test_saves_session(tmp_path):
session_dir = tmp_path / "sessions"
session_dir.mkdir()
manager = SessionManager(base_dir=session_dir)
await manager.save(SessionData(id="s1", turns=[]))
assert (session_dir / "s1.json").exists()
Mirror the source path in test paths:
| Source | Test |
|---|---|
openjiuwen/harness/deep_agent.py | tests/unit_tests/harness/test_deep_agent.py |
openjiuwen/core/single_agent/react_agent.py | tests/unit_tests/core/single_agent/test_react_agent.py |
openjiuwen/core/workflow/engine.py | tests/unit_tests/core/workflow/test_engine.py |
# pytest.ini
[pytest]
asyncio_mode = auto
asyncio_default_fixture_loop_scope = function
testpaths = tests/unit_tests tests/system_tests
markers =
unit: fast deterministic tests, no I/O
integration: tests requiring external services
slow: long-running tests, skipped by default
e2e: end-to-end workflow tests
# pyproject.toml additions
[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
filterwarnings = [
"ignore::DeprecationWarning",
]
[tool.coverage.run]
source = ["openjiuwen/"]
omit = [
"*/tests/*",
"*/__pycache__/*",
"*/migrations/*",
]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
]
fail_under = 80