428 lines
17 KiB
Python
428 lines
17 KiB
Python
import unittest
|
||
from types import SimpleNamespace
|
||
from unittest.mock import AsyncMock, patch
|
||
|
||
from app.models.signal_source_registry import SignalSourceRegistry
|
||
from app.services.llm import StructuredExtractionResult
|
||
from app.services.signal_source_orchestration_service import SignalSourceOrchestrationService
|
||
|
||
|
||
class _StubSignalSourceOrchestrationService(SignalSourceOrchestrationService):
|
||
def __init__(self) -> None:
|
||
super().__init__(db=None) # type: ignore[arg-type]
|
||
|
||
async def _load_project(self, **_: object):
|
||
return SimpleNamespace(id=3, title="项目A", name="项目A", description="desc", owner_id=1)
|
||
|
||
async def _load_signal(self, **_: object):
|
||
return SimpleNamespace(
|
||
id=9,
|
||
title="新增 GitHub 信号源",
|
||
summary="监控关键仓库 issue/PR",
|
||
signal_type="新需求",
|
||
recommended_action="enter_only",
|
||
content="新增 GitHub 信号源并自动拆任务",
|
||
)
|
||
|
||
async def _plan_onboarding(self, **_: object):
|
||
return (
|
||
{
|
||
"should_onboard": True,
|
||
"summary": "接入 GitHub 仓库信号源并下发 AI worker。",
|
||
"source": {
|
||
"source_kind": "github",
|
||
"source_name": "核心仓库",
|
||
"source_identifier": "github:core-repo",
|
||
"description": "跟踪 issue、pr、release",
|
||
},
|
||
"worker_roles": ["pm_dispatcher", "tech_architect"],
|
||
"openclaw_instruction": "为 GitHub 信号源创建接入任务并通知负责人。",
|
||
},
|
||
"planned",
|
||
{
|
||
"used_fallback": False,
|
||
"error_code": None,
|
||
"source_field": "message.content",
|
||
"provider": "glm",
|
||
"model": "GLM-4.7",
|
||
},
|
||
)
|
||
|
||
async def _create_or_reuse_registry(self, **_: object):
|
||
row = SignalSourceRegistry(
|
||
tenant_id=1,
|
||
project_id=3,
|
||
signal_id=9,
|
||
source_kind="github",
|
||
source_name="核心仓库",
|
||
source_identifier="github:core-repo",
|
||
description="跟踪 issue、pr、release",
|
||
requested_via="pmflow",
|
||
status="active",
|
||
onboarding_status="pending",
|
||
)
|
||
row.id = 11 # type: ignore[assignment]
|
||
return {
|
||
"status": "created",
|
||
"registry_id": 11,
|
||
"row": row,
|
||
}
|
||
|
||
async def _resolve_operator_user(self, **_: object):
|
||
return SimpleNamespace(id=1, role="BOSS")
|
||
|
||
async def _dispatch_openclaw(self, **_: object):
|
||
return {
|
||
"status": "completed",
|
||
"result": {
|
||
"trace_id": "openclaw-123",
|
||
"execution": {
|
||
"results": [
|
||
{
|
||
"operation_code": "task.create",
|
||
"status": "applied",
|
||
"entity_id": "21",
|
||
"payload": {"title": "接入 GitHub 监听"},
|
||
}
|
||
]
|
||
},
|
||
},
|
||
}
|
||
|
||
async def _execute_worker_roles(self, **_: object):
|
||
return [
|
||
{
|
||
"worker_role_key": "pm_dispatcher",
|
||
"worker_role_name": "产品经理(任务分派)",
|
||
"status": "completed",
|
||
"summary": "已输出接入拆解。",
|
||
},
|
||
{
|
||
"worker_role_key": "tech_architect",
|
||
"worker_role_name": "技术架构师",
|
||
"status": "completed",
|
||
"summary": "已输出接入约束。",
|
||
},
|
||
]
|
||
|
||
|
||
class SignalSourceOrchestrationServiceTest(unittest.TestCase):
|
||
def async_run(self, awaitable):
|
||
import asyncio
|
||
|
||
return asyncio.run(awaitable)
|
||
|
||
def test_detect_signal_source_request(self) -> None:
|
||
self.assertTrue(
|
||
SignalSourceOrchestrationService.looks_like_signal_source_request(
|
||
"请新增一个 GitHub 信号源,监控核心仓库 issue。"
|
||
)
|
||
)
|
||
self.assertTrue(
|
||
SignalSourceOrchestrationService.looks_like_signal_source_request(
|
||
"给这个项目加个信号源。"
|
||
)
|
||
)
|
||
self.assertFalse(
|
||
SignalSourceOrchestrationService.looks_like_signal_source_request(
|
||
"把任务 12 分给张三。"
|
||
)
|
||
)
|
||
|
||
def test_orchestrate_from_signal_returns_completed_payload(self) -> None:
|
||
service = _StubSignalSourceOrchestrationService()
|
||
|
||
result = self.async_run(
|
||
service.orchestrate_from_signal(
|
||
tenant_id=1,
|
||
project_id=3,
|
||
signal_id=9,
|
||
initiating_text="新增一个 GitHub 信号源,监控核心仓库 issue。",
|
||
requested_via="pmflow",
|
||
operator_user_id=1,
|
||
dry_run=True,
|
||
)
|
||
)
|
||
|
||
self.assertEqual(result["status"], "planned")
|
||
self.assertEqual(result["signal_source"]["registry_id"], 11)
|
||
self.assertEqual(result["signal_source"]["source_kind"], "github")
|
||
self.assertEqual(len(result["worker_executions"]), 2)
|
||
self.assertEqual(result["openclaw"]["status"], "completed")
|
||
|
||
def test_orchestrate_from_signal_skips_non_source_request(self) -> None:
|
||
service = _StubSignalSourceOrchestrationService()
|
||
|
||
result = self.async_run(
|
||
service.orchestrate_from_signal(
|
||
tenant_id=1,
|
||
project_id=3,
|
||
signal_id=9,
|
||
initiating_text="把任务 12 调成 P0。",
|
||
requested_via="pmflow",
|
||
operator_user_id=1,
|
||
dry_run=True,
|
||
)
|
||
)
|
||
|
||
self.assertEqual(result["status"], "skipped")
|
||
self.assertEqual(result["reason"], "not_signal_source_request")
|
||
|
||
def test_normalize_source_payload_uses_request_hash_for_generic_source_name(self) -> None:
|
||
service = _StubSignalSourceOrchestrationService()
|
||
|
||
normalized = service._normalize_source_payload(
|
||
source_payload={},
|
||
fallback_text="请新增一个 GitHub 信号源,监控核心仓库 issue 和 PR。",
|
||
requested_via="pmflow",
|
||
)
|
||
|
||
self.assertEqual(normalized["source_kind"], "github")
|
||
self.assertEqual(normalized["source_name"], "GitHub")
|
||
self.assertTrue(normalized["source_identifier"].startswith("github:req-"))
|
||
|
||
def test_build_source_identifier_prefers_explicit_repo_locator(self) -> None:
|
||
identifier = SignalSourceOrchestrationService._build_source_identifier(
|
||
source_kind="github",
|
||
source_name="GitHub",
|
||
text="新增 GitHub 信号源,仓库是 openai/codex。",
|
||
source_payload={"repo": "OpenAI/Codex"},
|
||
)
|
||
|
||
self.assertEqual(identifier, "github:openai/codex")
|
||
|
||
def test_map_orchestration_status_updates_signal_status_label(self) -> None:
|
||
self.assertEqual(
|
||
SignalSourceOrchestrationService._map_orchestration_status_to_signal_status("completed"),
|
||
"已编排",
|
||
)
|
||
self.assertEqual(
|
||
SignalSourceOrchestrationService._map_orchestration_status_to_signal_status("pending_manual"),
|
||
"待确认接入",
|
||
)
|
||
self.assertEqual(
|
||
SignalSourceOrchestrationService._map_orchestration_status_to_signal_status("partial"),
|
||
"部分编排",
|
||
)
|
||
self.assertEqual(
|
||
SignalSourceOrchestrationService._map_orchestration_status_to_signal_status("failed"),
|
||
"编排失败",
|
||
)
|
||
|
||
def test_resolve_registry_activation_requires_real_config_and_non_fallback_result(self) -> None:
|
||
registry_status, onboarding_status = SignalSourceOrchestrationService._resolve_registry_activation(
|
||
source_payload={
|
||
"source_kind": "pmflow",
|
||
"source_name": "Web搜索",
|
||
"source_identifier": "pmflow:req-abc123",
|
||
"description": "新增一个Web搜索信号源,关键词:医药行业。",
|
||
"config": {},
|
||
},
|
||
openclaw_result={
|
||
"status": "completed",
|
||
"result": {
|
||
"execution": {
|
||
"results": [
|
||
{
|
||
"operation_code": "task.create",
|
||
"status": "applied",
|
||
"payload": {"title": "接入 GitHub 监听"},
|
||
}
|
||
]
|
||
}
|
||
},
|
||
},
|
||
worker_results=[
|
||
{
|
||
"worker_role_key": "pm_dispatcher",
|
||
"status": "completed",
|
||
"used_fallback": True,
|
||
}
|
||
],
|
||
)
|
||
|
||
self.assertEqual(registry_status, "pending")
|
||
self.assertEqual(onboarding_status, "pending_manual")
|
||
|
||
def test_resolve_registry_activation_allows_explicit_repo_with_real_worker_output(self) -> None:
|
||
registry_status, onboarding_status = SignalSourceOrchestrationService._resolve_registry_activation(
|
||
source_payload={
|
||
"source_kind": "github",
|
||
"source_name": "核心仓库",
|
||
"source_identifier": "github:openai/codex",
|
||
"description": "监控 openai/codex issue 和 PR",
|
||
"config": {},
|
||
"repo": "openai/codex",
|
||
},
|
||
openclaw_result={
|
||
"status": "completed",
|
||
"result": {
|
||
"execution": {
|
||
"results": [
|
||
{
|
||
"operation_code": "task.create",
|
||
"status": "applied",
|
||
"payload": {"title": "接入 GitHub 监听"},
|
||
}
|
||
]
|
||
}
|
||
},
|
||
},
|
||
worker_results=[
|
||
{
|
||
"worker_role_key": "tech_architect",
|
||
"status": "completed",
|
||
"used_fallback": False,
|
||
}
|
||
],
|
||
)
|
||
|
||
self.assertEqual(registry_status, "active")
|
||
self.assertEqual(onboarding_status, "completed")
|
||
|
||
def test_plan_onboarding_accepts_reasoning_only_json(self) -> None:
|
||
service = SignalSourceOrchestrationService(db=None) # type: ignore[arg-type]
|
||
|
||
with patch(
|
||
"app.services.signal_source_orchestration_service.llm_service.generate_structured_object",
|
||
AsyncMock(
|
||
return_value=StructuredExtractionResult(
|
||
payload={
|
||
"should_onboard": True,
|
||
"summary": "接入 GitHub 仓库信号源。",
|
||
"source": {
|
||
"source_kind": "github",
|
||
"source_name": "核心仓库",
|
||
"source_identifier": "github:openai/codex",
|
||
"description": "监控 issue 与 PR",
|
||
},
|
||
"worker_roles": ["pm_dispatcher"],
|
||
"openclaw_instruction": "创建接入任务。",
|
||
},
|
||
source_field="message.reasoning_content",
|
||
provider="glm",
|
||
model="GLM-4.7",
|
||
validation="ok",
|
||
)
|
||
),
|
||
):
|
||
plan, planner_status, diagnostics = self.async_run(
|
||
service._plan_onboarding(
|
||
tenant_id=1,
|
||
project=SimpleNamespace(id=3, title="项目A", name="项目A", description="desc"),
|
||
signal=SimpleNamespace(id=9, title="新增 GitHub 信号源", summary="监控 issue", signal_type="新需求", recommended_action="enter_only"),
|
||
initiating_text="新增一个 GitHub 信号源,监控核心仓库 issue。",
|
||
requested_via="pmflow",
|
||
response_mode="fast",
|
||
)
|
||
)
|
||
|
||
self.assertEqual(planner_status, "planned")
|
||
self.assertTrue(plan["should_onboard"])
|
||
self.assertEqual(diagnostics["source_field"], "message.reasoning_content")
|
||
|
||
def test_plan_onboarding_uses_tool_call_payload_when_content_is_empty(self) -> None:
|
||
service = SignalSourceOrchestrationService(db=None) # type: ignore[arg-type]
|
||
|
||
with patch(
|
||
"app.services.signal_source_orchestration_service.llm_service.generate_structured_object",
|
||
AsyncMock(
|
||
return_value=StructuredExtractionResult(
|
||
payload={
|
||
"should_onboard": True,
|
||
"summary": "接入 GitHub 仓库信号源。",
|
||
"source": {
|
||
"source_kind": "github",
|
||
"source_name": "核心仓库",
|
||
"source_identifier": "github:openai/codex",
|
||
"description": "监控 issue 与 PR",
|
||
},
|
||
"worker_roles": ["pm_dispatcher"],
|
||
"openclaw_instruction": "创建接入任务。",
|
||
},
|
||
source_field="tool_calls.function.arguments",
|
||
provider="glm",
|
||
model="GLM-4.7",
|
||
validation="ok",
|
||
)
|
||
),
|
||
):
|
||
_, planner_status, diagnostics = self.async_run(
|
||
service._plan_onboarding(
|
||
tenant_id=1,
|
||
project=SimpleNamespace(id=3, title="项目A", name="项目A", description="desc"),
|
||
signal=SimpleNamespace(id=9, title="新增 GitHub 信号源", summary="监控 issue", signal_type="新需求", recommended_action="enter_only"),
|
||
initiating_text="新增一个 GitHub 信号源,监控核心仓库 issue。",
|
||
requested_via="pmflow",
|
||
response_mode="fast",
|
||
)
|
||
)
|
||
|
||
self.assertEqual(planner_status, "planned")
|
||
self.assertEqual(diagnostics["source_field"], "tool_calls.function.arguments")
|
||
|
||
def test_plan_onboarding_returns_schema_invalid_diagnostics(self) -> None:
|
||
service = SignalSourceOrchestrationService(db=None) # type: ignore[arg-type]
|
||
|
||
with patch(
|
||
"app.services.signal_source_orchestration_service.llm_service.generate_structured_object",
|
||
AsyncMock(
|
||
return_value=StructuredExtractionResult(
|
||
payload=None,
|
||
source_field="message.content",
|
||
provider="glm",
|
||
model="GLM-4.7",
|
||
error_code="structured_schema_invalid",
|
||
validation="invalid",
|
||
)
|
||
),
|
||
):
|
||
plan, planner_status, diagnostics = self.async_run(
|
||
service._plan_onboarding(
|
||
tenant_id=1,
|
||
project=SimpleNamespace(id=3, title="项目A", name="项目A", description="desc"),
|
||
signal=SimpleNamespace(id=9, title="新增 GitHub 信号源", summary="监控 issue", signal_type="新需求", recommended_action="enter_only"),
|
||
initiating_text="新增一个 GitHub 信号源,监控核心仓库 issue。",
|
||
requested_via="pmflow",
|
||
response_mode="fast",
|
||
)
|
||
)
|
||
|
||
self.assertEqual(planner_status, "fallback")
|
||
self.assertEqual(diagnostics["error_code"], "structured_schema_invalid")
|
||
self.assertTrue(plan["should_onboard"])
|
||
|
||
def test_plan_onboarding_returns_json_not_found_diagnostics(self) -> None:
|
||
service = SignalSourceOrchestrationService(db=None) # type: ignore[arg-type]
|
||
|
||
with patch(
|
||
"app.services.signal_source_orchestration_service.llm_service.generate_structured_object",
|
||
AsyncMock(
|
||
return_value=StructuredExtractionResult(
|
||
payload=None,
|
||
source_field="response.reasoning_content",
|
||
provider="glm",
|
||
model="GLM-4.7",
|
||
error_code="structured_json_not_found",
|
||
validation="invalid",
|
||
)
|
||
),
|
||
):
|
||
_, planner_status, diagnostics = self.async_run(
|
||
service._plan_onboarding(
|
||
tenant_id=1,
|
||
project=SimpleNamespace(id=3, title="项目A", name="项目A", description="desc"),
|
||
signal=SimpleNamespace(id=9, title="新增 GitHub 信号源", summary="监控 issue", signal_type="新需求", recommended_action="enter_only"),
|
||
initiating_text="新增一个 GitHub 信号源,监控核心仓库 issue。",
|
||
requested_via="pmflow",
|
||
response_mode="fast",
|
||
)
|
||
)
|
||
|
||
self.assertEqual(planner_status, "fallback")
|
||
self.assertEqual(diagnostics["error_code"], "structured_json_not_found")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|