feat(agent,report): 会话持久化与画像注入、报告反馈与预约留资——PM/开发双Agent迭代1-3
CI / lint / typecheck / import-linter / tests / secrets (push) Canceled after 0s
CI / lint / typecheck / import-linter / tests / secrets (push) Canceled after 0s
- 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 验证 画像注入与历史携带
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
"""上下文组装(架构 §7.2 步骤③)的纯逻辑测试:画像注入与历史截尾。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from packages.agent.context import (
|
||||
PROFILE_SUMMARY_MAX_CHARS,
|
||||
build_context_messages,
|
||||
build_profile_message,
|
||||
)
|
||||
from packages.llm.types import Message
|
||||
|
||||
|
||||
class TestBuildContextMessages:
|
||||
def test_profile_first_then_history(self) -> None:
|
||||
history = [
|
||||
Message(role="user", content="第一句"),
|
||||
Message(role="assistant", content="第一答"),
|
||||
]
|
||||
messages = build_context_messages(
|
||||
profile_summary='{"dimensions":[]}', history=history, history_turns=10
|
||||
)
|
||||
assert len(messages) == 3
|
||||
assert messages[0].role == "system"
|
||||
assert '{"dimensions":[]}' in messages[0].content
|
||||
assert [m.role for m in messages[1:]] == ["user", "assistant"]
|
||||
assert [m.content for m in messages[1:]] == ["第一句", "第一答"]
|
||||
|
||||
def test_no_profile_degrades_to_history_only(self) -> None:
|
||||
"""无画像用户(FR-AGT-10 降级):不注入画像消息,历史照常携带。"""
|
||||
history = [Message(role="user", content="hi")]
|
||||
messages = build_context_messages(profile_summary=None, history=history, history_turns=10)
|
||||
assert len(messages) == 1
|
||||
assert messages[0].role == "user"
|
||||
|
||||
def test_empty_profile_string_treated_as_absent(self) -> None:
|
||||
messages = build_context_messages(profile_summary="", history=[], history_turns=10)
|
||||
assert messages == []
|
||||
|
||||
def test_history_tail_kept_by_turns(self) -> None:
|
||||
"""一轮 = user+assistant 两条;N=2 时只留最近 4 条,截头不截尾。"""
|
||||
history: list[Message] = []
|
||||
for i in range(5):
|
||||
history.append(Message(role="user", content=f"u{i}"))
|
||||
history.append(Message(role="assistant", content=f"a{i}"))
|
||||
messages = build_context_messages(profile_summary=None, history=history, history_turns=2)
|
||||
assert [m.content for m in messages] == ["u3", "a3", "u4", "a4"]
|
||||
|
||||
def test_zero_turns_drops_history(self) -> None:
|
||||
history = [Message(role="user", content="u0")]
|
||||
messages = build_context_messages(profile_summary=None, history=history, history_turns=0)
|
||||
assert messages == []
|
||||
|
||||
def test_history_message_content_capped(self) -> None:
|
||||
history = [Message(role="user", content="x" * 5000)]
|
||||
messages = build_context_messages(profile_summary=None, history=history, history_turns=10)
|
||||
assert len(messages[0].content) == 2000
|
||||
|
||||
|
||||
class TestBuildProfileMessage:
|
||||
def test_wraps_summary_as_system_message(self) -> None:
|
||||
message = build_profile_message("{}")
|
||||
assert message.role == "system"
|
||||
assert message.content.endswith("{}")
|
||||
|
||||
def test_budget_constant_within_token_budget(self) -> None:
|
||||
"""画像摘要预算 ≤ 500 token(README 预算表),按 ~1.6 字符/token 折算。"""
|
||||
assert PROFILE_SUMMARY_MAX_CHARS <= 500 * 1.6
|
||||
@@ -0,0 +1,331 @@
|
||||
"""AgentService 编排测试:repo 层打桩 + 假网关/假计费,不连库不发网。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.agent import repo
|
||||
from packages.agent.service import AgentService
|
||||
from packages.billing.service import Reservation
|
||||
from packages.llm.types import Completion, Scene, Usage
|
||||
from packages.shared.errors import NotFoundError
|
||||
from packages.shared.models import AgConversation
|
||||
|
||||
USER_ID = 7001
|
||||
CONVERSATION_ID = 9001
|
||||
|
||||
|
||||
class FakeSession:
|
||||
def __init__(self) -> None:
|
||||
self.commits = 0
|
||||
self.rollbacks = 0
|
||||
|
||||
async def commit(self) -> None:
|
||||
self.commits += 1
|
||||
|
||||
async def rollback(self) -> None:
|
||||
self.rollbacks += 1
|
||||
|
||||
|
||||
class FakeGateway:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[dict[str, Any]] = []
|
||||
|
||||
async def complete(self, **kwargs: Any) -> Completion:
|
||||
self.calls.append(kwargs)
|
||||
return Completion(
|
||||
text="可以试试白衬衫加深蓝直筒裤。",
|
||||
scene=Scene.AGENT_CHAT,
|
||||
provider="deepseek",
|
||||
model="deepseek-v4-flash",
|
||||
prompt_id="agent_chat",
|
||||
prompt_version="v1",
|
||||
usage=Usage(tokens_in=100, tokens_out=50, tokens_cached=0, tokens_reasoning=0),
|
||||
latency_ms=800,
|
||||
cost_cents=Decimal("0.05"),
|
||||
call_id=555,
|
||||
)
|
||||
|
||||
|
||||
class FailingGateway(FakeGateway):
|
||||
async def complete(self, **kwargs: Any) -> Completion:
|
||||
raise RuntimeError("provider exploded")
|
||||
|
||||
|
||||
class FakeBilling:
|
||||
def __init__(self) -> None:
|
||||
self.refunds: list[str] = []
|
||||
|
||||
async def reserve(self, **kwargs: Any) -> Reservation:
|
||||
return Reservation(
|
||||
tx_id=1,
|
||||
user_id=kwargs["user_id"],
|
||||
quota_key=kwargs["quota_key"],
|
||||
period_key="day:2026-09-11",
|
||||
amount=1,
|
||||
remaining=9,
|
||||
idempotency_key="k",
|
||||
)
|
||||
|
||||
async def commit(self, reservation: Reservation) -> None:
|
||||
pass
|
||||
|
||||
async def refund(self, reservation: Reservation, *, reason: str) -> None:
|
||||
self.refunds.append(reason)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def store(monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]:
|
||||
"""repo 层全打桩:会话与消息落进内存 dict,断言编排逻辑而非 SQL。"""
|
||||
state: dict[str, Any] = {
|
||||
"conversations": {
|
||||
CONVERSATION_ID: AgConversation(
|
||||
conversation_id=str(CONVERSATION_ID),
|
||||
user_id=USER_ID,
|
||||
title="",
|
||||
message_count=0,
|
||||
last_message_at=None,
|
||||
)
|
||||
},
|
||||
"messages": [],
|
||||
"history": [],
|
||||
"next_id": 100,
|
||||
}
|
||||
|
||||
def _next_id() -> int:
|
||||
state["next_id"] += 1
|
||||
return state["next_id"]
|
||||
|
||||
async def fake_create(session: Any, *, conversation_id: int, user_id: int) -> AgConversation:
|
||||
conv = AgConversation(
|
||||
conversation_id=conversation_id,
|
||||
user_id=user_id,
|
||||
title="",
|
||||
message_count=0,
|
||||
last_message_at=None,
|
||||
)
|
||||
state["conversations"][conversation_id] = conv
|
||||
return conv
|
||||
|
||||
async def fake_get(session: Any, conversation_id: int) -> AgConversation | None:
|
||||
return state["conversations"].get(conversation_id)
|
||||
|
||||
async def fake_recent(session: Any, *, conversation_id: int, limit: int) -> list[Any]:
|
||||
return state["history"]
|
||||
|
||||
async def fake_append(
|
||||
session: Any,
|
||||
*,
|
||||
message_id: int,
|
||||
conversation: AgConversation,
|
||||
role: str,
|
||||
content: str,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
state["messages"].append({"role": role, "content": content, **kwargs})
|
||||
conversation.message_count += 1
|
||||
if not conversation.title and role == "user":
|
||||
conversation.title = content[:20]
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(repo, "create_conversation", fake_create)
|
||||
monkeypatch.setattr(repo, "get_conversation", fake_get)
|
||||
monkeypatch.setattr(repo, "list_recent_messages", fake_recent)
|
||||
monkeypatch.setattr(repo, "append_message", fake_append)
|
||||
monkeypatch.setattr("packages.agent.service.next_id", _next_id)
|
||||
return state
|
||||
|
||||
|
||||
def _service(store: dict[str, Any], **overrides: Any) -> AgentService:
|
||||
kwargs: dict[str, Any] = {
|
||||
"gateway": FakeGateway(),
|
||||
"billing": FakeBilling(),
|
||||
"profile_provider": None,
|
||||
"history_turns": 10,
|
||||
}
|
||||
kwargs.update(overrides)
|
||||
return AgentService(FakeSession(), **kwargs) # type: ignore[arg-type]
|
||||
|
||||
|
||||
class TestChat:
|
||||
async def test_implicit_conversation_created_and_messages_persisted(
|
||||
self, store: dict[str, Any]
|
||||
) -> None:
|
||||
service = _service(store)
|
||||
result = await service.chat(user_id=USER_ID, message="面试怎么穿", request_key="k1")
|
||||
# 隐式建会话,响应带回会话 ID
|
||||
assert result.conversation_id in store["conversations"]
|
||||
assert result.conversation_id != CONVERSATION_ID
|
||||
# 用户消息 + assistant 消息双行落库,assistant 行带复盘字段
|
||||
assert [m["role"] for m in store["messages"]] == ["user", "assistant"]
|
||||
assistant = store["messages"][1]
|
||||
assert assistant["call_id"] == 555
|
||||
assert assistant["model"] == "deepseek-v4-flash"
|
||||
assert assistant["cost_cents"] == Decimal("0.05")
|
||||
# 会话标题取首条用户消息
|
||||
conv = store["conversations"][result.conversation_id]
|
||||
assert conv.title == "面试怎么穿"
|
||||
assert result.quota_remaining == 9
|
||||
|
||||
async def test_existing_conversation_carries_history(self, store: dict[str, Any]) -> None:
|
||||
from packages.llm.types import Message
|
||||
|
||||
conv = store["conversations"][CONVERSATION_ID]
|
||||
conv.message_count = 2
|
||||
store["history"] = [
|
||||
_msg("user", "之前的问题"),
|
||||
_msg("assistant", "之前的回答"),
|
||||
]
|
||||
service = _service(store)
|
||||
await service.chat(
|
||||
user_id=USER_ID,
|
||||
message="那约会呢",
|
||||
request_key="k2",
|
||||
conversation_id=str(CONVERSATION_ID),
|
||||
)
|
||||
sent = service._gateway.calls[0]["messages"] # type: ignore[attr-defined]
|
||||
assert [m.content for m in sent] == ["之前的问题", "之前的回答"]
|
||||
assert isinstance(sent[0], Message)
|
||||
|
||||
async def test_profile_injected_as_leading_system_message(self, store: dict[str, Any]) -> None:
|
||||
async def profile_provider(user_id: int) -> str | None:
|
||||
assert user_id == USER_ID
|
||||
return '{"dimensions":[{"key":"body_shape","value":"梨形"}]}'
|
||||
|
||||
service = _service(store, profile_provider=profile_provider)
|
||||
await service.chat(
|
||||
user_id=USER_ID,
|
||||
message="推荐裤子",
|
||||
request_key="k3",
|
||||
conversation_id=str(CONVERSATION_ID),
|
||||
)
|
||||
sent = service._gateway.calls[0]["messages"] # type: ignore[attr-defined]
|
||||
assert sent[0].role == "system"
|
||||
assert "梨形" in sent[0].content
|
||||
|
||||
async def test_profile_failure_degrades_silently(self, store: dict[str, Any]) -> None:
|
||||
async def broken_provider(user_id: int) -> str | None:
|
||||
raise RuntimeError("report table down")
|
||||
|
||||
service = _service(store, profile_provider=broken_provider)
|
||||
result = await service.chat(
|
||||
user_id=USER_ID,
|
||||
message="hi",
|
||||
request_key="k4",
|
||||
conversation_id=str(CONVERSATION_ID),
|
||||
)
|
||||
assert result.reply # 画像失败不阻断对话
|
||||
sent = service._gateway.calls[0]["messages"] # type: ignore[attr-defined]
|
||||
assert all(m.role != "system" for m in sent)
|
||||
|
||||
async def test_other_users_conversation_not_found(self, store: dict[str, Any]) -> None:
|
||||
"""他人会话与不存在同口径 404,不暴露存在性。"""
|
||||
service = _service(store)
|
||||
with pytest.raises(NotFoundError):
|
||||
await service.chat(
|
||||
user_id=9999,
|
||||
message="hi",
|
||||
request_key="k5",
|
||||
conversation_id=str(CONVERSATION_ID),
|
||||
)
|
||||
|
||||
async def test_llm_failure_refunds_and_persists_nothing(self, store: dict[str, Any]) -> None:
|
||||
billing = FakeBilling()
|
||||
service = _service(store, gateway=FailingGateway(), billing=billing)
|
||||
with pytest.raises(RuntimeError):
|
||||
await service.chat(
|
||||
user_id=USER_ID,
|
||||
message="hi",
|
||||
request_key="k6",
|
||||
conversation_id=str(CONVERSATION_ID),
|
||||
)
|
||||
assert billing.refunds == ["RuntimeError"]
|
||||
assert store["messages"] == []
|
||||
|
||||
async def test_large_snowflake_id_as_string(self, store: dict[str, Any]) -> None:
|
||||
"""超 2^53 的雪花 ID 以字符串传入,无精度丢失地落到归属校验。"""
|
||||
big = 223843017600012288 # > 2**53,JS Number 无法精确表示
|
||||
store["conversations"][big] = AgConversation(
|
||||
conversation_id=big,
|
||||
user_id=USER_ID,
|
||||
title="",
|
||||
message_count=0,
|
||||
last_message_at=None,
|
||||
)
|
||||
service = _service(store)
|
||||
result = await service.chat(
|
||||
user_id=USER_ID,
|
||||
message="hi",
|
||||
request_key="k7",
|
||||
conversation_id=str(big),
|
||||
)
|
||||
assert result.conversation_id == big
|
||||
|
||||
async def test_invalid_conversation_id_same_404(self, store: dict[str, Any]) -> None:
|
||||
"""非法字符串与「不存在」同口径 40400,不给遍历者额外信号。"""
|
||||
service = _service(store)
|
||||
with pytest.raises(NotFoundError):
|
||||
await service.chat(
|
||||
user_id=USER_ID,
|
||||
message="hi",
|
||||
request_key="k8",
|
||||
conversation_id="not-a-number",
|
||||
)
|
||||
|
||||
async def test_context_build_failure_refunds(
|
||||
self, store: dict[str, Any], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""预扣 commit 之后,上下文组装(历史查询)抛异常也必须退还额度。"""
|
||||
|
||||
async def broken_recent(session: Any, *, conversation_id: int, limit: int) -> list[Any]:
|
||||
raise RuntimeError("db hiccup")
|
||||
|
||||
monkeypatch.setattr(repo, "list_recent_messages", broken_recent)
|
||||
conv = store["conversations"][CONVERSATION_ID]
|
||||
conv.message_count = 2 # 触发历史查询路径
|
||||
billing = FakeBilling()
|
||||
service = _service(store, billing=billing)
|
||||
with pytest.raises(RuntimeError):
|
||||
await service.chat(
|
||||
user_id=USER_ID,
|
||||
message="hi",
|
||||
request_key="k9",
|
||||
conversation_id=str(CONVERSATION_ID),
|
||||
)
|
||||
assert billing.refunds == ["RuntimeError"]
|
||||
assert store["messages"] == []
|
||||
|
||||
|
||||
class TestConversationApis:
|
||||
async def test_create_and_get_owned(self, store: dict[str, Any]) -> None:
|
||||
service = _service(store)
|
||||
conversation = await service.create_conversation(user_id=USER_ID)
|
||||
assert conversation.user_id == USER_ID
|
||||
owned = await service.get_owned_conversation(
|
||||
user_id=USER_ID, conversation_id=conversation.conversation_id
|
||||
)
|
||||
assert owned.conversation_id == conversation.conversation_id
|
||||
with pytest.raises(NotFoundError):
|
||||
await service.get_owned_conversation(user_id=USER_ID, conversation_id=404)
|
||||
|
||||
async def test_list_conversations_scoped_to_user(
|
||||
self, store: dict[str, Any], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
async def fake_list(session: Any, *, user_id: int, limit: int = 20) -> list[Any]:
|
||||
return [c for c in store["conversations"].values() if c.user_id == user_id]
|
||||
|
||||
monkeypatch.setattr(repo, "list_conversations", fake_list)
|
||||
service = _service(store)
|
||||
mine = await service.list_conversations(user_id=USER_ID)
|
||||
other = await service.list_conversations(user_id=9999)
|
||||
assert len(mine) == 1
|
||||
assert other == []
|
||||
|
||||
|
||||
def _msg(role: str, content: str) -> Any:
|
||||
from packages.llm.types import Message
|
||||
|
||||
return Message(role=role, content=content)
|
||||
@@ -0,0 +1,69 @@
|
||||
"""报告结论反馈的纯函数与 schema 校验(T-P3-21 最小可用版,不连库)。
|
||||
|
||||
覆盖:ok_dimension_keys 的可反馈维度集合口径(status=ok 才可选)、
|
||||
FeedbackCreateRequest 的 verdict 枚举与 correction 长度上限。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from packages.report.feedback_service import ok_dimension_keys
|
||||
from packages.report.schemas import FeedbackCreateRequest
|
||||
|
||||
|
||||
def _result(dimensions: list[dict[str, object]]) -> dict[str, object]:
|
||||
return {"diagnosis": {"dimensions": dimensions, "coverage": {"total": len(dimensions)}}}
|
||||
|
||||
|
||||
class TestOkDimensionKeys:
|
||||
def test_only_status_ok_dimensions_are_feedbackable(self) -> None:
|
||||
result = _result(
|
||||
[
|
||||
{"key": "body_shape", "status": "ok"},
|
||||
{"key": "color_season", "status": "skipped"},
|
||||
{"key": "style_type", "status": "ok"},
|
||||
]
|
||||
)
|
||||
assert ok_dimension_keys(result) == {"body_shape", "style_type"}
|
||||
|
||||
def test_empty_or_missing_diagnosis_yields_empty(self) -> None:
|
||||
assert ok_dimension_keys({}) == set()
|
||||
assert ok_dimension_keys({"diagnosis": {}}) == set()
|
||||
assert ok_dimension_keys({"diagnosis": {"dimensions": "not-a-list"}}) == set()
|
||||
|
||||
def test_malformed_items_are_ignored(self) -> None:
|
||||
result = _result(
|
||||
[
|
||||
"garbage",
|
||||
{"status": "ok"}, # 缺 key
|
||||
{"key": 123, "status": "ok"}, # key 非字符串
|
||||
{"key": "face_shape", "status": "ok"},
|
||||
]
|
||||
)
|
||||
assert ok_dimension_keys(result) == {"face_shape"}
|
||||
|
||||
|
||||
class TestFeedbackCreateRequest:
|
||||
def test_verdict_enum_rejects_unknown_value(self) -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
FeedbackCreateRequest.model_validate(
|
||||
{"dimension_key": "body_shape", "verdict": "kind-of"}
|
||||
)
|
||||
|
||||
def test_correction_max_200_chars(self) -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
FeedbackCreateRequest.model_validate(
|
||||
{
|
||||
"dimension_key": "body_shape",
|
||||
"verdict": "inaccurate",
|
||||
"correction": "长" * 201,
|
||||
}
|
||||
)
|
||||
|
||||
def test_minimal_valid_payload(self) -> None:
|
||||
req = FeedbackCreateRequest.model_validate(
|
||||
{"dimension_key": "body_shape", "verdict": "accurate"}
|
||||
)
|
||||
assert req.correction is None
|
||||
@@ -0,0 +1,83 @@
|
||||
"""report.profile.latest_profile_summary:repo 打桩,验证摘要选取纪律。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.report import repo
|
||||
from packages.report.profile import latest_profile_summary
|
||||
|
||||
_DIAGNOSIS = {
|
||||
"dimensions": [
|
||||
{
|
||||
"key": "body_shape",
|
||||
"label": "体型",
|
||||
"status": "ok",
|
||||
"value": "梨形",
|
||||
"confidence": 0.9,
|
||||
"evidence": ["问卷自报肩窄胯宽"],
|
||||
},
|
||||
{"key": "style_impression", "status": "skipped", "value": None},
|
||||
],
|
||||
"rule_hits": [
|
||||
{"rule_key": "pear_lower_dark", "dimension": "body_shape", "suggestion": "下装选深色"}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _report(report_id: int, *, status: str, result: dict[str, Any]) -> Any:
|
||||
return SimpleNamespace(report_id=report_id, status=status, result=result)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stub_list(monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]:
|
||||
state: dict[str, Any] = {"reports": []}
|
||||
|
||||
async def fake_list(session: Any, *, user_id: int, limit: int = 20) -> list[Any]:
|
||||
state["limit"] = limit
|
||||
return state["reports"]
|
||||
|
||||
monkeypatch.setattr(repo, "list_reports", fake_list)
|
||||
return state
|
||||
|
||||
|
||||
class TestLatestProfileSummary:
|
||||
async def test_no_reports_returns_none(self, stub_list: dict[str, Any]) -> None:
|
||||
assert await latest_profile_summary(None, user_id=1, max_chars=800) is None # type: ignore[arg-type]
|
||||
|
||||
async def test_succeeded_report_with_diagnosis(self, stub_list: dict[str, Any]) -> None:
|
||||
stub_list["reports"] = [_report(1, status="succeeded", result={"diagnosis": _DIAGNOSIS})]
|
||||
summary = await latest_profile_summary(None, user_id=1, max_chars=800) # type: ignore[arg-type]
|
||||
assert summary is not None
|
||||
payload = json.loads(summary)
|
||||
values = {d["key"]: d["value"] for d in payload["dimensions"]}
|
||||
assert values["body_shape"] == "梨形"
|
||||
# skipped 维度不进素材(防幻觉第一道闸)
|
||||
assert "style_impression" not in values
|
||||
assert payload["rule_hits"][0]["rule_key"] == "pear_lower_dark"
|
||||
|
||||
async def test_latest_failed_falls_back_to_earlier_succeeded(
|
||||
self, stub_list: dict[str, Any]
|
||||
) -> None:
|
||||
stub_list["reports"] = [
|
||||
_report(2, status="failed", result={}),
|
||||
_report(1, status="succeeded", result={"diagnosis": _DIAGNOSIS}),
|
||||
]
|
||||
summary = await latest_profile_summary(None, user_id=1, max_chars=800) # type: ignore[arg-type]
|
||||
assert summary is not None and "梨形" in summary
|
||||
|
||||
async def test_succeeded_without_diagnosis_is_skipped(self, stub_list: dict[str, Any]) -> None:
|
||||
stub_list["reports"] = [_report(1, status="succeeded", result={"vision": {}})]
|
||||
assert await latest_profile_summary(None, user_id=1, max_chars=800) is None # type: ignore[arg-type]
|
||||
|
||||
async def test_budget_is_forwarded(self, stub_list: dict[str, Any]) -> None:
|
||||
"""max_chars 收紧时低优先级维度被裁、硬结论保留(复用 copy 的裁剪纪律)。"""
|
||||
stub_list["reports"] = [_report(1, status="succeeded", result={"diagnosis": _DIAGNOSIS})]
|
||||
summary = await latest_profile_summary(None, user_id=1, max_chars=60) # type: ignore[arg-type]
|
||||
assert summary is not None
|
||||
assert len(summary) > 60 # 硬结论永不裁,载荷可超预算(copy.py 既定语义)
|
||||
assert "梨形" in summary
|
||||
Reference in New Issue
Block a user