Files
MoviePilot/tests/test_agent_runtime.py
jxxghp c5b716c231 feat: introduce unified agent runtime config and system task prompt framework
- Add structured runtime config files (AGENT_PROFILE.md, AGENT_WORKFLOW.md, AGENT_HOOKS.md, USER_PREFERENCES.md, SYSTEM_TASKS.md, CURRENT_PERSONA.md) for persona, workflow, hooks, and system tasks
- Implement agent_runtime_manager to load, validate, and render runtime config and system task prompts
- Refactor agent initialization to use runtime-managed directories for skills, jobs, memory, and activity logs
- Add AgentHooksMiddleware for structured pre/in/post hooks injection
- Replace hardcoded system task prompts with template-driven rendering from SYSTEM_TASKS.md
- Update tests to cover runtime config loading, migration, and system task prompt rendering
- Update .gitignore to exclude config/agent/
2026-04-28 13:04:28 +08:00

193 lines
7.0 KiB
Python

import shutil
import tempfile
import textwrap
import unittest
from pathlib import Path
from app.agent.runtime import AgentRuntimeConfigError, AgentRuntimeManager
class TestAgentRuntimeConfig(unittest.TestCase):
def setUp(self):
self._tempdir = tempfile.TemporaryDirectory()
self.addCleanup(self._tempdir.cleanup)
self.temp_root = Path(self._tempdir.name)
self.agent_root = self.temp_root / "agent"
self.bundled_root = (
Path(__file__).resolve().parents[1] / "app" / "agent" / "runtime_defaults"
)
def _manager(self) -> AgentRuntimeManager:
return AgentRuntimeManager(
agent_root_dir=self.agent_root,
bundled_runtime_dir=self.bundled_root,
)
def test_load_runtime_config_syncs_defaults_and_parses_sections(self):
manager = self._manager()
runtime_config = manager.load_runtime_config()
self.assertEqual(runtime_config.active_persona, "default")
self.assertIn("professional, concise, restrained", runtime_config.profile_text)
self.assertIn(
"omitting `season` means subscribe to season 1 only",
runtime_config.workflow_text,
)
self.assertTrue((self.agent_root / "runtime" / "CURRENT_PERSONA.md").exists())
def test_legacy_root_markdown_is_migrated_to_memory_directory(self):
self.agent_root.mkdir(parents=True, exist_ok=True)
legacy_memory = self.agent_root / "MEMORY.md"
legacy_memory.write_text("# Legacy Memory\n", encoding="utf-8")
legacy_persona = self.agent_root / "CURRENT_PERSONA.md"
legacy_persona.write_text(
textwrap.dedent(
"""\
---
active_persona: default
profile: personas/default/AGENT_PROFILE.md
workflow: personas/default/AGENT_WORKFLOW.md
hooks: personas/default/AGENT_HOOKS.md
system_tasks: system_tasks/SYSTEM_TASKS.md
user_preferences: USER_PREFERENCES.md
---
"""
),
encoding="utf-8",
)
manager = self._manager()
manager.ensure_layout()
self.assertFalse(legacy_memory.exists())
self.assertTrue((self.agent_root / "memory" / "MEMORY.md").exists())
self.assertFalse(legacy_persona.exists())
self.assertTrue((self.agent_root / "runtime" / "CURRENT_PERSONA.md").exists())
def test_render_system_task_message_uses_unified_system_tasks_definition(self):
manager = self._manager()
runtime_config = manager.load_runtime_config()
message = runtime_config.render_system_task_message("heartbeat")
self.assertIn("[System Heartbeat]", message)
self.assertIn("List all jobs with status 'pending' or 'in_progress'.", message)
self.assertIn("Do NOT include greetings, explanations, or conversational text.", message)
self.assertIn("If no jobs were executed, output nothing.", message)
def test_render_system_task_message_renders_template_context(self):
manager = self._manager()
runtime_config = manager.load_runtime_config()
message = runtime_config.render_system_task_message(
"transfer_failed_retry",
template_context={
"history_ids_csv": "7",
"history_count": 1,
"history_id": 7,
},
)
self.assertIn("Failed transfer history record IDs: 7", message)
self.assertIn("Total failed records: 1", message)
self.assertIn("history_id=7", message)
def test_missing_template_context_raises_clear_error(self):
manager = self._manager()
runtime_config = manager.load_runtime_config()
with self.assertRaises(AgentRuntimeConfigError):
runtime_config.render_system_task_message("transfer_failed_retry")
def test_invalid_user_runtime_config_falls_back_to_bundled_defaults(self):
manager = self._manager()
manager.ensure_layout()
invalid_current = self.agent_root / "runtime" / "CURRENT_PERSONA.md"
invalid_current.write_text(
textwrap.dedent(
"""\
---
active_persona: broken
profile: personas/default/AGENT_PROFILE.md
hooks: personas/default/AGENT_HOOKS.md
system_tasks: system_tasks/SYSTEM_TASKS.md
---
"""
),
encoding="utf-8",
)
manager.invalidate_cache()
runtime_config = manager.load_runtime_config()
self.assertTrue(runtime_config.used_fallback)
self.assertEqual(runtime_config.active_persona, "default")
self.assertIn("已回退到内置默认配置", runtime_config.warnings[0])
def test_deprecated_phrase_warning_is_reported(self):
self.agent_root.mkdir(parents=True, exist_ok=True)
runtime_root = self.agent_root / "runtime"
shutil.copytree(self.bundled_root, runtime_root)
current_persona = runtime_root / "CURRENT_PERSONA.md"
current_persona.write_text(
textwrap.dedent(
"""\
---
version: 1
active_persona: default
profile: personas/default/AGENT_PROFILE.md
workflow: personas/default/AGENT_WORKFLOW.md
hooks: personas/default/AGENT_HOOKS.md
user_preferences: USER_PREFERENCES.md
system_tasks: system_tasks/SYSTEM_TASKS.md
extra_context_files: []
deprecated_phrases:
- professional, concise, restrained
---
"""
),
encoding="utf-8",
)
manager = self._manager()
manager.invalidate_cache()
runtime_config = manager.load_runtime_config()
self.assertTrue(
any("professional, concise, restrained" in warning for warning in runtime_config.warnings)
)
def test_outdated_system_tasks_definition_falls_back_to_bundled_defaults(self):
self.agent_root.mkdir(parents=True, exist_ok=True)
runtime_root = self.agent_root / "runtime"
shutil.copytree(self.bundled_root, runtime_root)
system_tasks = runtime_root / "system_tasks" / "SYSTEM_TASKS.md"
system_tasks.write_text(
textwrap.dedent(
"""\
---
version: 1
shared_rules:
- legacy system tasks
task_types:
heartbeat:
header: "[Legacy Heartbeat]"
objective: "legacy"
---
"""
),
encoding="utf-8",
)
manager = self._manager()
manager.invalidate_cache()
runtime_config = manager.load_runtime_config()
self.assertTrue(runtime_config.used_fallback)
self.assertEqual(runtime_config.system_tasks.version, 2)
if __name__ == "__main__":
unittest.main()