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

432 lines
16 KiB
Python

"""Unit tests for ChatContextManager."""
class TestChatContextManagerBuildResearchContext:
"""Tests for ChatContextManager.build_research_context method."""
def test_build_research_context_returns_required_keys(
self, sample_messages, sample_accumulated_context
):
"""Test that build_research_context returns all required keys."""
from src.local_deep_research.chat.context import ChatContextManager
manager = ChatContextManager(
session_id="session-123",
messages=sample_messages,
accumulated_context=sample_accumulated_context,
)
result = manager.build_research_context()
# Actual return keys from implementation
required_keys = {
"session_id",
"original_query",
"accumulated_findings",
"past_findings",
"key_entities",
"topics",
"is_multi_turn",
"turn_count",
}
assert required_keys.issubset(set(result.keys()))
# past_findings must equal accumulated_findings (research engine expects this key)
assert result["past_findings"] == result["accumulated_findings"]
def test_build_research_context_is_multi_turn_false_for_empty_messages(
self, sample_accumulated_context
):
"""Test that is_multi_turn is False when no previous messages."""
from src.local_deep_research.chat.context import ChatContextManager
manager = ChatContextManager(
session_id="session-123",
messages=[],
accumulated_context=sample_accumulated_context,
)
result = manager.build_research_context()
assert result["is_multi_turn"] is False
def test_build_research_context_is_multi_turn_true_for_existing_messages(
self, sample_messages, sample_accumulated_context
):
"""Test that is_multi_turn is True when previous messages exist."""
from src.local_deep_research.chat.context import ChatContextManager
manager = ChatContextManager(
session_id="session-123",
messages=sample_messages,
accumulated_context=sample_accumulated_context,
)
result = manager.build_research_context()
assert result["is_multi_turn"] is True
def test_build_research_context_includes_turn_count(
self, sample_messages, sample_accumulated_context
):
"""Test that turn_count is included in the context."""
from src.local_deep_research.chat.context import ChatContextManager
manager = ChatContextManager(
session_id="session-123",
messages=sample_messages,
accumulated_context=sample_accumulated_context,
)
result = manager.build_research_context()
assert result["turn_count"] == len(sample_messages)
def test_build_research_context_includes_session_id(
self, sample_messages, sample_accumulated_context
):
"""Test that session_id is included in the context."""
from src.local_deep_research.chat.context import ChatContextManager
manager = ChatContextManager(
session_id="test-session-abc",
messages=sample_messages,
accumulated_context=sample_accumulated_context,
)
result = manager.build_research_context()
assert result["session_id"] == "test-session-abc"
class TestChatContextManagerExtraction:
"""Tests for ChatContextManager extraction methods via build_research_context."""
def test_extract_findings_from_history_only_assistant_with_research(
self, sample_messages, sample_accumulated_context
):
"""Test that only assistant messages with research_id are extracted."""
from src.local_deep_research.chat.context import ChatContextManager
manager = ChatContextManager(
session_id="session-123",
messages=sample_messages,
accumulated_context=sample_accumulated_context,
)
result = manager.build_research_context()
# accumulated_findings is a string containing research content
findings = result["accumulated_findings"]
assert isinstance(findings, str)
# Should contain content from assistant messages with research_id
assert len(findings) > 0, (
"Findings should contain assistant research content"
)
assert (
"quantum" in findings.lower() or "application" in findings.lower()
)
def test_extract_findings_limits_to_5_recent(
self, many_messages, sample_accumulated_context
):
"""Test that findings are limited to 5 most recent."""
from src.local_deep_research.chat.context import ChatContextManager
manager = ChatContextManager(
session_id="session-123",
messages=many_messages,
accumulated_context=sample_accumulated_context,
)
result = manager.build_research_context()
# accumulated_findings is a string (joined with "\n\n---\n\n")
findings = result["accumulated_findings"]
if findings:
# Count separators to determine number of findings
separator_count = findings.count("\n\n---\n\n")
# Number of findings = separator_count + 1 (if any content)
finding_count = separator_count + 1 if findings.strip() else 0
assert finding_count <= 5
class TestChatContextManagerCreateSummary:
"""Tests for ChatContextManager _create_summary method via extract_context_updates."""
def test_create_summary_finds_first_paragraph(self):
"""Test that _create_summary finds the first substantial paragraph."""
from src.local_deep_research.chat.context import ChatContextManager
content = """# Heading
This is the first paragraph with substantial content about quantum computing.
This is the second paragraph."""
manager = ChatContextManager(
session_id="session-123",
messages=[],
accumulated_context={},
)
result = manager.extract_context_updates(content)
# summary_addition should contain first substantial paragraph
summary = result["summary_addition"]
# Should find first substantial paragraph (skipping header)
assert "quantum computing" in summary.lower() or len(summary) > 0
def test_create_summary_skips_headers(self):
"""Test that _create_summary skips markdown headers when substantial content follows."""
from src.local_deep_research.chat.context import ChatContextManager
# Content with a substantial paragraph (>50 chars) after headers
content = """# Main Heading
## Sub Heading
This is the actual content paragraph with more than fifty characters of meaningful text about the topic."""
manager = ChatContextManager(
session_id="session-123",
messages=[],
accumulated_context={},
)
result = manager.extract_context_updates(content)
# summary_addition should not start with # (header)
# when there's a substantial paragraph after the headers
summary = result["summary_addition"]
if summary:
assert not summary.strip().startswith("#")
def test_create_summary_truncates_long_content(self):
"""Test that _create_summary truncates long paragraphs to 300 chars."""
from src.local_deep_research.chat.context import ChatContextManager
long_paragraph = "X" * 500
manager = ChatContextManager(
session_id="session-123",
messages=[],
accumulated_context={},
)
result = manager.extract_context_updates(long_paragraph)
# summary_addition should be truncated
summary = result["summary_addition"]
assert len(summary) <= 303 # 300 chars + "..."
class TestChatContextManagerExtractContextUpdates:
"""Tests for ChatContextManager.extract_context_updates method."""
def test_extract_context_updates_returns_required_keys(self):
"""Test that extract_context_updates returns all required keys."""
from src.local_deep_research.chat.context import ChatContextManager
manager = ChatContextManager(
session_id="session-123",
messages=[],
accumulated_context={},
)
result = manager.extract_context_updates("Some new content")
required_keys = {
"new_entities",
"new_topics",
"summary_addition",
}
assert required_keys.issubset(set(result.keys()))
class TestChatContextManagerKeyEntitiesAndTopics:
"""Tests for key entities and topics handling."""
def test_get_key_entities_from_accumulated_context(
self, sample_messages, sample_accumulated_context
):
"""Test that key entities are retrieved from accumulated context."""
from src.local_deep_research.chat.context import ChatContextManager
manager = ChatContextManager(
session_id="session-123",
messages=sample_messages,
accumulated_context=sample_accumulated_context,
)
result = manager.build_research_context()
# Should include entities from accumulated context (limited to 20)
assert "key_entities" in result
assert isinstance(result["key_entities"], list)
assert len(result["key_entities"]) <= 20
def test_get_topics_from_accumulated_context(
self, sample_messages, sample_accumulated_context
):
"""Test that topics are retrieved from accumulated context."""
from src.local_deep_research.chat.context import ChatContextManager
manager = ChatContextManager(
session_id="session-123",
messages=sample_messages,
accumulated_context=sample_accumulated_context,
)
result = manager.build_research_context()
# Should include topics from accumulated context (limited to 10)
assert "topics" in result
assert isinstance(result["topics"], list)
assert len(result["topics"]) <= 10
class TestBuildResearchContextFocusedSummary:
"""build_research_context condenses prior turns into a query-focused summary."""
def _conversation(self):
return [
{
"role": "user",
"content": "What is quantum computing?",
"message_type": "query",
},
{
"role": "assistant",
"content": "Quantum computing uses qubits and superposition.",
"message_type": "response",
"research_id": "r1",
},
]
def test_focused_summary_used_as_past_findings(self, mocker):
"""With a query + snapshot, past_findings is the focused LLM summary."""
from src.local_deep_research.chat.context import ChatContextManager
fake_llm = mocker.Mock()
fake_llm.invoke.return_value = mocker.Mock(
content="Prior work, focused on cost."
)
get_llm = mocker.patch(
"src.local_deep_research.config.llm_config.get_llm",
return_value=fake_llm,
)
manager = ChatContextManager(
session_id="s1",
messages=self._conversation(),
accumulated_context={},
settings_snapshot={"llm.provider": "ollama"},
)
result = manager.build_research_context(
current_query="How much does it cost?"
)
assert result["past_findings"] == "Prior work, focused on cost."
assert result["accumulated_findings"] == "Prior work, focused on cost."
get_llm.assert_called_once()
# The new question must drive the focus of the summary prompt.
prompt = fake_llm.invoke.call_args.args[0]
assert "How much does it cost?" in prompt
# The transcript fed to the summarizer includes both roles.
assert "User:" in prompt and "Assistant:" in prompt
def test_no_current_query_uses_raw_findings(self, mocker):
"""A no-arg call (no focus question) makes no LLM call."""
from src.local_deep_research.chat.context import ChatContextManager
get_llm = mocker.patch(
"src.local_deep_research.config.llm_config.get_llm"
)
manager = ChatContextManager(
session_id="s1",
messages=self._conversation(),
accumulated_context={},
settings_snapshot={"llm.provider": "ollama"},
)
result = manager.build_research_context()
get_llm.assert_not_called()
assert "qubits" in result["past_findings"].lower()
class TestFollowupContextModes:
"""chat.followup_context_mode selects what prior context a follow-up gets."""
def _conversation(self):
return [
{
"role": "user",
"content": "What is quantum computing?",
"message_type": "query",
},
{
"role": "assistant",
"content": "Quantum computing uses qubits and superposition.",
"message_type": "response",
"research_id": "r1",
},
]
def _manager(self, mode):
from src.local_deep_research.chat.context import ChatContextManager
return ChatContextManager(
session_id="s1",
messages=self._conversation(),
accumulated_context={},
settings_snapshot={"chat.followup_context_mode": mode},
)
def test_raw_mode_uses_recent_findings_no_llm(self, mocker):
get_llm = mocker.patch(
"src.local_deep_research.config.llm_config.get_llm"
)
result = self._manager("raw").build_research_context(
current_query="cost?"
)
get_llm.assert_not_called()
assert "qubits" in result["past_findings"].lower()
def test_full_mode_sends_whole_transcript_no_llm(self, mocker):
get_llm = mocker.patch(
"src.local_deep_research.config.llm_config.get_llm"
)
result = self._manager("full").build_research_context(
current_query="cost?"
)
get_llm.assert_not_called()
past = result["past_findings"]
assert "User:" in past and "Assistant:" in past
assert "What is quantum computing?" in past
def test_none_mode_sends_no_prior_findings_no_llm(self, mocker):
get_llm = mocker.patch(
"src.local_deep_research.config.llm_config.get_llm"
)
result = self._manager("none").build_research_context(
current_query="cost?"
)
get_llm.assert_not_called()
assert result["past_findings"] == ""
def test_summary_mode_invokes_llm(self, mocker):
fake_llm = mocker.Mock()
fake_llm.invoke.return_value = mocker.Mock(content="Focused summary.")
mocker.patch(
"src.local_deep_research.config.llm_config.get_llm",
return_value=fake_llm,
)
result = self._manager("summary").build_research_context(
current_query="cost?"
)
assert result["past_findings"] == "Focused summary."
fake_llm.invoke.assert_called_once()
def test_summary_mode_empty_when_llm_cannot_be_built(self, mocker):
"""A get_llm() failure (e.g. misconfigured provider) degrades the
summary to empty rather than crashing the follow-up request."""
mocker.patch(
"src.local_deep_research.config.llm_config.get_llm",
side_effect=RuntimeError("no provider configured"),
)
result = self._manager("summary").build_research_context(
current_query="cost?"
)
assert result["past_findings"] == ""