Files
arc53--docsgpt/tests/worker/test_graph_extraction_enqueue.py
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
9.9 KiB
Python

"""Ingest paths enqueue graph extraction after embed for graphrag sources.
Exercises ``remote_worker`` as the representative ingest path: the remote
loader, embedding pipeline, and ``upload_index`` are mocked, so the only thing
under test is the post-embed ``extract_graph.delay`` hook. ``graphrag_available``
is forced on so the gate doesn't depend on the test env's vector store. The
``sources`` row is real (seeded via ``pg_conn``) so the hook can read the
source's ``updated_at`` for the idempotency key.
"""
from __future__ import annotations
from unittest.mock import MagicMock
import pytest
from application.parser.schema.base import Document
from application.storage.db.repositories.sources import SourcesRepository
@pytest.fixture
def _mock_remote_pipeline(monkeypatch):
from application import worker
fake_loader = MagicMock(name="remote_loader")
fake_loader.load_data.return_value = [
Document(
text="page body",
extra_info={"file_path": "guides/setup.md", "title": "setup"},
doc_id="d1",
)
]
monkeypatch.setattr(
worker.RemoteCreator, "create_loader", lambda loader: fake_loader
)
monkeypatch.setattr(
worker,
"embed_and_store_documents",
lambda docs, full_path, source_id, task, **kw: None,
)
monkeypatch.setattr(
worker, "assert_index_complete", lambda *a, **kw: None
)
monkeypatch.setattr(
worker, "upload_index", lambda full_path, file_data: None
)
# Reset constructs a real GraphStore (pgvector); not under test here.
monkeypatch.setattr(
worker, "_reset_graph_for_source", lambda *a, **kw: None
)
def _patch_delay(monkeypatch):
delay = MagicMock(name="extract_graph_delay")
monkeypatch.setattr(
"application.api.user.tasks.extract_graph.delay", delay
)
return delay
def _seed_source(pg_conn, user, config):
src = SourcesRepository(pg_conn).create(
"graph-remote", user_id=user, type="crawler", config=config,
)
return str(src["id"])
@pytest.mark.unit
class TestGraphExtractionKey:
def test_shape_and_state_sensitivity(self):
from application.worker import _source_updated_at, graph_extraction_key
sid = "11111111-1111-1111-1111-111111111111"
key_a = graph_extraction_key(
sid, _source_updated_at({"updated_at": "2026-06-23T00:00:00+00:00"})
)
key_b = graph_extraction_key(
sid, _source_updated_at({"updated_at": "2026-06-23T00:00:01+00:00"})
)
key_a_again = graph_extraction_key(
sid, _source_updated_at({"updated_at": "2026-06-23T00:00:00+00:00"})
)
assert key_a.startswith(f"extract-graph:{sid}:")
# A changed ``updated_at`` (re-ingest/re-enable) yields a fresh key.
assert key_a != key_b
# The same state collapses to one key (concurrent dups dedupe).
assert key_a == key_a_again
def test_falls_back_to_date(self):
from application.worker import _source_updated_at
assert _source_updated_at({"date": "2026-06-23T00:00:00+00:00"}) == (
"2026-06-23T00:00:00+00:00"
)
assert _source_updated_at({"updated_at": None, "date": "x"}) == "x"
assert _source_updated_at(None) == ""
@pytest.mark.unit
class TestRemoteWorkerEnqueuesGraphExtraction:
def test_graphrag_source_enqueues_after_embed(
self, task_self, pg_conn, patch_worker_db, monkeypatch,
_mock_remote_pipeline,
):
from application import worker
monkeypatch.setattr(
"application.graphrag.graphrag_available", lambda: True
)
delay = _patch_delay(monkeypatch)
config = {"kind": "graphrag", "retrieval": {"retriever": "graphrag"}}
sid = _seed_source(pg_conn, "bob", config)
worker.remote_worker(
task_self,
{"urls": ["http://example.com"]},
"graph-remote",
"bob",
"crawler",
directory="temp",
retriever="classic",
operation_mode="upload",
config=config,
source_id=sid,
)
delay.assert_called_once()
assert delay.call_args.args[0] == sid
assert delay.call_args.args[1] == "bob"
key = delay.call_args.kwargs["idempotency_key"]
assert key.startswith(f"extract-graph:{sid}:")
def test_same_state_dedupes_to_one_key(
self, task_self, pg_conn, patch_worker_db, monkeypatch,
_mock_remote_pipeline,
):
"""Two enqueues for the same source state share a key (concurrent dups)."""
from application import worker
monkeypatch.setattr(
"application.graphrag.graphrag_available", lambda: True
)
delay = _patch_delay(monkeypatch)
config = {"kind": "graphrag", "retrieval": {"retriever": "graphrag"}}
sid = _seed_source(pg_conn, "bob", config)
def _run():
worker.remote_worker(
task_self,
{"urls": ["http://example.com"]},
"graph-remote",
"bob",
"crawler",
directory="temp",
retriever="classic",
operation_mode="upload",
config=config,
source_id=sid,
)
return delay.call_args.kwargs["idempotency_key"]
assert _run() == _run()
def test_resets_graph_before_enqueue(
self, task_self, pg_conn, patch_worker_db, monkeypatch,
_mock_remote_pipeline,
):
"""A re-ingest clears the prior graph before re-enqueuing extraction."""
from application import worker
monkeypatch.setattr(
"application.graphrag.graphrag_available", lambda: True
)
reset = MagicMock(name="reset_graph")
monkeypatch.setattr(worker, "_reset_graph_for_source", reset)
delay = _patch_delay(monkeypatch)
config = {"kind": "graphrag", "retrieval": {"retriever": "graphrag"}}
sid = _seed_source(pg_conn, "bob", config)
worker.remote_worker(
task_self,
{"urls": ["http://example.com"]},
"graph-remote",
"bob",
"crawler",
directory="temp",
retriever="classic",
operation_mode="upload",
config=config,
source_id=sid,
)
reset.assert_called_once_with(sid)
delay.assert_called_once()
def test_classic_source_does_not_enqueue(
self, task_self, pg_conn, patch_worker_db, monkeypatch,
_mock_remote_pipeline,
):
from application import worker
monkeypatch.setattr(
"application.graphrag.graphrag_available", lambda: True
)
delay = _patch_delay(monkeypatch)
sid = _seed_source(pg_conn, "bob", {"kind": "classic"})
worker.remote_worker(
task_self,
{"urls": ["http://example.com"]},
"classic-remote",
"bob",
"crawler",
directory="temp",
retriever="classic",
operation_mode="upload",
config={"kind": "classic"},
source_id=sid,
)
delay.assert_not_called()
def test_graphrag_unavailable_does_not_enqueue(
self, task_self, pg_conn, patch_worker_db, monkeypatch,
_mock_remote_pipeline,
):
from application import worker
monkeypatch.setattr(
"application.graphrag.graphrag_available", lambda: False
)
delay = _patch_delay(monkeypatch)
config = {"kind": "graphrag", "retrieval": {"retriever": "graphrag"}}
sid = _seed_source(pg_conn, "bob", config)
worker.remote_worker(
task_self,
{"urls": ["http://example.com"]},
"graph-remote",
"bob",
"crawler",
directory="temp",
retriever="classic",
operation_mode="upload",
config=config,
source_id=sid,
)
delay.assert_not_called()
@pytest.mark.unit
class TestEnqueueIsolatesBrokerFailures:
def test_delay_exception_does_not_propagate(
self, pg_conn, patch_worker_db, monkeypatch
):
"""A broker hiccup in ``.delay`` must not fail an otherwise-good ingest."""
from application import worker
from application.storage.db.source_config import SourceConfig
monkeypatch.setattr(
"application.graphrag.graphrag_available", lambda: True
)
monkeypatch.setattr(
worker, "_reset_graph_for_source", lambda *a, **kw: None
)
def _boom(*a, **kw):
raise RuntimeError("broker down")
monkeypatch.setattr(
"application.api.user.tasks.extract_graph.delay", _boom
)
config = {"kind": "graphrag", "retrieval": {"retriever": "graphrag"}}
sid = _seed_source(pg_conn, "bob", config)
worker._maybe_enqueue_graph_extraction(
SourceConfig.parse(config), sid, "bob"
)
def test_updated_at_read_exception_does_not_propagate(
self, pg_conn, patch_worker_db, monkeypatch
):
"""A DB hiccup reading ``updated_at`` must also be swallowed."""
from application import worker
from application.storage.db.source_config import SourceConfig
monkeypatch.setattr(
"application.graphrag.graphrag_available", lambda: True
)
monkeypatch.setattr(
worker, "_reset_graph_for_source", lambda *a, **kw: None
)
delay = _patch_delay(monkeypatch)
def _boom(self, source_id, user_id):
raise RuntimeError("pg down")
monkeypatch.setattr(SourcesRepository, "get_any", _boom)
config = {"kind": "graphrag", "retrieval": {"retriever": "graphrag"}}
sid = _seed_source(pg_conn, "bob", config)
worker._maybe_enqueue_graph_extraction(
SourceConfig.parse(config), sid, "bob"
)
delay.assert_not_called()