mirror of
https://github.com/langgenius/dify.git
synced 2026-11-10 21:35:45 +08:00
375 lines
14 KiB
Python
375 lines
14 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
from inspect import unwrap
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
from flask import Flask
|
|
from sqlalchemy import event
|
|
from sqlalchemy.orm import Session
|
|
|
|
from controllers.console.app import message as message_module
|
|
from core.app.entities.app_invoke_entities import InvokeFrom
|
|
from models.account import Account
|
|
from models.enums import ConversationFromSource, CreatorUserRole, FeedbackFromSource, FeedbackRating
|
|
from models.model import (
|
|
App,
|
|
AppAnnotationHitHistory,
|
|
AppMode,
|
|
Conversation,
|
|
Message,
|
|
MessageAgentThought,
|
|
MessageAnnotation,
|
|
MessageFeedback,
|
|
)
|
|
|
|
|
|
def _account() -> Account:
|
|
account = Account(name="Account", email="account@example.com")
|
|
account.id = "account-1"
|
|
return account
|
|
|
|
|
|
def _app(*, app_id: str = "app-1") -> App:
|
|
return App(
|
|
id=app_id,
|
|
tenant_id="tenant-1",
|
|
name="Chat App",
|
|
description="",
|
|
mode=AppMode.CHAT,
|
|
enable_site=True,
|
|
enable_api=True,
|
|
max_active_requests=0,
|
|
)
|
|
|
|
|
|
def _persist_message(session: Session, *, message_id: str, app_id: str = "app-1") -> Message:
|
|
conversation = Conversation(
|
|
app_id=app_id,
|
|
app_model_config_id=None,
|
|
model_provider=None,
|
|
override_model_configs=None,
|
|
model_id=None,
|
|
mode=AppMode.CHAT,
|
|
name="Conversation",
|
|
_inputs={},
|
|
introduction="",
|
|
system_instruction="",
|
|
system_instruction_tokens=0,
|
|
status="normal",
|
|
invoke_from=InvokeFrom.DEBUGGER,
|
|
from_source=ConversationFromSource.CONSOLE,
|
|
from_end_user_id=None,
|
|
from_account_id="account-1",
|
|
)
|
|
conversation.id = "conversation-1"
|
|
message = Message(
|
|
app_id=app_id,
|
|
conversation_id=conversation.id,
|
|
_inputs={},
|
|
query="query",
|
|
message="",
|
|
message_tokens=0,
|
|
message_unit_price=0,
|
|
message_price_unit=0,
|
|
answer="answer",
|
|
answer_tokens=0,
|
|
answer_unit_price=0,
|
|
answer_price_unit=0,
|
|
provider_response_latency=0,
|
|
total_price=0,
|
|
currency="USD",
|
|
invoke_from=InvokeFrom.DEBUGGER,
|
|
from_source=ConversationFromSource.CONSOLE,
|
|
from_end_user_id=None,
|
|
from_account_id="account-1",
|
|
app_mode=AppMode.CHAT,
|
|
)
|
|
message.id = message_id
|
|
session.add_all([_account(), _app(app_id=app_id), conversation, message])
|
|
session.flush()
|
|
return message
|
|
|
|
|
|
class TestMessageApiCases:
|
|
@staticmethod
|
|
def test_app_message_routes_pass_injected_session(
|
|
app: Flask, monkeypatch: pytest.MonkeyPatch, unbound_session: Session
|
|
) -> None:
|
|
session = unbound_session
|
|
current_user = _account()
|
|
app_model = _app()
|
|
message_id = "550e8400-e29b-41d4-a716-446655440000"
|
|
list_messages = MagicMock(return_value={"data": []})
|
|
update_feedback = MagicMock(return_value={"result": "success"})
|
|
get_message_detail = MagicMock(return_value={"id": message_id})
|
|
monkeypatch.setattr(message_module, "_list_chat_messages", list_messages)
|
|
monkeypatch.setattr(message_module, "_update_message_feedback", update_feedback)
|
|
monkeypatch.setattr(message_module, "_get_message_detail", get_message_detail)
|
|
|
|
assert unwrap(message_module.ChatMessageListApi.get)(
|
|
message_module.ChatMessageListApi(),
|
|
message_module.ChatMessagesQuery(conversation_id="550e8400-e29b-41d4-a716-446655440001"),
|
|
session,
|
|
current_user,
|
|
app_model,
|
|
) == {"data": []}
|
|
assert unwrap(message_module.MessageFeedbackApi.post)(
|
|
message_module.MessageFeedbackApi(),
|
|
message_module.AppMessageFeedbackPayload(message_id=message_id, rating="like"),
|
|
session,
|
|
current_user,
|
|
app_model,
|
|
) == {"result": "success"}
|
|
assert unwrap(message_module.MessageApi.get)(message_module.MessageApi(), session, app_model, message_id) == {
|
|
"id": message_id
|
|
}
|
|
|
|
assert list_messages.call_args.kwargs["session"] is session
|
|
assert update_feedback.call_args.kwargs["session"] is session
|
|
assert get_message_detail.call_args.kwargs["session"] is session
|
|
|
|
@staticmethod
|
|
def test_update_message_feedback_commits_injected_session(app: Flask, sqlite_session: Session) -> None:
|
|
message_id = "550e8400-e29b-41d4-a716-446655440000"
|
|
message = _persist_message(sqlite_session, message_id=message_id)
|
|
feedback = MessageFeedback(
|
|
app_id=message.app_id,
|
|
conversation_id=message.conversation_id,
|
|
message_id=message.id,
|
|
rating=FeedbackRating.DISLIKE,
|
|
from_source=FeedbackFromSource.ADMIN,
|
|
from_account_id="account-1",
|
|
)
|
|
sqlite_session.add(feedback)
|
|
sqlite_session.commit()
|
|
session = sqlite_session
|
|
commits: list[str] = []
|
|
event.listen(session, "after_commit", lambda _session: commits.append("commit"))
|
|
|
|
with app.test_request_context(json={"message_id": message_id, "rating": "like", "content": "helpful"}):
|
|
result = message_module._update_message_feedback(
|
|
args=message_module.AppMessageFeedbackPayload(
|
|
message_id=message_id,
|
|
rating="like",
|
|
content="helpful",
|
|
),
|
|
session=session,
|
|
current_user=_account(),
|
|
app_model=_app(),
|
|
)
|
|
|
|
assert result == {"result": "success"}
|
|
updated_feedback = sqlite_session.get(MessageFeedback, feedback.id)
|
|
assert updated_feedback is not None
|
|
assert updated_feedback.rating == FeedbackRating.LIKE
|
|
assert updated_feedback.content == "helpful"
|
|
assert commits == ["commit"]
|
|
|
|
@staticmethod
|
|
def test_get_message_detail_uses_injected_session(monkeypatch: pytest.MonkeyPatch, sqlite_session: Session) -> None:
|
|
message_id = "550e8400-e29b-41d4-a716-446655440000"
|
|
message = _persist_message(sqlite_session, message_id=message_id)
|
|
response_source = object()
|
|
response_source_factory = MagicMock(return_value=response_source)
|
|
session = sqlite_session
|
|
monkeypatch.setattr(message_module, "attach_message_extra_contents", MagicMock())
|
|
monkeypatch.setattr(message_module, "MessageResponseSource", response_source_factory)
|
|
monkeypatch.setattr(message_module, "dump_response", lambda _model, value: value)
|
|
|
|
result = message_module._get_message_detail(
|
|
session=session,
|
|
app_model=_app(),
|
|
message_id=message_id,
|
|
)
|
|
|
|
assert result is response_source
|
|
response_source_factory.assert_called_once_with(message, session=session)
|
|
|
|
@staticmethod
|
|
def test_message_response_source_uses_caller_session_for_nested_fields(sqlite_session: Session) -> None:
|
|
session = sqlite_session
|
|
message_id = "550e8400-e29b-41d4-a716-446655440000"
|
|
message = _persist_message(sqlite_session, message_id=message_id)
|
|
message.inputs = {"topic": "support"}
|
|
feedback = MessageFeedback(
|
|
app_id=message.app_id,
|
|
conversation_id=message.conversation_id,
|
|
message_id=message.id,
|
|
rating=FeedbackRating.LIKE,
|
|
from_source=FeedbackFromSource.USER,
|
|
from_account_id="account-1",
|
|
)
|
|
annotation = MessageAnnotation(
|
|
app_id=message.app_id,
|
|
question="Question",
|
|
content="Answer",
|
|
account_id="account-1",
|
|
conversation_id=message.conversation_id,
|
|
message_id=message.id,
|
|
)
|
|
annotation.id = "annotation-1"
|
|
annotation_hit = AppAnnotationHitHistory(
|
|
app_id=message.app_id,
|
|
annotation_id=annotation.id,
|
|
source="annotation",
|
|
question="Question",
|
|
account_id="account-1",
|
|
score=0.0,
|
|
message_id=message.id,
|
|
annotation_question="Question",
|
|
annotation_content="Answer",
|
|
)
|
|
thought = MessageAgentThought(
|
|
message_id=message.id,
|
|
position=1,
|
|
created_by_role=CreatorUserRole.ACCOUNT,
|
|
created_by="account-1",
|
|
thought="Consider the request",
|
|
tool_labels_str="{}",
|
|
tool_meta_str="{}",
|
|
)
|
|
sqlite_session.add_all([feedback, annotation, annotation_hit, thought])
|
|
sqlite_session.commit()
|
|
|
|
source = message_module.MessageResponseSource(message, session=session)
|
|
|
|
assert source.inputs == {"topic": "support"}
|
|
assert source.user_feedback is feedback
|
|
assert source.feedbacks[0].from_account.id == "account-1"
|
|
annotation_source = source.annotation
|
|
assert annotation_source is not None
|
|
assert annotation_source.account.id == "account-1"
|
|
annotation_hit_history_source = source.annotation_hit_history
|
|
assert annotation_hit_history_source is not None
|
|
assert annotation_hit_history_source.annotation_create_account.id == "account-1"
|
|
assert source.agent_thoughts == [thought]
|
|
assert source.message_files == []
|
|
|
|
@staticmethod
|
|
def test_chat_messages_query_valid(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Test valid ChatMessagesQuery with all fields."""
|
|
query = message_module.ChatMessagesQuery(
|
|
conversation_id="550e8400-e29b-41d4-a716-446655440000",
|
|
first_id="550e8400-e29b-41d4-a716-446655440001",
|
|
limit=50,
|
|
)
|
|
assert query.limit == 50
|
|
|
|
@staticmethod
|
|
def test_chat_messages_query_defaults(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Test ChatMessagesQuery with defaults."""
|
|
query = message_module.ChatMessagesQuery(conversation_id="550e8400-e29b-41d4-a716-446655440000")
|
|
assert query.first_id is None
|
|
assert query.limit == 20
|
|
|
|
@staticmethod
|
|
def test_chat_messages_query_empty_first_id(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Test ChatMessagesQuery converts empty first_id to None."""
|
|
query = message_module.ChatMessagesQuery(
|
|
conversation_id="550e8400-e29b-41d4-a716-446655440000",
|
|
first_id="",
|
|
)
|
|
assert query.first_id is None
|
|
|
|
@staticmethod
|
|
def test_message_feedback_payload_valid_like(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Test AppMessageFeedbackPayload with like rating."""
|
|
payload = message_module.AppMessageFeedbackPayload(
|
|
message_id="550e8400-e29b-41d4-a716-446655440000",
|
|
rating="like",
|
|
content="Good answer",
|
|
)
|
|
assert payload.rating == "like"
|
|
assert payload.content == "Good answer"
|
|
|
|
@staticmethod
|
|
def test_message_feedback_payload_valid_dislike(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Test AppMessageFeedbackPayload with dislike rating."""
|
|
payload = message_module.AppMessageFeedbackPayload(
|
|
message_id="550e8400-e29b-41d4-a716-446655440000",
|
|
rating="dislike",
|
|
)
|
|
assert payload.rating == "dislike"
|
|
|
|
@staticmethod
|
|
def test_message_feedback_payload_no_rating(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Test AppMessageFeedbackPayload without rating."""
|
|
payload = message_module.AppMessageFeedbackPayload(message_id="550e8400-e29b-41d4-a716-446655440000")
|
|
assert payload.rating is None
|
|
|
|
@staticmethod
|
|
def test_annotation_count_response(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Test AnnotationCountResponse creation."""
|
|
response = message_module.AnnotationCountResponse(count=10)
|
|
assert response.count == 10
|
|
|
|
@staticmethod
|
|
def test_message_detail_response_normalizes_aliases_and_timestamp(
|
|
app: Flask, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""Test MessageDetailResponse normalizes alias fields and datetime timestamps."""
|
|
created_at = datetime(2026, 1, 2, 3, 4, 5, tzinfo=UTC)
|
|
response = message_module.MessageDetailResponse.model_validate(
|
|
{
|
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"conversation_id": "550e8400-e29b-41d4-a716-446655440001",
|
|
"inputs": {"foo": "bar"},
|
|
"query": "hello",
|
|
"message": [{"text": "hello"}],
|
|
"message_tokens": 7,
|
|
"answer": "world",
|
|
"answer_tokens": 11,
|
|
"provider_response_latency": 1.25,
|
|
"from_source": "user",
|
|
"from_end_user_id": None,
|
|
"from_account_id": "550e8400-e29b-41d4-a716-446655440002",
|
|
"feedbacks": [],
|
|
"workflow_run_id": None,
|
|
"annotation": None,
|
|
"annotation_hit_history": None,
|
|
"status": "normal",
|
|
"created_at": created_at,
|
|
"agent_thoughts": [],
|
|
"message_files": [],
|
|
"message_metadata_dict": {"token_usage": 3},
|
|
"error": None,
|
|
"parent_message_id": None,
|
|
"extra_contents": [],
|
|
}
|
|
)
|
|
assert response.answer == "world"
|
|
assert response.message_tokens == 7
|
|
assert response.answer_tokens == 11
|
|
assert response.provider_response_latency == 1.25
|
|
assert response.metadata == {"token_usage": 3}
|
|
assert response.created_at == int(created_at.timestamp())
|
|
assert response.model_dump(mode="json") == {
|
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"conversation_id": "550e8400-e29b-41d4-a716-446655440001",
|
|
"inputs": {"foo": "bar"},
|
|
"query": "hello",
|
|
"message": [{"text": "hello"}],
|
|
"message_tokens": 7,
|
|
"answer": "world",
|
|
"answer_tokens": 11,
|
|
"provider_response_latency": 1.25,
|
|
"from_source": "user",
|
|
"from_end_user_id": None,
|
|
"from_account_id": "550e8400-e29b-41d4-a716-446655440002",
|
|
"feedbacks": [],
|
|
"workflow_run_id": None,
|
|
"annotation": None,
|
|
"annotation_hit_history": None,
|
|
"created_at": int(created_at.timestamp()),
|
|
"agent_thoughts": [],
|
|
"message_files": [],
|
|
"metadata": {"token_usage": 3},
|
|
"retriever_resources": [],
|
|
"status": "normal",
|
|
"error": None,
|
|
"parent_message_id": None,
|
|
"extra_contents": [],
|
|
}
|