docling-studio/document-parser/tests/test_lifecycle_aggregation.py
Pier-Jean Malandrino 4c30e5eb8f feat(#203): per (document, store) ingestion state
Introduces the data layer for multi-store ingestion. Documents can now
live in multiple stores, each with its own Ingested/Stale/Failed state.
The doc-level lifecycle (#202) becomes the aggregate over all per-store
links, computed by a pure domain function.

Domain
- Store entity (name, slug, kind, embedder, config, is_default)
- DocumentStoreLink entity with mark_ingested / mark_stale / mark_failed
  helpers
- StoreKind and DocumentStoreLinkState enums
- aggregate_lifecycle(): pure function — Failed > Stale > Ingested
  > fallback (the doc's pre-link Uploaded/Parsed/Chunked state)

Persistence
- New tables 'stores' and 'document_store_links' with the right indexes
  (doc_id, store_id, state) and a UNIQUE (doc, store) on the link
- Default 'opensearch' store seeded idempotently in init_db, embedder
  pulled from DEFAULT_EMBEDDER (fallback bge-m3)
- SqliteStoreRepository (find_by_slug, find_by_id, get_default, …)
- SqliteDocumentStoreLinkRepository with ON CONFLICT … DO UPDATE upsert

Ports
- StoreRepository and DocumentStoreLinkRepository protocols added

Tests
- 14 new tests: seed idempotency, insert/find round-trips, UNIQUE
  constraint, cascade delete with the document, every link state
  round-trips, aggregation rule with all branches

Refs #203
2026-04-29 17:08:34 +02:00

72 lines
2.1 KiB
Python

"""Tests for the document lifecycle aggregation rule (#203)."""
from __future__ import annotations
from domain.lifecycle_aggregation import aggregate_lifecycle
from domain.models import DocumentStoreLink
from domain.value_objects import DocumentLifecycleState, DocumentStoreLinkState
def _link(state: DocumentStoreLinkState) -> DocumentStoreLink:
return DocumentStoreLink(
id=f"link-{state.value}",
document_id="doc-1",
store_id=f"store-{state.value}",
state=state,
)
def test_no_links_returns_fallback() -> None:
assert (
aggregate_lifecycle([], fallback=DocumentLifecycleState.CHUNKED)
== DocumentLifecycleState.CHUNKED
)
def test_single_ingested_link_makes_doc_ingested() -> None:
assert (
aggregate_lifecycle(
[_link(DocumentStoreLinkState.INGESTED)],
fallback=DocumentLifecycleState.CHUNKED,
)
== DocumentLifecycleState.INGESTED
)
def test_any_stale_link_makes_doc_stale() -> None:
"""Stale outranks Ingested — a doc with one stale store is considered stale."""
assert (
aggregate_lifecycle(
[
_link(DocumentStoreLinkState.INGESTED),
_link(DocumentStoreLinkState.STALE),
],
fallback=DocumentLifecycleState.CHUNKED,
)
== DocumentLifecycleState.STALE
)
def test_any_failed_link_makes_doc_failed() -> None:
"""Failed outranks every other link state."""
assert (
aggregate_lifecycle(
[
_link(DocumentStoreLinkState.INGESTED),
_link(DocumentStoreLinkState.STALE),
_link(DocumentStoreLinkState.FAILED),
],
fallback=DocumentLifecycleState.CHUNKED,
)
== DocumentLifecycleState.FAILED
)
def test_only_failed_link_makes_doc_failed() -> None:
assert (
aggregate_lifecycle(
[_link(DocumentStoreLinkState.FAILED)],
fallback=DocumentLifecycleState.CHUNKED,
)
== DocumentLifecycleState.FAILED
)