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
351 lines
14 KiB
Python
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"
|