Files
wehub-resource-sync fed8b2eed7
Backend release / release (push) Waiting to run
Bandit Security Scan / bandit_scan (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / manifest (push) Blocked by required conditions
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / manifest (push) Blocked by required conditions
Python linting / ruff (push) Waiting to run
Run python tests with pytest / Run tests and count coverage (3.12) (push) Waiting to run
React Widget Build / build (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:28:29 +08:00

316 lines
10 KiB
Python

"""Smoke test for ``application.worker.ingest_worker``.
``ingest_worker`` does **not** write to Postgres directly. Its PG
side-effect (creating the ``sources`` row) goes through the
``upload_index`` HTTP callback to the backend, which writes to PG in
its own request context. That callback is intentionally out of scope
here — we can't reach it from the worker test without spinning up the
Flask app.
What we *can* smoke: the task body runs end-to-end, the pipeline gets
invoked with the expected job metadata, and ``upload_index`` is handed
a ``file_data`` payload that carries the caller-provided ``user``,
``job_name``, and ``retriever``. That's the contract the
backend-facing write depends on.
"""
from __future__ import annotations
import uuid
from io import BytesIO
from unittest.mock import MagicMock
import pytest
from application.parser.schema.base import Document
def _patch_ingest_pipeline(monkeypatch, captured):
"""Stub the non-PG boundaries used by ``ingest_worker``.
Storage, the directory reader, the embedding pipeline, and
``upload_index`` are all replaced with cheap fakes so the worker
runs end-to-end inside the test process. ``captured`` is appended
with each ``upload_index`` payload so callers can assert on the
derived ``source_id``.
"""
from application import worker
fake_storage = MagicMock(name="storage")
fake_storage.is_directory.return_value = False
fake_storage.get_file.return_value = BytesIO(b"hello")
monkeypatch.setattr(
worker.StorageCreator, "get_storage", lambda: fake_storage
)
fake_reader = MagicMock(name="reader")
fake_reader.load_data.return_value = [
Document(text="hello body", extra_info={"source": "a.txt"})
]
fake_reader.directory_structure = {
"a.txt": {
"type": "text/plain",
"size_bytes": 5,
"token_count": 2,
}
}
monkeypatch.setattr(
worker, "SimpleDirectoryReader", lambda *a, **kw: fake_reader
)
monkeypatch.setattr(
worker,
"embed_and_store_documents",
lambda docs, full_path, source_id, task, **kw: None,
)
monkeypatch.setattr(
worker, "upload_index",
lambda full_path, file_data: captured.append(file_data),
)
def _spy_chunker(monkeypatch):
"""Spy on ``ChunkerCreator.create_chunker`` and return the recorded kwargs.
Replaces it with a fake that records the call's kwargs and returns a
chunker whose ``chunk`` passes documents through unchanged, so the
config the worker threads into chunking can be asserted.
"""
from application import worker
calls: list[dict] = []
def _create_chunker(strategy, **kwargs):
calls.append({"strategy": strategy, **kwargs})
chunker = MagicMock(name="chunker")
chunker.chunk.side_effect = lambda documents: documents
return chunker
monkeypatch.setattr(
worker.ChunkerCreator, "create_chunker", staticmethod(_create_chunker)
)
return calls
@pytest.mark.unit
class TestIngestWorker:
def test_invokes_upload_index_with_expected_payload(
self, patch_worker_db, task_self, monkeypatch
):
from application import worker
captured: list[dict] = []
_patch_ingest_pipeline(monkeypatch, captured)
result = worker.ingest_worker(
task_self,
directory="inputs",
formats=[".txt"],
job_name="job1",
file_path="inputs/eve/job1/a.txt",
filename="a.txt",
user="eve",
retriever="classic",
)
assert result["limited"] is False
assert result["user"] == "eve"
assert result["name_job"] == "job1"
assert len(captured) == 1
payload = captured[0]
assert payload["user"] == "eve"
assert payload["name"] == "job1"
assert payload["retriever"] == "classic"
assert payload["type"] == "local"
# A fresh source UUID is minted for the backend /upload_index route.
assert payload["id"]
@pytest.mark.unit
class TestIngestWorkerConfigThreading:
"""``config.chunking`` must drive the chunker the worker builds (D1/D8)."""
def test_no_config_uses_classic_defaults(
self, patch_worker_db, task_self, monkeypatch
):
from application import worker
captured: list[dict] = []
_patch_ingest_pipeline(monkeypatch, captured)
calls = _spy_chunker(monkeypatch)
worker.ingest_worker(
task_self,
directory="inputs",
formats=[".txt"],
job_name="job1",
file_path="inputs/eve/job1/a.txt",
filename="a.txt",
user="eve",
)
assert len(calls) == 1
# Byte-identical-defaults guarantee: empty config → classic 1250/150.
assert calls[0]["strategy"] == "classic_chunk"
assert calls[0]["chunking_strategy"] == "classic_chunk"
assert calls[0]["max_tokens"] == 1250
assert calls[0]["min_tokens"] == 150
assert calls[0]["duplicate_headers"] is False
def test_non_default_config_is_threaded(
self, patch_worker_db, task_self, monkeypatch
):
from application import worker
captured: list[dict] = []
_patch_ingest_pipeline(monkeypatch, captured)
calls = _spy_chunker(monkeypatch)
worker.ingest_worker(
task_self,
directory="inputs",
formats=[".txt"],
job_name="job1",
file_path="inputs/eve/job1/a.txt",
filename="a.txt",
user="eve",
config={
"chunking": {
"strategy": "recursive",
"max_tokens": 800,
"min_tokens": 50,
}
},
)
assert len(calls) == 1
assert calls[0]["strategy"] == "recursive"
assert calls[0]["chunking_strategy"] == "recursive"
assert calls[0]["max_tokens"] == 800
assert calls[0]["min_tokens"] == 50
@pytest.mark.unit
class TestIngestWorkerDeterministicSourceId:
"""Retried ingests with the same key should land on the same source row."""
def test_uses_uuid5_when_idempotency_key_present(
self, patch_worker_db, task_self, monkeypatch
):
from application import worker
captured: list[dict] = []
_patch_ingest_pipeline(monkeypatch, captured)
worker.ingest_worker(
task_self,
directory="inputs",
formats=[".txt"],
job_name="job1",
file_path="inputs/eve/job1/a.txt",
filename="a.txt",
user="eve",
idempotency_key="abc",
)
worker.ingest_worker(
task_self,
directory="inputs",
formats=[".txt"],
job_name="job1",
file_path="inputs/eve/job1/a.txt",
filename="a.txt",
user="eve",
idempotency_key="abc",
)
expected = str(
uuid.uuid5(worker.DOCSGPT_INGEST_NAMESPACE, "abc")
)
assert len(captured) == 2
assert captured[0]["id"] == expected
assert captured[1]["id"] == expected
def test_falls_back_to_uuid4_without_key(
self, patch_worker_db, task_self, monkeypatch
):
from application import worker
captured: list[dict] = []
_patch_ingest_pipeline(monkeypatch, captured)
for _ in range(2):
worker.ingest_worker(
task_self,
directory="inputs",
formats=[".txt"],
job_name="job1",
file_path="inputs/eve/job1/a.txt",
filename="a.txt",
user="eve",
)
assert len(captured) == 2
# Random uuid4 fallback: two runs must produce different ids.
assert captured[0]["id"] != captured[1]["id"]
def test_double_ingest_writes_one_source_row(
self, pg_conn, patch_worker_db, task_self, monkeypatch
):
"""Run the worker body twice with the same key; assert the
backend-side ``upload_index`` would persist one row, not two.
``upload_index`` is the actual backend write path; the worker
feeds it a ``file_data`` payload keyed by the derived id. We
emulate that path locally (``SourcesRepository.create``) on the
first call and assert the second call hits the existing-row
branch instead of inserting again.
"""
from application import worker
from application.storage.db.repositories.sources import SourcesRepository
captured: list[dict] = []
_patch_ingest_pipeline(monkeypatch, captured)
def _persist(full_path, file_data):
captured.append(file_data)
repo = SourcesRepository(pg_conn)
existing = repo.get(file_data["id"], file_data["user"])
if existing is None:
repo.create(
file_data["name"],
source_id=file_data["id"],
user_id=file_data["user"],
type=file_data["type"],
retriever=file_data["retriever"],
tokens=file_data["tokens"],
)
monkeypatch.setattr(worker, "upload_index", _persist)
for _ in range(2):
worker.ingest_worker(
task_self,
directory="inputs",
formats=[".txt"],
job_name="job1",
file_path="inputs/eve/job1/a.txt",
filename="a.txt",
user="eve",
idempotency_key="dedupe-key",
)
expected = str(
uuid.uuid5(worker.DOCSGPT_INGEST_NAMESPACE, "dedupe-key")
)
assert captured[0]["id"] == expected
assert captured[1]["id"] == expected
# Exactly one row in PG for that derived id.
result = pg_conn.exec_driver_sql(
"SELECT count(*) FROM sources WHERE id = %s",
(expected,),
).fetchone()
assert result[0] == 1