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
72 lines
2.1 KiB
Python
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
|
|
)
|