from __future__ import annotations from collections.abc import Callable from datetime import datetime from typing import Protocol, cast import pytest from flask import Flask from controllers.openapi._app_response import _EMPTY_PARAMETERS, build_app_describe_response from controllers.openapi._input_schema import EMPTY_INPUT_SCHEMA from controllers.openapi._models import AppDescribeQuery, AppDescribeResponse from controllers.openapi.apps import AppDescribeApi, settings_hints from controllers.openapi.auth.requirements import CheckWebAppAuthEnterprise from machinery.context import AppRequestContext from models.model import AppMode from services.app.query_service import AppDescription from services.app_definition_query_service import AppParameterConfig from services.entities.app_entities import SUPPORTED_APP_TYPES, AppSummary, SupportedAppType from tests.unit_tests.controllers.conftest import ControllerTestServices class Endpoint[T](Protocol): __handler__: Callable[..., T] def description(config: AppParameterConfig | None, mode: str = AppMode.CHAT) -> AppDescription: return AppDescription( AppSummary( "app-id", "tenant-id", "Demo", "d", cast(AppMode, mode), "normal", datetime(2026, 1, 1), "maintainer" ), True, config, ) def test_full_description_projects_materialized_data_without_request_or_session() -> None: form = [{"text-input": {"variable": "industry", "label": "Industry", "required": True}}] response = build_app_describe_response(description(AppParameterConfig({}, form)), None) assert response.info is not None assert response.info.model_dump() == { "id": "app-id", "name": "Demo", "description": "d", "mode": "chat", "updated_at": "2026-01-01T00:00:00", "service_api_enabled": True, "is_agent": False, } assert response.parameters is not None assert response.parameters["user_input_form"] == form assert response.input_schema is not None assert response.input_schema["properties"]["inputs"]["required"] == ["industry"] @pytest.mark.parametrize("fields", [{"info"}, {"parameters"}, {"input_schema"}, set()]) def test_projection_honors_requested_fields_and_missing_config_fallbacks(fields: set[str]) -> None: response = build_app_describe_response(description(None), fields) assert (response.info is not None) == ("info" in fields) assert response.parameters == (_EMPTY_PARAMETERS if "parameters" in fields else None) assert response.input_schema == (EMPTY_INPUT_SCHEMA if "input_schema" in fields else None) def test_empty_parameters_preserve_public_response_contract() -> None: assert _EMPTY_PARAMETERS == { "opening_statement": None, "suggested_questions": [], "user_input_form": [], "file_upload": None, "system_parameters": {}, } def test_settings_hints_name_the_mode_ops(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(CheckWebAppAuthEnterprise, "available", staticmethod(lambda: True)) ops = [hint.op for hint in settings_hints("app-1", AppMode.WORKFLOW)] assert ops == [ "describe.app_info.workflow", "describe.webapp.workflow", "describe.service_api", "describe.webapp_access", ] agent_ops = [hint.op for hint in settings_hints("app-1", AppMode.AGENT)] assert agent_ops == [ "describe.app_info.agent", "describe.webapp.agent", "describe.service_api.agent", "describe.webapp_access.agent", ] assert all(hint.input == {"app_id": "app-1"} for hint in settings_hints("app-1", AppMode.CHAT)) def test_settings_hints_skip_web_app_access_without_web_app_auth(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(CheckWebAppAuthEnterprise, "available", staticmethod(lambda: False)) ops = [hint.op for hint in settings_hints("app-1", AppMode.WORKFLOW)] assert "describe.webapp_access" not in ops assert ops def test_shared_builder_adds_no_hints() -> None: """The builder also serves describe.console_app.external, whose callers can't use account-only settings ops.""" assert build_app_describe_response(description(None), None).hints == [] def _describe( app: Flask, services: ControllerTestServices, monkeypatch: pytest.MonkeyPatch, mode: str ) -> AppDescribeResponse: monkeypatch.setattr(services.apps.discovery, "describe", lambda *_args: description(None, mode)) api = AppDescribeApi() with app.test_request_context(): return cast(Endpoint[AppDescribeResponse], api.get).__handler__( api, AppRequestContext(tenant_id="tenant-id", app_id="app-id"), "app-id", query=AppDescribeQuery() ) def test_account_describe_adds_settings_hints( app: Flask, app_query_services: ControllerTestServices, monkeypatch: pytest.MonkeyPatch ) -> None: resp = _describe(app, app_query_services, monkeypatch, AppMode.CHAT) assert [hint.op for hint in resp.hints] == [hint.op for hint in settings_hints("app-id", AppMode.CHAT)] assert resp.hints def test_account_describe_unknown_mode_has_no_hints( app: Flask, app_query_services: ControllerTestServices, monkeypatch: pytest.MonkeyPatch ) -> None: resp = _describe(app, app_query_services, monkeypatch, "mode-from-a-newer-version") assert resp.hints == [] assert resp.info is not None """Unit tests for SupportedAppType — the listable subset of AppMode that the openapi `app` face (`get app`) exposes and the CLI `--mode` whitelist derives from. """ def test_supported_app_type_is_the_listable_subset_of_app_mode() -> None: """SupportedAppType (and the derived SUPPORTED_APP_TYPES tuple) is exactly the curated, listable subset of AppMode; non-app/runtime modes stay out.""" assert {t.value for t in SupportedAppType} == { "completion", "chat", "advanced-chat", "workflow", "agent-chat", } assert set(SUPPORTED_APP_TYPES) <= set(AppMode) assert AppMode.AGENT not in SUPPORTED_APP_TYPES assert AppMode.RAG_PIPELINE not in SUPPORTED_APP_TYPES assert AppMode.CHANNEL not in SUPPORTED_APP_TYPES