Make the governed RAG evidence path executable end to end
Some checks failed
verify / verify (push) Has been cancelled
Some checks failed
verify / verify (push) Has been cancelled
Separate local parsing from model indexing, bind review decisions to immutable manifests, persist vectors behind active profiles, and expose retrieval, chat, evaluation, and document workflows through the React workbench. Constraint: Live Bailian authentication currently fails for all three configured capabilities Rejected: Direct upload-to-embedding flow | bypasses local review and manifest binding Confidence: high Scope-risk: broad Directive: Keep private-data deployment blocked until authentication, RBAC, and separate database roles land Tested: make verify; fresh and replay Docker document smoke; worker recovery smoke; frozen synthetic evaluation; migration 0003-0004 roundtrip Not-tested: Successful live Bailian calls, OCR, real multi-user authorization
This commit is contained in:
575
backend/tests/unit/test_indexing_service.py
Normal file
575
backend/tests/unit/test_indexing_service.py
Normal file
@@ -0,0 +1,575 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import math
|
||||
import uuid
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from dataclasses import dataclass, field, replace
|
||||
|
||||
import pytest
|
||||
|
||||
from app.persistence.job_queue import JobLease, LeaseLostError
|
||||
from app.persistence.retrieval import ActiveEmbeddingProfile
|
||||
from app.ports.model_providers import (
|
||||
EmbeddingResult,
|
||||
ModelProviderError,
|
||||
ProviderErrorKind,
|
||||
ProviderUsage,
|
||||
)
|
||||
from app.services.indexing import (
|
||||
ApprovedIndexingPlan,
|
||||
AssignmentProgress,
|
||||
AssignmentStatus,
|
||||
CachedEmbedding,
|
||||
DocumentIndexingService,
|
||||
EmbeddingCacheLookup,
|
||||
EmbeddingWrite,
|
||||
IndexingItem,
|
||||
IndexingNotReadyError,
|
||||
InvalidEmbeddingResponseError,
|
||||
InvocationStatus,
|
||||
embedding_cache_key,
|
||||
)
|
||||
|
||||
DOCUMENT_VERSION_ID = uuid.UUID("10000000-0000-0000-0000-000000000001")
|
||||
KNOWLEDGE_BASE_ID = uuid.UUID("20000000-0000-0000-0000-000000000002")
|
||||
JOB_ID = uuid.UUID("30000000-0000-0000-0000-000000000003")
|
||||
LEASE_TOKEN = uuid.UUID("40000000-0000-0000-0000-000000000004")
|
||||
TRACE_ID = uuid.UUID("50000000-0000-0000-0000-000000000005")
|
||||
LEASE = JobLease(JOB_ID, "embedding-worker-a", LEASE_TOKEN)
|
||||
PROFILE = ActiveEmbeddingProfile(
|
||||
profile_hash="a" * 64,
|
||||
model="text-embedding-v4",
|
||||
dimension=1024,
|
||||
)
|
||||
|
||||
|
||||
def _text_hash(text: str) -> str:
|
||||
return hashlib.sha256(text.encode()).hexdigest()
|
||||
|
||||
|
||||
def _item(index: int, *, status: AssignmentStatus = "PENDING") -> IndexingItem:
|
||||
text = f"第 {index} 个已批准地质文本"
|
||||
return IndexingItem(
|
||||
chunk_id=uuid.UUID(int=1_000 + index),
|
||||
ordinal=index,
|
||||
embedding_text=text,
|
||||
embedding_text_sha256=_text_hash(text),
|
||||
assignment_status=status,
|
||||
)
|
||||
|
||||
|
||||
def _plan(count: int, *, profile: ActiveEmbeddingProfile = PROFILE) -> ApprovedIndexingPlan:
|
||||
return ApprovedIndexingPlan(
|
||||
knowledge_base_id=KNOWLEDGE_BASE_ID,
|
||||
document_version_id=DOCUMENT_VERSION_ID,
|
||||
review_state="CLOUD_APPROVED",
|
||||
outbound_manifest_sha256="b" * 64,
|
||||
expected_count=count,
|
||||
profile=profile,
|
||||
items=tuple(_item(index) for index in range(count)),
|
||||
)
|
||||
|
||||
|
||||
def _vector(index: int = 0) -> tuple[float, ...]:
|
||||
return (float(index + 1),) + (0.0,) * 1023
|
||||
|
||||
|
||||
@dataclass
|
||||
class SpyRepository:
|
||||
plan: ApprovedIndexingPlan
|
||||
events: list[str] = field(default_factory=list)
|
||||
ready_chunks: set[uuid.UUID] = field(default_factory=set)
|
||||
cache: dict[str, CachedEmbedding] = field(default_factory=dict)
|
||||
writes: list[tuple[EmbeddingWrite, ...]] = field(default_factory=list)
|
||||
begin_calls: list[dict[str, object]] = field(default_factory=list)
|
||||
finish_calls: list[dict[str, object]] = field(default_factory=list)
|
||||
write_leases: list[JobLease] = field(default_factory=list)
|
||||
in_repository: bool = False
|
||||
activation_allowed: bool = True
|
||||
report_incomplete: bool = False
|
||||
lose_lease_on: str | None = None
|
||||
persist_count: int = 0
|
||||
|
||||
def _enter(self, event: str) -> None:
|
||||
assert self.in_repository is False
|
||||
self.in_repository = True
|
||||
self.events.append(event)
|
||||
|
||||
def _leave(self) -> None:
|
||||
self.in_repository = False
|
||||
|
||||
def _maybe_lose(self, operation: str) -> None:
|
||||
if self.lose_lease_on == operation:
|
||||
raise LeaseLostError("lease moved")
|
||||
|
||||
def load_approved_plan(
|
||||
self,
|
||||
*,
|
||||
lease: JobLease,
|
||||
document_version_id: uuid.UUID,
|
||||
) -> ApprovedIndexingPlan:
|
||||
self._enter("repo.load")
|
||||
try:
|
||||
assert lease == LEASE
|
||||
assert document_version_id == DOCUMENT_VERSION_ID
|
||||
items = tuple(
|
||||
replace(
|
||||
item,
|
||||
assignment_status=(
|
||||
"READY" if item.chunk_id in self.ready_chunks else item.assignment_status
|
||||
),
|
||||
)
|
||||
for item in self.plan.items
|
||||
)
|
||||
return replace(self.plan, items=items)
|
||||
finally:
|
||||
self._leave()
|
||||
|
||||
def lookup_cache(
|
||||
self,
|
||||
*,
|
||||
lease: JobLease,
|
||||
lookups: Sequence[EmbeddingCacheLookup],
|
||||
) -> Mapping[str, CachedEmbedding]:
|
||||
self._enter(f"repo.cache:{len(lookups)}")
|
||||
try:
|
||||
assert lease == LEASE
|
||||
cache_keys = tuple(lookup.cache_key for lookup in lookups)
|
||||
return {key: self.cache[key] for key in cache_keys if key in self.cache}
|
||||
finally:
|
||||
self._leave()
|
||||
|
||||
def begin_model_invocation(
|
||||
self,
|
||||
*,
|
||||
lease: JobLease,
|
||||
trace_id: uuid.UUID,
|
||||
profile_hash: str,
|
||||
model: str,
|
||||
item_count: int,
|
||||
) -> uuid.UUID:
|
||||
self._enter(f"repo.begin:{item_count}")
|
||||
try:
|
||||
self.write_leases.append(lease)
|
||||
self._maybe_lose("begin")
|
||||
call = {
|
||||
"lease": lease,
|
||||
"trace_id": trace_id,
|
||||
"profile_hash": profile_hash,
|
||||
"model": model,
|
||||
"item_count": item_count,
|
||||
}
|
||||
self.begin_calls.append(call)
|
||||
return uuid.UUID(int=9_000 + len(self.begin_calls))
|
||||
finally:
|
||||
self._leave()
|
||||
|
||||
def finish_model_invocation(
|
||||
self,
|
||||
*,
|
||||
lease: JobLease,
|
||||
invocation_id: uuid.UUID,
|
||||
status: InvocationStatus,
|
||||
provider_request_id: str | None,
|
||||
usage: ProviderUsage,
|
||||
elapsed_ms: float,
|
||||
error_code: str | None,
|
||||
) -> None:
|
||||
self._enter(f"repo.finish:{status}")
|
||||
try:
|
||||
self.write_leases.append(lease)
|
||||
self._maybe_lose("finish")
|
||||
self.finish_calls.append(
|
||||
{
|
||||
"lease": lease,
|
||||
"invocation_id": invocation_id,
|
||||
"status": status,
|
||||
"provider_request_id": provider_request_id,
|
||||
"usage": usage,
|
||||
"elapsed_ms": elapsed_ms,
|
||||
"error_code": error_code,
|
||||
}
|
||||
)
|
||||
finally:
|
||||
self._leave()
|
||||
|
||||
def fenced_persist_batch(
|
||||
self,
|
||||
*,
|
||||
lease: JobLease,
|
||||
document_version_id: uuid.UUID,
|
||||
profile_hash: str,
|
||||
writes: Sequence[EmbeddingWrite],
|
||||
) -> AssignmentProgress:
|
||||
self._enter(f"repo.persist:{len(writes)}")
|
||||
try:
|
||||
self.write_leases.append(lease)
|
||||
self._maybe_lose("persist")
|
||||
assert document_version_id == DOCUMENT_VERSION_ID
|
||||
assert profile_hash == self.plan.profile.profile_hash
|
||||
assert 1 <= len(writes) <= 10
|
||||
batch = tuple(writes)
|
||||
self.writes.append(batch)
|
||||
self.persist_count += 1
|
||||
for write in batch:
|
||||
self.ready_chunks.add(write.chunk_id)
|
||||
if write.source == "provider":
|
||||
assert write.embedding is not None
|
||||
self.cache[write.cache_key] = CachedEmbedding(
|
||||
cache_key=write.cache_key,
|
||||
profile_hash=write.profile_hash,
|
||||
embedding_text_sha256=write.embedding_text_sha256,
|
||||
resolved_model=write.resolved_model,
|
||||
dimension=1024,
|
||||
)
|
||||
ready_count = len(self.ready_chunks)
|
||||
if self.report_incomplete and ready_count:
|
||||
ready_count -= 1
|
||||
return AssignmentProgress(len(self.plan.items), ready_count)
|
||||
finally:
|
||||
self._leave()
|
||||
|
||||
def fenced_activate(
|
||||
self,
|
||||
*,
|
||||
lease: JobLease,
|
||||
document_version_id: uuid.UUID,
|
||||
profile_hash: str,
|
||||
expected_count: int,
|
||||
) -> bool:
|
||||
self._enter("repo.activate")
|
||||
try:
|
||||
self.write_leases.append(lease)
|
||||
self._maybe_lose("activate")
|
||||
assert document_version_id == DOCUMENT_VERSION_ID
|
||||
assert profile_hash == self.plan.profile.profile_hash
|
||||
return (
|
||||
self.activation_allowed
|
||||
and expected_count == len(self.plan.items)
|
||||
and len(self.ready_chunks) == expected_count
|
||||
)
|
||||
finally:
|
||||
self._leave()
|
||||
|
||||
|
||||
ResultFactory = Callable[[Sequence[str], int], EmbeddingResult]
|
||||
|
||||
|
||||
@dataclass
|
||||
class SpyEmbeddingProvider:
|
||||
repository: SpyRepository
|
||||
model: str = "text-embedding-v4"
|
||||
result_factory: ResultFactory | None = None
|
||||
failures: dict[int, ModelProviderError] = field(default_factory=dict)
|
||||
calls: list[tuple[str, ...]] = field(default_factory=list)
|
||||
|
||||
async def embed_documents(self, texts: Sequence[str]) -> EmbeddingResult:
|
||||
assert self.repository.in_repository is False
|
||||
values = tuple(texts)
|
||||
assert 1 <= len(values) <= 10
|
||||
self.calls.append(values)
|
||||
call_number = len(self.calls)
|
||||
self.repository.events.append(f"provider:{len(values)}")
|
||||
if call_number in self.failures:
|
||||
raise self.failures[call_number]
|
||||
if self.result_factory is not None:
|
||||
return self.result_factory(values, call_number)
|
||||
return EmbeddingResult(
|
||||
vectors=tuple(_vector(index) for index in range(len(values))),
|
||||
model=self.model,
|
||||
request_id=f"embed-request-{call_number}",
|
||||
usage=ProviderUsage(input_tokens=len(values), total_tokens=len(values)),
|
||||
elapsed_ms=4.0,
|
||||
)
|
||||
|
||||
async def embed_query(self, text: str) -> EmbeddingResult:
|
||||
return await self.embed_documents((text,))
|
||||
|
||||
|
||||
def _service(
|
||||
repository: SpyRepository,
|
||||
provider: SpyEmbeddingProvider,
|
||||
*,
|
||||
synthetic_provider: SpyEmbeddingProvider | None = None,
|
||||
) -> DocumentIndexingService:
|
||||
return DocumentIndexingService(
|
||||
repository=repository,
|
||||
embedding_provider=provider,
|
||||
synthetic_embedding_provider=synthetic_provider,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_batches_are_ten_and_provider_runs_between_short_repository_calls() -> None:
|
||||
repository = SpyRepository(_plan(12))
|
||||
provider = SpyEmbeddingProvider(repository)
|
||||
|
||||
result = await _service(repository, provider).index_document_version(
|
||||
lease=LEASE,
|
||||
document_version_id=DOCUMENT_VERSION_ID,
|
||||
trace_id=TRACE_ID,
|
||||
)
|
||||
|
||||
assert [len(call) for call in provider.calls] == [10, 2]
|
||||
assert [len(batch) for batch in repository.writes] == [10, 2]
|
||||
assert repository.events == [
|
||||
"repo.load",
|
||||
"repo.cache:10",
|
||||
"repo.cache:2",
|
||||
"repo.begin:10",
|
||||
"provider:10",
|
||||
"repo.finish:SUCCEEDED",
|
||||
"repo.persist:10",
|
||||
"repo.begin:2",
|
||||
"provider:2",
|
||||
"repo.finish:SUCCEEDED",
|
||||
"repo.persist:2",
|
||||
"repo.activate",
|
||||
]
|
||||
assert all(lease == LEASE for lease in repository.write_leases)
|
||||
assert result.provider_call_count == 2
|
||||
assert result.ready_count == 12
|
||||
assert result.activated is True
|
||||
|
||||
provider_writes = [write for batch in repository.writes for write in batch]
|
||||
assert [write.batch_index for write in provider_writes[:10]] == list(range(10))
|
||||
assert provider_writes[0].embedding == _vector(0)
|
||||
assert provider_writes[1].embedding == _vector(1)
|
||||
assert all("embedding_text" not in call for call in repository.finish_calls)
|
||||
assert all("embedding" not in call for call in repository.finish_calls)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cache_key_hits_skip_model_and_only_misses_are_embedded() -> None:
|
||||
plan = _plan(3)
|
||||
repository = SpyRepository(plan)
|
||||
for item in plan.items[:2]:
|
||||
key = embedding_cache_key(item.embedding_text_sha256, PROFILE.profile_hash)
|
||||
repository.cache[key] = CachedEmbedding(
|
||||
cache_key=key,
|
||||
profile_hash=PROFILE.profile_hash,
|
||||
embedding_text_sha256=item.embedding_text_sha256,
|
||||
resolved_model=PROFILE.model,
|
||||
dimension=1024,
|
||||
)
|
||||
provider = SpyEmbeddingProvider(repository)
|
||||
|
||||
result = await _service(repository, provider).index_document_version(
|
||||
lease=LEASE,
|
||||
document_version_id=DOCUMENT_VERSION_ID,
|
||||
trace_id=TRACE_ID,
|
||||
)
|
||||
|
||||
expected_key = hashlib.sha256(
|
||||
f"{plan.items[0].embedding_text_sha256}{PROFILE.profile_hash}".encode()
|
||||
).hexdigest()
|
||||
assert (
|
||||
embedding_cache_key(plan.items[0].embedding_text_sha256, PROFILE.profile_hash)
|
||||
== expected_key
|
||||
)
|
||||
assert provider.calls == [(plan.items[2].embedding_text,)]
|
||||
assert result.cache_hit_count == 2
|
||||
assert result.newly_embedded_count == 1
|
||||
assert [write.source for batch in repository.writes for write in batch] == [
|
||||
"cache",
|
||||
"cache",
|
||||
"provider",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_partial_batch_resume_never_reembeds_persisted_first_batch() -> None:
|
||||
repository = SpyRepository(_plan(12))
|
||||
upstream_failure = ModelProviderError(
|
||||
operation="embedding.document",
|
||||
kind=ProviderErrorKind.UPSTREAM,
|
||||
status_code=503,
|
||||
retryable=True,
|
||||
)
|
||||
first_provider = SpyEmbeddingProvider(repository, failures={2: upstream_failure})
|
||||
|
||||
with pytest.raises(ModelProviderError):
|
||||
await _service(repository, first_provider).index_document_version(
|
||||
lease=LEASE,
|
||||
document_version_id=DOCUMENT_VERSION_ID,
|
||||
trace_id=TRACE_ID,
|
||||
)
|
||||
|
||||
assert len(repository.ready_chunks) == 10
|
||||
assert repository.finish_calls[-1]["status"] == "FAILED"
|
||||
assert repository.finish_calls[-1]["error_code"] == "EMBEDDING_UPSTREAM"
|
||||
|
||||
repository.events.clear()
|
||||
second_provider = SpyEmbeddingProvider(repository)
|
||||
result = await _service(repository, second_provider).index_document_version(
|
||||
lease=LEASE,
|
||||
document_version_id=DOCUMENT_VERSION_ID,
|
||||
trace_id=TRACE_ID,
|
||||
)
|
||||
|
||||
assert second_provider.calls == [
|
||||
tuple(item.embedding_text for item in repository.plan.items[10:])
|
||||
]
|
||||
assert result.newly_embedded_count == 2
|
||||
assert result.ready_count == 12
|
||||
|
||||
|
||||
def _invalid_result(case: str) -> ResultFactory:
|
||||
def factory(texts: Sequence[str], _call_number: int) -> EmbeddingResult:
|
||||
vectors = tuple(_vector(index) for index in range(len(texts)))
|
||||
model = PROFILE.model
|
||||
elapsed_ms = 1.0
|
||||
if case == "count":
|
||||
vectors = vectors[:-1]
|
||||
elif case == "dimension":
|
||||
vectors = ((1.0, 0.0),) + vectors[1:]
|
||||
elif case == "nonfinite":
|
||||
vectors = ((math.nan,) + (0.0,) * 1023,) + vectors[1:]
|
||||
elif case == "zero":
|
||||
vectors = ((0.0,) * 1024,) + vectors[1:]
|
||||
elif case == "model":
|
||||
model = "wrong-embedding-model"
|
||||
elif case == "elapsed":
|
||||
elapsed_ms = -1.0
|
||||
return EmbeddingResult(
|
||||
vectors=vectors,
|
||||
model=model,
|
||||
request_id="invalid-response-id",
|
||||
usage=ProviderUsage(input_tokens=len(texts), total_tokens=len(texts)),
|
||||
elapsed_ms=elapsed_ms,
|
||||
)
|
||||
|
||||
return factory
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("case", ["count", "dimension", "nonfinite", "zero", "model", "elapsed"])
|
||||
async def test_invalid_provider_response_fails_closed_before_vector_persistence(case: str) -> None:
|
||||
repository = SpyRepository(_plan(2))
|
||||
provider = SpyEmbeddingProvider(repository, result_factory=_invalid_result(case))
|
||||
|
||||
with pytest.raises(InvalidEmbeddingResponseError):
|
||||
await _service(repository, provider).index_document_version(
|
||||
lease=LEASE,
|
||||
document_version_id=DOCUMENT_VERSION_ID,
|
||||
trace_id=TRACE_ID,
|
||||
)
|
||||
|
||||
assert repository.finish_calls[-1]["status"] == "FAILED"
|
||||
assert repository.finish_calls[-1]["error_code"] == "INVALID_EMBEDDING_RESPONSE"
|
||||
assert repository.writes == []
|
||||
assert "repo.activate" not in repository.events
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("kind", "expected_status"),
|
||||
[
|
||||
(ProviderErrorKind.AUTHENTICATION, "FAILED"),
|
||||
(ProviderErrorKind.INVALID_REQUEST, "FAILED"),
|
||||
(ProviderErrorKind.RATE_LIMITED, "FAILED"),
|
||||
(ProviderErrorKind.UPSTREAM, "FAILED"),
|
||||
(ProviderErrorKind.TIMEOUT, "UNKNOWN"),
|
||||
],
|
||||
)
|
||||
async def test_provider_failures_are_not_retried_by_service_and_never_activate(
|
||||
kind: ProviderErrorKind,
|
||||
expected_status: InvocationStatus,
|
||||
) -> None:
|
||||
repository = SpyRepository(_plan(1))
|
||||
failure = ModelProviderError(
|
||||
operation="embedding.document",
|
||||
kind=kind,
|
||||
status_code=401 if kind is ProviderErrorKind.AUTHENTICATION else 503,
|
||||
retryable=kind not in {ProviderErrorKind.AUTHENTICATION, ProviderErrorKind.INVALID_REQUEST},
|
||||
)
|
||||
provider = SpyEmbeddingProvider(repository, failures={1: failure})
|
||||
|
||||
with pytest.raises(ModelProviderError):
|
||||
await _service(repository, provider).index_document_version(
|
||||
lease=LEASE,
|
||||
document_version_id=DOCUMENT_VERSION_ID,
|
||||
trace_id=TRACE_ID,
|
||||
)
|
||||
|
||||
assert len(provider.calls) == 1
|
||||
assert repository.finish_calls[-1]["status"] == expected_status
|
||||
assert repository.writes == []
|
||||
assert "repo.activate" not in repository.events
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("lease_loss_operation", ["finish", "persist", "activate"])
|
||||
async def test_lease_loss_blocks_every_subsequent_write_and_activation(
|
||||
lease_loss_operation: str,
|
||||
) -> None:
|
||||
repository = SpyRepository(_plan(1), lose_lease_on=lease_loss_operation)
|
||||
provider = SpyEmbeddingProvider(repository)
|
||||
|
||||
with pytest.raises(LeaseLostError):
|
||||
await _service(repository, provider).index_document_version(
|
||||
lease=LEASE,
|
||||
document_version_id=DOCUMENT_VERSION_ID,
|
||||
trace_id=TRACE_ID,
|
||||
)
|
||||
|
||||
if lease_loss_operation == "finish":
|
||||
assert repository.writes == []
|
||||
assert "repo.persist:1" not in repository.events
|
||||
assert "repo.activate" not in repository.events
|
||||
elif lease_loss_operation == "persist":
|
||||
assert repository.writes == []
|
||||
assert "repo.activate" not in repository.events
|
||||
else:
|
||||
assert repository.events[-1] == "repo.activate"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_activation_requires_expected_count_to_equal_ready_count() -> None:
|
||||
repository = SpyRepository(_plan(2), report_incomplete=True)
|
||||
provider = SpyEmbeddingProvider(repository)
|
||||
|
||||
with pytest.raises(IndexingNotReadyError):
|
||||
await _service(repository, provider).index_document_version(
|
||||
lease=LEASE,
|
||||
document_version_id=DOCUMENT_VERSION_ID,
|
||||
trace_id=TRACE_ID,
|
||||
)
|
||||
|
||||
assert len(repository.ready_chunks) == 2
|
||||
assert "repo.activate" not in repository.events
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_synthetic_profile_uses_only_explicit_local_provider() -> None:
|
||||
synthetic_profile = replace(
|
||||
PROFILE,
|
||||
model="fake-feature-hash-v1",
|
||||
synthetic=True,
|
||||
)
|
||||
repository = SpyRepository(_plan(1, profile=synthetic_profile))
|
||||
cloud_provider = SpyEmbeddingProvider(
|
||||
repository,
|
||||
failures={
|
||||
1: ModelProviderError(
|
||||
operation="must-not-run",
|
||||
kind=ProviderErrorKind.AUTHENTICATION,
|
||||
)
|
||||
},
|
||||
)
|
||||
synthetic_provider = SpyEmbeddingProvider(repository, model="fake-feature-hash-v1")
|
||||
|
||||
result = await _service(
|
||||
repository,
|
||||
cloud_provider,
|
||||
synthetic_provider=synthetic_provider,
|
||||
).index_document_version(
|
||||
lease=LEASE,
|
||||
document_version_id=DOCUMENT_VERSION_ID,
|
||||
trace_id=TRACE_ID,
|
||||
)
|
||||
|
||||
assert cloud_provider.calls == []
|
||||
assert len(synthetic_provider.calls) == 1
|
||||
assert result.activated is True
|
||||
Reference in New Issue
Block a user