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

717 lines
26 KiB
Python

"""
Tests for FollowUpContextHandler.
Tests cover:
- Initialization with model and settings
- Context building from research data
- Findings extraction from various data formats
- Source extraction and deduplication
- Entity extraction via LLM
- Summary generation for context and prompts
- Gap identification
- Settings snapshot formatting
- LLM context formatting
"""
from unittest.mock import Mock
import pytest
from local_deep_research.advanced_search_system.knowledge.followup_context_manager import (
FollowUpContextHandler,
)
class TestFollowUpContextHandlerInit:
"""Tests for FollowUpContextHandler initialization."""
def test_init_stores_model(self):
"""Handler stores the model reference."""
mock_model = Mock()
handler = FollowUpContextHandler(mock_model)
assert handler.model is mock_model
def test_init_with_settings_snapshot(self):
"""Handler stores settings snapshot."""
mock_model = Mock()
settings = {"key": "value"}
handler = FollowUpContextHandler(mock_model, settings_snapshot=settings)
assert handler.settings_snapshot == settings
def test_init_without_settings_snapshot(self):
"""Handler initializes empty settings snapshot when None."""
mock_model = Mock()
handler = FollowUpContextHandler(mock_model, settings_snapshot=None)
assert handler.settings_snapshot == {}
def test_init_creates_empty_cache(self):
"""Handler initializes empty research cache."""
mock_model = Mock()
handler = FollowUpContextHandler(mock_model)
assert handler.past_research_cache == {}
class TestBuildContext:
"""Tests for build_context method."""
@pytest.fixture
def mock_model(self):
"""Create mock model."""
mock = Mock()
mock.invoke.return_value = Mock(
content="Entity1\nEntity2\nSummary text"
)
return mock
@pytest.fixture
def handler(self, mock_model):
"""Create handler instance."""
return FollowUpContextHandler(mock_model)
@pytest.fixture
def sample_research_data(self):
"""Create sample research data."""
return {
"research_id": "research_123",
"query": "What is machine learning?",
"formatted_findings": "ML is a field of AI...",
"report_content": "Detailed report about ML...",
"resources": [
{"url": "https://example.com/1", "title": "Source 1"}
],
"all_links_of_system": [
{"url": "https://example.com/2", "title": "Source 2"}
],
"strategy": "detailed",
"mode": "research",
"created_at": "2024-01-14",
"research_meta": {"duration": 120},
}
def test_build_context_returns_dict(self, handler, sample_research_data):
"""build_context returns a dictionary."""
result = handler.build_context(
sample_research_data, "What are ML applications?"
)
assert isinstance(result, dict)
def test_build_context_surfaces_chat_past_findings(self, handler):
"""End-to-end of the fix: a chat-style context (only past_findings,
no formatted_findings/report_content) surfaces its summary as
past_findings, so it reaches generate_contextualized_query instead of
being replaced by the 'No previous findings available' sentinel."""
chat_context = {
"research_id": "r1",
"original_query": "What is machine learning?",
"past_findings": "Summary of the conversation, focused on cost.",
}
result = handler.build_context(chat_context, "How much does it cost?")
assert (
result["past_findings"]
== "Summary of the conversation, focused on cost."
)
def test_build_context_includes_parent_research_id(
self, handler, sample_research_data
):
"""Context includes parent_research_id."""
result = handler.build_context(
sample_research_data, "Follow-up question?"
)
assert result["parent_research_id"] == "research_123"
def test_build_context_includes_original_query(
self, handler, sample_research_data
):
"""Context includes original_query."""
result = handler.build_context(
sample_research_data, "Follow-up question?"
)
assert result["original_query"] == "What is machine learning?"
def test_build_context_includes_follow_up_query(
self, handler, sample_research_data
):
"""Context includes follow_up_query."""
result = handler.build_context(
sample_research_data, "What are ML applications?"
)
assert result["follow_up_query"] == "What are ML applications?"
def test_build_context_includes_all_keys(
self, handler, sample_research_data
):
"""Context includes all expected keys."""
result = handler.build_context(sample_research_data, "Follow-up?")
expected_keys = [
"parent_research_id",
"original_query",
"follow_up_query",
"past_findings",
"past_sources",
"key_entities",
"summary",
"report_content",
"formatted_findings",
"all_links_of_system",
"metadata",
]
for key in expected_keys:
assert key in result
class TestExtractFindings:
"""Tests for _extract_findings method."""
@pytest.fixture
def handler(self):
"""Create handler without model."""
return FollowUpContextHandler(None)
def test_extract_from_formatted_findings(self, handler):
"""Extract from formatted_findings field."""
data = {"formatted_findings": "Formatted findings text"}
result = handler._extract_findings(data)
assert "Formatted findings text" in result
def test_extract_from_report_content_when_no_formatted(self, handler):
"""Extract from report_content when no formatted_findings."""
data = {"report_content": "Report content text here"}
result = handler._extract_findings(data)
assert "Report content text" in result
def test_report_content_truncated_to_2000(self, handler):
"""Report content is truncated to 2000 characters."""
long_content = "A" * 5000
data = {"report_content": long_content}
result = handler._extract_findings(data)
assert len(result) == 2000
def test_returns_default_when_no_findings(self, handler):
"""Returns default message when no findings available."""
data = {}
result = handler._extract_findings(data)
assert result == "No previous findings available"
def test_honors_chat_past_findings_when_no_report(self, handler):
"""Multi-turn chat supplies prior findings under ``past_findings``
(it has no formatted_findings/report_content); that must be used as
the findings rather than the "nothing available" sentinel."""
data = {"past_findings": "Condensed summary of the conversation."}
result = handler._extract_findings(data)
assert result == "Condensed summary of the conversation."
def test_report_content_takes_precedence_over_past_findings(self, handler):
"""Non-chat callers populate report_content; past_findings must not
override it (keeps the standalone follow-up path unchanged)."""
data = {
"report_content": "Full parent report.",
"past_findings": "should not be used",
}
result = handler._extract_findings(data)
assert "Full parent report." in result
assert "should not be used" not in result
def test_prefers_formatted_over_report(self, handler):
"""Prefers formatted_findings over report_content."""
data = {
"formatted_findings": "Formatted text",
"report_content": "Report text",
}
result = handler._extract_findings(data)
assert "Formatted text" in result
class TestExtractSources:
"""Tests for _extract_sources method."""
@pytest.fixture
def handler(self):
"""Create handler without model."""
return FollowUpContextHandler(None)
def test_extract_from_resources(self, handler):
"""Extract sources from resources field."""
data = {"resources": [{"url": "https://a.com", "title": "A"}]}
result = handler._extract_sources(data)
assert len(result) == 1
assert result[0]["url"] == "https://a.com"
def test_extract_from_all_links_of_system(self, handler):
"""Extract sources from all_links_of_system field."""
data = {"all_links_of_system": [{"url": "https://b.com", "title": "B"}]}
result = handler._extract_sources(data)
assert len(result) == 1
def test_extract_from_past_links(self, handler):
"""Extract sources from past_links field."""
data = {"past_links": [{"url": "https://c.com", "title": "C"}]}
result = handler._extract_sources(data)
assert len(result) == 1
def test_deduplicates_by_url(self, handler):
"""Deduplicates sources by URL."""
data = {
"resources": [{"url": "https://a.com", "title": "A1"}],
"all_links_of_system": [{"url": "https://a.com", "title": "A2"}],
}
result = handler._extract_sources(data)
assert len(result) == 1
def test_includes_sources_without_url(self, handler):
"""Includes sources that have no URL."""
data = {"resources": [{"title": "No URL source"}]}
result = handler._extract_sources(data)
assert len(result) == 1
def test_combines_all_source_fields(self, handler):
"""Combines sources from all fields."""
data = {
"resources": [{"url": "https://a.com", "title": "A"}],
"all_links_of_system": [{"url": "https://b.com", "title": "B"}],
"past_links": [{"url": "https://c.com", "title": "C"}],
}
result = handler._extract_sources(data)
assert len(result) == 3
def test_returns_empty_list_when_no_sources(self, handler):
"""Returns empty list when no sources available."""
data = {}
result = handler._extract_sources(data)
assert result == []
class TestExtractEntities:
"""Tests for _extract_entities method."""
@pytest.fixture
def mock_model(self):
"""Create mock model."""
mock = Mock()
mock.invoke.return_value = Mock(content="Entity1\nEntity2\nEntity3")
return mock
@pytest.fixture
def handler(self, mock_model):
"""Create handler with mock model."""
return FollowUpContextHandler(mock_model)
def test_extracts_entities_from_findings(self, handler):
"""Extracts entities using LLM."""
data = {"formatted_findings": "Research findings about Python and ML"}
result = handler._extract_entities(data)
assert len(result) == 3
assert "Entity1" in result
def test_limits_to_10_entities(self, handler, mock_model):
"""Limits entities to 10."""
mock_model.invoke.return_value = Mock(
content="\n".join([f"Entity{i}" for i in range(15)])
)
data = {"formatted_findings": "Some findings"}
result = handler._extract_entities(data)
assert len(result) <= 10
def test_returns_empty_when_no_findings_and_no_model(self):
"""Returns empty list when no findings and no model."""
handler = FollowUpContextHandler(None)
data = {}
result = handler._extract_entities(data)
assert result == []
def test_returns_empty_when_no_model(self):
"""Returns empty list when no model available."""
handler = FollowUpContextHandler(None)
data = {"formatted_findings": "Some findings"}
result = handler._extract_entities(data)
assert result == []
def test_handles_llm_exception(self, handler, mock_model):
"""Handles LLM exception gracefully."""
mock_model.invoke.side_effect = RuntimeError("Connection failed")
data = {"formatted_findings": "Some findings"}
result = handler._extract_entities(data)
assert result == []
class TestCreateSummary:
"""Tests for _create_summary method."""
@pytest.fixture
def mock_model(self):
"""Create mock model."""
mock = Mock()
mock.invoke.return_value = Mock(content="Summary of findings")
return mock
@pytest.fixture
def handler(self, mock_model):
"""Create handler with mock model."""
return FollowUpContextHandler(mock_model)
def test_creates_summary_with_llm(self, handler):
"""Creates summary using LLM."""
data = {
"formatted_findings": "Research findings",
"query": "Original question?",
}
handler._create_summary(data, "Follow-up question?")
assert handler.model.invoke.called
def test_includes_original_query_in_prompt(self, handler):
"""Includes original query in prompt."""
data = {
"formatted_findings": "Research findings",
"query": "Original question?",
}
handler._create_summary(data, "Follow-up?")
call_args = handler.model.invoke.call_args[0][0]
assert "Original question?" in call_args
class TestSummarizeForFollowup:
"""Tests for summarize_for_followup method."""
@pytest.fixture
def mock_model(self):
"""Create mock model."""
mock = Mock()
mock.invoke.return_value = Mock(content="Concise summary")
return mock
@pytest.fixture
def handler(self, mock_model):
"""Create handler with mock model."""
return FollowUpContextHandler(mock_model)
def test_summarize_returns_string(self, handler):
"""summarize_for_followup returns a string."""
result = handler.summarize_for_followup(
"Long findings text...", "Follow-up question?"
)
assert isinstance(result, str)
def test_summarize_respects_max_length(self, handler, mock_model):
"""Result respects max_length constraint."""
mock_model.invoke.return_value = Mock(content="A" * 2000)
result = handler.summarize_for_followup(
"Findings", "Question?", max_length=500
)
assert len(result) <= 503 # 500 + "..."
def test_short_findings_returned_as_is(self, handler):
"""Short findings are returned as-is without LLM call."""
result = handler.summarize_for_followup(
"Short text", "Question?", max_length=1000
)
# Model should not be called for short text
assert result == "Short text"
class TestGenerateSummary:
"""Tests for _generate_summary method."""
@pytest.fixture
def mock_model(self):
"""Create mock model."""
mock = Mock()
mock.invoke.return_value = Mock(content="Generated summary")
return mock
@pytest.fixture
def handler(self, mock_model):
"""Create handler with mock model."""
return FollowUpContextHandler(mock_model)
def test_returns_empty_for_empty_findings(self, handler):
"""Returns empty string for empty findings."""
result = handler._generate_summary("", "Query")
assert result == ""
def test_returns_findings_when_short_enough(self, handler):
"""Returns findings as-is when within max_length."""
result = handler._generate_summary(
"Short findings", "Query", max_length=1000
)
assert result == "Short findings"
def test_context_purpose_includes_original_query(self, handler):
"""Context purpose prompt includes original query."""
handler._generate_summary(
"Long " * 500, # Force LLM call
"Follow-up?",
original_query="Original?",
purpose="context",
)
call_args = handler.model.invoke.call_args[0][0]
assert "Original?" in call_args
def test_prompt_purpose_omits_original_query(self, handler):
"""Prompt purpose omits original query reference."""
handler._generate_summary(
"Long " * 500,
"Follow-up?",
original_query="Original?",
purpose="prompt",
)
call_args = handler.model.invoke.call_args[0][0]
# Prompt purpose uses simpler format
assert "Follow-up?" in call_args
def test_fallback_without_model(self):
"""Fallback truncation when no model available."""
handler = FollowUpContextHandler(None)
result = handler._generate_summary("A" * 1000, "Query", max_length=100)
assert len(result) <= 103 # 100 + "..."
def test_handles_llm_exception(self, handler, mock_model):
"""Handles LLM exception with truncation fallback."""
mock_model.invoke.side_effect = RuntimeError("Error")
result = handler._generate_summary("A" * 1000, "Query", max_length=100)
assert len(result) <= 103
class TestIdentifyGaps:
"""Tests for identify_gaps method."""
@pytest.fixture
def mock_model(self):
"""Create mock model."""
mock = Mock()
mock.invoke.return_value = Mock(content="Gap 1\nGap 2\nGap 3")
return mock
@pytest.fixture
def handler(self, mock_model):
"""Create handler with mock model."""
return FollowUpContextHandler(mock_model)
def test_identifies_gaps(self, handler):
"""Identifies gaps using LLM."""
data = {"formatted_findings": "Some research findings"}
result = handler.identify_gaps(data, "Follow-up question?")
assert len(result) == 3
assert "Gap 1" in result
def test_limits_to_5_gaps(self, handler, mock_model):
"""Limits gaps to 5."""
mock_model.invoke.return_value = Mock(
content="\n".join([f"Gap {i}" for i in range(10)])
)
data = {"formatted_findings": "Findings"}
result = handler.identify_gaps(data, "Question?")
assert len(result) <= 5
def test_returns_empty_when_no_findings_and_no_model(self):
"""Returns empty list when no findings and no model."""
handler = FollowUpContextHandler(None)
data = {}
result = handler.identify_gaps(data, "Question?")
assert result == []
def test_returns_empty_when_no_model(self):
"""Returns empty list when no model."""
handler = FollowUpContextHandler(None)
data = {"formatted_findings": "Findings"}
result = handler.identify_gaps(data, "Question?")
assert result == []
def test_handles_llm_exception(self, handler, mock_model):
"""Handles LLM exception gracefully."""
mock_model.invoke.side_effect = RuntimeError("Error")
data = {"formatted_findings": "Findings"}
result = handler.identify_gaps(data, "Question?")
assert result == []
class TestFormatForSettingsSnapshot:
"""Tests for format_for_settings_snapshot method."""
@pytest.fixture
def handler(self):
"""Create handler without model."""
return FollowUpContextHandler(None)
def test_returns_minimal_metadata(self, handler):
"""Returns minimal metadata dictionary."""
context = {
"parent_research_id": "research_123",
"past_findings": "Some findings",
"extra_data": "Should not be included",
}
result = handler.format_for_settings_snapshot(context)
assert "followup_metadata" in result
assert (
result["followup_metadata"]["parent_research_id"] == "research_123"
)
assert result["followup_metadata"]["is_followup"] is True
def test_has_context_true_when_findings_present(self, handler):
"""has_context is True when past_findings present."""
context = {"past_findings": "Some findings"}
result = handler.format_for_settings_snapshot(context)
assert result["followup_metadata"]["has_context"] is True
def test_has_context_false_when_no_findings(self, handler):
"""has_context is False when no past_findings."""
context = {}
result = handler.format_for_settings_snapshot(context)
assert result["followup_metadata"]["has_context"] is False
class TestGetRelevantContextForLLM:
"""Tests for get_relevant_context_for_llm method."""
@pytest.fixture
def handler(self):
"""Create handler without model."""
return FollowUpContextHandler(None)
def test_includes_original_query(self, handler):
"""Includes original query in output."""
context = {"original_query": "Original question?"}
result = handler.get_relevant_context_for_llm(context)
assert "Original question?" in result
def test_includes_follow_up_query(self, handler):
"""Includes follow-up query in output."""
context = {"follow_up_query": "Follow-up question?"}
result = handler.get_relevant_context_for_llm(context)
assert "Follow-up question?" in result
def test_includes_summary(self, handler):
"""Includes summary in output."""
context = {"summary": "Summary of findings"}
result = handler.get_relevant_context_for_llm(context)
assert "Summary of findings" in result
def test_includes_key_entities(self, handler):
"""Includes key entities in output."""
context = {"key_entities": ["Entity1", "Entity2", "Entity3"]}
result = handler.get_relevant_context_for_llm(context)
assert "Entity1" in result
def test_limits_entities_to_5(self, handler):
"""Limits entities to first 5."""
context = {"key_entities": [f"Entity{i}" for i in range(10)]}
result = handler.get_relevant_context_for_llm(context)
assert "Entity4" in result
assert "Entity5" not in result or result.count("Entity") <= 5
def test_includes_source_count(self, handler):
"""Includes source count in output."""
context = {"past_sources": [{"url": "a"}, {"url": "b"}, {"url": "c"}]}
result = handler.get_relevant_context_for_llm(context)
assert "3" in result
def test_truncates_to_max_tokens(self, handler):
"""Truncates output to approximate max_tokens."""
context = {
"original_query": "A" * 1000,
"follow_up_query": "B" * 1000,
"summary": "C" * 10000,
}
result = handler.get_relevant_context_for_llm(context, max_tokens=500)
# max_tokens * 4 = 2000 chars
assert len(result) <= 2003 # 2000 + "..."
class TestExtractMetadata:
"""Tests for _extract_metadata method."""
@pytest.fixture
def handler(self):
"""Create handler without model."""
return FollowUpContextHandler(None)
def test_extracts_strategy(self, handler):
"""Extracts strategy from research data."""
data = {"strategy": "detailed"}
result = handler._extract_metadata(data)
assert result["strategy"] == "detailed"
def test_extracts_mode(self, handler):
"""Extracts mode from research data."""
data = {"mode": "research"}
result = handler._extract_metadata(data)
assert result["mode"] == "research"
def test_extracts_created_at(self, handler):
"""Extracts created_at from research data."""
data = {"created_at": "2024-01-14"}
result = handler._extract_metadata(data)
assert result["created_at"] == "2024-01-14"
def test_extracts_research_meta(self, handler):
"""Extracts research_meta from research data."""
data = {"research_meta": {"duration": 120}}
result = handler._extract_metadata(data)
assert result["research_meta"]["duration"] == 120
def test_returns_empty_strings_for_missing_fields(self, handler):
"""Returns empty strings for missing fields."""
data = {}
result = handler._extract_metadata(data)
assert result["strategy"] == ""
assert result["mode"] == ""
assert result["created_at"] == ""
assert result["research_meta"] == {}
class TestIntegration:
"""Integration tests for complete workflows."""
@pytest.fixture
def mock_model(self):
"""Create mock model with realistic responses."""
mock = Mock()
def invoke_side_effect(prompt):
if "entities" in prompt.lower():
return Mock(content="Python\nMachine Learning\nAI")
if "summary" in prompt.lower() or "summarize" in prompt.lower():
return Mock(content="A concise summary of the research.")
if "gaps" in prompt.lower():
return Mock(content="Gap 1\nGap 2")
return Mock(content="Default response")
mock.invoke.side_effect = invoke_side_effect
return mock
@pytest.fixture
def handler(self, mock_model):
"""Create handler with mock model."""
return FollowUpContextHandler(mock_model)
def test_full_workflow(self, handler):
"""Test complete workflow from research data to context."""
research_data = {
"research_id": "research_123",
"query": "What is machine learning?",
"formatted_findings": "Machine learning is a branch of AI that enables computers to learn from data.",
"resources": [{"url": "https://example.com", "title": "ML Guide"}],
"strategy": "detailed",
"mode": "research",
}
context = handler.build_context(
research_data, "How is ML used in healthcare?"
)
# Verify context structure
assert context["parent_research_id"] == "research_123"
assert context["original_query"] == "What is machine learning?"
assert context["follow_up_query"] == "How is ML used in healthcare?"
assert len(context["past_sources"]) == 1
assert len(context["key_entities"]) > 0
def test_full_workflow_with_gaps(self, handler):
"""Test gap identification in complete workflow."""
research_data = {
"formatted_findings": "Some research findings about ML."
}
gaps = handler.identify_gaps(research_data, "What about deep learning?")
assert isinstance(gaps, list)
assert len(gaps) <= 5