# Copyright (C) 2026 Microsoft # Licensed under the MIT License """Unit tests for the streaming embed_text operation.""" from collections.abc import AsyncIterator from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import numpy as np import pytest from graphrag.callbacks.noop_workflow_callbacks import ( NoopWorkflowCallbacks, ) from graphrag.index.operations.embed_text.embed_text import embed_text from graphrag.index.operations.embed_text.run_embed_text import ( TextEmbeddingResult, ) from graphrag_storage.tables.table import Table class FakeInputTable(Table): """In-memory table that yields rows via async iteration.""" def __init__(self, rows: list[dict[str, Any]]) -> None: """Store the rows to be yielded.""" self._rows = rows def __aiter__(self) -> AsyncIterator[dict[str, Any]]: """Return an async iterator yielding each stored row.""" return self._iter() async def _iter(self) -> AsyncIterator[dict[str, Any]]: """Yield rows one at a time.""" for row in self._rows: yield dict(row) async def length(self) -> int: """Return the number of rows.""" return len(self._rows) async def has(self, row_id: str) -> bool: """Check if a row with the given ID exists.""" return any(r.get("id") == row_id for r in self._rows) async def write(self, row: dict[str, Any]) -> None: """No-op write (input table is read-only).""" async def close(self) -> None: """No-op close.""" class FakeOutputTable(Table): """Collects rows written via write() for assertion.""" def __init__(self) -> None: """Initialize empty row collection.""" self.rows: list[dict[str, Any]] = [] def __aiter__(self) -> AsyncIterator[dict[str, Any]]: """Yield collected rows.""" return self._iter() async def _iter(self) -> AsyncIterator[dict[str, Any]]: """Yield rows one at a time.""" for row in self.rows: yield row async def length(self) -> int: """Return the number of written rows.""" return len(self.rows) async def has(self, row_id: str) -> bool: """Check if a row with the given ID was written.""" return any(r.get("id") == row_id for r in self.rows) async def write(self, row: dict[str, Any]) -> None: """Append a row to the collection.""" self.rows.append(row) async def close(self) -> None: """No-op close.""" def _make_mock_vector_store(): """Create a mock vector store with create_index and load_documents.""" store = MagicMock() store.create_index = MagicMock() store.load_documents = MagicMock() return store def _make_mock_model(embedding_values: list[float]): """Create a mock model that returns fixed embeddings.""" model = MagicMock() model.tokenizer = MagicMock() return model, embedding_values def _make_embedding_result(count: int, values: list[float]) -> TextEmbeddingResult: """Build a TextEmbeddingResult with count copies of values.""" return TextEmbeddingResult(embeddings=[list(values) for _ in range(count)]) @pytest.mark.asyncio async def test_embed_text_basic(): """Verify basic embedding: rows flow through to vector store and output table.""" rows = [ {"id": "a", "text": "hello world"}, {"id": "b", "text": "foo bar"}, {"id": "c", "text": "baz qux"}, ] input_table = FakeInputTable(rows) output_table = FakeOutputTable() vector_store = _make_mock_vector_store() embedding_values = [1.0, 2.0, 3.0] with patch( "graphrag.index.operations.embed_text.embed_text.run_embed_text", new_callable=AsyncMock, ) as mock_run: mock_run.return_value = _make_embedding_result(3, embedding_values) count = await embed_text( input_table=input_table, callbacks=NoopWorkflowCallbacks(), model=MagicMock(), tokenizer=MagicMock(), embed_column="text", batch_size=10, batch_max_tokens=8191, num_threads=1, vector_store=vector_store, output_table=output_table, ) assert count == 3 assert len(output_table.rows) == 3 assert output_table.rows[0]["id"] == "a" assert output_table.rows[0]["embedding"] == embedding_values assert output_table.rows[2]["id"] == "c" vector_store.create_index.assert_called_once() vector_store.load_documents.assert_called_once() docs = vector_store.load_documents.call_args[0][0] assert len(docs) == 3 assert docs[0].id == "a" assert docs[1].id == "b" @pytest.mark.asyncio async def test_embed_text_batching(): """Verify rows are flushed in batches sized by batch_size * num_threads. With batch_size=2 and num_threads=4, each flush holds up to 8 rows (enough to produce 4 API batches that saturate the concurrency limit). 10 rows should produce 2 flushes: one of 8 rows and a final remainder of 2. """ rows = [{"id": str(i), "text": f"text {i}"} for i in range(10)] input_table = FakeInputTable(rows) vector_store = _make_mock_vector_store() with patch( "graphrag.index.operations.embed_text.embed_text.run_embed_text", new_callable=AsyncMock, ) as mock_run: mock_run.side_effect = [ _make_embedding_result(8, [1.0]), _make_embedding_result(2, [2.0]), ] count = await embed_text( input_table=input_table, callbacks=NoopWorkflowCallbacks(), model=MagicMock(), tokenizer=MagicMock(), embed_column="text", batch_size=2, batch_max_tokens=8191, num_threads=4, vector_store=vector_store, ) assert count == 10 assert mock_run.call_count == 2 assert vector_store.load_documents.call_count == 2 @pytest.mark.asyncio async def test_embed_text_pretransformed_rows(): """Verify rows pre-transformed by table layer are embedded correctly.""" rows = [ { "id": "1", "title": "Alpha", "description": "First", "combined": "Alpha:First", }, { "id": "2", "title": "Beta", "description": "Second", "combined": "Beta:Second", }, ] input_table = FakeInputTable(rows) output_table = FakeOutputTable() vector_store = _make_mock_vector_store() with patch( "graphrag.index.operations.embed_text.embed_text.run_embed_text", new_callable=AsyncMock, ) as mock_run: mock_run.return_value = _make_embedding_result(2, [0.5]) count = await embed_text( input_table=input_table, callbacks=NoopWorkflowCallbacks(), model=MagicMock(), tokenizer=MagicMock(), embed_column="combined", batch_size=10, batch_max_tokens=8191, num_threads=1, vector_store=vector_store, output_table=output_table, ) assert count == 2 texts_arg = mock_run.call_args[0][0] assert texts_arg == ["Alpha:First", "Beta:Second"] @pytest.mark.asyncio async def test_embed_text_none_values_filled(): """Verify None embed_column values are replaced with empty string.""" rows = [ {"id": "1", "text": None}, {"id": "2", "text": "real text"}, ] input_table = FakeInputTable(rows) vector_store = _make_mock_vector_store() with patch( "graphrag.index.operations.embed_text.embed_text.run_embed_text", new_callable=AsyncMock, ) as mock_run: mock_run.return_value = _make_embedding_result(2, [1.0]) count = await embed_text( input_table=input_table, callbacks=NoopWorkflowCallbacks(), model=MagicMock(), tokenizer=MagicMock(), embed_column="text", batch_size=10, batch_max_tokens=8191, num_threads=1, vector_store=vector_store, ) assert count == 2 texts_arg = mock_run.call_args[0][0] assert texts_arg == ["", "real text"] @pytest.mark.asyncio async def test_embed_text_no_output_table(): """Verify embedding works without an output table (no snapshot).""" rows = [{"id": "x", "text": "data"}] input_table = FakeInputTable(rows) vector_store = _make_mock_vector_store() with patch( "graphrag.index.operations.embed_text.embed_text.run_embed_text", new_callable=AsyncMock, ) as mock_run: mock_run.return_value = _make_embedding_result(1, [9.0]) count = await embed_text( input_table=input_table, callbacks=NoopWorkflowCallbacks(), model=MagicMock(), tokenizer=MagicMock(), embed_column="text", batch_size=10, batch_max_tokens=8191, num_threads=1, vector_store=vector_store, output_table=None, ) assert count == 1 vector_store.load_documents.assert_called_once() @pytest.mark.asyncio async def test_embed_text_empty_input(): """Verify zero rows returns zero count with no calls.""" input_table = FakeInputTable([]) vector_store = _make_mock_vector_store() with patch( "graphrag.index.operations.embed_text.embed_text.run_embed_text", new_callable=AsyncMock, ) as mock_run: count = await embed_text( input_table=input_table, callbacks=NoopWorkflowCallbacks(), model=MagicMock(), tokenizer=MagicMock(), embed_column="text", batch_size=10, batch_max_tokens=8191, num_threads=1, vector_store=vector_store, ) assert count == 0 mock_run.assert_not_called() vector_store.load_documents.assert_not_called() @pytest.mark.asyncio async def test_embed_text_numpy_array_vectors(): """Verify np.ndarray embeddings are converted to plain lists.""" rows = [ {"id": "a", "text": "hello"}, {"id": "b", "text": "world"}, ] input_table = FakeInputTable(rows) output_table = FakeOutputTable() vector_store = _make_mock_vector_store() numpy_embeddings: list[list[float] | None] = [ np.array([1.0, 2.0]).tolist(), np.array([3.0, 4.0]).tolist(), ] with patch( "graphrag.index.operations.embed_text.embed_text.run_embed_text", new_callable=AsyncMock, ) as mock_run: # Simulate run_embed_text returning np.ndarray objects at runtime # by replacing the result embeddings after construction. result = TextEmbeddingResult(embeddings=numpy_embeddings) result.embeddings = [np.array([1.0, 2.0]), np.array([3.0, 4.0])] # type: ignore[list-item] mock_run.return_value = result count = await embed_text( input_table=input_table, callbacks=NoopWorkflowCallbacks(), model=MagicMock(), tokenizer=MagicMock(), embed_column="text", batch_size=10, batch_max_tokens=8191, num_threads=1, vector_store=vector_store, output_table=output_table, ) assert count == 2 docs = vector_store.load_documents.call_args[0][0] assert docs[0].vector == [1.0, 2.0] assert docs[1].vector == [3.0, 4.0] assert type(docs[0].vector) is list assert type(docs[1].vector) is list assert output_table.rows[0]["embedding"] == [1.0, 2.0] assert type(output_table.rows[0]["embedding"]) is list @pytest.mark.asyncio async def test_embed_text_partial_none_embeddings(): """Verify rows with None embeddings are skipped in store and output.""" rows = [ {"id": "a", "text": "good"}, {"id": "b", "text": "failed"}, {"id": "c", "text": "also good"}, ] input_table = FakeInputTable(rows) output_table = FakeOutputTable() vector_store = _make_mock_vector_store() mixed_embeddings = [[1.0, 2.0], None, [3.0, 4.0]] with patch( "graphrag.index.operations.embed_text.embed_text.run_embed_text", new_callable=AsyncMock, ) as mock_run: mock_run.return_value = TextEmbeddingResult(embeddings=mixed_embeddings) count = await embed_text( input_table=input_table, callbacks=NoopWorkflowCallbacks(), model=MagicMock(), tokenizer=MagicMock(), embed_column="text", batch_size=10, batch_max_tokens=8191, num_threads=1, vector_store=vector_store, output_table=output_table, ) assert count == 3 docs = vector_store.load_documents.call_args[0][0] assert len(docs) == 2 assert docs[0].id == "a" assert docs[1].id == "c" assert len(output_table.rows) == 2 assert output_table.rows[0]["id"] == "a" assert output_table.rows[1]["id"] == "c"