Files
meida_backend/tests/integration/test_report_feedback_reservations.py
xsl 7bcbb8d40b
CI / lint / typecheck / import-linter / tests / secrets (push) Canceled after 0s
feat(agent,report): 会话持久化与画像注入、报告反馈与预约留资——PM/开发双Agent迭代1-3
- agent: ag_conversation/ag_message 表(迁移0007),上下文组装注入最近报告画像摘要
  (report.profile.latest_profile_summary 经 apps 层闭包注入,同层不互调),历史带最近
  AGENT_HISTORY_TURNS=10 轮;POST/GET /agent/conversations 与消息历史接口
- agent 修复:ChatRequest.conversation_id 改 str(雪花ID超2^53,JS Number丢精度);
  _build_context 挪进 reserve commit 后同一 try,异常走 refund 兜底
- report: rp_feedback(维度反馈 upsert,认同率/修正率 SQL 可算,T-P3-21 最小版——
  L1 记忆未建,不回写结论)+ rp_reservation(预约留资幂等,T-P3-22,预约率口径
  行数/full_report.card_view 去重曝光,门槛≥8%),迁移0008
- 测试:全量 606 passed;ruff/mypy/lint-imports 全绿;E2E 真实 deepseek-v4-flash 验证
  画像注入与历史携带
2026-09-11 21:50:58 +08:00

