Initial curated RAGcore source from c16667f0c9397e2bfc8dd0255b114fba23c8d05e

This commit is contained in:
Public Source Publisher committed 2026-10-01 01:34:21 +02:00
commit 7ca726caf2
659 files changed
+98171

No files matched your search

@@ -0,0 +1,482 @@
from __future__ import annotations
import asyncio
import json
import re
from dataclasses import replace
from datetime import UTC, datetime
from pathlib import Path
from uuid import UUID, uuid4
import pytest
import yaml
from fastapi.testclient import TestClient
from ragcore.api.v1.answers.dependencies import get_answer_application, get_answer_principal
from ragcore.api.v1.answers.service import (
AnswerEventType,
AnswerQuery,
AnswerService,
)
from ragcore.api.v1.context.service import ContextExecutionRequest, ContextExecutionResult
from ragcore.api.v1.search.service import SearchQuery
from ragcore.application.retrieval.authorization import RetrievalAuthorizationService
from ragcore.domain.authorization import Principal, PrincipalType
from ragcore.domain.citations import Citation
from ragcore.domain.control import Application, Role, Workspace
from ragcore.main import create_app
from ragcore.retrieval.context import ContextCandidate
WORKSPACE_ID = UUID("00000000-0000-0000-0000-000000000001")
SPACE_ID = UUID("00000000-0000-0000-0000-000000000002")
EVIDENCE_EXCERPT = (
"The quarterly maintenance window for the payment gateway runs every "
"second Sunday from midnight to four in the morning Central European time."
)
class FixtureContextExecutor:
def __init__(
self,
candidate: ContextCandidate | None,
*,
degraded: bool = False,
degradation_codes: tuple[str, ...] = (),
) -> None:
self.candidate = candidate
self.degraded = degraded
self.degradation_codes = degradation_codes
self.calls: list[ContextExecutionRequest] = []
async def execute(self, request: ContextExecutionRequest) -> ContextExecutionResult:
self.calls.append(request)
if self.candidate is None:
return ContextExecutionResult(
degraded=self.degraded, degradation_codes=self.degradation_codes
)
citation = self.candidate.citation
refreshed = Citation.create(
workspace_id=citation.workspace_id,
space_id=citation.space_id,
document_id=citation.document_id,
document_version_id=citation.document_version_id,
chunk_id=citation.chunk_id,
parent_chunk_id=citation.parent_chunk_id,
location=citation.location,
excerpt=citation.excerpt,
source_uri=citation.source_uri,
acquired_at=citation.acquired_at,
retrieval_run_id=request.retrieval_run_id,
index_alias=citation.index_alias,
profile_version=citation.profile_version,
)
return ContextExecutionResult(
candidates=(replace(self.candidate, citation=refreshed),),
degraded=self.degraded,
degradation_codes=self.degradation_codes,
)
_CITATION_ID = re.compile(r"citation_ids=([0-9a-fA-F-]{36})")
class FixtureGenerator:
"""``None`` in ``responses`` means: build a valid answer citing whichever
citation id the prompt's evidence fence actually carries -- the real id
depends on the request's own (unpredictable) retrieval_run_id, so it
cannot be precomputed by the test itself.
"""
def __init__(self, responses: list[str | None] | None = None) -> None:
self.responses: list[str | None] = list(responses if responses is not None else [None])
self.calls: list[tuple[str, ...]] = []
async def generate(self, *, prompt: str, generation_profile: str, spaces) -> str:
self.calls.append(tuple(space.slug for space in spaces))
if not self.responses:
raise AssertionError("generator was called more times than expected")
response = self.responses.pop(0)
if response is not None:
return response
match = _CITATION_ID.search(prompt)
assert match is not None, "prompt did not contain a citation id fence"
return json.dumps(
{
"status": "answerable",
"answer": (
"The quarterly maintenance window for the payment gateway runs "
"every second Sunday from midnight to four in the morning."
),
"citations": [match.group(1)],
}
)
class HangingGenerator:
def __init__(self) -> None:
self.started = asyncio.Event()
self.cancelled = False
async def generate(self, *, prompt: str, generation_profile: str, spaces) -> str:
self.started.set()
try:
await asyncio.Event().wait()
except asyncio.CancelledError:
self.cancelled = True
raise
return "unreachable"
class FixtureInputs:
def __init__(self, workspace: Workspace, principal: Principal, space_id: UUID) -> None:
self.workspace = workspace
self.principal = principal
self.space_id = space_id
async def load(self, principal: Principal):
space = self.workspace.create_knowledge_space(
id=self.space_id, name="Answers space", slug="answers-space"
)
return type(
"Inputs",
(),
{
"spaces": (space,),
"grants": (),
"user_grants": (
self.workspace.grant_user(
id=uuid4(),
user_id=principal.id,
knowledge_space=space,
role=Role.EDITOR,
),
),
"application": Application(
id=uuid4(),
workspace_id=self.workspace.id,
name="Answers application",
default_knowledge_space_ids=frozenset({self.space_id}),
),
},
)()
def _candidate(excerpt: str = EVIDENCE_EXCERPT) -> ContextCandidate:
citation = Citation.create(
workspace_id=WORKSPACE_ID,
space_id=SPACE_ID,
document_id=uuid4(),
document_version_id=uuid4(),
chunk_id=uuid4(),
parent_chunk_id=None,
location={"page_start": 1},
excerpt=excerpt,
source_uri="https://example.test/doc.md",
acquired_at=datetime(2026, 8, 1, 12, tzinfo=UTC),
retrieval_run_id=uuid4(),
index_alias="rag_dense_hybrid_active",
profile_version="hybrid-v1",
)
return ContextCandidate(citation, excerpt, 1, 1.0)
def _fixture(
*,
candidate: ContextCandidate | None,
generator: object,
degraded: bool = False,
degradation_codes: tuple[str, ...] = (),
) -> tuple[object, Principal, UUID, FixtureContextExecutor]:
workspace = Workspace(WORKSPACE_ID, "Answers workspace", "answers-workspace")
space_id = SPACE_ID
principal = Principal(uuid4(), workspace.id, PrincipalType.HUMAN, frozenset({Role.EDITOR}))
executor = FixtureContextExecutor(
candidate, degraded=degraded, degradation_codes=degradation_codes
)
service = AnswerService(
authorization=RetrievalAuthorizationService(),
inputs=FixtureInputs(workspace, principal, space_id),
context_executor=executor,
generator=generator,
)
api = create_app()
api.dependency_overrides[get_answer_principal] = lambda: principal
api.dependency_overrides[get_answer_application] = lambda: service
return api, principal, space_id, executor
def test_openapi_and_runtime_expose_answer_operation() -> None:
candidate = _candidate()
api, _, _, _ = _fixture(candidate=candidate, generator=FixtureGenerator())
document = yaml.safe_load(
(Path(__file__).parents[4] / "docs" / "contracts" / "openapi.yaml").read_text()
)
assert set(api.openapi()["paths"]["/v1/answers"]) == {"post"}
assert "post" in document["paths"]["/v1/answers"]
def test_grounded_answer_returns_citations_and_claims() -> None:
candidate = _candidate()
generator = FixtureGenerator()
api, _, space_id, executor = _fixture(candidate=candidate, generator=generator)
response = TestClient(api).post(
"/v1/answers",
json={"query": "when is the maintenance window?", "requested_space_ids": [str(space_id)]},
)
assert response.status_code == 200, response.json()
body = response.json()
# A single contributing document assembles as "partially_answerable"
# (ContextAssembler's own document-diversity heuristic); generation is
# still expected to proceed for any status other than not_answerable.
assert body["answerability"] == "partially_answerable"
assert "maintenance window" in body["answer"]
assert body["citations"]
assert body["claims"]
assert all(claim["citation_ids"] for claim in body["claims"])
assert executor.calls[0].scope.effective_space_ids == frozenset({space_id})
def test_reranker_failure_degrades_but_still_returns_a_grounded_answer() -> None:
"""M8-01: a reranker that 404s or times out must not turn found evidence
into a not_answerable, 0-citation response -- it degrades to the fused
ranking, and the caller can see that happened via degraded=true."""
candidate = _candidate()
generator = FixtureGenerator()
api, _, space_id, _ = _fixture(
candidate=candidate,
generator=generator,
degraded=True,
degradation_codes=("rerank_rejected",),
)
response = TestClient(api).post(
"/v1/answers",
json={"query": "when is the maintenance window?", "requested_space_ids": [str(space_id)]},
)
assert response.status_code == 200, response.json()
body = response.json()
assert body["answerability"] != "not_answerable"
assert body["citations"]
assert body["degraded"] is True
assert "rerank_rejected" in body["degradation_reasons"]
def test_search_authorization_is_reused_and_narrows_to_effective_spaces() -> None:
candidate = _candidate()
generator = FixtureGenerator()
api, _, _, executor = _fixture(candidate=candidate, generator=generator)
forbidden_space = uuid4()
response = TestClient(api).post(
"/v1/answers",
json={"query": "anything", "requested_space_ids": [str(forbidden_space)]},
)
assert response.status_code == 200
body = response.json()
assert body["answerability"] == "not_answerable"
assert body["answer"] == ""
assert executor.calls == []
assert generator.calls == []
def test_not_answerable_context_never_calls_the_generator() -> None:
generator = FixtureGenerator()
api, _, space_id, executor = _fixture(candidate=None, generator=generator)
response = TestClient(api).post(
"/v1/answers",
json={"query": "anything", "requested_space_ids": [str(space_id)]},
)
assert response.status_code == 200
body = response.json()
assert body["answerability"] == "not_answerable"
assert body["answer"] == ""
assert body["claims"] == []
assert len(executor.calls) == 1
assert generator.calls == []
def test_invalid_generated_output_is_rejected_not_silently_accepted() -> None:
candidate = _candidate()
generator = FixtureGenerator(["not json at all", "still not json"])
api, _, space_id, _ = _fixture(candidate=candidate, generator=generator)
response = TestClient(api).post(
"/v1/answers",
json={
"query": "when is the maintenance window?",
"requested_space_ids": [str(space_id)],
},
)
assert response.status_code == 503
# ClaimCitationValidator's own retry loop exhausted both attempts and
# raises its stable "exhausted" code, not a silently-degraded answer.
assert response.json()["code"] == "VALIDATION_RETRIES_EXHAUSTED"
assert len(generator.calls) == 2
def test_debug_output_requires_admin_capability() -> None:
candidate = _candidate()
api, _, space_id, _ = _fixture(candidate=candidate, generator=FixtureGenerator())
response = TestClient(api).post(
"/v1/answers",
json={
"query": "anything",
"requested_space_ids": [str(space_id)],
"include_debug": True,
},
)
assert response.status_code == 403
assert response.json()["code"] == "DEBUG_NOT_ALLOWED"
def test_missing_answer_service_is_503_problem_details() -> None:
candidate = _candidate()
api, _, _, _ = _fixture(candidate=candidate, generator=FixtureGenerator())
# create_app() always wires a real answer_application now; popping the
# dependency override alone would just fall through to that real service
# instead of exercising get_answer_application's own missing-state
# fallback, so the state attribute itself has to be genuinely absent.
api.dependency_overrides.pop(get_answer_application)
del api.state.answer_application
response = TestClient(api).post("/v1/answers", json={"query": "anything"})
assert response.status_code == 503
assert response.json()["code"] == "ANSWERS_UNAVAILABLE"
def test_missing_query_is_a_validation_error() -> None:
candidate = _candidate()
api, _, _, _ = _fixture(candidate=candidate, generator=FixtureGenerator())
response = TestClient(api).post("/v1/answers", json={})
assert response.status_code == 422
assert response.json()["code"] == "VALIDATION_ERROR"
def test_sse_stream_emits_metadata_before_done_in_order() -> None:
candidate = _candidate()
generator = FixtureGenerator()
api, _, space_id, _ = _fixture(candidate=candidate, generator=generator)
with TestClient(api).stream(
"POST",
"/v1/answers",
json={
"query": "when is the maintenance window?",
"requested_space_ids": [str(space_id)],
"stream": True,
},
) as response:
assert response.status_code == 200
assert response.headers["content-type"].startswith("text/event-stream")
raw = response.read().decode()
frames = [frame for frame in raw.strip().split("\n\n") if frame]
assert len(frames) == 2
def _parse(frame: str) -> tuple[str, dict]:
lines = frame.splitlines()
event_line = next(line for line in lines if line.startswith("event: "))
data_line = next(line for line in lines if line.startswith("data: "))
return event_line[len("event: ") :], json.loads(data_line[len("data: ") :])
first_type, first_data = _parse(frames[0])
second_type, second_data = _parse(frames[1])
assert first_type == AnswerEventType.RETRIEVAL_METADATA.value
assert first_data["answerability"] == "partially_answerable"
assert "effective_space_ids" in first_data
assert second_type == AnswerEventType.DONE.value
assert second_data["answerability"] == "partially_answerable"
assert second_data["citations"]
assert "maintenance window" in second_data["answer"]
def test_sse_stream_reports_a_terminal_error_event_on_invalid_output() -> None:
candidate = _candidate()
generator = FixtureGenerator(["not json", "still not json"])
api, _, space_id, _ = _fixture(candidate=candidate, generator=generator)
with TestClient(api).stream(
"POST",
"/v1/answers",
json={
"query": "when is the maintenance window?",
"requested_space_ids": [str(space_id)],
"stream": True,
},
) as response:
assert response.status_code == 200
raw = response.read().decode()
frames = [frame for frame in raw.strip().split("\n\n") if frame]
assert len(frames) == 2
assert "event: retrieval_metadata" in frames[0]
assert "event: error" in frames[1]
assert '"validation_retries_exhausted"' in frames[1]
async def test_disconnect_cancels_provider_work() -> None:
candidate = _candidate()
generator = HangingGenerator()
workspace = Workspace(WORKSPACE_ID, "Answers workspace", "answers-workspace")
space_id = SPACE_ID
principal = Principal(uuid4(), workspace.id, PrincipalType.HUMAN, frozenset({Role.EDITOR}))
executor = FixtureContextExecutor(candidate)
service = AnswerService(
authorization=RetrievalAuthorizationService(),
inputs=FixtureInputs(workspace, principal, space_id),
context_executor=executor,
generator=generator,
)
query = AnswerQuery(
query=SearchQuery(
query="when is the maintenance window?", requested_space_ids=(space_id,)
)
)
authorized = await service.authorize(principal, query)
stream = service.answer_stream(principal, query, authorized)
first_event = await stream.__anext__()
assert first_event.type is AnswerEventType.RETRIEVAL_METADATA
with pytest.raises(TimeoutError):
await asyncio.wait_for(stream.__anext__(), timeout=0.2)
assert generator.cancelled is True
def test_answer_response_schema_matches_runtime_openapi() -> None:
candidate = _candidate()
api, _, _, _ = _fixture(candidate=candidate, generator=FixtureGenerator())
static = yaml.safe_load(
(Path(__file__).parents[4] / "docs" / "contracts" / "openapi.yaml").read_text()
)["components"]["schemas"]
runtime = api.openapi()["components"]["schemas"]
assert static["AnswerClaim"]["required"] == runtime["AnswerClaim"]["required"]
assert static["AnswerResponse"]["required"] == runtime["AnswerResponse"]["required"]
def _resolved_required(schemas: dict, ref: dict) -> set[str]:
name = ref["$ref"].rsplit("/", 1)[-1]
return set(schemas[name].get("required", ()))
static_citation_ref = static["AnswerResponse"]["properties"]["citations"]["items"]
runtime_citation_ref = runtime["AnswerResponse"]["properties"]["citations"]["items"]
assert _resolved_required(static, static_citation_ref) == _resolved_required(
runtime, runtime_citation_ref
)
@@ -0,0 +1,253 @@
from __future__ import annotations
from dataclasses import replace
from datetime import UTC, datetime
from pathlib import Path
from uuid import UUID, uuid4
import pytest
import yaml
from fastapi.testclient import TestClient
from ragcore.api.v1.context.dependencies import get_context_application, get_context_principal
from ragcore.api.v1.context.service import (
ContextExecutionRequest,
ContextExecutionResult,
ContextService,
)
from ragcore.application.retrieval.authorization import RetrievalAuthorizationService
from ragcore.domain.authorization import Principal, PrincipalType
from ragcore.domain.citations import Citation
from ragcore.domain.control import Application, Role, Workspace
from ragcore.main import create_app
from ragcore.retrieval.context import ContextCandidate
class FixtureInputs:
def __init__(self, workspace: Workspace, space_id: UUID, principal: Principal) -> None:
self.workspace = workspace
self.space_id = space_id
self.principal = principal
async def load(self, principal: Principal):
space = self.workspace.create_knowledge_space(
id=self.space_id,
name="Context space",
slug="context-space",
)
return type(
"Inputs",
(),
{
"spaces": (space,),
"grants": (),
"user_grants": (
self.workspace.grant_user(
id=uuid4(),
user_id=principal.id,
knowledge_space=space,
role=Role.EDITOR,
),
),
"application": Application(
id=uuid4(),
workspace_id=self.workspace.id,
name="Context application",
default_knowledge_space_ids=frozenset({self.space_id}),
),
},
)()
class FixtureExecutor:
def __init__(self, candidate: ContextCandidate) -> None:
self.candidate = candidate
self.calls: list[ContextExecutionRequest] = []
self.fail = False
self.degraded = False
self.degradation_codes: tuple[str, ...] = ()
async def execute(self, request: ContextExecutionRequest) -> ContextExecutionResult:
self.calls.append(request)
if self.fail:
raise RuntimeError("provider secret must not leak")
citation = self.candidate.citation
refreshed = Citation.create(
workspace_id=citation.workspace_id,
space_id=citation.space_id,
document_id=citation.document_id,
document_version_id=citation.document_version_id,
chunk_id=citation.chunk_id,
parent_chunk_id=citation.parent_chunk_id,
location=citation.location,
excerpt=citation.excerpt,
source_uri=citation.source_uri,
acquired_at=citation.acquired_at,
retrieval_run_id=request.retrieval_run_id,
index_alias=citation.index_alias,
profile_version=citation.profile_version,
)
return ContextExecutionResult(
candidates=(replace(self.candidate, citation=refreshed),),
degraded=self.degraded,
degradation_codes=self.degradation_codes,
)
def _fixture() -> tuple[object, Principal, UUID, ContextCandidate, FixtureExecutor]:
workspace = Workspace(uuid4(), "Context workspace", "context-workspace")
space_id = uuid4()
principal = Principal(uuid4(), workspace.id, PrincipalType.HUMAN, frozenset({Role.EDITOR}))
citation = Citation.create(
workspace_id=workspace.id,
space_id=space_id,
document_id=uuid4(),
document_version_id=uuid4(),
chunk_id=uuid4(),
parent_chunk_id=None,
location={"page_start": 1},
excerpt="Context evidence",
source_uri="https://example.test/context.md",
acquired_at=datetime(2026, 8, 1, 12, tzinfo=UTC),
retrieval_run_id=uuid4(),
index_alias="rag_dense_hybrid_active",
profile_version="hybrid-v1",
)
candidate = ContextCandidate(citation, "Context evidence", 1, 1.0)
executor = FixtureExecutor(candidate)
service = ContextService(
authorization=RetrievalAuthorizationService(),
inputs=FixtureInputs(workspace, space_id, principal),
executor=executor,
)
api = create_app()
api.dependency_overrides[get_context_principal] = lambda: principal
api.dependency_overrides[get_context_application] = lambda: service
return api, principal, space_id, candidate, executor
def test_openapi_and_runtime_expose_context_operation() -> None:
api, _, _, _, _ = _fixture()
document = yaml.safe_load(
(Path(__file__).parents[4] / "docs" / "contracts" / "openapi.yaml").read_text()
)
assert set(api.openapi()["paths"]["/v1/context"]) == {"post"}
assert "post" in document["paths"]["/v1/context"]
def test_context_preserves_citations_and_budget() -> None:
api, _, space_id, _, executor = _fixture()
response = TestClient(api).post(
"/v1/context",
json={
"query": "context",
"requested_space_ids": [str(space_id)],
"max_context_tokens": 256,
},
)
assert response.status_code == 200, response.json()
body = response.json()
assert body["blocks"][0]["citation_ids"] == [body["citations"][0]["id"]]
assert body["citations"][0]["excerpt"] in body["blocks"][0]["text"]
assert body["token_count"] <= 256
assert executor.calls[0].scope.effective_space_ids == frozenset({space_id})
def test_context_surfaces_degraded_retrieval() -> None:
"""M8-01: a caller must be able to see that context was assembled from a
degraded retrieval (e.g. reranker fallback), not just that citations
happen to still be there."""
api, _, space_id, _, executor = _fixture()
executor.degraded = True
executor.degradation_codes = ("rerank_rejected",)
response = TestClient(api).post(
"/v1/context",
json={"query": "context", "requested_space_ids": [str(space_id)]},
)
assert response.status_code == 200, response.json()
body = response.json()
assert body["degraded"] is True
assert "rerank_rejected" in body["degradation_reasons"]
assert body["citations"]
def test_context_cannot_expand_to_an_unauthorized_requested_space() -> None:
api, _, _, _, executor = _fixture()
response = TestClient(api).post(
"/v1/context",
json={"query": "context", "requested_space_ids": [str(uuid4())]},
)
assert response.status_code == 200
assert response.json()["blocks"] == []
assert executor.calls == []
def test_context_validation_errors_are_problem_details() -> None:
api, _, _, _, _ = _fixture()
response = TestClient(api).post(
"/v1/context",
json={"query": " ", "max_context_tokens": 256},
)
assert response.status_code == 422
assert response.json()["code"] == "VALIDATION_ERROR"
@pytest.mark.parametrize("field", ["query"])
def test_missing_required_context_values_are_rejected(field: str) -> None:
api, _, _, _, _ = _fixture()
payload = {"query": "context", "max_context_tokens": 256}
payload.pop(field)
response = TestClient(api).post("/v1/context", json=payload)
assert response.status_code == 422
def test_unexpected_executor_failure_is_redacted_and_stable() -> None:
api, _, space_id, _, executor = _fixture()
executor.fail = True
response = TestClient(api).post(
"/v1/context",
json={"query": "context", "requested_space_ids": [str(space_id)]},
)
assert response.status_code == 503
body = response.json()
assert body["code"] == "CONTEXT_UNAVAILABLE"
assert "provider secret" not in response.text
def test_context_response_schema_matches_runtime_openapi() -> None:
api, _, _, _, _ = _fixture()
static = yaml.safe_load(
(Path(__file__).parents[4] / "docs" / "contracts" / "openapi.yaml").read_text()
)["components"]["schemas"]
runtime = api.openapi()["components"]["schemas"]
assert set(static["ContextRequest"]["properties"]) == set(
runtime["ContextRequest"]["properties"]
)
assert static["ContextRequest"]["additionalProperties"] is False
max_context_tokens = static["ContextRequest"]["properties"]["max_context_tokens"]
assert {key: max_context_tokens[key] for key in ("type", "minimum", "maximum", "default")} == {
"type": "integer",
"minimum": 256,
"maximum": 64000,
"default": 8000,
}
assert static["ContextBlockResponse"]["required"] == runtime["ContextBlockResponse"]["required"]
assert static["ContextBlockResponse"]["additionalProperties"] is False
assert static["ContextResponse"]["required"] == runtime["ContextResponse"]["required"]
assert static["ContextResponse"]["additionalProperties"] is False
assert static["ContextResponse"]["properties"]["blocks"]["items"] == {
"$ref": "#/components/schemas/ContextBlockResponse"
}
assert static["ContextResponse"]["properties"]["citations"]["items"] == {
"$ref": "#/components/schemas/CitationResponse"
}
assert static["CitationResponse"]["required"] == runtime["CitationResponse"]["required"]
assert static["CitationResponse"]["additionalProperties"] is False
@@ -0,0 +1,278 @@
from __future__ import annotations
from pathlib import Path
from uuid import UUID, uuid4
import yaml
from fastapi.testclient import TestClient
from ragcore.api.v1.control.dependencies import (
get_control_application,
get_credential_service,
get_principal,
)
from ragcore.application.authorization import AuthorizationService
from ragcore.application.control.api import ControlPlaneApplication
from ragcore.application.identity.credentials import ServiceAccountCredentialService
from ragcore.domain.authorization import Principal, PrincipalType
from ragcore.domain.control import (
Application,
Grant,
KnowledgeSpace,
Policy,
Role,
ServiceAccount,
UserGrant,
Workspace,
)
from ragcore.infrastructure.database.identity.memory import InMemoryCredentialRepository
from ragcore.main import create_app
class FakeControlRepository:
def __init__(self, workspace: Workspace) -> None:
self.workspace = workspace
self.applications: list[Application] = []
self.service_accounts: list[ServiceAccount] = []
self.spaces: list[KnowledgeSpace] = []
self.grants: list[Grant] = []
self.policies: list[Policy] = []
async def get_workspace(self, workspace_id: UUID) -> Workspace | None:
return self.workspace if workspace_id == self.workspace.id else None
async def create_application(self, entity: Application) -> Application:
self.applications.append(entity)
return entity
async def get_application(self, application_id: UUID, workspace_id: UUID) -> Application | None:
return next(
(
item
for item in self.applications
if item.id == application_id and item.workspace_id == workspace_id
),
None,
)
async def list_applications(self, workspace_id: UUID) -> tuple[Application, ...]:
return tuple(item for item in self.applications if item.workspace_id == workspace_id)
async def create_service_account(self, entity: ServiceAccount) -> ServiceAccount:
self.service_accounts.append(entity)
return entity
async def get_service_account(
self, service_account_id: UUID, workspace_id: UUID
) -> ServiceAccount | None:
return next(
(
item
for item in self.service_accounts
if item.id == service_account_id and item.workspace_id == workspace_id
),
None,
)
async def get_service_account_by_id(self, service_account_id: UUID) -> ServiceAccount | None:
return next((item for item in self.service_accounts if item.id == service_account_id), None)
async def create_space(self, entity: KnowledgeSpace) -> KnowledgeSpace:
self.spaces.append(entity)
return entity
async def list_spaces(self, workspace_id: UUID) -> tuple[KnowledgeSpace, ...]:
return tuple(item for item in self.spaces if item.workspace_id == workspace_id)
async def create_grant(self, entity: Grant) -> Grant:
self.grants.append(entity)
return entity
async def create_user_grant(self, entity: UserGrant) -> UserGrant:
self.grants.append(entity)
return entity
async def create_policy(self, entity: Policy) -> Policy:
self.policies.append(entity)
return entity
async def list_policies(self, workspace_id: UUID) -> tuple[Policy, ...]:
return tuple(item for item in self.policies if item.workspace_id == workspace_id)
def _fixture():
workspace = Workspace(uuid4(), "Contract workspace", "contract-workspace")
principal = Principal(
uuid4(), workspace.id, PrincipalType.HUMAN, frozenset({Role.WORKSPACE_ADMIN})
)
repository = FakeControlRepository(workspace)
service = ControlPlaneApplication(repository, AuthorizationService())
credential_repository = InMemoryCredentialRepository()
credential_service = ServiceAccountCredentialService(
credential_repository,
"x" * 32,
repository.get_service_account_by_id,
)
app = create_app()
app.dependency_overrides[get_principal] = lambda: principal
app.dependency_overrides[get_control_application] = lambda: service
app.dependency_overrides[get_credential_service] = lambda: credential_service
return app, repository, principal
def test_openapi_and_runtime_expose_same_control_operations() -> None:
app, _, _ = _fixture()
document = yaml.safe_load(
(Path(__file__).parents[4] / "docs" / "contracts" / "openapi.yaml").read_text()
)
documented = {
path: set(operations)
for path, operations in document["paths"].items()
if path.startswith("/v1/") and "control" in str(document["paths"][path])
}
runtime = {
path: set(operations)
for path, operations in app.openapi()["paths"].items()
if path.startswith("/v1/") and "control" in str(operations)
}
assert set(runtime) == set(documented)
assert all(
{method.lower() for method in methods} == documented[path]
for path, methods in runtime.items()
)
def test_missing_authentication_is_problem_details() -> None:
app, _, _ = _fixture()
app.dependency_overrides.pop(get_principal)
response = TestClient(app).get("/v1/applications")
assert response.status_code == 401
assert response.headers["content-type"].startswith("application/problem+json")
assert response.json()["code"] == "AUTHENTICATION_REQUIRED"
def test_missing_idempotency_key_is_problem_details() -> None:
app, _, _ = _fixture()
response = TestClient(app).post(
"/v1/applications",
json={"slug": "missing-key", "display_name": "Missing key"},
)
assert response.status_code == 422
assert response.headers["content-type"].startswith("application/problem+json")
assert response.json()["code"] == "VALIDATION_ERROR"
def test_pagination_and_invalid_cursor_error_code() -> None:
app, repository, principal = _fixture()
client = TestClient(app)
headers = {"Idempotency-Key": "pagination-key-0001"}
for slug in ("first-app", "second-app"):
response = client.post(
"/v1/applications",
json={"slug": slug, "display_name": slug, "scopes": ["search"]},
headers=headers | {"Idempotency-Key": f"{slug}-idempotency"},
)
assert response.status_code == 201
first = client.get("/v1/applications?limit=1")
assert first.status_code == 200
assert len(first.json()["items"]) == 1
assert first.json()["next_cursor"] == "1"
second = client.get(f"/v1/applications?limit=1&cursor={first.json()['next_cursor']}")
assert second.status_code == 200
assert len(second.json()["items"]) == 1
invalid = client.get("/v1/applications?cursor=not-a-cursor")
assert invalid.status_code == 400
assert invalid.json()["code"] == "INVALID_CURSOR"
assert len(repository.applications) == 2
assert principal.workspace_id == repository.workspace.id
def test_idempotency_replay_and_conflict_do_not_duplicate() -> None:
app, repository, _ = _fixture()
client = TestClient(app)
headers = {"Idempotency-Key": "stable-create-key-01"}
payload = {"slug": "stable-app", "display_name": "Stable app", "scopes": ["search"]}
first = client.post("/v1/applications", json=payload, headers=headers)
replay = client.post("/v1/applications", json=payload, headers=headers)
conflict = client.post(
"/v1/applications",
json={"slug": "other-app", "display_name": "Other app"},
headers=headers,
)
assert first.status_code == 201
assert replay.status_code == 200
assert replay.json()["id"] == first.json()["id"]
assert conflict.status_code == 409
assert conflict.json()["code"] == "IDEMPOTENCY_KEY_REUSED"
assert len(repository.applications) == 1
def test_operation_policy_and_credentials_are_enforced() -> None:
app, repository, principal = _fixture()
client = TestClient(app)
create = client.post(
"/v1/applications",
json={"slug": "credential-app", "display_name": "Credential app", "scopes": ["search"]},
headers={"Idempotency-Key": "application-create-key"},
)
assert create.status_code == 201
application_id = create.json()["id"]
credential = client.post(
f"/v1/applications/{application_id}/credentials",
json={"name": "runtime", "scopes": ["search"]},
headers={"Idempotency-Key": "credential-create-key"},
)
assert credential.status_code == 201
assert credential.json()["token"].startswith("rc_sa_")
assert len(repository.service_accounts) == 1
replay = client.post(
f"/v1/applications/{application_id}/credentials",
json={"name": "runtime", "scopes": ["search"]},
headers={"Idempotency-Key": "credential-create-key"},
)
assert replay.status_code == 200
assert replay.json()["id"] == credential.json()["id"]
policy = client.post(
"/v1/policies",
json={"operation": "answer", "required_scopes": ["search"]},
headers={"Idempotency-Key": "policy-create-key"},
)
assert policy.status_code == 201
assert client.get("/v1/policies").status_code == 200
assert principal.roles == frozenset({Role.WORKSPACE_ADMIN})
def test_non_admin_cannot_manage_spaces() -> None:
app, _, principal = _fixture()
denied = Principal(
principal.id, principal.workspace_id, PrincipalType.HUMAN, frozenset({Role.VIEWER})
)
app.dependency_overrides[get_principal] = lambda: denied
response = TestClient(app).post(
"/v1/knowledge-spaces",
json={"slug": "private", "display_name": "Private", "data_policy": "local_only"},
headers={"Idempotency-Key": "space-create-key"},
)
assert response.status_code == 403
assert response.json()["code"] == "RESOURCE_NOT_FOUND"
def test_user_grant_is_workspace_authorized():
app, repository, principal = _fixture()
client = TestClient(app)
space = client.post(
"/v1/knowledge-spaces",
json={"slug": "user-space", "display_name": "User space", "data_policy": "local_only"},
headers={"Idempotency-Key": "user-space-create-key"},
)
assert space.status_code == 201
user_id = uuid4()
grant = client.post(
f"/v1/knowledge-spaces/{space.json()['id']}/grants",
json={"principal_type": "user", "principal_id": str(user_id), "permissions": ["read"]},
headers={"Idempotency-Key": "user-grant-create-key"},
)
assert grant.status_code == 201
assert grant.json()["principal_type"] == "user"
assert grant.json()["principal_id"] == str(user_id)
assert len(repository.grants) == 1
assert principal.workspace_id == repository.workspace.id
@@ -0,0 +1,389 @@
from __future__ import annotations
from pathlib import Path
from uuid import UUID, uuid4
import yaml
from fastapi.testclient import TestClient
from ragcore.api.v1.documents.dependencies import (
get_documents_read_principal,
get_documents_sync_principal,
)
from ragcore.application.authorization import AuthorizationService
from ragcore.application.control.api import ControlPlaneApplication
from ragcore.application.documents import (
DocumentRecord,
DocumentVersionRecord,
DocumentVersionState,
)
from ragcore.domain.authorization import Principal, PrincipalType
from ragcore.domain.control import Role
from ragcore.main import create_app
class FakeDocumentRepository:
def __init__(self) -> None:
self.documents: dict[UUID, DocumentRecord] = {}
self.versions: dict[UUID, list[DocumentVersionRecord]] = {}
self.retired: list[UUID] = []
def seed(self, document: DocumentRecord, *versions: DocumentVersionRecord) -> None:
self.documents[document.id] = document
self.versions[document.id] = list(versions)
async def get_document(self, document_id: UUID) -> DocumentRecord | None:
return self.documents.get(document_id)
async def get_document_by_source(
self, workspace_id: UUID, source_key: str, external_id: str
) -> DocumentRecord | None:
return next(
(
document
for document in self.documents.values()
if document.workspace_id == workspace_id
and document.source_key == source_key
and document.external_id == external_id
),
None,
)
async def list_documents_by_source(
self, workspace_id: UUID, source_id: UUID
) -> tuple[DocumentRecord, ...]:
return tuple(
document
for document in self.documents.values()
if document.workspace_id == workspace_id and document.source_id == source_id
)
async def list_versions(self, document_id: UUID) -> tuple[DocumentVersionRecord, ...]:
return tuple(self.versions.get(document_id, ()))
async def list_searchable_versions(
self, document_id: UUID
) -> tuple[DocumentVersionRecord, ...]:
document = self.documents[document_id]
if document.source_deleted:
return ()
return tuple(
version
for version in self.versions.get(document_id, ())
if version.state is DocumentVersionState.PUBLISHED
)
async def mark_source_deleted(self, document_id: UUID) -> None:
self.retired.append(document_id)
document = self.documents[document_id]
self.documents[document_id] = DocumentRecord(
id=document.id,
workspace_id=document.workspace_id,
space_id=document.space_id,
source_key=document.source_key,
external_id=document.external_id,
title=document.title,
source_deleted=True,
)
def _document(workspace_id: UUID, *, source_key: str | None = None) -> DocumentRecord:
return DocumentRecord(
id=uuid4(),
workspace_id=workspace_id,
space_id=uuid4(),
source_key=source_key or str(uuid4()),
external_id="wiki-page-1",
title="Wiki page",
)
def _version(document_id: UUID, state: DocumentVersionState) -> DocumentVersionRecord:
return DocumentVersionRecord(
id=uuid4(),
document_id=document_id,
source_fingerprint="a" * 64,
content_hash="b" * 64,
parser_version="text-v1",
chunk_profile_version="prose-v1",
state=state,
)
def _fixture():
workspace_id = uuid4()
principal = Principal(
uuid4(), workspace_id, PrincipalType.SERVICE_ACCOUNT, frozenset({Role.EDITOR})
)
repository = FakeDocumentRepository()
service = ControlPlaneApplication(
repository=None, # type: ignore[arg-type]
authorization=AuthorizationService(),
document_repository=repository, # type: ignore[arg-type]
)
app = create_app()
from ragcore.api.v1.control.dependencies import get_control_application
app.dependency_overrides[get_control_application] = lambda: service
app.dependency_overrides[get_documents_read_principal] = lambda: principal
app.dependency_overrides[get_documents_sync_principal] = lambda: principal
return app, repository, principal
def test_openapi_and_runtime_expose_same_document_and_job_operations() -> None:
app, _, _ = _fixture()
document = yaml.safe_load(
(Path(__file__).parents[4] / "docs" / "contracts" / "openapi.yaml").read_text()
)
for path in (
"/v1/documents",
"/v1/documents/{document_id}",
"/v1/documents/{document_id}/versions",
"/v1/documents/{document_id}/retire",
):
assert path in document["paths"], f"{path} missing from openapi.yaml"
documented_methods = {method.lower() for method in document["paths"][path]}
runtime_methods = {method.lower() for method in app.openapi()["paths"][path]}
assert runtime_methods == documented_methods, path
def test_list_documents_requires_a_source_id() -> None:
"""source_id alone lists the identity's documents (see
test_list_documents_by_source_returns_every_document_of_the_identity);
no filter at all is still rejected, and an unknown source is an empty page."""
app, _, _ = _fixture()
response = TestClient(app).get("/v1/documents")
assert response.status_code == 400
assert response.json()["code"] == "MISSING_FILTER"
unknown = TestClient(app).get(f"/v1/documents?source_id={uuid4()}")
assert unknown.status_code == 200
assert unknown.json() == {"items": [], "next_cursor": None}
def test_list_documents_empty_when_no_match() -> None:
app, _, _ = _fixture()
response = TestClient(app).get(f"/v1/documents?source_id={uuid4()}&external_id=missing")
assert response.status_code == 200
assert response.json() == {"items": [], "next_cursor": None}
def test_list_documents_returns_the_active_content_sha256() -> None:
app, repository, principal = _fixture()
source_id = uuid4()
document = _document(principal.workspace_id, source_key=str(source_id))
published = _version(document.id, DocumentVersionState.PUBLISHED)
repository.seed(document, published)
response = TestClient(app).get(
f"/v1/documents?source_id={source_id}&external_id={document.external_id}"
)
assert response.status_code == 200
[item] = response.json()["items"]
assert item["id"] == str(document.id)
assert item["source_id"] == str(source_id)
assert item["status"] == "active"
assert item["active_version"]["content_sha256"] == published.content_hash
def test_list_documents_cross_workspace_is_empty_not_leaked() -> None:
app, repository, _ = _fixture()
source_id = uuid4()
document = _document(uuid4(), source_key=str(source_id))
repository.seed(document)
response = TestClient(app).get(
f"/v1/documents?source_id={source_id}&external_id={document.external_id}"
)
assert response.status_code == 200
assert response.json()["items"] == []
def test_get_document_by_id_not_found_is_problem_details() -> None:
app, _, _ = _fixture()
response = TestClient(app).get(f"/v1/documents/{uuid4()}")
assert response.status_code == 404
assert response.json()["code"] == "DOCUMENT_NOT_FOUND"
def test_get_document_versions_lists_every_state() -> None:
app, repository, principal = _fixture()
document = _document(principal.workspace_id)
superseded = _version(document.id, DocumentVersionState.SUPERSEDED)
published = _version(document.id, DocumentVersionState.PUBLISHED)
repository.seed(document, superseded, published)
response = TestClient(app).get(f"/v1/documents/{document.id}/versions")
assert response.status_code == 200
ids = {item["id"] for item in response.json()["items"]}
assert ids == {str(superseded.id), str(published.id)}
def test_retire_document_marks_source_deleted_and_is_idempotent() -> None:
app, repository, principal = _fixture()
document = _document(principal.workspace_id)
repository.seed(document)
client = TestClient(app)
first = client.post(f"/v1/documents/{document.id}/retire")
assert first.status_code == 200
assert first.json()["status"] == "source_deleted"
second = client.post(f"/v1/documents/{document.id}/retire")
assert second.status_code == 200
assert repository.retired == [document.id]
def test_retire_document_does_not_hard_delete() -> None:
app, repository, principal = _fixture()
document = _document(principal.workspace_id)
repository.seed(document)
TestClient(app).post(f"/v1/documents/{document.id}/retire")
assert document.id in repository.documents
assert repository.documents[document.id].source_deleted is True
def test_missing_authentication_is_problem_details() -> None:
app, _, _ = _fixture()
app.dependency_overrides.pop(get_documents_read_principal)
response = TestClient(app).get(f"/v1/documents?source_id={uuid4()}&external_id=x")
assert response.status_code == 401
assert response.headers["content-type"].startswith("application/problem+json")
assert response.json()["code"] == "AUTHENTICATION_REQUIRED"
def test_insufficient_role_is_forbidden_problem_details() -> None:
app, repository, principal = _fixture()
document = _document(principal.workspace_id)
repository.seed(document)
denied = Principal(
principal.id,
principal.workspace_id,
PrincipalType.SERVICE_ACCOUNT,
frozenset({Role.AUDITOR}),
)
app.dependency_overrides[get_documents_read_principal] = lambda: denied
response = TestClient(app).get(f"/v1/documents/{document.id}")
assert response.status_code == 403
assert response.json()["code"] == "RESOURCE_NOT_FOUND"
def test_reconcile_space_index_runs_the_scoped_reconciler_and_maps_errors() -> None:
from ragcore.api.v1.documents.dependencies import get_space_index_reconciler
from ragcore.application.indexes.space_reconcile import SpaceReconciliationReport
from ragcore.domain.control import KnowledgeSpace
app, repository, _principal = _fixture()
space_id = uuid4()
class FakeControlRepository:
async def list_spaces(self, workspace_id: UUID):
return (
KnowledgeSpace(
id=space_id,
workspace_id=workspace_id,
name="portfolio-public",
slug="portfolio-public",
),
)
class FakeReconciler:
def __init__(self) -> None:
self.calls: list[UUID] = []
async def reconcile(self, space, documents):
self.calls.append(space)
return SpaceReconciliationReport(
space_id=space,
documents_total=3,
documents_retired=1,
searchable_versions=2,
points_before=10,
deleted_points=4,
orphaned_documents=2,
points_after=6,
)
from ragcore.api.v1.control.dependencies import get_control_application
service = ControlPlaneApplication(
repository=FakeControlRepository(), # type: ignore[arg-type]
authorization=AuthorizationService(),
document_repository=repository, # type: ignore[arg-type]
)
app.dependency_overrides[get_control_application] = lambda: service
reconciler = FakeReconciler()
app.dependency_overrides[get_space_index_reconciler] = lambda: reconciler
client = TestClient(app)
response = client.post(f"/v1/spaces/{space_id}/index/reconcile")
assert response.status_code == 200, response.text
assert response.json() == {
"space_id": str(space_id),
"documents_total": 3,
"documents_retired": 1,
"searchable_versions": 2,
"points_before": 10,
"deleted_points": 4,
"orphaned_documents": 2,
"points_after": 6,
}
assert reconciler.calls == [space_id]
missing = client.post(f"/v1/spaces/{uuid4()}/index/reconcile")
assert missing.status_code == 404
assert missing.json()["code"] == "SPACE_NOT_FOUND"
app.dependency_overrides[get_space_index_reconciler] = lambda: None
unavailable = client.post(f"/v1/spaces/{space_id}/index/reconcile")
assert unavailable.status_code == 503
assert unavailable.json()["code"] == "INDEX_UNAVAILABLE"
def test_openapi_documents_the_reconcile_endpoint() -> None:
app, _, _ = _fixture()
document = yaml.safe_load(
(Path(__file__).parents[4] / "docs" / "contracts" / "openapi.yaml").read_text()
)
path = "/v1/spaces/{space_id}/index/reconcile"
assert path in document["paths"]
assert {m.lower() for m in app.openapi()["paths"][path]} == {"post"}
def test_list_documents_by_source_returns_every_document_of_the_identity() -> None:
app, repository, principal = _fixture()
source_id = uuid4()
first = _document(principal.workspace_id, source_key=str(source_id))
second = DocumentRecord(
id=uuid4(),
workspace_id=principal.workspace_id,
space_id=first.space_id,
source_key=str(source_id),
external_id="wiki-page-2",
title="Second page",
source_deleted=True,
)
other = _document(principal.workspace_id) # another source identity, must not appear
repository.seed(first, _version(first.id, DocumentVersionState.PUBLISHED))
repository.seed(second, _version(second.id, DocumentVersionState.PUBLISHED))
repository.seed(other, _version(other.id, DocumentVersionState.PUBLISHED))
response = TestClient(app).get(f"/v1/documents?source_id={source_id}")
assert response.status_code == 200, response.text
items = response.json()["items"]
assert {item["external_id"] for item in items} == {"wiki-page-1", "wiki-page-2"}
by_id = {item["external_id"]: item for item in items}
assert by_id["wiki-page-1"]["status"] == "active"
assert by_id["wiki-page-1"]["active_version"]["source_fingerprint"] == "a" * 64
assert by_id["wiki-page-2"]["status"] == "source_deleted"
assert by_id["wiki-page-2"]["active_version"] is None
missing = TestClient(app).get("/v1/documents")
assert missing.status_code == 400
assert missing.json()["code"] == "MISSING_FILTER"
+141
View File
@@ -0,0 +1,141 @@
from __future__ import annotations
import asyncio
from datetime import UTC, datetime
from pathlib import Path
from uuid import UUID, uuid4
import yaml
from fastapi.testclient import TestClient
from ragcore.api.v1.jobs.dependencies import get_job_service, get_jobs_sync_principal
from ragcore.application.jobs import JobService
from ragcore.application.jobs.models import Job
from ragcore.domain.authorization import Principal, PrincipalType
from ragcore.domain.control import Role
from ragcore.main import create_app
class FakeJobRepository:
def __init__(self) -> None:
self.jobs: dict[UUID, Job] = {}
async def enqueue(self, job: Job) -> Job:
self.jobs[job.id] = job
return job
async def get(self, job_id: UUID) -> Job | None:
return self.jobs.get(job_id)
async def get_owned(self, job_id: UUID, *, workspace_id: UUID | None) -> Job | None:
job = self.jobs.get(job_id)
if job is None:
return None
if workspace_id is not None and job.workspace_id != workspace_id:
return None
return job
def _job(*, workspace_id: UUID | None = None, **overrides: object) -> Job:
payload: dict[str, object] = {"blob_digest": "a" * 64}
if workspace_id is not None:
payload["workspace_id"] = str(workspace_id)
defaults: dict[str, object] = dict(
id=uuid4(),
workspace_id=workspace_id,
job_key=f"job-{uuid4()}",
operation="ingest_upload",
target_key="blob:abc",
profile_version="upload-v1",
payload=payload,
available_at=datetime.now(UTC),
trace_id="trace",
)
defaults.update(overrides)
return Job(**defaults) # type: ignore[arg-type]
def _fixture():
workspace_id = uuid4()
principal = Principal(
uuid4(), workspace_id, PrincipalType.SERVICE_ACCOUNT, frozenset({Role.EDITOR})
)
repository = FakeJobRepository()
app = create_app()
app.dependency_overrides[get_job_service] = lambda: JobService(repository) # type: ignore[arg-type]
app.dependency_overrides[get_jobs_sync_principal] = lambda: principal
return app, repository, principal
def test_openapi_and_runtime_expose_same_job_operations() -> None:
app, _, _ = _fixture()
document = yaml.safe_load(
(Path(__file__).parents[4] / "docs" / "contracts" / "openapi.yaml").read_text()
)
assert "/v1/jobs/{job_id}" in document["paths"]
documented = {method.lower() for method in document["paths"]["/v1/jobs/{job_id}"]}
runtime = {method.lower() for method in app.openapi()["paths"]["/v1/jobs/{job_id}"]}
assert runtime == documented
def test_get_job_resolves_the_upload_status_url() -> None:
app, repository, principal = _fixture()
job = _job(workspace_id=principal.workspace_id)
asyncio.run(repository.enqueue(job))
response = TestClient(app).get(f"/v1/jobs/{job.id}")
assert response.status_code == 200
body = response.json()
assert body["id"] == str(job.id)
assert body["operation"] == "ingest_upload"
assert body["status"] == "ready"
assert body["attempt_count"] == 0
assert body["max_attempts"] == 3
def test_get_job_unknown_id_is_404_problem_details() -> None:
app, _, _ = _fixture()
response = TestClient(app).get(f"/v1/jobs/{uuid4()}")
assert response.status_code == 404
assert response.json()["code"] == "JOB_NOT_FOUND"
def test_get_job_cross_workspace_is_404_not_leaked() -> None:
app, repository, _ = _fixture()
job = _job(workspace_id=uuid4())
asyncio.run(repository.enqueue(job))
response = TestClient(app).get(f"/v1/jobs/{job.id}")
assert response.status_code == 404
assert response.json()["code"] == "JOB_NOT_FOUND"
def test_get_job_without_ownership_is_404_for_a_workspace_scoped_caller() -> None:
"""A job with no owner is nobody's, not everyone's.
This route used to accept any job whose payload simply lacked a
``workspace_id`` -- a fail-open default that handed unowned work to
whichever tenant asked for it first. Ownership is a column now, and an
absent owner (a legacy row that migration 0018 would not guess at) reads
exactly like a job that does not exist.
"""
app, repository, _ = _fixture()
job = _job(workspace_id=None)
asyncio.run(repository.enqueue(job))
response = TestClient(app).get(f"/v1/jobs/{job.id}")
assert response.status_code == 404
assert response.json()["code"] == "JOB_NOT_FOUND"
def test_missing_authentication_is_problem_details() -> None:
app, _, _ = _fixture()
app.dependency_overrides.pop(get_jobs_sync_principal)
response = TestClient(app).get(f"/v1/jobs/{uuid4()}")
assert response.status_code == 401
assert response.headers["content-type"].startswith("application/problem+json")
assert response.json()["code"] == "AUTHENTICATION_REQUIRED"
@@ -0,0 +1,278 @@
from __future__ import annotations
from dataclasses import replace
from datetime import UTC, datetime
from pathlib import Path
from uuid import UUID, uuid4
import pytest
import yaml
from fastapi.testclient import TestClient
from ragcore.api.v1.search.dependencies import get_search_application, get_search_principal
from ragcore.api.v1.search.service import (
AuthorizationInputs,
SearchExecutionRequest,
SearchExecutionResult,
SearchHit,
SearchService,
)
from ragcore.application.retrieval.authorization import RetrievalAuthorizationService
from ragcore.domain.authorization import Principal, PrincipalType
from ragcore.domain.citations import Citation
from ragcore.domain.control import Application, Role, Workspace
from ragcore.main import create_app
from ragcore.retrieval.ranking import FusedCandidate
from ragcore.retrieval.search.models import (
Candidate,
CandidateBackendKind,
CandidateProvenance,
)
class FixtureExecutor:
def __init__(self, hits: tuple[SearchHit, ...] = (), **metadata: object) -> None:
self.hits = hits
self.metadata = metadata
self.calls: list[SearchExecutionRequest] = []
async def execute(self, request: SearchExecutionRequest) -> SearchExecutionResult:
self.calls.append(request)
hits = tuple(
replace(
hit,
citation=Citation.create(
workspace_id=hit.citation.workspace_id,
space_id=hit.citation.space_id,
document_id=hit.citation.document_id,
document_version_id=hit.citation.document_version_id,
chunk_id=hit.citation.chunk_id,
parent_chunk_id=hit.citation.parent_chunk_id,
location=hit.citation.location,
excerpt=hit.citation.excerpt,
source_uri=hit.citation.source_uri,
acquired_at=hit.citation.acquired_at,
retrieval_run_id=request.retrieval_run_id,
index_alias=hit.citation.index_alias,
profile_version=hit.citation.profile_version,
),
)
for hit in self.hits
)
return SearchExecutionResult(hits=hits, **self.metadata)
class FixtureInputs:
def __init__(
self,
workspace: Workspace,
principal: Principal,
space_id: UUID,
*,
application: Application | None = None,
) -> None:
self.workspace = workspace
self.principal = principal
self.space_id = space_id
self.application = application
async def load(self, principal: Principal) -> AuthorizationInputs:
return AuthorizationInputs(
spaces=(self.workspace.create_knowledge_space(
id=self.space_id,
name="Search space",
slug="search-space",
),),
user_grants=(
self.workspace.grant_user(
id=uuid4(),
user_id=principal.id,
knowledge_space=self.workspace.create_knowledge_space(
id=self.space_id,
name="Search space",
slug="search-space",
),
role=Role.VIEWER,
),
),
application=self.application,
)
def _fixture(
*,
text: str = "Exact evidence excerpt.",
query: str = "exact",
metadata: dict[str, object] | None = None,
) -> tuple[object, Principal, UUID, SearchHit, FixtureExecutor]:
workspace = Workspace(uuid4(), "Search workspace", "search-workspace")
space_id = uuid4()
principal = Principal(uuid4(), workspace.id, PrincipalType.HUMAN, frozenset({Role.VIEWER}))
document_id, version_id, chunk_id = uuid4(), uuid4(), uuid4()
citation = Citation.create(
workspace_id=workspace.id,
space_id=space_id,
document_id=document_id,
document_version_id=version_id,
chunk_id=chunk_id,
parent_chunk_id=None,
location={"page_start": 2, "page_end": 2, "section": "Runbook"},
excerpt=text,
source_uri="https://example.test/runbook.md",
acquired_at=datetime(2026, 8, 1, 12, tzinfo=UTC),
retrieval_run_id=uuid4(),
index_alias="rag_dense_hybrid_active",
profile_version="hybrid-v1",
)
candidate = Candidate(
point_id=uuid4(),
score=0.91,
rank=1,
backend=CandidateBackendKind.DENSE,
provenance=CandidateProvenance(
workspace_id=workspace.id,
space_id=space_id,
document_id=document_id,
document_version_id=version_id,
chunk_id=chunk_id,
source_id=uuid4(),
parent_chunk_id=None,
status="published",
content_type="text/markdown",
language="nl",
trust_level="trusted_internal",
),
)
hit = SearchHit(
fused=FusedCandidate(candidate, 1 / 61, 1, None),
text=text,
title="Runbook",
citation=citation,
dense_score=0.91,
)
app = Application(
id=uuid4(),
workspace_id=workspace.id,
name="Search application",
default_knowledge_space_ids=frozenset({space_id}),
)
executor = FixtureExecutor((hit,), **(metadata or {}))
service = SearchService(
authorization=RetrievalAuthorizationService(),
inputs=FixtureInputs(workspace, principal, space_id, application=app),
executor=executor,
)
api = create_app()
api.dependency_overrides[get_search_principal] = lambda: principal
api.dependency_overrides[get_search_application] = lambda: service
return api, principal, space_id, hit, executor
def test_openapi_and_runtime_expose_search_operation() -> None:
api, _, _, _, _ = _fixture()
document = yaml.safe_load(
(Path(__file__).parents[4] / "docs" / "contracts" / "openapi.yaml").read_text()
)
assert set(api.openapi()["paths"]["/v1/search"]) == {"post"}
assert "post" in document["paths"]["/v1/search"]
@pytest.mark.parametrize("query", ["exact", "semantic"])
def test_exact_and_semantic_fixtures_return_citations(query: str) -> None:
api, _, space_id, _, executor = _fixture(query=query)
response = TestClient(api).post(
"/v1/search",
json={"query": query, "requested_space_ids": [str(space_id)]},
)
assert response.status_code == 200
body = response.json()
assert body["effective_space_ids"] == [str(space_id)]
assert body["results"][0]["citation"]["excerpt_sha256"]
assert executor.calls[0].scope.effective_space_ids == frozenset({space_id})
def test_requested_spaces_cannot_expand_effective_scope() -> None:
api, _, _, _, executor = _fixture()
forbidden_space = uuid4()
response = TestClient(api).post(
"/v1/search",
json={"query": "exact", "requested_space_ids": [str(forbidden_space)]},
)
assert response.status_code == 200
assert response.json()["effective_space_ids"] == []
assert response.json()["results"] == []
assert executor.calls == []
def test_degraded_metadata_is_stable() -> None:
api, _, space_id, _, _ = _fixture(
metadata={
"degraded": True,
"degradation_codes": ("backend_timeout",),
"timings_ms": {"dense": 12.5, "sparse": 5.0},
}
)
response = TestClient(api).post(
"/v1/search",
json={"query": "exact", "requested_space_ids": [str(space_id)]},
)
assert response.status_code == 200
assert response.json()["degraded"] is True
assert response.json()["degradation_codes"] == ["backend_timeout"]
assert response.json()["timings_ms"] == {"dense": 12.5, "sparse": 5.0}
def test_invalid_filter_duplicates_return_problem_details() -> None:
api, _, _, _, _ = _fixture()
response = TestClient(api).post(
"/v1/search",
json={"query": "exact", "filters": {"languages": ["nl", "nl"]}},
)
assert response.status_code == 422
assert response.json()["code"] == "VALIDATION_ERROR"
def test_missing_search_service_is_503_problem_details() -> None:
api, principal, _, _, _ = _fixture()
# create_app() always wires a real search_application now; popping the
# dependency override alone would just fall through to that real service
# instead of exercising get_search_application's own missing-state
# fallback, so the state attribute itself has to be genuinely absent.
api.dependency_overrides.pop(get_search_application)
del api.state.search_application
response = TestClient(api).post("/v1/search", json={"query": "exact"})
assert response.status_code == 503
assert response.json()["code"] == "SEARCH_UNAVAILABLE"
assert principal.workspace_id
def test_debug_output_requires_admin_capability() -> None:
api, _, space_id, _, _ = _fixture()
response = TestClient(api).post(
"/v1/search",
json={
"query": "exact",
"requested_space_ids": [str(space_id)],
"include_debug": True,
},
)
assert response.status_code == 403
assert response.json()["code"] == "DEBUG_NOT_ALLOWED"
def test_mismatched_profile_provenance_is_rejected() -> None:
api, _, space_id, _, _ = _fixture()
response = TestClient(api).post(
"/v1/search",
json={
"query": "exact",
"profile": "other-v1",
"requested_space_ids": [str(space_id)],
},
)
assert response.status_code == 503
assert response.json()["code"] == "UNAUTHORIZED_OR_MISMATCHED_RESULT"