Files
learningcircuit--local-deep…/tests/chat/test_chat_research_triggering.py
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

351 lines
14 KiB
Python

# allow: no-sut-import — black-box HTTP test; drives real routes through the Flask test client
"""
Tests for chat research triggering edge cases.
These tests verify the complex research triggering logic in the send_message endpoint.
"""
import json
from unittest.mock import patch, MagicMock
class TestResearchTriggering:
"""Tests for research triggering in send_message endpoint."""
def test_send_message_trigger_research_false_skips_research(
self, authenticated_client
):
"""Test that trigger_research=false skips research process."""
# Create a session first
create_response = authenticated_client.post(
"/api/chat/sessions",
json={"initial_query": "Test query"},
content_type="application/json",
)
session_id = json.loads(create_response.data)["session_id"]
# Send message with trigger_research=False
response = authenticated_client.post(
f"/api/chat/sessions/{session_id}/messages",
json={"content": "Test message", "trigger_research": False},
content_type="application/json",
)
assert response.status_code == 200
data = json.loads(response.data)
assert data["success"] is True
assert data["research_id"] is None
assert data["research_mode"] == "none"
def test_send_message_trigger_research_true_returns_research_id(
self, authenticated_client
):
"""Test that trigger_research=true starts research and returns research_id."""
# Create a session first
create_response = authenticated_client.post(
"/api/chat/sessions",
json={"initial_query": "Test query"},
content_type="application/json",
)
session_id = json.loads(create_response.data)["session_id"]
# Mock the research process to avoid actually running it
# Patch at the source module where the function is defined
with patch(
"local_deep_research.chat.routes.start_research_process"
) as mock_start_research:
response = authenticated_client.post(
f"/api/chat/sessions/{session_id}/messages",
json={
"content": "What is quantum computing?",
"trigger_research": True,
},
content_type="application/json",
)
assert response.status_code == 200
data = json.loads(response.data)
assert data["success"] is True
assert data["research_id"] is not None
assert data["research_mode"] == "quick"
# Verify research process was started
assert mock_start_research.called
def test_send_message_blocks_research_when_password_missing_encrypted(
self, authenticated_client
):
"""Encrypted DB + no DB password -> 401, no research spawned (#4457).
Mirrors the guard on /start_research and the follow-up route: a
chat-triggered research must not run passwordless, because its
background metric writes (token/search) would be silently dropped,
leaving the metrics dashboard empty while the research completes.
Trigger in production: session-password TTL expiry or a server
restart while the session cookie is still valid.
"""
create_response = authenticated_client.post(
"/api/chat/sessions",
json={"initial_query": "Test query"},
content_type="application/json",
)
session_id = json.loads(create_response.data)["session_id"]
# The route delegates the encryption-aware decision to the shared
# resolve_user_password helper; simulate "encrypted DB, password
# lost" -> (None, session_expired=True).
with (
patch(
"local_deep_research.chat.routes.resolve_user_password",
return_value=(None, True),
),
patch(
"local_deep_research.chat.routes.start_research_process"
) as mock_start_research,
):
response = authenticated_client.post(
f"/api/chat/sessions/{session_id}/messages",
json={
"content": "What is quantum computing?",
"trigger_research": True,
},
content_type="application/json",
)
assert response.status_code == 401
data = json.loads(response.data)
assert data["success"] is False
assert "log out" in data["error"].lower()
# No worker thread spawned — the run is refused before side effects.
mock_start_research.assert_not_called()
def test_send_message_unencrypted_db_allows_research_without_password(
self, authenticated_client
):
"""A plaintext (no-SQLCipher) DB must NOT be blocked by the password guard.
resolve_user_password returns (None, session_expired=False) for an
unencrypted install, so the chat research path must proceed.
"""
create_response = authenticated_client.post(
"/api/chat/sessions",
json={"initial_query": "Test query"},
content_type="application/json",
)
session_id = json.loads(create_response.data)["session_id"]
with (
patch(
"local_deep_research.chat.routes.resolve_user_password",
return_value=(None, False),
),
patch(
"local_deep_research.chat.routes.start_research_process"
) as mock_start_research,
):
response = authenticated_client.post(
f"/api/chat/sessions/{session_id}/messages",
json={
"content": "What is quantum computing?",
"trigger_research": True,
},
content_type="application/json",
)
assert response.status_code == 200
assert mock_start_research.called
def test_send_message_uses_quick_mode_by_default(
self, authenticated_client
):
"""Test that research always uses 'quick' mode in chat."""
# Create a session
create_response = authenticated_client.post(
"/api/chat/sessions",
json={"initial_query": "Test query"},
content_type="application/json",
)
session_id = json.loads(create_response.data)["session_id"]
# Mock research process at source module
with patch("local_deep_research.chat.routes.start_research_process"):
response = authenticated_client.post(
f"/api/chat/sessions/{session_id}/messages",
json={
"content": "Research question",
"trigger_research": True,
"research_mode": "detailed", # Try to request detailed
},
content_type="application/json",
)
# Even when requesting "detailed", chat uses "quick"
assert response.status_code == 200
data = json.loads(response.data)
assert data["research_mode"] == "quick"
def test_send_message_nonexistent_session_returns_404(
self, authenticated_client
):
"""Test that sending message to non-existent session returns 404."""
response = authenticated_client.post(
"/api/chat/sessions/non-existent-session-id-12345/messages",
json={"content": "Test message", "trigger_research": False},
content_type="application/json",
)
assert response.status_code == 404
data = json.loads(response.data)
assert data["success"] is False
assert "Session not found" in data["error"]
def test_send_message_context_includes_history(self, authenticated_client):
"""Test that research context includes conversation history."""
# Create a session
create_response = authenticated_client.post(
"/api/chat/sessions",
json={"initial_query": "Initial topic"},
content_type="application/json",
)
session_id = json.loads(create_response.data)["session_id"]
# Send first message without research
authenticated_client.post(
f"/api/chat/sessions/{session_id}/messages",
json={"content": "What is AI?", "trigger_research": False},
content_type="application/json",
)
# Send second message with research - verify context is passed
# Patch at correct locations - ChatContextManager is imported at module level in routes
with patch("local_deep_research.chat.routes.start_research_process"):
with patch(
"local_deep_research.chat.routes.ChatContextManager"
) as mock_context_manager_class:
mock_context_manager = MagicMock()
mock_context_manager.build_research_context.return_value = {
"session_id": session_id,
"is_multi_turn": True,
}
mock_context_manager_class.return_value = mock_context_manager
response = authenticated_client.post(
f"/api/chat/sessions/{session_id}/messages",
json={
"content": "Tell me more about machine learning",
"trigger_research": True,
},
content_type="application/json",
)
assert response.status_code == 200
# Verify context manager was called with messages
assert mock_context_manager_class.called
def test_send_message_default_trigger_research_is_true(
self, authenticated_client
):
"""Test that trigger_research defaults to True when not specified."""
# Create a session
create_response = authenticated_client.post(
"/api/chat/sessions",
json={"initial_query": "Test query"},
content_type="application/json",
)
session_id = json.loads(create_response.data)["session_id"]
# Mock research process at source module
with patch(
"local_deep_research.chat.routes.start_research_process"
) as mock_start_research:
response = authenticated_client.post(
f"/api/chat/sessions/{session_id}/messages",
json={
"content": "Test message"
}, # No trigger_research specified
content_type="application/json",
)
assert response.status_code == 200
data = json.loads(response.data)
# Should trigger research by default
assert data["research_id"] is not None
assert mock_start_research.called
def test_follow_up_uses_contextual_followup_strategy(
self, authenticated_client
):
"""Test that follow-up messages switch to enhanced-contextual-followup strategy."""
create_response = authenticated_client.post(
"/api/chat/sessions",
json={"initial_query": "Initial topic"},
content_type="application/json",
)
session_id = json.loads(create_response.data)["session_id"]
# Add first message without research
authenticated_client.post(
f"/api/chat/sessions/{session_id}/messages",
json={"content": "What is AI?", "trigger_research": False},
content_type="application/json",
)
# Send follow-up — mock context to return is_multi_turn=True
with patch(
"local_deep_research.chat.routes.start_research_process"
) as mock_start:
with patch(
"local_deep_research.chat.routes.ChatContextManager"
) as mock_ctx_cls:
mock_ctx = MagicMock()
mock_ctx.build_research_context.return_value = {
"session_id": session_id,
"is_multi_turn": True,
}
mock_ctx_cls.return_value = mock_ctx
authenticated_client.post(
f"/api/chat/sessions/{session_id}/messages",
json={"content": "Tell me more", "trigger_research": True},
content_type="application/json",
)
assert mock_start.called
call_kwargs = mock_start.call_args[1]
assert call_kwargs["strategy"] == "enhanced-contextual-followup"
assert (
call_kwargs["research_context"]["delegate_strategy"]
is not None
)
def test_first_message_uses_user_strategy(self, authenticated_client):
"""Test that first message uses the user's configured strategy, not followup."""
create_response = authenticated_client.post(
"/api/chat/sessions",
json={"initial_query": "First question"},
content_type="application/json",
)
session_id = json.loads(create_response.data)["session_id"]
with patch(
"local_deep_research.chat.routes.start_research_process"
) as mock_start:
with patch(
"local_deep_research.chat.routes.ChatContextManager"
) as mock_ctx_cls:
mock_ctx = MagicMock()
mock_ctx.build_research_context.return_value = {
"session_id": session_id,
"is_multi_turn": False,
}
mock_ctx_cls.return_value = mock_ctx
authenticated_client.post(
f"/api/chat/sessions/{session_id}/messages",
json={"content": "First query", "trigger_research": True},
content_type="application/json",
)
assert mock_start.called
call_kwargs = mock_start.call_args[1]
assert call_kwargs["strategy"] != "enhanced-contextual-followup"