Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

339 lines
12 KiB
Python

"""Unit tests for ChatService boundary conditions.
These tests verify behavior at limit boundaries and edge cases.
Based on constants in the source code.
"""
from unittest.mock import MagicMock, patch
from contextlib import contextmanager
class TestMessageBoundaries:
"""Tests for message content boundaries."""
def test_message_with_empty_content_accepted(self):
"""Test that empty message content is accepted."""
from src.local_deep_research.chat.service import ChatService
mock_session_obj = MagicMock()
mock_session_obj.id = "test-session"
mock_session_obj.message_count = 0
added_messages = []
@contextmanager
def mock_get_user_db_session(username, password=None):
mock_session = MagicMock()
def mock_add(msg):
added_messages.append(msg)
mock_session.add = mock_add
mock_session.commit = MagicMock()
mock_session.query.return_value.filter_by.return_value.with_for_update.return_value.first.return_value = mock_session_obj
mock_session.query.return_value.filter_by.return_value.first.return_value = mock_session_obj
yield mock_session
with patch(
"src.local_deep_research.chat.service.get_user_db_session",
mock_get_user_db_session,
):
service = ChatService(username="testuser")
message_id = service.add_message(
session_id="test-session",
role="user",
content="",
message_type="query",
)
assert message_id is not None
assert len(added_messages) == 1
assert added_messages[0].content == ""
def test_message_with_none_content_is_rejected(self):
"""chat_messages.content is NOT NULL.
add_message must reject content=None at the application layer."""
import pytest
from src.local_deep_research.chat.service import ChatService
service = ChatService(username="testuser")
with pytest.raises(ValueError, match="content is required"):
service.add_message(
session_id="test-session",
role="assistant",
content=None,
message_type="response",
research_id="research-123",
)
class TestContextBoundaries:
"""Tests for accumulated context boundaries."""
def test_accumulated_context_entities_limited_to_50(self):
"""Test that key_entities are limited to 50 items."""
from src.local_deep_research.chat.service import ChatService
mock_session_obj = MagicMock()
mock_session_obj.id = "test-session"
mock_session_obj.accumulated_context = {
"key_entities": [f"existing_{i}" for i in range(40)],
"topics": [],
"summary": "",
}
@contextmanager
def mock_get_user_db_session(username, password=None):
mock_session = MagicMock()
mock_session.query.return_value.filter_by.return_value.with_for_update.return_value.first.return_value = mock_session_obj
mock_session.query.return_value.filter_by.return_value.first.return_value = mock_session_obj
mock_session.commit = MagicMock()
yield mock_session
with patch(
"src.local_deep_research.chat.service.get_user_db_session",
mock_get_user_db_session,
):
service = ChatService(username="testuser")
# Try to add 20 more entities (40 + 20 = 60 > 50)
service.update_accumulated_context(
"test-session",
new_entities=[f"new_{i}" for i in range(20)],
)
# Should be capped at 50
entities = mock_session_obj.accumulated_context["key_entities"]
assert len(entities) <= 50
def test_accumulated_context_topics_limited_to_20(self):
"""Test that topics are limited to 20 items."""
from src.local_deep_research.chat.service import ChatService
mock_session_obj = MagicMock()
mock_session_obj.id = "test-session"
mock_session_obj.accumulated_context = {
"key_entities": [],
"topics": [f"existing_topic_{i}" for i in range(15)],
"summary": "",
}
@contextmanager
def mock_get_user_db_session(username, password=None):
mock_session = MagicMock()
mock_session.query.return_value.filter_by.return_value.with_for_update.return_value.first.return_value = mock_session_obj
mock_session.query.return_value.filter_by.return_value.first.return_value = mock_session_obj
mock_session.commit = MagicMock()
yield mock_session
with patch(
"src.local_deep_research.chat.service.get_user_db_session",
mock_get_user_db_session,
):
service = ChatService(username="testuser")
# Try to add 10 more topics (15 + 10 = 25 > 20)
service.update_accumulated_context(
"test-session",
new_topics=[f"new_topic_{i}" for i in range(10)],
)
# Should be capped at 20
topics = mock_session_obj.accumulated_context["topics"]
assert len(topics) <= 20
def test_accumulated_summary_truncated_to_8000_chars(self):
"""Test that summary is truncated to 8000 characters."""
from src.local_deep_research.chat.service import ChatService
mock_session_obj = MagicMock()
mock_session_obj.id = "test-session"
mock_session_obj.accumulated_context = {
"key_entities": [],
"topics": [],
"summary": "A" * 7000, # Already 7000 chars
}
@contextmanager
def mock_get_user_db_session(username, password=None):
mock_session = MagicMock()
mock_session.query.return_value.filter_by.return_value.with_for_update.return_value.first.return_value = mock_session_obj
mock_session.query.return_value.filter_by.return_value.first.return_value = mock_session_obj
mock_session.commit = MagicMock()
yield mock_session
with patch(
"src.local_deep_research.chat.service.get_user_db_session",
mock_get_user_db_session,
):
service = ChatService(username="testuser")
# Add 2000 more chars (7000 + 2000 + separator = ~9000+ > 8000)
service.update_accumulated_context(
"test-session",
summary_addition="B" * 2000,
)
# Should be capped at 8000
summary = mock_session_obj.accumulated_context["summary"]
assert len(summary) <= 8000
class TestSessionBoundaries:
"""Tests for session creation boundaries."""
def test_session_title_at_max_100_chars(self):
"""Test that title from query is limited to 100 chars + ellipsis."""
from src.local_deep_research.chat.service import ChatService
added_sessions = []
@contextmanager
def mock_get_user_db_session(username, password=None):
mock_session = MagicMock()
def mock_add(session):
added_sessions.append(session)
mock_session.add = mock_add
mock_session.commit = MagicMock()
yield mock_session
with patch(
"src.local_deep_research.chat.service.get_user_db_session",
mock_get_user_db_session,
):
service = ChatService(username="testuser")
# Create query longer than 100 chars
long_query = "A" * 150
service.create_session(initial_query=long_query)
assert len(added_sessions) == 1
title = added_sessions[0].title
# _fallback_title truncates over-long queries to a fixed 100-char
# display budget: query[:97].strip() + "...". The ellipsis lives
# *inside* the 100-char cap, not on top of it — keeps sidebar
# widths predictable.
assert len(title) == 100
assert title.endswith("...")
def test_session_title_exactly_100_chars_no_ellipsis(self):
"""Test that 100-char query doesn't get ellipsis."""
from src.local_deep_research.chat.service import ChatService
added_sessions = []
@contextmanager
def mock_get_user_db_session(username, password=None):
mock_session = MagicMock()
def mock_add(session):
added_sessions.append(session)
mock_session.add = mock_add
mock_session.commit = MagicMock()
yield mock_session
with patch(
"src.local_deep_research.chat.service.get_user_db_session",
mock_get_user_db_session,
):
service = ChatService(username="testuser")
# Create query exactly 100 chars
exact_query = "A" * 100
service.create_session(initial_query=exact_query)
title = added_sessions[0].title
# Title should be exactly the query (no ellipsis)
assert len(title) == 100
assert not title.endswith("...")
class TestContextManagerBoundaries:
"""Tests for ChatContextManager boundaries."""
def test_findings_limited_to_5(self):
"""Test that findings are limited to MAX_FINDINGS_TO_INCLUDE (5)."""
from src.local_deep_research.chat.context import ChatContextManager
# Create 10 assistant messages with research_id
messages = [
{
"id": f"msg-{i}",
"role": "assistant",
"content": f"Finding {i}",
"message_type": "response",
"research_id": f"research-{i}",
}
for i in range(10)
]
manager = ChatContextManager(
session_id="test-session",
messages=messages,
accumulated_context={},
)
findings = manager._extract_findings_from_history()
# Findings is a combined string, but it should only have 5 messages worth
# Count the number of "Finding" occurrences
finding_count = findings.count("Finding")
assert finding_count <= 5
def test_create_summary_truncates_long_paragraphs_to_300(self):
"""Test that _create_summary truncates paragraphs to 300 chars."""
from src.local_deep_research.chat.context import ChatContextManager
long_paragraph = "Z" * 500
manager = ChatContextManager(
session_id="test-session",
messages=[],
accumulated_context={},
)
summary = manager._create_summary(long_paragraph)
# Should be truncated to 300 + "..."
assert len(summary) == 303
assert summary.endswith("...")
class TestEdgeCases:
"""Tests for various edge cases."""
def test_entity_deduplication(self):
"""Test that duplicate entities are deduplicated."""
from src.local_deep_research.chat.service import ChatService
mock_session_obj = MagicMock()
mock_session_obj.id = "test-session"
mock_session_obj.accumulated_context = {
"key_entities": ["quantum", "computing"],
"topics": [],
"summary": "",
}
@contextmanager
def mock_get_user_db_session(username, password=None):
mock_session = MagicMock()
mock_session.query.return_value.filter_by.return_value.with_for_update.return_value.first.return_value = mock_session_obj
mock_session.query.return_value.filter_by.return_value.first.return_value = mock_session_obj
mock_session.commit = MagicMock()
yield mock_session
with patch(
"src.local_deep_research.chat.service.get_user_db_session",
mock_get_user_db_session,
):
service = ChatService(username="testuser")
# Add duplicate entity
service.update_accumulated_context(
"test-session",
new_entities=["quantum", "superposition"],
)
entities = mock_session_obj.accumulated_context["key_entities"]
# quantum should appear only once
assert entities.count("quantum") == 1
# All three unique entities should be present
assert set(entities) == {"quantum", "computing", "superposition"}