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
339 lines
12 KiB
Python
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"}
|