212 lines
6.9 KiB
Python
Raw Permalink 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.
"""结论反馈 + 预约留资的端到端断言(T-P3-21 / T-P3-22,迭代 3)。
覆盖验收点:反馈 upsert 幂等(同维度覆盖更新不堆行)、归属校验他人报告
40420、dimension_key 校验(不存在/skipped → 40001)、预约重复 created=false。
报告行直接 INSERT(不走流水线)——反馈/预约断言的是路由与服务口径,
不是生成链路(生成链路由 test_report_flow 覆盖)。
"""
from __future__ import annotations
import os
import uuid
from collections.abc import AsyncIterator
from typing import Any
import pytest
def _infra_available() -> bool:
return bool(os.getenv("MEIDA_RUN_INTEGRATION"))
pytestmark = [
pytest.mark.integration,
pytest.mark.skipif(
not _infra_available(),
reason="set MEIDA_RUN_INTEGRATION=1 with PostgreSQL and Redis running",
),
]
_OK_DIM = "body_shape"
_SKIPPED_DIM = "color_season"
@pytest.fixture
async def client() -> AsyncIterator[Any]:
import httpx
from apps.api.main import create_app
from packages.shared.config import get_settings
app = create_app(get_settings())
transport = httpx.ASGITransport(app=app)
async with (
httpx.AsyncClient(transport=transport, base_url="http://test") as http,
app.router.lifespan_context(app),
):
yield http
async def _login(client: Any) -> tuple[dict[str, str], int]:
resp = await client.post(
"/api/v1/auth/dev-token", json={"device_key": f"fb-it-{uuid.uuid4().hex[:8]}"}
)
assert resp.json()["code"] == 0
data = resp.json()["data"]
return {"Authorization": f"Bearer {data['access_token']}"}, int(data["user_id"])
async def _insert_report(user_id: int) -> int:
"""直接落一份 succeeded 报告:两个维度,一个 ok 一个 skipped。"""
from packages.domain.ids import next_id
from packages.shared.db import session_scope
from packages.shared.models import RpReport
report_id = next_id()
async with session_scope() as session:
session.add(
RpReport(
report_id=report_id,
user_id=user_id,
tier="basic",
status="succeeded",
version=1,
result={
"diagnosis": {
"dimensions": [
{"number": 5, "key": _OK_DIM, "status": "ok", "value": "X"},
{"number": 19, "key": _SKIPPED_DIM, "status": "skipped", "value": None},
],
"coverage": {"basic_full": False, "total": 2},
}
},
)
)
return report_id
async def _count_feedback(report_id: int) -> int:
from sqlalchemy import func, select
from packages.shared.db import session_scope
from packages.shared.models import RpFeedback
async with session_scope() as session:
result = await session.execute(
select(func.count()).select_from(RpFeedback).where(RpFeedback.report_id == report_id)
)
return int(result.scalar_one())
# --- 反馈:upsert 幂等 --------------------------------------------------------
async def test_feedback_upsert_overwrites_same_dimension(client: Any) -> None:
headers, user_id = await _login(client)
report_id = await _insert_report(user_id)
first = await client.post(
f"/api/v1/reports/{report_id}/feedback",
headers=headers,
json={"dimension_key": _OK_DIM, "verdict": "accurate"},
)
assert first.status_code == 200
assert first.json()["data"]["updated"] is False
# 改判:同维度再反馈 = 覆盖更新(updated=true),不是堆行
second = await client.post(
f"/api/v1/reports/{report_id}/feedback",
headers=headers,
json={"dimension_key": _OK_DIM, "verdict": "inaccurate", "correction": "其实我是梨形"},
)
assert second.status_code == 200
assert second.json()["data"]["updated"] is True
assert await _count_feedback(report_id) == 1
listing = (await client.get(f"/api/v1/reports/{report_id}/feedback", headers=headers)).json()[
"data"
]
assert len(listing) == 1
assert listing[0]["dimension_key"] == _OK_DIM
assert listing[0]["verdict"] == "inaccurate"
assert listing[0]["correction"] == "其实我是梨形"
# --- 反馈:归属与维度校验 ------------------------------------------------------
async def test_feedback_on_others_report_returns_40420(client: Any) -> None:
headers, _ = await _login(client)
_, other_user_id = await _login(client)
report_id = await _insert_report(other_user_id)
resp = await client.post(
f"/api/v1/reports/{report_id}/feedback",
headers=headers,
json={"dimension_key": _OK_DIM, "verdict": "accurate"},
)
assert resp.json()["code"] == 40420
get_resp = await client.get(f"/api/v1/reports/{report_id}/feedback", headers=headers)
assert get_resp.json()["code"] == 40420
async def test_feedback_dimension_must_exist_and_be_ok(client: Any) -> None:
headers, user_id = await _login(client)
report_id = await _insert_report(user_id)
# 不存在的维度
resp = await client.post(
f"/api/v1/reports/{report_id}/feedback",
headers=headers,
json={"dimension_key": "no_such_dim", "verdict": "accurate"},
)
assert resp.json()["code"] == 40001
# skipped 维度没有结论,不可反馈
resp2 = await client.post(
f"/api/v1/reports/{report_id}/feedback",
headers=headers,
json={"dimension_key": _SKIPPED_DIM, "verdict": "accurate"},
)
assert resp2.json()["code"] == 40001
# verdict 枚举外 → 40001(pydantic 422 → 统一信封 VALIDATION_FAILED)
resp3 = await client.post(
f"/api/v1/reports/{report_id}/feedback",
headers=headers,
json={"dimension_key": _OK_DIM, "verdict": "kind-of"},
)
assert resp3.json()["code"] == 40001
assert await _count_feedback(report_id) == 0
# --- 预约:幂等 ---------------------------------------------------------------
async def test_reservation_duplicate_returns_created_false(client: Any) -> None:
headers, _ = await _login(client)
first = await client.post(
"/api/v1/reservations", headers=headers, json={"topic": "full_report"}
)
assert first.status_code == 200
assert first.json()["data"]["created"] is True
again = await client.post(
"/api/v1/reservations", headers=headers, json={"topic": "full_report"}
)
assert again.status_code == 200
assert again.json()["data"]["created"] is False
listing = (await client.get("/api/v1/reservations?topic=full_report", headers=headers)).json()[
"data"
]
assert listing["topics"] == ["full_report"]
# topic 参数缺省返回全部
all_topics = (await client.get("/api/v1/reservations", headers=headers)).json()["data"]
assert "full_report" in all_topics["topics"]