Feat: Add knowledge compilation workflows (#16515)

## Summary
- Add knowledge compilation template APIs, services, and builtin
template seed data
- Add advanced knowledge compile structure/artifact/RAPTOR workflow
support
- Update parsing, dataset/document APIs, and supporting services for
compilation workflows
This commit is contained in:
Kevin Hu
2026-07-02 23:22:07 +08:00
committed by GitHub
parent 7d64a78f83
commit 62f94cd59b
57 changed files with 14587 additions and 3094 deletions

View File

@@ -120,7 +120,16 @@ class FileTestModel(BaseTestModel):
db_table = "file"
_TABLES = [FileCommitTestModel, FileCommitItemTestModel, FileTestModel]
class UserTestModel(BaseTestModel):
id = CharField(max_length=32, primary_key=True)
nickname = CharField(max_length=100, null=False, index=True)
email = CharField(max_length=255, null=False)
class Meta:
db_table = "user"
_TABLES = [FileCommitTestModel, FileCommitItemTestModel, FileTestModel, UserTestModel]
sqlite_db.create_tables(_TABLES)
@@ -241,6 +250,7 @@ def _load_module(monkeypatch):
db_models_mod.FileCommit = FileCommitTestModel
db_models_mod.FileCommitItem = FileCommitItemTestModel
db_models_mod.File = FileTestModel
db_models_mod.User = UserTestModel
db_models_mod.DataBaseModel = BaseTestModel
monkeypatch.setitem(sys.modules, "api.db.db_models", db_models_mod)

View File

@@ -71,7 +71,7 @@
# "rag.utils.base64_image",
# "rag.prompts.generator",
# "rag.raptor",
# "rag.advanced_rag.knowlege_compile.raptor",
# "rag.app",
# "rag.graphrag.utils",
# ]

View File

@@ -1,438 +0,0 @@
#
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Unit tests for ChunkPostProcessor module.
Mock strategy: the LLM prompt functions (``keyword_extraction``, ``question_proposal``,
``gen_metadata``, ``content_tagging``) are mocked since they make actual LLM API
calls. ``get_llm_cache`` / ``set_llm_cache`` run as real code, so cache
population and retrieval are exercised. ``rag_tokenizer`` is mocked because
it requires NLTK data in the test environment.
"""
import pytest
from unittest.mock import MagicMock, patch
from rag.svr.task_executor_refactor.chunk_post_processor import (
extract_keywords,
generate_questions,
generate_metadata,
apply_tags,
count_with_key,
build_metadata_config,
)
from test.unit_test.rag.svr.task_executor_refactor.conftest import (
make_task_context,
MockChatModel,
)
class _BasePostProcessorTest:
"""Shared helpers for post-processor test classes."""
@staticmethod
def _mock_llm_binding(chat_model_cls=MockChatModel):
"""Patch model config lookup + LLMBundle to return a MockChatModel."""
p1 = patch("rag.svr.task_executor_refactor.chunk_post_processor.get_model_config_from_provider_instance",
return_value=MagicMock())
p2 = patch("rag.svr.task_executor_refactor.chunk_post_processor.LLMBundle",
return_value=chat_model_cls())
return p1, p2
@staticmethod
def _patch_prompt_func(func_path: str, return_value):
"""Patch a prompt-level LLM function (the actual API call)."""
return patch(func_path, return_value=return_value)
class TestExtractKeywords(_BasePostProcessorTest):
"""Tests for extract_keywords function."""
@pytest.mark.asyncio
async def test_extract_keywords_success(self):
"""Test successful keyword extraction — cache miss → LLM prompt runs."""
ctx = make_task_context(parser_config={"auto_keywords": 5})
docs = [
{"content_with_weight": "This is test content one"},
{"content_with_weight": "This is test content two"},
]
p1, p2 = self._mock_llm_binding()
p3 = patch("rag.svr.task_executor_refactor.chunk_post_processor.get_llm_cache",
return_value=None) # cache miss
p4 = patch("rag.svr.task_executor_refactor.chunk_post_processor.set_llm_cache") # Redis stub
p5 = self._patch_prompt_func(
"rag.svr.task_executor_refactor.chunk_post_processor.keyword_extraction",
return_value="keyword1, keyword2",
)
p6 = patch("rag.svr.task_executor_refactor.chunk_post_processor.rag_tokenizer")
with p1, p2, p3, p4, p5, p6 as mock_tok:
mock_tok.tokenize.return_value = "keyword1 keyword2"
await extract_keywords(docs, ctx)
assert "important_kwd" in docs[0]
assert "important_tks" in docs[0]
@pytest.mark.asyncio
async def test_extract_keywords_canceled(self):
"""Test keyword extraction when task is canceled."""
ctx = make_task_context(parser_config={"auto_keywords": 5},
has_canceled_func=MagicMock(return_value=True))
docs = [{"content_with_weight": "This is test content"}]
p1, p2 = self._mock_llm_binding()
with p1, p2:
await extract_keywords(docs, ctx)
assert "important_kwd" not in docs[0]
@pytest.mark.asyncio
async def test_extract_keywords_empty_docs(self):
"""Test keyword extraction with empty docs list."""
ctx = make_task_context(parser_config={"auto_keywords": 5})
docs = []
p1, p2 = self._mock_llm_binding()
with p1, p2:
await extract_keywords(docs, ctx)
ctx.progress_cb.assert_called()
class TestGenerateQuestions(_BasePostProcessorTest):
"""Tests for generate_questions function."""
@pytest.mark.asyncio
async def test_generate_questions_success(self):
"""Test successful question generation — cache miss → LLM prompt runs."""
ctx = make_task_context(parser_config={"auto_questions": 3})
docs = [{"content_with_weight": "This is test content one"}]
p1, p2 = self._mock_llm_binding()
p3 = patch("rag.svr.task_executor_refactor.chunk_post_processor.get_llm_cache",
return_value=None)
p4 = patch("rag.svr.task_executor_refactor.chunk_post_processor.set_llm_cache")
p5 = self._patch_prompt_func(
"rag.svr.task_executor_refactor.chunk_post_processor.question_proposal",
return_value="Question 1\nQuestion 2",
)
p6 = patch("rag.svr.task_executor_refactor.chunk_post_processor.rag_tokenizer")
with p1, p2, p3, p4, p5, p6 as mock_tok:
mock_tok.tokenize.return_value = "Question 1 Question 2"
await generate_questions(docs, ctx)
assert "question_kwd" in docs[0]
assert "question_tks" in docs[0]
@pytest.mark.asyncio
async def test_generate_questions_canceled(self):
"""Test question generation when task is canceled."""
ctx = make_task_context(parser_config={"auto_questions": 3},
has_canceled_func=MagicMock(return_value=True))
docs = [{"content_with_weight": "This is test content"}]
p1, p2 = self._mock_llm_binding()
with p1, p2:
await generate_questions(docs, ctx)
assert "question_kwd" not in docs[0]
class TestGenerateMetadata(_BasePostProcessorTest):
"""Tests for generate_metadata function."""
@pytest.mark.asyncio
async def test_generate_metadata_success(self):
"""Test successful metadata generation — cache miss → LLM prompt runs."""
ctx = make_task_context(
parser_config={
"enable_metadata": True,
"metadata": [{"key": "category", "type": "string"}],
"built_in_metadata": [{"key": "update_time", "type": "time"}],
},
)
docs = [{"content_with_weight": "This is test content"}]
p1, p2 = self._mock_llm_binding()
p3 = patch("rag.svr.task_executor_refactor.chunk_post_processor.get_llm_cache",
return_value=None)
p4 = patch("rag.svr.task_executor_refactor.chunk_post_processor.set_llm_cache")
p5 = patch("rag.svr.task_executor_refactor.chunk_post_processor.DocMetadataService")
with p1, p2, p3, p4, p5 as mock_meta:
mock_meta.get_document_metadata.return_value = {}
mock_meta.update_document_metadata = MagicMock()
await generate_metadata(docs, ctx)
mock_meta.update_document_metadata.assert_called_once()
@pytest.mark.asyncio
async def test_generate_metadata_with_write_interceptor(self):
"""Test metadata generation with write interceptor."""
ctx = make_task_context(
parser_config={
"enable_metadata": True,
"metadata": [{"key": "category", "type": "string"}],
"built_in_metadata": [{"key": "update_time", "type": "time"}],
},
write_interceptor=MagicMock(),
)
docs = [{"content_with_weight": "This is test content"}]
p1, p2 = self._mock_llm_binding()
p3 = patch("rag.svr.task_executor_refactor.chunk_post_processor.get_llm_cache",
return_value=None)
p4 = patch("rag.svr.task_executor_refactor.chunk_post_processor.set_llm_cache")
p5 = patch("rag.svr.task_executor_refactor.chunk_post_processor.DocMetadataService")
with p1, p2, p3, p4, p5:
await generate_metadata(docs, ctx)
ctx.write_interceptor.intercept.assert_called_once_with(
"DocMetadataService.update_document_metadata"
)
@pytest.mark.asyncio
async def test_generate_metadata_empty_config_does_not_crash(self):
"""Empty parser_config — no metadata configured — should not crash."""
ctx = make_task_context(parser_config={})
docs = [{"content_with_weight": "test"}]
p1, p2 = self._mock_llm_binding()
p3 = patch("rag.svr.task_executor_refactor.chunk_post_processor.DocMetadataService")
with p1, p2, p3:
await generate_metadata(docs, ctx) # no exception = pass
@pytest.mark.asyncio
async def test_generate_metadata_enum_none_accepted(self):
"""enum: None in metadata — treated as absent, should not crash."""
ctx = make_task_context(
parser_config={
"enable_metadata": True,
"metadata": [{"key": "format", "type": "string", "enum": None}],
},
)
docs = [{"content_with_weight": "test"}]
p1, p2 = self._mock_llm_binding()
p3 = patch("rag.svr.task_executor_refactor.chunk_post_processor.get_llm_cache",
return_value=None)
p4 = patch("rag.svr.task_executor_refactor.chunk_post_processor.set_llm_cache")
p5 = patch("rag.svr.task_executor_refactor.chunk_post_processor.DocMetadataService")
with p1, p2, p3, p4, p5:
await generate_metadata(docs, ctx) # no exception = pass
@pytest.mark.asyncio
async def test_generate_metadata_description_none_accepted(self):
"""description: None in metadata — should not crash."""
ctx = make_task_context(
parser_config={
"enable_metadata": True,
"metadata": [{"key": "test", "type": "string", "description": None}],
},
)
docs = [{"content_with_weight": "test"}]
p1, p2 = self._mock_llm_binding()
p3 = patch("rag.svr.task_executor_refactor.chunk_post_processor.get_llm_cache",
return_value=None)
p4 = patch("rag.svr.task_executor_refactor.chunk_post_processor.set_llm_cache")
p5 = patch("rag.svr.task_executor_refactor.chunk_post_processor.DocMetadataService")
with p1, p2, p3, p4, p5:
await generate_metadata(docs, ctx) # no exception = pass
@pytest.mark.asyncio
async def test_generate_metadata_built_in_with_enum_none(self):
"""built_in_metadata with enum: None — should not crash."""
ctx = make_task_context(
parser_config={
"enable_metadata": True,
"built_in_metadata": [
{"key": "update_time", "type": "time", "description": None, "enum": None},
],
},
)
docs = [{"content_with_weight": "test"}]
p1, p2 = self._mock_llm_binding()
p3 = patch("rag.svr.task_executor_refactor.chunk_post_processor.get_llm_cache",
return_value=None)
p4 = patch("rag.svr.task_executor_refactor.chunk_post_processor.set_llm_cache")
p5 = patch("rag.svr.task_executor_refactor.chunk_post_processor.DocMetadataService")
with p1, p2, p3, p4, p5:
await generate_metadata(docs, ctx) # no exception = pass
class TestApplyTags(_BasePostProcessorTest):
"""Tests for apply_tags function."""
@pytest.mark.asyncio
async def test_apply_tags_success(self):
"""Test successful tag application with tag cache miss."""
ctx = make_task_context(
kb_parser_config={"tag_kb_ids": ["kb_1"], "topn_tags": 3},
)
docs = [{"content_with_weight": "This is test content"}]
p1, p2 = self._mock_llm_binding()
p3 = patch("rag.svr.task_executor_refactor.chunk_post_processor.settings")
p4 = patch("rag.svr.task_executor_refactor.chunk_post_processor.get_llm_cache",
return_value='{"tag1": 1}') # cache hit → skip LLM
p5 = patch("rag.svr.task_executor_refactor.chunk_post_processor.set_llm_cache")
p6 = patch("rag.svr.task_executor_refactor.chunk_post_processor.get_tags_from_cache",
return_value=None)
p7 = patch("rag.svr.task_executor_refactor.chunk_post_processor.set_tags_to_cache")
with p1, p2, p3 as mock_settings, p4, p5, p6 as mock_get_tags, p7 as mock_set_tags:
mock_settings.retriever.all_tags_in_portion.return_value = {"tag1": 10, "tag2": 5}
mock_settings.retriever.tag_content.return_value = True
await apply_tags(docs, ctx)
assert len(docs) == 1
mock_get_tags.assert_called_once()
mock_set_tags.assert_called_once()
@pytest.mark.asyncio
async def test_apply_tags_canceled(self):
"""Test tag application when task is canceled."""
ctx = make_task_context(
kb_parser_config={"tag_kb_ids": ["kb_1"], "topn_tags": 3},
has_canceled_func=MagicMock(return_value=True),
)
docs = [{"content_with_weight": "This is test content"}]
p1, p2 = self._mock_llm_binding()
p3 = patch("rag.svr.task_executor_refactor.chunk_post_processor.settings")
p4 = patch("rag.svr.task_executor_refactor.chunk_post_processor.get_tags_from_cache",
return_value=None)
p5 = patch("rag.svr.task_executor_refactor.chunk_post_processor.set_tags_to_cache")
with p1, p2, p3 as mock_settings, p4, p5:
mock_settings.retriever.all_tags_in_portion.return_value = {"tag1": 10}
await apply_tags(docs, ctx)
@pytest.mark.asyncio
async def test_apply_tags_tag_cache_miss(self):
"""Test apply_tags when get_tags_from_cache returns None (cache miss)."""
ctx = make_task_context(
kb_parser_config={"tag_kb_ids": ["kb_1"], "topn_tags": 3},
)
docs = [{"content_with_weight": "This is test content"}]
p1, p2 = self._mock_llm_binding()
p3 = patch("rag.svr.task_executor_refactor.chunk_post_processor.settings")
p4 = patch("rag.svr.task_executor_refactor.chunk_post_processor.get_llm_cache",
return_value='{"tag1": 1}') # cache hit → skip LLM
p5 = patch("rag.svr.task_executor_refactor.chunk_post_processor.set_llm_cache")
p6 = patch("rag.svr.task_executor_refactor.chunk_post_processor.get_tags_from_cache",
return_value=None)
p7 = patch("rag.svr.task_executor_refactor.chunk_post_processor.set_tags_to_cache")
with p1, p2, p3 as mock_settings, p4, p5, p6 as mock_get_tags, p7 as mock_set_tags:
mock_settings.retriever.all_tags_in_portion.return_value = {"tag1": 10, "tag2": 5}
mock_settings.retriever.tag_content.return_value = True
await apply_tags(docs, ctx)
mock_get_tags.assert_called_once_with(["kb_1"])
mock_set_tags.assert_called_once()
mock_settings.retriever.all_tags_in_portion.assert_called_once()
@pytest.mark.asyncio
async def test_apply_tags_tag_cache_hit(self):
"""Test apply_tags when get_tags_from_cache returns valid data (cache hit)."""
ctx = make_task_context(
kb_parser_config={"tag_kb_ids": ["kb_1"], "topn_tags": 3},
)
docs = [{"content_with_weight": "This is test content"}]
p1, p2 = self._mock_llm_binding()
p3 = patch("rag.svr.task_executor_refactor.chunk_post_processor.settings")
p4 = patch("rag.svr.task_executor_refactor.chunk_post_processor.get_llm_cache",
return_value='{"tag1": 1}') # cache hit → skip LLM
p5 = patch("rag.svr.task_executor_refactor.chunk_post_processor.set_llm_cache")
p6 = patch("rag.svr.task_executor_refactor.chunk_post_processor.get_tags_from_cache",
return_value='{"cached_tag": 10}')
p7 = patch("rag.svr.task_executor_refactor.chunk_post_processor.set_tags_to_cache")
with p1, p2, p3 as mock_settings, p4, p5, p6 as mock_get_tags, p7 as mock_set_tags:
mock_settings.retriever.tag_content.return_value = True
await apply_tags(docs, ctx)
mock_get_tags.assert_called_once_with(["kb_1"])
mock_set_tags.assert_not_called()
mock_settings.retriever.all_tags_in_portion.assert_not_called()
class TestCountWithKey:
"""Tests for count_with_key function."""
def test_count_with_key_all_have_key(self):
docs = [{"tag": 1}, {"tag": 2}, {"tag": 3}]
assert count_with_key(docs, "tag") == 3
def test_count_with_key_some_have_key(self):
docs = [{"tag": 1}, {"other": 2}, {"tag": 3}]
assert count_with_key(docs, "tag") == 2
def test_count_with_key_none_have_key(self):
docs = [{"other": 1}, {"other": 2}]
assert count_with_key(docs, "tag") == 0
def test_count_with_key_empty_docs(self):
assert count_with_key([], "tag") == 0
def test_count_with_key_falsy_value(self):
docs = [{"tag": 0}, {"tag": ""}, {"tag": None}]
assert count_with_key(docs, "tag") == 0
def test_count_with_key_truthy_value(self):
docs = [{"tag": 1}, {"tag": "value"}, {"tag": [1, 2]}]
assert count_with_key(docs, "tag") == 3
class TestBuildMetadataConfig:
"""Tests for build_metadata_config function."""
def test_dict_without_properties_returns_schema(self):
parser_config = {"metadata": {"type": "object"}, "built_in_metadata": []}
assert build_metadata_config(parser_config) == {"type": "object", "properties": {}}
def test_dict_with_properties_and_built_in(self):
parser_config = {
"metadata": {"type": "object", "properties": {"a": {"type": "string"}}},
"built_in_metadata": [{"key": "author", "description": "Author name", "enum": ["alice", "bob"]}],
}
result = build_metadata_config(parser_config)
assert "a" in result["properties"]
assert "author" in result["properties"]
def test_dict_with_properties_no_built_in(self):
parser_config = {
"metadata": {"type": "object", "properties": {"a": {"type": "string"}}},
"built_in_metadata": [],
}
result = build_metadata_config(parser_config)
assert result == {"type": "object", "properties": {"a": {"type": "string"}}}
def test_list_with_built_in(self):
parser_config = {
"metadata": [{"key": "category"}],
"built_in_metadata": [{"key": "author"}],
}
assert build_metadata_config(parser_config) == [{"key": "category"}, {"key": "author"}]
def test_list_without_built_in(self):
parser_config = {"metadata": [{"key": "category"}], "built_in_metadata": []}
assert build_metadata_config(parser_config) == [{"key": "category"}]
def test_other_type_with_built_in(self):
parser_config = {"metadata": [], "built_in_metadata": [{"key": "author"}]}
assert build_metadata_config(parser_config) == [{"key": "author"}]
def test_idempotent_same_input(self):
parser_config = {
"metadata": [{"key": "category"}],
"built_in_metadata": [{"key": "author"}],
}
assert build_metadata_config(parser_config) == build_metadata_config(parser_config)
def test_missing_metadata_key(self):
assert build_metadata_config({"built_in_metadata": []}) == []
def test_metadata_is_none(self):
"""When metadata is None, built_in_metadata alone is returned."""
parser_config = {"metadata": None, "built_in_metadata": [{"key": "author"}]}
result = build_metadata_config(parser_config)
assert result == [{"key": "author"}]

View File

@@ -1,318 +0,0 @@
#
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Unit tests for ChunkService module.
Note: After refactoring, some functionality has been moved to:
- chunk_builder.py: Parser factory, run_chunking, extract_outline
- chunk_post_processor.py: Keyword extraction, question generation, metadata, tagging
This test file now focuses on ChunkService-specific functionality:
- build_chunks orchestration
- _prepare_docs_and_upload
- insert_chunks and related methods
"""
import pytest
from unittest.mock import MagicMock, patch, AsyncMock
from rag.svr.task_executor_refactor.chunk_service import ChunkService
from test.unit_test.rag.svr.task_executor_refactor.conftest import make_task_context
class TestChunkServiceInit:
"""Tests for ChunkService initialization."""
def test_init_stores_task_context(self):
"""Test that task context is stored."""
ctx = MagicMock()
service = ChunkService(ctx=ctx)
assert service._task_context is ctx
class TestChunkServiceBuildChunks:
"""Tests for build_chunks method."""
@pytest.mark.asyncio
async def test_build_chunks_file_size_exceeded(self):
"""Test build_chunks returns empty list when file size exceeds limit."""
ctx = make_task_context(size=1000000000) # Very large size
service = ChunkService(ctx=ctx)
with patch("rag.svr.task_executor_refactor.chunk_service.settings") as mock_settings:
mock_settings.DOC_MAXIMUM_SIZE = 1000 # Small limit
mock_rec_ctx = MagicMock()
ctx.recording_context = mock_rec_ctx
result = await service.build_chunks(b"test binary")
assert result == []
mock_rec_ctx.record.assert_any_call("file_size_exceeded", True)
@pytest.mark.asyncio
async def test_build_chunks_file_size_ok(self):
"""Test build_chunks proceeds when file size is within limit."""
ctx = make_task_context(size=1000)
service = ChunkService(ctx=ctx)
with patch("rag.svr.task_executor_refactor.chunk_service.settings") as mock_settings:
mock_settings.DOC_MAXIMUM_SIZE = 10000000 # Large limit
mock_rec_ctx = MagicMock()
ctx.recording_context = mock_rec_ctx
with patch("rag.svr.task_executor_refactor.chunk_service.get_parser") as mock_get_parser:
mock_parser = MagicMock()
mock_get_parser.return_value = mock_parser
with patch("rag.svr.task_executor_refactor.chunk_service.run_chunking", new_callable=AsyncMock) as mock_run_chunking:
mock_run_chunking.return_value = [{"content_with_weight": "test"}]
with patch("rag.svr.task_executor_refactor.chunk_service.extract_outline", new_callable=AsyncMock):
with patch.object(service, '_prepare_docs_and_upload', new_callable=AsyncMock) as mock_prepare:
mock_prepare.return_value = [{"id": "chunk_1", "content_with_weight": "test"}]
await service.build_chunks(b"test binary")
mock_rec_ctx.record.assert_any_call("file_size_exceeded", False)
mock_rec_ctx.record.assert_any_call("parser_id", "naive")
mock_get_parser.assert_called_once_with("naive")
@pytest.mark.asyncio
@pytest.mark.parametrize("task_kwargs,func_path,func_name", [
({"parser_config": {"auto_keywords": 5}}, "extract_keywords", "extract_keywords"),
({"parser_config": {"auto_questions": 3}}, "generate_questions", "generate_questions"),
({"kb_parser_config": {"tag_kb_ids": ["kb_1"]}}, "apply_tags", "apply_tags"),
({"parser_config": {"enable_metadata": True, "metadata": [{"name": "category", "type": "string"}]}},
"generate_metadata", "generate_metadata"),
])
async def test_build_chunks_with_post_processing(self, task_kwargs, func_path, func_name):
"""Test build_chunks triggers post-processing when configured."""
ctx = make_task_context(**task_kwargs)
service = ChunkService(ctx=ctx)
mock_rec_ctx = MagicMock()
ctx.recording_context = mock_rec_ctx
with patch("rag.svr.task_executor_refactor.chunk_service.settings") as mock_settings, \
patch("rag.svr.task_executor_refactor.chunk_service.get_parser") as mock_get_parser, \
patch("rag.svr.task_executor_refactor.chunk_service.run_chunking", new_callable=AsyncMock) as mock_run_chunking, \
patch("rag.svr.task_executor_refactor.chunk_service.extract_outline", new_callable=AsyncMock), \
patch.object(service, '_prepare_docs_and_upload', new_callable=AsyncMock) as mock_prepare, \
patch(f"rag.svr.task_executor_refactor.chunk_service.{func_path}", new_callable=AsyncMock) as mock_fn:
mock_settings.DOC_MAXIMUM_SIZE = 10000000
mock_get_parser.return_value = MagicMock()
mock_run_chunking.return_value = []
mock_prepare.return_value = [{"id": "chunk_1", "content_with_weight": "test"}]
await service.build_chunks(b"test binary")
mock_fn.assert_called_once()
class TestChunkServicePrepareDocsAndUpload:
"""Tests for _prepare_docs_and_upload method."""
@pytest.mark.asyncio
async def test_prepare_docs_and_upload_basic(self):
"""Test basic document preparation."""
ctx = make_task_context()
service = ChunkService(ctx=ctx)
cks = [{"content_with_weight": "test chunk"}]
with patch("rag.svr.task_executor_refactor.chunk_service.settings") as mock_settings:
mock_settings.STORAGE_IMPL = MagicMock()
mock_settings.STORAGE_IMPL.put = MagicMock()
with patch("rag.svr.task_executor_refactor.chunk_service.image2id", new_callable=AsyncMock):
docs = await service._prepare_docs_and_upload(cks)
assert len(docs) == 1
assert docs[0]["doc_id"] == "doc_1"
assert docs[0]["kb_id"] == "kb_1"
@pytest.mark.asyncio
async def test_prepare_docs_and_upload_with_pagerank(self):
"""Test document preparation with pagerank."""
ctx = make_task_context()
ctx.pagerank = 5
service = ChunkService(ctx=ctx)
cks = [{"content_with_weight": "test chunk"}]
with patch("rag.svr.task_executor_refactor.chunk_service.settings") as mock_settings:
mock_settings.STORAGE_IMPL = MagicMock()
with patch("rag.svr.task_executor_refactor.chunk_service.image2id", new_callable=AsyncMock):
docs = await service._prepare_docs_and_upload(cks)
assert docs[0].get("pagerank_fea") == 5
class TestChunkServiceInsertChunks:
"""Tests for insert_chunks method."""
@pytest.mark.asyncio
async def test_insert_chunks_success(self):
"""Test successful chunk insertion."""
ctx = make_task_context()
service = ChunkService(ctx=ctx)
chunks = [
{"id": "chunk_1", "content_with_weight": "test1"},
{"id": "chunk_2", "content_with_weight": "test2"},
]
with patch("rag.svr.task_executor_refactor.chunk_service.settings") as mock_settings:
mock_settings.DOC_BULK_SIZE = 100
mock_settings.docStoreConn = MagicMock()
mock_settings.docStoreConn.insert = MagicMock(return_value=None)
with patch("rag.svr.task_executor_refactor.chunk_service.search.index_name") as mock_index:
mock_index.return_value = "test_index"
with patch("rag.svr.task_executor_refactor.chunk_service.thread_pool_exec") as mock_thread:
mock_thread.return_value = None
with patch("rag.svr.task_executor_refactor.chunk_service.TaskService") as mock_task:
mock_task.update_chunk_ids = MagicMock()
result = await service.insert_chunks("task_1", "tenant_1", "kb_1", chunks)
assert result is True
@pytest.mark.asyncio
async def test_insert_chunks_canceled(self):
"""Test chunk insertion when task is canceled."""
ctx = make_task_context()
ctx.has_canceled_func = MagicMock(return_value=True)
service = ChunkService(ctx=ctx)
chunks = [{"id": "chunk_1", "content_with_weight": "test1"}]
with patch("rag.svr.task_executor_refactor.chunk_service.settings") as mock_settings:
mock_settings.DOC_BULK_SIZE = 100
mock_settings.docStoreConn = MagicMock()
mock_settings.docStoreConn.insert = MagicMock(return_value=None)
with patch("rag.svr.task_executor_refactor.chunk_service.search.index_name") as mock_index:
mock_index.return_value = "test_index"
with patch("rag.svr.task_executor_refactor.chunk_service.thread_pool_exec") as mock_thread:
mock_thread.return_value = None
result = await service.insert_chunks("task_1", "tenant_1", "kb_1", chunks)
assert result is False
ctx.progress_cb.assert_called_with(-1, msg="Task has been canceled.")
@pytest.mark.asyncio
async def test_insert_chunks_doc_store_error(self):
"""Test chunk insertion when doc store returns error."""
ctx = make_task_context()
service = ChunkService(ctx=ctx)
chunks = [{"id": "chunk_1", "content_with_weight": "test1"}]
with patch("rag.svr.task_executor_refactor.chunk_service.settings") as mock_settings:
mock_settings.DOC_BULK_SIZE = 100
mock_settings.docStoreConn = MagicMock()
mock_settings.docStoreConn.insert = MagicMock(return_value="Error message")
with patch("rag.svr.task_executor_refactor.chunk_service.search.index_name") as mock_index:
mock_index.return_value = "test_index"
with patch("rag.svr.task_executor_refactor.chunk_service.thread_pool_exec") as mock_thread:
mock_thread.return_value = "Error"
with pytest.raises(Exception, match="Insert chunk error"):
await service.insert_chunks("task_1", "tenant_1", "kb_1", chunks)
class TestChunkServiceCreateMotherChunks:
"""Tests for _create_mother_chunks class method."""
def test_create_mother_chunks_with_mom_field(self):
"""Test creating mother chunks from mom field."""
chunks = [
{"id": "chunk_1", "mom": "Summary text 1", "content_with_weight": "test1"},
]
mothers = ChunkService._create_mother_chunks(chunks)
assert len(mothers) == 1
assert mothers[0]["content_with_weight"] == "Summary text 1"
assert mothers[0]["available_int"] == 0
def test_create_mother_chunks_with_mom_with_weight_field(self):
"""Test creating mother chunks from mom_with_weight field."""
chunks = [
{"id": "chunk_1", "mom_with_weight": "Summary text 2", "content_with_weight": "test1"},
]
mothers = ChunkService._create_mother_chunks(chunks)
assert len(mothers) == 1
assert mothers[0]["content_with_weight"] == "Summary text 2"
def test_create_mother_chunks_no_mom_field(self):
"""Test creating mother chunks when no mom field present."""
chunks = [
{"id": "chunk_1", "content_with_weight": "test1"},
]
mothers = ChunkService._create_mother_chunks(chunks)
assert len(mothers) == 0
def test_create_mother_chunks_empty_mom(self):
"""Test creating mother chunks with empty mom field."""
chunks = [
{"id": "chunk_1", "mom": "", "content_with_weight": "test1"},
]
mothers = ChunkService._create_mother_chunks(chunks)
assert len(mothers) == 0
def test_create_mother_chunks_deduplicates_ids(self):
"""Test that mother chunks deduplicate by ID."""
chunks = [
{"id": "chunk_1", "mom": "Same summary", "content_with_weight": "test1"},
{"id": "chunk_2", "mom": "Same summary", "content_with_weight": "test2"},
]
mothers = ChunkService._create_mother_chunks(chunks)
assert len(mothers) == 1
def test_create_mother_chunks_filters_fields(self):
"""Test that mother chunks only keep allowed fields."""
chunks = [
{"id": "chunk_1", "mom": "Summary", "extra_field": "should be removed", "content_with_weight": "test1"},
]
mothers = ChunkService._create_mother_chunks(chunks)
assert "extra_field" not in mothers[0]
assert "id" in mothers[0]
assert "content_with_weight" in mothers[0]

View File

@@ -1,316 +0,0 @@
#
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Unit tests for TaskHandler module.
Mock strategy: external boundaries (LLMBundle, model config services, settings)
are mocked so that ``handle()`` and ``_bind_embedding_model`` execute their
real logic. Heavy orchestration methods (``_run_standard_chunking``,
``_run_raptor``, ``_run_graphrag``) are mocked since they are tested
exhaustively in the integration test suite.
Stable pure helpers (_build_toc) are tested directly.
"""
import pytest
from unittest.mock import MagicMock, AsyncMock, patch
from rag.svr.task_executor_refactor.task_handler import TaskHandler
# Reuse shared helpers from conftest
from test.unit_test.rag.svr.task_executor_refactor.conftest import (
patch_embedding_binding,
create_mock_settings,
make_task_context,
)
class TestTaskHandlerHandleTask:
"""Tests for the public handle_task() entry point."""
@pytest.mark.asyncio
async def test_handle_task_calls_handle(self):
"""Test handle_task delegates to handle()."""
ctx = MagicMock()
ctx.id = "task_1"
ctx.tenant_id = "tenant_1"
ctx.kb_id = "kb_1"
ctx.doc_id = "doc_1"
ctx.has_canceled_func = MagicMock(return_value=False)
handler = TaskHandler(ctx=ctx)
handler.handle = AsyncMock()
await handler.handle_task()
handler.handle.assert_called_once()
@pytest.mark.asyncio
async def test_handle_task_cleanup_on_cancel(self):
"""Test handle_task cleans up docStore when canceled."""
from common import settings
mock_doc_store = MagicMock()
mock_doc_store.index_exist = MagicMock(return_value=True)
mock_doc_store.delete = MagicMock(return_value=None)
orig = settings.docStoreConn
settings.docStoreConn = mock_doc_store
try:
ctx = MagicMock()
ctx.id = "task_1"
ctx.tenant_id = "tenant_1"
ctx.kb_id = "kb_1"
ctx.doc_id = "doc_1"
ctx.has_canceled_func = MagicMock(return_value=True)
ctx.recording_context = MagicMock()
handler = TaskHandler(ctx=ctx)
handler.handle = AsyncMock(side_effect=Exception("test error"))
with pytest.raises(Exception, match="test error"):
await handler.handle_task()
mock_doc_store.delete.assert_called()
finally:
settings.docStoreConn = orig
@pytest.mark.asyncio
async def test_handle_task_cleanup_skips_when_index_missing(self):
"""Cancel cleanup should not call delete when the index doesn't exist."""
from common import settings
mock_doc_store = MagicMock()
mock_doc_store.index_exist = MagicMock(return_value=False)
mock_doc_store.delete = MagicMock()
orig = settings.docStoreConn
settings.docStoreConn = mock_doc_store
try:
ctx = MagicMock()
ctx.id = "task_1"
ctx.tenant_id = "tenant_1"
ctx.kb_id = "kb_1"
ctx.doc_id = "doc_1"
ctx.has_canceled_func = MagicMock(return_value=True)
ctx.recording_context = MagicMock()
handler = TaskHandler(ctx=ctx)
handler.handle = AsyncMock(side_effect=Exception("test error"))
with pytest.raises(Exception, match="test error"):
await handler.handle_task()
mock_doc_store.delete.assert_not_called()
finally:
settings.docStoreConn = orig
class TestTaskHandlerHandle:
"""Tests for the public handle() method.
External boundaries (LLMBundle, model config services, settings) are mocked
so that ``_bind_embedding_model`` and ``_init_kb`` execute their real logic
through ``handle()``. Only the heavy orchestration methods
(``_run_standard_chunking``, ``_run_raptor``, ``_run_graphrag``) are mocked.
"""
# ── Context factory: make_task_context from conftest — see import above
@pytest.mark.asyncio
async def test_handle_memory_task(self):
"""Test handle returns after dispatching memory task — no further processing."""
ctx = make_task_context(task_type="memory")
ctx.raw_task = {"memory_id": "mem_1", "id": "task_1"}
with patch("rag.svr.task_executor_refactor.task_handler.handle_save_to_memory_task",
new_callable=AsyncMock) as mock_handle:
handler = TaskHandler(ctx=ctx)
handler._run_standard_chunking = AsyncMock()
handler._run_dataflow = AsyncMock()
await handler.handle()
mock_handle.assert_called_once_with(ctx.raw_task)
# After memory task, should return immediately — no further routing
handler._run_standard_chunking.assert_not_called()
handler._run_dataflow.assert_not_called()
@pytest.mark.asyncio
async def test_handle_dataflow_task(self):
"""Test handle dispatches dataflow tasks (after embedding binding + init_kb)."""
ctx = make_task_context(task_type="dataflow", doc_id="doc_1")
with patch_embedding_binding(), \
patch("rag.svr.task_executor_refactor.task_handler.settings", create_mock_settings()), \
patch("rag.svr.task_executor_refactor.task_handler.search.index_name", return_value="test_idx"):
handler = TaskHandler(ctx=ctx)
handler._run_dataflow = AsyncMock()
await handler.handle()
handler._run_dataflow.assert_called_once()
@pytest.mark.asyncio
async def test_handle_canceled_task(self):
"""Test handle returns early when task is canceled."""
ctx = make_task_context(has_canceled_func=MagicMock(return_value=True))
handler = TaskHandler(ctx=ctx)
await handler.handle()
ctx.progress_cb.assert_called_once_with(-1, msg="Task has been canceled.")
@pytest.mark.asyncio
async def test_handle_standard_chunking(self):
"""Test handle routes to standard chunking.
``_bind_embedding_model`` and ``_init_kb`` run their real code;
only the external boundary (LLM API, settings) is mocked.
"""
ctx = make_task_context()
with patch_embedding_binding(), \
patch("rag.svr.task_executor_refactor.task_handler.settings", create_mock_settings()), \
patch("rag.svr.task_executor_refactor.task_handler.search.index_name", return_value="test_idx"):
handler = TaskHandler(ctx=ctx)
handler._run_standard_chunking = AsyncMock()
await handler.handle()
handler._run_standard_chunking.assert_called_once()
@pytest.mark.asyncio
async def test_handle_raptor_task(self):
"""Test handle routes to RAPTOR with real embedding binding."""
ctx = make_task_context(task_type="raptor")
with patch_embedding_binding(), \
patch("rag.svr.task_executor_refactor.task_handler.settings", create_mock_settings()), \
patch("rag.svr.task_executor_refactor.task_handler.search.index_name", return_value="test_idx"):
handler = TaskHandler(ctx=ctx)
handler._run_raptor = AsyncMock()
await handler.handle()
handler._run_raptor.assert_called_once()
@pytest.mark.asyncio
async def test_handle_graphrag_task(self):
"""Test handle routes to GraphRAG with real embedding binding."""
ctx = make_task_context(task_type="graphrag")
with patch_embedding_binding(), \
patch("rag.svr.task_executor_refactor.task_handler.settings", create_mock_settings()), \
patch("rag.svr.task_executor_refactor.task_handler.search.index_name", return_value="test_idx"):
handler = TaskHandler(ctx=ctx)
handler._run_graphrag = AsyncMock()
await handler.handle()
handler._run_graphrag.assert_called_once()
@pytest.mark.asyncio
async def test_handle_embedding_model_failure(self):
"""Test handle returns early when embedding model binding fails.
``LLMBundle`` is patched to raise, so ``_bind_embedding_model``
itself raises — no need to mock the private method.
"""
ctx = make_task_context()
with patch("rag.svr.task_executor_refactor.task_handler.get_model_config_from_provider_instance") as mock_cfg, \
patch("rag.svr.task_executor_refactor.task_handler.get_tenant_default_model_by_type") as mock_default, \
patch("rag.svr.task_executor_refactor.task_handler.LLMBundle") as mock_bundle:
mock_cfg.return_value = MagicMock()
mock_default.return_value = MagicMock()
mock_bundle.side_effect = RuntimeError("embedding service unavailable")
handler = TaskHandler(ctx=ctx)
with pytest.raises(RuntimeError, match="embedding service unavailable"):
await handler.handle()
@pytest.mark.asyncio
async def test_handle_storage_binary_none_raises_file_not_found(self):
"""Verify that None binary from storage raises FileNotFoundError."""
ctx = make_task_context()
with patch_embedding_binding(), \
patch("rag.svr.task_executor_refactor.task_handler.settings", create_mock_settings()), \
patch("rag.svr.task_executor_refactor.task_handler.search.index_name", return_value="test_idx"), \
patch("rag.svr.task_executor_refactor.task_handler.File2DocumentService.get_storage_address",
return_value=("bucket_test", "name_test")), \
patch.object(TaskHandler, "_get_storage_binary", new_callable=AsyncMock, return_value=None):
handler = TaskHandler(ctx=ctx)
# Do NOT mock _run_standard_chunking — we want real code path for the check
with pytest.raises(FileNotFoundError, match="Can not find file <test.pdf> from minio"):
await handler.handle()
class TestTaskHandlerBuildToc:
"""Tests for _build_toc — stable pure helper (requires LLM mocking)."""
def test_build_toc_with_empty_docs(self):
"""Test _build_toc returns None when run_toc_from_text returns empty."""
ctx = MagicMock()
ctx.tenant_id = "tenant_1"
ctx.llm_id = "llm_1"
ctx.language = "en"
docs = [{"id": "chunk_1", "content_with_weight": "text", "page_num_int": [1], "top_int": [0]}]
def mock_asyncio_run(coro):
coro.close()
return []
with patch("rag.svr.task_executor_refactor.task_handler.get_model_config_from_provider_instance") as mock_cfg, \
patch("rag.svr.task_executor_refactor.task_handler.LLMBundle") as mock_bundle, \
patch("rag.svr.task_executor_refactor.task_handler.asyncio.run", side_effect=mock_asyncio_run):
mock_cfg.return_value = MagicMock()
mock_msg = MagicMock()
mock_bundle.return_value.__enter__.return_value = mock_msg
result = TaskHandler._build_toc(ctx, docs, MagicMock())
assert result is None
def test_build_toc_with_results(self):
"""Test _build_toc builds TOC chunk when results exist."""
ctx = MagicMock()
ctx.tenant_id = "tenant_1"
ctx.llm_id = "llm_1"
ctx.language = "en"
docs = [{"id": "chunk_0", "content_with_weight": "text", "doc_id": "doc_1", "page_num_int": [1], "top_int": [0]}]
toc_result = [{"chunk_id": "0", "title": "Section 1"}]
def mock_asyncio_run(coro):
coro.close()
return toc_result
with patch("rag.svr.task_executor_refactor.task_handler.get_model_config_from_provider_instance") as mock_cfg, \
patch("rag.svr.task_executor_refactor.task_handler.LLMBundle") as mock_bundle, \
patch("rag.svr.task_executor_refactor.task_handler.asyncio.run", side_effect=mock_asyncio_run):
mock_cfg.return_value = MagicMock()
mock_msg = MagicMock()
mock_bundle.return_value.__enter__.return_value = mock_msg
result = TaskHandler._build_toc(ctx, docs, MagicMock())
assert result is not None
assert "toc_kwd" in result
assert result["toc_kwd"] == "toc"
assert result["available_int"] == 0
class TestTaskHandlerInit:
"""Tests for TaskHandler initialization."""
def test_init_stores_context_and_hook(self):
ctx = MagicMock()
hook = MagicMock()
handler = TaskHandler(ctx=ctx, billing_hook=hook)
assert handler._task_context is ctx
assert handler._billing_hook is hook
def test_init_default_hook_none(self):
ctx = MagicMock()
handler = TaskHandler(ctx=ctx)
assert handler._billing_hook is None

View File

@@ -1,856 +0,0 @@
#
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Integration tests for TaskHandler orchestration.
"""
import asyncio
import gc
from typing import Any, Dict
from unittest.mock import MagicMock, AsyncMock, patch
import pytest
from rag.svr.task_executor_refactor.task_handler import TaskHandler
from rag.svr.task_executor_refactor.task_context import TaskContext, TaskLimiters, TaskCallbacks
from rag.svr.task_executor_refactor.recording_context import BaseRecordingContext, RecordingContext
from rag.svr.task_executor_refactor.constants import CANVAS_DEBUG_DOC_ID, GRAPH_RAPTOR_FAKE_DOC_ID
# Import shared helpers from conftest
from test.unit_test.rag.svr.task_executor_refactor.conftest import (
AsyncMockLimiter,
create_mock_embedding_model,
create_default_chunks,
create_mock_settings,
create_mock_chunk_service,
make_task_dict,
patch_get_storage_binary,
patch_task_handler_settings,
mock_thread_return_binary,
mock_thread_return_none,
)
def create_task_context(
task_dict: Dict[str, Any],
is_canceled: bool = False,
recording_context: BaseRecordingContext | None = None,
) -> TaskContext:
"""Create a real TaskContext with mocked limiters and callbacks.
Args:
task_dict: Task dictionary with all task attributes.
is_canceled: If True, has_canceled_func returns True.
recording_context: RecordingContext to inject. If None, a new one
is created automatically so that recording_context access works.
Returns:
TaskContext with all required dependencies injected.
"""
if recording_context is None:
recording_context = RecordingContext()
limiter = AsyncMockLimiter()
progress_callback = MagicMock()
ctx = TaskContext(
task=task_dict,
limiters=TaskLimiters(
chat=limiter,
minio=limiter,
chunk=limiter,
embed=limiter,
kg=limiter,
),
callbacks=TaskCallbacks(
progress=progress_callback,
has_canceled=MagicMock(return_value=is_canceled),
),
recording_context=recording_context,
)
# Add progress_callback property for task_handler compatibility
ctx.progress_callback = progress_callback
# Add set_progress_cb method for task_handler compatibility
ctx.set_progress_cb = lambda cb: setattr(ctx.callbacks, 'progress_cb', cb)
return ctx
class TestStandardChunkingPipelineIntegration:
"""P0: Integration tests for the complete standard chunking pipeline."""
@pytest.mark.asyncio
async def test_full_chunking_pipeline_records_task_status(self):
"""Verify that the complete pipeline records task_status as 'completed'."""
task_dict = make_task_dict()
ctx = create_task_context(task_dict)
mock_embedding = create_mock_embedding_model(vector_size=128)
mock_settings = create_mock_settings()
mock_chunk_service = create_mock_chunk_service()
with patch_get_storage_binary(), \
patch_task_handler_settings(mock_settings), \
patch("rag.svr.task_executor_refactor.chunk_service.settings", mock_settings), \
patch("rag.svr.task_executor_refactor.task_handler.get_model_config_from_provider_instance") as mock_get_config, \
patch("rag.svr.task_executor_refactor.task_handler.LLMBundle") as mock_bundle, \
patch("rag.svr.task_executor_refactor.task_handler.get_tenant_default_model_by_type") as mock_get_default, \
patch("rag.svr.task_executor_refactor.task_handler.File2DocumentService") as mock_file_service, \
patch("rag.svr.task_executor_refactor.task_handler.thread_pool_exec") as mock_thread_exec, \
patch("rag.svr.task_executor_refactor.chunk_service.thread_pool_exec") as mock_chunk_thread_exec, \
patch("rag.svr.task_executor_refactor.task_handler.DocumentService") as mock_doc_service, \
patch("rag.svr.task_executor_refactor.task_handler.search.index_name") as mock_index_name, \
patch("rag.svr.task_executor_refactor.task_handler.ChunkService") as mock_chunk_service_cls:
mock_get_config.return_value = MagicMock()
mock_get_default.return_value = MagicMock()
mock_bundle.return_value = mock_embedding
mock_file_service.get_storage_address.return_value = ("bucket_test", "name_test")
mock_index_name.return_value = "test_index"
mock_doc_service.increment_chunk_num = MagicMock()
mock_doc_service.get_document_metadata.return_value = {}
mock_doc_service.update_document_metadata = MagicMock()
mock_chunk_service_cls.return_value = mock_chunk_service
mock_thread_exec.side_effect = mock_thread_return_binary
mock_chunk_thread_exec.side_effect = mock_thread_return_binary
handler = TaskHandler(ctx=ctx)
await handler.handle()
recording_ctx = ctx.recording_context
task_status = recording_ctx.get("task_status")
assert task_status == "completed", f"Expected task_status='completed', got {task_status}"
@pytest.mark.asyncio
async def test_full_chunking_pipeline_records_insertion_result(self):
"""Verify that insertion_result is recorded as 'success'."""
task_dict = make_task_dict()
ctx = create_task_context(task_dict)
mock_embedding = create_mock_embedding_model(vector_size=128)
mock_settings = create_mock_settings()
mock_chunk_service = create_mock_chunk_service()
with patch_get_storage_binary(), \
patch_task_handler_settings(mock_settings), \
patch("rag.svr.task_executor_refactor.chunk_service.settings", mock_settings), \
patch("rag.svr.task_executor_refactor.task_handler.get_model_config_from_provider_instance") as mock_get_config, \
patch("rag.svr.task_executor_refactor.task_handler.LLMBundle") as mock_bundle, \
patch("rag.svr.task_executor_refactor.task_handler.get_tenant_default_model_by_type") as mock_get_default, \
patch("rag.svr.task_executor_refactor.task_handler.File2DocumentService") as mock_file_service, \
patch("rag.svr.task_executor_refactor.task_handler.thread_pool_exec") as mock_thread_exec, \
patch("rag.svr.task_executor_refactor.chunk_service.thread_pool_exec") as mock_chunk_thread_exec, \
patch("rag.svr.task_executor_refactor.task_handler.DocumentService") as mock_doc_service, \
patch("rag.svr.task_executor_refactor.task_handler.search.index_name") as mock_index_name, \
patch("rag.svr.task_executor_refactor.task_handler.ChunkService") as mock_chunk_service_cls:
mock_get_config.return_value = MagicMock()
mock_get_default.return_value = MagicMock()
mock_bundle.return_value = mock_embedding
mock_file_service.get_storage_address.return_value = ("bucket_test", "name_test")
mock_index_name.return_value = "test_index"
mock_doc_service.increment_chunk_num = MagicMock()
mock_doc_service.get_document_metadata.return_value = {}
mock_doc_service.update_document_metadata = MagicMock()
mock_chunk_service_cls.return_value = mock_chunk_service
mock_thread_exec.side_effect = mock_thread_return_binary
mock_chunk_thread_exec.side_effect = mock_thread_return_binary
handler = TaskHandler(ctx=ctx)
await handler.handle()
recording_ctx = ctx.recording_context
insertion_result = recording_ctx.get("insertion_result")
assert insertion_result == "success", f"Expected insertion_result='success', got {insertion_result}"
@pytest.mark.asyncio
async def test_full_chunking_pipeline_records_chunk_ids(self):
"""Verify that chunk_ids_count is recorded after build_chunks."""
task_dict = make_task_dict()
ctx = create_task_context(task_dict)
mock_embedding = create_mock_embedding_model(vector_size=128)
mock_settings = create_mock_settings()
mock_chunks = create_default_chunks(count=3)
mock_chunk_service = create_mock_chunk_service(chunks=mock_chunks)
with patch_get_storage_binary(), \
patch_task_handler_settings(mock_settings), \
patch("rag.svr.task_executor_refactor.chunk_service.settings", mock_settings), \
patch("rag.svr.task_executor_refactor.task_handler.get_model_config_from_provider_instance") as mock_get_config, \
patch("rag.svr.task_executor_refactor.task_handler.LLMBundle") as mock_bundle, \
patch("rag.svr.task_executor_refactor.task_handler.get_tenant_default_model_by_type") as mock_get_default, \
patch("rag.svr.task_executor_refactor.task_handler.File2DocumentService") as mock_file_service, \
patch("rag.svr.task_executor_refactor.task_handler.thread_pool_exec") as mock_thread_exec, \
patch("rag.svr.task_executor_refactor.chunk_service.thread_pool_exec") as mock_chunk_thread_exec, \
patch("rag.svr.task_executor_refactor.task_handler.DocumentService") as mock_doc_service, \
patch("rag.svr.task_executor_refactor.task_handler.search.index_name") as mock_index_name, \
patch("rag.svr.task_executor_refactor.task_handler.run_toc_from_text", new_callable=AsyncMock) as mock_run_toc, \
patch("rag.svr.task_executor_refactor.task_handler.ChunkService") as mock_chunk_service_cls:
mock_get_config.return_value = MagicMock()
mock_get_default.return_value = MagicMock()
mock_bundle.return_value = mock_embedding
mock_file_service.get_storage_address.return_value = ("bucket_test", "name_test")
mock_index_name.return_value = "test_index"
mock_doc_service.increment_chunk_num = MagicMock()
mock_doc_service.get_document_metadata.return_value = {}
mock_doc_service.update_document_metadata = MagicMock()
mock_chunk_service_cls.return_value = mock_chunk_service
mock_run_toc.return_value = [] # TOC returns empty when not enabled
mock_thread_exec.side_effect = mock_thread_return_binary
mock_chunk_thread_exec.side_effect = mock_thread_return_binary
handler = TaskHandler(ctx=ctx)
await handler.handle()
recording_ctx = ctx.recording_context
chunk_ids_count = recording_ctx.get("chunk_ids_count")
assert chunk_ids_count is not None, "chunk_ids_count should be recorded"
assert chunk_ids_count == 3, f"Expected chunk_ids_count=3, got {chunk_ids_count}"
@pytest.mark.asyncio
async def test_full_chunking_pipeline_records_token_count(self):
"""Verify that token_count and vector_size are recorded after embedding."""
task_dict = make_task_dict()
ctx = create_task_context(task_dict)
mock_embedding = create_mock_embedding_model(vector_size=128)
mock_settings = create_mock_settings()
mock_chunk_service = create_mock_chunk_service()
with patch_get_storage_binary(), \
patch_task_handler_settings(mock_settings), \
patch("rag.svr.task_executor_refactor.chunk_service.settings", mock_settings), \
patch("rag.svr.task_executor_refactor.task_handler.get_model_config_from_provider_instance") as mock_get_config, \
patch("rag.svr.task_executor_refactor.task_handler.LLMBundle") as mock_bundle, \
patch("rag.svr.task_executor_refactor.task_handler.get_tenant_default_model_by_type") as mock_get_default, \
patch("rag.svr.task_executor_refactor.task_handler.File2DocumentService") as mock_file_service, \
patch("rag.svr.task_executor_refactor.task_handler.thread_pool_exec") as mock_thread_exec, \
patch("rag.svr.task_executor_refactor.chunk_service.thread_pool_exec") as mock_chunk_thread_exec, \
patch("rag.svr.task_executor_refactor.task_handler.DocumentService") as mock_doc_service, \
patch("rag.svr.task_executor_refactor.task_handler.search.index_name") as mock_index_name, \
patch("rag.svr.task_executor_refactor.task_handler.ChunkService") as mock_chunk_service_cls:
mock_get_config.return_value = MagicMock()
mock_get_default.return_value = MagicMock()
mock_bundle.return_value = mock_embedding
mock_file_service.get_storage_address.return_value = ("bucket_test", "name_test")
mock_index_name.return_value = "test_index"
mock_doc_service.increment_chunk_num = MagicMock()
mock_doc_service.get_document_metadata.return_value = {}
mock_doc_service.update_document_metadata = MagicMock()
mock_chunk_service_cls.return_value = mock_chunk_service
mock_thread_exec.side_effect = mock_thread_return_binary
mock_chunk_thread_exec.side_effect = mock_thread_return_binary
handler = TaskHandler(ctx=ctx)
await handler.handle()
recording_ctx = ctx.recording_context
token_count = recording_ctx.get("token_count")
vector_size = recording_ctx.get("vector_size")
assert token_count is not None, "token_count should be recorded"
assert vector_size is not None, "vector_size should be recorded"
assert vector_size == 128, f"Expected vector_size=128, got {vector_size}"
@pytest.mark.asyncio
async def test_full_chunking_pipeline_progress_callback_invoked(self):
"""Verify that progress_callback is invoked multiple times during pipeline."""
task_dict = make_task_dict()
ctx = create_task_context(task_dict)
mock_embedding = create_mock_embedding_model(vector_size=128)
mock_settings = create_mock_settings()
mock_chunk_service = create_mock_chunk_service()
with patch_get_storage_binary(), \
patch_task_handler_settings(mock_settings), \
patch("rag.svr.task_executor_refactor.chunk_service.settings", mock_settings), \
patch("rag.svr.task_executor_refactor.task_handler.get_model_config_from_provider_instance") as mock_get_config, \
patch("rag.svr.task_executor_refactor.task_handler.LLMBundle") as mock_bundle, \
patch("rag.svr.task_executor_refactor.task_handler.get_tenant_default_model_by_type") as mock_get_default, \
patch("rag.svr.task_executor_refactor.task_handler.File2DocumentService") as mock_file_service, \
patch("rag.svr.task_executor_refactor.task_handler.thread_pool_exec") as mock_thread_exec, \
patch("rag.svr.task_executor_refactor.chunk_service.thread_pool_exec") as mock_chunk_thread_exec, \
patch("rag.svr.task_executor_refactor.task_handler.DocumentService") as mock_doc_service, \
patch("rag.svr.task_executor_refactor.task_handler.search.index_name") as mock_index_name, \
patch("rag.svr.task_executor_refactor.task_handler.ChunkService") as mock_chunk_service_cls:
mock_get_config.return_value = MagicMock()
mock_get_default.return_value = MagicMock()
mock_bundle.return_value = mock_embedding
mock_file_service.get_storage_address.return_value = ("bucket_test", "name_test")
mock_index_name.return_value = "test_index"
mock_doc_service.increment_chunk_num = MagicMock()
mock_doc_service.get_document_metadata.return_value = {}
mock_doc_service.update_document_metadata = MagicMock()
mock_chunk_service_cls.return_value = mock_chunk_service
mock_thread_exec.side_effect = mock_thread_return_binary
mock_chunk_thread_exec.side_effect = mock_thread_return_binary
handler = TaskHandler(ctx=ctx)
await handler.handle()
ctx.progress_callback.assert_called()
call_count = ctx.progress_callback.call_count
assert call_count > 0, "progress_callback should have been invoked at least once"
class TestTaskCancellationCleanupIntegration:
"""P0: Integration tests for task cancellation cleanup flow."""
@pytest.mark.asyncio
async def test_canceled_task_calls_docstore_delete(self):
"""Verify that docStoreConn.delete is called when task is canceled."""
task_dict = make_task_dict()
ctx = create_task_context(task_dict, is_canceled=True)
mock_settings = create_mock_settings()
call_log = []
def mock_thread_impl(func, *args, **kwargs):
# Get the actual method name from the mock
func_repr = repr(func)
call_log.append(func_repr)
if 'index_exist' in func_repr:
return True
if 'delete' in func_repr:
return {"result": "deleted"}
return {"result": "deleted"}
with patch_task_handler_settings(mock_settings), \
patch("rag.svr.task_executor_refactor.task_handler.search.index_name", return_value="test_index"), \
patch("rag.svr.task_executor_refactor.task_handler.thread_pool_exec", side_effect=mock_thread_impl):
handler = TaskHandler(ctx=ctx)
await handler.handle_task()
# Verify delete was called by checking the call log
delete_calls = [c for c in call_log if 'delete' in c]
assert len(delete_calls) >= 1, f"Expected at least one delete call, got: {call_log}"
@pytest.mark.asyncio
async def test_canceled_task_progress_callback_with_negative_one(self):
"""Verify that progress_callback is called with -1 when task is canceled."""
task_dict = make_task_dict()
ctx = create_task_context(task_dict, is_canceled=True)
mock_settings = create_mock_settings()
def mock_thread_impl(func, *args, **kwargs):
func_repr = repr(func)
if 'index_exist' in func_repr:
return True
if 'delete' in func_repr:
return {"result": "deleted"}
return {"result": "deleted"}
with patch_task_handler_settings(mock_settings), \
patch("rag.svr.task_executor_refactor.task_handler.search.index_name", return_value="test_index"), \
patch("rag.svr.task_executor_refactor.task_handler.thread_pool_exec", side_effect=mock_thread_impl):
handler = TaskHandler(ctx=ctx)
await handler.handle_task()
ctx.progress_callback.assert_called()
call_args_list = ctx.progress_callback.call_args_list
# Check for -1 in any position of the call arguments
has_negative_progress = False
for call in call_args_list:
# Check positional args
for arg in call[0]:
if arg == -1:
has_negative_progress = True
break
# Check keyword args
if call[1].get("prog") == -1:
has_negative_progress = True
if has_negative_progress:
break
assert has_negative_progress, f"progress_callback should have been called with -1 progress. Calls: {call_args_list}"
@pytest.mark.asyncio
async def test_canceled_task_does_not_proceed_to_chunking(self):
"""Verify that canceled task does not proceed to embedding model binding."""
task_dict = make_task_dict()
ctx = create_task_context(task_dict, is_canceled=True)
mock_settings = create_mock_settings()
with patch_task_handler_settings(mock_settings), \
patch("rag.svr.task_executor_refactor.task_handler.search.index_name") as mock_index_name, \
patch("rag.svr.task_executor_refactor.task_handler.thread_pool_exec") as mock_thread_exec, \
patch("rag.svr.task_executor_refactor.task_handler.get_model_config_from_provider_instance") as mock_get_config, \
patch("rag.svr.task_executor_refactor.task_handler.LLMBundle") as mock_bundle, \
patch("rag.svr.task_executor_refactor.task_handler.get_tenant_default_model_by_type") as mock_get_default:
mock_index_name.return_value = "test_index"
mock_settings.docStoreConn.index_exist.return_value = True
mock_settings.docStoreConn.delete.return_value = {"result": "deleted"}
async def mock_thread_impl(func, *args, **kwargs):
return {"result": "deleted"}
mock_thread_exec.side_effect = mock_thread_impl
mock_get_config.return_value = MagicMock()
mock_get_default.return_value = MagicMock()
handler = TaskHandler(ctx=ctx)
await handler.handle_task()
mock_bundle.assert_not_called()
class TestRaptorPipelineIntegration:
"""P1: Integration tests for the RAPTOR pipeline."""
@pytest.mark.asyncio
async def test_raptor_pipeline_records_task_status(self):
"""Verify that RAPTOR pipeline records task_status."""
task_dict = make_task_dict(doc_id=GRAPH_RAPTOR_FAKE_DOC_ID, doc_ids=["doc1", "doc2"], task_type="raptor", parser_config={"raptor": {"use_raptor": False}}, kb_parser_config={"raptor": {"use_raptor": False}})
ctx = create_task_context(task_dict)
mock_embedding = create_mock_embedding_model(vector_size=128)
mock_settings = create_mock_settings()
mock_kb = MagicMock()
mock_kb.id = "kb_test"
mock_kb.parser_config = {"raptor": {"use_raptor": False}}
with patch_task_handler_settings(mock_settings), \
patch("rag.svr.task_executor_refactor.chunk_service.settings", mock_settings), \
patch("rag.svr.task_executor_refactor.task_handler.get_model_config_from_provider_instance") as mock_get_config, \
patch("rag.svr.task_executor_refactor.task_handler.LLMBundle") as mock_bundle, \
patch("rag.svr.task_executor_refactor.task_handler.get_tenant_default_model_by_type") as mock_get_default, \
patch("rag.svr.task_executor_refactor.task_handler.search.index_name") as mock_index_name, \
patch("rag.svr.task_executor_refactor.task_handler.thread_pool_exec") as mock_thread_exec, \
patch("rag.svr.task_executor_refactor.task_handler.KnowledgebaseService") as mock_kb_service, \
patch("rag.svr.task_executor_refactor.task_handler.RaptorService") as mock_raptor_service, \
patch("rag.svr.task_executor_refactor.task_handler.ChunkService") as mock_chunk_service, \
patch("rag.svr.task_executor_refactor.task_handler.DocumentService") as mock_doc_service:
mock_get_config.return_value = MagicMock()
mock_get_default.return_value = MagicMock()
mock_bundle.return_value = mock_embedding
mock_index_name.return_value = "test_index"
mock_kb_service.get_by_id.return_value = (True, mock_kb)
mock_kb_service.update_by_id.return_value = True
mock_raptor_service.return_value.run_raptor_for_kb = AsyncMock(return_value=([], 0, []))
mock_chunk_service.return_value.insert_chunks = AsyncMock(return_value=True)
mock_doc_service.increment_chunk_num = MagicMock()
mock_thread_exec.side_effect = mock_thread_return_none
handler = TaskHandler(ctx=ctx)
await handler.handle()
recording_ctx = ctx.recording_context
task_status = recording_ctx.get("task_status")
assert task_status == "completed", f"Expected task_status='completed', got {task_status}"
@pytest.mark.asyncio
async def test_raptor_pipeline_enables_raptor_if_not_configured(self):
"""Verify that RAPTOR is enabled if not already configured."""
task_dict = make_task_dict(doc_id=GRAPH_RAPTOR_FAKE_DOC_ID, doc_ids=["doc1", "doc2"], task_type="raptor", parser_config={"raptor": {"use_raptor": False}}, kb_parser_config={"raptor": {"use_raptor": False}})
ctx = create_task_context(task_dict)
mock_embedding = create_mock_embedding_model(vector_size=128)
mock_settings = create_mock_settings()
mock_kb = MagicMock()
mock_kb.id = "kb_test"
mock_kb.parser_config = {"raptor": {"use_raptor": False}}
with patch_task_handler_settings(mock_settings), \
patch("rag.svr.task_executor_refactor.chunk_service.settings", mock_settings), \
patch("rag.svr.task_executor_refactor.task_handler.get_model_config_from_provider_instance") as mock_get_config, \
patch("rag.svr.task_executor_refactor.task_handler.LLMBundle") as mock_bundle, \
patch("rag.svr.task_executor_refactor.task_handler.get_tenant_default_model_by_type") as mock_get_default, \
patch("rag.svr.task_executor_refactor.task_handler.search.index_name") as mock_index_name, \
patch("rag.svr.task_executor_refactor.task_handler.thread_pool_exec") as mock_thread_exec, \
patch("rag.svr.task_executor_refactor.task_handler.KnowledgebaseService") as mock_kb_service, \
patch("rag.svr.task_executor_refactor.task_handler.RaptorService") as mock_raptor_service, \
patch("rag.svr.task_executor_refactor.task_handler.ChunkService") as mock_chunk_service, \
patch("rag.svr.task_executor_refactor.task_handler.DocumentService") as mock_doc_service:
mock_get_config.return_value = MagicMock()
mock_get_default.return_value = MagicMock()
mock_bundle.return_value = mock_embedding
mock_index_name.return_value = "test_index"
mock_kb_service.get_by_id.return_value = (True, mock_kb)
mock_kb_service.update_by_id.return_value = True
mock_raptor_service.return_value.run_raptor_for_kb = AsyncMock(return_value=([], 0, []))
mock_chunk_service.return_value.insert_chunks = AsyncMock(return_value=True)
mock_doc_service.increment_chunk_num = MagicMock()
mock_thread_exec.side_effect = mock_thread_return_none
handler = TaskHandler(ctx=ctx)
await handler.handle()
# Check that the kb parser_config was updated
mock_kb_service.update_by_id.assert_called_once()
call_args = mock_kb_service.update_by_id.call_args
update_dict = call_args[0][1]
assert update_dict.get("parser_config", {}).get("raptor", {}).get("use_raptor") is True, \
"RAPTOR should be enabled in parser_config after running"
class TestEmbeddingModelBindingFailureIntegration:
"""P1: Integration tests for embedding model binding failure."""
@pytest.mark.asyncio
async def test_embedding_binding_failure_raises_exception(self):
"""Verify that embedding model binding failure raises an exception."""
task_dict = make_task_dict()
ctx = create_task_context(task_dict)
with patch("rag.svr.task_executor_refactor.task_handler.get_model_config_from_provider_instance") as mock_get_config, \
patch("rag.svr.task_executor_refactor.task_handler.get_tenant_default_model_by_type") as mock_get_default:
mock_get_config.side_effect = Exception("Model not found")
mock_get_default.side_effect = Exception("Model not found")
handler = TaskHandler(ctx=ctx)
with pytest.raises(Exception, match="Model not found"):
await handler.handle()
@pytest.mark.asyncio
async def test_embedding_binding_failure_calls_progress_callback(self):
"""Verify that embedding model binding failure calls progress_callback."""
task_dict = make_task_dict()
ctx = create_task_context(task_dict)
with patch("rag.svr.task_executor_refactor.task_handler.get_model_config_from_provider_instance") as mock_get_config, \
patch("rag.svr.task_executor_refactor.task_handler.get_tenant_default_model_by_type") as mock_get_default:
mock_get_config.side_effect = Exception("Model not found")
mock_get_default.side_effect = Exception("Model not found")
handler = TaskHandler(ctx=ctx)
with pytest.raises(Exception):
await handler.handle()
ctx.progress_callback.assert_called()
class TestDataflowPipelineIntegration:
"""P2: Integration tests for the dataflow pipeline."""
@pytest.mark.asyncio
async def test_dataflow_pipeline_calls_dataflow_service(self):
"""Verify that dataflow pipeline calls DataflowService.run_dataflow()."""
task_dict = make_task_dict(doc_id=CANVAS_DEBUG_DOC_ID, task_type="dataflow")
ctx = create_task_context(task_dict)
mock_embedding = create_mock_embedding_model(vector_size=128)
with patch("rag.svr.task_executor_refactor.task_handler.get_model_config_from_provider_instance") as mock_get_config, \
patch("rag.svr.task_executor_refactor.task_handler.LLMBundle") as mock_bundle, \
patch("rag.svr.task_executor_refactor.task_handler.get_tenant_default_model_by_type") as mock_get_default, \
patch("rag.svr.task_executor_refactor.task_handler.search.index_name", return_value="test_idx"), \
patch("rag.svr.task_executor_refactor.task_handler.settings") as mock_settings, \
patch("rag.svr.task_executor_refactor.task_handler.DataflowService") as mock_dataflow_service:
mock_get_config.return_value = MagicMock()
mock_get_default.return_value = MagicMock()
mock_bundle.return_value = mock_embedding
mock_settings.docStoreConn = MagicMock()
mock_settings.docStoreConn.create_idx = MagicMock()
mock_instance = MagicMock()
mock_instance.run_dataflow = AsyncMock(return_value=None)
mock_dataflow_service.return_value = mock_instance
handler = TaskHandler(ctx=ctx)
await handler.handle()
mock_dataflow_service.assert_called_once()
mock_instance.run_dataflow.assert_called_once()
class TestTocAsyncFlowIntegration:
"""P2: Integration tests for TOC async flow."""
@pytest.mark.asyncio
async def test_toc_async_flow_creates_toc_thread(self):
"""Verify that TOC async flow creates a TOC thread when enabled."""
task_dict = make_task_dict(parser_config={"auto_keywords": 0, "auto_questions": 0, "enable_metadata": False, "toc_extraction": True})
ctx = create_task_context(task_dict)
mock_embedding = create_mock_embedding_model(vector_size=128)
mock_settings = create_mock_settings()
mock_chunk_service = create_mock_chunk_service()
with patch_get_storage_binary(), \
patch_task_handler_settings(mock_settings), \
patch("rag.svr.task_executor_refactor.chunk_service.settings", mock_settings), \
patch("rag.svr.task_executor_refactor.task_handler.get_model_config_from_provider_instance") as mock_get_config, \
patch("rag.svr.task_executor_refactor.task_handler.LLMBundle") as mock_bundle, \
patch("rag.svr.task_executor_refactor.task_handler.get_tenant_default_model_by_type") as mock_get_default, \
patch("rag.svr.task_executor_refactor.task_handler.File2DocumentService") as mock_file_service, \
patch("rag.svr.task_executor_refactor.task_handler.thread_pool_exec") as mock_thread_exec, \
patch("rag.svr.task_executor_refactor.chunk_service.thread_pool_exec") as mock_chunk_thread_exec, \
patch("rag.svr.task_executor_refactor.task_handler.DocumentService") as mock_doc_service, \
patch("rag.svr.task_executor_refactor.task_handler.search.index_name") as mock_index_name, \
patch("rag.svr.task_executor_refactor.task_handler.run_toc_from_text", new_callable=AsyncMock) as mock_run_toc, \
patch("rag.svr.task_executor_refactor.task_handler.ChunkService") as mock_chunk_service_cls, \
patch("rag.svr.task_executor_refactor.post_processor.DocumentService") as mock_post_doc_service:
mock_get_config.return_value = MagicMock()
mock_get_default.return_value = MagicMock()
mock_bundle.return_value = mock_embedding
mock_file_service.get_storage_address.return_value = ("bucket_test", "name_test")
mock_index_name.return_value = "test_index"
mock_doc_service.increment_chunk_num = MagicMock()
mock_doc_service.get_document_metadata.return_value = {}
mock_doc_service.update_document_metadata = MagicMock()
mock_chunk_service_cls.return_value = mock_chunk_service
mock_run_toc.return_value = [{"title": "Test TOC", "level": 1}]
mock_post_doc_service.increment_chunk_num = MagicMock()
mock_thread_exec.side_effect = mock_thread_return_binary
mock_chunk_thread_exec.side_effect = mock_thread_return_binary
handler = TaskHandler(ctx=ctx)
await handler.handle()
mock_run_toc.assert_called()
# Explicit cleanup to prevent resource leaks
del mock_embedding, mock_settings, mock_chunk_service
del mock_get_config, mock_get_default, mock_bundle, mock_file_service
del mock_index_name, mock_doc_service, mock_chunk_service_cls, mock_run_toc, mock_post_doc_service
del mock_thread_exec, mock_chunk_thread_exec
# Allow pending callbacks to execute
await asyncio.sleep(0)
gc.collect()
@pytest.mark.asyncio(loop_scope="function")
@pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning")
async def test_toc_async_flow_does_not_create_thread_when_disabled(self):
"""Verify that TOC async flow does not create a thread when disabled.
Note: This test has a known issue with resource leaks (unclosed sockets and
event loops) when run as part of the full test suite. The warning filter
above suppresses these warnings temporarily. The root cause is related to
asyncio.to_thread creating new event loops that are not properly cleaned up
by pytest-asyncio.
"""
task_dict = make_task_dict(parser_config={"auto_keywords": 0, "auto_questions": 0, "enable_metadata": False, "toc_extraction": True})
task_dict["parser_config"]["toc_extraction"] = False
ctx = create_task_context(task_dict)
mock_embedding = create_mock_embedding_model(vector_size=128)
mock_settings = create_mock_settings()
mock_chunk_service = create_mock_chunk_service()
with patch_get_storage_binary(), \
patch_task_handler_settings(mock_settings), \
patch("rag.svr.task_executor_refactor.chunk_service.settings", mock_settings), \
patch("rag.svr.task_executor_refactor.task_handler.get_model_config_from_provider_instance") as mock_get_config, \
patch("rag.svr.task_executor_refactor.task_handler.LLMBundle") as mock_bundle, \
patch("rag.svr.task_executor_refactor.task_handler.get_tenant_default_model_by_type") as mock_get_default, \
patch("rag.svr.task_executor_refactor.task_handler.File2DocumentService") as mock_file_service, \
patch("rag.svr.task_executor_refactor.task_handler.thread_pool_exec") as mock_thread_exec, \
patch("rag.svr.task_executor_refactor.chunk_service.thread_pool_exec") as mock_chunk_thread_exec, \
patch("rag.svr.task_executor_refactor.task_handler.DocumentService") as mock_doc_service, \
patch("rag.svr.task_executor_refactor.task_handler.search.index_name") as mock_index_name, \
patch("rag.svr.task_executor_refactor.task_handler.run_toc_from_text", new_callable=AsyncMock) as mock_run_toc, \
patch("rag.svr.task_executor_refactor.task_handler.ChunkService") as mock_chunk_service_cls:
mock_get_config.return_value = MagicMock()
mock_get_default.return_value = MagicMock()
mock_bundle.return_value = mock_embedding
mock_file_service.get_storage_address.return_value = ("bucket_test", "name_test")
mock_index_name.return_value = "test_index"
mock_doc_service.increment_chunk_num = MagicMock()
mock_doc_service.get_document_metadata.return_value = {}
mock_doc_service.update_document_metadata = MagicMock()
mock_chunk_service_cls.return_value = mock_chunk_service
mock_thread_exec.side_effect = mock_thread_return_binary
mock_chunk_thread_exec.side_effect = mock_thread_return_binary
handler = TaskHandler(ctx=ctx)
await handler.handle()
mock_run_toc.assert_not_called()
# Explicit cleanup to prevent resource leaks
del mock_embedding, mock_settings, mock_chunk_service
del mock_get_config, mock_get_default, mock_bundle, mock_file_service
del mock_index_name, mock_doc_service, mock_chunk_service_cls, mock_run_toc
del mock_thread_exec, mock_chunk_thread_exec
# Allow pending callbacks to execute and close event loop
await asyncio.sleep(0)
# Cancel all pending tasks
current_task = asyncio.current_task()
pending = [t for t in asyncio.all_tasks() if t is not current_task and not t.done()]
for task in pending:
task.cancel()
if pending:
await asyncio.gather(*pending, return_exceptions=True)
gc.collect()
class TestRecordingContextDataFlowAssertions:
"""P2: Integration tests for RecordingContext data flow assertions."""
@pytest.mark.asyncio
async def test_recording_context_captures_file_size_check(self):
"""Verify that RecordingContext captures file_size_exceeded result."""
task_dict = make_task_dict()
ctx = create_task_context(task_dict)
mock_embedding = create_mock_embedding_model(vector_size=128)
mock_settings = create_mock_settings()
mock_chunk_service = create_mock_chunk_service()
with patch_get_storage_binary(), \
patch_task_handler_settings(mock_settings), \
patch("rag.svr.task_executor_refactor.chunk_service.settings", mock_settings), \
patch("rag.svr.task_executor_refactor.task_handler.get_model_config_from_provider_instance") as mock_get_config, \
patch("rag.svr.task_executor_refactor.task_handler.LLMBundle") as mock_bundle, \
patch("rag.svr.task_executor_refactor.task_handler.get_tenant_default_model_by_type") as mock_get_default, \
patch("rag.svr.task_executor_refactor.task_handler.File2DocumentService") as mock_file_service, \
patch("rag.svr.task_executor_refactor.task_handler.thread_pool_exec") as mock_thread_exec, \
patch("rag.svr.task_executor_refactor.chunk_service.thread_pool_exec") as mock_chunk_thread_exec, \
patch("rag.svr.task_executor_refactor.task_handler.DocumentService") as mock_doc_service, \
patch("rag.svr.task_executor_refactor.task_handler.search.index_name") as mock_index_name, \
patch("rag.svr.task_executor_refactor.task_handler.ChunkService") as mock_chunk_service_cls:
mock_get_config.return_value = MagicMock()
mock_get_default.return_value = MagicMock()
mock_bundle.return_value = mock_embedding
mock_file_service.get_storage_address.return_value = ("bucket_test", "name_test")
mock_index_name.return_value = "test_index"
mock_doc_service.increment_chunk_num = MagicMock()
mock_doc_service.get_document_metadata.return_value = {}
mock_doc_service.update_document_metadata = MagicMock()
mock_chunk_service_cls.return_value = mock_chunk_service
mock_thread_exec.side_effect = mock_thread_return_binary
mock_chunk_thread_exec.side_effect = mock_thread_return_binary
handler = TaskHandler(ctx=ctx)
await handler.handle()
recording_ctx = ctx.recording_context
file_size_exceeded = recording_ctx.get("file_size_exceeded")
assert file_size_exceeded is None or file_size_exceeded is False, \
f"Expected file_size_exceeded to be False/None for small file, got {file_size_exceeded}"
@pytest.mark.asyncio
async def test_recording_context_captures_parser_id(self):
"""Verify that RecordingContext captures parser_id from task context."""
task_dict = make_task_dict()
ctx = create_task_context(task_dict)
mock_embedding = create_mock_embedding_model(vector_size=128)
mock_settings = create_mock_settings()
mock_chunk_service = create_mock_chunk_service()
with patch_get_storage_binary(), \
patch_task_handler_settings(mock_settings), \
patch("rag.svr.task_executor_refactor.chunk_service.settings", mock_settings), \
patch("rag.svr.task_executor_refactor.task_handler.get_model_config_from_provider_instance") as mock_get_config, \
patch("rag.svr.task_executor_refactor.task_handler.LLMBundle") as mock_bundle, \
patch("rag.svr.task_executor_refactor.task_handler.get_tenant_default_model_by_type") as mock_get_default, \
patch("rag.svr.task_executor_refactor.task_handler.File2DocumentService") as mock_file_service, \
patch("rag.svr.task_executor_refactor.task_handler.thread_pool_exec") as mock_thread_exec, \
patch("rag.svr.task_executor_refactor.chunk_service.thread_pool_exec") as mock_chunk_thread_exec, \
patch("rag.svr.task_executor_refactor.task_handler.DocumentService") as mock_doc_service, \
patch("rag.svr.task_executor_refactor.task_handler.search.index_name") as mock_index_name, \
patch("rag.svr.task_executor_refactor.task_handler.ChunkService") as mock_chunk_service_cls:
mock_get_config.return_value = MagicMock()
mock_get_default.return_value = MagicMock()
mock_bundle.return_value = mock_embedding
mock_file_service.get_storage_address.return_value = ("bucket_test", "name_test")
mock_index_name.return_value = "test_index"
mock_doc_service.increment_chunk_num = MagicMock()
mock_doc_service.get_document_metadata.return_value = {}
mock_doc_service.update_document_metadata = MagicMock()
mock_chunk_service_cls.return_value = mock_chunk_service
mock_thread_exec.side_effect = mock_thread_return_binary
mock_chunk_thread_exec.side_effect = mock_thread_return_binary
handler = TaskHandler(ctx=ctx)
await handler.handle()
recording_ctx = ctx.recording_context
# parser_id is available in the task context, verify task completion
task_status = recording_ctx.get("task_status")
assert task_status == "completed", f"Expected task_status='completed', got {task_status}"
# Verify the parser_id is accessible from the task context
assert ctx.parser_id == "naive", f"Expected parser_id='naive', got {ctx.parser_id}"
class TestGraphragPipelineIntegration:
"""P2: Integration tests for GraphRAG pipeline default configuration."""
@pytest.mark.asyncio
async def test_graphrag_pipeline_configures_full_defaults(self):
"""Verify that GraphRAG configures all default parameters when not already set."""
task_dict = make_task_dict(doc_ids=["doc1", "doc2"], task_type="graphrag")
rec_ctx = RecordingContext()
ctx = create_task_context(task_dict, recording_context=rec_ctx)
mock_embedding = create_mock_embedding_model(vector_size=128)
mock_settings = create_mock_settings()
mock_kb = MagicMock()
mock_kb.id = "kb_test"
mock_kb.parser_config = {}
with patch_task_handler_settings(mock_settings), \
patch("rag.svr.task_executor_refactor.chunk_service.settings", mock_settings), \
patch("rag.svr.task_executor_refactor.task_handler.get_model_config_from_provider_instance") as mock_get_config, \
patch("rag.svr.task_executor_refactor.task_handler.LLMBundle") as mock_bundle, \
patch("rag.svr.task_executor_refactor.task_handler.get_tenant_default_model_by_type") as mock_get_default, \
patch("rag.svr.task_executor_refactor.task_handler.search.index_name") as mock_index_name, \
patch("rag.svr.task_executor_refactor.task_handler.thread_pool_exec") as mock_thread_exec, \
patch("rag.svr.task_executor_refactor.task_handler.KnowledgebaseService") as mock_kb_service, \
patch("rag.svr.task_executor_refactor.task_handler.run_graphrag_for_kb") as mock_run_graphrag, \
patch("rag.svr.task_executor_refactor.task_handler.DocumentService"):
mock_get_config.return_value = MagicMock()
mock_get_default.return_value = MagicMock()
mock_bundle.return_value = mock_embedding
mock_index_name.return_value = "test_index"
mock_kb_service.get_by_id.return_value = (True, mock_kb)
mock_kb_service.update_by_id.return_value = True
mock_run_graphrag.return_value = {"status": "completed"}
mock_thread_exec.side_effect = mock_thread_return_none
handler = TaskHandler(ctx=ctx)
await handler.handle()
# Verify update_by_id was called with full default config
mock_kb_service.update_by_id.assert_called_once()
call_args = mock_kb_service.update_by_id.call_args
config = call_args[0][1]["parser_config"]["graphrag"]
assert config["use_graphrag"] is True
assert "organization" in config["entity_types"]
assert "person" in config["entity_types"]
assert "geo" in config["entity_types"]
assert "event" in config["entity_types"]
assert "category" in config["entity_types"]
assert config["method"] == "light"
assert "batch_chunk_token_size" in config
assert "retry_attempts" in config
assert "retry_backoff_seconds" in config
assert "retry_backoff_max_seconds" in config
assert "build_subgraph_timeout_per_chunk_seconds" in config
assert "build_subgraph_min_timeout_seconds" in config
assert "merge_timeout_seconds" in config
assert "resolution_timeout_seconds" in config
assert "community_timeout_seconds" in config
assert "lock_acquire_timeout_seconds" in config, \
"All GraphRAG default config parameters should be present"

View File

@@ -15,8 +15,10 @@
#
import importlib
import os
import sys
import types
from unittest.mock import MagicMock
import pytest
@@ -113,10 +115,28 @@ def raptor_module(monkeypatch):
monkeypatch.setitem(sys.modules, "common.token_utils", token_utils_module)
monkeypatch.setitem(sys.modules, "rag.graphrag.utils", graphrag_utils_module)
monkeypatch.setitem(sys.modules, "common.misc_utils", misc_utils_module)
monkeypatch.delitem(sys.modules, "rag.raptor", raising=False)
module = importlib.import_module("rag.raptor")
# Create stub parent packages and load raptor directly via spec_from_file_location
# to bypass rag/advanced_rag/__init__.py (which triggers ES connection etc.).
_test_dir = os.path.dirname(__file__)
_rag_adv_kc_dir = os.path.normpath(os.path.join(_test_dir, "../../../rag/advanced_rag/knowlege_compile"))
_rag_adv = types.ModuleType("rag.advanced_rag")
_rag_adv.__path__ = [os.path.normpath(os.path.join(_test_dir, "../../../rag/advanced_rag"))]
_rag_adv.__package__ = "rag.advanced_rag"
monkeypatch.setitem(sys.modules, "rag.advanced_rag", _rag_adv)
_rag_adv_kc = types.ModuleType("rag.advanced_rag.knowlege_compile")
_rag_adv_kc.__path__ = [_rag_adv_kc_dir]
_rag_adv_kc.__package__ = "rag.advanced_rag.knowlege_compile"
monkeypatch.setitem(sys.modules, "rag.advanced_rag.knowlege_compile", _rag_adv_kc)
monkeypatch.delitem(sys.modules, "rag.advanced_rag.knowlege_compile.raptor", raising=False)
_raptor_spec = importlib.util.spec_from_file_location(
"rag.advanced_rag.knowlege_compile.raptor",
os.path.join(_rag_adv_kc_dir, "raptor.py"),
)
module = importlib.util.module_from_spec(_raptor_spec)
sys.modules["rag.advanced_rag.knowlege_compile.raptor"] = module
_raptor_spec.loader.exec_module(module)
yield module
monkeypatch.delitem(sys.modules, "rag.raptor", raising=False)
monkeypatch.delitem(sys.modules, "rag.advanced_rag.knowlege_compile.raptor", raising=False)
class FakeChatModel:
@@ -372,7 +392,7 @@ async def test_psi_tree_builder_materializes_rebalanced_summary_layers_without_u
def fail_umap(*args, **kwargs):
raise AssertionError("Psi tree builder must use original embeddings, not UMAP")
monkeypatch.setattr(raptor_module.umap, "UMAP", fail_umap)
monkeypatch.setattr("umap.UMAP", fail_umap)
raptor = _make_raptor(raptor_module, max_cluster=2)
chunks, layers = await raptor(_chunks(), random_state=0)