Files
mnemosyne/mnemosyne/library/tests/test_pipeline.py
Robert Helewka 539d9b6c34
All checks were successful
CVE Scan & Docker Build / security-scan (pull_request) Successful in 5m24s
CVE Scan & Docker Build / build-and-push (pull_request) Successful in 2m58s
fix(tests): repair stale mock.patch targets after service refactors
Several library tests patched symbols at import paths that no longer
expose them, so they errored (AttributeError) instead of testing anything
— giving false confidence. The underlying code is correct; only the test
patch targets were stale after earlier refactors moved imports
function-local.

- test_pipeline: patch source modules (library.models.Item,
  llm_manager.models.LLMModel, library.services.parsers.DocumentParser,
  .chunker.ContentTypeChunker, .embedding_client.EmbeddingClient,
  .vision.VisionAnalyzer, .concepts.ConceptExtractor) since pipeline.py
  imports them inside methods. default_storage stays (still module-level).
- test_search_api: patch library.services.search.SearchService (the view
  imports it function-local).
- test_tasks: patch library.services.pipeline.EmbeddingPipeline (tasks.py
  imports it function-local).
- test_search_views_admin_scope: patch library.utils.neo4j_available; the
  guard moved to utils when views._all_library_uids became a thin alias.
- test_concepts: remove SampleIndexSelectionTests — _select_sample_indices
  was deleted in the document-level concept-extraction refactor (dead test).

Not addressed here: SearchAPIAuthTest / SearchAPIValidationTest return 302
instead of 401/400. Static analysis ruled out routing, middleware, and DRF
config; reproducing needs a running server (DB-backed). Flagged for sandbox
diagnosis — not a stale-patch issue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 20:12:46 -04:00

329 lines
13 KiB
Python

