Files
replypier/backend/tests/repo/test_chat_repo.py
T
WangShen007 d9fe727c3f test: 补充知识处理、检索与 Agent 回归用例
覆盖切块、向量检索、工具调用和历史预算;模型相关用例使用桩。
2026-10-04 20:55:00 +08:00

335 lines
11 KiB
Python

"""
Copyright 2024-2026 ReplyPier
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import pytest
from datetime import datetime
from sqlalchemy import text
from app.repositories.chat import ChatRepository
from app.models.chat_history import ChatHistory
from app.models.customer import Customer
from app.models.agent import Agent, AgentType
from app.models.session_to_agent import SessionToAgent, SessionStatus
from app.models.user import User, UserGroup
from uuid import uuid4, UUID
from app.channels.interactive import from_options
from app.channels.sent_choices import SENT_CHOICES_ATTRIBUTE, encode_sent_choices
from app.models.schemas.chat import ChatDetailResponse
@pytest.fixture
def chat_repo(db):
return ChatRepository(db)
@pytest.fixture
def test_data(db, test_organization_id):
"""Create test data including customer, agent, user, and session"""
customer = Customer(
id=uuid4(),
email="customer@test.com",
full_name="Test Customer",
organization_id=test_organization_id
)
db.add(customer)
agent = Agent(
id=uuid4(),
name="test-agent",
display_name="Test Agent",
organization_id=test_organization_id,
description="Test agent description",
agent_type=AgentType.CUSTOMER_SUPPORT,
instructions=["Be helpful"],
is_active=True
)
db.add(agent)
user = User(
id=uuid4(),
email="user@test.com",
full_name="Test User",
hashed_password="dummy_hash",
is_active=True,
organization_id=test_organization_id
)
db.add(user)
# Create a test group
group = UserGroup(
id=uuid4(),
name="Test Group",
description="Test group description",
organization_id=test_organization_id
)
db.add(group)
db.flush()
session_id = uuid4()
session = SessionToAgent(
session_id=session_id,
organization_id=test_organization_id,
agent_id=agent.id,
user_id=user.id,
customer_id=customer.id,
status=SessionStatus.OPEN,
group_id=group.id
)
db.add(session)
# Add some chat messages
messages = []
for i in range(3):
message = ChatHistory(
organization_id=test_organization_id,
session_id=session_id,
customer_id=customer.id,
agent_id=agent.id,
user_id=user.id,
message=f"Test message {i}",
message_type="text",
attributes={"key": f"value{i}"}
)
messages.append(message)
db.add(message)
db.commit()
return {
"org_id": test_organization_id,
"customer": customer,
"agent": agent,
"user": user,
"session_id": session_id,
"messages": messages,
"group": group
}
def test_create_message(chat_repo, test_data):
"""Test creating a new chat message"""
message_data = {
"organization_id": test_data["org_id"],
"session_id": test_data["session_id"],
"customer_id": test_data["customer"].id,
"agent_id": test_data["agent"].id,
"user_id": test_data["user"].id,
"message": "New test message",
"message_type": "text",
"attributes": {"test": "value"}
}
message = chat_repo.create_message(message_data)
assert message is not None
assert message.message == "New test message"
assert message.message_type == "text"
assert message.attributes == {"test": "value"}
@pytest.mark.asyncio
async def test_get_session_history(chat_repo, test_data):
"""Test retrieving chat history for a session"""
history = await chat_repo.get_session_history(test_data["session_id"])
assert len(history) == 3
for i, msg in enumerate(history):
assert msg.message == f"Test message {i}"
assert msg.user is not None
assert msg.agent is not None
def test_get_user_history(chat_repo, test_data):
"""Test retrieving chat history for a user"""
history = chat_repo.get_user_history(test_data["user"].id)
assert len(history) == 3
for msg in history:
assert msg.user_id == test_data["user"].id
def test_get_recent_chats(chat_repo, test_data):
"""Test retrieving recent chat overviews"""
chats = chat_repo.get_recent_chats(
organization_id=test_data["org_id"],
user_id=test_data["user"].id,
user_groups=[str(test_data["group"].id)]
)
assert len(chats) == 1
chat = chats[0]
assert chat["customer"]["email"] == "customer@test.com"
assert chat["agent"]["name"] == "test-agent"
assert chat["message_count"] == 3
assert chat["status"] == SessionStatus.OPEN
assert chat["group_id"] == str(test_data["group"].id)
def test_get_recent_chats_preview_is_the_latest_message(chat_repo, test_data, db):
"""The preview is the newest message, decrypted — not the lexicographic max,
which is what an aggregate over ciphertext would return."""
db.add(ChatHistory(
organization_id=test_data["org_id"],
session_id=test_data["session_id"],
customer_id=test_data["customer"].id,
agent_id=test_data["agent"].id,
user_id=test_data["user"].id,
# Sorts before every "Test message N" alphabetically, so a max() would miss it.
message="A final reply from the agent",
message_type="text",
))
db.commit()
chats = chat_repo.get_recent_chats(
organization_id=test_data["org_id"],
user_id=test_data["user"].id,
user_groups=[str(test_data["group"].id)]
)
assert chats[0]["last_message"] == "A final reply from the agent"
def test_get_recent_chats_preview_reads_legacy_plaintext(chat_repo, test_data, db):
"""Rows written before encryption at rest have no prefix and must still show."""
legacy = ChatHistory(
organization_id=test_data["org_id"],
session_id=test_data["session_id"],
customer_id=test_data["customer"].id,
agent_id=test_data["agent"].id,
user_id=test_data["user"].id,
message="placeholder",
message_type="text",
created_at=datetime(2030, 1, 1),
)
db.add(legacy)
db.commit()
# Overwrite in raw SQL so the value stays plaintext, as it would be in a row
# written before encryption at rest.
db.execute(
text("UPDATE chat_history SET message = :message WHERE id = :id"),
{"message": "legacy plaintext message", "id": legacy.id},
)
db.commit()
chats = chat_repo.get_recent_chats(
organization_id=test_data["org_id"],
user_id=test_data["user"].id,
user_groups=[str(test_data["group"].id)]
)
assert chats[0]["last_message"] == "legacy plaintext message"
@pytest.mark.asyncio
async def test_check_session_access(chat_repo, test_data):
"""Test checking session access"""
# Test access with user_id
has_access = await chat_repo.check_session_access(
test_data["session_id"],
test_data["user"].id,
[str(test_data["group"].id)]
)
assert has_access is True
# Test access with group
has_access = await chat_repo.check_session_access(
test_data["session_id"],
uuid4(), # Different user
[str(test_data["group"].id)]
)
assert has_access is True
# Test no access
has_access = await chat_repo.check_session_access(
test_data["session_id"],
uuid4(), # Different user
[str(uuid4())] # Different group
)
assert has_access is False
@pytest.mark.asyncio
async def test_get_chat_detail(chat_repo, test_data):
"""Test retrieving detailed chat information"""
detail = await chat_repo.get_chat_detail(
test_data["session_id"],
test_data["org_id"]
)
assert detail is not None
assert detail["customer"]["email"] == "customer@test.com"
assert detail["customer"]["meta_data"] is None
assert detail["agent"]["name"] == "test-agent"
assert detail["status"] == SessionStatus.OPEN
assert detail["group_id"] == str(test_data["group"].id)
assert detail["user_id"] == test_data["user"].id
assert detail["user_name"] == "Test User"
assert len(detail["messages"]) == 3
@pytest.mark.asyncio
async def test_get_nonexistent_chat_detail(chat_repo):
"""Test retrieving detail for nonexistent chat"""
detail = await chat_repo.get_chat_detail(uuid4(), uuid4())
assert detail is None
@pytest.mark.asyncio
async def test_get_chat_detail_includes_customer_meta_data(chat_repo, db, test_data):
"""meta_data set on the customer (e.g. via /generate-token custom_data) is
surfaced in the chat detail response so the inbox can display it."""
test_data["customer"].meta_data = {
"student_name": "Aarav Krishnan",
"center_name": "Special Academy U12"
}
db.commit()
detail = await chat_repo.get_chat_detail(
test_data["session_id"],
test_data["org_id"]
)
assert detail["customer"]["meta_data"] == {
"student_name": "Aarav Krishnan",
"center_name": "Special Academy U12"
}
@pytest.mark.asyncio
async def test_get_chat_detail_exposes_channel_delivery_fields(chat_repo, db, test_data):
"""The inbox matches receipts by id, shows ticks, and reads the sent
choices decrypted — and the response schema must not strip any of it."""
sent = chat_repo.create_message({
"message": "Confirm?", "message_type": "bot",
"session_id": test_data["session_id"], "organization_id": test_data["org_id"],
"external_message_id": "wamid.X",
"attributes": {SENT_CHOICES_ATTRIBUTE: encode_sent_choices(
from_options("Confirm?", ["Yes", "No"]), as_text=False)},
})
sent.channel_status = "read"
db.commit()
detail = await chat_repo.get_chat_detail(test_data["session_id"], test_data["org_id"])
message = ChatDetailResponse(**detail).model_dump()["messages"][-1]
assert message["id"] == sent.id
assert message["external_message_id"] == "wamid.X"
assert message["channel_status"] == "read"
assert [c["text"] for c in message["attributes"][SENT_CHOICES_ATTRIBUTE]["choices"]] == ["Yes", "No"]
# still encrypted at rest
db.refresh(sent)
assert "Yes" not in str(sent.attributes)
def test_app_sent_messages_cost_no_ai_quota(chat_repo, test_data):
common = {"session_id": test_data["session_id"], "organization_id": test_data["org_id"],
"message_type": "bot"}
before = chat_repo.get_message_count_for_period(test_data["org_id"], None, None)
chat_repo.create_message({**common, "message": "AI answer", "attributes": {"source": "ai"}})
chat_repo.create_message({**common, "message": "Old reply"})
chat_repo.create_message({**common, "message": "Fee reminder", "attributes": {"source": "app"}})
assert chat_repo.get_message_count_for_period(test_data["org_id"], None, None) == before + 2