test: 补充知识处理、检索与 Agent 回归用例
覆盖切块、向量检索、工具调用和历史预算;模型相关用例使用桩。
This commit is contained in:
1 parent
2dd062787d
commit
d9fe727c3f
123 files changed
+29005
No files matched your search
@@ -0,0 +1,334 @@
|
||||
"""
|
||||
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
|
||||
Reference in new issue
Block a user