"""
Tests for the embedding pipeline orchestrator.
Pipeline tests mock external dependencies (Neo4j, S3, LLM APIs).
"""
from unittest.mock import MagicMock, patch
from django.test import TestCase
from library.services.pipeline import (
CHUNK_S3_KEY,
IMAGE_S3_KEY,
ORIGINAL_S3_KEY,
EmbeddingPipeline,
)
class S3KeyPatternTests(TestCase):
"""Tests for S3 key pattern formatting."""
def test_original_key_format(self):
key = ORIGINAL_S3_KEY.format(item_uid="abc123", ext="pdf")
self.assertEqual(key, "items/abc123/original.pdf")
def test_chunk_key_format(self):
key = CHUNK_S3_KEY.format(item_uid="abc123", index=5)
self.assertEqual(key, "chunks/abc123/chunk_5.txt")
def test_image_key_format(self):
key = IMAGE_S3_KEY.format(item_uid="abc123", index=2, ext="png")
self.assertEqual(key, "images/abc123/2.png")
class EmbeddingPipelineInitTests(TestCase):
"""Tests for pipeline initialization."""
def test_init_without_user(self):
pipeline = EmbeddingPipeline()
self.assertIsNone(pipeline.user)
def test_init_with_user(self):
user = MagicMock()
pipeline = EmbeddingPipeline(user=user)
self.assertEqual(pipeline.user, user)
class PipelineItemNotFoundTests(TestCase):
"""Tests for handling missing items."""
@patch("library.models.Item")
def test_process_nonexistent_item_raises(self, mock_item_cls):
mock_item_cls.nodes.get.side_effect = Exception("Not found")
pipeline = EmbeddingPipeline()
with self.assertRaises(ValueError) as ctx:
pipeline.process_item("nonexistent-uid")
self.assertIn("Item not found", str(ctx.exception))
@patch("library.models.Item")
def test_reprocess_nonexistent_item_raises(self, mock_item_cls):
mock_item_cls.nodes.get.side_effect = Exception("Not found")
pipeline = EmbeddingPipeline()
with self.assertRaises(ValueError):
pipeline.reprocess_item("nonexistent-uid")
class PipelineNoEmbeddingModelTests(TestCase):
"""Tests for handling missing system embedding model."""
@patch("llm_manager.models.LLMModel")
@patch("library.services.pipeline.default_storage")
@patch("library.services.parsers.DocumentParser")
def test_no_embedding_model_raises(self, mock_parser, mock_storage, mock_llm):
"""Pipeline raises ValueError if no system embedding model is configured."""
mock_llm.get_system_embedding_model.return_value = None
# Mock item
mock_item = MagicMock()
mock_item.uid = "test-uid"
mock_item.title = "Test"
mock_item.file_type = "txt"
mock_item.s3_key = "items/test-uid/original.txt"
mock_item.embedding_status = "pending"
mock_item.chunks.all.return_value = []
mock_item.images.all.return_value = []
with patch("library.models.Item") as mock_item_cls:
mock_item_cls.nodes.get.return_value = mock_item
# Mock S3 read
mock_storage.open.return_value.__enter__ = MagicMock(
return_value=MagicMock(read=MagicMock(return_value=b"test content"))
)
mock_storage.open.return_value.__exit__ = MagicMock(return_value=False)
pipeline = EmbeddingPipeline()
with self.assertRaises(ValueError) as ctx:
pipeline.process_item("test-uid")
self.assertIn("No system embedding model", str(ctx.exception))
class PipelineVisionPromptTests(TestCase):
"""Tests for the _get_vision_prompt helper."""
def test_returns_empty_for_no_library(self):
pipeline = EmbeddingPipeline()
result = pipeline._get_vision_prompt(None)
self.assertEqual(result, "")
@patch("library.content_types.get_library_type_config")
def test_returns_vision_prompt_from_config(self, mock_config):
mock_config.return_value = {
"vision_prompt": "Analyze this technical diagram.",
}
mock_library = MagicMock()
mock_library.library_type = "technical"
pipeline = EmbeddingPipeline()
result = pipeline._get_vision_prompt(mock_library)
self.assertEqual(result, "Analyze this technical diagram.")
mock_config.assert_called_once_with("technical")
@patch("library.content_types.get_library_type_config")
def test_returns_empty_when_no_vision_prompt_key(self, mock_config):
mock_config.return_value = {"embedding_instruction": "something"}
mock_library = MagicMock()
mock_library.library_type = "fiction"
pipeline = EmbeddingPipeline()
result = pipeline._get_vision_prompt(mock_library)
self.assertEqual(result, "")
@patch("library.content_types.get_library_type_config")
def test_returns_empty_on_exception(self, mock_config):
mock_config.side_effect = ValueError("Unknown type")
mock_library = MagicMock()
mock_library.library_type = "bogus"
pipeline = EmbeddingPipeline()
result = pipeline._get_vision_prompt(mock_library)
self.assertEqual(result, "")
class PipelineVisionStageTests(TestCase):
"""Tests for Stage 5.5 — vision analysis integration in _run_pipeline."""
def _make_mock_item(self):
"""Create a common mock Item for pipeline tests."""
item = MagicMock()
item.uid = "test-uid"
item.title = "Test Doc"
item.file_type = "pdf"
item.s3_key = "items/test-uid/original.pdf"
item.embedding_status = "pending"
item.content_hash = ""
item.chunks = MagicMock()
item.chunks.all.return_value = []
item.images = MagicMock()
item.images.all.return_value = []
return item
@patch("library.services.concepts.ConceptExtractor")
@patch("library.services.embedding_client.EmbeddingClient")
@patch("library.services.chunker.ContentTypeChunker")
@patch("library.services.parsers.DocumentParser")
@patch("llm_manager.models.LLMModel")
@patch("library.services.pipeline.default_storage")
def test_no_vision_model_marks_images_skipped(
self, mock_storage, mock_llm, mock_parser_cls,
mock_chunker_cls, mock_embed_cls, mock_concept_cls,
):
"""When no vision model is configured, images get analysis_status='skipped'."""
# Setup embedding model
mock_embed_model = MagicMock()
mock_embed_model.name = "test-embed"
mock_embed_model.vector_dimensions = None
mock_embed_model.supports_multimodal = False
mock_llm.get_system_embedding_model.return_value = mock_embed_model
mock_llm.get_system_vision_model.return_value = None
mock_llm.get_system_chat_model.return_value = None
# Setup parser — returns text + images
mock_parse_result = MagicMock()
mock_parse_result.images = [MagicMock(source_index=0, ext="png", data=b"img", width=100, height=100, source_page=0)]
mock_parse_result.text_blocks = []
mock_parser = MagicMock()
mock_parser.parse_bytes.return_value = mock_parse_result
mock_parser_cls.return_value = mock_parser
# Setup chunker — empty chunks
mock_chunk_result = MagicMock()
mock_chunk_result.chunks = []
mock_chunk_result.chunk_page_map = {}
mock_chunker = MagicMock()
mock_chunker.chunk.return_value = mock_chunk_result
mock_chunker_cls.return_value = mock_chunker
# Setup S3
mock_file = MagicMock()
mock_file.read.return_value = b"file data"
mock_storage.open.return_value.__enter__ = MagicMock(return_value=mock_file)
mock_storage.open.return_value.__exit__ = MagicMock(return_value=False)
item = self._make_mock_item()
pipeline = EmbeddingPipeline()
# Mock _store_images to return a mock image node
img_node = MagicMock()
img_node.s3_key = "images/test-uid/0.png"
with patch.object(pipeline, "_get_item_library", return_value=None), \
patch.object(pipeline, "_read_item_from_s3", return_value=b"data"), \
patch.object(pipeline, "_store_chunks", return_value=[]), \
patch.object(pipeline, "_store_images", return_value=[img_node]), \
patch.object(pipeline, "_associate_images_with_chunks"):
result = pipeline._run_pipeline(item, None)
# Image should be marked as skipped
self.assertEqual(img_node.analysis_status, "skipped")
img_node.save.assert_called()
self.assertEqual(result["images_analyzed"], 0)
@patch("library.services.vision.VisionAnalyzer")
@patch("library.services.concepts.ConceptExtractor")
@patch("library.services.embedding_client.EmbeddingClient")
@patch("library.services.chunker.ContentTypeChunker")
@patch("library.services.parsers.DocumentParser")
@patch("llm_manager.models.LLMModel")
@patch("library.services.pipeline.default_storage")
def test_vision_model_triggers_analysis(
self, mock_storage, mock_llm, mock_parser_cls,
mock_chunker_cls, mock_embed_cls, mock_concept_cls, mock_vision_cls,
):
"""When vision model is configured and images exist, analysis runs."""
# Setup models
mock_embed_model = MagicMock()
mock_embed_model.name = "test-embed"
mock_embed_model.vector_dimensions = None
mock_embed_model.supports_multimodal = False
mock_vision_model = MagicMock()
mock_llm.get_system_embedding_model.return_value = mock_embed_model
mock_llm.get_system_vision_model.return_value = mock_vision_model
mock_llm.get_system_chat_model.return_value = None
# Setup parser
mock_parse_result = MagicMock()
mock_parse_result.images = []
mock_parse_result.text_blocks = []
mock_parser = MagicMock()
mock_parser.parse_bytes.return_value = mock_parse_result
mock_parser_cls.return_value = mock_parser
# Setup chunker
mock_chunk_result = MagicMock()
mock_chunk_result.chunks = []
mock_chunk_result.chunk_page_map = {}
mock_chunker = MagicMock()
mock_chunker.chunk.return_value = mock_chunk_result
mock_chunker_cls.return_value = mock_chunker
# Setup vision analyzer
mock_analyzer = MagicMock()
mock_analyzer.analyze_images.return_value = 3
mock_vision_cls.return_value = mock_analyzer
item = self._make_mock_item()
img_nodes = [MagicMock(), MagicMock(), MagicMock()]
pipeline = EmbeddingPipeline()
with patch.object(pipeline, "_get_item_library", return_value=None), \
patch.object(pipeline, "_read_item_from_s3", return_value=b"data"), \
patch.object(pipeline, "_store_chunks", return_value=[]), \
patch.object(pipeline, "_store_images", return_value=img_nodes), \
patch.object(pipeline, "_associate_images_with_chunks"), \
patch.object(pipeline, "_get_vision_prompt", return_value="Analyze"):
result = pipeline._run_pipeline(item, None)
self.assertEqual(result["images_analyzed"], 3)
mock_vision_cls.assert_called_once_with(mock_vision_model, user=None)
mock_analyzer.analyze_images.assert_called_once()
@patch("llm_manager.models.LLMModel")
def test_no_images_skips_vision_entirely(self, mock_llm):
"""When there are no images, vision stage is a no-op regardless of model."""
mock_vision_model = MagicMock()
mock_llm.get_system_vision_model.return_value = mock_vision_model
mock_llm.get_system_embedding_model.return_value = MagicMock(
name="embed", vector_dimensions=None, supports_multimodal=False
)
mock_llm.get_system_chat_model.return_value = None
item = self._make_mock_item()
pipeline = EmbeddingPipeline()
mock_chunk_result = MagicMock()
mock_chunk_result.chunks = []
mock_chunk_result.chunk_page_map = {}
with patch.object(pipeline, "_get_item_library", return_value=None), \
patch.object(pipeline, "_read_item_from_s3", return_value=b"data"), \
patch.object(pipeline, "_store_chunks", return_value=[]), \
patch.object(pipeline, "_store_images", return_value=[]), \
patch.object(pipeline, "_associate_images_with_chunks"), \
patch("library.services.parsers.DocumentParser") as mock_parser_cls, \
patch("library.services.chunker.ContentTypeChunker") as mock_chunker_cls, \
patch("library.services.embedding_client.EmbeddingClient"), \
patch("library.services.vision.VisionAnalyzer") as mock_vision_cls:
mock_parser = MagicMock()
mock_parser.parse_bytes.return_value = MagicMock(images=[], text_blocks=[])
mock_parser_cls.return_value = mock_parser
mock_chunker = MagicMock()
mock_chunker.chunk.return_value = mock_chunk_result
mock_chunker_cls.return_value = mock_chunker
result = pipeline._run_pipeline(item, None)
# VisionAnalyzer should never be instantiated
mock_vision_cls.assert_not_called()
self.assertEqual(result["images_analyzed"], 0)