from __future__ import annotations import pytest from app.services.ai_pmo.ai_pmo_intent import CanonicalIntent from app.services.ai_pmo.ai_pmo_typed_executor_discovery.mixin_discover import ( DiscoveryFlowMixin, ) from app.services.ai_pmo.ai_pmo_typed_executor_execute.mixin_execute_signal_source import ( ExecuteSignalSourceMixin, ) def test_signal_source_onboard_idempotency_key_prefers_message_key() -> None: key_one = ExecuteSignalSourceMixin._build_signal_source_onboard_idempotency_key( source_system="feishu", source_message_id="msg-123", ) key_two = ExecuteSignalSourceMixin._build_signal_source_onboard_idempotency_key( source_system="feishu", source_message_id="msg-123", ) bare_key = ExecuteSignalSourceMixin._build_signal_source_onboard_idempotency_key( source_system="", source_message_id="msg-123", ) assert key_one == "feishu:msg-123" assert key_two == "feishu:msg-123" assert bare_key == "msg-123" @pytest.mark.asyncio async def test_signal_source_onboard_replays_existing_ledger_without_reexecution() -> None: class _FakeLedger: def __init__(self) -> None: self.status = "applied" self.result_payload = { "status": "succeeded", "signal_id": 88, "project_id": 11, "signal_source_workflow": { "status": "completed", "run_status": "completed", }, "signal_source_orchestration": { "signal_source": { "registry_id": 501, "source_name": "Webhook A", } }, "auto_tasks": [], } class _FakeWorkflow: def __init__(self) -> None: self.called = False async def run(self, **kwargs): self.called = True raise AssertionError("signal_source_workflow.run should not be called on replay") class _FakeDB: def __init__(self) -> None: self.committed = False async def commit(self): self.committed = True class _FakeExecutor(ExecuteSignalSourceMixin): def __init__(self) -> None: self.db = _FakeDB() self.tenant_id = 1 self.signal_source_workflow = _FakeWorkflow() self.loaded_key = None async def _load_signal_source_onboard_ledger(self, *, idempotency_key): self.loaded_key = idempotency_key return _FakeLedger() async def _claim_signal_source_onboard_ledger(self, **kwargs): raise AssertionError("ledger claim should not be reached when replaying an applied result") def _build_signal_source_semantic_payload(self, **kwargs): raise AssertionError("semantic payload should not be rebuilt on replay") async def _create_signal(self, **kwargs): raise AssertionError("signal creation should not be reached on replay") @staticmethod def _normalize_title_candidate(value): return str(value or "").strip() @staticmethod def _is_acceptable_title(text): return bool(text) @staticmethod def _extract_auto_tasks(openclaw_payload): return [] kernel = _FakeExecutor() result = await kernel._execute_signal_source_onboard_intent( intent=type("Intent", (), {"project_id": 11, "signal_summary": "摘要", "signal_title": "标题"})(), message_text="请接入这个信号源", operator_user_id=9, source_system="feishu", source_message_id="msg-abc", channel_ref="oc-1", ) assert result["status"] == "succeeded" assert result["signal_id"] == 88 assert kernel.loaded_key == "feishu:msg-abc" assert kernel.db.committed is False assert kernel.signal_source_workflow.called is False @pytest.mark.asyncio async def test_project_signal_discovery_uses_project_id_without_name_error() -> None: class _FakeDiscovery(DiscoveryFlowMixin): tenant_id = 1 @staticmethod def _extract_requested_signal_count(*, message_text, metadata): _ = (message_text, metadata) return 1 async def _analyze_signal_batch_with_high_tier_model(self, **kwargs): _ = kwargs return { "items": [ { "title": "候选信号", "summary": "摘要", "content": "内容", "signal_type": "新需求", "priority": "medium", } ] } async def _collect_project_signal_discovery_context(self, **kwargs): _ = kwargs return { "retrieval_items": [{"content": "evidence"}], "retrieval_metrics": {}, "entity_matches": [], "tool_trace": [], "source_registries": [], "truth_nodes": [], "experiments": [], "recent_signals": [], "capability_bundle": {}, } async def _refine_signal_title_with_ai(self, **kwargs): _ = kwargs return "信号A" @staticmethod def _build_candidate_dedupe_key(*, project_id, candidate): return f"{project_id}:{candidate.get('title')}" @staticmethod def _build_signal_title_refine_error_message(exc): return str(exc) kernel = _FakeDiscovery() result = await kernel.discover_project_signal_candidates( intent=CanonicalIntent( name="project_signal.discover", confidence=0.9, project_id=2, signal_title=None, signal_summary=None, metadata={}, ), message_text="给项目 2 先出一条候选信号", source_system="feishu", source_message_id="msg-1", channel_ref="oc-chat-1", ) assert result["status"] == "pending_confirmation" assert result["project_id"] == 2 assert result["candidate_signals"][0]["title"] == "信号A"