from __future__ import annotations import os from datetime import datetime, timedelta from types import SimpleNamespace import pytest import pytest_asyncio from sqlalchemy import select from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine os.environ["DATABASE_URL"] = "sqlite+aiosqlite:///:memory:" from app.models.base import Base from app.models.feedback_center import FeedbackCenterRecord, FeedbackCenterRecordRevision from app.services.feedback_center.mappers import ( map_qa_feedback_to_record_payload, map_signal_feedback_to_record_payload, map_task_override_to_record_payloads, ) from app.services.feedback_center.reason_catalog import build_reason_candidates import app.services.feedback_center.service as feedback_center_service_module from app.services.feedback_center.service import FeedbackCenterService @pytest_asyncio.fixture async def db_session(): engine = create_async_engine("sqlite+aiosqlite:///:memory:") async with engine.begin() as conn: await conn.run_sync( lambda sync_conn: Base.metadata.create_all( sync_conn, tables=[ FeedbackCenterRecord.__table__, FeedbackCenterRecordRevision.__table__, ], ) ) maker = async_sessionmaker(engine, expire_on_commit=False) async with maker() as session: yield session await engine.dispose() @pytest.mark.asyncio async def test_summary_list_and_detail_respect_tenant_and_project_scope(db_session) -> None: db_session.add_all( [ FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="qa_feedback", source_id=101, object_type="qa_record", object_id=1, field_name="qa_feedback", before_value={"answer": "A"}, after_value={"rating": "negative"}, review_status="pending", reason_code="answer_incorrect", ), FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="signal_feedback", source_id=102, object_type="signal", object_id=2, field_name="strategic_classification", before_value={"predicted": True}, after_value={"actual": False}, review_status="reviewed", reason_code="answer_incorrect", ), FeedbackCenterRecord( tenant_id=1, project_id=22, source_type="task_override", source_id=103, object_type="task", object_id=3, field_name="estimated_hours", before_value=8, after_value=13, review_status="accepted", reason_code="effort_estimation_bias", ), FeedbackCenterRecord( tenant_id=2, project_id=11, source_type="qa_feedback", source_id=201, object_type="qa_record", object_id=9, field_name="qa_feedback", before_value={"answer": "B"}, after_value={"rating": "negative"}, review_status="pending", reason_code="answer_incorrect", ), ] ) await db_session.commit() service = FeedbackCenterService(db_session) summary = await service.summary(tenant_id=1, project_id=11) records = await service.list_records(tenant_id=1, project_id=11, limit=50, offset=0) assert summary["total"] == 2 assert summary["by_status"]["pending"] == 1 assert summary["by_status"]["reviewed"] == 1 assert summary["by_source_type"]["qa_feedback"] == 1 assert summary["by_source_type"]["signal_feedback"] == 1 top_reason_code = summary["top_reasons"][0].get("reason_code") or summary["top_reasons"][0].get("code") assert top_reason_code == "answer_incorrect" assert summary["top_reasons"][0]["count"] == 2 assert records["total"] == 2 assert len(records["items"]) == 2 assert all(item["tenant_id"] == 1 for item in records["items"]) assert all(item["project_id"] == 11 for item in records["items"]) detail = await service.get_detail( tenant_id=1, project_id=11, record_id=records["items"][0]["id"], ) assert detail is not None assert detail["record"]["project_id"] == 11 assert detail["revisions"] == [] @pytest.mark.asyncio async def test_review_updates_record_and_appends_revision_in_single_flow(db_session) -> None: record = FeedbackCenterRecord( tenant_id=1, project_id=31, source_type="task_override", source_id=501, object_type="task", object_id=88, field_name="priority", before_value="P2", after_value="P0", reason_code="manual_override", reason_text="初始记录", review_status="pending", ) db_session.add(record) await db_session.commit() service = FeedbackCenterService(db_session) reviewed = await service.review( tenant_id=1, record_id=record.id, reviewer_id=9001, review_status="accepted", reason_code="priority_misalignment", reason_text="已确认优先级应提升", reason_candidates=[{"code": "priority_misalignment", "label": "优先级偏差"}], ) assert reviewed["id"] == record.id assert reviewed["review_status"] == "accepted" assert reviewed["reason_code"] == "priority_misalignment" assert reviewed["reviewed_by"] == 9001 assert reviewed["reviewed_at"] is not None revisions = ( await db_session.execute( select(FeedbackCenterRecordRevision).where( FeedbackCenterRecordRevision.tenant_id == 1, FeedbackCenterRecordRevision.record_id == record.id, ) ) ).scalars().all() assert len(revisions) == 1 assert revisions[0].before_snapshot["review_status"] == "pending" assert revisions[0].after_snapshot["review_status"] == "accepted" assert revisions[0].reviewed_by == 9001 @pytest.mark.asyncio async def test_ingest_payloads_upsert_same_identity_and_append_revision(db_session) -> None: service = FeedbackCenterService(db_session) initial = await service.ingest_payloads( payloads=[ { "tenant_id": 1, "project_id": 44, "source_type": "task_override", "source_id": 808, "object_type": "task", "object_id": 909, "field_name": "priority", "source_title": "优先级调整", "object_title": "优先级调整", "before_value": "P2", "after_value": "P1", "ai_snapshot": {"priority": "P2"}, "ai_confidence": 0.61, "reason_candidates": [{"code": "priority_misalignment", "label": "优先级错配"}], "reason_code": "priority_misalignment", "review_status": "pending", "reviewed_by": None, "reviewed_at": None, "auto_assigned": False, } ], commit=True, ) assert len(initial) == 1 record_id = initial[0].id updated = await service.ingest_payloads( payloads=[ { "tenant_id": 1, "project_id": 44, "source_type": "task_override", "source_id": 808, "object_type": "task", "object_id": 909, "field_name": "priority", "source_title": "优先级调整(人工确认)", "object_title": "优先级调整(人工确认)", "before_value": "P2", "after_value": "P0", "ai_snapshot": {"priority": "P2"}, "ai_confidence": 0.72, "reason_candidates": [{"code": "priority_misalignment", "label": "优先级错配"}], "reason_code": "priority_misalignment", "review_status": "accepted", "reviewed_by": 42, "reviewed_at": None, "auto_assigned": False, } ], commit=True, ) assert len(updated) == 1 assert updated[0].id == record_id detail = await service.get_detail(tenant_id=1, record_id=record_id) assert detail is not None assert detail["record"]["after_value"] == "P0" assert detail["record"]["review_status"] == "accepted" assert len(detail["revisions"]) == 1 assert detail["revisions"][0]["before_snapshot"]["after_value"] == "P1" assert detail["revisions"][0]["after_snapshot"]["after_value"] == "P0" def test_reason_catalog_is_deterministic_for_field_diff() -> None: first = build_reason_candidates( source_type="task_override", field_name="estimated_hours", before_value=5, after_value=13, ) second = build_reason_candidates( source_type="task_override", field_name="estimated_hours", before_value=5, after_value=13, ) assert first == second assert first[0]["code"] == "effort_estimation_bias" def test_source_mappers_generate_unified_payloads() -> None: qa_payload = map_qa_feedback_to_record_payload( qa_feedback=SimpleNamespace( id=1, tenant_id=1, qa_id=7, rating="negative", helpfulness="low", error_types=["hallucination"], correct_answer="正确答案", submitted_by=100, created_at=None, ), qa_record=SimpleNamespace( project_id=99, question="问题", answer="旧答案", ), ) assert qa_payload["source_type"] == "qa_feedback" assert qa_payload["project_id"] == 99 assert qa_payload["object_id"] == 7 signal_payload = map_signal_feedback_to_record_payload( signal_feedback=SimpleNamespace( id=2, tenant_id=1, signal_id=33, was_strategic=False, ai_predicted_strategic=True, ai_confidence=0.81, reason="噪音信号", user_id="u-1", created_at=None, ), signal=SimpleNamespace( matched_project_id=55, title="上线异常告警", ), ) assert signal_payload["source_type"] == "signal_feedback" assert signal_payload["project_id"] == 55 assert signal_payload["field_name"] == "strategic_classification" task_payloads = map_task_override_to_record_payloads( task=SimpleNamespace( id=66, tenant_id=1, project_id=77, title="实现回放", ai_breakdown_payload={"estimated_hours": 5, "priority": "P2"}, manual_overrides={"estimated_hours": 8}, ) ) assert len(task_payloads) == 1 assert task_payloads[0]["source_type"] == "task_override" assert task_payloads[0]["before_value"] == 5 assert task_payloads[0]["after_value"] == 8 @pytest.mark.asyncio async def test_summary_metrics_cover_learning_kpis(db_session) -> None: base_time = datetime(2025, 1, 1, 10, 0, 0) db_session.add_all( [ FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="task_breakdown", source_id=201, object_type="task", object_id=1, field_name="task_breakdown", review_status="accepted", auto_assigned=True, created_at=base_time, reviewed_at=base_time + timedelta(minutes=60), ), FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="task_breakdown", source_id=202, object_type="task", object_id=2, field_name="task_breakdown", review_status="rejected", auto_assigned=False, created_at=base_time + timedelta(minutes=10), reviewed_at=base_time + timedelta(minutes=40), ), FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="task_override", source_id=301, object_type="task", object_id=1, field_name="priority", review_status="accepted", created_at=base_time + timedelta(minutes=5), reviewed_at=base_time + timedelta(minutes=20), ), FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="task_override", source_id=302, object_type="task", object_id=1, field_name="estimated_hours", review_status="pending", ), FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="qa_feedback", source_id=401, object_type="qa_record", object_id=7, field_name="qa_feedback", review_status="pending", ), FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="signal_feedback", source_id=501, object_type="signal", object_id=9, field_name="strategic_classification", review_status="reviewed", ), ] ) await db_session.commit() target_record = ( await db_session.execute( select(FeedbackCenterRecord).where( FeedbackCenterRecord.tenant_id == 1, FeedbackCenterRecord.project_id == 11, FeedbackCenterRecord.source_type == "task_override", FeedbackCenterRecord.field_name == "priority", ) ) ).scalar_one() db_session.add_all( [ FeedbackCenterRecordRevision( tenant_id=1, record_id=target_record.id, before_snapshot={"review_status": "pending"}, after_snapshot={"review_status": "reviewed"}, reviewed_by=9001, reviewed_at=base_time + timedelta(minutes=25), ), FeedbackCenterRecordRevision( tenant_id=1, record_id=target_record.id, before_snapshot={"review_status": "reviewed"}, after_snapshot={"review_status": "accepted"}, reviewed_by=9001, reviewed_at=base_time + timedelta(minutes=30), ), ] ) await db_session.commit() summary = await FeedbackCenterService(db_session).summary(tenant_id=1, project_id=11) metrics = summary["metrics"] assert metrics["accepted_rate_reviewed"] == pytest.approx(2 / 3) assert metrics["manual_override_rate"] == pytest.approx(0.5) assert metrics["auto_assigned_rate"] == pytest.approx(0.5) assert metrics["rework_rate"] == pytest.approx(1 / 6) assert metrics["avg_review_latency_minutes"] == pytest.approx(35.0) assert metrics["total_task_breakdown_count"] == 2 assert metrics["total_task_override_count"] == 2 assert metrics["total_qa_feedback_count"] == 1 assert metrics["total_signal_feedback_count"] == 1 assert metrics["total_revision_count"] == 2 @pytest.mark.asyncio async def test_summary_exposes_decision_insights_for_trend_page(db_session, monkeypatch) -> None: base_time = datetime(2025, 1, 20, 10, 0, 0) db_session.add_all( [ FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="task_breakdown", source_id=701, object_type="task", object_id=1001, field_name="task_breakdown", review_status="accepted", auto_assigned=False, created_at=base_time, reviewed_at=base_time + timedelta(minutes=90), ), FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="task_override", source_id=702, object_type="task", object_id=1001, field_name="priority", review_status="reviewed", created_at=base_time + timedelta(minutes=2), reviewed_at=base_time + timedelta(minutes=95), ), FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="task_breakdown", source_id=703, object_type="task", object_id=1002, field_name="task_breakdown", review_status="accepted", auto_assigned=True, created_at=base_time + timedelta(minutes=4), reviewed_at=base_time + timedelta(minutes=20), ), FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="task_override", source_id=704, object_type="task", object_id=1002, field_name="estimated_hours", review_status="pending", created_at=base_time + timedelta(minutes=6), ), ] ) await db_session.commit() fixed_now = datetime(2025, 1, 20, 12, 0, 0) monkeypatch.setattr(feedback_center_service_module, "now_cn_naive", lambda: fixed_now) summary = await FeedbackCenterService(db_session).summary( tenant_id=1, project_id=11, window_days=7, source_group="task", ) insights = summary["insights"] assert len(insights) >= 2 insight_codes = {item["code"] for item in insights} assert "manual_override_pressure" in insight_codes assert "auto_assignment_gap" in insight_codes manual_override_insight = next(item for item in insights if item["code"] == "manual_override_pressure") assert manual_override_insight["target_section"] == "task" assert manual_override_insight["target_source_type"] == "task_override" assert manual_override_insight["action_label"] == "查看任务覆盖样本" auto_assignment_insight = next(item for item in insights if item["code"] == "auto_assignment_gap") assert auto_assignment_insight["target_source_type"] == "task_breakdown" assert auto_assignment_insight["severity"] in {"warning", "critical"} @pytest.mark.asyncio async def test_summary_trend_cover_daily_windows_and_deltas(db_session, monkeypatch) -> None: fixed_now = datetime(2025, 1, 15, 12, 0, 0) monkeypatch.setattr(feedback_center_service_module, "now_cn_naive", lambda: fixed_now) records = [ FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="task_breakdown", source_id=601, object_type="task", object_id=100, field_name="task_breakdown", review_status="accepted", auto_assigned=True, created_at=datetime(2025, 1, 15, 8, 0, 0), reviewed_at=datetime(2025, 1, 15, 8, 30, 0), ), FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="task_breakdown", source_id=602, object_type="task", object_id=101, field_name="task_breakdown", review_status="rejected", auto_assigned=False, created_at=datetime(2025, 1, 14, 8, 0, 0), reviewed_at=datetime(2025, 1, 14, 8, 40, 0), ), FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="task_override", source_id=603, object_type="task", object_id=100, field_name="priority", review_status="pending", created_at=datetime(2025, 1, 14, 9, 0, 0), reviewed_at=datetime(2025, 1, 14, 9, 50, 0), ), FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="qa_feedback", source_id=604, object_type="qa_record", object_id=401, field_name="qa_feedback", review_status="reviewed", created_at=datetime(2025, 1, 13, 9, 0, 0), reviewed_at=datetime(2025, 1, 13, 9, 20, 0), ), FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="signal_feedback", source_id=605, object_type="signal", object_id=501, field_name="strategic_classification", review_status="pending", created_at=datetime(2025, 1, 12, 9, 0, 0), ), FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="task_breakdown", source_id=606, object_type="task", object_id=200, field_name="task_breakdown", review_status="accepted", auto_assigned=False, created_at=datetime(2025, 1, 8, 8, 0, 0), reviewed_at=datetime(2025, 1, 8, 8, 20, 0), ), ] db_session.add_all(records) await db_session.commit() target_record = ( await db_session.execute( select(FeedbackCenterRecord).where( FeedbackCenterRecord.tenant_id == 1, FeedbackCenterRecord.project_id == 11, FeedbackCenterRecord.source_type == "task_override", FeedbackCenterRecord.field_name == "priority", ) ) ).scalar_one() db_session.add_all( [ FeedbackCenterRecordRevision( tenant_id=1, record_id=target_record.id, before_snapshot={"review_status": "pending"}, after_snapshot={"review_status": "reviewed"}, reviewed_by=9001, reviewed_at=datetime(2025, 1, 14, 10, 0, 0), ), FeedbackCenterRecordRevision( tenant_id=1, record_id=target_record.id, before_snapshot={"review_status": "reviewed"}, after_snapshot={"review_status": "accepted"}, reviewed_by=9001, reviewed_at=datetime(2025, 1, 14, 10, 10, 0), ), ] ) await db_session.commit() summary = await FeedbackCenterService(db_session).summary(tenant_id=1, project_id=11) trend = summary["trend"] assert trend["window_days"] == 7 assert trend["current_window_start"] == datetime(2025, 1, 9).date() assert trend["previous_window_start"] == datetime(2025, 1, 2).date() assert len(trend["daily"]) == 7 jan14 = next(item for item in trend["daily"] if item["date"] == "2025-01-14") assert jan14["total_records"] == 2 assert jan14["task_breakdown_count"] == 1 assert jan14["task_override_count"] == 1 assert jan14["rework_count"] == 1 assert jan14["avg_review_latency_minutes"] == pytest.approx(45.0) current = trend["current"] previous = trend["previous"] deltas = trend["deltas"] assert current["total_records"] == 5 assert current["accepted_rate_reviewed"] == pytest.approx(0.5) assert current["manual_override_rate"] == pytest.approx(0.5) assert current["auto_assigned_rate"] == pytest.approx(0.5) assert current["rework_rate"] == pytest.approx(1 / 5) assert current["avg_review_latency_minutes"] == pytest.approx(35.0) assert current["revision_count"] == 2 assert previous["total_records"] == 1 assert previous["accepted_rate_reviewed"] == pytest.approx(1.0) assert previous["manual_override_rate"] == pytest.approx(0.0) assert previous["auto_assigned_rate"] == pytest.approx(0.0) assert previous["rework_rate"] == pytest.approx(0.0) assert previous["avg_review_latency_minutes"] == pytest.approx(20.0) assert deltas["total_records_delta"] == 4 assert deltas["accepted_rate_reviewed_delta"] == pytest.approx(-0.5) assert deltas["manual_override_rate_delta"] == pytest.approx(0.5) assert deltas["auto_assigned_rate_delta"] == pytest.approx(0.5) assert deltas["rework_rate_delta"] == pytest.approx(1 / 5) assert deltas["avg_review_latency_minutes_delta"] == pytest.approx(15.0) @pytest.mark.asyncio async def test_summary_trend_buckets_reviewed_activity_by_review_time(db_session, monkeypatch) -> None: fixed_now = datetime(2025, 1, 15, 12, 0, 0) monkeypatch.setattr(feedback_center_service_module, "now_cn_naive", lambda: fixed_now) db_session.add( FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="task_override", source_id=705, object_type="task", object_id=801, field_name="priority", review_status="accepted", reason_code="priority_misalignment", created_at=datetime(2025, 1, 8, 9, 0, 0), reviewed_at=datetime(2025, 1, 15, 9, 15, 0), ) ) await db_session.commit() summary = await FeedbackCenterService(db_session).summary( tenant_id=1, project_id=11, window_days=7, source_group="task", ) trend = summary["trend"] assert trend["current"]["total_records"] == 1 assert trend["previous"]["total_records"] == 0 assert any(item["date"] == "2025-01-15" and item["total_records"] == 1 for item in trend["daily"]) @pytest.mark.asyncio async def test_summary_insights_follow_current_window_reason_distribution(db_session, monkeypatch) -> None: fixed_now = datetime(2025, 1, 15, 12, 0, 0) monkeypatch.setattr(feedback_center_service_module, "now_cn_naive", lambda: fixed_now) old_records = [ FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="task_breakdown", source_id=801 + index, object_type="task", object_id=900 + index, field_name="task_breakdown", review_status="accepted", reason_code="owner_mismatch", created_at=datetime(2025, 1, 3, 9, 0, 0), reviewed_at=datetime(2025, 1, 3, 9, 20, 0), ) for index in range(5) ] current_record = FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="task_breakdown", source_id=900, object_type="task", object_id=1000, field_name="task_breakdown", review_status="accepted", reason_code="skill_gap", created_at=datetime(2025, 1, 15, 9, 0, 0), reviewed_at=datetime(2025, 1, 15, 9, 25, 0), ) db_session.add_all(old_records + [current_record]) await db_session.commit() summary = await FeedbackCenterService(db_session).summary( tenant_id=1, project_id=11, window_days=7, source_group="task", ) trend = summary["trend"] assert trend["current"]["top_reasons"][0]["code"] == "skill_gap" insights = summary["insights"] assert any( item["code"] == "reason_concentration" and item["target_reason_code"] == "skill_gap" for item in insights ) @pytest.mark.asyncio async def test_summary_trend_respects_source_group_scope(db_session, monkeypatch) -> None: fixed_now = datetime(2025, 1, 15, 12, 0, 0) monkeypatch.setattr(feedback_center_service_module, "now_cn_naive", lambda: fixed_now) records = [ FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="qa_feedback", source_id=701, object_type="qa_record", object_id=301, field_name="qa_feedback", review_status="accepted", created_at=datetime(2025, 1, 14, 9, 0, 0), reviewed_at=datetime(2025, 1, 14, 9, 20, 0), ), FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="signal_feedback", source_id=702, object_type="signal", object_id=401, field_name="strategic_classification", review_status="pending", created_at=datetime(2025, 1, 14, 10, 0, 0), ), FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="task_breakdown", source_id=703, object_type="task", object_id=501, field_name="task_breakdown", review_status="accepted", auto_assigned=True, created_at=datetime(2025, 1, 13, 8, 0, 0), reviewed_at=datetime(2025, 1, 13, 8, 25, 0), ), FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="task_override", source_id=704, object_type="task", object_id=501, field_name="priority", review_status="reviewed", created_at=datetime(2025, 1, 13, 9, 0, 0), reviewed_at=datetime(2025, 1, 13, 9, 30, 0), ), ] db_session.add_all(records) await db_session.commit() service = FeedbackCenterService(db_session) summary_qa = await service.summary(tenant_id=1, project_id=11, window_days=30, source_group="qa") assert summary_qa["total"] == 1 assert summary_qa["by_source_type"] == {"qa_feedback": 1} assert summary_qa["metrics"]["total_qa_feedback_count"] == 1 assert summary_qa["metrics"]["total_task_breakdown_count"] == 0 assert summary_qa["metrics"]["total_task_override_count"] == 0 assert summary_qa["metrics"]["total_signal_feedback_count"] == 0 trend = summary_qa["trend"] assert trend["window_days"] == 30 assert len(trend["daily"]) == 30 jan14 = next(item for item in trend["daily"] if item["date"] == "2025-01-14") assert jan14["total_records"] == 1 assert jan14["qa_feedback_count"] == 1 assert jan14["task_breakdown_count"] == 0 summary_task = await service.summary(tenant_id=1, project_id=11, window_days=7, source_group="task") assert summary_task["total"] == 2 assert summary_task["by_source_type"]["task_breakdown"] == 1 assert summary_task["by_source_type"]["task_override"] == 1 @pytest.mark.asyncio async def test_summary_supports_30_day_task_scope(db_session, monkeypatch) -> None: fixed_now = datetime(2025, 1, 15, 12, 0, 0) monkeypatch.setattr(feedback_center_service_module, "now_cn_naive", lambda: fixed_now) db_session.add_all( [ FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="task_breakdown", source_id=701, object_type="task", object_id=300, field_name="task_breakdown", review_status="accepted", auto_assigned=True, created_at=datetime(2025, 1, 15, 8, 0, 0), reviewed_at=datetime(2025, 1, 15, 8, 30, 0), ), FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="task_breakdown", source_id=702, object_type="task", object_id=301, field_name="task_breakdown", review_status="rejected", auto_assigned=False, created_at=datetime(2025, 1, 14, 8, 0, 0), reviewed_at=datetime(2025, 1, 14, 8, 40, 0), ), FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="task_override", source_id=703, object_type="task", object_id=300, field_name="priority", review_status="reviewed", created_at=datetime(2025, 1, 14, 9, 0, 0), reviewed_at=datetime(2025, 1, 14, 9, 50, 0), ), FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="qa_feedback", source_id=704, object_type="qa_record", object_id=401, field_name="qa_feedback", review_status="accepted", created_at=datetime(2025, 1, 13, 9, 0, 0), reviewed_at=datetime(2025, 1, 13, 9, 20, 0), ), FeedbackCenterRecord( tenant_id=1, project_id=11, source_type="signal_feedback", source_id=705, object_type="signal", object_id=501, field_name="strategic_classification", review_status="pending", created_at=datetime(2025, 1, 12, 9, 0, 0), ), ] ) await db_session.commit() summary = await FeedbackCenterService(db_session).summary( tenant_id=1, project_id=11, window_days=30, source_group="task", ) assert summary["source_group"] == "task" trend = summary["trend"] assert trend["window_days"] == 30 assert len(trend["daily"]) == 30 assert trend["current"]["total_records"] == 3 assert trend["current"]["task_breakdown_count"] == 2 assert trend["current"]["task_override_count"] == 1 assert trend["current"]["qa_feedback_count"] == 0 assert trend["current"]["signal_feedback_count"] == 0 assert trend["current"]["accepted_rate_reviewed"] == pytest.approx(0.5) assert trend["current"]["manual_override_rate"] == pytest.approx(0.5) assert trend["current"]["auto_assigned_rate"] == pytest.approx(0.5) assert trend["previous"]["total_records"] == 0 assert summary["by_source_type"] == {"task_breakdown": 2, "task_override": 1}