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
316 lines
10 KiB
Python
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
|