From c61d33f91e32278881514ad77182ca167cd85b1a Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Sun, 26 Jul 2026 20:02:30 +0800 Subject: [PATCH] refactor: move trial app capability out of system features (#39562) --- .../console/explore/recommended_app.py | 21 +-- api/controllers/console/explore/trial.py | 10 +- api/controllers/console/explore/wraps.py | 5 +- api/openapi/markdown/console-openapi.md | 5 +- api/openapi/markdown/web-openapi.md | 1 - api/services/feature_service.py | 2 - api/services/recommended_app_service.py | 41 +++-- .../commands/test_generate_swagger_specs.py | 9 +- .../console/explore/test_recommended_app.py | 17 ++- .../controllers/console/explore/test_trial.py | 6 +- .../controllers/console/explore/test_wraps.py | 20 +-- .../services/test_recommended_app_service.py | 142 ++++++++---------- .../api/console/explore/types.gen.ts | 6 +- .../generated/api/console/explore/zod.gen.ts | 6 +- .../api/console/system-features/types.gen.ts | 1 - .../api/console/system-features/zod.gen.ts | 1 - .../contracts/generated/api/web/types.gen.ts | 1 - .../contracts/generated/api/web/zod.gen.ts | 1 - .../components/apps/__tests__/index.spec.tsx | 11 ++ web/app/components/apps/index.tsx | 8 +- web/app/components/explore/app-list/index.tsx | 9 +- .../explore/try-app/__tests__/index.spec.tsx | 39 ++++- web/app/components/explore/try-app/index.tsx | 41 ++--- web/service/explore.spec.ts | 1 + web/service/explore.ts | 4 +- web/test/console/system-features.ts | 1 - 26 files changed, 220 insertions(+), 189 deletions(-) diff --git a/api/controllers/console/explore/recommended_app.py b/api/controllers/console/explore/recommended_app.py index 79eaa305d61..af46cdc8c9b 100644 --- a/api/controllers/console/explore/recommended_app.py +++ b/api/controllers/console/explore/recommended_app.py @@ -11,7 +11,7 @@ from controllers.console import console_ns from controllers.console.wraps import account_initialization_required, with_current_user from extensions.ext_database import db from fields.base import ResponseModel -from libs.helper import build_icon_url +from libs.helper import build_icon_url, dump_response from libs.login import login_required from models import Account from services.recommended_app_service import RecommendedAppService @@ -58,7 +58,7 @@ class RecommendedAppResponse(ResponseModel): categories: list[str] = Field(default_factory=list) position: int | None = None is_listed: bool | None = None - can_trial: bool | None = None + can_trial: bool class RecommendedAppListResponse(ResponseModel): @@ -77,7 +77,7 @@ class RecommendedAppDetailResponse(ResponseModel): icon_background: str | None = None mode: str export_data: str - can_trial: bool | None = None + can_trial: bool class RecommendedAppDetailNullableResponse(RootModel[RecommendedAppDetailResponse | None]): @@ -119,10 +119,10 @@ class RecommendedAppListApi(Resource): args = RecommendedAppsQuery.model_validate(request.args.to_dict(flat=True)) language_prefix = _resolve_language(args.language, current_user) - return RecommendedAppListResponse.model_validate( + return dump_response( + RecommendedAppListResponse, RecommendedAppService.get_recommended_apps_and_categories(language_prefix, session=db.session()), - from_attributes=True, - ).model_dump(mode="json") + ) @console_ns.route("/explore/apps/learn-dify") @@ -136,10 +136,10 @@ class LearnDifyAppListApi(Resource): args = RecommendedAppsQuery.model_validate(request.args.to_dict(flat=True)) language_prefix = _resolve_language(args.language, current_user) - return LearnDifyAppListResponse.model_validate( + return dump_response( + LearnDifyAppListResponse, RecommendedAppService.get_learn_dify_apps(language_prefix, session=db.session()), - from_attributes=True, - ).model_dump(mode="json") + ) @console_ns.route("/explore/apps/") @@ -148,4 +148,5 @@ class RecommendedAppApi(Resource): @login_required @account_initialization_required def get(self, app_id: UUID): - return RecommendedAppService.get_recommend_app_detail(str(app_id), session=db.session()) + result = RecommendedAppService.get_recommend_app_detail(str(app_id), session=db.session()) + return RecommendedAppDetailNullableResponse.model_validate(result).model_dump(mode="json") diff --git a/api/controllers/console/explore/trial.py b/api/controllers/console/explore/trial.py index 67ff9708959..553e65202ce 100644 --- a/api/controllers/console/explore/trial.py +++ b/api/controllers/console/explore/trial.py @@ -46,7 +46,7 @@ from controllers.console.explore.error import ( NotCompletionAppError, NotWorkflowAppError, ) -from controllers.console.explore.wraps import TrialAppResource, trial_feature_enable +from controllers.console.explore.wraps import TrialAppResource from controllers.console.files import FILE_UPLOAD_PARAMS, upload_file_from_request from controllers.console.remote_files import RemoteFileUploadPayload, upload_remote_file_from_request from controllers.console.wraps import cloud_edition_billing_resource_check, with_current_user @@ -432,7 +432,6 @@ simple_account_model = console_ns.models[TrialSimpleAccount.__name__] class TrialAppFileUploadApi(TrialAppResource): - @trial_feature_enable @cloud_edition_billing_resource_check("documents") @console_ns.doc(consumes=["multipart/form-data"], params=FILE_UPLOAD_PARAMS) @console_ns.response(201, "File uploaded successfully", console_ns.models[FileResponse.__name__]) @@ -447,7 +446,6 @@ class TrialAppFileUploadApi(TrialAppResource): class TrialAppRemoteFileUploadApi(TrialAppResource): - @trial_feature_enable @cloud_edition_billing_resource_check("documents") @console_ns.expect(console_ns.models[RemoteFileUploadPayload.__name__]) @console_ns.response(201, "File uploaded successfully", console_ns.models[FileWithSignedUrl.__name__]) @@ -462,7 +460,6 @@ class TrialAppRemoteFileUploadApi(TrialAppResource): class TrialAppWorkflowRunApi(TrialAppResource): - @trial_feature_enable @console_ns.expect(console_ns.models[WorkflowRunRequest.__name__]) @console_ns.response(200, "Success") @with_current_user @@ -513,7 +510,6 @@ class TrialAppWorkflowRunApi(TrialAppResource): class TrialAppWorkflowTaskStopApi(TrialAppResource): @console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__]) - @trial_feature_enable def post(self, trial_app, task_id: str): """ Stop workflow task @@ -538,7 +534,6 @@ class TrialAppWorkflowTaskStopApi(TrialAppResource): class TrialChatApi(TrialAppResource): @console_ns.expect(console_ns.models[ChatRequest.__name__]) @console_ns.response(200, "Success") - @trial_feature_enable @with_current_user @with_session def post(self, session: Session, current_user: Account, trial_app): @@ -640,7 +635,6 @@ class TrialMessageSuggestedQuestionApi(TrialAppResource): class TrialChatAudioApi(TrialAppResource): @console_ns.response(200, "Success", console_ns.models[AudioTranscriptResponse.__name__]) - @trial_feature_enable @with_current_user def post(self, current_user: Account, trial_app): app_model = trial_app @@ -691,7 +685,6 @@ class TrialChatAudioApi(TrialAppResource): class TrialChatTextApi(TrialAppResource): @console_ns.expect(console_ns.models[TextToSpeechRequest.__name__]) @console_ns.response(200, "Success", console_ns.models[AudioBinaryResponse.__name__]) - @trial_feature_enable @with_current_user def post(self, current_user: Account, trial_app): app_model = trial_app @@ -752,7 +745,6 @@ class TrialChatTextApi(TrialAppResource): class TrialCompletionApi(TrialAppResource): @console_ns.expect(console_ns.models[CompletionRequest.__name__]) @console_ns.response(200, "Success") - @trial_feature_enable @with_current_user @with_session def post(self, session: Session, current_user: Account, trial_app): diff --git a/api/controllers/console/explore/wraps.py b/api/controllers/console/explore/wraps.py index d67f3e18d53..01234172849 100644 --- a/api/controllers/console/explore/wraps.py +++ b/api/controllers/console/explore/wraps.py @@ -14,6 +14,7 @@ from libs.login import current_account_with_tenant, login_required from models import AccountTrialAppRecord, App, InstalledApp, TrialApp from services.enterprise.enterprise_service import EnterpriseService from services.feature_service import FeatureService +from services.recommended_app_service import RecommendedAppService def installed_app_required[**P, R](view: Callable[Concatenate[InstalledApp, P], R] | None = None): @@ -106,8 +107,7 @@ def trial_app_required[**P, R](view: Callable[Concatenate[App, P], R] | None = N def trial_feature_enable[**P, R](view: Callable[P, R]): @wraps(view) def decorated(*args: P.args, **kwargs: P.kwargs): - features = FeatureService.get_system_features() - if not features.enable_trial_app: + if not RecommendedAppService.is_trial_app_enabled(): abort(403, "Trial app feature is not enabled.") return view(*args, **kwargs) @@ -141,6 +141,7 @@ class TrialAppResource(Resource): method_decorators = [ trial_app_required, + trial_feature_enable, account_initialization_required, login_required, ] diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index ef38f3c891d..367ba473e9e 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -21028,7 +21028,7 @@ Whitelist scopes accepted by RBAC app and dataset access config APIs. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| can_trial | boolean | | No | +| can_trial | boolean | | Yes | | export_data | string | | Yes | | icon | string | | No | | icon_background | string | | No | @@ -21061,7 +21061,7 @@ Whitelist scopes accepted by RBAC app and dataset access config APIs. | ---- | ---- | ----------- | -------- | | app | [RecommendedAppInfoResponse](#recommendedappinforesponse) | | No | | app_id | string | | Yes | -| can_trial | boolean | | No | +| can_trial | boolean | | Yes | | categories | [ string ] | | No | | copyright | string | | No | | custom_disclaimer | string | | No | @@ -22058,7 +22058,6 @@ Non-sensitive bootstrap snapshot exposed before Console or Web authentication. | enable_marketplace | boolean | | Yes | | enable_social_oauth_login | boolean | | Yes | | enable_step_by_step_tour | boolean | | Yes | -| enable_trial_app | boolean | | Yes | | is_allow_register | boolean | | Yes | | is_email_setup | boolean | | Yes | | knowledge_fs_enabled | boolean | | Yes | diff --git a/api/openapi/markdown/web-openapi.md b/api/openapi/markdown/web-openapi.md index 2ba8c35dd17..09c6842329c 100644 --- a/api/openapi/markdown/web-openapi.md +++ b/api/openapi/markdown/web-openapi.md @@ -1557,7 +1557,6 @@ Non-sensitive bootstrap snapshot exposed before Console or Web authentication. | enable_marketplace | boolean | | Yes | | enable_social_oauth_login | boolean | | Yes | | enable_step_by_step_tour | boolean | | Yes | -| enable_trial_app | boolean | | Yes | | is_allow_register | boolean | | Yes | | is_email_setup | boolean | | Yes | | knowledge_fs_enabled | boolean | | Yes | diff --git a/api/services/feature_service.py b/api/services/feature_service.py index 6225562ec25..d80d0344788 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -181,7 +181,6 @@ class SystemFeatureModel(FeatureResponseModel): plugin_installation_permission: PluginInstallationPermissionModel = PluginInstallationPermissionModel() enable_change_email: bool = True enable_creators_platform: bool = False - enable_trial_app: bool = False enable_explore_banner: bool = False enable_learn_app: bool = True enable_step_by_step_tour: bool = False @@ -310,7 +309,6 @@ class FeatureService: system_features.is_allow_register = dify_config.ALLOW_REGISTER system_features.is_email_setup = dify_config.MAIL_TYPE is not None and dify_config.MAIL_TYPE != "" system_features.enable_change_email = dify_config.ENABLE_CHANGE_EMAIL - system_features.enable_trial_app = dify_config.ENABLE_TRIAL_APP system_features.enable_explore_banner = dify_config.ENABLE_EXPLORE_BANNER system_features.enable_learn_app = dify_config.ENABLE_LEARN_APP system_features.webapp_auth.allow_public_access = dify_config.WEBAPP_PUBLIC_ACCESS_ENABLED diff --git a/api/services/recommended_app_service.py b/api/services/recommended_app_service.py index 3bdfbe6f365..8c9194e731d 100644 --- a/api/services/recommended_app_service.py +++ b/api/services/recommended_app_service.py @@ -4,12 +4,19 @@ from sqlalchemy import select from sqlalchemy.orm import Session from configs import dify_config +from enums.deployment_edition import DeploymentEdition from models.model import AccountTrialAppRecord, App, TrialApp -from services.feature_service import FeatureService from services.recommend_app.recommend_app_factory import RecommendAppRetrievalFactory class RecommendedAppService: + """Own recommended app retrieval and Cloud-only trial eligibility.""" + + @staticmethod + def is_trial_app_enabled() -> bool: + """Return whether trial execution is enabled for this deployment.""" + return dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and dify_config.ENABLE_TRIAL_APP + @classmethod def get_app(cls, app_id: str, *, session: Session) -> App | None: """Return a normal app only when it belongs to the recommended catalog.""" @@ -38,11 +45,12 @@ class RecommendedAppService: ) ) - if FeatureService.get_system_features().enable_trial_app: - apps = result["recommended_apps"] - for app in apps: - app_id = app["app_id"] - app["can_trial"] = cls._can_trial_app(session, app_id) + apps = result["recommended_apps"] + trial_app_ids = ( + cls._get_trial_app_ids(session, [app["app_id"] for app in apps]) if cls.is_trial_app_enabled() else set() + ) + for app in apps: + app["can_trial"] = app["app_id"] in trial_app_ids return result @classmethod @@ -56,11 +64,14 @@ class RecommendedAppService: retrieval_instance = RecommendAppRetrievalFactory.get_recommend_app_factory(mode)() result = retrieval_instance.get_learn_dify_apps(language, session=session) - if FeatureService.get_system_features().enable_trial_app: - for app in result["recommended_apps"]: - app["can_trial"] = cls._can_trial_app(session, app["app_id"]) + apps = result["recommended_apps"] + trial_app_ids = ( + cls._get_trial_app_ids(session, [app["app_id"] for app in apps]) if cls.is_trial_app_enabled() else set() + ) + for app in apps: + app["can_trial"] = app["app_id"] in trial_app_ids - return {"recommended_apps": result["recommended_apps"]} + return {"recommended_apps": apps} @classmethod def get_recommend_app_detail(cls, app_id: str, *, session: Session) -> dict[str, Any] | None: @@ -74,9 +85,7 @@ class RecommendedAppService: result: dict[str, Any] | None = retrieval_instance.get_recommend_app_detail(app_id, session=session) if result is None: return None - if FeatureService.get_system_features().enable_trial_app: - app_id = result["id"] - result["can_trial"] = cls._can_trial_app(session, app_id) + result["can_trial"] = cls.is_trial_app_enabled() and cls._can_trial_app(session, result["id"]) return result @classmethod @@ -102,3 +111,9 @@ class RecommendedAppService: def _can_trial_app(session: Session, app_id: str) -> bool: trial_app_model = session.scalar(select(TrialApp).where(TrialApp.app_id == app_id).limit(1)) return trial_app_model is not None + + @staticmethod + def _get_trial_app_ids(session: Session, app_ids: list[str]) -> set[str]: + if not app_ids: + return set() + return set(session.scalars(select(TrialApp.app_id).where(TrialApp.app_id.in_(app_ids))).all()) diff --git a/api/tests/unit_tests/commands/test_generate_swagger_specs.py b/api/tests/unit_tests/commands/test_generate_swagger_specs.py index 7ec832c526c..8726067822b 100644 --- a/api/tests/unit_tests/commands/test_generate_swagger_specs.py +++ b/api/tests/unit_tests/commands/test_generate_swagger_specs.py @@ -115,6 +115,7 @@ def test_system_features_specs_exclude_backend_only_fields(tmp_path): written_paths = module.generate_specs(tmp_path) excluded_fields = { + "enable_trial_app", "is_allow_create_workspace", "max_plugin_package_size", "plugin_manager", @@ -223,7 +224,13 @@ def test_generate_specs_include_console_contract_shapes_for_schema_migration(tmp app_detail_schema = schemas["RecommendedAppDetailResponse"] assert app_detail_schema["properties"]["id"]["type"] == "string" assert app_detail_schema["properties"]["export_data"]["type"] == "string" - assert {"type": "boolean"} in app_detail_schema["properties"]["can_trial"]["anyOf"] + assert app_detail_schema["properties"]["can_trial"]["type"] == "boolean" + assert "anyOf" not in app_detail_schema["properties"]["can_trial"] + assert "can_trial" in app_detail_schema["required"] + app_list_item_schema = schemas["RecommendedAppResponse"] + assert app_list_item_schema["properties"]["can_trial"]["type"] == "boolean" + assert "anyOf" not in app_list_item_schema["properties"]["can_trial"] + assert "can_trial" in app_list_item_schema["required"] app_detail_nullable_schema = schemas["RecommendedAppDetailNullableResponse"] assert _response_schema(paths["/explore/apps/{app_id}"]["get"])["$ref"] == ( "#/components/schemas/RecommendedAppDetailNullableResponse" diff --git a/api/tests/unit_tests/controllers/console/explore/test_recommended_app.py b/api/tests/unit_tests/controllers/console/explore/test_recommended_app.py index 4adeaaa90dd..9c9338ccc43 100644 --- a/api/tests/unit_tests/controllers/console/explore/test_recommended_app.py +++ b/api/tests/unit_tests/controllers/console/explore/test_recommended_app.py @@ -1,7 +1,9 @@ from inspect import unwrap from unittest.mock import ANY, patch +import pytest from flask import Flask +from pydantic import ValidationError import controllers.console.explore.recommended_app as module from models import Account @@ -119,7 +121,13 @@ class TestRecommendedAppApi: api = module.RecommendedAppApi() method = unwrap(api.get) - result_data = {"id": "app1"} + result_data = { + "id": "app1", + "name": "App", + "mode": "chat", + "export_data": "{}", + "can_trial": False, + } with ( app.test_request_context("/"), @@ -132,7 +140,7 @@ class TestRecommendedAppApi: result = method(api, "11111111-1111-1111-1111-111111111111") service_mock.assert_called_once_with("11111111-1111-1111-1111-111111111111", session=ANY) - assert result == result_data + assert result == {**result_data, "icon": None, "icon_background": None} class TestRecommendedAppResponseModels: @@ -198,6 +206,7 @@ class TestRecommendedAppResponseModels: "categories": ["Workflow"], "position": 1, "is_listed": True, + "can_trial": False, } ], } @@ -205,3 +214,7 @@ class TestRecommendedAppResponseModels: assert response["recommended_apps"][0]["app_id"] == "app-1" assert response["recommended_apps"][0]["categories"] == ["Workflow"] + + def test_recommended_app_response_requires_can_trial(self): + with pytest.raises(ValidationError): + module.RecommendedAppResponse.model_validate({"app_id": "app-1"}) diff --git a/api/tests/unit_tests/controllers/console/explore/test_trial.py b/api/tests/unit_tests/controllers/console/explore/test_trial.py index 426f83d5047..5c2d20c75a1 100644 --- a/api/tests/unit_tests/controllers/console/explore/test_trial.py +++ b/api/tests/unit_tests/controllers/console/explore/test_trial.py @@ -1109,15 +1109,13 @@ class TestTrialChatTextApi: class TestTrialAppWorkflowTaskStopApi: def test_not_workflow_app(self, app: Flask, trial_app_chat: MagicMock) -> None: api = module.TrialAppWorkflowTaskStopApi() - method = unwrap(api.post) with app.test_request_context("/"): with pytest.raises(NotWorkflowAppError): - method(api, trial_app_chat, str(uuid4())) + api.post(trial_app_chat, str(uuid4())) def test_success(self, app: Flask, trial_app_workflow: MagicMock) -> None: api = module.TrialAppWorkflowTaskStopApi() - method = unwrap(api.post) task_id = str(uuid4()) with ( @@ -1125,7 +1123,7 @@ class TestTrialAppWorkflowTaskStopApi: patch.object(module.AppQueueManager, "set_stop_flag_no_user_check") as mock_set_flag, patch.object(module.GraphEngineManager, "send_stop_command") as mock_send_cmd, ): - result = method(api, trial_app_workflow, task_id) + result = api.post(trial_app_workflow, task_id) assert result == {"result": "success"} mock_set_flag.assert_called_once_with(task_id) diff --git a/api/tests/unit_tests/controllers/console/explore/test_wraps.py b/api/tests/unit_tests/controllers/console/explore/test_wraps.py index a60c13315b8..f2eb8523bbf 100644 --- a/api/tests/unit_tests/controllers/console/explore/test_wraps.py +++ b/api/tests/unit_tests/controllers/console/explore/test_wraps.py @@ -255,11 +255,9 @@ def test_trial_feature_enable_disabled(): def view(): return "ok" - features = MagicMock(enable_trial_app=False) - with patch( - "controllers.console.explore.wraps.FeatureService.get_system_features", - return_value=features, + "controllers.console.explore.wraps.RecommendedAppService.is_trial_app_enabled", + return_value=False, ): with pytest.raises(Forbidden): view() @@ -270,11 +268,9 @@ def test_trial_feature_enable_enabled(): def view(): return "ok" - features = MagicMock(enable_trial_app=True) - with patch( - "controllers.console.explore.wraps.FeatureService.get_system_features", - return_value=features, + "controllers.console.explore.wraps.RecommendedAppService.is_trial_app_enabled", + return_value=True, ): assert view() == "ok" @@ -285,5 +281,9 @@ def test_installed_app_resource_decorators(): def test_trial_app_resource_decorators(): - decorators = TrialAppResource.method_decorators - assert len(decorators) == 3 + assert TrialAppResource.method_decorators == [ + trial_app_required, + trial_feature_enable, + wraps_module.account_initialization_required, + wraps_module.login_required, + ] diff --git a/api/tests/unit_tests/services/test_recommended_app_service.py b/api/tests/unit_tests/services/test_recommended_app_service.py index 827ded2d61a..6ebb5b62015 100644 --- a/api/tests/unit_tests/services/test_recommended_app_service.py +++ b/api/tests/unit_tests/services/test_recommended_app_service.py @@ -13,7 +13,6 @@ from sqlalchemy.orm import Session from enums.deployment_edition import DeploymentEdition from models.model import AccountTrialAppRecord, App, AppMode, TrialApp from services import recommended_app_service as service_module -from services.feature_service import SystemFeatureModel from services.recommended_app_service import RecommendedAppService pytestmark = pytest.mark.parametrize( @@ -49,6 +48,30 @@ class AppDetailKwargs(TypedDict, total=False): tools: list[str] +@pytest.mark.parametrize( + ("edition", "enterprise_enabled", "feature_enabled", "expected"), + [ + ("CLOUD", False, True, True), + ("CLOUD", False, False, False), + ("SELF_HOSTED", False, True, False), + ("SELF_HOSTED", True, True, False), + ], +) +def test_trial_app_policy_is_cloud_only( + monkeypatch: pytest.MonkeyPatch, + sqlite_session: Session, + edition: str, + enterprise_enabled: bool, + feature_enabled: bool, + expected: bool, +) -> None: + monkeypatch.setattr(service_module.dify_config, "EDITION", edition) + monkeypatch.setattr(service_module.dify_config, "ENTERPRISE_ENABLED", enterprise_enabled) + monkeypatch.setattr(service_module.dify_config, "ENABLE_TRIAL_APP", feature_enabled) + + assert RecommendedAppService.is_trial_app_enabled() is expected + + # ── Helpers ──────────────────────────────────────────────────────────── @@ -58,8 +81,8 @@ def _apps_response( ) -> AppsResponse: if recommended_apps is None: recommended_apps = [ - {"id": "app-1", "name": "Test App 1", "description": "d1", "category": "productivity"}, - {"id": "app-2", "name": "Test App 2", "description": "d2", "category": "communication"}, + {"app_id": "app-1", "name": "Test App 1", "description": "d1", "category": "productivity"}, + {"app_id": "app-2", "name": "Test App 2", "description": "d2", "category": "communication"}, ] if categories is None: categories = ["productivity", "communication", "utilities"] @@ -175,7 +198,7 @@ class TestRecommendedAppServiceGetApps: mock_config.HOSTED_FETCH_APP_TEMPLATES_MODE = "remote" empty_response = AppsResponse(recommended_apps=[], categories=[]) builtin_response = _apps_response( - recommended_apps=[{"id": "builtin-1", "name": "Builtin App", "category": "default"}] + recommended_apps=[{"app_id": "builtin-1", "name": "Builtin App", "category": "default"}] ) mock_remote_instance = MagicMock() @@ -189,7 +212,7 @@ class TestRecommendedAppServiceGetApps: result = RecommendedAppService.get_recommended_apps_and_categories("zh-CN", session=sqlite_session) assert result == builtin_response - assert result["recommended_apps"][0]["id"] == "builtin-1" + assert result["recommended_apps"][0]["app_id"] == "builtin-1" mock_builtin_instance.fetch_recommended_apps_from_builtin.assert_called_once_with("en-US") @patch("services.recommended_app_service.RecommendAppRetrievalFactory", autospec=True) @@ -223,7 +246,7 @@ class TestRecommendedAppServiceGetApps: for language in ["en-US", "zh-CN", "ja-JP", "fr-FR"]: lang_response = _apps_response( - recommended_apps=[{"id": f"app-{language}", "name": f"App {language}", "category": "test"}] + recommended_apps=[{"app_id": f"app-{language}", "name": f"App {language}", "category": "test"}] ) mock_instance = MagicMock() mock_instance.get_recommended_apps_and_categories.return_value = lang_response @@ -231,7 +254,7 @@ class TestRecommendedAppServiceGetApps: result = RecommendedAppService.get_recommended_apps_and_categories(language, session=sqlite_session) - assert result["recommended_apps"][0]["id"] == f"app-{language}" + assert result["recommended_apps"][0]["app_id"] == f"app-{language}" mock_instance.get_recommended_apps_and_categories.assert_called_with(language, session=sqlite_session) @patch("services.recommended_app_service.RecommendAppRetrievalFactory", autospec=True) @@ -262,14 +285,14 @@ class TestRecommendedAppServiceGetApp: monkeypatch, result=RecommendedAppPayload(id=app.id), ) - feature_lookup = MagicMock(side_effect=AssertionError("get_app must not inspect trial features")) - monkeypatch.setattr(service_module.FeatureService, "get_system_features", feature_lookup) + trial_policy = MagicMock(side_effect=AssertionError("get_app must not inspect trial policy")) + monkeypatch.setattr(RecommendedAppService, "is_trial_app_enabled", trial_policy) result = RecommendedAppService.get_app(app.id, session=sqlite_session) assert result is app retrieval_instance.get_recommend_app_detail.assert_called_once_with(app.id, session=sqlite_session) - feature_lookup.assert_not_called() + trial_policy.assert_not_called() def test_returns_none_when_app_is_not_recommended( self, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session @@ -288,21 +311,17 @@ class TestRecommendedAppServiceGetApp: class TestRecommendedAppServiceGetDetail: - @patch("services.recommended_app_service.FeatureService", autospec=True) @patch("services.recommended_app_service.RecommendAppRetrievalFactory", autospec=True) @patch("services.recommended_app_service.dify_config") def test_returns_retrieval_detail_when_trial_disabled( self, mock_config: MagicMock, mock_factory_class: MagicMock, - mock_feature_service: MagicMock, sqlite_session: Session, ) -> None: mock_config.HOSTED_FETCH_APP_TEMPLATES_MODE = "remote" - mock_feature_service.get_system_features.return_value = SystemFeatureModel( - deployment_edition=DeploymentEdition.COMMUNITY, - enable_trial_app=False, - ) + mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY + mock_config.ENABLE_TRIAL_APP = True cases: list[tuple[str, RecommendedAppPayload]] = [ ( "complex-app", @@ -328,23 +347,20 @@ class TestRecommendedAppServiceGetDetail: result = RecommendedAppService.get_recommend_app_detail(app_id, session=sqlite_session) - assert result == expected + assert result is not None + assert result["can_trial"] is False mock_instance.get_recommend_app_detail.assert_called_once_with(app_id, session=sqlite_session) - @patch("services.recommended_app_service.FeatureService", autospec=True) @patch("services.recommended_app_service.RecommendAppRetrievalFactory", autospec=True) @patch("services.recommended_app_service.dify_config") def test_different_modes( self, mock_config: MagicMock, mock_factory_class: MagicMock, - mock_feature_service: MagicMock, sqlite_session: Session, ) -> None: - mock_feature_service.get_system_features.return_value = SystemFeatureModel( - deployment_edition=DeploymentEdition.COMMUNITY, - enable_trial_app=False, - ) + mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY + mock_config.ENABLE_TRIAL_APP = True for mode in ["remote", "builtin", "db"]: mock_config.HOSTED_FETCH_APP_TEMPLATES_MODE = mode detail = _app_detail(app_id="test-app", name=f"App from {mode}") @@ -363,21 +379,17 @@ class TestRecommendedAppServiceGetDetail: class TestRecommendedAppServiceGetLearnDifyApps: - @patch("services.recommended_app_service.FeatureService", autospec=True) @patch("services.recommended_app_service.RecommendAppRetrievalFactory", autospec=True) @patch("services.recommended_app_service.dify_config") def test_uses_configured_retrieval_source( self, mock_config: MagicMock, mock_factory_class: MagicMock, - mock_feature_service: MagicMock, sqlite_session: Session, ) -> None: mock_config.HOSTED_FETCH_APP_TEMPLATES_MODE = "remote" - mock_feature_service.get_system_features.return_value = SystemFeatureModel( - deployment_edition=DeploymentEdition.COMMUNITY, - enable_trial_app=False, - ) + mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY + mock_config.ENABLE_TRIAL_APP = True expected_app = RecommendedAppPayload(app_id="app-1", category="Workflow") mock_instance = MagicMock() mock_instance.get_learn_dify_apps.return_value = { @@ -388,7 +400,7 @@ class TestRecommendedAppServiceGetLearnDifyApps: result = RecommendedAppService.get_learn_dify_apps("en-US", session=sqlite_session) - assert result == {"recommended_apps": [expected_app]} + assert result == {"recommended_apps": [{**expected_app, "can_trial": False}]} mock_factory_class.get_recommend_app_factory.assert_called_once_with("remote") mock_instance.get_learn_dify_apps.assert_called_once_with("en-US", session=sqlite_session) @@ -409,23 +421,14 @@ class TestRecommendedAppServiceGetLearnDifyApps: "get_recommend_app_factory", MagicMock(return_value=mock_retrieval_factory), ) - monkeypatch.setattr( - service_module.FeatureService, - "get_system_features", - MagicMock( - return_value=SystemFeatureModel( - deployment_edition=DeploymentEdition.COMMUNITY, - enable_trial_app=True, - ) - ), - ) - can_trial_mock = MagicMock(return_value=True) - monkeypatch.setattr(RecommendedAppService, "_can_trial_app", can_trial_mock) + monkeypatch.setattr(RecommendedAppService, "is_trial_app_enabled", MagicMock(return_value=True)) + trial_app_ids = MagicMock(return_value={"app-1"}) + monkeypatch.setattr(RecommendedAppService, "_get_trial_app_ids", trial_app_ids) result = RecommendedAppService.get_learn_dify_apps("en-US", session=sqlite_session) assert result["recommended_apps"][0]["can_trial"] is True - can_trial_mock.assert_called_once_with(sqlite_session, "app-1") + trial_app_ids.assert_called_once_with(sqlite_session, ["app-1"]) # ── Integration tests: trial app features (real DB) ──────────────────── @@ -435,22 +438,20 @@ class TestRecommendedAppServiceTrialFeatures: def test_get_apps_should_not_query_trial_table_when_disabled( self, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session ) -> None: - expected = AppsResponse(recommended_apps=[RecommendedAppPayload(app_id="app-1")], categories=["all"]) - retrieval_instance, builtin_instance = _mock_factory_for_apps(monkeypatch, mode="remote", result=expected) - monkeypatch.setattr( - service_module.FeatureService, - "get_system_features", - MagicMock( - return_value=SystemFeatureModel( - deployment_edition=DeploymentEdition.COMMUNITY, - enable_trial_app=False, - ) - ), + upstream_result = AppsResponse( + recommended_apps=[RecommendedAppPayload(app_id="app-1", can_trial=True)], categories=["all"] ) + retrieval_instance, builtin_instance = _mock_factory_for_apps( + monkeypatch, mode="remote", result=upstream_result + ) + monkeypatch.setattr(RecommendedAppService, "is_trial_app_enabled", MagicMock(return_value=False)) + trial_app_ids = MagicMock(side_effect=AssertionError("disabled trial must not query TrialApp")) + monkeypatch.setattr(RecommendedAppService, "_get_trial_app_ids", trial_app_ids) result = RecommendedAppService.get_recommended_apps_and_categories("en-US", session=sqlite_session) - assert result == expected + assert result["recommended_apps"][0]["can_trial"] is False + trial_app_ids.assert_not_called() retrieval_instance.get_recommended_apps_and_categories.assert_called_once_with("en-US", session=sqlite_session) builtin_instance.fetch_recommended_apps_from_builtin.assert_not_called() @@ -473,16 +474,7 @@ class TestRecommendedAppServiceTrialFeatures: _, builtin_instance = _mock_factory_for_apps( monkeypatch, mode="remote", result=remote_result, fallback_result=fallback_result ) - monkeypatch.setattr( - service_module.FeatureService, - "get_system_features", - MagicMock( - return_value=SystemFeatureModel( - deployment_edition=DeploymentEdition.COMMUNITY, - enable_trial_app=True, - ) - ), - ) + monkeypatch.setattr(RecommendedAppService, "is_trial_app_enabled", MagicMock(return_value=True)) result = RecommendedAppService.get_recommended_apps_and_categories("ja-JP", session=sqlite_session) @@ -514,16 +506,7 @@ class TestRecommendedAppServiceTrialFeatures: "get_recommend_app_factory", MagicMock(return_value=retrieval_factory), ) - monkeypatch.setattr( - service_module.FeatureService, - "get_system_features", - MagicMock( - return_value=SystemFeatureModel( - deployment_edition=DeploymentEdition.COMMUNITY, - enable_trial_app=True, - ) - ), - ) + monkeypatch.setattr(RecommendedAppService, "is_trial_app_enabled", MagicMock(return_value=True)) result = RecommendedAppService.get_recommend_app_detail(app_id, session=sqlite_session) assert result is not None @@ -532,26 +515,25 @@ class TestRecommendedAppServiceTrialFeatures: assert detail_result["id"] == app_id assert detail_result["can_trial"] is has_trial_app - @patch("services.recommended_app_service.FeatureService", autospec=True) @patch("services.recommended_app_service.RecommendAppRetrievalFactory", autospec=True) @patch("services.recommended_app_service.dify_config") def test_get_detail_returns_none_before_reading_trial_flag( self, mock_config: MagicMock, mock_factory_class: MagicMock, - mock_feature_service: MagicMock, sqlite_session: Session, ) -> None: mock_config.HOSTED_FETCH_APP_TEMPLATES_MODE = "remote" mock_instance = MagicMock() mock_instance.get_recommend_app_detail.return_value = None mock_factory_class.get_recommend_app_factory.return_value = MagicMock(return_value=mock_instance) - - result = RecommendedAppService.get_recommend_app_detail("nonexistent", session=sqlite_session) + trial_policy = MagicMock(side_effect=AssertionError("missing app must not inspect trial policy")) + with patch.object(RecommendedAppService, "is_trial_app_enabled", trial_policy): + result = RecommendedAppService.get_recommend_app_detail("nonexistent", session=sqlite_session) assert result is None mock_instance.get_recommend_app_detail.assert_called_once_with("nonexistent", session=sqlite_session) - mock_feature_service.get_system_features.assert_not_called() + trial_policy.assert_not_called() def test_add_trial_app_record_increments_count_for_existing(self, sqlite_session: Session) -> None: app_id = str(uuid.uuid4()) diff --git a/packages/contracts/generated/api/console/explore/types.gen.ts b/packages/contracts/generated/api/console/explore/types.gen.ts index 331815aaf56..7528792fca1 100644 --- a/packages/contracts/generated/api/console/explore/types.gen.ts +++ b/packages/contracts/generated/api/console/explore/types.gen.ts @@ -20,7 +20,7 @@ export type BannerListResponse = Array export type RecommendedAppResponse = { app?: RecommendedAppInfoResponse | null app_id: string - can_trial?: boolean | null + can_trial: boolean categories?: Array copyright?: string | null custom_disclaimer?: string | null @@ -31,7 +31,7 @@ export type RecommendedAppResponse = { } export type RecommendedAppDetailResponse = { - can_trial?: boolean | null + can_trial: boolean export_data: string icon?: string | null icon_background?: string | null @@ -71,7 +71,7 @@ export type LearnDifyAppListResponseWritable = { export type RecommendedAppResponseWritable = { app?: RecommendedAppInfoResponseWritable | null app_id: string - can_trial?: boolean | null + can_trial: boolean categories?: Array copyright?: string | null custom_disclaimer?: string | null diff --git a/packages/contracts/generated/api/console/explore/zod.gen.ts b/packages/contracts/generated/api/console/explore/zod.gen.ts index b835f405436..895e7f9f230 100644 --- a/packages/contracts/generated/api/console/explore/zod.gen.ts +++ b/packages/contracts/generated/api/console/explore/zod.gen.ts @@ -6,7 +6,7 @@ import * as z from 'zod' * RecommendedAppDetailResponse */ export const zRecommendedAppDetailResponse = z.object({ - can_trial: z.boolean().nullish(), + can_trial: z.boolean(), export_data: z.string(), icon: z.string().nullish(), icon_background: z.string().nullish(), @@ -56,7 +56,7 @@ export const zRecommendedAppInfoResponse = z.object({ export const zRecommendedAppResponse = z.object({ app: zRecommendedAppInfoResponse.nullish(), app_id: z.string(), - can_trial: z.boolean().nullish(), + can_trial: z.boolean(), categories: z.array(z.string()).optional(), copyright: z.string().nullish(), custom_disclaimer: z.string().nullish(), @@ -99,7 +99,7 @@ export const zRecommendedAppInfoResponseWritable = z.object({ export const zRecommendedAppResponseWritable = z.object({ app: zRecommendedAppInfoResponseWritable.nullish(), app_id: z.string(), - can_trial: z.boolean().nullish(), + can_trial: z.boolean(), categories: z.array(z.string()).optional(), copyright: z.string().nullish(), custom_disclaimer: z.string().nullish(), diff --git a/packages/contracts/generated/api/console/system-features/types.gen.ts b/packages/contracts/generated/api/console/system-features/types.gen.ts index 076f98546cc..46ccea0793b 100644 --- a/packages/contracts/generated/api/console/system-features/types.gen.ts +++ b/packages/contracts/generated/api/console/system-features/types.gen.ts @@ -18,7 +18,6 @@ export type SystemFeatureModel = { enable_marketplace: boolean enable_social_oauth_login: boolean enable_step_by_step_tour: boolean - enable_trial_app: boolean is_allow_register: boolean is_email_setup: boolean knowledge_fs_enabled: boolean diff --git a/packages/contracts/generated/api/console/system-features/zod.gen.ts b/packages/contracts/generated/api/console/system-features/zod.gen.ts index 20cf33d3891..7c2d20f47db 100644 --- a/packages/contracts/generated/api/console/system-features/zod.gen.ts +++ b/packages/contracts/generated/api/console/system-features/zod.gen.ts @@ -125,7 +125,6 @@ export const zSystemFeatureModel = z.object({ enable_marketplace: z.boolean().default(false), enable_social_oauth_login: z.boolean().default(false), enable_step_by_step_tour: z.boolean().default(false), - enable_trial_app: z.boolean().default(false), is_allow_register: z.boolean().default(false), is_email_setup: z.boolean().default(false), knowledge_fs_enabled: z.boolean().default(false), diff --git a/packages/contracts/generated/api/web/types.gen.ts b/packages/contracts/generated/api/web/types.gen.ts index 14e9bbf2e53..d832ca3d98a 100644 --- a/packages/contracts/generated/api/web/types.gen.ts +++ b/packages/contracts/generated/api/web/types.gen.ts @@ -511,7 +511,6 @@ export type SystemFeatureModel = { enable_marketplace: boolean enable_social_oauth_login: boolean enable_step_by_step_tour: boolean - enable_trial_app: boolean is_allow_register: boolean is_email_setup: boolean knowledge_fs_enabled: boolean diff --git a/packages/contracts/generated/api/web/zod.gen.ts b/packages/contracts/generated/api/web/zod.gen.ts index 3dbb9584950..0e1cd8fc76d 100644 --- a/packages/contracts/generated/api/web/zod.gen.ts +++ b/packages/contracts/generated/api/web/zod.gen.ts @@ -772,7 +772,6 @@ export const zSystemFeatureModel = z.object({ enable_marketplace: z.boolean().default(false), enable_social_oauth_login: z.boolean().default(false), enable_step_by_step_tour: z.boolean().default(false), - enable_trial_app: z.boolean().default(false), is_allow_register: z.boolean().default(false), is_email_setup: z.boolean().default(false), knowledge_fs_enabled: z.boolean().default(false), diff --git a/web/app/components/apps/__tests__/index.spec.tsx b/web/app/components/apps/__tests__/index.spec.tsx index c39835ba4c3..a3142b53882 100644 --- a/web/app/components/apps/__tests__/index.spec.tsx +++ b/web/app/components/apps/__tests__/index.spec.tsx @@ -266,6 +266,7 @@ describe('Apps', () => { icon_background: '#fff', mode: AppModeEnum.CHAT, export_data: 'yaml-content', + can_trial: true, }) }) @@ -305,6 +306,16 @@ describe('Apps', () => { expect(await screen.findByTestId('try-app-panel')).toBeInTheDocument() }) + it('should close the template preview', async () => { + const user = userEvent.setup() + renderWithClient() + + await user.click(screen.getByTestId('open-preview')) + await user.click(await screen.findByTestId('try-app-close')) + + expect(screen.queryByTestId('try-app-panel')).not.toBeInTheDocument() + }) + it('should open the create modal from Learn Dify', async () => { const user = userEvent.setup() renderWithClient() diff --git a/web/app/components/apps/index.tsx b/web/app/components/apps/index.tsx index 07376c80d18..c593b97ca90 100644 --- a/web/app/components/apps/index.tsx +++ b/web/app/components/apps/index.tsx @@ -206,11 +206,11 @@ const Apps = () => { onCreateLearnDify={handleCreateLearnDify} onTryLearnDify={handleTryLearnDify} /> - {isShowTryAppPanel && ( + {isShowTryAppPanel && currentTryAppParams && ( diff --git a/web/app/components/explore/app-list/index.tsx b/web/app/components/explore/app-list/index.tsx index 277afe0eb67..b8ec1a132ce 100644 --- a/web/app/components/explore/app-list/index.tsx +++ b/web/app/components/explore/app-list/index.tsx @@ -241,7 +241,6 @@ const Apps = ({ onSuccess }: { onSuccess?: () => void }) => { const shouldCompleteHomeTourOnCreateRef = useRef(false) const isSubmittingHomeTourCreateRef = useRef(false) const wasHomeTryAppCreateGuideActiveRef = useRef(false) - const isShowTryAppPanel = !!currentTryApp const shouldForceShowLearnDifyForTour = activeStepByStepTourTaskId === HOME_STEP_BY_STEP_TOUR_TASK_ID && !completedStepByStepTourTaskIds.includes(HOME_STEP_BY_STEP_TOUR_TASK_ID) && @@ -550,12 +549,12 @@ const Apps = ({ onSuccess }: { onSuccess?: () => void }) => { /> )} - {isShowTryAppPanel && ( + {currentTryApp && ( - renderWithConsoleQuery(ui, { systemFeatures: { deployment_edition: 'CLOUD' } }) +const defaultApp = { can_trial: true } as ExploreApp + +function TryApp({ + app = defaultApp, + ...props +}: Omit, 'app'> & { + app?: ExploreApp +}) { + return +} const mockUseGetTryAppInfo = vi.fn() @@ -143,6 +152,26 @@ describe('TryApp (main index.tsx)', () => { }) describe('content rendering', () => { + it('uses app trial eligibility as the authoritative default tab', async () => { + const app = { can_trial: true } as ExploreApp + + render() + + expect(await screen.findByTestId('app-component')).toBeInTheDocument() + }) + + it('defaults to details and disables trial when the app is ineligible', async () => { + const app = { can_trial: false } as ExploreApp + + render() + + expect(await screen.findByTestId('preview-component')).toBeInTheDocument() + expect(screen.getByRole('tab', { name: 'explore.tryApp.tabHeader.try' })).toHaveAttribute( + 'aria-disabled', + 'true', + ) + }) + it('renders Tab component', async () => { render() diff --git a/web/app/components/explore/try-app/index.tsx b/web/app/components/explore/try-app/index.tsx index 32f3ba7a367..294dbbf17e0 100644 --- a/web/app/components/explore/try-app/index.tsx +++ b/web/app/components/explore/try-app/index.tsx @@ -1,17 +1,14 @@ /* eslint-disable style/multiline-ternary */ 'use client' -import type { FC } from 'react' import type { App as AppType } from '@/models/explore' import { Button } from '@langgenius/dify-ui/button' import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' import { Tabs, TabsList, TabsPanel, TabsTab } from '@langgenius/dify-ui/tabs' -import { useSuspenseQuery } from '@tanstack/react-query' import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' import AppUnavailable from '@/app/components/base/app-unavailable' import Loading from '@/app/components/base/loading' -import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { useGetTryAppInfo } from '@/service/use-try-app' import App from './app' import AppInfo from './app-info' @@ -20,7 +17,7 @@ import { TypeEnum } from './types' type Props = Readonly<{ appId: string - app?: AppType + app: AppType canCreate?: boolean categories?: string[] createButtonStepByStepTourTarget?: string @@ -28,7 +25,7 @@ type Props = Readonly<{ onCreate: () => void }> -const TryApp: FC = ({ +function TryApp({ appId, app, canCreate = true, @@ -36,11 +33,9 @@ const TryApp: FC = ({ createButtonStepByStepTourTarget, onClose, onCreate, -}) => { +}: Props) { const { t } = useTranslation() - const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) - const isTrialApp = !!(app && app.can_trial && systemFeatures.enable_trial_app) - const canUseTryTab = systemFeatures.deployment_edition === 'CLOUD' && (app ? isTrialApp : true) + const canUseTryTab = app.can_trial const [type, setType] = useState(() => (canUseTryTab ? TypeEnum.TRY : TypeEnum.DETAIL)) const activeType = canUseTryTab ? type : TypeEnum.DETAIL const { data: appDetail, isLoading, isError, error } = useGetTryAppInfo(appId) @@ -77,17 +72,15 @@ const TryApp: FC = ({ >
- {systemFeatures.deployment_edition === 'CLOUD' && ( - - - {t(($) => $['tryApp.tabHeader.try'], { ns: 'explore' })} - - - )} + + + {t(($) => $['tryApp.tabHeader.try'], { ns: 'explore' })} + + = ({
{/* Main content */}
- {systemFeatures.deployment_edition === 'CLOUD' && ( - - - - )} + + + diff --git a/web/service/explore.spec.ts b/web/service/explore.spec.ts index 4779caaa3c3..45200f46024 100644 --- a/web/service/explore.spec.ts +++ b/web/service/explore.spec.ts @@ -53,6 +53,7 @@ describe('explore service normalizers', () => { icon_background: '', mode: 'rag-pipeline', export_data: 'kind: app', + can_trial: false, }) await expect(fetchAppList()).resolves.toMatchObject({ diff --git a/web/service/explore.ts b/web/service/explore.ts index 8f474b09316..2e0a1081950 100644 --- a/web/service/explore.ts +++ b/web/service/explore.ts @@ -40,7 +40,7 @@ type ExploreAppDetailResponse = { icon_background: string mode: string export_data: string - can_trial?: boolean | null + can_trial: boolean } type InstalledAppsResponse = { @@ -141,7 +141,7 @@ const normalizeRecommendedApp = (app: RecommendedAppResponse): App => { installed: false, editable: false, is_agent: false, - can_trial: app.can_trial ?? false, + can_trial: app.can_trial, } } diff --git a/web/test/console/system-features.ts b/web/test/console/system-features.ts index 50f01491730..0d12aef5cab 100644 --- a/web/test/console/system-features.ts +++ b/web/test/console/system-features.ts @@ -50,7 +50,6 @@ const baseSystemFeatures = { }, rbac_enabled: false, enable_creators_platform: false, - enable_trial_app: false, enable_explore_banner: false, enable_learn_app: true, enable_step_by_step_tour: false,