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

518 lines
18 KiB
Python

"""
Property-based fuzz tests for chat components using Hypothesis.
These tests verify that chat functions don't crash on arbitrary input
and handle edge cases gracefully.
"""
import pytest
from hypothesis import given, settings, strategies as st, assume, HealthCheck
from unittest.mock import MagicMock, patch
from contextlib import contextmanager
# =============================================================================
# Custom Strategies for Chat Testing
# =============================================================================
def message_roles():
"""Generate valid message roles."""
return st.sampled_from(["user", "assistant"])
def message_types():
"""Generate valid message types.
The schema removed "step" from ChatMessageType (steps live in
chat_progress_steps with their own enum-less schema); the chat
context layer filters out any "step" rows before they reach here.
"""
return st.sampled_from(["query", "followup", "response"])
def safe_text(max_size=200):
"""Generate text without problematic Unicode."""
return st.text(
alphabet=st.characters(
blacklist_categories=("Cs",), # Exclude surrogates
blacklist_characters=["\x00"], # Exclude null bytes
),
max_size=max_size,
)
def message_dict():
"""Generate a message dictionary."""
return st.fixed_dictionaries(
{
"id": st.text(
min_size=5, max_size=20, alphabet="abcdef0123456789-"
),
"role": message_roles(),
"content": safe_text(max_size=100),
"message_type": message_types(),
"research_id": st.one_of(
st.none(),
st.text(min_size=5, max_size=20, alphabet="abcdef0123456789-"),
),
}
)
def accumulated_context_dict():
"""Generate an accumulated context dictionary."""
return st.fixed_dictionaries(
{
"key_entities": st.lists(safe_text(max_size=50), max_size=20),
"topics": st.lists(safe_text(max_size=30), max_size=10),
"summary": safe_text(max_size=200),
}
)
# =============================================================================
# ChatContextManager Fuzz Tests
# =============================================================================
class TestChatContextManagerFuzz:
"""Fuzz tests for ChatContextManager."""
@given(
messages=st.lists(message_dict(), max_size=10),
accumulated=st.one_of(st.none(), accumulated_context_dict()),
)
@settings(max_examples=50, suppress_health_check=[HealthCheck.too_slow])
def test_build_context_handles_arbitrary_messages(
self, messages, accumulated
):
"""Test that build_research_context never crashes on arbitrary input."""
from src.local_deep_research.chat.context import ChatContextManager
try:
manager = ChatContextManager(
session_id="test-session",
messages=messages,
accumulated_context=accumulated,
)
context = manager.build_research_context()
# Verify result structure
assert isinstance(context, dict)
assert "session_id" in context
assert "is_multi_turn" in context
except Exception as e:
pytest.fail(
f"build_research_context crashed: {type(e).__name__}: {e}"
)
@given(accumulated=accumulated_context_dict())
@settings(max_examples=50, suppress_health_check=[HealthCheck.too_slow])
def test_accumulated_context_handles_arbitrary_data(self, accumulated):
"""Test that accumulated context with arbitrary data is handled."""
from src.local_deep_research.chat.context import ChatContextManager
try:
manager = ChatContextManager(
session_id="test-session",
messages=[],
accumulated_context=accumulated,
)
# Test key entity extraction
entities = manager._get_key_entities()
assert isinstance(entities, list)
assert len(entities) <= 20 # Should be limited
# Test topics extraction
topics = manager._get_topics()
assert isinstance(topics, list)
assert len(topics) <= 10 # Should be limited
except Exception as e:
pytest.fail(f"Context handling crashed: {type(e).__name__}: {e}")
@given(content=safe_text(max_size=500))
@settings(max_examples=50, suppress_health_check=[HealthCheck.too_slow])
def test_create_summary_handles_arbitrary_content(self, content):
"""Test that summary creation handles arbitrary content."""
from src.local_deep_research.chat.context import ChatContextManager
try:
manager = ChatContextManager("test-session", [], {})
summary = manager._create_summary(content)
# Should return a string
assert isinstance(summary, str)
# Should be limited in length
assert len(summary) <= 400 # 300 + "..."
except Exception as e:
pytest.fail(f"Summary creation crashed: {type(e).__name__}: {e}")
@given(content=safe_text(max_size=200))
@settings(max_examples=30, suppress_health_check=[HealthCheck.too_slow])
def test_extract_context_updates_handles_arbitrary_input(self, content):
"""Test that context update extraction handles arbitrary input."""
from src.local_deep_research.chat.context import ChatContextManager
try:
manager = ChatContextManager("test-session", [], {})
updates = manager.extract_context_updates(content)
# Verify structure
assert isinstance(updates, dict)
assert "new_entities" in updates
assert "new_topics" in updates
assert "summary_addition" in updates
except Exception as e:
pytest.fail(
f"Context update extraction crashed: {type(e).__name__}: {e}"
)
# =============================================================================
# ChatService Fuzz Tests
# =============================================================================
class TestChatServiceFuzz:
"""Fuzz tests for ChatService."""
@given(query=st.one_of(st.none(), safe_text(max_size=200)))
@settings(max_examples=50, suppress_health_check=[HealthCheck.too_slow])
def test_title_generation_handles_any_string(self, query):
"""Test that title generation handles any input string."""
from src.local_deep_research.chat.service import ChatService
try:
service = ChatService(username="test_user")
title = service._generate_title(query)
# Should return a string
assert isinstance(title, str)
# Should have reasonable length
assert len(title) <= 150 # 100 + "..."
except Exception as e:
pytest.fail(f"Title generation crashed: {type(e).__name__}: {e}")
@given(
content=safe_text(max_size=100),
role=message_roles(),
message_type=message_types(),
)
@settings(max_examples=30, suppress_health_check=[HealthCheck.too_slow])
def test_add_message_handles_unicode(self, content, role, message_type):
"""Test that add_message handles various Unicode content."""
from src.local_deep_research.chat.service import ChatService
# Skip empty content for valid messages
assume(len(content.strip()) > 0)
@contextmanager
def mock_get_user_db_session(username, password=None):
mock_session = MagicMock()
# Mock the session query to return a valid session
mock_chat_session = MagicMock()
mock_chat_session.message_count = 0
mock_session.query.return_value.filter_by.return_value.with_for_update.return_value.first.return_value = mock_chat_session
mock_session.query.return_value.filter_by.return_value.first.return_value = mock_chat_session
mock_session.add = MagicMock()
mock_session.commit = MagicMock()
yield mock_session
with patch(
"src.local_deep_research.chat.service.get_user_db_session",
mock_get_user_db_session,
):
try:
service = ChatService(username="test_user")
message_id = service.add_message(
session_id="test-session",
role=role,
content=content,
message_type=message_type,
)
# Should return a UUID string
assert isinstance(message_id, str)
assert len(message_id) > 0
except ValueError:
# Session not found is expected in some cases
pass
except Exception as e:
pytest.fail(f"add_message crashed: {type(e).__name__}: {e}")
# =============================================================================
# Chat API Fuzz Tests (Parametrized for performance)
# =============================================================================
# Test data for API fuzzing - these are checked without Hypothesis
# to avoid the fixture overhead with each hypothesis iteration
SPECIAL_CONTENT_CASES = [
"Normal message",
"", # Empty
" " * 100, # Whitespace
"Unicode: Привет 你好 🎉",
"<script>alert('XSS')</script>",
"'\"<>&", # HTML special chars
"Line1\nLine2\rLine3",
"A" * 9999, # Near limit
"\x00\x01\x02", # Control chars
]
UNICODE_TITLE_CASES = [
"Normal Title",
"",
"Привет мир",
"你好世界",
"🎉🔥💯",
"<script>",
"A" * 499, # Near limit
]
PAGINATION_CASES = [
(-1, 0),
(0, 0),
(1, 0),
(100, 0),
(101, 0),
(20, -1),
(20, 0),
(20, 1000),
]
STATUS_CASES = [
"active",
"archived",
"all",
"invalid",
"",
"' OR '1'='1",
"<script>",
]
class TestChatAPIFuzz:
"""Fuzz tests for Chat API endpoints using parametrized tests."""
@pytest.mark.parametrize("content", SPECIAL_CONTENT_CASES)
def test_send_message_handles_special_characters(
self, content, authenticated_client
):
"""Test that send message handles special characters in content."""
import json
# Create a session first
create_response = authenticated_client.post(
"/api/chat/sessions",
json={"initial_query": "Test"},
content_type="application/json",
)
# Session creation is the precondition, NOT the system under
# fuzz. Skipping on a non-200 here would silently mask any
# regression in POST /api/chat/sessions (CI would go green for
# every parametrized case). Assert so a broken precondition
# fails loudly.
assert create_response.status_code == 200, (
f"session creation precondition failed: "
f"{create_response.status_code} {create_response.data!r}"
)
session_id = json.loads(create_response.data)["session_id"]
# Send message with test content
response = authenticated_client.post(
f"/api/chat/sessions/{session_id}/messages",
json={"content": content, "trigger_research": False},
content_type="application/json",
)
# Should not crash. 200 = accepted, 400 = validation error.
# 500 here would be an unhandled-exception regression on user input.
assert response.status_code in [200, 400]
@pytest.mark.parametrize("title", UNICODE_TITLE_CASES)
def test_session_title_update_handles_unicode(
self, title, authenticated_client
):
"""Test that session title update handles Unicode strings."""
import json
# Create a session
create_response = authenticated_client.post(
"/api/chat/sessions",
json={"initial_query": "Test"},
content_type="application/json",
)
# Precondition assert — see test_message_send_handles_unicode
# for the rationale (skip would mask session-create regression).
assert create_response.status_code == 200, (
f"session creation precondition failed: "
f"{create_response.status_code} {create_response.data!r}"
)
session_id = json.loads(create_response.data)["session_id"]
# Update with test title
response = authenticated_client.patch(
f"/api/chat/sessions/{session_id}",
json={"title": title},
content_type="application/json",
)
# Should not crash. 200 = accepted, 400 = validation error.
# 500 here would be an unhandled-exception regression on user input.
assert response.status_code in [200, 400]
@pytest.mark.parametrize("limit,offset", PAGINATION_CASES)
def test_pagination_handles_edge_cases(
self, limit, offset, authenticated_client
):
"""Test that pagination parameters handle edge cases."""
import json
response = authenticated_client.get(
f"/api/chat/sessions?limit={limit}&offset={offset}"
)
# Should not crash - may clamp values to valid range
assert response.status_code in [200, 400]
if response.status_code == 200:
data = json.loads(response.data)
assert "sessions" in data
@pytest.mark.parametrize("status", STATUS_CASES)
def test_list_sessions_handles_invalid_status(
self, status, authenticated_client
):
"""Test that list sessions handles invalid status values."""
import json
response = authenticated_client.get(
f"/api/chat/sessions?status={status}"
)
# Should not crash - invalid status should default to "active"
assert response.status_code == 200
data = json.loads(response.data)
assert data["success"] is True
# =============================================================================
# Prompt Context Fuzz Tests
# =============================================================================
class TestPromptContextFuzz:
"""Fuzz tests for findings extraction."""
@given(messages=st.lists(message_dict(), min_size=5, max_size=20))
@settings(max_examples=20, suppress_health_check=[HealthCheck.too_slow])
def test_findings_extraction_handles_many_messages(self, messages):
"""Test that findings extraction handles many messages."""
from src.local_deep_research.chat.context import ChatContextManager
try:
manager = ChatContextManager("test-session", messages, {})
findings = manager._extract_findings_from_history()
# Should return a string
assert isinstance(findings, str)
except Exception as e:
pytest.fail(f"Findings extraction crashed: {type(e).__name__}: {e}")
# =============================================================================
# Edge Case Tests
# =============================================================================
class TestEdgeCases:
"""Tests for specific edge cases that could cause issues."""
def test_empty_messages_list(self):
"""Test handling of empty messages list."""
from src.local_deep_research.chat.context import ChatContextManager
manager = ChatContextManager("test-session", [], {})
context = manager.build_research_context()
assert context["is_multi_turn"] is False
assert context["turn_count"] == 0
def test_none_messages_list(self):
"""Test handling of None messages."""
from src.local_deep_research.chat.context import ChatContextManager
manager = ChatContextManager("test-session", None, {})
context = manager.build_research_context()
assert context["is_multi_turn"] is False
def test_none_accumulated_context(self):
"""Test handling of None accumulated context."""
from src.local_deep_research.chat.context import ChatContextManager
manager = ChatContextManager("test-session", [], None)
entities = manager._get_key_entities()
topics = manager._get_topics()
assert entities == []
assert topics == []
def test_very_long_content_truncation(self):
"""Test that very long content is properly truncated."""
from src.local_deep_research.chat.context import ChatContextManager
# Create message with very long content
long_content = "A" * 10000
messages = [
{
"id": "msg-1",
"role": "assistant",
"content": long_content,
"message_type": "response",
"research_id": "research-1",
}
]
manager = ChatContextManager("test-session", messages, {})
findings = manager.build_research_context()["accumulated_findings"]
# Should be significantly shorter than original
assert len(findings) < len(long_content)
def test_special_characters_in_entities(self):
"""Test handling of special characters in entities."""
from src.local_deep_research.chat.context import ChatContextManager
accumulated = {
"key_entities": [
"entity<script>",
"entity'with'quotes",
'entity"double',
"entity\nnewline",
],
"topics": ["topic<tag>", "topic;semicolon"],
"summary": "Summary with <html> tags",
}
manager = ChatContextManager("test-session", [], accumulated)
context = manager.build_research_context()
# Should not crash and should include entities
assert len(context["key_entities"]) > 0