Files
hkuds--lightrag/tests/pipeline/test_parse_worker_content_sanitized.py
2026-07-13 12:08:54 +08:00

115 lines
4.1 KiB
Python

"""The parse worker must align its in-memory body with the sanitized copy
that ``_persist_parsed_full_docs`` wrote to full_docs.
A parser may return ``ParseResult(content=...)`` carrying the pre-clean text
(the legacy engine returns the raw UTF-8 extraction verbatim). The worker uses
that body for ``content_summary`` / ``content_length`` on doc_status and for
the post-parse duplicate-length check, so control chars left on it would reach
doc_status (and NUL would break PostgreSQL text writes). The worker now strips
control chars off ``parsed_data_w["content"]`` right after ``parser.parse()``,
so both full_docs and doc_status see the same cleaned body.
"""
from __future__ import annotations
import asyncio
from uuid import uuid4
import numpy as np
import pytest
from lightrag import LightRAG
from lightrag.base import DocStatus
from lightrag.constants import FULL_DOCS_FORMAT_PENDING_PARSE
from lightrag.utils import EmbeddingFunc, Tokenizer, compute_mdhash_id
from lightrag.utils_pipeline import strip_lightrag_doc_prefix
pytestmark = pytest.mark.offline
class _SimpleTokenizerImpl:
def encode(self, content: str) -> list[int]:
return [ord(ch) for ch in content]
def decode(self, tokens: list[int]) -> str:
return "".join(chr(t) for t in tokens)
async def _dummy_embedding(texts: list[str]) -> np.ndarray:
return np.ones((len(texts), 8), dtype=float)
async def _dummy_llm(*args, **kwargs) -> str:
return "ok"
def _deterministic_chunking(
tokenizer,
content: str,
split_by_character,
split_by_character_only: bool,
chunk_overlap_token_size: int,
chunk_token_size: int,
) -> list[dict]:
return [{"tokens": 1, "content": f"{content}::chunk1", "chunk_order_index": 0}]
async def _build_rag(tmp_path) -> LightRAG:
rag = LightRAG(
working_dir=str(tmp_path / "wd"),
workspace=f"parseworker-{uuid4().hex[:8]}",
llm_model_func=_dummy_llm,
embedding_func=EmbeddingFunc(
embedding_dim=8, max_token_size=8192, func=_dummy_embedding
),
tokenizer=Tokenizer("mock-tokenizer", _SimpleTokenizerImpl()),
chunking_func=_deterministic_chunking,
max_parallel_insert=1,
)
await rag.initialize_storages()
return rag
def test_legacy_parse_worker_sanitizes_doc_status_and_full_docs(tmp_path, monkeypatch):
"""A PENDING_PARSE .txt whose body carries C0 separators: after the legacy
worker runs, neither doc_status (content_summary/content_length) nor
full_docs retains \\x1c-\\x1f / NUL, and the two agree on the cleaned body."""
async def _run():
input_dir = tmp_path / "input"
input_dir.mkdir()
monkeypatch.setenv("INPUT_DIR", str(input_dir))
rag = await _build_rag(tmp_path)
try:
dirty = "Alpha\x1cbody\x1d\x1f\x00 paragraph with real words."
clean = "Alphabody中文 paragraph with real words."
source_path = input_dir / "doc.txt"
source_path.write_text(dirty, encoding="utf-8")
await rag.apipeline_enqueue_documents(
"",
file_paths=str(source_path),
docs_format=FULL_DOCS_FORMAT_PENDING_PARSE,
parse_engine="legacy",
)
doc_id = compute_mdhash_id("doc.txt", prefix="doc-")
await rag.apipeline_process_enqueue_documents()
status = await rag.doc_status.get_by_id(doc_id)
assert status["status"] == DocStatus.PROCESSED
# content_summary (a substring of the body) and the persisted body
# must both be control-char-free.
summary = status["content_summary"]
assert not any(c in summary for c in "\x1c\x1d\x1f\x00")
full = await rag.full_docs.get_by_id(doc_id)
body = strip_lightrag_doc_prefix(full["content"], full.get("parse_format"))
assert body == clean
# content_length matches the cleaned, persisted body length — not
# the longer pre-clean extraction.
assert status["content_length"] == len(clean)
finally:
await rag.finalize_storages()
asyncio.run(_run())