from __future__ import annotations import inspect import uuid from collections.abc import Callable from typing import Protocol, runtime_checkable import pytest from flask import Flask from pydantic import BaseModel, ValidationError from sqlalchemy.orm import Session from werkzeug.exceptions import NotFound from werkzeug.routing.rules import Rule from controllers.openapi import bp as openapi_bp from controllers.openapi.auth.context import Context from controllers.openapi.auth.subjects import AccountSubject from enums import DeploymentEdition from libs.oauth_bearer import AuthContext, TokenType from services.knowledge_fs.feature_service import KnowledgeFSFeatures from services.knowledge_fs.product_dto import KnowledgeFSListQuery from tests.helpers.unit.controllers.knowledge_fs_controller_fakes import ( FakeComponent, FakeRuntime, Recorder, invocation, narrowed, raising, ) from tests.unit_tests.controllers.openapi.conftest import AdmittedWorld @runtime_checkable class ResourceView(Protocol): view_class: type @pytest.fixture(autouse=True) def _enabled_knowledge_fs(config_overrides: Callable[..., None]) -> None: config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD, KNOWLEDGE_FS_ENABLED=True) def _route(app: Flask, path: str) -> Rule: return next(rule for rule in app.url_map.iter_rules() if rule.rule == path) def test_disabled_knowledge_fs_hides_openapi_route_before_facade_access(config_overrides: Callable[..., None]) -> None: from controllers.openapi.knowledge_fs import KnowledgeFsEntryListApi config_overrides(KNOWLEDGE_FS_ENABLED=False) handler = inspect.unwrap( KnowledgeFsEntryListApi.get, stop=lambda view: "_knowledge_fs_errors..decorated" in view.__code__.co_qualname, ) with pytest.raises(NotFound): handler(KnowledgeFsEntryListApi(), "workspace-1", "space-1") def test_each_knowledge_fs_command_has_its_own_command_oriented_openapi_route() -> None: from controllers.openapi.knowledge_fs import ( KnowledgeFsEntryCompareApi, KnowledgeFsEntryContentSearchApi, KnowledgeFsEntryInspectApi, KnowledgeFsEntryListApi, KnowledgeFsEntryReadContentApi, KnowledgeFsEntrySearchApi, KnowledgeFsEntryTreeApi, ) app = Flask(__name__) app.config["TESTING"] = True app.register_blueprint(openapi_bp) prefix = "/openapi/v1/workspaces//knowledge-fs/knowledge-spaces//fs" expected = { f"{prefix}:cat": (KnowledgeFsEntryReadContentApi, "GET"), f"{prefix}:diff": (KnowledgeFsEntryCompareApi, "POST"), f"{prefix}:find": (KnowledgeFsEntrySearchApi, "GET"), f"{prefix}:grep": (KnowledgeFsEntryContentSearchApi, "GET"), f"{prefix}:ls": (KnowledgeFsEntryListApi, "GET"), f"{prefix}:stat": (KnowledgeFsEntryInspectApi, "GET"), f"{prefix}:tree": (KnowledgeFsEntryTreeApi, "GET"), } for path, (view_class, method) in expected.items(): rule = _route(app, path) view = app.view_functions[rule.endpoint] assert isinstance(view, ResourceView) assert view.view_class is view_class assert rule.methods is not None assert method in rule.methods registered_paths = {rule.rule for rule in app.url_map.iter_rules()} assert not any("/knowledge-fs/spaces/" in path or "/entries" in path for path in registered_paths) def test_list_controller_adapts_public_pagination_to_the_product_operation(monkeypatch: pytest.MonkeyPatch) -> None: from controllers.openapi import knowledge_fs as module from controllers.openapi.knowledge_fs import KnowledgeFsEntryListApi from services.knowledge_fs.product_dto import KnowledgeFSListResponse facade = FakeComponent( list_knowledge_fs=Recorder( KnowledgeFSListResponse.model_validate( { "items": [], "nextCursor": "opaque-next-page", "path": "/knowledge", "truncated": True, } ) ) ) monkeypatch.setattr(module, "_knowledge_fs_facade", lambda: facade) account_id = uuid.uuid4() api = KnowledgeFsEntryListApi() app = Flask(__name__) with app.test_request_context( "/openapi/v1/workspaces/workspace-1/knowledge-fs/knowledge-spaces/space-1/fs:ls" "?path=/knowledge&page_size=25&page_token=opaque-page&consistency_class=path-consistent", method="GET", ): # Peel only the outermost OpenAPI wrapper so the inner decorators still run. get = inspect.unwrap(KnowledgeFsEntryListApi.get, stop=lambda fn: fn is not KnowledgeFsEntryListApi.get) body, status = get( api, workspace_id="workspace-1", knowledge_space_id="space-1", ctx=Context( subject=AccountSubject( AuthContext( token_type=TokenType.OAUTH_ACCOUNT, account_id=account_id, subject_email=None, subject_issuer=None, client_id=None, token_id=uuid.uuid4(), expires_at=None, ) ), session=Session(), view_args={}, ), ) assert status == 200 assert body == { "consistency_class": None, "data": [], "has_more": True, "next_page_token": "opaque-next-page", "path": "/knowledge", "preview": None, "truncated": True, } call = facade.list_knowledge_fs.only_call.kwargs assert call["tenant_id"] == "workspace-1" assert call["account_id"] == str(account_id) assert call["control_space_id"] == "space-1" query = narrowed(call["query"], KnowledgeFSListQuery) assert query.path == "/knowledge" assert query.limit == 25 assert query.cursor == "opaque-page" assert query.consistency_class == "path-consistent" @pytest.mark.parametrize( "incomplete_filter", [{"metadata_key": "owner"}, {"metadata_value": "legal"}], ) def test_entry_search_requires_a_complete_metadata_pair(incomplete_filter: dict[str, str]) -> None: from controllers.openapi.knowledge_fs import KnowledgeFSEntrySearchQuery with pytest.raises(ValidationError, match="must be supplied together"): KnowledgeFSEntrySearchQuery.model_validate({"path": "/knowledge", **incomplete_filter}) @pytest.mark.parametrize( ("api_name", "method_name", "public_query", "facade_method", "product_query_type", "expected_fields"), [ ( "KnowledgeFsEntryTreeApi", "get", {"path": "/knowledge", "page_size": 12, "page_token": "page", "depth": 3}, "tree_knowledge_fs", "KnowledgeFSTreeQuery", {"path": "/knowledge", "limit": 12, "cursor": "page", "depth": 3}, ), ( "KnowledgeFsEntryContentSearchApi", "get", {"path": "/knowledge", "text": "TODO", "page_size": 10, "timeout_ms": 500}, "grep_knowledge_fs", "KnowledgeFSGrepQuery", {"path": "/knowledge", "query": "TODO", "limit": 10, "timeout_ms": 500}, ), ( "KnowledgeFsEntrySearchApi", "get", {"path": "/knowledge", "name_contains": "readme", "resource_type": "document"}, "find_knowledge_fs", "KnowledgeFSFindQuery", {"path": "/knowledge", "name_contains": "readme", "resource_type": "document"}, ), ( "KnowledgeFsEntryCompareApi", "post", { "old_path": "/knowledge/old.md", "new_path": "/knowledge/new.md", "mode": "word", "include_semantic_summary": True, }, "diff_knowledge_fs", "KnowledgeFSDiffQuery", { "old_path": "/knowledge/old.md", "new_path": "/knowledge/new.md", "mode": "word", "semantic": True, }, ), ( "KnowledgeFsEntryReadContentApi", "get", {"path": "/knowledge/readme.md", "page_size": 40, "page_token": "page"}, "cat_knowledge_fs", "KnowledgeFSCatQuery", {"path": "/knowledge/readme.md", "limit": 40, "cursor": "page"}, ), ( "KnowledgeFsEntryInspectApi", "get", {"path": "/knowledge/readme.md"}, "stat_knowledge_fs", "KnowledgeFSStatQuery", {"path": "/knowledge/readme.md"}, ), ], ) def test_each_custom_interface_delegates_with_an_internal_product_query( monkeypatch: pytest.MonkeyPatch, api_name: str, method_name: str, public_query: dict[str, object], facade_method: str, product_query_type: str, expected_fields: dict[str, object], ) -> None: from controllers.openapi import knowledge_fs as module from services.knowledge_fs import product_dto product_responses = { "tree_knowledge_fs": product_dto.KnowledgeFSTreeResponse.model_validate( { "path": "/knowledge", "root": {"kind": "directory", "metadata": {}, "name": "knowledge", "path": "/knowledge"}, "truncated": False, } ), "grep_knowledge_fs": product_dto.KnowledgeFSGrepResponse(matches=[], path="/knowledge", truncated=False), "find_knowledge_fs": product_dto.KnowledgeFSListResponse(items=[], path="/knowledge", truncated=False), "diff_knowledge_fs": product_dto.KnowledgeFSDiffResponse.model_validate( { "mode": "word", "new_path": "/knowledge/new.md", "old_path": "/knowledge/old.md", "operations": [], "stats": {"delete": 0, "equal": 0, "insert": 0}, } ), "cat_knowledge_fs": product_dto.KnowledgeFSCatResponse( content_type="text/markdown", path="/knowledge/readme.md", text="hello", truncated=False ), "stat_knowledge_fs": product_dto.KnowledgeFSStatResponse.model_validate( { "metadata": {}, "path": "/knowledge/readme.md", "resource_type": "document", "target_id": "document-1", } ), } delegate = Recorder(product_responses[facade_method]) facade = FakeComponent(**{facade_method: delegate}) monkeypatch.setattr(module, "_knowledge_fs_facade", lambda: facade) account_id = uuid.uuid4() api_class, handler = { "KnowledgeFsEntryTreeApi": (module.KnowledgeFsEntryTreeApi, module.KnowledgeFsEntryTreeApi.get), "KnowledgeFsEntryContentSearchApi": ( module.KnowledgeFsEntryContentSearchApi, module.KnowledgeFsEntryContentSearchApi.get, ), "KnowledgeFsEntrySearchApi": (module.KnowledgeFsEntrySearchApi, module.KnowledgeFsEntrySearchApi.get), "KnowledgeFsEntryCompareApi": (module.KnowledgeFsEntryCompareApi, module.KnowledgeFsEntryCompareApi.post), "KnowledgeFsEntryReadContentApi": ( module.KnowledgeFsEntryReadContentApi, module.KnowledgeFsEntryReadContentApi.get, ), "KnowledgeFsEntryInspectApi": (module.KnowledgeFsEntryInspectApi, module.KnowledgeFsEntryInspectApi.get), }[api_name] public_model_type = { "KnowledgeFsEntryTreeApi": module.KnowledgeFSEntryTreeQuery, "KnowledgeFsEntryContentSearchApi": module.KnowledgeFSEntryContentSearchQuery, "KnowledgeFsEntrySearchApi": module.KnowledgeFSEntrySearchQuery, "KnowledgeFsEntryCompareApi": module.KnowledgeFSEntryComparePayload, "KnowledgeFsEntryReadContentApi": module.KnowledgeFSEntryReadContentQuery, "KnowledgeFsEntryInspectApi": module.KnowledgeFSEntryInspectQuery, }[api_name] public_model = public_model_type.model_validate(public_query) result = inspect.unwrap(handler)( api_class(), workspace_id="workspace-1", knowledge_space_id="space-1", ctx=Context( subject=AccountSubject( AuthContext( token_type=TokenType.OAUTH_ACCOUNT, account_id=account_id, subject_email=None, subject_issuer=None, client_id=None, token_id=uuid.uuid4(), expires_at=None, ) ), session=Session(), view_args={}, ), **({"body": public_model} if method_name == "post" else {"query": public_model}), ) assert isinstance(result, BaseModel) call = delegate.only_call.kwargs assert call["tenant_id"] == "workspace-1" assert call["account_id"] == str(account_id) assert call["control_space_id"] == "space-1" assert isinstance( call["query"], { "KnowledgeFSCatQuery": product_dto.KnowledgeFSCatQuery, "KnowledgeFSDiffQuery": product_dto.KnowledgeFSDiffQuery, "KnowledgeFSFindQuery": product_dto.KnowledgeFSFindQuery, "KnowledgeFSGrepQuery": product_dto.KnowledgeFSGrepQuery, "KnowledgeFSStatQuery": product_dto.KnowledgeFSStatQuery, "KnowledgeFSTreeQuery": product_dto.KnowledgeFSTreeQuery, }[product_query_type], ) for field, value in expected_fields.items(): assert call["query"].model_dump()[field] == value def test_knowledge_fs_facade_uses_the_configured_runtime(monkeypatch: pytest.MonkeyPatch) -> None: from controllers.openapi import knowledge_fs as module session_maker = object() facade = FakeComponent() get_runtime = Recorder(FakeRuntime(facade=facade)) monkeypatch.setattr( module, "get_knowledge_fs_features", lambda: KnowledgeFSFeatures(enabled=True, upload_enabled=False, agent_enabled=False), ) monkeypatch.setattr(module.session_factory, "get_session_maker", lambda: session_maker) monkeypatch.setattr(module, "get_knowledge_fs_runtime", get_runtime) assert module._knowledge_fs_facade() is facade assert get_runtime.calls == [invocation(session_maker)] def test_knowledge_fs_facade_does_not_start_runtime_when_disabled(monkeypatch: pytest.MonkeyPatch) -> None: from controllers.openapi import knowledge_fs as module get_runtime = Recorder(None) monkeypatch.setattr( module, "get_knowledge_fs_features", lambda: KnowledgeFSFeatures(enabled=False, upload_enabled=False, agent_enabled=False), ) monkeypatch.setattr(module, "get_knowledge_fs_runtime", get_runtime) with pytest.raises(NotFound): module._knowledge_fs_facade() assert get_runtime.calls == [] def test_knowledge_fs_error_adapter_uses_stable_domain_errors() -> None: from controllers.openapi import knowledge_fs as module from controllers.openapi._errors import ( KnowledgeFsAccessDeniedError, KnowledgeFsConflictError, KnowledgeFsInvalidRequestError, KnowledgeFsRequestRejectedError, KnowledgeFsRequestTooLargeError, KnowledgeFsResourceNotFoundError, KnowledgeFsUnavailableError, ) from services.knowledge_fs.product_authorization import KnowledgeFSProductNotFoundError from services.knowledge_fs.product_remote import ( KnowledgeFSOperationUnavailableError, KnowledgeFSProductRemoteError, KnowledgeFSProductRequestRejectedError, KnowledgeFSProductResourceNotFoundError, ) from services.knowledge_fs_capability import KnowledgeFSCapabilityConfigurationError mappings = ( (KnowledgeFSProductNotFoundError("hidden"), KnowledgeFsResourceNotFoundError), (KnowledgeFSProductResourceNotFoundError("missing"), KnowledgeFsResourceNotFoundError), (KnowledgeFSProductRequestRejectedError(status_code=400), KnowledgeFsInvalidRequestError), (KnowledgeFSProductRequestRejectedError(status_code=409), KnowledgeFsConflictError), (KnowledgeFSProductRequestRejectedError(status_code=413), KnowledgeFsRequestTooLargeError), (KnowledgeFSProductRequestRejectedError(status_code=422), KnowledgeFsRequestRejectedError), (PermissionError("forbidden"), KnowledgeFsAccessDeniedError), (KnowledgeFSCapabilityConfigurationError("misconfigured"), KnowledgeFsUnavailableError), (KnowledgeFSOperationUnavailableError("unavailable"), KnowledgeFsUnavailableError), (KnowledgeFSProductRemoteError("upstream"), KnowledgeFsUnavailableError), ) for domain_error, openapi_error in mappings: guarded = module._knowledge_fs_errors(raising(domain_error)) with pytest.raises(openapi_error): guarded() @pytest.mark.parametrize("operation", ["ls", "tree", "grep", "find", "cat", "stat", "diff"]) def test_knowledge_fs_catalog_gate_precedes_request_validation(admitted_bearer: AdmittedWorld, operation: str) -> None: """A CLI request must carry the downloaded fingerprint before its handler can run.""" path = ( f"/openapi/v1/workspaces/{admitted_bearer.workspace_id}" f"/knowledge-fs/knowledge-spaces/00000000-0000-4000-8000-000000000001/fs:{operation}" ) method = "POST" if operation == "diff" else "GET" without = {"Authorization": admitted_bearer.headers["Authorization"]} refused = admitted_bearer.client.open(path, method=method, headers=without) admitted = admitted_bearer.client.open(path, method=method, headers=admitted_bearer.headers) assert refused.status_code == 412 assert refused.get_json()["code"] == "catalog_stale" assert admitted.status_code == 422 # Required path/body absent; the catalog gate admitted it.