PMFlow/backend/tests/test_feishu_pipeline_queue_...

944 lines
33 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 inspect
from types import SimpleNamespace
import pytest
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_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"
@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 _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)
service = ProactiveSignalCaptureService(object())
result = await service.capture_from_feishu(
group_chat_id="chat-1",
message_content="我感觉第一个垂直应用前景比较好,但是垂直哪个行业呢?",
sender="测试用户",
tenant_id=1,
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_business_review_query_forwards_semantic_bundle_into_kernel(monkeypatch) -> None:
captured: dict[str, object] = {}
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)
service = ProactiveSignalCaptureService(object())
result = await service.capture_from_feishu(
group_chat_id="chat-br-1",
message_content="我现在要创建需求为一个大模型api聚合平台,你帮我分析下要怎么做,",
sender="测试用户",
tenant_id=1,
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"
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_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 _looks_like_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)