ReMe/tests/unit/test_service.py
jinliyl e7d44f6f3b
Some checks are pending
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Tests ReMe / Unit Tests - py3.11 (push) Waiting to run
Tests ReMe / Unit Tests - py3.12 (push) Waiting to run
Tests ReMe / Unit Tests - py3.13 (push) Waiting to run
Windows Smoke / CLI smoke - py3.11 (push) Waiting to run
refactor(agent): unify agent subprocess env, sessions, skills, and MCP/service jobs (#382)
* feat(config): add environment variable configuration for agent subprocesses

- Add environment field to ApplicationConfig to store variables for agent subprocesses
- Remove dynamic loading of .env files in agent wrappers
- Introduce subprocess_environment property in base agent wrapper
- Pass application-level environment variables to Claude Code and Codex agents
- Load environment variables once at startup and pass to ReMe application
- Remove dependency on load_env utility in agent wrapper implementations
- Update tests to use configured environment instead of dynamic loading
- Remove unused environment loading utilities and related test cases

* refactor(mcp): remove channel notification system and related components

- Removed channel notification step implementation
- Removed claim channel step implementation
- Removed ChannelSink class from MCP service
- Removed channel-related documentation from AGENTS.md
- Removed channel instruction text from MCP service
- Removed all channel-related tests
- Updated application context metadata comment to remove channel sink reference
- Removed channel module initialization and imports

* feat(service): add job whitelisting capability to BaseService

- Add optional jobs parameter to BaseService.__init__ to configure job whitelist
- Store jobs as set in self.jobs attribute for efficient lookup operations
- Modify add_jobs method to filter jobs based on whitelist configuration
- Update documentation in both English and Chinese to describe new feature
- Add comprehensive unit tests for job whitelisting behavior
- Implement flowchart update showing new filtering logic
- Preserve existing enable_serve flag behavior alongside new whitelisting

* refactor(service): enhance service job validation and MCP tool injection

- Add strict validation for service jobs whitelist with detailed error messages
- Implement injected job arguments support for MCP services with conflict detection
- Add tool error handling for unsuccessful responses in MCP services
- Remove duplicate job names in Codex agent wrapper using dict.fromkeys
- Update MCP server argument format from single JSON array to repeated --job flags
- Add comprehensive test coverage for job injection and error handling scenarios
- Update documentation to reflect service job validation and MCP features
- Ensure application cleanup occurs even when service lifespan encounters errors

* feat(agent): update skill handling to preserve existing Claude skills

- Change skills parameter processing to use 'all' instead of filtered list
- Add logic to select project skills without restricting Claude's existing skills
- Update variable naming from 'skills' to 'selected_skills' for clarity
- Modify application context metadata documentation to clarify in-memory state usage
- Add test case to verify configured skills are added without filtering existing skills
- Update internal skill directory handling to use renamed variable consistently

* refactor(agent): restructure agent wrapper components and session storage

- Move CcFileSessionStore to separate module for better organization
- Add SDK package version logging in base agent wrapper
- Update Claude Code agent to use new session store structure with project keys
- Refactor Claude Code agent wrapper to use proper type hints and SDK integration
- Add support for server tool use events in Claude Code message processing
- Improve error handling and resource cleanup in streaming operations
- Update Codex agent wrapper with proper type annotations and configuration
- Remove deprecated system prompt mode handling from Claude Code wrapper
- Fix session path construction for Claude Code transcript storage
- Update dependency injection and configuration handling patterns

* fix(cc_agent_wrapper): resolve Claude Code SDK integration issues

- Added dataclass import and created _BlockState for content block metadata tracking
- Implemented proper MCP server name constant and tool context ID validation
- Fixed tool_context_id injection to prevent duplicate assignment errors
- Resolved skills parameter handling in build_options method
- Enhanced job tools integration with MCP servers mapping validation
- Replaced deprecated block_ids/block_types/tool_call_names with block_states dict
- Updated message_delta to emit USAGE chunks instead of REPLY_END
- Fixed stream result handling to ensure proper REPLY_END emission
- Improved error handling for session mirror failures and rate limits
- Added proper cleanup for expected trailing errors in streams
- Refactored Codex agent wrapper initialization and configuration management
- Removed obsolete system_prompt_mode from default config
- Enhanced test coverage for new block state and error handling features
- Fixed async generator handling with aclosing context manager
- Improved chunk type mapping for Claude Code SDK events

* refactor(tests): remove demo config tests from config parser test suite

- Removed test_demo_config_registers_llm_jobs function and its assertions
- Eliminated verification of LLM demo job configurations
- Removed checks for agent wrapper component settings
- Deleted assertions for model configurations and parameters
- Cleaned up deprecated test cases related to demo config parsing

* refactor(evolve): simplify Claude Code session store path structure

- Removed redundant project key subdirectory from session link generation
- Updated CcFileSessionStore initialization to use direct session directory path
- Maintained existing session layout compatibility for backward compatibility
- Added unit tests to verify session persistence behavior with existing transcripts
- Ensured UUID-based session files remain accessible at expected locations
- Preserved existing session directory structure without additional nesting

* refactor(agent): defer optional Codex SDK imports until first use

- Moved openai-codex imports inside functions to avoid mandatory dependencies
- Added TYPE_CHECKING guard for development time type checking only
- Implemented lazy loading mechanism with _get_async_codex_class function
- Updated AsyncCodex initialization to occur on demand rather than at module level
- Maintained backward compatibility while improving import performance
- Added test case to verify package import works without optional Codex SDK
- Updated agentscope dependency to version 2.0.4.post1 in pyproject.toml

* test(embedded): add compatibility tests for in-process ReMe embedding

- Add test suite for QwenPaw-style embedded configurations
- Verify optional defaults remain preserved in embedded configs
- Ensure in-process application API stays compatible
- Test model injection and lifecycle management compatibility
- Remove obsolete hermes agent plugin tests
- Update CLI import test to cover multiple optional SDKs
- Block claude_agent_sdk and openai_codex during import testing
2026-07-20 23:52:14 +08:00

192 lines
6.2 KiB
Python

"""Tests for service job registration behavior."""
import asyncio
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
from reme.components.job import BaseJob, StreamJob
from reme.components.service import MCPService
from reme.schema import Response
def _dummy_app():
"""Minimal object needed by MCPService.build_service."""
async def start():
return None
async def close():
return None
return SimpleNamespace(
config=SimpleNamespace(app_name="test"),
context=SimpleNamespace(metadata={}),
start=start,
close=close,
)
def _app_with_jobs(**jobs):
"""Minimal object needed by BaseService.add_jobs."""
return SimpleNamespace(context=SimpleNamespace(jobs=jobs))
def test_service_registers_all_enabled_jobs_by_default():
"""Omitting service.jobs preserves registration of every service-enabled job."""
service = MCPService()
service.add_job = Mock(return_value=True)
enabled = BaseJob(name="enabled")
disabled = BaseJob(name="disabled", enable_serve=False)
service.add_jobs(_app_with_jobs(enabled=enabled, disabled=disabled))
service.add_job.assert_called_once_with(enabled)
def test_service_jobs_restricts_registration_to_configured_names():
"""service.jobs acts as a whitelist without overriding enable_serve."""
service = MCPService(jobs=["selected"])
service.add_job = Mock(return_value=True)
selected = BaseJob(name="selected")
unselected = BaseJob(name="unselected")
disabled = BaseJob(name="disabled", enable_serve=False)
service.add_jobs(
_app_with_jobs(selected=selected, unselected=unselected, disabled=disabled),
)
service.add_job.assert_called_once_with(selected)
def test_empty_service_jobs_disables_job_registration():
"""An explicit empty whitelist exposes no jobs."""
service = MCPService(jobs=[])
service.add_job = Mock(return_value=True)
service.add_jobs(_app_with_jobs(enabled=BaseJob(name="enabled")))
service.add_job.assert_not_called()
def test_explicit_service_jobs_reject_missing_disabled_and_unsupported_jobs():
"""An explicit service.jobs list fails instead of starting an incomplete service."""
missing_service = MCPService(jobs=["missing"])
with pytest.raises(KeyError, match="missing"):
missing_service.add_jobs(_app_with_jobs())
disabled_service = MCPService(jobs=["disabled"])
with pytest.raises(ValueError, match="disabled"):
disabled_service.add_jobs(_app_with_jobs(disabled=BaseJob(name="disabled", enable_serve=False)))
stream_service = MCPService(jobs=["stream"])
stream_service.add_job = Mock(return_value=False)
with pytest.raises(TypeError, match="stream"):
stream_service.add_jobs(_app_with_jobs(stream=StreamJob(name="stream")))
def test_mcp_service_registers_job_with_empty_parameters():
"""Empty job parameters must remain a dict for FastMCP FunctionTool validation."""
service = MCPService()
service.build_service(_dummy_app())
job = BaseJob(name="empty_params", parameters={})
assert service.add_job(job) is True
def test_mcp_service_reports_stream_job_skipped():
"""MCPService intentionally does not expose StreamJob tools."""
service = MCPService()
service.build_service(_dummy_app())
job = StreamJob(name="stream")
assert service.add_job(job) is False
class _RecordingJob:
"""Small callable matching the job contract used by MCPService.add_job."""
name = "record"
description = "Record arguments"
parameters = {"type": "object", "properties": {"query": {"type": "string"}}}
def __init__(self, response: Response | None = None):
self.response = response or Response(answer="ok")
self.calls = []
async def __call__(self, **kwargs):
self.calls.append(kwargs)
return self.response
def test_mcp_service_injects_job_kwargs_and_rejects_conflicts():
"""Configured job arguments are injected exactly once and remain server-owned."""
async def run():
service = MCPService(injected_job_kwargs={"tool_context_id": "ctx-1"})
service.build_service(_dummy_app())
job = _RecordingJob()
job.parameters = {
"type": "object",
"properties": {
"query": {"type": "string"},
"tool_context_id": {"type": "string"},
},
"required": ["query", "tool_context_id"],
}
assert service.add_job(job) is True
tool = await service.service.get_tool(job.name)
assert tool is not None
assert "tool_context_id" not in tool.parameters["properties"]
assert tool.parameters["required"] == ["query"]
result = await tool.run({"query": "alpha"})
assert job.calls == [{"query": "alpha", "tool_context_id": "ctx-1"}]
assert "ok" in str(result.content)
with pytest.raises(Exception, match="tool_context_id injected by the MCP server"):
await tool.run({"query": "alpha", "tool_context_id": "caller"})
asyncio.run(run())
def test_mcp_service_can_raise_tool_error_for_unsuccessful_response():
"""Configured MCP services translate failed Responses into tool errors."""
async def run():
service = MCPService(tool_error_on_failure=True)
service.build_service(_dummy_app())
job = _RecordingJob(Response(answer="failed", success=False))
assert service.add_job(job) is True
tool = await service.service.get_tool(job.name)
assert tool is not None
with pytest.raises(Exception, match="failed"):
await tool.run({})
asyncio.run(run())
def test_service_lifespan_closes_app_after_error():
"""Application resources close even when serving exits with an exception."""
async def run():
events = []
async def start():
events.append("start")
async def close():
events.append("close")
app = SimpleNamespace(start=start, close=close)
lifespan = MCPService()._lifespan(app, "127.0.0.1", 0) # pylint: disable=protected-access
with pytest.raises(RuntimeError, match="stop"):
async with lifespan(None):
raise RuntimeError("stop")
assert events == ["start", "close"]
asyncio.run(run())