PMFlow/backend/tests/test_feishu_pipeline_queue_...

2161 lines
78 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import asyncio
import inspect
from types import SimpleNamespace
import pytest
from app.services.ai_pmo.draft_confirm_handler import DraftConfirmHandler
from app.services.integrations.feishu_signal_pipeline.mixin_gate_project import (
FeishuPipelineGateProjectMixin,
)
from app.services.integrations.feishu_signal_pipeline.mixin_command_commit_project import (
FeishuPipelineCommandCommitProjectMixin,
)
from app.services.integrations.feishu_signal_pipeline.mixin_command_commit_task import (
FeishuPipelineCommandCommitTaskMixin,
)
from app.services.integrations.feishu_signal_pipeline.mixin_context_prefill_b import (
FeishuPipelineContextPrefillBMixin,
)
from app.services.integrations.feishu_signal_pipeline.mixin_extraction import (
FeishuPipelineExtractionMixin,
)
from app.services.integrations.feishu_signal_pipeline.mixin_item_processing import (
FeishuPipelineItemProcessingMixin,
)
from app.services.integrations.feishu_signal_pipeline.mixin_followup_a import (
FeishuPipelineFollowupAMixin,
)
from app.services.integrations.feishu_signal_pipeline.mixin_followup_b import FeishuPipelineFollowupBMixin
from app.services.integrations.feishu_signal_pipeline.mixin_gate_dispatch import (
FeishuPipelineGateDispatchMixin,
)
from app.services.integrations.feishu_signal_pipeline.mixin_item_finalize_routing import (
FeishuPipelineItemFinalizeRoutingMixin,
)
from app.services.integrations.feishu_signal_pipeline.mixin_message_utils_b import (
FeishuPipelineMessageUtilsBMixin,
)
from app.services.integrations.feishu_signal_pipeline.mixin_message_utils_a import (
FeishuPipelineMessageUtilsAMixin,
)
from app.services.integrations.feishu_signal_pipeline.mixin_process_message_helpers_b import (
FeishuPipelineProcessMessageHelpersBMixin,
)
from app.services.integrations.feishu_signal_pipeline.mixin_queue_confirm_undo import (
FeishuPipelineQueueConfirmUndoMixin,
)
from app.services.integrations.feishu_signal_pipeline.service import FeishuSignalPipelineService
from app.services.integrations.feishu_signal_pipeline import mixin_process_message_core
from app.services.signal.signal_capture_service import mixin_capture_entry
from app.services.signal.signal_capture_service.service import ProactiveSignalCaptureService
from app.services.signal.signal_capture_service.mixin_task_orchestration_main import (
SignalCaptureTaskOrchestrationMainMixin,
)
from app.services.signal.signal_capture_service.mixin_task_orchestration_skill import (
SignalCaptureTaskOrchestrationSkillMixin,
)
class _Item:
def __init__(self, *, item_id: int, level: str, operation_code: str, raw_message: str = "") -> None:
self.id = item_id
self.level = level
self.card_type = operation_code.replace(".", "_")
self.raw_message = raw_message
self.result_payload = {"operation_code": operation_code}
self.feishu_message_id = f"msg-{item_id}"
def test_followup_status_target_requires_unique_match() -> None:
items = [
_Item(item_id=1, level="command", operation_code="project.create"),
_Item(item_id=2, level="command", operation_code="project.create"),
]
target = FeishuPipelineFollowupBMixin._select_followup_status_target(
message="刚才创建项目现在怎么样了",
recent_items=items,
)
assert target is None
def test_followup_status_target_rejects_single_item_without_anchor() -> None:
items = [
_Item(item_id=1, level="project", operation_code="project.create"),
]
target = FeishuPipelineFollowupBMixin._select_followup_status_target(
message="它现在怎么样?",
recent_items=items,
)
assert target is None
def test_followup_status_target_does_not_use_lexical_project_hints_without_anchor() -> None:
items = [
_Item(item_id=1, level="project", operation_code="project.create"),
]
target = FeishuPipelineFollowupBMixin._select_followup_status_target(
message="刚才创建项目现在怎么样了",
recent_items=items,
)
assert target is None
def test_followup_status_target_prefers_thread_anchor() -> None:
items = [
_Item(item_id=1, level="project", operation_code="project.create"),
_Item(item_id=2, level="project", operation_code="project.create"),
]
items[0].feishu_message_id = "root-1"
items[1].feishu_message_id = "root-2"
target = FeishuPipelineFollowupBMixin._select_followup_status_target(
message="他是什么时候创建的?",
recent_items=items,
thread_context={"root_message_id": "root-1"},
)
assert target is items[0]
def test_followup_status_target_rejects_unmatched_thread_anchor() -> None:
items = [
_Item(item_id=1, level="project", operation_code="project.create"),
_Item(item_id=2, level="project", operation_code="project.create"),
]
items[0].feishu_message_id = "root-1"
items[1].feishu_message_id = "root-2"
target = FeishuPipelineFollowupBMixin._select_followup_status_target(
message="他是什么时候创建的?",
recent_items=items,
thread_context={"reply_to_message_id": "reply-miss"},
)
assert target is None
@pytest.mark.asyncio
async def test_followup_status_reply_reports_project_created_at(monkeypatch) -> None:
from datetime import datetime
class _FakeDBResult:
def __init__(self, value) -> None:
self._value = value
def scalar_one_or_none(self):
return self._value
class _FakeDB:
async def execute(self, query):
_ = query
return _FakeDBResult(type("ProjectRow", (), {"id": 7, "tenant_id": 1, "title": "自动开票系统", "name": "自动开票系统", "created_at": datetime(2026, 3, 1, 9, 30)})())
class _FakeFollowup(FeishuPipelineFollowupBMixin):
def __init__(self) -> None:
self.db = _FakeDB()
self.tenant_id = 1
async def _get_project_suggestion(self, item_id: int):
_ = item_id
return type("Suggestion", (), {"created_project_id": 7})()
@staticmethod
def _normalize_route_status(value):
return str(value)
item = _Item(item_id=1, level="project", operation_code="project.create")
item.route_status = "auto_done"
item.matched_project_id = 7
reply = await _FakeFollowup()._build_followup_status_reply(item, message="他是什么时候创建的?")
assert "创建于 2026-03-01 09:30" in reply
@pytest.mark.asyncio
async def test_followup_queue_handler_ignores_unmatched_thread_anchor() -> None:
class _FakeItem:
def __init__(self) -> None:
self.id = 1
self.route_status = "queued"
self.level = "project"
self.feishu_message_id = "queue-root-1"
self.blocking_reason = "请确认"
class _FakeFollowup(FeishuPipelineFollowupAMixin):
def __init__(self) -> None:
self.db = object()
self.tenant_id = 1
@staticmethod
def _looks_like_continue_followup(message: str) -> bool:
_ = message
return False
async def _load_recent_chat_items(self, *, source_chat_id, source_sender_open_id, sender, limit=5):
_ = (source_chat_id, source_sender_open_id, sender, limit)
return [_FakeItem()]
result = await _FakeFollowup()._maybe_handle_queue_followup(
message="他是什么时候创建的?",
sender="测试用户",
source_chat_id="chat-1",
source_sender_open_id="ou-1",
feishu_message_id="msg-1",
process_started_at=0.0,
thread_context={"reply_to_message_id": "reply-miss"},
)
assert result is None
def test_process_message_no_longer_calls_legacy_queue_followup() -> None:
source = inspect.getsource(mixin_process_message_core.FeishuPipelineProcessMessageCoreMixin.process_message)
assert "_maybe_handle_queue_followup" not in source
def test_feishu_signal_pipeline_service_runtime_excludes_legacy_followup_mixins() -> None:
source = inspect.getsource(FeishuSignalPipelineService)
assert "FeishuPipelineFollowupAMixin" not in source
assert "FeishuPipelineFollowupBMixin" not in source
def test_proactive_signal_capture_service_runtime_excludes_legacy_capture_mixin() -> None:
source = inspect.getsource(ProactiveSignalCaptureService)
assert "SignalCaptureLegacyMixin" not in source
def test_decorate_capture_result_adds_queue_action_card_metadata() -> None:
result = FeishuPipelineProcessMessageHelpersBMixin._decorate_capture_result(
{
"status": "captured",
"results": [{"item_id": 301, "route_status": "queued_for_decision"}],
"direct_reply_text": "已转待确认。",
},
chat_type="group",
normalized_text="请创建项目",
route="group.intent_gate.project_create",
db_mutations=[],
)
assert result["reply_template_key"] == "queue_pending_confirm"
assert result["queue_item_id"] == 301
def test_rag_conversation_key_uses_stable_scope_without_reply_anchor() -> None:
key = FeishuPipelineMessageUtilsAMixin._build_rag_conversation_key(
source_chat_id="chat-1",
source_sender_open_id="ou-1",
sender="测试用户",
source_message_id="msg-123",
thread_context=None,
)
assert key == "feishu:chat-1:ou-1"
def test_rag_conversation_key_prefers_thread_anchor_when_reply_present() -> None:
key = FeishuPipelineMessageUtilsAMixin._build_rag_conversation_key(
source_chat_id="chat-1",
source_sender_open_id="ou-1",
sender="测试用户",
source_message_id="msg-123",
thread_context={"reply_to_message_id": "om-bot-reply-1"},
)
assert key == "feishu:chat-1:om-bot-reply-1"
@pytest.mark.asyncio
async def test_build_direct_qa_capture_exposes_reason_code_and_anchor_kind() -> None:
class _FakeHelpers(FeishuPipelineProcessMessageHelpersBMixin):
def __init__(self) -> None:
self.db = object()
self.tenant_id = 1
async def _write_processing_log(self, **kwargs):
_ = kwargs
def _build_rag_reply_text(self, *, answer: str, sources: list[dict[str, object]]) -> str:
_ = sources
return answer
helpers = _FakeHelpers()
capture = await helpers._build_direct_qa_capture(
message="这个是哪个模型回复的?",
rag_result={
"answer": "这条回复命中的逻辑层是 low。",
"clarification_code": None,
"reason_code": "meta_followup_answered",
"anchor_kind": "meta_recent_reply",
"meta_followup_kind": "reply_model_identity",
"sources": [],
},
inferred_project_id=None,
feishu_message_id="msg-1",
process_started_at=0.0,
route_reason="direct qa",
intent_label="question",
)
assert capture["reason_code"] == "meta_followup_answered"
assert capture["anchor_kind"] == "meta_recent_reply"
assert capture["meta_followup_kind"] == "reply_model_identity"
@pytest.mark.asyncio
async def test_intent_gate_query_does_not_inherit_project_context(monkeypatch) -> None:
captured: dict[str, object] = {}
class _FakeAdaptiveRAGService:
def __init__(self, db, tenant_id: int):
captured["tenant_id"] = tenant_id
_ = db
async def classify_intent(self, text: str):
captured["classified_text"] = text
return {"intent": "question", "question_part": text}
async def answer_question(self, **kwargs):
captured["answer_kwargs"] = kwargs
return {"answer": "AI 行业动态正常", "sources": []}
class _FakeGate(FeishuPipelineGateDispatchMixin, FeishuPipelineProcessMessageHelpersBMixin):
def __init__(self) -> None:
class _FakeDB:
async def commit(self) -> None:
return None
self.db = _FakeDB()
self.tenant_id = 1
async def _write_processing_log(self, **kwargs):
captured["write_log"] = kwargs
def _build_rag_conversation_key(self, **kwargs):
_ = kwargs
return "feishu:chat-1"
def _build_rag_reply_text(self, *, answer: str, sources: list[dict[str, object]]) -> str:
_ = sources
return answer
monkeypatch.setattr(
"app.services.integrations.feishu_signal_pipeline.mixin_gate_dispatch.AdaptiveRAGService",
_FakeAdaptiveRAGService,
)
result = await _FakeGate()._maybe_handle_intent_gate_capture(
intent_gate={"intent": "query"},
message="最近AI行业有什么动态",
sender="测试用户",
source_chat_id="chat-1",
source_sender_open_id="ou-1",
feishu_message_id="msg-1",
captured_at=None,
inferred_project_id=6,
projects=[],
process_started_at=0.0,
normalized_chat_type="group",
thread_context={"reply_to_message_id": "msg-root-1"},
)
assert isinstance(result, dict)
assert result["linked_project_ids"] == []
assert captured["answer_kwargs"]["project_id"] is None
assert captured["answer_kwargs"]["source_sender_open_id"] == "ou-1"
assert captured["answer_kwargs"]["thread_context"] == {"reply_to_message_id": "msg-root-1"}
@pytest.mark.asyncio
async def test_feishu_question_like_messages_use_sync_high_accuracy_path(monkeypatch) -> None:
calls = {"sync": 0, "session": 0, "cache": 0}
class _FakeBundle:
def __init__(self) -> None:
self.primary_workflow = "query_flow"
self.workflows = [SimpleNamespace(workflow_type="query_flow", workflow_action="answer")]
self.diagnostics = {}
def to_dict(self) -> dict[str, object]:
return {
"bundle_id": "semantic-query",
"semantic_version": "2026-03-27.workflow-v1",
"source_system": "feishu",
"primary_workflow": self.primary_workflow,
"workflows": [
{
"workflow_type": "query_flow",
"workflow_action": "answer",
"target_scope": "global",
"operation_codes": ["query.answer"],
"requires_confirmation": False,
"confidence": 0.9,
"slots": [],
"clarification_needed": False,
"metadata": {},
}
],
"message_gate": "auto_execute",
"reply_contract": {
"mode": "aggregate_card",
"template_key": "workflow_bundle",
"summary": "query_flow",
},
"diagnostics": {},
}
class _FakeKernel:
@staticmethod
def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str):
_ = (source_system, source_message_id, channel_ref, sender, message_text)
return "msg-1"
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def _get_or_create_session(self, **kwargs):
calls["session"] += 1
_ = kwargs
raise AssertionError("question-like Feishu messages should not create an async queue session")
def _is_short_confirmation_message(self, **kwargs):
_ = kwargs
return False
async def process_feishu_message(self, **kwargs):
calls["sync"] += 1
_ = kwargs
return {
"handled": True,
"status": "answered",
"classification": "question",
"linked_project_ids": [],
"signal_id": None,
"created_project_id": None,
"auto_tasks": [],
"ai_trace_id": "trace-1",
"direct_reply_required": True,
"direct_reply_text": "垂直行业建议先看目标客户与交付边界。",
}
async def _noop_sync_recent_cache(self, **kwargs):
calls["cache"] += 1
_ = kwargs
monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel)
monkeypatch.setattr(ProactiveSignalCaptureService, "_sync_feishu_recent_cache", _noop_sync_recent_cache)
monkeypatch.setattr(
mixin_capture_entry.WorkflowSemanticPlanner,
"aplan_message",
classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())),
)
service = ProactiveSignalCaptureService(object())
result = await service.capture_from_feishu(
group_chat_id="chat-1",
message_content="我感觉第一个垂直应用前景比较好,但是垂直哪个行业呢?",
sender="测试用户",
tenant_id=1,
mentioned_bot_names=["PMFlow"],
feishu_message_id="msg-1",
chat_type="group",
)
assert result["status"] == "answered"
assert "已收到,正在处理。" not in str(result.get("direct_reply_text") or "")
assert calls["sync"] == 1
assert calls["session"] == 0
assert calls["cache"] == 1
@pytest.mark.asyncio
async def test_feishu_question_like_messages_require_explicit_group_trigger(monkeypatch) -> None:
calls = {"sync": 0, "session": 0}
class _FakeBundle:
def __init__(self) -> None:
self.primary_workflow = "query_flow"
self.workflows = [SimpleNamespace(workflow_type="query_flow", workflow_action="answer")]
self.diagnostics = {}
class _FakeKernel:
@staticmethod
def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str):
_ = (source_system, source_message_id, channel_ref, sender, message_text)
return "msg-query-no-trigger"
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def _get_or_create_session(self, **kwargs):
calls["session"] += 1
_ = kwargs
raise AssertionError("group query without explicit trigger should not create an async queue session")
async def process_feishu_message(self, **kwargs):
calls["sync"] += 1
_ = kwargs
raise AssertionError("group query without explicit trigger should not run sync QA")
monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel)
monkeypatch.setattr(
mixin_capture_entry.WorkflowSemanticPlanner,
"aplan_message",
classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())),
)
service = ProactiveSignalCaptureService(object())
result = await service.capture_from_feishu(
group_chat_id="chat-query-no-trigger",
message_content="最近AI行业有什么动态",
sender="测试用户",
tenant_id=1,
mentioned_bot_names=[],
feishu_message_id="msg-query-no-trigger",
chat_type="group",
thread_context=None,
)
assert result["status"] == "ignored"
assert result["reason"] == "group_explicit_trigger_required"
assert result["direct_reply_required"] is False
assert calls["sync"] == 0
assert calls["session"] == 0
@pytest.mark.asyncio
async def test_feishu_business_review_query_forwards_semantic_bundle_into_kernel(monkeypatch) -> None:
captured: dict[str, object] = {}
class _FakeBundle:
def __init__(self) -> None:
self.primary_workflow = "query_flow"
self.workflows = [SimpleNamespace(workflow_type="query_flow", workflow_action="answer")]
self.diagnostics = {
"message_intent": "business_review",
"consultant_mode_preference": "clarify",
}
def to_dict(self) -> dict[str, object]:
return {
"primary_workflow": "query_flow",
"workflows": [
{
"workflow_type": "query_flow",
"workflow_action": "answer",
}
],
"diagnostics": dict(self.diagnostics),
}
class _FakeKernel:
@staticmethod
def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str):
_ = (source_system, source_message_id, channel_ref, sender, message_text)
return "msg-business-review"
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def _get_or_create_session(self, **kwargs):
_ = kwargs
raise AssertionError("business review sync path should not create async queue session")
def _is_short_confirmation_message(self, **kwargs):
_ = kwargs
return False
async def process_feishu_message(self, **kwargs):
captured["kwargs"] = kwargs
return {
"handled": True,
"status": "answered",
"classification": "question",
"linked_project_ids": [],
"signal_id": None,
"created_project_id": None,
"auto_tasks": [],
"ai_trace_id": "trace-br",
"direct_reply_required": True,
"direct_reply_text": "五大口径评审中。",
}
async def _noop_sync_recent_cache(self, **kwargs):
_ = (self, kwargs)
monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel)
monkeypatch.setattr(ProactiveSignalCaptureService, "_sync_feishu_recent_cache", _noop_sync_recent_cache)
monkeypatch.setattr(
mixin_capture_entry.WorkflowSemanticPlanner,
"aplan_message",
classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())),
)
service = ProactiveSignalCaptureService(object())
result = await service.capture_from_feishu(
group_chat_id="chat-br-1",
message_content="我现在要创建需求为一个大模型api聚合平台,你帮我分析下要怎么做,",
sender="测试用户",
tenant_id=1,
mentioned_bot_names=["PMFlow"],
feishu_message_id="msg-business-review",
chat_type="group",
)
assert result["status"] == "answered"
kwargs = captured["kwargs"]
assert isinstance(kwargs, dict)
semantic_bundle = kwargs["semantic_bundle"]
assert semantic_bundle["primary_workflow"] == "query_flow"
assert semantic_bundle["diagnostics"]["message_intent"] == "business_review"
@pytest.mark.asyncio
async def test_force_sync_feishu_query_flow_still_forwards_semantic_bundle_into_kernel(monkeypatch) -> None:
captured: dict[str, object] = {}
class _FakeBundle:
def __init__(self) -> None:
self.primary_workflow = "query_flow"
self.workflows = [SimpleNamespace(workflow_type="query_flow", workflow_action="answer")]
self.diagnostics = {
"message_intent": "business_review",
"consultant_mode_preference": "clarify",
}
def to_dict(self) -> dict[str, object]:
return {
"primary_workflow": "query_flow",
"workflows": [
{
"workflow_type": "query_flow",
"workflow_action": "answer",
}
],
"diagnostics": dict(self.diagnostics),
}
class _FakeDB:
async def commit(self) -> None:
return None
async def rollback(self) -> None:
return None
class _FakeKernel:
@staticmethod
def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str):
_ = (source_system, source_message_id, channel_ref, sender, message_text)
return "msg-force-sync-query"
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def _get_or_create_session(self, **kwargs):
_ = kwargs
raise AssertionError("force_sync query path should not create async queue session")
def _is_short_confirmation_message(self, **kwargs):
_ = kwargs
return False
async def process_feishu_message(self, **kwargs):
captured["kwargs"] = kwargs
return {
"handled": True,
"status": "answered",
"classification": "question",
"linked_project_ids": [],
"signal_id": None,
"created_project_id": None,
"auto_tasks": [],
"ai_trace_id": "trace-force-sync-query",
"direct_reply_required": True,
"direct_reply_text": "先做行业调研和需求收敛。",
}
async def _noop_sync_recent_cache(self, **kwargs):
_ = (self, kwargs)
monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel)
monkeypatch.setattr(ProactiveSignalCaptureService, "_sync_feishu_recent_cache", _noop_sync_recent_cache)
monkeypatch.setattr(
mixin_capture_entry.WorkflowSemanticPlanner,
"aplan_message",
classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())),
)
service = ProactiveSignalCaptureService(_FakeDB())
result = await service.capture_from_feishu(
group_chat_id="chat-force-sync-query",
message_content="想了解下当前电动车的行业,以此做一个mvp需求",
sender="测试用户",
tenant_id=1,
feishu_message_id="msg-force-sync-query",
chat_type="group",
force_sync=True,
)
assert result["status"] == "answered"
kwargs = captured["kwargs"]
assert isinstance(kwargs, dict)
semantic_bundle = kwargs["semantic_bundle"]
assert semantic_bundle["primary_workflow"] == "query_flow"
assert semantic_bundle["diagnostics"]["message_intent"] == "business_review"
@pytest.mark.asyncio
async def test_process_feishu_with_kernel_sync_rolls_back_current_session_and_surfaces_real_error() -> None:
class _FakeDB:
def __init__(self) -> None:
self.rollback_calls = 0
async def rollback(self) -> None:
self.rollback_calls += 1
class _FakeKernel:
async def process_feishu_message(self, **kwargs):
_ = kwargs
return {
"handled": False,
"reason": "kernel_exception",
"error": "No row was found when one was required",
}
service = ProactiveSignalCaptureService(_FakeDB())
result = await service._process_feishu_with_kernel_sync(
trace_id="trace-kernel-error",
tenant_id=1,
kernel=_FakeKernel(),
group_chat_id="chat-rollback",
cleaned_message="先不要直接创建,我还在补充需求。",
proposer_name="测试用户",
proposer_open_id="ou-rollback",
captured_dt=mixin_capture_entry.now_cn_aware(),
feishu_message_id="msg-rollback",
chat_type="group",
)
assert service.db.rollback_calls == 1
assert result["status"] == "error"
assert result["reason"] == "kernel_exception"
assert "No row was found when one was required" in result["direct_reply_text"]
@pytest.mark.asyncio
async def test_process_feishu_with_kernel_sync_commits_successful_runtime_finalize() -> None:
class _FakeDB:
def __init__(self) -> None:
self.commit_calls = 0
self.rollback_calls = 0
async def commit(self) -> None:
self.commit_calls += 1
async def rollback(self) -> None:
self.rollback_calls += 1
class _FakeKernel:
async def process_feishu_message(self, **kwargs):
_ = kwargs
return {
"handled": True,
"status": "captured",
"classification": "command",
"work_item_id": 55,
"direct_reply_required": True,
"direct_reply_text": "已进入 mounted runtime。",
}
class _FakeService(ProactiveSignalCaptureService):
async def _apply_pipeline_post_actions(self, **kwargs):
return kwargs["pipeline_result"]
async def _sync_feishu_recent_cache(self, **kwargs):
_ = kwargs
return None
service = _FakeService(_FakeDB())
result = await service._process_feishu_with_kernel_sync(
trace_id="trace-sync-commit",
tenant_id=1,
kernel=_FakeKernel(),
group_chat_id="chat-sync-commit",
cleaned_message="先不要创建,我还在补范围。",
proposer_name="测试用户",
proposer_open_id="ou-sync-commit",
captured_dt=mixin_capture_entry.now_cn_aware(),
feishu_message_id="msg-sync-commit",
chat_type="group",
)
assert result["status"] == "captured"
assert service.db.commit_calls == 1
assert service.db.rollback_calls == 0
@pytest.mark.asyncio
async def test_capture_from_feishu_outer_exception_rolls_back_current_session_and_surfaces_error(monkeypatch) -> None:
class _FakeDB:
def __init__(self) -> None:
self.rollback_calls = 0
async def rollback(self) -> None:
self.rollback_calls += 1
class _FakeKernel:
@staticmethod
def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str):
_ = (source_system, source_message_id, channel_ref, sender, message_text)
return "msg-outer-exception"
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def process_feishu_message(self, **kwargs):
_ = kwargs
raise RuntimeError("boom")
class _FakeSemanticBundle:
primary_workflow = "query_flow"
diagnostics: dict[str, object] = {}
workflows: list[object] = []
def to_dict(self) -> dict[str, object]:
return {
"primary_workflow": self.primary_workflow,
"diagnostics": {},
"workflows": [],
}
async def _fake_aplan_message(**kwargs):
_ = kwargs
return _FakeSemanticBundle()
monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel)
monkeypatch.setattr(mixin_capture_entry.WorkflowSemanticPlanner, "aplan_message", _fake_aplan_message)
service = ProactiveSignalCaptureService(_FakeDB())
result = await service.capture_from_feishu(
group_chat_id="chat-outer-exception",
message_content="帮我看看这个需求",
sender="测试用户",
tenant_id=1,
feishu_message_id="msg-outer-exception",
chat_type="group",
force_sync=True,
)
assert service.db.rollback_calls == 1
assert result["status"] == "error"
assert result["reason"] == "kernel_failed"
assert "boom" in result["direct_reply_text"]
@pytest.mark.asyncio
async def test_capture_from_feishu_skips_when_source_activation_whitelist_misses(monkeypatch) -> None:
class _FakeKernel:
@staticmethod
def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str):
_ = (source_system, source_message_id, channel_ref, sender, message_text)
return "msg-gated"
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def _get_or_create_session(self, **kwargs):
raise AssertionError("source activation miss should stop before queue session creation")
async def process_feishu_message(self, **kwargs):
raise AssertionError("source activation miss should not reach kernel processing")
class _FakeMountedRuntimeWriteService:
def __init__(self, db):
_ = db
async def resolve_source_activation_scope(
self,
*,
tenant_id: int,
source_system: str,
channel_ref: str | None,
thread_context: dict | None,
):
_ = (tenant_id, source_system, channel_ref, thread_context)
return None, True
monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel)
monkeypatch.setattr(mixin_capture_entry, "MountedAIRuntimeWriteService", _FakeMountedRuntimeWriteService)
service = ProactiveSignalCaptureService(object())
result = await service.capture_from_feishu(
group_chat_id="chat-not-whitelisted",
message_content="这个需求帮我继续推进一下",
sender="测试用户",
tenant_id=1,
feishu_message_id="msg-gated",
chat_type="group",
)
assert result["status"] == "ignored"
assert result["reason"] == "mounted_runtime_scope_not_activated"
assert result["direct_reply_required"] is False
def test_capture_from_feishu_no_longer_references_legacy_fallback() -> None:
source = inspect.getsource(mixin_capture_entry.SignalCaptureEntryMixin.capture_from_feishu)
assert "_capture_from_feishu_legacy_flow" not in source
@pytest.mark.asyncio
async def test_should_process_feishu_high_accuracy_treats_requirement_plan_as_sync(monkeypatch) -> None:
class _FakeBundle:
primary_workflow = "query_flow"
workflows = [SimpleNamespace(workflow_type="query_flow", workflow_action="answer")]
diagnostics = {"message_intent": "requirement_plan"}
monkeypatch.setattr(
mixin_capture_entry.WorkflowSemanticPlanner,
"aplan_message",
classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())),
)
should_sync, reason = await mixin_capture_entry.SignalCaptureEntryMixin._should_process_feishu_high_accuracy(
message_text="@PMFlow 想跟你聊聊需求,做一个自动给每日的大A趋势打分的小工具,先不创建,先收敛第一版。",
)
assert should_sync is True
assert reason == "semantic:requirement_plan"
@pytest.mark.asyncio
async def test_should_process_feishu_high_accuracy_treats_requirement_plan_slot_answer_as_sync(monkeypatch) -> None:
class _FakeBundle:
primary_workflow = "query_flow"
workflows = [SimpleNamespace(workflow_type="query_flow", workflow_action="answer")]
diagnostics = {"message_intent": "requirement_plan"}
monkeypatch.setattr(
mixin_capture_entry.WorkflowSemanticPlanner,
"aplan_message",
classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())),
)
should_sync, reason = await mixin_capture_entry.SignalCaptureEntryMixin._should_process_feishu_high_accuracy(
message_text="@PMFlow 先按技术指标来,主要给投顾看。",
)
assert should_sync is True
assert reason == "semantic:requirement_plan"
@pytest.mark.asyncio
async def test_should_process_feishu_high_accuracy_no_longer_uses_question_suffix_fallback(monkeypatch) -> None:
class _FakeBundle:
primary_workflow = "project_signal_flow"
workflows = [SimpleNamespace(workflow_type="project_signal_flow", workflow_action="capture")]
diagnostics = {"message_intent": "casual_chat"}
monkeypatch.setattr(
mixin_capture_entry.WorkflowSemanticPlanner,
"aplan_message",
classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())),
)
should_sync, reason = await mixin_capture_entry.SignalCaptureEntryMixin._should_process_feishu_high_accuracy(
message_text="@PMFlow 现在几点?",
)
assert should_sync is False
assert reason == "async_queue"
@pytest.mark.asyncio
async def test_capture_from_feishu_requirement_plan_skips_async_ack_and_forwards_semantic_bundle(monkeypatch) -> None:
captured: dict[str, object] = {}
class _FakeBundle:
primary_workflow = "query_flow"
workflows = [SimpleNamespace(workflow_type="query_flow", workflow_action="answer")]
diagnostics = {"message_intent": "requirement_plan"}
def to_dict(self) -> dict[str, object]:
return {
"primary_workflow": "query_flow",
"workflows": [{"workflow_type": "query_flow", "workflow_action": "answer"}],
"diagnostics": dict(self.diagnostics),
}
class _FakeDB:
async def commit(self) -> None:
return None
async def rollback(self) -> None:
return None
class _FakeKernel:
@staticmethod
def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str):
_ = (source_system, source_message_id, channel_ref, sender, message_text)
return "msg-plan-sync"
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def _get_or_create_session(self, **kwargs):
_ = kwargs
raise AssertionError("requirement plan sync path should not create async queue session")
def _is_short_confirmation_message(self, **kwargs):
_ = kwargs
return False
async def process_feishu_message(self, **kwargs):
captured["kwargs"] = kwargs
return {
"handled": True,
"status": "answered",
"classification": "question",
"linked_project_ids": [],
"signal_id": None,
"created_project_id": None,
"auto_tasks": [],
"ai_trace_id": "trace-plan-sync",
"direct_reply_required": True,
"direct_reply_text": "可以,我们先只聊需求,不创建。先定一个最关键的问题:你这个分数主要想根据什么来打,比如技术指标、情绪数据,或者你已经有一套规则想自动化?",
}
class _FakeMountedRuntimeWriteService:
def __init__(self, db):
_ = db
async def resolve_source_activation_scope(
self,
*,
tenant_id: int,
source_system: str,
channel_ref: str | None,
thread_context: dict | None,
):
_ = (tenant_id, source_system, channel_ref, thread_context)
return SimpleNamespace(id=1, source_system="feishu"), True
async def _noop_sync_recent_cache(self, **kwargs):
_ = (self, kwargs)
monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel)
monkeypatch.setattr(mixin_capture_entry, "MountedAIRuntimeWriteService", _FakeMountedRuntimeWriteService)
monkeypatch.setattr(ProactiveSignalCaptureService, "_sync_feishu_recent_cache", _noop_sync_recent_cache)
monkeypatch.setattr(
mixin_capture_entry.WorkflowSemanticPlanner,
"aplan_message",
classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())),
)
service = ProactiveSignalCaptureService(_FakeDB())
result = await service.capture_from_feishu(
group_chat_id="chat-plan-sync",
message_content="@PMFlow 想跟你聊聊需求,做一个自动给每日的大A趋势打分的小工具,先不创建,先收敛第一版。",
sender="测试用户",
tenant_id=1,
feishu_message_id="msg-plan-sync",
chat_type="group",
)
assert result["status"] == "answered"
assert result["direct_reply_text"].startswith("可以,我们先只聊需求")
kwargs = captured["kwargs"]
assert isinstance(kwargs, dict)
semantic_bundle = kwargs["semantic_bundle"]
assert semantic_bundle["diagnostics"]["message_intent"] == "requirement_plan"
assert semantic_bundle["primary_workflow"] == "query_flow"
@pytest.mark.asyncio
async def test_capture_from_feishu_requirement_plan_slot_answer_stays_sync_without_async_ack(monkeypatch) -> None:
captured: dict[str, object] = {}
class _FakeBundle:
primary_workflow = "query_flow"
workflows = [SimpleNamespace(workflow_type="query_flow", workflow_action="answer")]
diagnostics = {"message_intent": "requirement_plan"}
def to_dict(self) -> dict[str, object]:
return {
"primary_workflow": "query_flow",
"workflows": [{"workflow_type": "query_flow", "workflow_action": "answer"}],
"diagnostics": dict(self.diagnostics),
}
class _FakeDB:
async def commit(self) -> None:
return None
async def rollback(self) -> None:
return None
class _FakeKernel:
@staticmethod
def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str):
_ = (source_system, source_message_id, channel_ref, sender, message_text)
return "msg-plan-followup-sync"
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def _get_or_create_session(self, **kwargs):
_ = kwargs
raise AssertionError("requirement plan slot answers should not create async queue session")
def _is_short_confirmation_message(self, **kwargs):
_ = kwargs
return False
async def process_feishu_message(self, **kwargs):
captured["kwargs"] = kwargs
return {
"handled": True,
"status": "answered",
"classification": "question",
"linked_project_ids": [],
"signal_id": None,
"created_project_id": None,
"auto_tasks": [],
"ai_trace_id": "trace-plan-followup-sync",
"direct_reply_required": True,
"direct_reply_text": "好,我先记下:第一版按技术指标打分,主要给投顾看。下一个关键问题:你更想让系统怎么给你这个结果,是飞书日报、页面看板,还是只推送一个分数加一句解释?",
}
class _FakeMountedRuntimeWriteService:
def __init__(self, db):
_ = db
async def resolve_source_activation_scope(
self,
*,
tenant_id: int,
source_system: str,
channel_ref: str | None,
thread_context: dict | None,
):
_ = (tenant_id, source_system, channel_ref, thread_context)
return SimpleNamespace(id=1, source_system="feishu"), True
async def _noop_sync_recent_cache(self, **kwargs):
_ = (self, kwargs)
monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel)
monkeypatch.setattr(mixin_capture_entry, "MountedAIRuntimeWriteService", _FakeMountedRuntimeWriteService)
monkeypatch.setattr(ProactiveSignalCaptureService, "_sync_feishu_recent_cache", _noop_sync_recent_cache)
monkeypatch.setattr(
mixin_capture_entry.WorkflowSemanticPlanner,
"aplan_message",
classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())),
)
service = ProactiveSignalCaptureService(_FakeDB())
result = await service.capture_from_feishu(
group_chat_id="chat-plan-followup-sync",
message_content="@PMFlow 先按技术指标来,主要给投顾看。",
sender="测试用户",
tenant_id=1,
feishu_message_id="msg-plan-followup-sync",
chat_type="group",
)
assert result["status"] == "answered"
assert "技术指标" in result["direct_reply_text"]
semantic_bundle = captured["kwargs"]["semantic_bundle"]
assert semantic_bundle["diagnostics"]["message_intent"] == "requirement_plan"
@pytest.mark.asyncio
async def test_capture_from_feishu_allows_group_choice_protocol_without_repeat_mention_when_requirement_plan_active(
monkeypatch,
) -> None:
captured: dict[str, object] = {}
class _FakeBundle:
primary_workflow = "query_flow"
workflows = [SimpleNamespace(workflow_type="query_flow", workflow_action="answer")]
diagnostics = {}
def to_dict(self) -> dict[str, object]:
return {
"primary_workflow": "query_flow",
"workflows": [{"workflow_type": "query_flow", "workflow_action": "answer"}],
"diagnostics": {},
}
class _FakeDB:
async def commit(self) -> None:
return None
async def rollback(self) -> None:
return None
class _FakeKernel:
@staticmethod
def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str):
_ = (source_system, source_message_id, channel_ref, sender, message_text)
return "msg-plan-choice-followup"
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def _bind_active_context_target(self, **kwargs):
captured["bind_kwargs"] = dict(kwargs)
return SimpleNamespace(id=185), SimpleNamespace(id=31, state="collecting"), "requirement_plan"
async def _get_or_create_session(self, **kwargs):
_ = kwargs
raise AssertionError("active requirement choice followup should stay sync")
async def process_feishu_message(self, **kwargs):
captured["kwargs"] = kwargs
return {
"handled": True,
"status": "answered",
"classification": "question",
"linked_project_ids": [],
"signal_id": None,
"created_project_id": None,
"auto_tasks": [],
"ai_trace_id": "trace-plan-choice-followup",
"direct_reply_required": True,
"direct_reply_text": "已确认选项 A,继续下一轮。",
}
class _FakeMountedRuntimeWriteService:
def __init__(self, db):
_ = db
async def resolve_source_activation_scope(
self,
*,
tenant_id: int,
source_system: str,
channel_ref: str | None,
thread_context: dict | None,
):
_ = (tenant_id, source_system, channel_ref, thread_context)
return SimpleNamespace(id=1, source_system="feishu"), True
async def _noop_sync_recent_cache(self, **kwargs):
_ = (self, kwargs)
monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel)
monkeypatch.setattr(mixin_capture_entry, "MountedAIRuntimeWriteService", _FakeMountedRuntimeWriteService)
monkeypatch.setattr(ProactiveSignalCaptureService, "_sync_feishu_recent_cache", _noop_sync_recent_cache)
monkeypatch.setattr(
mixin_capture_entry.WorkflowSemanticPlanner,
"aplan_message",
classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())),
)
service = ProactiveSignalCaptureService(_FakeDB())
result = await service.capture_from_feishu(
group_chat_id="chat-plan-choice-followup",
message_content="A",
sender="测试用户",
tenant_id=1,
proposer_open_id="ou-plan-choice-followup",
mentioned_bot_names=[],
feishu_message_id="msg-plan-choice-followup",
chat_type="group",
thread_context=None,
)
assert result["status"] == "answered"
assert result["direct_reply_text"] == "已确认选项 A,继续下一轮。"
assert isinstance(captured.get("bind_kwargs"), dict)
assert str(captured["bind_kwargs"]["message_text"]) == "A"
assert isinstance(captured.get("kwargs"), dict)
@pytest.mark.asyncio
async def test_capture_from_feishu_allows_group_confirm_without_repeat_mention_when_draft_pending_confirm_active(
monkeypatch,
) -> None:
captured: dict[str, object] = {}
class _FakeBundle:
primary_workflow = "query_flow"
workflows = [SimpleNamespace(workflow_type="query_flow", workflow_action="answer")]
diagnostics = {}
def to_dict(self) -> dict[str, object]:
return {
"primary_workflow": "query_flow",
"workflows": [{"workflow_type": "query_flow", "workflow_action": "answer"}],
"diagnostics": {},
}
class _FakeDB:
async def commit(self) -> None:
return None
async def rollback(self) -> None:
return None
class _FakeKernel:
@staticmethod
def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str):
_ = (source_system, source_message_id, channel_ref, sender, message_text)
return "msg-draft-confirm-followup"
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def _bind_active_context_target(self, **kwargs):
captured["bind_kwargs"] = dict(kwargs)
return SimpleNamespace(id=185), SimpleNamespace(id=31, state="draft_pending_confirm"), "draft_pending_confirm"
async def _get_or_create_session(self, **kwargs):
_ = kwargs
raise AssertionError("active draft confirm followup should stay sync")
async def process_feishu_message(self, **kwargs):
captured["kwargs"] = kwargs
return {
"handled": True,
"status": "answered",
"classification": "question",
"linked_project_ids": [119],
"signal_id": 408,
"created_project_id": 119,
"auto_tasks": [],
"ai_trace_id": "trace-draft-confirm-followup",
"direct_reply_required": True,
"direct_reply_text": "✅ 已入库",
}
class _FakeMountedRuntimeWriteService:
def __init__(self, db):
_ = db
async def resolve_source_activation_scope(
self,
*,
tenant_id: int,
source_system: str,
channel_ref: str | None,
thread_context: dict | None,
):
_ = (tenant_id, source_system, channel_ref, thread_context)
return SimpleNamespace(id=1, source_system="feishu"), True
async def _noop_sync_recent_cache(self, **kwargs):
_ = (self, kwargs)
monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel)
monkeypatch.setattr(mixin_capture_entry, "MountedAIRuntimeWriteService", _FakeMountedRuntimeWriteService)
monkeypatch.setattr(ProactiveSignalCaptureService, "_sync_feishu_recent_cache", _noop_sync_recent_cache)
monkeypatch.setattr(
mixin_capture_entry.WorkflowSemanticPlanner,
"aplan_message",
classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())),
)
service = ProactiveSignalCaptureService(_FakeDB())
result = await service.capture_from_feishu(
group_chat_id="chat-draft-confirm-followup",
message_content="确认",
sender="测试用户",
tenant_id=1,
proposer_open_id="ou-draft-confirm-followup",
mentioned_bot_names=[],
feishu_message_id="msg-draft-confirm-followup",
chat_type="group",
thread_context=None,
)
assert result["status"] == "answered"
assert result["direct_reply_text"] == "✅ 已入库"
assert isinstance(captured.get("bind_kwargs"), dict)
assert str(captured["bind_kwargs"]["message_text"]) == "确认"
assert isinstance(captured.get("kwargs"), dict)
@pytest.mark.asyncio
async def test_draft_confirm_handler_resolves_active_mounted_draft_when_topic_mode_is_draft_ready(
monkeypatch,
) -> None:
class _FakeResult:
def __init__(self, rows) -> None:
self._rows = list(rows)
def all(self):
return list(self._rows)
draft_row = SimpleNamespace(
id=31,
topic_workspace_id=185,
active_work_item_id=901,
state="draft_pending_confirm",
latest_reply_message_id=None,
latest_source_message_id="msg-draft-root",
)
topic_row = SimpleNamespace(
id=185,
tenant_id=1,
source_system="feishu",
mode="draft_ready",
status="active",
channel_ref="chat-draft-confirm-followup",
conversation_scope_key="feishu:chat-draft-confirm-followup:ou-draft-confirm-followup",
plan_status="draft_pending_confirm",
root_message_id="msg-draft-root",
thread_key="",
last_message_id="msg-draft-root",
updated_at=None,
)
class _FakeDB:
async def execute(self, query):
sql = str(
query.compile(compile_kwargs={"literal_binds": True})
).lower()
if "mounted_topic_workspaces.mode = 'plan'" in sql:
return _FakeResult([])
if "'draft_ready'" in sql and "'plan'" in sql:
return _FakeResult([(draft_row, topic_row)])
return _FakeResult([])
async def _fake_load_work_item_target_by_id(self, *, work_item_id: int):
return SimpleNamespace(id=work_item_id, state="draft_pending_confirm"), None
async def _fake_release_mounted_draft_binding(self, **kwargs):
raise AssertionError(f"active mounted draft should not be released: {kwargs}")
monkeypatch.setattr(
DraftConfirmHandler,
"_load_work_item_target_by_id",
_fake_load_work_item_target_by_id,
)
monkeypatch.setattr(
DraftConfirmHandler,
"_release_mounted_draft_binding",
_fake_release_mounted_draft_binding,
)
handler = DraftConfirmHandler(SimpleNamespace(db=_FakeDB(), tenant_id=1))
result = await handler._resolve_active_mounted_draft(
sender="测试用户",
source_system="feishu",
channel_ref="chat-draft-confirm-followup",
source_sender_open_id="ou-draft-confirm-followup",
thread_context={"reply_to_message_id": "msg-draft-root"},
)
assert result == (draft_row, topic_row)
@pytest.mark.asyncio
async def test_capture_from_feishu_group_choice_protocol_prefers_active_requirement_over_older_pending_draft(
monkeypatch,
) -> None:
captured: dict[str, object] = {}
class _FakeBundle:
primary_workflow = "query_flow"
workflows = [SimpleNamespace(workflow_type="query_flow", workflow_action="answer")]
diagnostics = {}
def to_dict(self) -> dict[str, object]:
return {
"primary_workflow": "query_flow",
"workflows": [{"workflow_type": "query_flow", "workflow_action": "answer"}],
"diagnostics": {},
}
class _FakeDB:
async def commit(self) -> None:
return None
async def rollback(self) -> None:
return None
class _FakeKernel:
@staticmethod
def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str):
_ = (source_system, source_message_id, channel_ref, sender, message_text)
return "msg-plan-choice-priority"
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def _find_active_requirement_plan_workspace(self, **kwargs):
captured["finder_kwargs"] = dict(kwargs)
return SimpleNamespace(id=185), SimpleNamespace(id=31, state="collecting")
@staticmethod
def _derive_active_context_kind(*, topic, draft):
_ = (topic, draft)
return "requirement_plan"
async def _bind_active_context_target(self, **kwargs):
captured["bind_kwargs"] = dict(kwargs)
return SimpleNamespace(id=178), SimpleNamespace(id=30, state="draft_pending_confirm"), "draft_pending_confirm"
async def _get_or_create_session(self, **kwargs):
_ = kwargs
raise AssertionError("choice protocol should stay sync on active requirement")
async def process_feishu_message(self, **kwargs):
captured["kwargs"] = kwargs
return {
"handled": True,
"status": "answered",
"classification": "question",
"linked_project_ids": [],
"signal_id": None,
"created_project_id": None,
"auto_tasks": [],
"ai_trace_id": "trace-plan-choice-priority",
"direct_reply_required": True,
"direct_reply_text": "已确认选项 A,继续下一轮。",
}
class _FakeMountedRuntimeWriteService:
def __init__(self, db):
_ = db
async def resolve_source_activation_scope(
self,
*,
tenant_id: int,
source_system: str,
channel_ref: str | None,
thread_context: dict | None,
):
_ = (tenant_id, source_system, channel_ref, thread_context)
return SimpleNamespace(id=1, source_system="feishu"), True
async def _noop_sync_recent_cache(self, **kwargs):
_ = (self, kwargs)
monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel)
monkeypatch.setattr(mixin_capture_entry, "MountedAIRuntimeWriteService", _FakeMountedRuntimeWriteService)
monkeypatch.setattr(ProactiveSignalCaptureService, "_sync_feishu_recent_cache", _noop_sync_recent_cache)
monkeypatch.setattr(
mixin_capture_entry.WorkflowSemanticPlanner,
"aplan_message",
classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())),
)
service = ProactiveSignalCaptureService(_FakeDB())
result = await service.capture_from_feishu(
group_chat_id="chat-plan-choice-priority",
message_content="A",
sender="测试用户",
tenant_id=1,
proposer_open_id="ou-plan-choice-priority",
mentioned_bot_names=[],
feishu_message_id="msg-plan-choice-priority",
chat_type="group",
thread_context=None,
)
assert result["status"] == "answered"
assert result["direct_reply_text"] == "已确认选项 A,继续下一轮。"
assert isinstance(captured.get("finder_kwargs"), dict)
assert isinstance(captured.get("kwargs"), dict)
@pytest.mark.asyncio
async def test_capture_from_feishu_group_choice_protocol_uses_executor_requirement_finder_when_kernel_lacks_direct_finder(
monkeypatch,
) -> None:
captured: dict[str, object] = {}
class _FakeBundle:
primary_workflow = "query_flow"
workflows = [SimpleNamespace(workflow_type="query_flow", workflow_action="answer")]
diagnostics = {}
def to_dict(self) -> dict[str, object]:
return {
"primary_workflow": "query_flow",
"workflows": [{"workflow_type": "query_flow", "workflow_action": "answer"}],
"diagnostics": {},
}
class _FakeDB:
async def commit(self) -> None:
return None
async def rollback(self) -> None:
return None
class _FakeExecutor:
async def _find_active_requirement_plan_workspace(self, **kwargs):
captured["finder_kwargs"] = dict(kwargs)
return SimpleNamespace(id=185, root_message_id="om-active-plan-root"), SimpleNamespace(id=31, state="collecting")
class _FakeKernel:
@staticmethod
def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str):
_ = (source_system, source_message_id, channel_ref, sender, message_text)
return "msg-plan-choice-executor-finder"
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
self.executor = _FakeExecutor()
@staticmethod
def _derive_active_context_kind(*, topic, draft):
_ = (topic, draft)
return "requirement_plan"
async def _bind_active_context_target(self, **kwargs):
captured["bind_kwargs"] = dict(kwargs)
return None
async def _get_or_create_session(self, **kwargs):
_ = kwargs
raise AssertionError("executor-based requirement finder should keep choice followup sync")
async def process_feishu_message(self, **kwargs):
captured["kwargs"] = kwargs
return {
"handled": True,
"status": "answered",
"classification": "question",
"linked_project_ids": [],
"signal_id": None,
"created_project_id": None,
"auto_tasks": [],
"ai_trace_id": "trace-plan-choice-executor-finder",
"direct_reply_required": True,
"direct_reply_text": "已确认选项 A,继续下一轮。",
}
class _FakeMountedRuntimeWriteService:
def __init__(self, db):
_ = db
async def resolve_source_activation_scope(
self,
*,
tenant_id: int,
source_system: str,
channel_ref: str | None,
thread_context: dict | None,
):
_ = (tenant_id, source_system, channel_ref, thread_context)
return SimpleNamespace(id=1, source_system="feishu"), True
async def _noop_sync_recent_cache(self, **kwargs):
_ = (self, kwargs)
monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel)
monkeypatch.setattr(mixin_capture_entry, "MountedAIRuntimeWriteService", _FakeMountedRuntimeWriteService)
monkeypatch.setattr(ProactiveSignalCaptureService, "_sync_feishu_recent_cache", _noop_sync_recent_cache)
monkeypatch.setattr(
mixin_capture_entry.WorkflowSemanticPlanner,
"aplan_message",
classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())),
)
service = ProactiveSignalCaptureService(_FakeDB())
result = await service.capture_from_feishu(
group_chat_id="chat-plan-choice-executor-finder",
message_content="A",
sender="测试用户",
tenant_id=1,
proposer_open_id="ou-plan-choice-executor-finder",
mentioned_bot_names=[],
feishu_message_id="msg-plan-choice-executor-finder",
chat_type="group",
thread_context=None,
)
assert result["status"] == "answered"
assert result["direct_reply_text"] == "已确认选项 A,继续下一轮。"
assert isinstance(captured.get("finder_kwargs"), dict)
assert isinstance(captured.get("kwargs"), dict)
thread_context = captured["kwargs"]["thread_context"]
assert isinstance(thread_context, dict)
assert thread_context["root_message_id"] == "om-active-plan-root"
assert thread_context["reply_to_message_id"] == "om-active-plan-root"
@pytest.mark.asyncio
async def test_project_context_prefill_skips_chatwide_reply_fallback(monkeypatch) -> None:
called = {"fallback": False}
class _FakePrefill(FeishuPipelineContextPrefillBMixin):
def __init__(self) -> None:
self.db = object()
self.tenant_id = 1
@staticmethod
def _is_contextual_project_request(message: str) -> bool:
return True
async def _load_recent_chat_turns(self, **kwargs):
_ = kwargs
return []
async def _load_recent_chat_reply_fallback_text(self, *, source_chat_id: str, limit: int = 5) -> str:
_ = (source_chat_id, limit)
called["fallback"] = True
raise AssertionError("reply fallback should not be used when no recent turns are available")
fields = {
"title": "",
"description": "",
"project_scope": "",
"missing_fields": ["title", "description", "project_scope"],
"ready": False,
}
result = await _FakePrefill()._maybe_prefill_project_fields_from_context(
message="根据这个创建一个项目",
source_chat_id="chat-1",
source_sender_open_id="ou-1",
sender="测试用户",
fields=fields,
source_message_id="msg-1",
thread_context={"reply_to_message_id": "msg-root-1"},
)
assert result == fields
assert called["fallback"] is False
@pytest.mark.asyncio
async def test_project_prefill_fallback_leaves_owner_and_timeline_empty() -> None:
class _FakeLLM:
async def generate(self, *args, **kwargs):
_ = args, kwargs
raise RuntimeError("boom")
class _FakeExtraction(FeishuPipelineExtractionMixin):
def __init__(self) -> None:
self.db = object()
self.tenant_id = 1
self.llm = _FakeLLM()
result = await _FakeExtraction()._extract_project_prefill(
"我们想做一个内部知识库升级项目,先把项目名称和范围整理出来。"
)
assert result["suggested_owner"] is None
assert result["suggested_timeline"] is None
assert float(result["confidence"]) <= 0.35
assert len(result["reasoning"]) >= 1
@pytest.mark.asyncio
async def test_create_project_suggestion_preserves_uncertain_owner_and_timeline() -> None:
class _FakeDB:
def __init__(self) -> None:
self.added = []
def add(self, obj):
self.added.append(obj)
async def flush(self):
return None
class _FakeProcessing(FeishuPipelineItemProcessingMixin):
def __init__(self) -> None:
self.db = _FakeDB()
self.tenant_id = 1
async def _extract_project_prefill(self, message: str):
_ = message
return {
"suggested_name": "内部知识库升级",
"suggested_description": "围绕内部知识库问答能力升级",
"suggested_owner": None,
"suggested_timeline": None,
"confidence": 0.31,
"reasoning": ["需求里没有明确负责人", "需求里没有明确时间线"],
}
project_info, suggestion = await _FakeProcessing()._create_project_suggestion(
item_id=12,
content="我们想做一个内部知识库升级项目,先把项目名称和范围整理出来。",
sender="测试用户",
fallback_confidence=0.31,
)
assert project_info["suggested_owner"] is None
assert project_info["suggested_timeline"] is None
assert suggestion.suggested_owner is None
assert suggestion.suggested_timeline is None
def test_project_create_idempotency_key_prefers_message_key() -> None:
key_one = FeishuPipelineCommandCommitProjectMixin._build_project_create_idempotency_key(
idempotency_key="feishu:sm-123",
trace_id="trace-1",
)
key_two = FeishuPipelineCommandCommitProjectMixin._build_project_create_idempotency_key(
idempotency_key="feishu:sm-123",
trace_id="trace-2",
)
fallback_key = FeishuPipelineCommandCommitProjectMixin._build_project_create_idempotency_key(
idempotency_key=None,
trace_id="trace-3",
)
assert key_one == "feishu:sm-123"
assert key_two == "feishu:sm-123"
assert fallback_key == "trace-3"
def test_task_orchestration_charter_context_flattens_modules() -> None:
charter = SimpleNamespace(
id=55,
title="内部知识库升级宪章",
version=2,
status="aligning",
content={
"modules": {
"module_1": {
"problem_statement": "减少重复检索",
"goal_statement": "提升命中率 / 降低重复提问",
"kpis": "首响 < 3s",
},
"module_2": {
"in_scope": "问答检索,知识补充",
"out_of_scope": "权限模型重构",
},
"module_4": {
"milestones": "需求冻结\n开发完成",
},
"module_5": {
"risk_register": "需求膨胀; 依赖不清",
},
"module_6": {
"governance_rule": "附件仅作证据引用",
},
}
},
)
context = SignalCaptureTaskOrchestrationMainMixin._build_task_orchestration_charter_context(charter)
assert context["title"] == "内部知识库升级宪章"
assert context["summary"] == "减少重复检索"
assert context["objectives"] == ["提升命中率", "降低重复提问"]
assert context["in_scope"] == ["问答检索", "知识补充"]
assert context["out_of_scope"] == ["权限模型重构"]
assert context["milestones"] == ["需求冻结", "开发完成"]
assert "附件仅作证据引用" in context["constraints"][0]
def test_task_orchestration_prompt_embeds_charter_context() -> None:
prompt = SignalCaptureTaskOrchestrationMainMixin._build_task_orchestration_prompt(
project_payload={
"id": 6,
"title": "内部知识库升级",
"description": "围绕内部知识库问答能力升级",
"assigned_role": "backend",
"status": "active",
},
charter_payload={
"id": 55,
"title": "内部知识库升级宪章",
"version": 2,
"status": "aligning",
"summary": "减少重复检索",
"objectives": ["提升命中率", "降低重复提问"],
"scope": "知识库检索与问答",
"in_scope": ["问答检索", "知识补充"],
"out_of_scope": ["权限模型重构"],
"success_criteria": ["首响 < 3s"],
"constraints": ["附件仅作证据引用"],
"milestones": ["需求冻结", "开发完成"],
"risks": ["需求膨胀"],
},
signal_payload={
"signal_id": 8,
"message": "支持导出知识库内容",
"classification": {"classification": "new_project"},
"proposer": "测试用户",
"captured_at": "2026-03-25T10:00:00+08:00",
},
team_members=[
{"user_id": 1, "name": "Alice", "workload_ratio": 42.0, "skill_tag_ids": [11, 12]},
],
)
assert "charter_json" in prompt
assert "内部知识库升级宪章" in prompt
assert "success_criteria" in prompt
assert "附件仅作证据引用" in prompt
def test_task_orchestration_fallback_uses_charter_context() -> None:
class _FakeSkill(SignalCaptureTaskOrchestrationSkillMixin):
@staticmethod
def _normalize_single_line_text(text: str, max_len: int = 64) -> str:
return " ".join(str(text or "").split())[:max_len]
tasks = _FakeSkill()._build_task_orchestration_fallback(
cleaned_message="新增内部知识库导出需求",
classification={"summary": "补充内部知识库导出能力"},
charter_context={
"title": "内部知识库升级宪章",
"summary": "围绕内部知识库问答体验升级",
},
)
assert len(tasks) == 2
assert all("内部知识库升级宪章" in task["description"] for task in tasks)
def test_queue_confirm_undo_prefers_feishu_message_id_for_idempotency() -> None:
item = _Item(item_id=42, level="command", operation_code="project.create")
key = FeishuPipelineQueueConfirmUndoMixin._build_operation_idempotency_key(item=item)
assert key == "feishu:msg-42"
def test_task_create_idempotency_key_prefers_message_key() -> None:
key_one = FeishuPipelineCommandCommitTaskMixin._build_task_create_idempotency_key(
idempotency_key="feishu:sm-456",
trace_id="trace-1",
)
key_two = FeishuPipelineCommandCommitTaskMixin._build_task_create_idempotency_key(
idempotency_key="feishu:sm-456",
trace_id="trace-2",
)
fallback_key = FeishuPipelineCommandCommitTaskMixin._build_task_create_idempotency_key(
idempotency_key=None,
trace_id="trace-3",
)
assert key_one == "feishu:sm-456"
assert key_two == "feishu:sm-456"
assert fallback_key == "trace-3"
@pytest.mark.asyncio
async def test_task_create_replays_existing_ledger_without_persisting_task(monkeypatch) -> None:
from app.services.integrations.feishu_signal_pipeline import mixin_command_commit_task as task_commit_module
class _FakeResult:
def __init__(self, value) -> None:
self._value = value
def scalar_one_or_none(self):
return self._value
def scalars(self):
return self
def first(self):
return self._value
class _FakeDB:
def __init__(self) -> None:
self.calls = 0
self.committed = False
async def execute(self, query):
self.calls += 1
if self.calls == 1:
return _FakeResult(type("ProjectRow", (), {"id": 11, "tenant_id": 1})())
if self.calls == 2:
return _FakeResult(type("TruthNodeRow", (), {"id": 22, "tenant_id": 1})())
raise AssertionError("unexpected execute")
async def commit(self):
self.committed = True
class _FakeLedger:
def __init__(self) -> None:
self.status = "applied"
self.result_payload = {
"status": "applied",
"message": "task created #777",
"entity_type": "task",
"entity_id": "777",
"result": {
"task_id": 777,
"title": "补充埋点",
"project_id": 11,
},
}
class _FakeCommit(FeishuPipelineCommandCommitTaskMixin):
def __init__(self) -> None:
self.db = _FakeDB()
self.tenant_id = 1
self.sync_called = False
async def _load_task_create_ledger(self, *, idempotency_key):
self.loaded_key = idempotency_key
return _FakeLedger()
async def _claim_task_create_ledger(self, *, idempotency_key, trace_id, payload):
raise AssertionError("ledger claim should not be reached when replaying an applied result")
def _sync_task_rag(self, *, task):
self.sync_called = True
async def _fail_persist_task(*args, **kwargs):
raise AssertionError("persist_task should not be called when replaying an existing ledger")
monkeypatch.setattr(task_commit_module, "persist_task", _fail_persist_task)
kernel = _FakeCommit()
result = await kernel._commit_task_create(
operator_user=type("User", (), {"id": 9})(),
payload={"project_id": 11, "title": "补充埋点", "description": "补充行为埋点"},
trace_id="trace-task-1",
idempotency_key="feishu:msg-777",
)
assert result["status"] == "applied"
assert result["result"]["task_id"] == 777
assert kernel.loaded_key == "feishu:msg-777"
assert kernel.db.committed is False
assert kernel.sync_called is False
@pytest.mark.asyncio
async def test_project_gate_always_queues_ready_create(monkeypatch) -> None:
called: dict[str, object] = {}
class _FakeGate(FeishuPipelineGateProjectMixin):
def __init__(self) -> None:
self.db = object()
self.tenant_id = 1
@staticmethod
def _build_project_clarification_message(missing_fields):
return "请补充项目信息。"
@staticmethod
def _stable_positive_int(*parts):
return 42
async def _resolve_command_operator_user(self, *, sender):
called["operator_sender"] = sender
return object()
def _can_execute_high_value_action(self, operator_user):
called["can_execute"] = operator_user
return True
async def _queue_project_intent_item(self, **kwargs):
called["queue_kwargs"] = kwargs
return {
"item_id": 42,
"level": "project",
"project_id": None,
"signal_id": None,
"route_status": "queued",
"clarification_message": "建项目已进入待确认队列,请点击继续执行。",
}
async def _write_processing_log(self, **kwargs):
called["write_processing_log"] = kwargs
async def _execute_controlled_operation(self, **kwargs):
raise AssertionError("project.create should not execute before confirmation")
kernel = _FakeGate()
result = await kernel._handle_create_project_gate(
message="请创建一个新项目《AI PMO闭环重构实战-0314》:目标是完成工作流路由重构并上线验收。",
sender="林宇航",
source_chat_id="chat-1",
source_sender_open_id="ou-1",
feishu_message_id="msg-1",
captured_at=None,
intent_gate={
"intent": "project.create",
"fields": {
"ready": True,
"title": "AI PMO闭环重构实战-0314",
"description": "目标是完成工作流路由重构并上线验收。",
"project_scope": "workflow routing",
},
},
process_started_at=0.0,
)
assert result["direct_reply_required"] is True
assert result["created_project_id"] is None
assert result["results"][0]["item_id"] == 42
assert result["reply_template_key"] == "queue_pending_confirm"
assert result["queue_item_id"] == 42
assert called["queue_kwargs"]["message"].startswith("请创建一个新项目")
@pytest.mark.asyncio
async def test_signal_route_item_ignores_low_confidence_threshold_when_project_is_matched() -> None:
class _FakeDBResult:
def scalar_one_or_none(self):
return None
class _FakeDB:
def __init__(self) -> None:
self.added = []
async def execute(self, query):
_ = query
return _FakeDBResult()
def add(self, obj):
self.added.append(obj)
async def flush(self):
return None
class _FakeFinalize(FeishuPipelineItemFinalizeRoutingMixin, FeishuPipelineMessageUtilsBMixin):
def __init__(self) -> None:
self.db = _FakeDB()
self.tenant_id = 1
self.log_kwargs: dict[str, object] = {}
self.notified: tuple[int, int] | None = None
async def _ensure_signal_link(self, *, signal_id: int, project_id: int) -> None:
_ = (signal_id, project_id)
async def _write_processing_log(self, **kwargs):
self.log_kwargs = kwargs
async def _notify_signal_auto_entered(self, *, project_id: int, signal) -> None:
self.notified = (project_id, int(signal.id))
item = SimpleNamespace(id=1)
signal = SimpleNamespace(id=88, title="直播礼物量分析")
kernel = _FakeFinalize()
result = await kernel._route_signal_item(
item=item,
signal=signal,
matched_project_id=62,
confidence=0.03,
cold_start_enabled=False,
signal_auto_enter_threshold=0.80,
)
assert result == 62
assert item.safe_to_auto_execute is True
assert item.auto_execute_state == "done"
assert item.blocking_reason is None
assert item.route_status == "auto_done"
assert kernel.notified == (62, 88)