Files
dify/api/tests/unit_tests/controllers/console/app/test_message.py
T

287 lines
10 KiB
Python

from __future__ import annotations
from datetime import UTC, datetime
from decimal import Decimal
import pytest
from flask import Flask
from sqlalchemy.orm import Session
from controllers.console.app import message as message_module
from fields.conversation_fields import MessageResponseSource
from models.account import Account
from models.enums import (
ConversationFromSource,
ConversationStatus,
CreatorUserRole,
FeedbackFromSource,
FeedbackRating,
)
from models.enums import (
InvokeFrom as ModelInvokeFrom,
)
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=ConversationStatus.NORMAL,
invoke_from=ModelInvokeFrom.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=Decimal(0),
message_price_unit=Decimal(0),
answer="answer",
answer_tokens=0,
answer_unit_price=Decimal(0),
answer_price_unit=Decimal(0),
provider_response_latency=0,
total_price=Decimal(0),
currency="USD",
invoke_from=ModelInvokeFrom.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_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 = MessageResponseSource(message, session=session)
assert source.inputs == {"topic": "support"}
assert source.user_feedback is feedback
feedback_source = source.feedbacks[0]
assert feedback_source.from_account is not None
assert feedback_source.from_account.id == "account-1"
annotation_source = source.annotation
assert annotation_source is not None
assert annotation_source.account 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 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
@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": [],
}