From e54383d0fe369479941d555ba7d77bfccd53df6d Mon Sep 17 00:00:00 2001 From: Akash Kumar Date: Thu, 2 Apr 2026 12:10:52 +0530 Subject: [PATCH 001/549] test: added test for api/services/rag_pipeline folder (#33222) Co-authored-by: sahil-infocusp <73810410+sahil-infocusp@users.noreply.github.com> --- api/services/rag_pipeline/rag_pipeline.py | 2 +- .../test_built_in_retrieval.py | 110 + .../test_customized_retrieval.py | 89 + .../test_database_retrieval.py | 87 + .../pipeline_template/test_package_imports.py | 19 + .../test_pipeline_template_base.py | 43 + .../test_pipeline_template_factory.py | 34 + .../test_pipeline_template_type.py | 8 + .../test_remote_retrieval.py | 98 + .../test_pipeline_generate_service.py | 155 ++ .../test_pipeline_service_api_entities.py | 34 + .../test_rag_pipeline_dsl_service.py | 1325 ++++++++++ .../test_rag_pipeline_manage_service.py | 24 + .../rag_pipeline/test_rag_pipeline_service.py | 2318 +++++++++++++++++ .../test_rag_pipeline_task_proxy.py | 159 ++ .../test_rag_pipeline_transform_service.py | 516 ++++ 16 files changed, 5020 insertions(+), 1 deletion(-) create mode 100644 api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_built_in_retrieval.py create mode 100644 api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_customized_retrieval.py create mode 100644 api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_database_retrieval.py create mode 100644 api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_package_imports.py create mode 100644 api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_pipeline_template_base.py create mode 100644 api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_pipeline_template_factory.py create mode 100644 api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_pipeline_template_type.py create mode 100644 api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_remote_retrieval.py create mode 100644 api/tests/unit_tests/services/rag_pipeline/test_pipeline_generate_service.py create mode 100644 api/tests/unit_tests/services/rag_pipeline/test_pipeline_service_api_entities.py create mode 100644 api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_dsl_service.py create mode 100644 api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_manage_service.py create mode 100644 api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_service.py create mode 100644 api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_task_proxy.py create mode 100644 api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_transform_service.py diff --git a/api/services/rag_pipeline/rag_pipeline.py b/api/services/rag_pipeline/rag_pipeline.py index bcf5973d7b2..50f34d5a8ad 100644 --- a/api/services/rag_pipeline/rag_pipeline.py +++ b/api/services/rag_pipeline/rag_pipeline.py @@ -574,7 +574,7 @@ class RagPipelineService: outputs=workflow_node_execution.outputs, ) session.commit() - if workflow_node_execution_db_model is not None: + if isinstance(workflow_node_execution_db_model, WorkflowNodeExecutionModel): enqueue_draft_node_execution_trace( execution=workflow_node_execution_db_model, outputs=workflow_node_execution.outputs, diff --git a/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_built_in_retrieval.py b/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_built_in_retrieval.py new file mode 100644 index 00000000000..1928958ea4a --- /dev/null +++ b/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_built_in_retrieval.py @@ -0,0 +1,110 @@ +from services.rag_pipeline.pipeline_template.built_in.built_in_retrieval import BuiltInPipelineTemplateRetrieval +from services.rag_pipeline.pipeline_template.pipeline_template_type import PipelineTemplateType + + +def test_get_type() -> None: + retrieval = BuiltInPipelineTemplateRetrieval() + + assert retrieval.get_type() == PipelineTemplateType.BUILTIN + + +def test_get_pipeline_templates(mocker) -> None: + mocker.patch.object( + BuiltInPipelineTemplateRetrieval, + "_get_builtin_data", + return_value={ + "pipeline_templates": { + "en-US": {"pipeline_templates": [{"id": "tpl-1"}]}, + "tpl-1": {"id": "tpl-1", "name": "Template 1"}, + } + }, + ) + retrieval = BuiltInPipelineTemplateRetrieval() + + templates = retrieval.get_pipeline_templates("en-US") + + assert templates == {"pipeline_templates": [{"id": "tpl-1"}]} + + +def test_get_pipeline_template_detail(mocker) -> None: + mocker.patch.object( + BuiltInPipelineTemplateRetrieval, + "_get_builtin_data", + return_value={ + "pipeline_templates": { + "tpl-1": {"id": "tpl-1", "name": "Template 1"}, + } + }, + ) + retrieval = BuiltInPipelineTemplateRetrieval() + + detail = retrieval.get_pipeline_template_detail("tpl-1") + + assert detail == {"id": "tpl-1", "name": "Template 1"} + + +def test_get_pipeline_templates_missing_language_returns_empty_dict(mocker) -> None: + mocker.patch.object( + BuiltInPipelineTemplateRetrieval, + "_get_builtin_data", + return_value={"pipeline_templates": {}}, + ) + retrieval = BuiltInPipelineTemplateRetrieval() + + result = retrieval.get_pipeline_templates("fr-FR") + + assert result == {} + + +def test_get_pipeline_template_detail_returns_none_for_unknown_id(mocker) -> None: + mocker.patch.object( + BuiltInPipelineTemplateRetrieval, + "_get_builtin_data", + return_value={"pipeline_templates": {"tpl-1": {"id": "tpl-1"}}}, + ) + retrieval = BuiltInPipelineTemplateRetrieval() + + result = retrieval.get_pipeline_template_detail("nonexistent-id") + + assert result is None + + +def test_get_builtin_data_reads_from_file_and_caches(mocker) -> None: + import json + + # Ensure no cached data + BuiltInPipelineTemplateRetrieval.builtin_data = None + + mock_app = mocker.Mock() + mock_app.root_path = "/fake/root" + + mocker.patch( + "services.rag_pipeline.pipeline_template.built_in.built_in_retrieval.current_app", + mock_app, + ) + + test_data = {"pipeline_templates": {"en-US": {"templates": []}}} + mocker.patch( + "services.rag_pipeline.pipeline_template.built_in.built_in_retrieval.Path.read_text", + return_value=json.dumps(test_data), + ) + + result = BuiltInPipelineTemplateRetrieval._get_builtin_data() + + assert result == test_data + assert BuiltInPipelineTemplateRetrieval.builtin_data == test_data + + # Reset class state + BuiltInPipelineTemplateRetrieval.builtin_data = None + + +def test_get_builtin_data_returns_cache_on_second_call(mocker) -> None: + cached_data = {"pipeline_templates": {"en-US": {}}} + BuiltInPipelineTemplateRetrieval.builtin_data = cached_data + + result = BuiltInPipelineTemplateRetrieval._get_builtin_data() + + assert result == cached_data + + # Reset class state + BuiltInPipelineTemplateRetrieval.builtin_data = None diff --git a/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_customized_retrieval.py b/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_customized_retrieval.py new file mode 100644 index 00000000000..647a2f0bfc9 --- /dev/null +++ b/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_customized_retrieval.py @@ -0,0 +1,89 @@ +from types import SimpleNamespace + +from services.rag_pipeline.pipeline_template.customized.customized_retrieval import CustomizedPipelineTemplateRetrieval +from services.rag_pipeline.pipeline_template.pipeline_template_type import PipelineTemplateType + + +def test_get_pipeline_templates(mocker) -> None: + mocker.patch( + "services.rag_pipeline.pipeline_template.customized.customized_retrieval.current_account_with_tenant", + return_value=("account-id", "tenant-id"), + ) + customized_template = SimpleNamespace( + id="tpl-1", + name="Custom Template", + description="desc", + icon={"background": "#fff"}, + position=2, + chunk_structure="parent-child", + ) + scalars_mock = mocker.Mock() + scalars_mock.all.return_value = [customized_template] + session_mock = mocker.Mock() + session_mock.scalars.return_value = scalars_mock + mocker.patch( + "services.rag_pipeline.pipeline_template.customized.customized_retrieval.db", + new=SimpleNamespace(session=session_mock), + ) + retrieval = CustomizedPipelineTemplateRetrieval() + + result = retrieval.get_pipeline_templates("en-US") + + assert retrieval.get_type() == PipelineTemplateType.CUSTOMIZED + assert result == { + "pipeline_templates": [ + { + "id": "tpl-1", + "name": "Custom Template", + "description": "desc", + "icon": {"background": "#fff"}, + "position": 2, + "chunk_structure": "parent-child", + } + ] + } + + +def test_get_pipeline_template_detail_returns_detail(mocker) -> None: + session_mock = mocker.Mock() + session_mock.get.return_value = SimpleNamespace( + id="tpl-1", + name="Custom Template", + icon={"background": "#fff"}, + description="desc", + chunk_structure="parent-child", + yaml_content="workflow:\n graph:\n edges: []", + created_user_name="creator", + ) + mocker.patch( + "services.rag_pipeline.pipeline_template.customized.customized_retrieval.db", + new=SimpleNamespace(session=session_mock), + ) + retrieval = CustomizedPipelineTemplateRetrieval() + + detail = retrieval.get_pipeline_template_detail("tpl-1") + + assert detail == { + "id": "tpl-1", + "name": "Custom Template", + "icon_info": {"background": "#fff"}, + "description": "desc", + "chunk_structure": "parent-child", + "export_data": "workflow:\n graph:\n edges: []", + "graph": {"edges": []}, + "created_by": "creator", + } + + +def test_get_pipeline_template_detail_returns_none_when_not_found(mocker) -> None: + session_mock = mocker.Mock() + session_mock.get.return_value = None + mocker.patch( + "services.rag_pipeline.pipeline_template.customized.customized_retrieval.db", + new=SimpleNamespace(session=session_mock), + ) + retrieval = CustomizedPipelineTemplateRetrieval() + + result = retrieval.get_pipeline_template_detail("missing") + + assert result is None diff --git a/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_database_retrieval.py b/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_database_retrieval.py new file mode 100644 index 00000000000..0175f66808b --- /dev/null +++ b/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_database_retrieval.py @@ -0,0 +1,87 @@ +from types import SimpleNamespace + +from services.rag_pipeline.pipeline_template.database.database_retrieval import DatabasePipelineTemplateRetrieval +from services.rag_pipeline.pipeline_template.pipeline_template_type import PipelineTemplateType + + +def test_get_pipeline_templates(mocker) -> None: + built_in_template = SimpleNamespace( + id="tpl-1", + name="Template 1", + description="desc", + icon={"background": "#fff"}, + copyright="copyright", + privacy_policy="https://example.com/privacy", + position=1, + chunk_structure="general", + ) + scalars_mock = mocker.Mock() + scalars_mock.all.return_value = [built_in_template] + session_mock = mocker.Mock() + session_mock.scalars.return_value = scalars_mock + mocker.patch( + "services.rag_pipeline.pipeline_template.database.database_retrieval.db", + new=SimpleNamespace(session=session_mock), + ) + retrieval = DatabasePipelineTemplateRetrieval() + + result = retrieval.get_pipeline_templates("en-US") + + assert retrieval.get_type() == PipelineTemplateType.DATABASE + assert result == { + "pipeline_templates": [ + { + "id": "tpl-1", + "name": "Template 1", + "description": "desc", + "icon": {"background": "#fff"}, + "copyright": "copyright", + "privacy_policy": "https://example.com/privacy", + "position": 1, + "chunk_structure": "general", + } + ] + } + + +def test_get_pipeline_template_detail_returns_detail(mocker) -> None: + session_mock = mocker.Mock() + session_mock.get.return_value = SimpleNamespace( + id="tpl-1", + name="Template 1", + icon={"background": "#fff"}, + description="desc", + chunk_structure="general", + yaml_content="workflow:\n graph:\n nodes: []", + ) + mocker.patch( + "services.rag_pipeline.pipeline_template.database.database_retrieval.db", + new=SimpleNamespace(session=session_mock), + ) + retrieval = DatabasePipelineTemplateRetrieval() + + detail = retrieval.get_pipeline_template_detail("tpl-1") + + assert detail == { + "id": "tpl-1", + "name": "Template 1", + "icon_info": {"background": "#fff"}, + "description": "desc", + "chunk_structure": "general", + "export_data": "workflow:\n graph:\n nodes: []", + "graph": {"nodes": []}, + } + + +def test_get_pipeline_template_detail_returns_none_when_not_found(mocker) -> None: + session_mock = mocker.Mock() + session_mock.get.return_value = None + mocker.patch( + "services.rag_pipeline.pipeline_template.database.database_retrieval.db", + new=SimpleNamespace(session=session_mock), + ) + retrieval = DatabasePipelineTemplateRetrieval() + + result = retrieval.get_pipeline_template_detail("missing") + + assert result is None diff --git a/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_package_imports.py b/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_package_imports.py new file mode 100644 index 00000000000..a8b545508ff --- /dev/null +++ b/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_package_imports.py @@ -0,0 +1,19 @@ +import importlib + +import pytest + + +@pytest.mark.parametrize( + "module_name", + [ + "services.rag_pipeline.pipeline_template", + "services.rag_pipeline.pipeline_template.built_in", + "services.rag_pipeline.pipeline_template.customized", + "services.rag_pipeline.pipeline_template.database", + "services.rag_pipeline.pipeline_template.remote", + ], +) +def test_package_imports(module_name: str) -> None: + module = importlib.import_module(module_name) + + assert module is not None diff --git a/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_pipeline_template_base.py b/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_pipeline_template_base.py new file mode 100644 index 00000000000..304ee8faa39 --- /dev/null +++ b/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_pipeline_template_base.py @@ -0,0 +1,43 @@ +import pytest + +from services.rag_pipeline.pipeline_template.pipeline_template_base import PipelineTemplateRetrievalBase + + +class DummyRetrieval(PipelineTemplateRetrievalBase): + def get_pipeline_templates(self, language: str) -> dict: + return {"language": language} + + def get_pipeline_template_detail(self, template_id: str) -> dict | None: + return {"id": template_id} + + def get_type(self) -> str: + return "dummy" + + +class MissingTypeRetrieval(PipelineTemplateRetrievalBase): + def get_pipeline_templates(self, language: str) -> dict: + return {"language": language} + + def get_pipeline_template_detail(self, template_id: str) -> dict | None: + return {"id": template_id} + + +def test_pipeline_template_retrieval_base_concrete_implementation() -> None: + retrieval = DummyRetrieval() + + assert retrieval.get_pipeline_templates("en-US") == {"language": "en-US"} + assert retrieval.get_pipeline_template_detail("tpl-1") == {"id": "tpl-1"} + assert retrieval.get_type() == "dummy" + + +def test_pipeline_template_retrieval_base_requires_abstract_methods() -> None: + assert "get_type" in MissingTypeRetrieval.__abstractmethods__ + + +def test_pipeline_template_retrieval_base_default_methods_raise() -> None: + with pytest.raises(NotImplementedError): + PipelineTemplateRetrievalBase.get_pipeline_templates(DummyRetrieval(), "en-US") + with pytest.raises(NotImplementedError): + PipelineTemplateRetrievalBase.get_pipeline_template_detail(DummyRetrieval(), "tpl-1") + with pytest.raises(NotImplementedError): + PipelineTemplateRetrievalBase.get_type(DummyRetrieval()) diff --git a/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_pipeline_template_factory.py b/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_pipeline_template_factory.py new file mode 100644 index 00000000000..d8178490e91 --- /dev/null +++ b/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_pipeline_template_factory.py @@ -0,0 +1,34 @@ +import pytest + +from services.rag_pipeline.pipeline_template.built_in.built_in_retrieval import BuiltInPipelineTemplateRetrieval +from services.rag_pipeline.pipeline_template.customized.customized_retrieval import CustomizedPipelineTemplateRetrieval +from services.rag_pipeline.pipeline_template.database.database_retrieval import DatabasePipelineTemplateRetrieval +from services.rag_pipeline.pipeline_template.pipeline_template_factory import PipelineTemplateRetrievalFactory +from services.rag_pipeline.pipeline_template.pipeline_template_type import PipelineTemplateType +from services.rag_pipeline.pipeline_template.remote.remote_retrieval import RemotePipelineTemplateRetrieval + + +@pytest.mark.parametrize( + ("mode", "expected_cls"), + [ + (PipelineTemplateType.REMOTE, RemotePipelineTemplateRetrieval), + (PipelineTemplateType.CUSTOMIZED, CustomizedPipelineTemplateRetrieval), + (PipelineTemplateType.DATABASE, DatabasePipelineTemplateRetrieval), + (PipelineTemplateType.BUILTIN, BuiltInPipelineTemplateRetrieval), + ], +) +def test_get_pipeline_template_factory(mode: str, expected_cls: type) -> None: + result = PipelineTemplateRetrievalFactory.get_pipeline_template_factory(mode) + + assert result is expected_cls + + +def test_get_pipeline_template_factory_invalid_mode() -> None: + with pytest.raises(ValueError): + PipelineTemplateRetrievalFactory.get_pipeline_template_factory("invalid") + + +def test_get_built_in_pipeline_template_retrieval() -> None: + result = PipelineTemplateRetrievalFactory.get_built_in_pipeline_template_retrieval() + + assert result is BuiltInPipelineTemplateRetrieval diff --git a/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_pipeline_template_type.py b/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_pipeline_template_type.py new file mode 100644 index 00000000000..738ab6a5e7b --- /dev/null +++ b/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_pipeline_template_type.py @@ -0,0 +1,8 @@ +from services.rag_pipeline.pipeline_template.pipeline_template_type import PipelineTemplateType + + +def test_pipeline_template_type_values() -> None: + assert PipelineTemplateType.REMOTE == "remote" + assert PipelineTemplateType.DATABASE == "database" + assert PipelineTemplateType.CUSTOMIZED == "customized" + assert PipelineTemplateType.BUILTIN == "builtin" diff --git a/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_remote_retrieval.py b/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_remote_retrieval.py new file mode 100644 index 00000000000..10b5bc7cf67 --- /dev/null +++ b/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_remote_retrieval.py @@ -0,0 +1,98 @@ +import pytest + +from services.rag_pipeline.pipeline_template.database.database_retrieval import DatabasePipelineTemplateRetrieval +from services.rag_pipeline.pipeline_template.pipeline_template_type import PipelineTemplateType +from services.rag_pipeline.pipeline_template.remote.remote_retrieval import RemotePipelineTemplateRetrieval + + +def test_get_pipeline_templates_fallbacks_to_database_on_error(mocker) -> None: + fetch_mock = mocker.patch.object( + RemotePipelineTemplateRetrieval, + "fetch_pipeline_templates_from_dify_official", + side_effect=RuntimeError("boom"), + ) + fallback_mock = mocker.patch.object( + DatabasePipelineTemplateRetrieval, + "fetch_pipeline_templates_from_db", + return_value={"pipeline_templates": [{"id": "db-1"}]}, + ) + retrieval = RemotePipelineTemplateRetrieval() + + result = retrieval.get_pipeline_templates("en-US") + + assert retrieval.get_type() == PipelineTemplateType.REMOTE + assert result == {"pipeline_templates": [{"id": "db-1"}]} + fetch_mock.assert_called_once_with("en-US") + fallback_mock.assert_called_once_with("en-US") + + +def test_get_pipeline_template_detail_fallbacks_to_database_on_error(mocker) -> None: + fetch_mock = mocker.patch.object( + RemotePipelineTemplateRetrieval, + "fetch_pipeline_template_detail_from_dify_official", + side_effect=RuntimeError("boom"), + ) + fallback_mock = mocker.patch.object( + DatabasePipelineTemplateRetrieval, + "fetch_pipeline_template_detail_from_db", + return_value={"id": "db-1"}, + ) + retrieval = RemotePipelineTemplateRetrieval() + + result = retrieval.get_pipeline_template_detail("tpl-1") + + assert result == {"id": "db-1"} + fetch_mock.assert_called_once_with("tpl-1") + fallback_mock.assert_called_once_with("tpl-1") + + +def test_fetch_pipeline_templates_from_dify_official(mocker) -> None: + mocker.patch( + "services.rag_pipeline.pipeline_template.remote.remote_retrieval" + ".dify_config.HOSTED_FETCH_PIPELINE_TEMPLATES_REMOTE_DOMAIN", + "https://example.com", + ) + + success_response = mocker.Mock(status_code=200) + success_response.json.return_value = {"pipeline_templates": [{"id": "remote-1"}]} + + failed_response = mocker.Mock(status_code=500) + + http_get_mock = mocker.patch( + "services.rag_pipeline.pipeline_template.remote.remote_retrieval.httpx.get", + side_effect=[success_response, failed_response], + ) + + success_result = RemotePipelineTemplateRetrieval.fetch_pipeline_templates_from_dify_official("en-US") + + with pytest.raises(ValueError): + RemotePipelineTemplateRetrieval.fetch_pipeline_templates_from_dify_official("en-US") + + assert success_result == {"pipeline_templates": [{"id": "remote-1"}]} + assert http_get_mock.call_count == 2 + + +def test_fetch_pipeline_template_detail_from_dify_official(mocker) -> None: + mocker.patch( + "services.rag_pipeline.pipeline_template.remote.remote_retrieval" + ".dify_config.HOSTED_FETCH_PIPELINE_TEMPLATES_REMOTE_DOMAIN", + "https://example.com", + ) + + success_response = mocker.Mock(status_code=200) + success_response.json.return_value = {"id": "remote-1", "name": "Remote Template"} + + failed_response = mocker.Mock(status_code=404) + failed_response.text = "Not Found" + + http_get_mock = mocker.patch( + "services.rag_pipeline.pipeline_template.remote.remote_retrieval.httpx.get", + side_effect=[success_response, failed_response], + ) + + success_result = RemotePipelineTemplateRetrieval.fetch_pipeline_template_detail_from_dify_official("remote-1") + with pytest.raises(ValueError): + RemotePipelineTemplateRetrieval.fetch_pipeline_template_detail_from_dify_official("missing") + + assert success_result == {"id": "remote-1", "name": "Remote Template"} + assert http_get_mock.call_count == 2 diff --git a/api/tests/unit_tests/services/rag_pipeline/test_pipeline_generate_service.py b/api/tests/unit_tests/services/rag_pipeline/test_pipeline_generate_service.py new file mode 100644 index 00000000000..82a5598b13d --- /dev/null +++ b/api/tests/unit_tests/services/rag_pipeline/test_pipeline_generate_service.py @@ -0,0 +1,155 @@ +from types import SimpleNamespace +from typing import cast + +import pytest + +from core.app.entities.app_invoke_entities import InvokeFrom +from models.dataset import Pipeline +from models.model import Account, App, EndUser +from services.rag_pipeline.pipeline_generate_service import PipelineGenerateService + + +def test_get_max_active_requests_uses_smallest_non_zero_limit(mocker) -> None: + mocker.patch("services.rag_pipeline.pipeline_generate_service.dify_config.APP_DEFAULT_ACTIVE_REQUESTS", 5) + mocker.patch("services.rag_pipeline.pipeline_generate_service.dify_config.APP_MAX_ACTIVE_REQUESTS", 3) + + app_model = cast(App, SimpleNamespace(max_active_requests=10)) + + result = PipelineGenerateService._get_max_active_requests(app_model) + + assert result == 3 + + +def test_get_max_active_requests_returns_zero_when_all_unlimited(mocker) -> None: + mocker.patch("services.rag_pipeline.pipeline_generate_service.dify_config.APP_DEFAULT_ACTIVE_REQUESTS", 0) + mocker.patch("services.rag_pipeline.pipeline_generate_service.dify_config.APP_MAX_ACTIVE_REQUESTS", 0) + + app_model = cast(App, SimpleNamespace(max_active_requests=0)) + + result = PipelineGenerateService._get_max_active_requests(app_model) + + assert result == 0 + + +@pytest.mark.parametrize( + ("invoke_from", "workflow", "expected_error"), + [ + (InvokeFrom.DEBUGGER, None, "Workflow not initialized"), + (InvokeFrom.WEB_APP, None, "Workflow not published"), + (InvokeFrom.DEBUGGER, SimpleNamespace(id="wf-1"), None), + ], +) +def test_get_workflow(mocker, invoke_from, workflow, expected_error) -> None: + rag_pipeline_service_cls = mocker.patch("services.rag_pipeline.pipeline_generate_service.RagPipelineService") + rag_pipeline_service = rag_pipeline_service_cls.return_value + rag_pipeline_service.get_draft_workflow.return_value = workflow + rag_pipeline_service.get_published_workflow.return_value = workflow + + pipeline = cast(Pipeline, SimpleNamespace(id="pipeline-1")) + + if expected_error: + with pytest.raises(ValueError, match=expected_error): + PipelineGenerateService._get_workflow(pipeline, invoke_from) + else: + result = PipelineGenerateService._get_workflow(pipeline, invoke_from) + assert result == workflow + + +def test_generate_updates_document_status_and_returns_event_stream(mocker) -> None: + pipeline = cast(Pipeline, SimpleNamespace(id="pipeline-1")) + user = cast(Account | EndUser, SimpleNamespace(id="user-1")) + args = {"original_document_id": "doc-1", "query": "hello"} + + mocker.patch.object(PipelineGenerateService, "_get_workflow", return_value=SimpleNamespace(id="wf-1")) + update_status_mock = mocker.patch.object(PipelineGenerateService, "update_document_status") + + generator_cls = mocker.patch("services.rag_pipeline.pipeline_generate_service.PipelineGenerator") + generator_instance = generator_cls.return_value + generator_instance.generate.return_value = "raw-events" + generator_cls.convert_to_event_stream.return_value = "stream-events" + + result = PipelineGenerateService.generate( + pipeline=pipeline, + user=user, + args=args, + invoke_from=InvokeFrom.WEB_APP, + streaming=True, + ) + + assert result == "stream-events" + update_status_mock.assert_called_once_with("doc-1") + + +def test_update_document_status_updates_existing_document(mocker) -> None: + document = SimpleNamespace(indexing_status="completed") + + session_mock = mocker.Mock() + session_mock.get.return_value = document + add_mock = session_mock.add + commit_mock = session_mock.commit + mocker.patch( + "services.rag_pipeline.pipeline_generate_service.db", + new=SimpleNamespace(session=session_mock), + ) + + PipelineGenerateService.update_document_status("doc-1") + + assert document.indexing_status == "waiting" + add_mock.assert_called_once_with(document) + commit_mock.assert_called_once() + + +def test_update_document_status_skips_when_document_missing(mocker) -> None: + session_mock = mocker.Mock() + session_mock.get.return_value = None + add_mock = session_mock.add + commit_mock = session_mock.commit + mocker.patch( + "services.rag_pipeline.pipeline_generate_service.db", + new=SimpleNamespace(session=session_mock), + ) + + PipelineGenerateService.update_document_status("missing") + + add_mock.assert_not_called() + commit_mock.assert_not_called() + + +# --- generate_single_iteration --- + + +def test_generate_single_iteration_delegates(mocker) -> None: + mocker.patch.object(PipelineGenerateService, "_get_workflow", return_value=SimpleNamespace(id="wf-1")) + + generator_cls = mocker.patch("services.rag_pipeline.pipeline_generate_service.PipelineGenerator") + generator_instance = generator_cls.return_value + generator_instance.single_iteration_generate.return_value = "raw-iter" + generator_cls.convert_to_event_stream.return_value = "stream-iter" + + pipeline = cast(Pipeline, SimpleNamespace(id="p1")) + user = cast(Account, SimpleNamespace(id="u1")) + + result = PipelineGenerateService.generate_single_iteration(pipeline, user, "node-1", {"key": "val"}) + + assert result == "stream-iter" + generator_instance.single_iteration_generate.assert_called_once() + + +# --- generate_single_loop --- + + +def test_generate_single_loop_delegates(mocker) -> None: + mocker.patch.object(PipelineGenerateService, "_get_workflow", return_value=SimpleNamespace(id="wf-1")) + + generator_cls = mocker.patch("services.rag_pipeline.pipeline_generate_service.PipelineGenerator") + generator_instance = generator_cls.return_value + generator_instance.single_loop_generate.return_value = "raw-loop" + generator_cls.convert_to_event_stream.return_value = "stream-loop" + + pipeline = cast(Pipeline, SimpleNamespace(id="p1")) + user = cast(Account, SimpleNamespace(id="u1")) + + result = PipelineGenerateService.generate_single_loop(pipeline, user, "node-1", {"key": "val"}) + + assert result == "stream-loop" + generator_instance.single_loop_generate.assert_called_once() diff --git a/api/tests/unit_tests/services/rag_pipeline/test_pipeline_service_api_entities.py b/api/tests/unit_tests/services/rag_pipeline/test_pipeline_service_api_entities.py new file mode 100644 index 00000000000..30dda6127af --- /dev/null +++ b/api/tests/unit_tests/services/rag_pipeline/test_pipeline_service_api_entities.py @@ -0,0 +1,34 @@ +import pytest +from pydantic import ValidationError + +from services.rag_pipeline.entity.pipeline_service_api_entities import ( + DatasourceNodeRunApiEntity, + PipelineRunApiEntity, +) + + +def test_datasource_node_run_api_entity_valid_payload() -> None: + entity = DatasourceNodeRunApiEntity( + pipeline_id="pipeline-1", + node_id="node-1", + inputs={"q": "hello"}, + datasource_type="local_file", + credential_id="cred-1", + is_published=True, + ) + + assert entity.pipeline_id == "pipeline-1" + assert entity.credential_id == "cred-1" + + +def test_pipeline_run_api_entity_requires_start_node_id() -> None: + with pytest.raises(ValidationError): + PipelineRunApiEntity.model_validate( + { + "inputs": {"q": "hello"}, + "datasource_type": "local_file", + "datasource_info_list": [{"id": "ds-1"}], + "is_published": True, + "response_mode": "streaming", + } + ) diff --git a/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_dsl_service.py b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_dsl_service.py new file mode 100644 index 00000000000..f4fdac5f9f6 --- /dev/null +++ b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_dsl_service.py @@ -0,0 +1,1325 @@ +from types import SimpleNamespace +from typing import cast +from unittest.mock import MagicMock, Mock + +import pytest +import yaml +from graphon.enums import BuiltinNodeTypes +from sqlalchemy.orm import Session + +from core.workflow.nodes.knowledge_index import KNOWLEDGE_INDEX_NODE_TYPE +from services.entities.knowledge_entities.rag_pipeline_entities import IconInfo, RagPipelineDatasetCreateEntity +from services.rag_pipeline.rag_pipeline_dsl_service import ( + ImportStatus, + RagPipelineDslService, + _check_version_compatibility, +) + + +@pytest.mark.parametrize( + ("imported_version", "expected_status"), + [ + ("invalid", ImportStatus.FAILED), + ("1.0.0", ImportStatus.PENDING), + ("0.0.9", ImportStatus.COMPLETED_WITH_WARNINGS), + ("0.1.0", ImportStatus.COMPLETED), + ], +) +def test_check_version_compatibility(imported_version: str, expected_status: ImportStatus) -> None: + assert _check_version_compatibility(imported_version) == expected_status + + +def test_encrypt_decrypt_dataset_id_roundtrip() -> None: + service = RagPipelineDslService(session=Mock()) + + encrypted = service.encrypt_dataset_id("dataset-1", "tenant-1") + decrypted = service.decrypt_dataset_id(encrypted, "tenant-1") + + assert decrypted == "dataset-1" + + +def test_decrypt_dataset_id_returns_none_for_invalid_payload() -> None: + service = RagPipelineDslService(session=Mock()) + + result = service.decrypt_dataset_id("not-base64", "tenant-1") + + assert result is None + + +def test_get_leaked_dependencies_returns_empty_list_for_empty_input() -> None: + result = RagPipelineDslService.get_leaked_dependencies("tenant-1", []) + + assert result == [] + + +def test_get_leaked_dependencies_delegates_to_analysis_service(mocker) -> None: + expected = [Mock()] + get_leaked_mock = mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.DependenciesAnalysisService.get_leaked_dependencies", + return_value=expected, + ) + + dependency = Mock() + result = RagPipelineDslService.get_leaked_dependencies("tenant-1", [dependency]) + + assert result == expected + get_leaked_mock.assert_called_once_with(tenant_id="tenant-1", dependencies=[dependency]) + + +# --- check_dependencies --- + + +def test_check_dependencies_returns_empty_when_no_redis_data(mocker) -> None: + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.redis_client.get", + return_value=None, + ) + service = RagPipelineDslService(session=Mock()) + pipeline = Mock(id="p1", tenant_id="t1") + + result = service.check_dependencies(pipeline=pipeline) + + assert result.leaked_dependencies == [] + + +def test_check_dependencies_returns_leaked_deps_from_redis(mocker) -> None: + from core.plugin.entities.plugin import PluginDependency + from services.rag_pipeline.rag_pipeline_dsl_service import CheckDependenciesPendingData + + dep = PluginDependency( + type=PluginDependency.Type.Marketplace, + value=PluginDependency.Marketplace(marketplace_plugin_unique_identifier="test/plugin:0.1.0"), + ) + pending_data = CheckDependenciesPendingData( + dependencies=[dep], + pipeline_id="p1", + ) + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.redis_client.get", + return_value=pending_data.model_dump_json(), + ) + leaked = [dep] + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.DependenciesAnalysisService.get_leaked_dependencies", + return_value=leaked, + ) + service = RagPipelineDslService(session=Mock()) + pipeline = Mock(id="p1", tenant_id="t1") + + result = service.check_dependencies(pipeline=pipeline) + + assert result.leaked_dependencies == leaked + + +# --- _extract_dependencies_from_model_config --- + + +def test_extract_dependencies_from_model_config_extracts_model(mocker) -> None: + analyze_mock = mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.DependenciesAnalysisService.analyze_model_provider_dependency", + return_value="langgenius/openai", + ) + config = {"model": {"provider": "openai"}} + + result = RagPipelineDslService._extract_dependencies_from_model_config(config) + + assert "langgenius/openai" in result + analyze_mock.assert_called_with("openai") + + +def test_extract_dependencies_from_model_config_extracts_tools(mocker) -> None: + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.DependenciesAnalysisService.analyze_model_provider_dependency", + return_value="x", + ) + analyze_tool_mock = mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.DependenciesAnalysisService.analyze_tool_dependency", + return_value="langgenius/google", + ) + config = { + "model": {"provider": "openai"}, + "agent_mode": {"tools": [{"provider_id": "google"}]}, + } + + result = RagPipelineDslService._extract_dependencies_from_model_config(config) + + assert "langgenius/google" in result + analyze_tool_mock.assert_called_with("google") + + +def test_extract_dependencies_from_model_config_empty_config() -> None: + result = RagPipelineDslService._extract_dependencies_from_model_config({}) + + assert result == [] + + +# --- _extract_dependencies_from_workflow_graph --- + + +def test_extract_dependencies_from_workflow_graph_ignores_unknown_types(mocker) -> None: + service = RagPipelineDslService(session=Mock()) + graph = {"nodes": [{"data": {"type": "some-unknown-type"}}]} + + result = service._extract_dependencies_from_workflow_graph(graph) + + assert result == [] + + +def test_extract_dependencies_from_workflow_graph_handles_empty_graph() -> None: + service = RagPipelineDslService(session=Mock()) + + result = service._extract_dependencies_from_workflow_graph({}) + + assert result == [] + + +def test_extract_dependencies_from_workflow_graph_handles_malformed_node(mocker) -> None: + service = RagPipelineDslService(session=Mock()) + # Node with TOOL type but invalid data should be caught by exception handler + from graphon.enums import BuiltinNodeTypes + + graph = {"nodes": [{"data": {"type": BuiltinNodeTypes.TOOL}}]} + + result = service._extract_dependencies_from_workflow_graph(graph) + + # Should not raise, error is caught internally + assert isinstance(result, list) + + +# --- export_rag_pipeline_dsl --- + + +def test_export_rag_pipeline_dsl_raises_when_dataset_missing() -> None: + pipeline = Mock() + pipeline.retrieve_dataset.return_value = None + + service = RagPipelineDslService(session=Mock()) + + with pytest.raises(ValueError, match="Missing dataset"): + service.export_rag_pipeline_dsl(pipeline=pipeline) + + +# --- import_rag_pipeline --- + + +def test_import_rag_pipeline_url_fetch_error(mocker) -> None: + mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.ssrf_proxy.get", side_effect=Exception("fetch failed")) + service = RagPipelineDslService(session=Mock()) + account = Mock(current_tenant_id="t1") + + result = service.import_rag_pipeline( + account=account, import_mode="yaml-url", yaml_url="https://example.com/dsl.yml" + ) + + assert result.status == ImportStatus.FAILED + assert "fetch failed" in result.error + + +def test_import_rag_pipeline_yaml_content_success(mocker) -> None: + yaml_content = """ +version: 0.1.0 +kind: rag_pipeline +rag_pipeline: + name: Test Pipeline +workflow: + graph: + nodes: + - data: + type: knowledge-index +""" + pipeline = Mock() + pipeline.name = "Test Pipeline" + pipeline.description = "desc" + pipeline.id = "p1" + pipeline.is_published = False + mocker.patch.object(RagPipelineDslService, "_create_or_update_pipeline", return_value=pipeline) + + config_mock = Mock() + config_mock.indexing_technique = "high_quality" + config_mock.embedding_model = "m" + config_mock.embedding_model_provider = "p" + config_mock.summary_index_setting = None + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.KnowledgeConfiguration.model_validate", + return_value=config_mock, + ) + + dataset_mock = Mock() + dataset_mock.id = "d1" + mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.Dataset", return_value=dataset_mock) + + session = cast(MagicMock, Mock()) + service = RagPipelineDslService(session=cast(Session, session)) + session.query.return_value.filter_by.return_value.all.return_value = [] + account = Mock(current_tenant_id="t1") + + result = service.import_rag_pipeline(account=account, import_mode="yaml-content", yaml_content=yaml_content) + + if result.status == ImportStatus.FAILED: + print(f"DEBUG: {result.error}") + assert result.status == ImportStatus.COMPLETED + + +def test_import_rag_pipeline_pending_version(mocker) -> None: + yaml_content = "version: 1.0.0\nkind: rag_pipeline\nrag_pipeline: {name: x}" + mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.redis_client.setex") + service = RagPipelineDslService(session=Mock()) + account = Mock(current_tenant_id="t1", id="u1") + + result = service.import_rag_pipeline(account=account, import_mode="yaml-content", yaml_content=yaml_content) + + assert result.status == ImportStatus.PENDING + assert result.imported_dsl_version == "1.0.0" + + +# --- confirm_import --- + + +def test_confirm_import_success(mocker) -> None: + from services.rag_pipeline.rag_pipeline_dsl_service import RagPipelinePendingData + + yaml_content = """ +version: 0.1.0 +kind: rag_pipeline +rag_pipeline: + name: Test Pipeline +workflow: + graph: + nodes: + - data: + type: knowledge-index +""" + pending = RagPipelinePendingData(import_mode="yaml-content", yaml_content=yaml_content, pipeline_id="p1") + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.redis_client.get", + return_value=pending.model_dump_json(), + ) + mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.redis_client.delete") + + pipeline = Mock() + pipeline.id = "p1" + pipeline.name = "Test Pipeline" + pipeline.description = "desc" + pipeline.retrieve_dataset.return_value = None + + mocker.patch.object(RagPipelineDslService, "_create_or_update_pipeline", return_value=pipeline) + + config_mock = Mock() + config_mock.indexing_technique = "high_quality" + config_mock.embedding_model = "m" + config_mock.embedding_model_provider = "p" + config_mock.chunk_structure = "text_model" + config_mock.retrieval_model.model_dump.return_value = {} + config_mock.summary_index_setting = None + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.KnowledgeConfiguration.model_validate", + return_value=config_mock, + ) + + dataset_mock = Mock() + dataset_mock.id = "d1" + mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.Dataset", return_value=dataset_mock) + mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.DatasetCollectionBinding", return_value=Mock(id="b1")) + + service = RagPipelineDslService(session=Mock()) + # Mocking self._session.scalar for the pipeline lookup + service._session.scalar.return_value = pipeline + + account = Mock() + account.id = "u1" + account.current_tenant_id = "t1" + + result = service.confirm_import(account=account, import_id="imp-1") + + assert result.status == ImportStatus.COMPLETED + assert result.pipeline_id == "p1" + assert result.dataset_id == "d1" + + +# --- _extract_dependencies_from_workflow_graph all types --- + + +@pytest.mark.parametrize( + "node_type", + [ + BuiltinNodeTypes.TOOL, + BuiltinNodeTypes.LLM, + BuiltinNodeTypes.KNOWLEDGE_RETRIEVAL, + BuiltinNodeTypes.PARAMETER_EXTRACTOR, + BuiltinNodeTypes.QUESTION_CLASSIFIER, + ], +) +def test_extract_dependencies_from_workflow_graph_types(mocker, node_type) -> None: + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.DependenciesAnalysisService.analyze_tool_dependency", + return_value="t1", + ) + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.DependenciesAnalysisService.analyze_model_provider_dependency", + return_value="m1", + ) + + # Mock all potential node data classes + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.ToolNodeData.model_validate", + return_value=Mock(provider_id="p1"), + ) + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.LLMNodeData.model_validate", + return_value=Mock(model=Mock(provider="p1")), + ) + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.KnowledgeRetrievalNodeData.model_validate", + return_value=Mock( + retrieval_mode="single", + single_retrieval_config=Mock(model=Mock(provider="p1")), + ), + ) + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.ParameterExtractorNodeData.model_validate", + return_value=Mock(model=Mock(provider="p1")), + ) + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.QuestionClassifierNodeData.model_validate", + return_value=Mock(model=Mock(provider="p1")), + ) + + service = RagPipelineDslService(session=Mock()) + graph = {"nodes": [{"data": {"type": node_type}}]} + + result = service._extract_dependencies_from_workflow_graph(graph) + + assert len(result) > 0 + + +# --- _create_or_update_pipeline --- + + +def test_create_or_update_pipeline_create_new(mocker) -> None: + session = cast(MagicMock, Mock()) + service = RagPipelineDslService(session=cast(Session, session)) + account = Mock(current_tenant_id="t1", id="u1") + data = { + "rag_pipeline": {"name": "New", "description": "desc"}, + "workflow": {"graph": {"nodes": []}}, + } + + mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.current_user", SimpleNamespace(id="u1")) + mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.Workflow", return_value=Mock()) + pipeline_cls = mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.Pipeline") + pipeline_instance = pipeline_cls.return_value + pipeline_instance.tenant_id = "t1" + pipeline_instance.id = "p1" + pipeline_instance.name = "P" + pipeline_instance.is_published = False + + result = service._create_or_update_pipeline(pipeline=None, data=data, account=account, dependencies=[]) + + assert result == pipeline_instance + session.add.assert_called() + + +# --- export_rag_pipeline_dsl comprehensive --- + + +def test_export_rag_pipeline_dsl_with_workflow(mocker) -> None: + session = cast(MagicMock, Mock()) + service = RagPipelineDslService(session=cast(Session, session)) + pipeline = Mock() + pipeline.id = "p1" + pipeline.tenant_id = "t1" + pipeline.name = "P" + pipeline.description = "d" + + dataset = Mock() + dataset.id = "d1" + dataset.name = "D" + dataset.chunk_structure = "text_model" + dataset.doc_form = "text_model" + dataset.icon_info = {"icon": "i"} + pipeline.retrieve_dataset.return_value = dataset + + workflow = Mock() + workflow.app_id = "p1" + workflow.graph_dict = {"nodes": []} + workflow.environment_variables = [] + workflow.conversation_variables = [] + workflow.rag_pipeline_variables = [] + workflow.to_dict.return_value = {"graph": {"nodes": []}} + + # Mocking single .where() call + session.query.return_value.where.return_value.first.return_value = workflow + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.DependenciesAnalysisService.generate_dependencies", + return_value=[], + ) + + result_yaml = service.export_rag_pipeline_dsl(pipeline=pipeline) + data = yaml.safe_load(result_yaml) + + assert data["kind"] == "rag_pipeline" + assert data["rag_pipeline"]["name"] == "D" + assert "workflow" in data + + +# --- _extract_dependencies_from_workflow_graph more types --- + + +def test_extract_dependencies_from_workflow_graph_datasource(mocker) -> None: + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.DatasourceNodeData.model_validate", + return_value=Mock(provider_type="online", plugin_id="ds1"), + ) + service = RagPipelineDslService(session=Mock()) + graph = {"nodes": [{"data": {"type": BuiltinNodeTypes.DATASOURCE}}]} + + result = service._extract_dependencies_from_workflow_graph(graph) + + assert "ds1" in result + + +def test_import_rag_pipeline_raises_for_invalid_mode() -> None: + service = RagPipelineDslService(session=Mock()) + account = Mock(current_tenant_id="t1") + + with pytest.raises(ValueError, match="Invalid import_mode"): + service.import_rag_pipeline(account=account, import_mode="invalid-mode") + + +def test_import_rag_pipeline_yaml_url_requires_url() -> None: + service = RagPipelineDslService(session=Mock()) + account = Mock(current_tenant_id="t1") + + result = service.import_rag_pipeline(account=account, import_mode="yaml-url", yaml_url=None) + + assert result.status == ImportStatus.FAILED + assert "yaml_url is required" in result.error + + +def test_import_rag_pipeline_yaml_content_requires_content() -> None: + service = RagPipelineDslService(session=Mock()) + account = Mock(current_tenant_id="t1") + + result = service.import_rag_pipeline(account=account, import_mode="yaml-content", yaml_content=None) + + assert result.status == ImportStatus.FAILED + assert "yaml_content is required" in result.error + + +def test_import_rag_pipeline_yaml_content_requires_mapping() -> None: + service = RagPipelineDslService(session=Mock()) + account = Mock(current_tenant_id="t1") + + result = service.import_rag_pipeline(account=account, import_mode="yaml-content", yaml_content="- one\n- two") + + assert result.status == ImportStatus.FAILED + assert "content must be a mapping" in result.error + + +def test_confirm_import_returns_failed_when_pending_data_is_invalid_type(mocker) -> None: + mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.redis_client.get", return_value=object()) + service = RagPipelineDslService(session=Mock()) + account = Mock(current_tenant_id="t1") + + result = service.confirm_import(import_id="imp-1", account=account) + + assert result.status == ImportStatus.FAILED + assert "Invalid import information" in result.error + + +def test_append_workflow_export_data_filters_credentials(mocker) -> None: + session = cast(MagicMock, Mock()) + service = RagPipelineDslService(session=cast(Session, session)) + workflow = Mock() + workflow.graph_dict = {"nodes": []} + workflow.to_dict.return_value = { + "graph": { + "nodes": [ + { + "data": { + "type": BuiltinNodeTypes.TOOL, + "credential_id": "secret", + } + }, + { + "data": { + "type": BuiltinNodeTypes.AGENT, + "agent_parameters": {"tools": {"value": [{"credential_id": "secret-agent"}]}}, + } + }, + ] + } + } + session.query.return_value.where.return_value.first.return_value = workflow + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.DependenciesAnalysisService.generate_dependencies", + return_value=[], + ) + export_data: dict = {} + pipeline = Mock(id="p1", tenant_id="t1") + + service._append_workflow_export_data(export_data=export_data, pipeline=pipeline, include_secret=False) + + nodes = export_data["workflow"]["graph"]["nodes"] + assert "credential_id" not in nodes[0]["data"] + assert "credential_id" not in nodes[1]["data"]["agent_parameters"]["tools"]["value"][0] + + +def test_create_rag_pipeline_dataset_raises_when_name_conflicts(mocker) -> None: + session = cast(MagicMock, Mock()) + service = RagPipelineDslService(session=cast(Session, session)) + session.query.return_value.filter_by.return_value.first.return_value = Mock() + create_entity = RagPipelineDatasetCreateEntity( + name="Existing Name", + description="", + icon_info=IconInfo(icon="book"), + permission="only_me", + yaml_content="x", + ) + + with pytest.raises(ValueError, match="already exists"): + service.create_rag_pipeline_dataset("tenant-1", create_entity) + + +def test_create_rag_pipeline_dataset_generates_name_when_missing(mocker) -> None: + session = cast(MagicMock, Mock()) + service = RagPipelineDslService(session=cast(Session, session)) + session.query.return_value.filter_by.return_value.first.return_value = None + session.query.return_value.filter_by.return_value.all.return_value = [Mock(name="Untitled")] + mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.generate_incremental_name", return_value="Untitled 2") + mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.current_user", Mock(id="u1", current_tenant_id="t1")) + mocker.patch.object( + service, + "import_rag_pipeline", + return_value=SimpleNamespace( + id="imp-1", + dataset_id="d1", + pipeline_id="p1", + status=ImportStatus.COMPLETED, + imported_dsl_version="0.1.0", + current_dsl_version="0.1.0", + error="", + ), + ) + create_entity = RagPipelineDatasetCreateEntity( + name="", + description="", + icon_info=IconInfo(icon="book"), + permission="only_me", + yaml_content="x", + ) + + result = service.create_rag_pipeline_dataset("tenant-1", create_entity) + + assert create_entity.name == "Untitled 2" + assert result["status"] == ImportStatus.COMPLETED + + +def test_append_workflow_export_data_encrypts_knowledge_retrieval_dataset_ids(mocker) -> None: + session = cast(MagicMock, Mock()) + service = RagPipelineDslService(session=cast(Session, session)) + workflow = Mock() + workflow.graph_dict = {"nodes": []} + workflow.to_dict.return_value = { + "graph": { + "nodes": [ + { + "data": { + "type": BuiltinNodeTypes.KNOWLEDGE_RETRIEVAL, + "dataset_ids": ["d1", "d2"], + } + } + ] + } + } + session.query.return_value.where.return_value.first.return_value = workflow + mocker.patch.object(service, "encrypt_dataset_id", side_effect=lambda dataset_id, tenant_id: f"enc-{dataset_id}") + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.DependenciesAnalysisService.generate_dependencies", + return_value=[], + ) + export_data: dict = {} + pipeline = Mock(id="p1", tenant_id="t1") + + service._append_workflow_export_data(export_data=export_data, pipeline=pipeline, include_secret=False) + + ids = export_data["workflow"]["graph"]["nodes"][0]["data"]["dataset_ids"] + assert ids == ["enc-d1", "enc-d2"] + + +def test_confirm_import_updates_existing_dataset(mocker) -> None: + from services.rag_pipeline.rag_pipeline_dsl_service import RagPipelinePendingData + + yaml_content = ( + "version: 0.1.0\n" + "kind: rag_pipeline\n" + "rag_pipeline: {name: x}\n" + "workflow: {graph: {nodes: [{data: {type: knowledge-index}}]}}" + ) + pending = RagPipelinePendingData(import_mode="yaml-content", yaml_content=yaml_content, pipeline_id="p1") + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.redis_client.get", + return_value=pending.model_dump_json(), + ) + mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.redis_client.delete") + pipeline = Mock(id="p1", name="P", description="D") + dataset = Mock(id="d1") + pipeline.retrieve_dataset.return_value = dataset + mocker.patch.object(RagPipelineDslService, "_create_or_update_pipeline", return_value=pipeline) + config_mock = Mock() + config_mock.indexing_technique = "economy" + config_mock.keyword_number = 3 + config_mock.retrieval_model.model_dump.return_value = {"top_k": 3} + config_mock.chunk_structure = "text_model" + config_mock.summary_index_setting = None + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.KnowledgeConfiguration.model_validate", + return_value=config_mock, + ) + service = RagPipelineDslService(session=Mock()) + service._session.scalar.return_value = pipeline + account = Mock(id="u1", current_tenant_id="t1") + + result = service.confirm_import(import_id="imp-1", account=account) + + assert result.status == ImportStatus.COMPLETED + assert dataset.indexing_technique == "economy" + + +def test_import_rag_pipeline_yaml_url_handles_empty_content_after_github_rewrite(mocker) -> None: + response = Mock() + response.raise_for_status.return_value = None + response.content = b"" + get_mock = mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.ssrf_proxy.get", return_value=response) + service = RagPipelineDslService(session=Mock()) + account = Mock(current_tenant_id="t1") + + result = service.import_rag_pipeline( + account=account, + import_mode="yaml-url", + yaml_url="https://github.com/langgenius/dify/blob/main/pipeline.yml", + ) + + assert result.status == ImportStatus.FAILED + assert "Empty content from url" in result.error + called_url = get_mock.call_args.args[0] + assert "raw.githubusercontent.com" in called_url + + +def test_create_or_update_pipeline_decrypts_knowledge_retrieval_dataset_ids(mocker) -> None: + session = cast(MagicMock, Mock()) + service = RagPipelineDslService(session=cast(Session, session)) + account = Mock(id="u1", current_tenant_id="t1") + pipeline = Mock(id="p1", tenant_id="t1", name="N", description="D") + data = { + "rag_pipeline": {"name": "N2", "description": "D2"}, + "workflow": { + "graph": { + "nodes": [ + { + "data": { + "type": BuiltinNodeTypes.KNOWLEDGE_RETRIEVAL, + "dataset_ids": ["enc-1", "enc-2"], + } + } + ] + } + }, + } + draft_workflow = Mock(id="wf1") + session.query.return_value.where.return_value.first.return_value = draft_workflow + mocker.patch.object(service, "decrypt_dataset_id", side_effect=["d1", None]) + + result = service._create_or_update_pipeline(pipeline=pipeline, data=data, account=account) + + assert result is pipeline + assert data["workflow"]["graph"]["nodes"][0]["data"]["dataset_ids"] == ["d1"] + assert draft_workflow.graph is not None + + +def test_create_or_update_pipeline_creates_draft_when_missing(mocker) -> None: + session = cast(MagicMock, Mock()) + service = RagPipelineDslService(session=cast(Session, session)) + account = Mock(id="u1", current_tenant_id="t1") + pipeline = Mock(id="p1", tenant_id="t1", name="N", description="D") + data = {"rag_pipeline": {"name": "N2", "description": "D2"}, "workflow": {"graph": {"nodes": []}}} + session.query.return_value.where.return_value.first.return_value = None + workflow_cls = mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.Workflow") + workflow_cls.return_value.id = "wf-new" + + service._create_or_update_pipeline(pipeline=pipeline, data=data, account=account) + + assert pipeline.workflow_id == "wf-new" + + +def test_import_rag_pipeline_url_size_exceeds_limit(mocker) -> None: + response = Mock() + response.raise_for_status.return_value = None + response.content = b"x" * (10 * 1024 * 1024 + 1) + mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.ssrf_proxy.get", return_value=response) + service = RagPipelineDslService(session=Mock()) + account = Mock(current_tenant_id="t1") + + result = service.import_rag_pipeline( + account=account, + import_mode="yaml-url", + yaml_url="https://example.com/pipeline.yaml", + ) + + assert result.status == ImportStatus.FAILED + assert "10MB" in result.error + + +def test_import_rag_pipeline_fails_when_rag_pipeline_data_missing() -> None: + service = RagPipelineDslService(session=Mock()) + account = Mock(current_tenant_id="t1") + result = service.import_rag_pipeline( + account=account, + import_mode="yaml-content", + yaml_content="version: 0.1.0\nkind: rag_pipeline\nworkflow: {}", + ) + + assert result.status == ImportStatus.FAILED + assert "Missing rag_pipeline data" in result.error + + +def test_import_rag_pipeline_fails_when_pipeline_id_not_found() -> None: + session = cast(MagicMock, Mock()) + session.scalar.return_value = None + service = RagPipelineDslService(session=cast(Session, session)) + account = Mock(current_tenant_id="t1") + + result = service.import_rag_pipeline( + account=account, + import_mode="yaml-content", + yaml_content="version: 0.1.0\nkind: rag_pipeline\nrag_pipeline: {name: x}\nworkflow: {}", + pipeline_id="missing-pipeline", + ) + + assert result.status == ImportStatus.FAILED + assert "Pipeline not found" in result.error + + +def test_import_rag_pipeline_fails_for_non_string_version_type() -> None: + service = RagPipelineDslService(session=Mock()) + account = Mock(current_tenant_id="t1") + + result = service.import_rag_pipeline( + account=account, + import_mode="yaml-content", + yaml_content="version: 1\nkind: rag_pipeline\nrag_pipeline: {name: x}\nworkflow: {}", + ) + + assert result.status == ImportStatus.FAILED + assert "Invalid version type" in result.error + + +def test_append_workflow_export_data_raises_when_draft_workflow_missing() -> None: + session = cast(MagicMock, Mock()) + service = RagPipelineDslService(session=cast(Session, session)) + session.query.return_value.where.return_value.first.return_value = None + + with pytest.raises(ValueError, match="Missing draft workflow configuration"): + service._append_workflow_export_data(export_data={}, pipeline=Mock(tenant_id="t1"), include_secret=False) + + +def test_append_workflow_export_data_keeps_secret_fields_when_include_secret_true(mocker) -> None: + session = cast(MagicMock, Mock()) + service = RagPipelineDslService(session=cast(Session, session)) + workflow = Mock() + workflow.graph_dict = {"nodes": []} + workflow.to_dict.return_value = { + "graph": { + "nodes": [ + {"data": {"type": BuiltinNodeTypes.TOOL, "credential_id": "tool-secret"}}, + { + "data": { + "type": BuiltinNodeTypes.AGENT, + "agent_parameters": {"tools": {"value": [{"credential_id": "agent-secret"}]}}, + } + }, + ] + } + } + session.query.return_value.where.return_value.first.return_value = workflow + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.DependenciesAnalysisService.generate_dependencies", + return_value=[], + ) + + export_data: dict[str, object] = {} + service._append_workflow_export_data(export_data=export_data, pipeline=Mock(tenant_id="t1"), include_secret=True) + + workflow_data = cast(dict[str, object], export_data["workflow"]) + graph = cast(dict[str, object], workflow_data["graph"]) + nodes = cast(list[dict[str, object]], graph["nodes"]) + node0_data = cast(dict[str, object], nodes[0]["data"]) + node1_data = cast(dict[str, object], nodes[1]["data"]) + agent_parameters = cast(dict[str, object], node1_data["agent_parameters"]) + tools = cast(dict[str, object], agent_parameters["tools"]) + tool_values = cast(list[dict[str, object]], tools["value"]) + assert node0_data["credential_id"] == "tool-secret" + assert tool_values[0]["credential_id"] == "agent-secret" + + +def test_extract_dependencies_from_workflow_graph_skips_local_file_datasource(mocker) -> None: + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.DatasourceNodeData.model_validate", + return_value=Mock(provider_type="local_file", plugin_id="plugin-x"), + ) + service = RagPipelineDslService(session=Mock()) + + result = service._extract_dependencies_from_workflow_graph( + {"nodes": [{"data": {"type": BuiltinNodeTypes.DATASOURCE}}]} + ) + + assert result == [] + + +def test_extract_dependencies_from_workflow_graph_knowledge_index_reranking(mocker) -> None: + analyze = mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.DependenciesAnalysisService.analyze_model_provider_dependency", + side_effect=lambda provider: f"dep:{provider}", + ) + knowledge = Mock() + knowledge.indexing_technique = "high_quality" + knowledge.embedding_model_provider = "embed-provider" + knowledge.retrieval_model.reranking_mode = "reranking_model" + knowledge.retrieval_model.reranking_enable = True + knowledge.retrieval_model.reranking_model.reranking_provider_name = "rerank-provider" + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.KnowledgeConfiguration.model_validate", + return_value=knowledge, + ) + service = RagPipelineDslService(session=Mock()) + + result = service._extract_dependencies_from_workflow_graph( + {"nodes": [{"data": {"type": KNOWLEDGE_INDEX_NODE_TYPE}}]} + ) + + assert result == ["dep:embed-provider", "dep:rerank-provider"] + assert analyze.call_count == 2 + + +def test_extract_dependencies_from_workflow_graph_multiple_retrieval_weighted_score(mocker) -> None: + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.DependenciesAnalysisService.analyze_model_provider_dependency", + return_value="dep:weighted", + ) + retrieval = Mock() + retrieval.retrieval_mode = "multiple" + retrieval.multiple_retrieval_config.reranking_mode = "weighted_score" + retrieval.multiple_retrieval_config.weights.vector_setting.embedding_provider_name = "emb-provider" + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.KnowledgeRetrievalNodeData.model_validate", + return_value=retrieval, + ) + service = RagPipelineDslService(session=Mock()) + + result = service._extract_dependencies_from_workflow_graph( + {"nodes": [{"data": {"type": BuiltinNodeTypes.KNOWLEDGE_RETRIEVAL}}]} + ) + + assert result == ["dep:weighted"] + + +def test_extract_dependencies_from_workflow_graph_multiple_retrieval_reranking_model(mocker) -> None: + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.DependenciesAnalysisService.analyze_model_provider_dependency", + return_value="dep:rerank", + ) + retrieval = Mock() + retrieval.retrieval_mode = "multiple" + retrieval.multiple_retrieval_config.reranking_mode = "reranking_model" + retrieval.multiple_retrieval_config.reranking_model.provider = "rerank-provider" + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.KnowledgeRetrievalNodeData.model_validate", + return_value=retrieval, + ) + service = RagPipelineDslService(session=Mock()) + + result = service._extract_dependencies_from_workflow_graph( + {"nodes": [{"data": {"type": BuiltinNodeTypes.KNOWLEDGE_RETRIEVAL}}]} + ) + + assert result == ["dep:rerank"] + + +def test_extract_dependencies_from_model_config_includes_dataset_reranking_and_tools(mocker) -> None: + model_analyze = mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.DependenciesAnalysisService.analyze_model_provider_dependency", + side_effect=["dep:model", "dep:rerank"], + ) + tool_analyze = mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.DependenciesAnalysisService.analyze_tool_dependency", + return_value="dep:tool", + ) + config = { + "model": {"provider": "openai"}, + "dataset_configs": { + "datasets": { + "datasets": [ + { + "reranking_model": { + "reranking_provider_name": {"provider": "cohere"}, + } + } + ] + } + }, + "agent_mode": {"tools": [{"provider_id": "google"}]}, + } + + deps = RagPipelineDslService._extract_dependencies_from_model_config(config) + + assert deps == ["dep:model", "dep:rerank", "dep:tool"] + assert model_analyze.call_count == 2 + tool_analyze.assert_called_once_with("google") + + +def test_check_version_compatibility_hits_major_older_branch(mocker) -> None: + mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.CURRENT_DSL_VERSION", "1.0.0") + + status = _check_version_compatibility("0.9.0") + + assert status == ImportStatus.PENDING + + +def test_import_rag_pipeline_sets_default_version_and_kind(mocker) -> None: + session = cast(MagicMock, Mock()) + service = RagPipelineDslService(session=cast(Session, session)) + account = Mock(current_tenant_id="t1") + pipeline = Mock(id="p1", name="P", description="D", is_published=False) + mocker.patch.object(service, "_create_or_update_pipeline", return_value=pipeline) + config = Mock() + config.indexing_technique = "economy" + config.keyword_number = 2 + config.retrieval_model.model_dump.return_value = {} + config.summary_index_setting = None + config.chunk_structure = "text_model" + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.KnowledgeConfiguration.model_validate", + return_value=config, + ) + dataset = Mock(id="d1") + mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.Dataset", return_value=dataset) + session.query.return_value.filter_by.return_value.all.return_value = [] + mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.generate_incremental_name", return_value="P") + + result = service.import_rag_pipeline( + account=account, + import_mode="yaml-content", + yaml_content="rag_pipeline: {name: x}\nworkflow: {graph: {nodes: [{data: {type: knowledge-index}}]}}", + ) + + assert result.status == ImportStatus.COMPLETED + assert result.imported_dsl_version == "0.1.0" + + +def test_import_rag_pipeline_creates_pending_for_dependencies(mocker) -> None: + session = cast(MagicMock, Mock()) + service = RagPipelineDslService(session=cast(Session, session)) + account = Mock(current_tenant_id="t1") + setex = mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.redis_client.setex") + yaml_content = """ +version: 1.0.0 +kind: rag_pipeline +rag_pipeline: {name: x} +dependencies: + - type: marketplace + value: + marketplace_plugin_unique_identifier: langgenius/example:0.1.0 +workflow: {graph: {nodes: []}} +""" + + result = service.import_rag_pipeline(account=account, import_mode="yaml-content", yaml_content=yaml_content) + + assert result.status == ImportStatus.PENDING + setex.assert_called_once() + + +def test_confirm_import_returns_failed_when_pending_pipeline_missing(mocker) -> None: + from services.rag_pipeline.rag_pipeline_dsl_service import RagPipelinePendingData + + pending = RagPipelinePendingData(import_mode="yaml-content", yaml_content="version: 0.1.0", pipeline_id="p1") + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.redis_client.get", return_value=pending.model_dump_json() + ) + session = cast(MagicMock, Mock()) + service = RagPipelineDslService(session=cast(Session, session)) + session.scalar.return_value = None + mocker.patch.object(RagPipelineDslService, "_create_or_update_pipeline", side_effect=ValueError("pipeline missing")) + + result = service.confirm_import(import_id="imp-1", account=Mock(current_tenant_id="t1")) + + assert result.status == ImportStatus.FAILED + + +def test_append_workflow_export_data_skips_empty_node_data(mocker) -> None: + session = cast(MagicMock, Mock()) + service = RagPipelineDslService(session=cast(Session, session)) + workflow = Mock() + workflow.graph_dict = {"nodes": []} + workflow.to_dict.return_value = {"graph": {"nodes": [{"data": {}}, {}]}} + session.query.return_value.where.return_value.first.return_value = workflow + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.DependenciesAnalysisService.generate_dependencies", + return_value=[], + ) + export_data = {} + + service._append_workflow_export_data(export_data=export_data, pipeline=Mock(tenant_id="t1"), include_secret=False) + + assert "workflow" in export_data + + +def test_extract_dependencies_from_workflow_graph_multiple_config_none(mocker) -> None: + retrieval = Mock() + retrieval.retrieval_mode = "multiple" + retrieval.multiple_retrieval_config = None + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.KnowledgeRetrievalNodeData.model_validate", + return_value=retrieval, + ) + service = RagPipelineDslService(session=Mock()) + + result = service._extract_dependencies_from_workflow_graph( + {"nodes": [{"data": {"type": BuiltinNodeTypes.KNOWLEDGE_RETRIEVAL}}]} + ) + + assert result == [] + + +def test_extract_dependencies_from_workflow_graph_single_config_none(mocker) -> None: + retrieval = Mock() + retrieval.retrieval_mode = "single" + retrieval.single_retrieval_config = None + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.KnowledgeRetrievalNodeData.model_validate", + return_value=retrieval, + ) + service = RagPipelineDslService(session=Mock()) + + result = service._extract_dependencies_from_workflow_graph( + {"nodes": [{"data": {"type": BuiltinNodeTypes.KNOWLEDGE_RETRIEVAL}}]} + ) + + assert result == [] + + +def test_create_or_update_pipeline_raises_when_workflow_missing() -> None: + service = RagPipelineDslService(session=Mock()) + account = Mock(current_tenant_id="t1", id="u1") + + with pytest.raises(ValueError, match="Missing workflow data for rag pipeline"): + service._create_or_update_pipeline(pipeline=None, data={"rag_pipeline": {"name": "x"}}, account=account) + + +def test_import_rag_pipeline_with_pipeline_id_uses_existing_dataset(mocker) -> None: + session = cast(MagicMock, Mock()) + service = RagPipelineDslService(session=cast(Session, session)) + existing_dataset = Mock(id="d1", chunk_structure="text_model") + existing_pipeline = Mock(id="p1", name="P", description="D", is_published=False) + existing_pipeline.retrieve_dataset.return_value = existing_dataset + session.scalar.return_value = existing_pipeline + mocker.patch.object(service, "_create_or_update_pipeline", return_value=existing_pipeline) + config = Mock() + config.indexing_technique = "economy" + config.keyword_number = 3 + config.chunk_structure = "text_model" + config.summary_index_setting = {"enabled": True} + config.retrieval_model.model_dump.return_value = {"top_k": 3} + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.KnowledgeConfiguration.model_validate", return_value=config + ) + + yaml_content = ( + "version: 0.1.0\n" + "kind: rag_pipeline\n" + "rag_pipeline: {name: x}\n" + "workflow: {graph: {nodes: [{data: {type: knowledge-index}}]}}" + ) + + result = service.import_rag_pipeline( + account=Mock(id="u1", current_tenant_id="t1"), + import_mode="yaml-content", + yaml_content=yaml_content, + pipeline_id="p1", + ) + + assert result.status == ImportStatus.COMPLETED + assert result.dataset_id == "d1" + + +def test_import_rag_pipeline_raises_for_chunk_structure_mismatch_on_published(mocker) -> None: + session = cast(MagicMock, Mock()) + service = RagPipelineDslService(session=cast(Session, session)) + existing_dataset = Mock(id="d1", chunk_structure="hierarchical_model") + existing_pipeline = Mock(id="p1", name="P", description="D", is_published=True) + existing_pipeline.retrieve_dataset.return_value = existing_dataset + session.scalar.return_value = existing_pipeline + mocker.patch.object(service, "_create_or_update_pipeline", return_value=existing_pipeline) + config = Mock() + config.chunk_structure = "text_model" + config.indexing_technique = "economy" + config.keyword_number = 3 + config.summary_index_setting = None + config.retrieval_model.model_dump.return_value = {} + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.KnowledgeConfiguration.model_validate", return_value=config + ) + + yaml_content = ( + "version: 0.1.0\n" + "kind: rag_pipeline\n" + "rag_pipeline: {name: x}\n" + "workflow: {graph: {nodes: [{data: {type: knowledge-index}}]}}" + ) + + result = service.import_rag_pipeline( + account=Mock(id="u1", current_tenant_id="t1"), + import_mode="yaml-content", + yaml_content=yaml_content, + pipeline_id="p1", + ) + + assert result.status == ImportStatus.FAILED + assert "Chunk structure is not compatible" in result.error + + +def test_import_rag_pipeline_fails_when_no_knowledge_index_node(mocker) -> None: + service = RagPipelineDslService(session=Mock()) + pipeline = Mock(id="p1", name="P", description="D", is_published=False) + mocker.patch.object(service, "_create_or_update_pipeline", return_value=pipeline) + + yaml_content = ( + "version: 0.1.0\n" + "kind: rag_pipeline\n" + "rag_pipeline: {name: x}\n" + "workflow: {graph: {nodes: [{data: {type: start}}]}}" + ) + + result = service.import_rag_pipeline( + account=Mock(id="u1", current_tenant_id="t1"), + import_mode="yaml-content", + yaml_content=yaml_content, + ) + + assert result.status == ImportStatus.FAILED + assert "Knowledge Index node" in result.error + + +def test_confirm_import_fails_when_no_knowledge_index_node(mocker) -> None: + from services.rag_pipeline.rag_pipeline_dsl_service import RagPipelinePendingData + + yaml_content = ( + "version: 0.1.0\n" + "kind: rag_pipeline\n" + "rag_pipeline: {name: x}\n" + "workflow: {graph: {nodes: [{data: {type: start}}]}}" + ) + + pending = RagPipelinePendingData( + import_mode="yaml-content", + yaml_content=yaml_content, + pipeline_id=None, + ) + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.redis_client.get", return_value=pending.model_dump_json() + ) + service = RagPipelineDslService(session=Mock()) + pipeline = Mock(id="p1", name="P", description="D") + pipeline.retrieve_dataset.return_value = None + mocker.patch.object(service, "_create_or_update_pipeline", return_value=pipeline) + + result = service.confirm_import(import_id="imp-1", account=Mock(id="u1", current_tenant_id="t1")) + + assert result.status == ImportStatus.FAILED + assert "Knowledge Index node" in result.error + + +def test_create_or_update_pipeline_saves_dependencies_to_redis(mocker) -> None: + from core.plugin.entities.plugin import PluginDependency + + session = cast(MagicMock, Mock()) + service = RagPipelineDslService(session=cast(Session, session)) + account = Mock(id="u1", current_tenant_id="t1") + mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.current_user", SimpleNamespace(id="u1")) + mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.Workflow", return_value=Mock(id="wf-1")) + pipeline_cls = mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.Pipeline") + pipeline = pipeline_cls.return_value + pipeline.tenant_id = "t1" + pipeline.id = "p1" + session.query.return_value.where.return_value.first.return_value = None + setex = mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.redis_client.setex") + dependency = PluginDependency( + type=PluginDependency.Type.Marketplace, + value=PluginDependency.Marketplace(marketplace_plugin_unique_identifier="langgenius/example:0.1.0"), + ) + + service._create_or_update_pipeline( + pipeline=None, + data={"rag_pipeline": {"name": "x"}, "workflow": {"graph": {"nodes": []}}}, + account=account, + dependencies=[dependency], + ) + + setex.assert_called_once() + + +def test_extract_dependencies_from_workflow_graph_knowledge_index_without_embedding_provider(mocker) -> None: + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.DependenciesAnalysisService.analyze_model_provider_dependency", + return_value="dep", + ) + knowledge = Mock() + knowledge.indexing_technique = "high_quality" + knowledge.embedding_model_provider = None + knowledge.retrieval_model.reranking_mode = "reranking_model" + knowledge.retrieval_model.reranking_enable = False + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.KnowledgeConfiguration.model_validate", return_value=knowledge + ) + service = RagPipelineDslService(session=Mock()) + + result = service._extract_dependencies_from_workflow_graph( + {"nodes": [{"data": {"type": KNOWLEDGE_INDEX_NODE_TYPE}}]} + ) + + assert result == [] + + +def test_extract_dependencies_from_workflow_graph_multiple_reranking_without_model(mocker) -> None: + retrieval = Mock() + retrieval.retrieval_mode = "multiple" + retrieval.multiple_retrieval_config.reranking_mode = "reranking_model" + retrieval.multiple_retrieval_config.reranking_model = None + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.KnowledgeRetrievalNodeData.model_validate", + return_value=retrieval, + ) + service = RagPipelineDslService(session=Mock()) + + result = service._extract_dependencies_from_workflow_graph( + {"nodes": [{"data": {"type": BuiltinNodeTypes.KNOWLEDGE_RETRIEVAL}}]} + ) + + assert result == [] + + +def test_extract_dependencies_from_workflow_graph_multiple_weighted_without_weights(mocker) -> None: + retrieval = Mock() + retrieval.retrieval_mode = "multiple" + retrieval.multiple_retrieval_config.reranking_mode = "weighted_score" + retrieval.multiple_retrieval_config.weights = None + mocker.patch( + "services.rag_pipeline.rag_pipeline_dsl_service.KnowledgeRetrievalNodeData.model_validate", + return_value=retrieval, + ) + service = RagPipelineDslService(session=Mock()) + + result = service._extract_dependencies_from_workflow_graph( + {"nodes": [{"data": {"type": BuiltinNodeTypes.KNOWLEDGE_RETRIEVAL}}]} + ) + + assert result == [] diff --git a/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_manage_service.py b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_manage_service.py new file mode 100644 index 00000000000..bd75e699dc8 --- /dev/null +++ b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_manage_service.py @@ -0,0 +1,24 @@ +from types import SimpleNamespace + +from services.rag_pipeline.rag_pipeline_manage_service import RagPipelineManageService + + +def test_list_rag_pipeline_datasources_marks_authorized(mocker) -> None: + datasource_1 = SimpleNamespace(provider="notion", plugin_id="plugin-1", is_authorized=False) + datasource_2 = SimpleNamespace(provider="jina", plugin_id="plugin-2", is_authorized=False) + + manager_cls = mocker.patch("services.rag_pipeline.rag_pipeline_manage_service.PluginDatasourceManager") + manager_cls.return_value.fetch_datasource_providers.return_value = [datasource_1, datasource_2] + + provider_cls = mocker.patch("services.rag_pipeline.rag_pipeline_manage_service.DatasourceProviderService") + provider_instance = provider_cls.return_value + provider_instance.get_datasource_credentials.side_effect = [ + {"access_token": "token"}, + None, + ] + + result = RagPipelineManageService.list_rag_pipeline_datasources("tenant-1") + + assert result == [datasource_1, datasource_2] + assert datasource_1.is_authorized is True + assert datasource_2.is_authorized is False diff --git a/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_service.py b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_service.py new file mode 100644 index 00000000000..cb3c2d742d9 --- /dev/null +++ b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_service.py @@ -0,0 +1,2318 @@ +import time +from types import SimpleNamespace + +import pytest +from sqlalchemy.orm import sessionmaker + +from services.entities.knowledge_entities.rag_pipeline_entities import IconInfo, PipelineTemplateInfoEntity +from services.rag_pipeline.rag_pipeline import RagPipelineService + + +@pytest.fixture +def rag_pipeline_service(mocker) -> RagPipelineService: + mocker.patch( + "services.rag_pipeline.rag_pipeline.DifyAPIRepositoryFactory.create_api_workflow_node_execution_repository", + return_value=MockRepo(), + ) + mocker.patch( + "services.rag_pipeline.rag_pipeline.DifyAPIRepositoryFactory.create_api_workflow_run_repository", + return_value=MockRepo(), + ) + return RagPipelineService(session_maker=sessionmaker()) + + +class MockRepo: + pass + + +def test_get_pipeline_templates_fallbacks_to_builtin_for_non_english_empty_result(mocker) -> None: + mocker.patch("services.rag_pipeline.rag_pipeline.dify_config.HOSTED_FETCH_PIPELINE_TEMPLATES_MODE", "remote") + + remote_retrieval = mocker.Mock() + remote_retrieval.get_pipeline_templates.return_value = {"pipeline_templates": []} + + factory_mock = mocker.patch("services.rag_pipeline.rag_pipeline.PipelineTemplateRetrievalFactory") + factory_mock.get_pipeline_template_factory.return_value.return_value = remote_retrieval + + builtin_retrieval = mocker.Mock() + builtin_retrieval.fetch_pipeline_templates_from_builtin.return_value = {"pipeline_templates": [{"id": "builtin-1"}]} + factory_mock.get_built_in_pipeline_template_retrieval.return_value = builtin_retrieval + + result = RagPipelineService.get_pipeline_templates(type="built-in", language="ja-JP") + + assert result == {"pipeline_templates": [{"id": "builtin-1"}]} + builtin_retrieval.fetch_pipeline_templates_from_builtin.assert_called_once_with("en-US") + + +def test_get_pipeline_templates_customized_mode_uses_customized_factory(mocker) -> None: + retrieval = mocker.Mock() + retrieval.get_pipeline_templates.return_value = {"pipeline_templates": [{"id": "custom-1"}]} + + factory_mock = mocker.patch("services.rag_pipeline.rag_pipeline.PipelineTemplateRetrievalFactory") + factory_mock.get_pipeline_template_factory.return_value.return_value = retrieval + + result = RagPipelineService.get_pipeline_templates(type="customized", language="en-US") + + assert result == {"pipeline_templates": [{"id": "custom-1"}]} + factory_mock.get_pipeline_template_factory.assert_called_with("customized") + + +@pytest.mark.parametrize("template_type", ["built-in", "customized"]) +def test_get_pipeline_template_detail_uses_expected_mode(mocker, template_type: str) -> None: + mocker.patch("services.rag_pipeline.rag_pipeline.dify_config.HOSTED_FETCH_PIPELINE_TEMPLATES_MODE", "remote") + retrieval = mocker.Mock() + retrieval.get_pipeline_template_detail.return_value = {"id": "tpl-1"} + + factory_mock = mocker.patch("services.rag_pipeline.rag_pipeline.PipelineTemplateRetrievalFactory") + factory_mock.get_pipeline_template_factory.return_value.return_value = retrieval + + result = RagPipelineService.get_pipeline_template_detail("tpl-1", type=template_type) + + assert result == {"id": "tpl-1"} + expected_mode = "remote" if template_type == "built-in" else "customized" + factory_mock.get_pipeline_template_factory.assert_called_with(expected_mode) + + +def test_get_published_workflow_returns_none_when_pipeline_has_no_workflow_id(rag_pipeline_service) -> None: + pipeline = SimpleNamespace(workflow_id=None) + + result = rag_pipeline_service.get_published_workflow(pipeline) + + assert result is None + + +def test_get_all_published_workflow_returns_empty_for_unpublished_pipeline(rag_pipeline_service) -> None: + pipeline = SimpleNamespace(workflow_id=None) + session = SimpleNamespace() + + workflows, has_more = rag_pipeline_service.get_all_published_workflow( + session=session, + pipeline=pipeline, + page=1, + limit=20, + user_id=None, + named_only=False, + ) + + assert workflows == [] + assert has_more is False + + +def test_get_all_published_workflow_applies_limit_and_has_more(rag_pipeline_service) -> None: + scalars_result = SimpleNamespace(all=lambda: ["wf1", "wf2", "wf3"]) + session = SimpleNamespace(scalars=lambda stmt: scalars_result) + pipeline = SimpleNamespace(id="pipeline-1", workflow_id="wf-live") + + workflows, has_more = rag_pipeline_service.get_all_published_workflow( + session=session, + pipeline=pipeline, + page=1, + limit=2, + user_id="user-1", + named_only=True, + ) + + assert workflows == ["wf1", "wf2"] + assert has_more is True + + +def test_get_pipeline_raises_when_dataset_not_found(mocker, rag_pipeline_service) -> None: + first_query = mocker.Mock() + first_query.where.return_value.first.return_value = None + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=first_query) + + with pytest.raises(ValueError, match="Dataset not found"): + rag_pipeline_service.get_pipeline("tenant-1", "dataset-1") + + +# --- update_customized_pipeline_template --- + + +def test_update_customized_pipeline_template_success(mocker) -> None: + template = SimpleNamespace(name="old", description="old", icon={}, updated_by=None) + + # First query finds the template, second query (duplicate check) returns None + query_mock_1 = mocker.Mock() + query_mock_1.where.return_value.first.return_value = template + query_mock_2 = mocker.Mock() + query_mock_2.where.return_value.first.return_value = None + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", side_effect=[query_mock_1, query_mock_2]) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.commit") + mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1")) + + info = PipelineTemplateInfoEntity( + name="new", + description="new desc", + icon_info=IconInfo(icon="🔥"), + ) + result = RagPipelineService.update_customized_pipeline_template("tpl-1", info) + + assert result.name == "new" + assert result.description == "new desc" + + +def test_update_customized_pipeline_template_not_found(mocker) -> None: + query_mock = mocker.Mock() + query_mock.where.return_value.first.return_value = None + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query_mock) + mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1")) + + info = PipelineTemplateInfoEntity(name="x", description="d", icon_info=IconInfo(icon="i")) + with pytest.raises(ValueError, match="Customized pipeline template not found"): + RagPipelineService.update_customized_pipeline_template("tpl-missing", info) + + +def test_update_customized_pipeline_template_duplicate_name(mocker) -> None: + template = SimpleNamespace(name="old", description="old", icon={}, updated_by=None) + duplicate = SimpleNamespace(name="dup") + + query_mock = mocker.Mock() + query_mock.where.return_value.first.side_effect = [template, duplicate] + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query_mock) + mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1")) + + info = PipelineTemplateInfoEntity(name="dup", description="d", icon_info=IconInfo(icon="i")) + with pytest.raises(ValueError, match="Template name is already exists"): + RagPipelineService.update_customized_pipeline_template("tpl-1", info) + + +# --- delete_customized_pipeline_template --- + + +def test_delete_customized_pipeline_template_success(mocker) -> None: + template = SimpleNamespace(id="tpl-1") + query_mock = mocker.Mock() + query_mock.where.return_value.first.return_value = template + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query_mock) + delete_mock = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.delete") + commit_mock = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.commit") + + mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1")) + + RagPipelineService.delete_customized_pipeline_template("tpl-1") + + delete_mock.assert_called_once_with(template) + commit_mock.assert_called_once() + + +def test_delete_customized_pipeline_template_not_found(mocker) -> None: + query_mock = mocker.Mock() + query_mock.where.return_value.first.return_value = None + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query_mock) + mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1")) + + with pytest.raises(ValueError, match="Customized pipeline template not found"): + RagPipelineService.delete_customized_pipeline_template("tpl-missing") + + +# --- sync_draft_workflow --- + + +def test_sync_draft_workflow_creates_new_when_none_exists(mocker, rag_pipeline_service) -> None: + mocker.patch.object(rag_pipeline_service, "get_draft_workflow", return_value=None) + + class FakeWorkflow: + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + self.id = "wf-new" + + mocker.patch("services.rag_pipeline.rag_pipeline.Workflow", FakeWorkflow) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.add") + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.flush") + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.commit") + + pipeline = SimpleNamespace(tenant_id="t1", id="p1", workflow_id=None) + account = SimpleNamespace(id="u1") + + result = rag_pipeline_service.sync_draft_workflow( + pipeline=pipeline, + graph={"nodes": []}, + unique_hash=None, + account=account, + environment_variables=[], + conversation_variables=[], + rag_pipeline_variables=[], + ) + + assert result.id == "wf-new" + assert pipeline.workflow_id == "wf-new" + + +def test_sync_draft_workflow_raises_on_hash_mismatch(mocker, rag_pipeline_service) -> None: + from services.errors.app import WorkflowHashNotEqualError + + existing_wf = SimpleNamespace(unique_hash="hash-old") + mocker.patch.object(rag_pipeline_service, "get_draft_workflow", return_value=existing_wf) + + pipeline = SimpleNamespace(tenant_id="t1", id="p1") + account = SimpleNamespace(id="u1") + + with pytest.raises(WorkflowHashNotEqualError): + rag_pipeline_service.sync_draft_workflow( + pipeline=pipeline, + graph={"nodes": []}, + unique_hash="hash-different", + account=account, + environment_variables=[], + conversation_variables=[], + rag_pipeline_variables=[], + ) + + +def test_sync_draft_workflow_updates_existing(mocker, rag_pipeline_service) -> None: + existing_wf = SimpleNamespace( + unique_hash="hash-1", + graph=None, + updated_by=None, + updated_at=None, + environment_variables=None, + conversation_variables=None, + rag_pipeline_variables=None, + ) + mocker.patch.object(rag_pipeline_service, "get_draft_workflow", return_value=existing_wf) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.commit") + + pipeline = SimpleNamespace(tenant_id="t1", id="p1") + account = SimpleNamespace(id="u1") + + result = rag_pipeline_service.sync_draft_workflow( + pipeline=pipeline, + graph={"nodes": [{"id": "n1"}]}, + unique_hash="hash-1", + account=account, + environment_variables=["env1"], + conversation_variables=["conv1"], + rag_pipeline_variables=["rp1"], + ) + + assert result is existing_wf + assert result.updated_by == "u1" + assert result.environment_variables == ["env1"] + + +# --- get_default_block_config --- + + +def test_get_default_block_config_returns_config_for_valid_type(mocker, rag_pipeline_service) -> None: + fake_node_class = mocker.Mock() + fake_node_class.get_default_config.return_value = {"type": "start", "config": {}} + + # Use a simpler approach: test with a known valid node type + from graphon.enums import BuiltinNodeTypes + + mocker.patch( + "services.rag_pipeline.rag_pipeline.get_node_type_classes_mapping", + return_value={BuiltinNodeTypes.START: {"1": fake_node_class}}, + ) + mocker.patch("services.rag_pipeline.rag_pipeline.LATEST_VERSION", "1") + + result = rag_pipeline_service.get_default_block_config("start") + + assert result == {"type": "start", "config": {}} + + +def test_get_default_block_config_returns_none_for_unmapped_type(rag_pipeline_service) -> None: + assert rag_pipeline_service.get_default_block_config("nonexistent-type") is None + + +# --- update_workflow --- + + +def test_update_workflow_updates_allowed_fields(mocker, rag_pipeline_service) -> None: + workflow = SimpleNamespace( + id="wf-1", marked_name="", marked_comment="", updated_by=None, updated_at=None, disallowed="original" + ) + session = mocker.Mock() + session.scalar.return_value = workflow + + result = rag_pipeline_service.update_workflow( + session=session, + workflow_id="wf-1", + tenant_id="t1", + account_id="u1", + data={"marked_name": "v1", "marked_comment": "release", "disallowed": "hacked"}, + ) + + assert result.marked_name == "v1" + assert result.marked_comment == "release" + assert result.disallowed == "original" # non-allowed field not updated + assert result.updated_by == "u1" + + +def test_update_workflow_returns_none_when_not_found(mocker, rag_pipeline_service) -> None: + session = mocker.Mock() + session.scalar.return_value = None + + result = rag_pipeline_service.update_workflow( + session=session, + workflow_id="wf-missing", + tenant_id="t1", + account_id="u1", + data={"marked_name": "v1"}, + ) + + assert result is None + + +# --- get_rag_pipeline_paginate_workflow_runs --- + + +def test_get_rag_pipeline_paginate_workflow_runs_delegates(mocker, rag_pipeline_service) -> None: + expected = mocker.Mock() + repo_mock = mocker.Mock() + repo_mock.get_paginated_workflow_runs.return_value = expected + rag_pipeline_service._workflow_run_repo = repo_mock + + pipeline = SimpleNamespace(tenant_id="t1", id="p1") + result = rag_pipeline_service.get_rag_pipeline_paginate_workflow_runs(pipeline, {"limit": 10, "last_id": "abc"}) + + assert result is expected + repo_mock.get_paginated_workflow_runs.assert_called_once_with( + tenant_id="t1", + app_id="p1", + triggered_from=mocker.ANY, + limit=10, + last_id="abc", + ) + + +# --- get_rag_pipeline_workflow_run --- + + +def test_get_rag_pipeline_workflow_run_delegates(mocker, rag_pipeline_service) -> None: + expected = mocker.Mock() + repo_mock = mocker.Mock() + repo_mock.get_workflow_run_by_id.return_value = expected + rag_pipeline_service._workflow_run_repo = repo_mock + + pipeline = SimpleNamespace(tenant_id="t1", id="p1") + result = rag_pipeline_service.get_rag_pipeline_workflow_run(pipeline, "run-1") + + assert result is expected + repo_mock.get_workflow_run_by_id.assert_called_once_with(tenant_id="t1", app_id="p1", run_id="run-1") + + +# --- is_workflow_exist --- + + +def test_is_workflow_exist_returns_true_when_draft_exists(mocker, rag_pipeline_service) -> None: + query_mock = mocker.Mock() + query_mock.where.return_value.count.return_value = 1 + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query_mock) + + pipeline = SimpleNamespace(tenant_id="t1", id="p1") + assert rag_pipeline_service.is_workflow_exist(pipeline) is True + + +def test_is_workflow_exist_returns_false_when_no_draft(mocker, rag_pipeline_service) -> None: + query_mock = mocker.Mock() + query_mock.where.return_value.count.return_value = 0 + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query_mock) + + pipeline = SimpleNamespace(tenant_id="t1", id="p1") + assert rag_pipeline_service.is_workflow_exist(pipeline) is False + + +# --- publish_workflow --- + + +def test_publish_workflow_success(mocker, rag_pipeline_service) -> None: + # Don't import Workflow from rag_pipeline to avoid confusion during patching + + # 1. Mock select to bypass SQLAlchemy validation + mock_select = mocker.patch("services.rag_pipeline.rag_pipeline.select") + + # 2. Setup draft workflow mock + draft_wf = mocker.Mock() + draft_wf.id = "wf-draft" + draft_wf.unique_hash = "hash-1" + draft_wf.graph = { + "nodes": [ + { + "data": { + "type": "knowledge-index", + "dataset_id": "d1", + "chunk_structure": "paragraph", + "indexing_technique": "high_quality", + "process_rule": {"mode": "automatic"}, + "retrieval_model": {"search_method": "hybrid_search", "top_k": 3}, + } + } + ] + } + draft_wf.environment_variables = [] + draft_wf.conversation_variables = [] + draft_wf.rag_pipeline_variables = [] + draft_wf.type = "workflow" + draft_wf.features = {} + + # 3. Setup pipeline and account + pipeline = mocker.Mock() + pipeline.id = "p1" + pipeline.tenant_id = "t1" + pipeline.workflow_id = "wf-old-published" + + account = mocker.Mock() + account.id = "u1" + + # 4. Mock Workflow class and its .new() method + mock_workflow_class = mocker.patch("services.rag_pipeline.rag_pipeline.Workflow") + new_wf = mocker.Mock() + new_wf.id = "wf-published-new" + new_wf.graph_dict = draft_wf.graph + mock_workflow_class.new.return_value = new_wf + + # 5. Mock entire db object and DatasetService + mock_db = mocker.Mock() + mocker.patch("services.rag_pipeline.rag_pipeline.db", mock_db) + mock_dataset_service_class = mocker.patch("services.dataset_service.DatasetService") + mock_dataset_service = mock_dataset_service_class.return_value + + # 6. Mock session and its scalar/query methods + mock_session = mocker.Mock() + mock_session.scalar.return_value = draft_wf + + # Mock dataset update query (needed even if service is mocked, as rag_pipeline fetches it first) + dataset = mocker.Mock() + dataset.retrieval_model_dict = {} + dataset_query = mocker.Mock() + dataset_query.where.return_value.first.return_value = dataset + + # Mock node execution copy + node_exec_query = mocker.Mock() + node_exec_query.where.return_value.all.return_value = [] + + # Mocked session query side effects + mock_session.query.side_effect = [node_exec_query, dataset_query] + + # 7. Run test + result = rag_pipeline_service.publish_workflow(session=mock_session, pipeline=pipeline, account=account) + + # 8. Assertions + assert result == new_wf + # Note: dataset settings are updated via DatasetService now, so we can verify the call + mock_dataset_service_class.update_rag_pipeline_dataset_settings.assert_called_once() + + +# --- run_datasource_workflow_node --- + + +def test_run_datasource_workflow_node_website_crawl(mocker, rag_pipeline_service) -> None: + from core.datasource.entities.datasource_entities import DatasourceProviderType + + # 1. Setup workflow and node + pipeline = mocker.Mock() + pipeline.id = "p1" + pipeline.tenant_id = "t1" + + workflow = mocker.Mock() + workflow.graph_dict = { + "nodes": [ + { + "id": "node-1", + "data": { + "type": "datasource", + "plugin_id": "p-1", + "provider_name": "firecrawl", + "datasource_name": "website_crawl", + "datasource_parameters": {"url": {"value": "{{#start.url#}}"}}, + }, + } + ] + } + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) + + # 2. Mock DatasourceManager and Runtime + mock_runtime = mocker.Mock() + mock_runtime.datasource_provider_type.return_value = DatasourceProviderType.WEBSITE_CRAWL + + # Mock the generator result for website crawl + def mock_crawl_gen(**kwargs): + yield mocker.Mock(result=mocker.Mock(status="processing", total=10, completed=2)) + yield mocker.Mock( + result=mocker.Mock(status="completed", total=10, completed=10, web_info_list=[{"title": "test"}]) + ) + + mock_runtime.get_website_crawl.side_effect = mock_crawl_gen + + mocker.patch( + "core.datasource.datasource_manager.DatasourceManager.get_datasource_runtime", + return_value=mock_runtime, + ) + + # 3. Mock DatasourceProviderService + mocker.patch( + "services.rag_pipeline.rag_pipeline.DatasourceProviderService.get_datasource_credentials", + return_value={"api_key": "sk-123"}, + ) + + # 4. Mock Enums to avoid import issues or for consistency + mocker.patch("services.rag_pipeline.rag_pipeline.DatasourceProviderType", DatasourceProviderType) + + # 5. Run test + gen = rag_pipeline_service.run_datasource_workflow_node( + pipeline=pipeline, + node_id="node-1", + user_inputs={"url": "https://example.com"}, + account=mocker.Mock(id="u1"), + datasource_type="website_crawl", + is_published=True, + ) + + events = list(gen) + + # 6. Assertions + assert len(events) == 2 + assert events[0]["total"] == 10 + assert events[0]["completed"] == 2 + assert events[1]["data"] == [{"title": "test"}] + assert events[1]["total"] == 10 + assert events[1]["completed"] == 10 + + +# --- run_datasource_node_preview --- + + +def test_run_datasource_node_preview_online_document(mocker, rag_pipeline_service) -> None: + from core.datasource.entities.datasource_entities import DatasourceMessage, DatasourceProviderType + + # 1. Setup workflow and node + pipeline = mocker.Mock() + pipeline.id = "p1" + pipeline.tenant_id = "t1" + + workflow = mocker.Mock() + workflow.graph_dict = { + "nodes": [ + { + "id": "node-1", + "data": { + "type": "datasource", + "plugin_id": "p-1", + "provider_name": "notion", + "datasource_name": "online_document", + "datasource_parameters": { + "workspace_id": {"value": "ws-1"}, + "page_id": {"value": "pg-1"}, + "type": {"value": "page"}, + }, + }, + } + ] + } + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) + + # 2. Mock Runtime and results + mock_runtime = mocker.Mock() + + def mock_doc_gen(**kwargs): + # Yield a variable message + msg1 = DatasourceMessage( + type=DatasourceMessage.MessageType.VARIABLE, + message=DatasourceMessage.VariableMessage(variable_name="content", variable_value="Hello ", stream=True), + ) + yield msg1 + msg2 = DatasourceMessage( + type=DatasourceMessage.MessageType.VARIABLE, + message=DatasourceMessage.VariableMessage(variable_name="content", variable_value="World", stream=True), + ) + yield msg2 + + mock_runtime.get_online_document_page_content.side_effect = mock_doc_gen + mocker.patch( + "core.datasource.datasource_manager.DatasourceManager.get_datasource_runtime", + return_value=mock_runtime, + ) + mocker.patch( + "services.rag_pipeline.rag_pipeline.DatasourceProviderService.get_datasource_credentials", + return_value={"token": "abc"}, + ) + mocker.patch("services.rag_pipeline.rag_pipeline.DatasourceProviderType", DatasourceProviderType) + + # 3. Run test + result = rag_pipeline_service.run_datasource_node_preview( + pipeline=pipeline, + node_id="node-1", + user_inputs={}, + account=mocker.Mock(id="u1"), + datasource_type="online_document", + is_published=True, + ) + + # 4. Assertions + assert result == {"content": "Hello World"} + + +# --- _handle_node_run_result --- + + +def test_handle_node_run_result_success(mocker, rag_pipeline_service) -> None: + from graphon.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus + from graphon.graph_events import NodeRunSucceededEvent + from graphon.node_events.base import NodeRunResult + + # 1. Setup mock node and result + node_instance = mocker.Mock() + node_instance.workflow_id = "wf-1" + node_instance.node_type = "start" + node_instance.title = "Start" + + node_run_result = NodeRunResult( + status=WorkflowNodeExecutionStatus.SUCCEEDED, + inputs={"q": "hi"}, + outputs={"ans": "hello"}, + metadata={WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS: 10}, + ) + + def mock_getter(): + event = NodeRunSucceededEvent( + id="event-1", + start_at=time.time(), + node_id="node-1", + node_type="start", + node_run_result=node_run_result, + route_node_id=None, + ) + yield event + + # 2. Run test + result = rag_pipeline_service._handle_node_run_result( + getter=lambda: (node_instance, mock_getter()), start_at=time.perf_counter(), tenant_id="t1", node_id="node-1" + ) + + # 3. Assertions + assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED + assert result.inputs == {"q": "hi"} + assert result.outputs == {"ans": "hello"} + assert result.metadata == {WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS: 10} + + +# --- get_first_step_parameters / get_second_step_parameters --- + + +def test_get_first_step_parameters_success(mocker, rag_pipeline_service) -> None: + # 1. Setup mock workflow + pipeline = mocker.Mock() + workflow = mocker.Mock() + workflow.graph_dict = { + "nodes": [{"id": "node-1", "data": {"datasource_parameters": {"url": {"value": "{{#start.url#}}"}}}}] + } + workflow.rag_pipeline_variables = [{"variable": "url", "label": "URL", "type": "string"}] + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) + + # 2. Run test + result = rag_pipeline_service.get_first_step_parameters(pipeline=pipeline, node_id="node-1", is_draft=False) + + # 3. Assertions + assert len(result) == 1 + assert result[0]["variable"] == "url" + + +def test_get_second_step_parameters_success(mocker, rag_pipeline_service) -> None: + # 1. Setup mock workflow + pipeline = mocker.Mock() + workflow = mocker.Mock() + workflow.graph_dict = { + "nodes": [ + { + "id": "node-1", + "data": {}, # Second step logic is slightly different in how it gets variables + } + ] + } + workflow.rag_pipeline_variables = [{"variable": "var1", "label": "Var 1"}] + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) + + # 2. Run test + result = rag_pipeline_service.get_second_step_parameters(pipeline=pipeline, node_id="node-1", is_draft=False) + + # 3. Assertions + # Note: get_second_step_parameters also filters by variable names found in node data + # (Checking the code again, it seems to iterate through nodes but doesn't do much with variables yet) + # Wait, let me check the code for get_second_step_parameters again. + assert len(result) == 0 # Based on current implementation which seems to filter but no logic added yet? + + +# --- publish_customized_pipeline_template --- + + +def test_publish_customized_pipeline_template_success(mocker, rag_pipeline_service) -> None: + from models.dataset import Dataset, Pipeline, PipelineCustomizedTemplate + from models.workflow import Workflow + + # 1. Setup mocks + pipeline = mocker.Mock(spec=Pipeline) + pipeline.id = "p1" + pipeline.tenant_id = "t1" + pipeline.workflow_id = "wf-1" + pipeline.is_published = True + + workflow = mocker.Mock() + workflow.id = "wf-1" + + # Mock db itself to avoid app context errors + mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db") + + # Improved mocking for session.query + def mock_query_side_effect(model): + m = mocker.Mock() + if model == Pipeline: + m.where.return_value.first.return_value = pipeline + elif model == Workflow: + m.where.return_value.first.return_value = workflow + elif model == PipelineCustomizedTemplate: + m.where.return_value.first.return_value = None + elif model == Dataset: + m.where.return_value.first.return_value = mocker.Mock() + else: + # For func.max cases + m.where.return_value.scalar.return_value = 5 + m.where.return_value.first.return_value = mocker.Mock() + return m + + mock_db.session.query.side_effect = mock_query_side_effect + + # Mock retrieve_dataset + dataset = mocker.Mock() + pipeline.retrieve_dataset.return_value = dataset + + # Mock max position + mocker.patch("services.rag_pipeline.rag_pipeline.func.max", return_value=1) + mocker.patch( + "services.rag_pipeline.rag_pipeline.db.session.query.return_value.where.return_value.scalar", + return_value=5, + ) + + # Mock RagPipelineDslService + mock_dsl_service = mocker.Mock() + mock_dsl_service.export_rag_pipeline_dsl.return_value = {"dsl": "content"} + mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.RagPipelineDslService", return_value=mock_dsl_service) + + # Mock Session and commit + mocker.patch("services.rag_pipeline.rag_pipeline.Session", return_value=mocker.MagicMock()) + + # Mock current_user + mock_user = mocker.Mock() + mock_user.id = "user-123" + mocker.patch("services.rag_pipeline.rag_pipeline.current_user", mock_user) + + # 2. Run test + args = {"name": "New Template", "description": "Desc", "icon_info": {"icon": "star"}, "tags": ["tag1"]} + rag_pipeline_service.publish_customized_pipeline_template("p1", args) + + # 3. Assertions + # Verify a new template was added to session or similar? + # Since we can't easily check the session inside the context manager with Mock, + # we just check that no error was raised and DSL was exported. + mock_dsl_service.export_rag_pipeline_dsl.assert_called_once() + + +# --- get_datasource_plugins --- + + +def test_get_datasource_plugins_success(mocker, rag_pipeline_service) -> None: + from models.dataset import Dataset, Pipeline + + # 1. Setup mocks + dataset = mocker.Mock(spec=Dataset) + dataset.pipeline_id = "p1" + + pipeline = mocker.Mock(spec=Pipeline) + pipeline.id = "p1" + + workflow = mocker.Mock() + workflow.graph_dict = { + "nodes": [ + { + "id": "node-1", + "data": { + "type": "datasource", + "plugin_id": "p-1", + "provider_name": "notion", + "provider_type": "online_document", + "title": "Notion", + }, + } + ] + } + workflow.rag_pipeline_variables = [] + + # Mock queries + mock_query = mocker.Mock() + mock_query.where.return_value.first.side_effect = [dataset, pipeline] + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=mock_query) + + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) + + # Mock DatasourceProviderService + mock_provider_service = mocker.Mock() + mock_provider_service.list_datasource_credentials.return_value = [ + {"id": "c1", "name": "Cred 1", "type": "token", "is_default": True} + ] + mocker.patch("services.rag_pipeline.rag_pipeline.DatasourceProviderService", return_value=mock_provider_service) + + # 2. Run test + result = rag_pipeline_service.get_datasource_plugins("t1", "d1", True) + + # 3. Assertions + assert len(result) == 1 + assert result[0]["node_id"] == "node-1" + assert result[0]["credentials"][0]["id"] == "c1" + + +# --- retry_error_document --- + + +def test_retry_error_document_success(mocker, rag_pipeline_service) -> None: + from models.dataset import Document, DocumentPipelineExecutionLog, Pipeline + + # 1. Setup mocks + dataset = mocker.Mock() + document = mocker.Mock(spec=Document) + document.id = "doc-1" + + log = mocker.Mock(spec=DocumentPipelineExecutionLog) + log.pipeline_id = "p-1" + log.datasource_info = "{}" # Ensure it's a string if it's used as JSON later + + pipeline = mocker.Mock(spec=Pipeline) + pipeline.id = "p-1" + + workflow = mocker.Mock() + + # Mock queries + mock_query = mocker.Mock() + # Log lookup, then Pipeline lookup + mock_query.where.return_value.first.side_effect = [log, pipeline] + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=mock_query) + + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) + + # Mock PipelineGenerator + mock_gen_instance = mocker.Mock() + mocker.patch("services.rag_pipeline.rag_pipeline.PipelineGenerator", return_value=mock_gen_instance) + + # 2. Run test + user = mocker.Mock() + rag_pipeline_service.retry_error_document(dataset, document, user) + + # 3. Assertions + mock_gen_instance.generate.assert_called_once() + + +# --- set_datasource_variables --- + + +def test_set_datasource_variables_success(mocker, rag_pipeline_service) -> None: + from graphon.entities.workflow_node_execution import WorkflowNodeExecution + + from models.dataset import Pipeline + + # 1. Setup mocks + # Mock db aggressively + mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db") + mock_db.engine = mocker.Mock() + mock_db.session.query.return_value.where.return_value.first.return_value = mocker.Mock() + + pipeline = mocker.Mock(spec=Pipeline) + pipeline.id = "p-1" + pipeline.tenant_id = "t1" + + draft_wf = mocker.Mock() + draft_wf.id = "wf-1" + draft_wf.get_enclosing_node_type_and_id.return_value = None # Avoid unpacking error + mocker.patch.object(rag_pipeline_service, "get_draft_workflow", return_value=draft_wf) + + execution = mocker.Mock(spec=WorkflowNodeExecution) + execution.id = "exec-1" + execution.process_data = {} + execution.inputs = {} + execution.outputs = {} + mocker.patch.object(rag_pipeline_service, "_handle_node_run_result", return_value=execution) + + # Mock Repository + mock_repo_instance = mocker.Mock() + mocker.patch( + "services.rag_pipeline.rag_pipeline.SQLAlchemyWorkflowNodeExecutionRepository", + return_value=mock_repo_instance, + ) + # Repository._to_db_model is also called + mock_db_exec = mocker.Mock() + mock_db_exec.node_id = "node-1" + mock_db_exec.node_type = "datasource" + mock_repo_instance._to_db_model.return_value = mock_db_exec + + # Mock Session and begin + mocker.patch("services.rag_pipeline.rag_pipeline.Session", return_value=mocker.MagicMock()) + + # Mock DraftVariableSaver + mock_saver_instance = mocker.Mock() + mocker.patch("services.rag_pipeline.rag_pipeline.DraftVariableSaver", return_value=mock_saver_instance) + + # 2. Run test + args = {"start_node_id": "node-1"} + user = mocker.Mock() + user.id = "user-1" + rag_pipeline_service.set_datasource_variables(pipeline, args, user) + + # 3. Assertions + mock_repo_instance.save.assert_called_once() + mock_saver_instance.save.assert_called_once() + + +# --- Utility Methods --- + + +def test_get_draft_workflow_success(mocker, rag_pipeline_service) -> None: + from models.dataset import Pipeline + from models.workflow import Workflow + + # 1. Setup mocks + pipeline = mocker.Mock(spec=Pipeline) + pipeline.id = "p1" + pipeline.tenant_id = "t1" + + workflow = mocker.Mock(spec=Workflow) + + mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db") + mock_db.session.query.return_value.where.return_value.first.return_value = workflow + + # 2. Run test + result = rag_pipeline_service.get_draft_workflow(pipeline) + + # 3. Assertions + assert result == workflow + + +def test_get_published_workflow_success(mocker, rag_pipeline_service) -> None: + from models.dataset import Pipeline + from models.workflow import Workflow + + # 1. Setup mocks + pipeline = mocker.Mock(spec=Pipeline) + pipeline.id = "p1" + pipeline.tenant_id = "t1" + pipeline.workflow_id = "wf-pub" + + workflow = mocker.Mock(spec=Workflow) + + mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db") + mock_db.session.query.return_value.where.return_value.first.return_value = workflow + + # 2. Run test + result = rag_pipeline_service.get_published_workflow(pipeline) + + # 3. Assertions + assert result == workflow + + +def test_get_default_block_configs_success(rag_pipeline_service) -> None: + # This calls static methods on node classes, should be safe with default mocks or as-is + # unless they access db. + result = rag_pipeline_service.get_default_block_configs() + assert isinstance(result, list) + assert len(result) > 0 + + +def test_get_default_block_config_success(rag_pipeline_service) -> None: + from graphon.enums import BuiltinNodeTypes + + result = rag_pipeline_service.get_default_block_config(BuiltinNodeTypes.LLM) + assert result is not None + assert result["type"] == "llm" + + +def test_publish_workflow_raises_when_draft_workflow_missing(mocker, rag_pipeline_service) -> None: + session = mocker.Mock() + session.scalar.return_value = None + pipeline = SimpleNamespace(id="p1", tenant_id="t1") + account = SimpleNamespace(id="u1") + + with pytest.raises(ValueError, match="No valid workflow found"): + rag_pipeline_service.publish_workflow(session=session, pipeline=pipeline, account=account) + + +def test_get_default_block_config_returns_none_when_mapped_type_missing(mocker, rag_pipeline_service) -> None: + from graphon.enums import BuiltinNodeTypes + + mocker.patch("services.rag_pipeline.rag_pipeline.get_node_type_classes_mapping", return_value={}) + + assert rag_pipeline_service.get_default_block_config(BuiltinNodeTypes.START) is None + + +def test_get_default_block_config_injects_http_request_filter(mocker, rag_pipeline_service) -> None: + from graphon.enums import BuiltinNodeTypes + + fake_node_cls = mocker.Mock() + fake_node_cls.get_default_config.return_value = {"type": "http-request"} + mocker.patch( + "services.rag_pipeline.rag_pipeline.get_node_type_classes_mapping", + return_value={BuiltinNodeTypes.HTTP_REQUEST: {"1": fake_node_cls}}, + ) + mocker.patch("services.rag_pipeline.rag_pipeline.LATEST_VERSION", "1") + + rag_pipeline_service.get_default_block_config(BuiltinNodeTypes.HTTP_REQUEST) + + called_filters = fake_node_cls.get_default_config.call_args.kwargs["filters"] + assert "http_request_config" in called_filters + + +def test_run_draft_workflow_node_raises_when_workflow_missing(mocker, rag_pipeline_service) -> None: + pipeline = SimpleNamespace(id="p1", tenant_id="t1") + account = SimpleNamespace(id="u1") + mocker.patch.object(rag_pipeline_service, "get_draft_workflow", return_value=None) + + with pytest.raises(ValueError, match="Workflow not initialized"): + rag_pipeline_service.run_draft_workflow_node(pipeline, "node-1", {}, account) + + +def test_run_draft_workflow_node_saves_execution_and_variables(mocker, rag_pipeline_service) -> None: + mocker.patch("services.rag_pipeline.rag_pipeline.db", mocker.Mock(engine=mocker.Mock())) + pipeline = SimpleNamespace(id="p1", tenant_id="t1") + account = SimpleNamespace(id="u1") + draft_workflow = mocker.Mock(id="wf-1") + draft_workflow.get_node_config_by_id.return_value = {"id": "node-1"} + draft_workflow.get_enclosing_node_type_and_id.return_value = ("loop", "enclosing-node") + mocker.patch.object(rag_pipeline_service, "get_draft_workflow", return_value=draft_workflow) + + execution = SimpleNamespace(id="exec-1", node_id="node-1", node_type="llm", process_data={}, outputs={}) + mocker.patch.object(rag_pipeline_service, "_handle_node_run_result", return_value=execution) + + repo = mocker.Mock() + mocker.patch( + "services.rag_pipeline.rag_pipeline.DifyCoreRepositoryFactory.create_workflow_node_execution_repository", + return_value=repo, + ) + rag_pipeline_service._node_execution_service_repo = mocker.Mock(get_execution_by_id=mocker.Mock(return_value="db")) + saver = mocker.Mock() + mocker.patch("services.rag_pipeline.rag_pipeline.DraftVariableSaver", return_value=saver) + + session_ctx = mocker.MagicMock() + begin_ctx = mocker.MagicMock() + session_ctx.begin.return_value = begin_ctx + mocker.patch("services.rag_pipeline.rag_pipeline.Session", return_value=session_ctx) + + result = rag_pipeline_service.run_draft_workflow_node(pipeline, "node-1", {"q": "x"}, account) + + assert result == "db" + assert execution.workflow_id == "wf-1" + repo.save.assert_called_once_with(execution) + saver.save.assert_called_once() + + +def test_run_datasource_workflow_node_returns_error_when_workflow_missing(mocker, rag_pipeline_service) -> None: + pipeline = SimpleNamespace(id="p1", tenant_id="t1") + mocker.patch.object(rag_pipeline_service, "get_draft_workflow", return_value=None) + + events = list( + rag_pipeline_service.run_datasource_workflow_node( + pipeline=pipeline, + node_id="node-1", + user_inputs={}, + account=SimpleNamespace(id="u1"), + datasource_type="online_document", + is_published=False, + ) + ) + + assert events[0]["event"] == "datasource_error" + + +def test_run_datasource_workflow_node_online_document_success(mocker, rag_pipeline_service) -> None: + from core.datasource.entities.datasource_entities import DatasourceProviderType + + pipeline = SimpleNamespace(id="p1", tenant_id="t1") + workflow = mocker.Mock() + workflow.graph_dict = { + "nodes": [ + { + "id": "node-1", + "data": { + "type": "datasource", + "plugin_id": "pid", + "provider_name": "notion", + "datasource_name": "online_document", + "datasource_parameters": {"workspace_id": {"value": None}, "page_id": {"value": "fixed"}}, + }, + } + ] + } + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) + + runtime = mocker.Mock() + runtime.runtime = SimpleNamespace(credentials=None) + runtime.datasource_provider_type.return_value = DatasourceProviderType.ONLINE_DOCUMENT + runtime.get_online_document_pages.return_value = [SimpleNamespace(result=[{"id": "pg-1"}])] + mocker.patch("core.datasource.datasource_manager.DatasourceManager.get_datasource_runtime", return_value=runtime) + mocker.patch( + "services.rag_pipeline.rag_pipeline.DatasourceProviderService.get_datasource_credentials", + return_value={"token": "x"}, + ) + + events = list( + rag_pipeline_service.run_datasource_workflow_node( + pipeline=pipeline, + node_id="node-1", + user_inputs={}, + account=SimpleNamespace(id="u1"), + datasource_type=DatasourceProviderType.ONLINE_DOCUMENT, + is_published=True, + ) + ) + + assert events[0]["event"] == "datasource_processing" + assert events[1]["event"] == "datasource_completed" + + +def test_run_datasource_workflow_node_online_drive_success(mocker, rag_pipeline_service) -> None: + from core.datasource.entities.datasource_entities import DatasourceProviderType + + pipeline = SimpleNamespace(id="p1", tenant_id="t1") + workflow = mocker.Mock() + workflow.graph_dict = { + "nodes": [ + { + "id": "node-1", + "data": { + "type": "datasource", + "plugin_id": "pid", + "provider_name": "drive", + "datasource_name": "online_drive", + "datasource_parameters": {"bucket": {"value": "bucket-1"}, "next_page_parameters": {"value": []}}, + }, + } + ] + } + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) + + runtime = mocker.Mock() + runtime.runtime = SimpleNamespace(credentials=None) + runtime.datasource_provider_type.return_value = DatasourceProviderType.ONLINE_DRIVE + runtime.online_drive_browse_files.return_value = [SimpleNamespace(result=[{"name": "f1"}])] + mocker.patch("core.datasource.datasource_manager.DatasourceManager.get_datasource_runtime", return_value=runtime) + mocker.patch( + "services.rag_pipeline.rag_pipeline.DatasourceProviderService.get_datasource_credentials", + return_value={}, + ) + + events = list( + rag_pipeline_service.run_datasource_workflow_node( + pipeline=pipeline, + node_id="node-1", + user_inputs={"bucket": "bucket-1"}, + account=SimpleNamespace(id="u1"), + datasource_type=DatasourceProviderType.ONLINE_DRIVE, + is_published=True, + ) + ) + + assert events[0]["event"] == "datasource_processing" + assert events[1]["event"] == "datasource_completed" + + +def test_handle_node_run_result_default_value_strategy(mocker, rag_pipeline_service) -> None: + from datetime import datetime + + from graphon.enums import BuiltinNodeTypes, ErrorStrategy, WorkflowNodeExecutionStatus + from graphon.graph_events import NodeRunFailedEvent + from graphon.node_events.base import NodeRunResult + + node_instance = SimpleNamespace( + workflow_id="wf-1", + node_type=BuiltinNodeTypes.START, + title="Start", + error_strategy=ErrorStrategy.DEFAULT_VALUE, + default_value_dict={"fallback": "ok"}, + graph_runtime_state=SimpleNamespace(variable_pool=mocker.Mock()), + ) + + failed_result = NodeRunResult( + status=WorkflowNodeExecutionStatus.FAILED, + error="boom", + error_type="runtime_error", + inputs={"x": 1}, + ) + + def _events(): + yield NodeRunFailedEvent( + id="e-1", + node_id="node-1", + node_type=BuiltinNodeTypes.START, + start_at=datetime.now(), + error="boom", + node_run_result=failed_result, + ) + + result = rag_pipeline_service._handle_node_run_result( + getter=lambda: (node_instance, _events()), + start_at=time.perf_counter(), + tenant_id="t1", + node_id="node-1", + ) + + assert result.status == WorkflowNodeExecutionStatus.EXCEPTION + assert result.outputs + assert result.outputs["fallback"] == "ok" + + +def test_get_first_step_parameters_raises_when_datasource_node_missing(mocker, rag_pipeline_service) -> None: + workflow = SimpleNamespace(graph_dict={"nodes": []}, rag_pipeline_variables=[{"variable": "url"}]) + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) + + with pytest.raises(ValueError, match="Datasource node data not found"): + rag_pipeline_service.get_first_step_parameters(SimpleNamespace(), "missing-node") + + +def test_get_second_step_parameters_handles_string_and_list_variable_references(mocker, rag_pipeline_service) -> None: + workflow = SimpleNamespace( + rag_pipeline_variables=[ + {"variable": "url", "belong_to_node_id": "node-1"}, + {"variable": "bucket", "belong_to_node_id": "shared"}, + {"variable": "keep", "belong_to_node_id": "node-1"}, + ], + graph_dict={ + "nodes": [ + { + "id": "node-1", + "data": { + "datasource_parameters": { + "u": {"value": "{{#start.url#}}"}, + "b": {"value": ["start", "bucket"]}, + } + }, + } + ] + }, + ) + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) + + result = rag_pipeline_service.get_second_step_parameters(SimpleNamespace(), "node-1") + + assert result == [{"variable": "keep", "belong_to_node_id": "node-1"}] + + +def test_get_rag_pipeline_workflow_run_node_executions_empty_when_run_missing(mocker, rag_pipeline_service) -> None: + pipeline = SimpleNamespace(id="p1", tenant_id="t1") + mocker.patch.object(rag_pipeline_service, "get_rag_pipeline_workflow_run", return_value=None) + + result = rag_pipeline_service.get_rag_pipeline_workflow_run_node_executions( + pipeline=pipeline, run_id="run-1", user=SimpleNamespace(id="u1") + ) + + assert result == [] + + +def test_get_rag_pipeline_workflow_run_node_executions_returns_sorted_executions(mocker, rag_pipeline_service) -> None: + mocker.patch("services.rag_pipeline.rag_pipeline.db", mocker.Mock(engine=mocker.Mock())) + pipeline = SimpleNamespace(id="p1", tenant_id="t1") + mocker.patch.object(rag_pipeline_service, "get_rag_pipeline_workflow_run", return_value=SimpleNamespace(id="run-1")) + repo = mocker.Mock() + repo.get_db_models_by_workflow_run.return_value = ["n1", "n2"] + mocker.patch("services.rag_pipeline.rag_pipeline.SQLAlchemyWorkflowNodeExecutionRepository", return_value=repo) + + result = rag_pipeline_service.get_rag_pipeline_workflow_run_node_executions( + pipeline=pipeline, run_id="run-1", user=SimpleNamespace(id="u1") + ) + + assert result == ["n1", "n2"] + + +def test_get_recommended_plugins_returns_empty_when_no_active_plugins(mocker, rag_pipeline_service) -> None: + query = mocker.Mock() + query.where.return_value = query + query.order_by.return_value.all.return_value = [] + mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db") + mock_db.session.query.return_value = query + + result = rag_pipeline_service.get_recommended_plugins("all") + + assert result == { + "installed_recommended_plugins": [], + "uninstalled_recommended_plugins": [], + } + + +def test_get_recommended_plugins_returns_installed_and_uninstalled(mocker, rag_pipeline_service) -> None: + plugin_a = SimpleNamespace(plugin_id="plugin-a") + plugin_b = SimpleNamespace(plugin_id="plugin-b") + query = mocker.Mock() + query.where.return_value = query + query.order_by.return_value.all.return_value = [plugin_a, plugin_b] + mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db") + mock_db.session.query.return_value = query + mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1")) + mocker.patch( + "services.rag_pipeline.rag_pipeline.BuiltinToolManageService.list_builtin_tools", + return_value=[SimpleNamespace(plugin_id="plugin-a", to_dict=lambda: {"plugin_id": "plugin-a"})], + ) + mocker.patch( + "services.rag_pipeline.rag_pipeline.marketplace.batch_fetch_plugin_by_ids", + return_value=[{"plugin_id": "plugin-b", "name": "Plugin B"}], + ) + + result = rag_pipeline_service.get_recommended_plugins("custom") + + assert result["installed_recommended_plugins"] == [{"plugin_id": "plugin-a"}] + assert result["uninstalled_recommended_plugins"] == [{"plugin_id": "plugin-b", "name": "Plugin B"}] + + +def test_get_node_last_run_delegates_to_repository(mocker, rag_pipeline_service) -> None: + mocker.patch("services.rag_pipeline.rag_pipeline.db", mocker.Mock(engine=mocker.Mock())) + repo = mocker.Mock() + repo.get_node_last_execution.return_value = "node-exec" + mocker.patch( + "services.rag_pipeline.rag_pipeline.DifyAPIRepositoryFactory.create_api_workflow_node_execution_repository", + return_value=repo, + ) + pipeline = SimpleNamespace(id="p1", tenant_id="t1") + workflow = SimpleNamespace(id="wf1") + + result = rag_pipeline_service.get_node_last_run(pipeline, workflow, "node-1") + + assert result == "node-exec" + + +def test_set_datasource_variables_raises_when_node_id_missing(mocker, rag_pipeline_service) -> None: + pipeline = SimpleNamespace(id="p1", tenant_id="t1") + workflow = mocker.Mock() + mocker.patch.object(rag_pipeline_service, "get_draft_workflow", return_value=workflow) + + with pytest.raises(ValueError, match="Node id is required"): + rag_pipeline_service.set_datasource_variables(pipeline, {"start_node_id": ""}, SimpleNamespace(id="u1")) + + +def test_get_default_block_configs_skips_empty_configs(mocker, rag_pipeline_service) -> None: + from graphon.enums import BuiltinNodeTypes + + http_node = mocker.Mock() + http_node.get_default_config.return_value = {"type": "http-request"} + empty_node = mocker.Mock() + empty_node.get_default_config.return_value = None + + mocker.patch( + "services.rag_pipeline.rag_pipeline.get_node_type_classes_mapping", + return_value={ + BuiltinNodeTypes.HTTP_REQUEST: {"1": http_node}, + BuiltinNodeTypes.START: {"1": empty_node}, + }, + ) + mocker.patch("services.rag_pipeline.rag_pipeline.LATEST_VERSION", "1") + + result = rag_pipeline_service.get_default_block_configs() + + assert result == [{"type": "http-request"}] + http_node.get_default_config.assert_called_once() + empty_node.get_default_config.assert_called_once() + + +def test_run_datasource_workflow_node_returns_error_when_node_missing(mocker, rag_pipeline_service) -> None: + pipeline = SimpleNamespace(id="p1", tenant_id="t1") + workflow = SimpleNamespace(graph_dict={"nodes": []}) + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) + + events = list( + rag_pipeline_service.run_datasource_workflow_node( + pipeline=pipeline, + node_id="missing-node", + user_inputs={}, + account=SimpleNamespace(id="u1"), + datasource_type="online_document", + is_published=True, + ) + ) + + assert len(events) == 1 + assert "Datasource node data not found" in events[0]["error"] + + +def test_run_datasource_workflow_node_online_document_exception(mocker, rag_pipeline_service) -> None: + pipeline = SimpleNamespace(id="p1", tenant_id="t1") + workflow = SimpleNamespace( + graph_dict={ + "nodes": [ + { + "id": "node-1", + "data": { + "plugin_id": "plugin-1", + "provider_name": "provider-1", + "datasource_name": "doc", + "datasource_parameters": {}, + }, + } + ] + } + ) + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) + + runtime = mocker.Mock() + + class _FailingIterator: + def __iter__(self): + return self + + def __next__(self): + raise RuntimeError("doc failed") + + runtime.get_online_document_pages.return_value = _FailingIterator() + runtime.datasource_provider_type.return_value = "online_document" + + mocker.patch("core.datasource.datasource_manager.DatasourceManager.get_datasource_runtime", return_value=runtime) + mocker.patch( + "services.rag_pipeline.rag_pipeline.DatasourceProviderService.get_datasource_credentials", return_value=None + ) + + events = list( + rag_pipeline_service.run_datasource_workflow_node( + pipeline=pipeline, + node_id="node-1", + user_inputs={}, + account=SimpleNamespace(id="u1"), + datasource_type="online_document", + is_published=True, + ) + ) + + assert len(events) == 2 + assert events[0]["event"] == "datasource_processing" + assert "doc failed" in events[1]["error"] + + +def test_run_datasource_node_preview_raises_for_stream_non_string(mocker, rag_pipeline_service) -> None: + from core.datasource.entities.datasource_entities import DatasourceMessage + + pipeline = SimpleNamespace(id="p1", tenant_id="t1") + workflow = SimpleNamespace( + graph_dict={ + "nodes": [ + { + "id": "node-1", + "data": { + "plugin_id": "plugin-1", + "provider_name": "provider-1", + "datasource_name": "doc", + "datasource_parameters": {}, + }, + } + ] + } + ) + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) + + runtime = mocker.Mock() + + def _bad_stream_generator(*args, **kwargs): + yield DatasourceMessage( + type=DatasourceMessage.MessageType.VARIABLE, + message=DatasourceMessage.VariableMessage(variable_name="content", variable_value=1, stream=True), + ) + + runtime.get_online_document_page_content.side_effect = _bad_stream_generator + runtime.datasource_provider_type.return_value = "online_document" + + mocker.patch("core.datasource.datasource_manager.DatasourceManager.get_datasource_runtime", return_value=runtime) + mocker.patch( + "services.rag_pipeline.rag_pipeline.DatasourceProviderService.get_datasource_credentials", return_value=None + ) + + with pytest.raises(RuntimeError, match="must be a string"): + rag_pipeline_service.run_datasource_node_preview( + pipeline=pipeline, + node_id="node-1", + user_inputs={}, + account=SimpleNamespace(id="u1"), + datasource_type="online_document", + is_published=True, + ) + + +def test_get_first_step_parameters_returns_empty_when_no_rag_variables(mocker, rag_pipeline_service) -> None: + workflow = SimpleNamespace( + graph_dict={"nodes": [{"id": "node-1", "data": {"datasource_parameters": {"url": {"value": "literal"}}}}]}, + rag_pipeline_variables=[], + ) + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) + + result = rag_pipeline_service.get_first_step_parameters(SimpleNamespace(), "node-1") + + assert result == [] + + +def test_get_second_step_parameters_filters_first_step_variables(mocker, rag_pipeline_service) -> None: + workflow = SimpleNamespace( + graph_dict={ + "nodes": [ + { + "id": "node-1", + "data": { + "datasource_parameters": { + "workspace": {"value": "{{#start.workspace#}}"}, + "bucket": {"value": ["input", "bucket"]}, + } + }, + } + ] + }, + rag_pipeline_variables=[ + {"variable": "workspace", "belong_to_node_id": "shared"}, + {"variable": "bucket", "belong_to_node_id": "shared"}, + {"variable": "keep", "belong_to_node_id": "shared"}, + {"variable": "other-node", "belong_to_node_id": "node-x"}, + ], + ) + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) + + result = rag_pipeline_service.get_second_step_parameters(SimpleNamespace(), "node-1") + + assert result == [{"variable": "keep", "belong_to_node_id": "shared"}] + + +def test_retry_error_document_raises_when_execution_log_not_found(mocker, rag_pipeline_service) -> None: + query = mocker.Mock() + query.where.return_value.first.return_value = None + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + + with pytest.raises(ValueError, match="Document pipeline execution log not found"): + rag_pipeline_service.retry_error_document( + SimpleNamespace(), SimpleNamespace(id="doc-1"), SimpleNamespace(id="u1") + ) + + +def test_get_datasource_plugins_raises_when_workflow_not_found(mocker, rag_pipeline_service) -> None: + dataset = SimpleNamespace(pipeline_id="p1") + pipeline = SimpleNamespace(id="p1", tenant_id="t1") + query = mocker.Mock() + query.where.return_value.first.side_effect = [dataset, pipeline] + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=None) + + with pytest.raises(ValueError, match="Pipeline or workflow not found"): + rag_pipeline_service.get_datasource_plugins("t1", "d1", True) + + +def test_handle_node_run_result_raises_when_no_terminal_event(mocker, rag_pipeline_service) -> None: + node_instance = SimpleNamespace( + workflow_id="wf-1", + node_type="start", + title="Start", + graph_runtime_state=SimpleNamespace(variable_pool=SimpleNamespace(get=lambda _: None)), + error_strategy=None, + ) + + def _event_generator(): + yield object() + + with pytest.raises(ValueError, match="Node run failed with no run result"): + rag_pipeline_service._handle_node_run_result( + getter=lambda: (node_instance, _event_generator()), + start_at=time.perf_counter(), + tenant_id="t1", + node_id="node-1", + ) + + +def test_handle_node_run_result_marks_document_error_for_published_invoke(mocker, rag_pipeline_service) -> None: + from graphon.enums import WorkflowNodeExecutionStatus + from graphon.graph_events import NodeRunFailedEvent + from graphon.node_events.base import NodeRunResult + + from core.app.entities.app_invoke_entities import InvokeFrom + + class FakeVariablePool: + def __init__(self): + self._values = { + ("sys", "invoke_from"): SimpleNamespace(value=InvokeFrom.PUBLISHED_PIPELINE), + ("sys", "document_id"): SimpleNamespace(value="doc-1"), + } + + def get(self, path): + return self._values.get(tuple(path)) + + node_instance = SimpleNamespace( + workflow_id="wf-1", + node_type="start", + title="Start", + graph_runtime_state=SimpleNamespace(variable_pool=FakeVariablePool()), + error_strategy=None, + ) + run_result = NodeRunResult( + status=WorkflowNodeExecutionStatus.FAILED, + error="boom", + error_type="runtime", + inputs={}, + outputs={}, + ) + + def _event_generator(): + yield NodeRunFailedEvent( + id="evt-1", + start_at=time.time(), + node_id="node-1", + node_type="start", + node_run_result=run_result, + error="boom", + route_node_id=None, + ) + + document = SimpleNamespace(indexing_status="waiting", error=None) + query = mocker.Mock() + query.where.return_value.first.return_value = document + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + add_mock = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.add") + commit_mock = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.commit") + + result = rag_pipeline_service._handle_node_run_result( + getter=lambda: (node_instance, _event_generator()), + start_at=time.perf_counter(), + tenant_id="t1", + node_id="node-1", + ) + + assert result.status == WorkflowNodeExecutionStatus.FAILED + assert document.indexing_status == "error" + assert document.error == "boom" + add_mock.assert_called_once_with(document) + commit_mock.assert_called_once() + + +def test_run_datasource_node_preview_raises_for_unsupported_provider(mocker, rag_pipeline_service) -> None: + pipeline = SimpleNamespace(id="p1", tenant_id="t1") + workflow = SimpleNamespace( + graph_dict={ + "nodes": [ + { + "id": "node-1", + "data": { + "plugin_id": "plugin-1", + "provider_name": "provider-1", + "datasource_name": "doc", + "datasource_parameters": {}, + }, + } + ] + } + ) + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) + runtime = mocker.Mock() + runtime.datasource_provider_type.return_value = "unsupported" + mocker.patch("core.datasource.datasource_manager.DatasourceManager.get_datasource_runtime", return_value=runtime) + mocker.patch( + "services.rag_pipeline.rag_pipeline.DatasourceProviderService.get_datasource_credentials", return_value=None + ) + + with pytest.raises(RuntimeError, match="Unsupported datasource provider"): + rag_pipeline_service.run_datasource_node_preview( + pipeline=pipeline, + node_id="node-1", + user_inputs={}, + account=SimpleNamespace(id="u1"), + datasource_type="website_crawl", + is_published=True, + ) + + +def test_publish_customized_pipeline_template_raises_for_missing_pipeline(mocker, rag_pipeline_service) -> None: + query = mocker.Mock() + query.where.return_value.first.return_value = None + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + + with pytest.raises(ValueError, match="Pipeline not found"): + rag_pipeline_service.publish_customized_pipeline_template("p1", {}) + + +def test_publish_customized_pipeline_template_raises_for_missing_workflow_id(mocker, rag_pipeline_service) -> None: + pipeline = SimpleNamespace(id="p1", tenant_id="t1", workflow_id=None) + query = mocker.Mock() + query.where.return_value.first.return_value = pipeline + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + + with pytest.raises(ValueError, match="Pipeline workflow not found"): + rag_pipeline_service.publish_customized_pipeline_template("p1", {"name": "template-name"}) + + +def test_get_pipeline_raises_when_dataset_missing(mocker, rag_pipeline_service) -> None: + query = mocker.Mock() + query.where.return_value.first.return_value = None + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + + with pytest.raises(ValueError, match="Dataset not found"): + rag_pipeline_service.get_pipeline("t1", "d1") + + +def test_get_pipeline_raises_when_pipeline_missing(mocker, rag_pipeline_service) -> None: + dataset = SimpleNamespace(pipeline_id="p1") + query = mocker.Mock() + query.where.return_value.first.side_effect = [dataset, None] + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + + with pytest.raises(ValueError, match="Pipeline not found"): + rag_pipeline_service.get_pipeline("t1", "d1") + + +def test_init_uses_default_sessionmaker_when_none(mocker) -> None: + default_session_maker = mocker.Mock() + mocker.patch("services.rag_pipeline.rag_pipeline.sessionmaker", return_value=default_session_maker) + mocker.patch("services.rag_pipeline.rag_pipeline.db", SimpleNamespace(engine=mocker.Mock())) + create_exec_repo = mocker.patch( + "services.rag_pipeline.rag_pipeline.DifyAPIRepositoryFactory.create_api_workflow_node_execution_repository" + ) + create_run_repo = mocker.patch( + "services.rag_pipeline.rag_pipeline.DifyAPIRepositoryFactory.create_api_workflow_run_repository" + ) + + RagPipelineService(session_maker=None) + + create_exec_repo.assert_called_once_with(default_session_maker) + create_run_repo.assert_called_once_with(default_session_maker) + + +def test_get_pipeline_templates_builtin_en_us_no_fallback(mocker) -> None: + mocker.patch("services.rag_pipeline.rag_pipeline.dify_config.HOSTED_FETCH_PIPELINE_TEMPLATES_MODE", "remote") + retrieval = mocker.Mock() + retrieval.get_pipeline_templates.return_value = {"pipeline_templates": []} + factory = mocker.patch("services.rag_pipeline.rag_pipeline.PipelineTemplateRetrievalFactory") + factory.get_pipeline_template_factory.return_value.return_value = retrieval + builtin = factory.get_built_in_pipeline_template_retrieval.return_value + + result = RagPipelineService.get_pipeline_templates(type="built-in", language="en-US") + + assert result == {"pipeline_templates": []} + builtin.fetch_pipeline_templates_from_builtin.assert_not_called() + + +def test_update_customized_pipeline_template_commits_when_name_empty(mocker) -> None: + template = SimpleNamespace(name="old", description="old", icon={}, updated_by=None) + query = mocker.Mock() + query.where.return_value.first.return_value = template + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + commit = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.commit") + mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1")) + + info = PipelineTemplateInfoEntity(name="", description="updated", icon_info=IconInfo(icon="i")) + result = RagPipelineService.update_customized_pipeline_template("tpl-1", info) + + assert result.description == "updated" + commit.assert_called_once() + + +def test_get_all_published_workflow_without_filters_has_no_more(rag_pipeline_service) -> None: + session = SimpleNamespace(scalars=lambda stmt: SimpleNamespace(all=lambda: ["wf1"])) + pipeline = SimpleNamespace(id="p1", workflow_id="wf-live") + + workflows, has_more = rag_pipeline_service.get_all_published_workflow( + session=session, + pipeline=pipeline, + page=1, + limit=2, + user_id=None, + named_only=False, + ) + + assert workflows == ["wf1"] + assert has_more is False + + +def test_publish_workflow_skips_dataset_update_for_non_knowledge_nodes(mocker, rag_pipeline_service) -> None: + draft = SimpleNamespace( + type="workflow", + graph={"nodes": [{"data": {"type": "start"}}]}, + features={}, + environment_variables=[], + conversation_variables=[], + rag_pipeline_variables=[], + ) + session = mocker.Mock() + session.scalar.return_value = draft + published = SimpleNamespace(graph_dict={"nodes": [{"data": {"type": "start"}}]}) + mocker.patch("services.rag_pipeline.rag_pipeline.select") + mocker.patch("services.rag_pipeline.rag_pipeline.Workflow.new", return_value=published) + + result = rag_pipeline_service.publish_workflow( + session=session, + pipeline=SimpleNamespace(id="p1", tenant_id="t1", is_published=False, retrieve_dataset=lambda session: None), + account=SimpleNamespace(id="u1"), + ) + + assert result is published + + +def test_get_default_block_config_returns_none_when_default_empty(mocker, rag_pipeline_service) -> None: + from graphon.enums import BuiltinNodeTypes + + node_cls = mocker.Mock() + node_cls.get_default_config.return_value = None + mocker.patch( + "services.rag_pipeline.rag_pipeline.get_node_type_classes_mapping", + return_value={BuiltinNodeTypes.START: {"1": node_cls}}, + ) + mocker.patch("services.rag_pipeline.rag_pipeline.LATEST_VERSION", "1") + + assert rag_pipeline_service.get_default_block_config("start") is None + + +def test_run_datasource_workflow_node_handles_variable_parameter_types(mocker, rag_pipeline_service) -> None: + from core.datasource.entities.datasource_entities import DatasourceProviderType + + workflow = SimpleNamespace( + graph_dict={ + "nodes": [ + { + "id": "node-1", + "data": { + "plugin_id": "p", + "provider_name": "provider", + "datasource_name": "crawl", + "datasource_parameters": { + "a": {"value": None}, + "b": {"value": "literal"}, + "c": {"value": ["input", "k"]}, + }, + }, + } + ] + } + ) + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) + runtime = mocker.Mock() + + def crawl_gen(**kwargs): + yield SimpleNamespace(result=SimpleNamespace(status="completed", total=1, completed=1, web_info_list=[])) + + runtime.get_website_crawl.side_effect = crawl_gen + runtime.datasource_provider_type.return_value = DatasourceProviderType.WEBSITE_CRAWL + mocker.patch("core.datasource.datasource_manager.DatasourceManager.get_datasource_runtime", return_value=runtime) + mocker.patch( + "services.rag_pipeline.rag_pipeline.DatasourceProviderService.get_datasource_credentials", return_value=None + ) + + events = list( + rag_pipeline_service.run_datasource_workflow_node( + pipeline=SimpleNamespace(id="p1", tenant_id="t1"), + node_id="node-1", + user_inputs={"k": "mapped"}, + account=SimpleNamespace(id="u1"), + datasource_type="website_crawl", + is_published=True, + ) + ) + + assert events + assert events[0]["data"] == [] + + +def test_run_datasource_workflow_node_online_drive_branch(mocker, rag_pipeline_service) -> None: + from core.datasource.entities.datasource_entities import DatasourceProviderType + + workflow = SimpleNamespace( + graph_dict={ + "nodes": [ + { + "id": "node-1", + "data": { + "plugin_id": "p", + "provider_name": "provider", + "datasource_name": "drive", + "datasource_parameters": {}, + }, + } + ] + } + ) + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) + runtime = mocker.Mock() + + def drive_gen(**kwargs): + yield SimpleNamespace(result={"items": [1]}) + + runtime.online_drive_browse_files.side_effect = drive_gen + runtime.datasource_provider_type.return_value = DatasourceProviderType.ONLINE_DRIVE + mocker.patch("core.datasource.datasource_manager.DatasourceManager.get_datasource_runtime", return_value=runtime) + mocker.patch( + "services.rag_pipeline.rag_pipeline.DatasourceProviderService.get_datasource_credentials", return_value=None + ) + + events = list( + rag_pipeline_service.run_datasource_workflow_node( + pipeline=SimpleNamespace(id="p1", tenant_id="t1"), + node_id="node-1", + user_inputs={}, + account=SimpleNamespace(id="u1"), + datasource_type="online_drive", + is_published=True, + ) + ) + + assert len(events) == 2 + assert events[1]["data"] == {"items": [1]} + + +def test_run_datasource_node_preview_not_published_uses_draft(mocker, rag_pipeline_service) -> None: + from core.datasource.entities.datasource_entities import DatasourceMessage + + workflow = SimpleNamespace( + graph_dict={ + "nodes": [ + { + "id": "n1", + "data": { + "plugin_id": "p", + "provider_name": "provider", + "datasource_name": "doc", + "datasource_parameters": {"workspace_id": {"value": "w"}}, + }, + } + ] + } + ) + get_draft = mocker.patch.object(rag_pipeline_service, "get_draft_workflow", return_value=workflow) + runtime = mocker.Mock() + + def doc_gen(**kwargs): + yield DatasourceMessage( + type=DatasourceMessage.MessageType.VARIABLE, + message=DatasourceMessage.VariableMessage(variable_name="x", variable_value="v", stream=False), + ) + + runtime.get_online_document_page_content.side_effect = doc_gen + mocker.patch("core.datasource.datasource_manager.DatasourceManager.get_datasource_runtime", return_value=runtime) + mocker.patch( + "services.rag_pipeline.rag_pipeline.DatasourceProviderService.get_datasource_credentials", return_value=None + ) + + result = rag_pipeline_service.run_datasource_node_preview( + pipeline=SimpleNamespace(id="p1", tenant_id="t1"), + node_id="n1", + user_inputs={}, + account=SimpleNamespace(id="u1"), + datasource_type="online_document", + is_published=False, + ) + + assert result == {"x": "v"} + get_draft.assert_called_once() + + +def test_run_free_workflow_node_delegates_to_handle_result(mocker, rag_pipeline_service) -> None: + expected = SimpleNamespace(id="exec-1") + handle = mocker.patch.object(rag_pipeline_service, "_handle_node_run_result", return_value=expected) + + result = rag_pipeline_service.run_free_workflow_node( + node_data={"type": "start"}, + tenant_id="t1", + user_id="u1", + node_id="n1", + user_inputs={}, + ) + + assert result is expected + handle.assert_called_once() + + +def test_publish_customized_pipeline_template_raises_when_workflow_missing(mocker, rag_pipeline_service) -> None: + pipeline = SimpleNamespace(id="p1", tenant_id="t1", workflow_id="wf-1") + query = mocker.Mock() + query.where.return_value.first.side_effect = [pipeline, None] + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + + with pytest.raises(ValueError, match="Workflow not found"): + rag_pipeline_service.publish_customized_pipeline_template("p1", {}) + + +def test_publish_customized_pipeline_template_raises_when_dataset_missing(mocker, rag_pipeline_service) -> None: + pipeline = SimpleNamespace(id="p1", tenant_id="t1", workflow_id="wf-1") + workflow = SimpleNamespace(id="wf-1") + query = mocker.Mock() + query.where.return_value.first.side_effect = [pipeline, workflow] + mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db") + mock_db.engine = mocker.Mock() + mock_db.session.query.return_value = query + session_ctx = mocker.MagicMock() + session_ctx.__enter__.return_value = SimpleNamespace() + session_ctx.__exit__.return_value = False + mocker.patch("services.rag_pipeline.rag_pipeline.Session", return_value=session_ctx) + pipeline.retrieve_dataset = lambda session: None + + with pytest.raises(ValueError, match="Dataset not found"): + rag_pipeline_service.publish_customized_pipeline_template("p1", {}) + + +def test_get_recommended_plugins_skips_manifest_when_missing(mocker, rag_pipeline_service) -> None: + plugin = SimpleNamespace(plugin_id="plugin-a") + query = mocker.Mock() + query.where.return_value = query + query.order_by.return_value.all.return_value = [plugin] + mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db") + mock_db.session.query.return_value = query + mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1")) + mocker.patch("services.rag_pipeline.rag_pipeline.BuiltinToolManageService.list_builtin_tools", return_value=[]) + mocker.patch("services.rag_pipeline.rag_pipeline.marketplace.batch_fetch_plugin_by_ids", return_value=[]) + + result = rag_pipeline_service.get_recommended_plugins("all") + + assert result["installed_recommended_plugins"] == [] + assert result["uninstalled_recommended_plugins"] == [] + + +def test_retry_error_document_raises_when_pipeline_missing(mocker, rag_pipeline_service) -> None: + exec_log = SimpleNamespace(pipeline_id="p1") + query = mocker.Mock() + query.where.return_value.first.side_effect = [exec_log, None] + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + + with pytest.raises(ValueError, match="Pipeline not found"): + rag_pipeline_service.retry_error_document( + SimpleNamespace(), SimpleNamespace(id="doc-1"), SimpleNamespace(id="u1") + ) + + +def test_retry_error_document_raises_when_workflow_missing(mocker, rag_pipeline_service) -> None: + exec_log = SimpleNamespace(pipeline_id="p1") + pipeline = SimpleNamespace(id="p1") + query = mocker.Mock() + query.where.return_value.first.side_effect = [exec_log, pipeline] + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=None) + + with pytest.raises(ValueError, match="Workflow not found"): + rag_pipeline_service.retry_error_document( + SimpleNamespace(), SimpleNamespace(id="doc-1"), SimpleNamespace(id="u1") + ) + + +def test_get_datasource_plugins_returns_empty_for_non_datasource_nodes(mocker, rag_pipeline_service) -> None: + dataset = SimpleNamespace(pipeline_id="p1") + pipeline = SimpleNamespace(id="p1", tenant_id="t1") + workflow = SimpleNamespace( + graph_dict={"nodes": [{"id": "n1", "data": {"type": "start"}}]}, rag_pipeline_variables=[] + ) + query = mocker.Mock() + query.where.return_value.first.side_effect = [dataset, pipeline] + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) + + assert rag_pipeline_service.get_datasource_plugins("t1", "d1", True) == [] + + +def test_publish_workflow_raises_when_knowledge_index_dataset_missing(mocker, rag_pipeline_service) -> None: + draft = SimpleNamespace( + type="workflow", + graph={"nodes": [{"data": {"type": "knowledge-index"}}]}, + features={}, + environment_variables=[], + conversation_variables=[], + rag_pipeline_variables=[], + ) + session = mocker.Mock() + session.scalar.return_value = draft + mocker.patch("services.rag_pipeline.rag_pipeline.select") + mocker.patch( + "services.rag_pipeline.rag_pipeline.Workflow.new", + return_value=SimpleNamespace(graph_dict={"nodes": [{"data": {"type": "knowledge-index"}}]}), + ) + mocker.patch("services.rag_pipeline.rag_pipeline.KnowledgeConfiguration.model_validate", return_value=mocker.Mock()) + pipeline = SimpleNamespace(id="p1", tenant_id="t1", is_published=False, retrieve_dataset=lambda session: None) + + with pytest.raises(ValueError, match="Dataset not found"): + rag_pipeline_service.publish_workflow(session=session, pipeline=pipeline, account=SimpleNamespace(id="u1")) + + +def test_run_datasource_node_preview_raises_when_workflow_missing(mocker, rag_pipeline_service) -> None: + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=None) + + with pytest.raises(RuntimeError, match="Workflow not initialized"): + rag_pipeline_service.run_datasource_node_preview( + pipeline=SimpleNamespace(id="p1", tenant_id="t1"), + node_id="n1", + user_inputs={}, + account=SimpleNamespace(id="u1"), + datasource_type="online_document", + is_published=True, + ) + + +def test_run_datasource_node_preview_raises_when_node_missing(mocker, rag_pipeline_service) -> None: + mocker.patch.object( + rag_pipeline_service, "get_published_workflow", return_value=SimpleNamespace(graph_dict={"nodes": []}) + ) + + with pytest.raises(RuntimeError, match="Datasource node data not found"): + rag_pipeline_service.run_datasource_node_preview( + pipeline=SimpleNamespace(id="p1", tenant_id="t1"), + node_id="missing", + user_inputs={}, + account=SimpleNamespace(id="u1"), + datasource_type="online_document", + is_published=True, + ) + + +def test_run_datasource_node_preview_keeps_existing_user_input(mocker, rag_pipeline_service) -> None: + from core.datasource.entities.datasource_entities import DatasourceMessage + + workflow = SimpleNamespace( + graph_dict={ + "nodes": [ + { + "id": "n1", + "data": { + "plugin_id": "p", + "provider_name": "provider", + "datasource_name": "doc", + "datasource_parameters": {"workspace_id": {"value": "default"}}, + }, + } + ] + } + ) + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) + runtime = mocker.Mock() + + def gen(**kwargs): + request = kwargs["datasource_parameters"] + assert request.workspace_id == "existing" + yield DatasourceMessage( + type=DatasourceMessage.MessageType.VARIABLE, + message=DatasourceMessage.VariableMessage(variable_name="ok", variable_value="1", stream=False), + ) + + runtime.get_online_document_page_content.side_effect = gen + mocker.patch("core.datasource.datasource_manager.DatasourceManager.get_datasource_runtime", return_value=runtime) + mocker.patch( + "services.rag_pipeline.rag_pipeline.DatasourceProviderService.get_datasource_credentials", return_value=None + ) + + result = rag_pipeline_service.run_datasource_node_preview( + pipeline=SimpleNamespace(id="p1", tenant_id="t1"), + node_id="n1", + user_inputs={"workspace_id": "existing"}, + account=SimpleNamespace(id="u1"), + datasource_type="online_document", + is_published=True, + ) + assert result == {"ok": "1"} + + +def test_run_datasource_node_preview_ignores_non_variable_messages(mocker, rag_pipeline_service) -> None: + workflow = SimpleNamespace( + graph_dict={ + "nodes": [ + { + "id": "n1", + "data": { + "plugin_id": "p", + "provider_name": "provider", + "datasource_name": "doc", + "datasource_parameters": {}, + }, + } + ] + } + ) + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) + runtime = mocker.Mock() + + def gen(**kwargs): + yield SimpleNamespace(type="log", message=None) + + runtime.get_online_document_page_content.side_effect = gen + mocker.patch("core.datasource.datasource_manager.DatasourceManager.get_datasource_runtime", return_value=runtime) + mocker.patch( + "services.rag_pipeline.rag_pipeline.DatasourceProviderService.get_datasource_credentials", return_value=None + ) + + result = rag_pipeline_service.run_datasource_node_preview( + pipeline=SimpleNamespace(id="p1", tenant_id="t1"), + node_id="n1", + user_inputs={}, + account=SimpleNamespace(id="u1"), + datasource_type="online_document", + is_published=True, + ) + assert result == {} + + +def test_set_datasource_variables_raises_when_workflow_missing(mocker, rag_pipeline_service) -> None: + mocker.patch.object(rag_pipeline_service, "get_draft_workflow", return_value=None) + + with pytest.raises(ValueError, match="Workflow not initialized"): + rag_pipeline_service.set_datasource_variables( + SimpleNamespace(id="p1", tenant_id="t1"), + {"start_node_id": "n1"}, + SimpleNamespace(id="u1"), + ) + + +def test_get_datasource_plugins_handles_empty_datasource_data_and_non_published(mocker, rag_pipeline_service) -> None: + dataset = SimpleNamespace(pipeline_id="p1") + pipeline = SimpleNamespace(id="p1", tenant_id="t1") + workflow = SimpleNamespace( + graph_dict={"nodes": [{"id": "n1", "data": {"type": "datasource", "datasource_parameters": {}}}]}, + rag_pipeline_variables=[{"variable": "v1", "belong_to_node_id": "shared"}], + ) + query = mocker.Mock() + query.where.return_value.first.side_effect = [dataset, pipeline] + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + mocker.patch.object(rag_pipeline_service, "get_draft_workflow", return_value=workflow) + mocker.patch( + "services.rag_pipeline.rag_pipeline.DatasourceProviderService.list_datasource_credentials", return_value=[] + ) + + result = rag_pipeline_service.get_datasource_plugins("t1", "d1", False) + + assert len(result) == 1 + + +def test_get_datasource_plugins_extracts_user_inputs_and_credentials(mocker, rag_pipeline_service) -> None: + dataset = SimpleNamespace(pipeline_id="p1") + pipeline = SimpleNamespace(id="p1", tenant_id="t1") + workflow = SimpleNamespace( + graph_dict={ + "nodes": [ + { + "id": "n1", + "data": { + "type": "datasource", + "plugin_id": "plugin-1", + "provider_name": "provider", + "provider_type": "online_document", + "title": "Datasource", + "datasource_parameters": { + "a": {"value": "{{#start.v1#}}"}, + "b": {"value": ["x", "v2"]}, + }, + }, + } + ] + }, + rag_pipeline_variables=[ + {"variable": "v1", "belong_to_node_id": "shared"}, + {"variable": "v2", "belong_to_node_id": "shared"}, + {"variable": "v3", "belong_to_node_id": "shared"}, + ], + ) + query = mocker.Mock() + query.where.return_value.first.side_effect = [dataset, pipeline] + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) + mocker.patch( + "services.rag_pipeline.rag_pipeline.DatasourceProviderService.list_datasource_credentials", + return_value=[{"id": "c1", "name": "Cred", "type": "api", "is_default": True}], + ) + + result = rag_pipeline_service.get_datasource_plugins("t1", "d1", True) + + assert len(result) == 1 + assert len(result[0]["user_input_variables"]) == 2 + assert result[0]["credentials"][0]["id"] == "c1" + + +def test_get_pipeline_returns_pipeline_when_found(mocker, rag_pipeline_service) -> None: + dataset = SimpleNamespace(pipeline_id="p1") + pipeline = SimpleNamespace(id="p1") + query = mocker.Mock() + query.where.return_value.first.side_effect = [dataset, pipeline] + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query) + + result = rag_pipeline_service.get_pipeline("t1", "d1") + + assert result is pipeline diff --git a/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_task_proxy.py b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_task_proxy.py new file mode 100644 index 00000000000..1a2d0622089 --- /dev/null +++ b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_task_proxy.py @@ -0,0 +1,159 @@ +from types import SimpleNamespace +from unittest.mock import Mock + +import pytest + +from services.rag_pipeline.rag_pipeline_task_proxy import RagPipelineTaskProxy + + +@pytest.fixture +def proxy(mocker): + """Create a RagPipelineTaskProxy with mocked dependencies.""" + mocker.patch("services.rag_pipeline.rag_pipeline_task_proxy.TenantIsolatedTaskQueue") + entity = Mock() + entity.model_dump.return_value = {"doc": "data"} + return RagPipelineTaskProxy( + dataset_tenant_id="tenant-1", + user_id="user-1", + rag_pipeline_invoke_entities=[entity], + ) + + +# --- delay --- + + +def test_delay_with_empty_entities_logs_warning_and_returns(mocker) -> None: + mocker.patch("services.rag_pipeline.rag_pipeline_task_proxy.TenantIsolatedTaskQueue") + proxy = RagPipelineTaskProxy( + dataset_tenant_id="tenant-1", + user_id="user-1", + rag_pipeline_invoke_entities=[], + ) + dispatch_mock = mocker.patch.object(proxy, "_dispatch") + + proxy.delay() + + dispatch_mock.assert_not_called() + + +def test_delay_with_entities_calls_dispatch(mocker, proxy) -> None: + dispatch_mock = mocker.patch.object(proxy, "_dispatch") + + proxy.delay() + + dispatch_mock.assert_called_once() + + +# --- _dispatch --- + + +def test_dispatch_billing_sandbox_uses_default_tenant_queue(mocker, proxy) -> None: + upload_mock = mocker.patch.object(proxy, "_upload_invoke_entities", return_value="file-1") + send_mock = mocker.patch.object(proxy, "_send_to_default_tenant_queue") + + from enums.cloud_plan import CloudPlan + + features = SimpleNamespace( + billing=SimpleNamespace(enabled=True, subscription=SimpleNamespace(plan=CloudPlan.SANDBOX)) + ) + mocker.patch.object(type(proxy), "features", new_callable=lambda: property(lambda self: features)) + + proxy._dispatch() + + upload_mock.assert_called_once() + send_mock.assert_called_once_with("file-1") + + +def test_dispatch_billing_non_sandbox_uses_priority_tenant_queue(mocker, proxy) -> None: + upload_mock = mocker.patch.object(proxy, "_upload_invoke_entities", return_value="file-1") + send_mock = mocker.patch.object(proxy, "_send_to_priority_tenant_queue") + + from enums.cloud_plan import CloudPlan + + features = SimpleNamespace( + billing=SimpleNamespace(enabled=True, subscription=SimpleNamespace(plan=CloudPlan.PROFESSIONAL)) + ) + mocker.patch.object(type(proxy), "features", new_callable=lambda: property(lambda self: features)) + + proxy._dispatch() + + upload_mock.assert_called_once() + send_mock.assert_called_once_with("file-1") + + +def test_dispatch_no_billing_uses_priority_direct_queue(mocker, proxy) -> None: + upload_mock = mocker.patch.object(proxy, "_upload_invoke_entities", return_value="file-1") + send_mock = mocker.patch.object(proxy, "_send_to_priority_direct_queue") + + features = SimpleNamespace(billing=SimpleNamespace(enabled=False, subscription=SimpleNamespace(plan="free"))) + mocker.patch.object(type(proxy), "features", new_callable=lambda: property(lambda self: features)) + + proxy._dispatch() + + upload_mock.assert_called_once() + send_mock.assert_called_once_with("file-1") + + +def test_dispatch_raises_on_empty_upload_file_id(mocker, proxy) -> None: + mocker.patch.object(proxy, "_upload_invoke_entities", return_value="") + + features = SimpleNamespace(billing=SimpleNamespace(enabled=False, subscription=SimpleNamespace(plan="free"))) + mocker.patch.object(type(proxy), "features", new_callable=lambda: property(lambda self: features)) + + with pytest.raises(ValueError, match="upload_file_id is empty"): + proxy._dispatch() + + +# --- _send_to_direct_queue --- + + +def test_send_to_direct_queue_calls_task_func_delay(mocker, proxy) -> None: + task_func = Mock() + + proxy._send_to_direct_queue("file-1", task_func) + + task_func.delay.assert_called_once_with( + rag_pipeline_invoke_entities_file_id="file-1", + tenant_id="tenant-1", + ) + + +# --- _send_to_tenant_queue --- + + +def test_send_to_tenant_queue_pushes_when_task_key_exists(mocker, proxy) -> None: + proxy._tenant_isolated_task_queue.get_task_key.return_value = "existing-key" + task_func = Mock() + + proxy._send_to_tenant_queue("file-1", task_func) + + proxy._tenant_isolated_task_queue.push_tasks.assert_called_once_with(["file-1"]) + task_func.delay.assert_not_called() + + +def test_send_to_tenant_queue_sets_waiting_time_and_calls_delay(mocker, proxy) -> None: + proxy._tenant_isolated_task_queue.get_task_key.return_value = None + task_func = Mock() + + proxy._send_to_tenant_queue("file-1", task_func) + + proxy._tenant_isolated_task_queue.set_task_waiting_time.assert_called_once() + task_func.delay.assert_called_once_with( + rag_pipeline_invoke_entities_file_id="file-1", + tenant_id="tenant-1", + ) + + +# --- _upload_invoke_entities --- + + +def test_upload_invoke_entities_returns_file_id(mocker, proxy) -> None: + upload_file = SimpleNamespace(id="uploaded-file-1") + file_service_cls = mocker.patch("services.rag_pipeline.rag_pipeline_task_proxy.FileService") + file_service_cls.return_value.upload_text.return_value = upload_file + mocker.patch("services.rag_pipeline.rag_pipeline_task_proxy.db", mocker.Mock(engine="fake-engine")) + + result = proxy._upload_invoke_entities() + + assert result == "uploaded-file-1" + file_service_cls.return_value.upload_text.assert_called_once() diff --git a/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_transform_service.py b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_transform_service.py new file mode 100644 index 00000000000..82e5e973c14 --- /dev/null +++ b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_transform_service.py @@ -0,0 +1,516 @@ +from datetime import UTC, datetime +from types import SimpleNamespace +from typing import cast + +import pytest + +from models.dataset import Dataset +from services.entities.knowledge_entities.rag_pipeline_entities import KnowledgeConfiguration +from services.rag_pipeline.rag_pipeline_transform_service import RagPipelineTransformService + + +@pytest.mark.parametrize( + ("doc_form", "datasource_type", "indexing_technique"), + [ + ("text_model", "upload_file", "high_quality"), + ("text_model", "upload_file", "economy"), + ("text_model", "notion_import", "high_quality"), + ("text_model", "notion_import", "economy"), + ("text_model", "website_crawl", "high_quality"), + ("text_model", "website_crawl", "economy"), + ("hierarchical_model", "upload_file", None), + ("hierarchical_model", "notion_import", None), + ("hierarchical_model", "website_crawl", None), + ], +) +def test_get_transform_yaml_returns_workflow(doc_form: str, datasource_type: str, indexing_technique: str | None): + service = RagPipelineTransformService() + + result = service._get_transform_yaml(doc_form, datasource_type, indexing_technique) + + assert isinstance(result, dict) + assert "workflow" in result + + +def test_get_transform_yaml_raises_for_unsupported_doc_form() -> None: + service = RagPipelineTransformService() + + with pytest.raises(ValueError, match="Unsupported doc form"): + service._get_transform_yaml("unknown", "upload_file", "high_quality") + + +@pytest.mark.parametrize("doc_form", ["text_model", "hierarchical_model"]) +def test_get_transform_yaml_raises_for_unsupported_datasource_type(doc_form: str) -> None: + service = RagPipelineTransformService() + + with pytest.raises(ValueError, match="Unsupported datasource type"): + service._get_transform_yaml(doc_form, "unsupported", "high_quality") + + +def test_deal_file_extensions_filters_and_normalizes_extensions() -> None: + service = RagPipelineTransformService() + node = {"data": {"fileExtensions": ["pdf", "TXT", "exe"]}} + + result = service._deal_file_extensions(node) + + assert result["data"]["fileExtensions"] == ["pdf", "txt"] + + +def test_deal_file_extensions_returns_original_when_empty() -> None: + service = RagPipelineTransformService() + node = {"data": {"fileExtensions": []}} + + result = service._deal_file_extensions(node) + + assert result is node + + +def test_deal_dependencies_installs_missing_marketplace_plugins(mocker) -> None: + service = RagPipelineTransformService() + + installer_cls = mocker.patch("services.rag_pipeline.rag_pipeline_transform_service.PluginInstaller") + installer_cls.return_value.list_plugins.return_value = [SimpleNamespace(plugin_id="installed-plugin")] + + migration_cls = mocker.patch("services.rag_pipeline.rag_pipeline_transform_service.PluginMigration") + migration_cls.return_value._fetch_plugin_unique_identifier.return_value = "missing-plugin:1.0.0" + + install_mock = mocker.patch( + "services.rag_pipeline.rag_pipeline_transform_service.PluginService.install_from_marketplace_pkg" + ) + + pipeline_yaml = { + "dependencies": [ + {"type": "marketplace", "value": {"plugin_unique_identifier": "installed-plugin:0.1.0"}}, + {"type": "marketplace", "value": {"plugin_unique_identifier": "missing-plugin:0.1.0"}}, + ] + } + + service._deal_dependencies(pipeline_yaml, "tenant-1") + + install_mock.assert_called_once_with("tenant-1", ["missing-plugin:1.0.0"]) + + +def test_transform_to_empty_pipeline_updates_dataset_and_commits(mocker) -> None: + service = RagPipelineTransformService() + mocker.patch( + "services.rag_pipeline.rag_pipeline_transform_service.current_user", + SimpleNamespace(id="user-1"), + ) + + class FakePipeline: + def __init__(self, **kwargs): + self.id = "pipeline-1" + self.tenant_id = kwargs["tenant_id"] + self.name = kwargs["name"] + self.description = kwargs["description"] + self.created_by = kwargs["created_by"] + + mocker.patch("services.rag_pipeline.rag_pipeline_transform_service.Pipeline", FakePipeline) + session_mock = mocker.Mock() + add_mock = session_mock.add + flush_mock = session_mock.flush + commit_mock = session_mock.commit + mocker.patch( + "services.rag_pipeline.rag_pipeline_transform_service.db", + new=SimpleNamespace(session=session_mock), + ) + + dataset = SimpleNamespace( + id="dataset-1", + tenant_id="tenant-1", + name="Dataset", + description="desc", + pipeline_id=None, + runtime_mode="general", + updated_by=None, + updated_at=None, + ) + + result = service._transform_to_empty_pipeline(cast(Dataset, dataset)) + + assert result == {"pipeline_id": "pipeline-1", "dataset_id": "dataset-1", "status": "success"} + assert dataset.pipeline_id == "pipeline-1" + assert dataset.runtime_mode == "rag_pipeline" + assert dataset.updated_by == "user-1" + add_mock.assert_called() + flush_mock.assert_called_once() + commit_mock.assert_called_once() + + +# --- transform_dataset --- + + +def test_transform_dataset_returns_early_when_pipeline_exists(mocker) -> None: + service = RagPipelineTransformService() + dataset = SimpleNamespace( + id="d1", + pipeline_id="p1", + runtime_mode="rag_pipeline", + ) + session_mock = mocker.Mock() + session_mock.get.return_value = dataset + mocker.patch( + "services.rag_pipeline.rag_pipeline_transform_service.db", + new=SimpleNamespace(session=session_mock), + ) + + result = service.transform_dataset("d1") + + assert result == {"pipeline_id": "p1", "dataset_id": "d1", "status": "success"} + + +def test_transform_dataset_raises_for_dataset_not_found(mocker) -> None: + service = RagPipelineTransformService() + session_mock = mocker.Mock() + session_mock.get.return_value = None + mocker.patch( + "services.rag_pipeline.rag_pipeline_transform_service.db", + new=SimpleNamespace(session=session_mock), + ) + + with pytest.raises(ValueError, match="Dataset not found"): + service.transform_dataset("d1") + + +def test_transform_dataset_raises_for_external_dataset(mocker) -> None: + service = RagPipelineTransformService() + dataset = SimpleNamespace( + id="d1", + pipeline_id=None, + runtime_mode=None, + provider="external", + ) + session_mock = mocker.Mock() + session_mock.get.return_value = dataset + mocker.patch( + "services.rag_pipeline.rag_pipeline_transform_service.db", + new=SimpleNamespace(session=session_mock), + ) + + with pytest.raises(ValueError, match="External dataset is not supported"): + service.transform_dataset("d1") + + +def test_transform_dataset_calls_empty_pipeline_when_no_datasource(mocker) -> None: + service = RagPipelineTransformService() + dataset = SimpleNamespace( + id="d1", + pipeline_id=None, + runtime_mode=None, + provider="vendor", + data_source_type=None, + indexing_technique=None, + ) + session_mock = mocker.Mock() + session_mock.get.return_value = dataset + mocker.patch( + "services.rag_pipeline.rag_pipeline_transform_service.db", + new=SimpleNamespace(session=session_mock), + ) + + empty_result = {"pipeline_id": "p-empty", "dataset_id": "d1", "status": "success"} + mocker.patch.object(service, "_transform_to_empty_pipeline", return_value=empty_result) + + result = service.transform_dataset("d1") + + assert result == empty_result + + +def test_transform_dataset_calls_empty_pipeline_when_no_doc_form(mocker) -> None: + service = RagPipelineTransformService() + dataset = SimpleNamespace( + id="d1", + pipeline_id=None, + runtime_mode=None, + provider="vendor", + data_source_type="upload_file", + indexing_technique="high_quality", + doc_form=None, + ) + session_mock = mocker.Mock() + session_mock.get.return_value = dataset + mocker.patch( + "services.rag_pipeline.rag_pipeline_transform_service.db", + new=SimpleNamespace(session=session_mock), + ) + + empty_result = {"pipeline_id": "p-empty", "dataset_id": "d1", "status": "success"} + mocker.patch.object(service, "_transform_to_empty_pipeline", return_value=empty_result) + + result = service.transform_dataset("d1") + + assert result == empty_result + + +# --- _deal_knowledge_index --- + + +def test_deal_knowledge_index_high_quality_sets_embedding(mocker) -> None: + service = RagPipelineTransformService() + dataset = cast( + Dataset, + SimpleNamespace( + embedding_model="text-embedding-ada-002", + embedding_model_provider="openai", + retrieval_model=None, + summary_index_setting=None, + ), + ) + node = { + "data": { + "type": "knowledge-index", + "indexing_technique": "high_quality", + "embedding_model": "", + "embedding_model_provider": "", + "retrieval_model": { + "search_method": "semantic_search", + "reranking_enable": False, + "reranking_mode": None, + "reranking_model": None, + "weights": None, + "top_k": 3, + "score_threshold_enabled": False, + "score_threshold": None, + }, + "chunk_structure": "text_model", + "keyword_number": None, + "summary_index_setting": None, + } + } + + # Create KnowledgeConfiguration from node data + knowledge_configuration = KnowledgeConfiguration.model_validate(node.get("data", {})) + retrieval_model = knowledge_configuration.retrieval_model + + result = service._deal_knowledge_index( + knowledge_configuration, + dataset, + "high_quality", + retrieval_model, + node, + ) + + assert result["data"]["embedding_model"] == "text-embedding-ada-002" + assert result["data"]["embedding_model_provider"] == "openai" + + +# --- _deal_document_data --- + + +def test_deal_document_data_notion(mocker) -> None: + service = RagPipelineTransformService() + dataset = SimpleNamespace(id="d1", pipeline_id="p1") + doc = SimpleNamespace( + id="doc1", + dataset_id="d1", + data_source_type="notion_import", + data_source_info_dict={ + "notion_workspace_id": "ws1", + "notion_page_id": "page1", + "notion_page_icon": "icon1", + "type": "page", + "last_edited_time": 12345, + }, + name="Notion Doc", + created_by="u1", + created_at=datetime.now(UTC).replace(tzinfo=None), + data_source_info=None, + ) + + scalars_mock = mocker.Mock() + scalars_mock.all.return_value = [doc] + session_mock = mocker.Mock() + session_mock.scalars.return_value = scalars_mock + add_mock = session_mock.add + mocker.patch( + "services.rag_pipeline.rag_pipeline_transform_service.db", + new=SimpleNamespace(session=session_mock), + ) + + service._deal_document_data(cast(Dataset, dataset)) + + assert doc.data_source_type == "online_document" + assert "page1" in doc.data_source_info + assert add_mock.call_count == 2 # document + log + + +@pytest.mark.parametrize(("provider", "node_id"), [("firecrawl", "1752565402678"), ("jinareader", "1752491761974")]) +def test_deal_document_data_website(mocker, provider: str, node_id: str) -> None: + service = RagPipelineTransformService() + dataset = SimpleNamespace(id="d1", pipeline_id="p1") + doc = SimpleNamespace( + id="doc1", + dataset_id="d1", + data_source_type="website_crawl", + data_source_info_dict={ + "url": "https://example.com", + "provider": provider, + }, + name="Web Doc", + created_by="u1", + created_at=datetime.now(UTC).replace(tzinfo=None), + data_source_info=None, + ) + + scalars_mock = mocker.Mock() + scalars_mock.all.return_value = [doc] + session_mock = mocker.Mock() + session_mock.scalars.return_value = scalars_mock + add_mock = session_mock.add + mocker.patch( + "services.rag_pipeline.rag_pipeline_transform_service.db", + new=SimpleNamespace(session=session_mock), + ) + + service._deal_document_data(cast(Dataset, dataset)) + + assert doc.data_source_type == "website_crawl" + assert "example.com" in doc.data_source_info + # Check if correct node id was used in log + log = add_mock.call_args_list[1][0][0] + assert log.datasource_node_id == node_id + + +# --- transform_dataset complex flow --- + + +def test_transform_dataset_full_flow(mocker) -> None: + service = RagPipelineTransformService() + dataset = SimpleNamespace( + id="d1", + tenant_id="t1", + name="D", + description="d", + pipeline_id=None, + runtime_mode=None, + provider="vendor", + data_source_type="upload_file", + indexing_technique="high_quality", + doc_form="text_model", + retrieval_model={"search_method": "semantic_search", "top_k": 3}, + embedding_model="m1", + embedding_model_provider="p1", + summary_index_setting=None, + chunk_structure=None, + ) + + session_mock = mocker.Mock() + session_mock.get.return_value = dataset + mocker.patch( + "services.rag_pipeline.rag_pipeline_transform_service.db", + new=SimpleNamespace(session=session_mock), + ) + + mocker.patch.object(service, "_deal_dependencies") + mocker.patch.object(service, "_deal_document_data") + session_mock.commit = mocker.Mock() + + # Mock current_user to have the same tenant_id as dataset + mock_current_user = SimpleNamespace(current_tenant_id="t1") + mocker.patch("services.rag_pipeline.rag_pipeline_transform_service.current_user", mock_current_user) + + pipeline = SimpleNamespace(id="p-new") + mocker.patch.object(service, "_create_pipeline", return_value=pipeline) + + result = service.transform_dataset("d1") + + assert result["pipeline_id"] == "p-new" + assert dataset.runtime_mode == "rag_pipeline" + assert dataset.chunk_structure == "text_model" + + +def test_transform_dataset_raises_for_unsupported_doc_form_after_pipeline_create(mocker) -> None: + service = RagPipelineTransformService() + dataset = SimpleNamespace( + id="d1", + tenant_id="t1", + name="D", + description="d", + pipeline_id=None, + runtime_mode=None, + provider="vendor", + data_source_type="upload_file", + indexing_technique="high_quality", + doc_form="unsupported", + retrieval_model=None, + ) + session_mock = mocker.Mock() + session_mock.get.return_value = dataset + mocker.patch( + "services.rag_pipeline.rag_pipeline_transform_service.db", + new=SimpleNamespace(session=session_mock), + ) + mocker.patch.object(service, "_get_transform_yaml", return_value={"workflow": {"graph": {"nodes": []}}}) + mocker.patch.object(service, "_deal_dependencies") + mocker.patch.object(service, "_create_pipeline", return_value=SimpleNamespace(id="p-new")) + + with pytest.raises(ValueError, match="Unsupported doc form"): + service.transform_dataset("d1") + + +def test_transform_dataset_raises_when_transform_yaml_missing_workflow(mocker) -> None: + service = RagPipelineTransformService() + dataset = SimpleNamespace( + id="d1", + tenant_id="t1", + name="D", + description="d", + pipeline_id=None, + runtime_mode=None, + provider="vendor", + data_source_type="upload_file", + indexing_technique="high_quality", + doc_form="text_model", + retrieval_model=None, + ) + session_mock = mocker.Mock() + session_mock.get.return_value = dataset + mocker.patch( + "services.rag_pipeline.rag_pipeline_transform_service.db", + new=SimpleNamespace(session=session_mock), + ) + mocker.patch.object(service, "_get_transform_yaml", return_value={}) + mocker.patch.object(service, "_deal_dependencies") + + with pytest.raises(ValueError, match="Missing workflow data for rag pipeline"): + service.transform_dataset("d1") + + +def test_create_pipeline_raises_when_workflow_data_missing() -> None: + service = RagPipelineTransformService() + + with pytest.raises(ValueError, match="Missing workflow data for rag pipeline"): + service._create_pipeline({"rag_pipeline": {"name": "N"}}) + + +def test_deal_document_data_upload_file_with_existing_file(mocker) -> None: + service = RagPipelineTransformService() + dataset = SimpleNamespace(id="d1", pipeline_id="p1") + document = SimpleNamespace( + id="doc-1", + dataset_id="d1", + data_source_type="upload_file", + data_source_info_dict={"upload_file_id": "file-1"}, + name="Doc", + created_by="u1", + created_at=datetime.now(UTC).replace(tzinfo=None), + data_source_info=None, + ) + upload_file = SimpleNamespace(name="f.txt", size=10, extension="txt", mime_type="text/plain") + + scalars_mock = mocker.Mock() + scalars_mock.all.return_value = [document] + session_mock = mocker.Mock() + session_mock.scalars.return_value = scalars_mock + session_mock.get.return_value = upload_file + add_mock = session_mock.add + mocker.patch( + "services.rag_pipeline.rag_pipeline_transform_service.db", + new=SimpleNamespace(session=session_mock), + ) + + service._deal_document_data(cast(Dataset, dataset)) + + assert document.data_source_type == "local_file" + assert "real_file_id" in document.data_source_info + assert add_mock.call_count >= 2 From 9a8c853a2e705e26f151cd35c1b1cb18d463d068 Mon Sep 17 00:00:00 2001 From: Akash Kumar Date: Thu, 2 Apr 2026 12:20:58 +0530 Subject: [PATCH 002/549] test: added unit test for remaining files in core helper folder (#33288) Co-authored-by: rajatagarwal-oss Co-authored-by: sahil-infocusp <73810410+sahil-infocusp@users.noreply.github.com> --- .../jinja2/test_jinja2_formatter.py | 24 +++ .../code_executor/test_code_executor.py | 110 ++++++++++++ .../code_executor/test_code_node_provider.py | 29 ++++ .../test_template_transformer.py | 81 +++++++++ .../core/helper/test_credential_utils.py | 138 +++++++++++++++ .../unit_tests/core/helper/test_download.py | 53 ++++++ .../core/helper/test_http_client_pooling.py | 41 +++++ .../core/helper/test_marketplace.py | 110 ++++++++++++ .../unit_tests/core/helper/test_moderation.py | 158 ++++++++++++++++++ .../core/helper/test_name_generator.py | 33 ++++ .../core/helper/test_tool_parameter_cache.py | 71 ++++++++ 11 files changed, 848 insertions(+) create mode 100644 api/tests/unit_tests/core/helper/code_executor/jinja2/test_jinja2_formatter.py create mode 100644 api/tests/unit_tests/core/helper/code_executor/test_code_executor.py create mode 100644 api/tests/unit_tests/core/helper/code_executor/test_code_node_provider.py create mode 100644 api/tests/unit_tests/core/helper/code_executor/test_template_transformer.py create mode 100644 api/tests/unit_tests/core/helper/test_credential_utils.py create mode 100644 api/tests/unit_tests/core/helper/test_download.py create mode 100644 api/tests/unit_tests/core/helper/test_http_client_pooling.py create mode 100644 api/tests/unit_tests/core/helper/test_marketplace.py create mode 100644 api/tests/unit_tests/core/helper/test_moderation.py create mode 100644 api/tests/unit_tests/core/helper/test_name_generator.py create mode 100644 api/tests/unit_tests/core/helper/test_tool_parameter_cache.py diff --git a/api/tests/unit_tests/core/helper/code_executor/jinja2/test_jinja2_formatter.py b/api/tests/unit_tests/core/helper/code_executor/jinja2/test_jinja2_formatter.py new file mode 100644 index 00000000000..60002a757d5 --- /dev/null +++ b/api/tests/unit_tests/core/helper/code_executor/jinja2/test_jinja2_formatter.py @@ -0,0 +1,24 @@ +from pytest_mock import MockerFixture + +from core.helper.code_executor.jinja2.jinja2_formatter import Jinja2Formatter + + +def test_format_returns_result_value_as_string(mocker: MockerFixture) -> None: + execute_mock = mocker.patch( + "core.helper.code_executor.jinja2.jinja2_formatter.CodeExecutor.execute_workflow_code_template", + return_value={"result": 123}, + ) + + formatted = Jinja2Formatter.format("Hello {{ name }}", {"name": "Dify"}) + + assert formatted == "123" + execute_mock.assert_called_once() + + +def test_format_returns_empty_string_when_result_missing(mocker: MockerFixture) -> None: + mocker.patch( + "core.helper.code_executor.jinja2.jinja2_formatter.CodeExecutor.execute_workflow_code_template", + return_value={}, + ) + + assert Jinja2Formatter.format("Hello", {"name": "Dify"}) == "" diff --git a/api/tests/unit_tests/core/helper/code_executor/test_code_executor.py b/api/tests/unit_tests/core/helper/code_executor/test_code_executor.py new file mode 100644 index 00000000000..e09dd03489d --- /dev/null +++ b/api/tests/unit_tests/core/helper/code_executor/test_code_executor.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +from typing import Any, cast +from unittest.mock import MagicMock + +import pytest +from pytest_mock import MockerFixture + +from core.helper.code_executor import code_executor as code_executor_module + + +def test_execute_workflow_code_template_raises_for_unsupported_language() -> None: + with pytest.raises(code_executor_module.CodeExecutionError, match="Unsupported language"): + code_executor_module.CodeExecutor.execute_workflow_code_template(cast(Any, "ruby"), "print(1)", {}) + + +def test_execute_workflow_code_template_uses_transformer(mocker: MockerFixture) -> None: + transformer = MagicMock() + transformer.transform_caller.return_value = ("runner-script", "preload-script") + transformer.transform_response.return_value = {"result": "ok"} + execute_mock = mocker.patch.object( + code_executor_module.CodeExecutor, + "execute_code", + return_value='<>{"result":"ok"}<>', + ) + mocker.patch.dict(code_executor_module.CodeExecutor.code_template_transformers, {"fake": transformer}, clear=False) + + result = code_executor_module.CodeExecutor.execute_workflow_code_template(cast(Any, "fake"), "code", {"a": 1}) + + assert result == {"result": "ok"} + transformer.transform_caller.assert_called_once_with("code", {"a": 1}) + execute_mock.assert_called_once_with("fake", "preload-script", "runner-script") + + +def test_execute_code_raises_service_unavailable_for_503(mocker: MockerFixture) -> None: + response = MagicMock() + response.status_code = 503 + client = MagicMock() + client.post.return_value = response + mocker.patch("core.helper.code_executor.code_executor.get_pooled_http_client", return_value=client) + + with pytest.raises(code_executor_module.CodeExecutionError, match="service is unavailable"): + code_executor_module.CodeExecutor.execute_code(cast(Any, "python3"), preload="", code="print(1)") + + +def test_execute_code_returns_stdout_on_success(mocker: MockerFixture) -> None: + response = MagicMock() + response.status_code = 200 + response.json.return_value = {"code": 0, "message": "ok", "data": {"stdout": "done", "error": None}} + client = MagicMock() + client.post.return_value = response + mocker.patch("core.helper.code_executor.code_executor.get_pooled_http_client", return_value=client) + + assert code_executor_module.CodeExecutor.execute_code(cast(Any, "python3"), preload="", code="print(1)") == "done" + + +def test_execute_code_raises_for_non_200_status(mocker: MockerFixture) -> None: + response = MagicMock() + response.status_code = 500 + client = MagicMock() + client.post.return_value = response + mocker.patch("core.helper.code_executor.code_executor.get_pooled_http_client", return_value=client) + + with pytest.raises(code_executor_module.CodeExecutionError, match="likely a network issue"): + code_executor_module.CodeExecutor.execute_code(cast(Any, "python3"), preload="", code="print(1)") + + +def test_execute_code_raises_when_client_post_fails(mocker: MockerFixture) -> None: + client = MagicMock() + client.post.side_effect = RuntimeError("timeout") + mocker.patch("core.helper.code_executor.code_executor.get_pooled_http_client", return_value=client) + + with pytest.raises(code_executor_module.CodeExecutionError, match="likely a network issue"): + code_executor_module.CodeExecutor.execute_code(cast(Any, "python3"), preload="", code="print(1)") + + +def test_execute_code_raises_when_response_json_is_invalid(mocker: MockerFixture) -> None: + response = MagicMock() + response.status_code = 200 + response.json.side_effect = ValueError("bad json") + client = MagicMock() + client.post.return_value = response + mocker.patch("core.helper.code_executor.code_executor.get_pooled_http_client", return_value=client) + + with pytest.raises(code_executor_module.CodeExecutionError, match="Failed to parse response"): + code_executor_module.CodeExecutor.execute_code(cast(Any, "python3"), preload="", code="print(1)") + + +def test_execute_code_raises_when_sandbox_returns_error_code(mocker: MockerFixture) -> None: + response = MagicMock() + response.status_code = 200 + response.json.return_value = {"code": 1, "message": "boom", "data": {"stdout": "", "error": None}} + client = MagicMock() + client.post.return_value = response + mocker.patch("core.helper.code_executor.code_executor.get_pooled_http_client", return_value=client) + + with pytest.raises(code_executor_module.CodeExecutionError, match="Got error code: 1"): + code_executor_module.CodeExecutor.execute_code(cast(Any, "python3"), preload="", code="print(1)") + + +def test_execute_code_raises_when_response_contains_runtime_error(mocker: MockerFixture) -> None: + response = MagicMock() + response.status_code = 200 + response.json.return_value = {"code": 0, "message": "ok", "data": {"stdout": "", "error": "runtime failed"}} + client = MagicMock() + client.post.return_value = response + mocker.patch("core.helper.code_executor.code_executor.get_pooled_http_client", return_value=client) + + with pytest.raises(code_executor_module.CodeExecutionError, match="runtime failed"): + code_executor_module.CodeExecutor.execute_code(cast(Any, "python3"), preload="", code="print(1)") diff --git a/api/tests/unit_tests/core/helper/code_executor/test_code_node_provider.py b/api/tests/unit_tests/core/helper/code_executor/test_code_node_provider.py new file mode 100644 index 00000000000..47761a32ac8 --- /dev/null +++ b/api/tests/unit_tests/core/helper/code_executor/test_code_node_provider.py @@ -0,0 +1,29 @@ +from core.helper.code_executor.code_node_provider import CodeNodeProvider + + +class _DummyProvider(CodeNodeProvider): + @staticmethod + def get_language() -> str: + return "dummy" + + @classmethod + def get_default_code(cls) -> str: + return "def main():\n return {'result': 'ok'}" + + +def test_is_accept_language() -> None: + assert _DummyProvider.is_accept_language("dummy") is True + assert _DummyProvider.is_accept_language("other") is False + + +def test_get_default_config_contains_expected_shape() -> None: + config = _DummyProvider.get_default_config() + + assert config["type"] == "code" + assert config["config"]["code_language"] == "dummy" + assert config["config"]["code"] == _DummyProvider.get_default_code() + assert config["config"]["variables"] == [ + {"variable": "arg1", "value_selector": []}, + {"variable": "arg2", "value_selector": []}, + ] + assert config["config"]["outputs"] == {"result": {"type": "string", "children": None}} diff --git a/api/tests/unit_tests/core/helper/code_executor/test_template_transformer.py b/api/tests/unit_tests/core/helper/code_executor/test_template_transformer.py new file mode 100644 index 00000000000..5b54b8e6474 --- /dev/null +++ b/api/tests/unit_tests/core/helper/code_executor/test_template_transformer.py @@ -0,0 +1,81 @@ +import json +from base64 import b64decode +from collections.abc import Mapping +from typing import Any + +import pytest + +from core.helper.code_executor.template_transformer import TemplateTransformer + + +class _DummyTransformer(TemplateTransformer): + @classmethod + def get_runner_script(cls) -> str: + return f"CODE={cls._code_placeholder};INPUTS={cls._inputs_placeholder}" + + +def test_serialize_code_encodes_to_base64() -> None: + encoded = _DummyTransformer.serialize_code("print('hi')") + + assert b64decode(encoded.encode()).decode() == "print('hi')" + + +def test_assemble_runner_script_embeds_code_and_inputs() -> None: + script = _DummyTransformer.assemble_runner_script("x = 1", {"a": "b"}) + + assert "CODE=x = 1" in script + payload = script.split("INPUTS=", maxsplit=1)[1] + assert json.loads(b64decode(payload.encode()).decode()) == {"a": "b"} + + +def test_transform_caller_returns_runner_and_empty_preload() -> None: + runner, preload = _DummyTransformer.transform_caller("x = 2", {"k": "v"}) + + assert "CODE=x = 2" in runner + assert preload == "" + + +def test_serialize_inputs_encodes_payload() -> None: + payload = _DummyTransformer.serialize_inputs({"foo": "bar"}) + + assert json.loads(b64decode(payload.encode()).decode()) == {"foo": "bar"} + + +def test_transform_response_parses_json_result_and_converts_scientific_notation() -> None: + response = '<>{"value": "1e+3", "nested": {"x": "2E-2"}, "arr": ["3e+1"]}<>' + + result: Mapping[str, Any] = _DummyTransformer.transform_response(response) + + assert result == {"value": 1000.0, "nested": {"x": 0.02}, "arr": [30.0]} + + +def test_transform_response_raises_for_invalid_json() -> None: + with pytest.raises(ValueError, match="Failed to parse JSON response"): + _DummyTransformer.transform_response("<>{invalid json}<>") + + +def test_transform_response_raises_for_non_dict_result() -> None: + with pytest.raises(ValueError, match="Result must be a dict"): + _DummyTransformer.transform_response("<>[1,2,3]<>") + + +def test_transform_response_raises_for_non_string_keys(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr("json.loads", lambda _: {1: "x"}) + + with pytest.raises(ValueError, match="Result keys must be strings"): + _DummyTransformer.transform_response('<>{"ignored": true}<>') + + +def test_transform_response_raises_for_unexpected_errors(monkeypatch: pytest.MonkeyPatch) -> None: + def _raise_unexpected(_: str) -> Any: + raise RuntimeError("boom") + + monkeypatch.setattr("json.loads", _raise_unexpected) + + with pytest.raises(ValueError, match="Unexpected error during response transformation"): + _DummyTransformer.transform_response('<>{"a":1}<>') + + +def test_transform_response_raises_for_missing_result_tag() -> None: + with pytest.raises(ValueError, match="no result tag found"): + _DummyTransformer.transform_response("plain output") diff --git a/api/tests/unit_tests/core/helper/test_credential_utils.py b/api/tests/unit_tests/core/helper/test_credential_utils.py new file mode 100644 index 00000000000..7e0d7d0af70 --- /dev/null +++ b/api/tests/unit_tests/core/helper/test_credential_utils.py @@ -0,0 +1,138 @@ +from types import SimpleNamespace +from typing import cast + +import pytest +from pytest_mock import MockerFixture + +from core.helper.credential_utils import check_credential_policy_compliance, is_credential_exists +from services.enterprise.plugin_manager_service import PluginCredentialType + + +def test_check_credential_policy_compliance_returns_when_feature_disabled( + mocker: MockerFixture, +) -> None: + mocker.patch( + "services.feature_service.FeatureService.get_system_features", + return_value=SimpleNamespace(plugin_manager=SimpleNamespace(enabled=False)), + ) + check_call = mocker.patch( + "services.enterprise.plugin_manager_service.PluginManagerService.check_credential_policy_compliance" + ) + + check_credential_policy_compliance("cred-1", "openai", PluginCredentialType.MODEL) + + check_call.assert_not_called() + + +def test_check_credential_policy_compliance_raises_when_credential_missing( + mocker: MockerFixture, +) -> None: + mocker.patch( + "services.feature_service.FeatureService.get_system_features", + return_value=SimpleNamespace(plugin_manager=SimpleNamespace(enabled=True)), + ) + mocker.patch("core.helper.credential_utils.is_credential_exists", return_value=False) + + with pytest.raises(ValueError, match="Credential with id cred-1 for provider openai not found."): + check_credential_policy_compliance("cred-1", "openai", PluginCredentialType.TOOL) + + +def test_check_credential_policy_compliance_calls_plugin_manager_with_request( + mocker: MockerFixture, +) -> None: + mocker.patch( + "services.feature_service.FeatureService.get_system_features", + return_value=SimpleNamespace(plugin_manager=SimpleNamespace(enabled=True)), + ) + mocker.patch("core.helper.credential_utils.is_credential_exists", return_value=True) + check_call = mocker.patch( + "services.enterprise.plugin_manager_service.PluginManagerService.check_credential_policy_compliance" + ) + + check_credential_policy_compliance("cred-1", "openai", PluginCredentialType.MODEL) + + check_call.assert_called_once() + request_arg = check_call.call_args.args[0] + assert request_arg.dify_credential_id == "cred-1" + assert request_arg.provider == "openai" + assert request_arg.credential_type == PluginCredentialType.MODEL + + +def test_check_credential_policy_compliance_skips_existence_check_when_disabled( + mocker: MockerFixture, +) -> None: + mocker.patch( + "services.feature_service.FeatureService.get_system_features", + return_value=SimpleNamespace(plugin_manager=SimpleNamespace(enabled=True)), + ) + exists_call = mocker.patch("core.helper.credential_utils.is_credential_exists") + check_call = mocker.patch( + "services.enterprise.plugin_manager_service.PluginManagerService.check_credential_policy_compliance" + ) + + check_credential_policy_compliance( + credential_id="cred-1", + provider="openai", + credential_type=PluginCredentialType.MODEL, + check_existence=False, + ) + + exists_call.assert_not_called() + check_call.assert_called_once() + + +def test_check_credential_policy_compliance_returns_when_credential_id_empty( + mocker: MockerFixture, +) -> None: + mocker.patch( + "services.feature_service.FeatureService.get_system_features", + return_value=SimpleNamespace(plugin_manager=SimpleNamespace(enabled=True)), + ) + exists_call = mocker.patch("core.helper.credential_utils.is_credential_exists") + check_call = mocker.patch( + "services.enterprise.plugin_manager_service.PluginManagerService.check_credential_policy_compliance" + ) + + check_credential_policy_compliance("", "openai", PluginCredentialType.MODEL) + + exists_call.assert_not_called() + check_call.assert_not_called() + + +@pytest.mark.parametrize( + ("credential_type", "scalar_result", "expected"), + [ + (PluginCredentialType.MODEL, "model-credential", True), + (PluginCredentialType.MODEL, None, False), + (PluginCredentialType.TOOL, "tool-credential", True), + (PluginCredentialType.TOOL, None, False), + ], +) +def test_is_credential_exists_by_type( + mocker: MockerFixture, + credential_type: PluginCredentialType, + scalar_result: str | None, + expected: bool, +) -> None: + mocker.patch("extensions.ext_database.db", new=SimpleNamespace(engine=object())) + session_cls = mocker.patch("sqlalchemy.orm.Session") + session = session_cls.return_value.__enter__.return_value + session.scalar.return_value = scalar_result + + result = is_credential_exists("cred-1", credential_type) + + assert result is expected + session.scalar.assert_called_once() + + +def test_is_credential_exists_returns_false_for_unknown_type( + mocker: MockerFixture, +) -> None: + mocker.patch("extensions.ext_database.db", new=SimpleNamespace(engine=object())) + session_cls = mocker.patch("sqlalchemy.orm.Session") + session = session_cls.return_value.__enter__.return_value + + result = is_credential_exists("cred-1", cast(PluginCredentialType, "unknown")) + + assert result is False + session.scalar.assert_not_called() diff --git a/api/tests/unit_tests/core/helper/test_download.py b/api/tests/unit_tests/core/helper/test_download.py new file mode 100644 index 00000000000..0755c25826b --- /dev/null +++ b/api/tests/unit_tests/core/helper/test_download.py @@ -0,0 +1,53 @@ +from collections.abc import Iterator + +import pytest +from pytest_mock import MockerFixture + +from core.helper.download import download_with_size_limit + + +class _StubResponse: + def __init__(self, status_code: int, chunks: list[bytes]) -> None: + self.status_code = status_code + self._chunks = chunks + + def iter_bytes(self) -> Iterator[bytes]: + return iter(self._chunks) + + +def test_download_with_size_limit_returns_content(mocker: MockerFixture) -> None: + response = _StubResponse(status_code=200, chunks=[b"ab", b"cd", b"ef"]) + mock_get = mocker.patch("core.helper.download.ssrf_proxy.get", return_value=response) + + content = download_with_size_limit("https://example.com/a.txt", max_download_size=6, timeout=10) + + assert content == b"abcdef" + mock_get.assert_called_once_with("https://example.com/a.txt", follow_redirects=True, timeout=10) + + +def test_download_with_size_limit_raises_for_404(mocker: MockerFixture) -> None: + mocker.patch("core.helper.download.ssrf_proxy.get", return_value=_StubResponse(status_code=404, chunks=[])) + + with pytest.raises(ValueError, match="file not found"): + download_with_size_limit("https://example.com/missing.txt", max_download_size=10) + + +def test_download_with_size_limit_raises_when_size_exceeds_limit( + mocker: MockerFixture, +) -> None: + response = _StubResponse(status_code=200, chunks=[b"abc", b"de"]) + mocker.patch("core.helper.download.ssrf_proxy.get", return_value=response) + + with pytest.raises(ValueError, match="Max file size reached"): + download_with_size_limit("https://example.com/large.bin", max_download_size=4) + + +def test_download_with_size_limit_accepts_content_equal_to_limit( + mocker: MockerFixture, +) -> None: + response = _StubResponse(status_code=200, chunks=[b"ab", b"cd"]) + mocker.patch("core.helper.download.ssrf_proxy.get", return_value=response) + + content = download_with_size_limit("https://example.com/exact.bin", max_download_size=4) + + assert content == b"abcd" diff --git a/api/tests/unit_tests/core/helper/test_http_client_pooling.py b/api/tests/unit_tests/core/helper/test_http_client_pooling.py new file mode 100644 index 00000000000..c29962f1b1a --- /dev/null +++ b/api/tests/unit_tests/core/helper/test_http_client_pooling.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from unittest.mock import MagicMock + +import httpx + +from core.helper.http_client_pooling import HttpClientPoolFactory + + +def test_get_or_create_reuses_client_for_same_key() -> None: + factory = HttpClientPoolFactory() + first_client = MagicMock(spec=httpx.Client) + second_client = MagicMock(spec=httpx.Client) + clients = [first_client, second_client] + + def _builder() -> httpx.Client: + return clients.pop(0) + + assert factory.get_or_create("shared", _builder) is first_client + assert factory.get_or_create("shared", _builder) is first_client + + +def test_get_or_create_creates_distinct_clients_for_distinct_keys() -> None: + factory = HttpClientPoolFactory() + client_a = MagicMock(spec=httpx.Client) + client_b = MagicMock(spec=httpx.Client) + + assert factory.get_or_create("a", lambda: client_a) is client_a + assert factory.get_or_create("b", lambda: client_b) is client_b + + +def test_close_all_closes_pooled_clients_and_allows_recreate() -> None: + factory = HttpClientPoolFactory() + first_client = MagicMock(spec=httpx.Client) + replacement_client = MagicMock(spec=httpx.Client) + + assert factory.get_or_create("x", lambda: first_client) is first_client + factory.close_all() + + first_client.close.assert_called_once() + assert factory.get_or_create("x", lambda: replacement_client) is replacement_client diff --git a/api/tests/unit_tests/core/helper/test_marketplace.py b/api/tests/unit_tests/core/helper/test_marketplace.py new file mode 100644 index 00000000000..bd561b16373 --- /dev/null +++ b/api/tests/unit_tests/core/helper/test_marketplace.py @@ -0,0 +1,110 @@ +from types import SimpleNamespace +from unittest.mock import MagicMock + +from pytest_mock import MockerFixture + +from core.helper.marketplace import ( + batch_fetch_plugin_by_ids, + batch_fetch_plugin_manifests, + download_plugin_pkg, + fetch_global_plugin_manifest, + get_plugin_pkg_url, + record_install_plugin_event, +) + + +def test_get_plugin_pkg_url_contains_unique_identifier() -> None: + url = get_plugin_pkg_url("plugin@1.0.0") + + assert "api/v1/plugins/download" in url + assert "unique_identifier=plugin@1.0.0" in url + + +def test_download_plugin_pkg_delegates_with_configured_size(mocker: MockerFixture) -> None: + mocked_download = mocker.patch("core.helper.marketplace.download_with_size_limit", return_value=b"pkg") + mocker.patch("core.helper.marketplace.dify_config.PLUGIN_MAX_PACKAGE_SIZE", 1234) + + result = download_plugin_pkg("plugin.a.b") + + assert result == b"pkg" + mocked_download.assert_called_once() + called_url, called_limit = mocked_download.call_args.args + assert "unique_identifier=plugin.a.b" in called_url + assert called_limit == 1234 + + +def test_batch_fetch_plugin_by_ids_returns_empty_for_empty_input(mocker: MockerFixture) -> None: + post_mock = mocker.patch("core.helper.marketplace.httpx.post") + + assert batch_fetch_plugin_by_ids([]) == [] + post_mock.assert_not_called() + + +def test_batch_fetch_plugin_by_ids_returns_plugins_from_response(mocker: MockerFixture) -> None: + response = MagicMock() + response.json.return_value = {"data": {"plugins": [{"id": "p1"}]}} + response.raise_for_status.return_value = None + post_mock = mocker.patch("core.helper.marketplace.httpx.post", return_value=response) + + plugins = batch_fetch_plugin_by_ids(["p1"]) + + assert plugins == [{"id": "p1"}] + post_mock.assert_called_once() + response.raise_for_status.assert_called_once() + + +def test_batch_fetch_plugin_manifests_returns_empty_for_empty_input(mocker: MockerFixture) -> None: + post_mock = mocker.patch("core.helper.marketplace.httpx.post") + + assert batch_fetch_plugin_manifests([]) == [] + post_mock.assert_not_called() + + +def test_batch_fetch_plugin_manifests_validates_and_returns_plugins(mocker: MockerFixture) -> None: + response = MagicMock() + response.raise_for_status.return_value = None + response.json.return_value = {"data": {"plugins": [{"id": "p1"}, {"id": "p2"}]}} + post_mock = mocker.patch("core.helper.marketplace.httpx.post", return_value=response) + validate_mock = mocker.patch( + "core.helper.marketplace.MarketplacePluginDeclaration.model_validate", + side_effect=["manifest-1", "manifest-2"], + ) + + result = batch_fetch_plugin_manifests(["p1", "p2"]) + + assert result == ["manifest-1", "manifest-2"] + post_mock.assert_called_once() + assert validate_mock.call_count == 2 + response.raise_for_status.assert_called_once() + + +def test_record_install_plugin_event_posts_and_checks_status(mocker: MockerFixture) -> None: + response = MagicMock() + response.raise_for_status.return_value = None + post_mock = mocker.patch("core.helper.marketplace.httpx.post", return_value=response) + + record_install_plugin_event("plugin.a") + + post_mock.assert_called_once() + response.raise_for_status.assert_called_once() + + +def test_fetch_global_plugin_manifest_caches_each_plugin(mocker: MockerFixture) -> None: + response = MagicMock() + response.raise_for_status.return_value = None + response.json.return_value = {"plugins": [{"id": "a"}, {"id": "b"}]} + mocker.patch("core.helper.marketplace.httpx.get", return_value=response) + + snapshot_a = SimpleNamespace(plugin_id="plugin-a", model_dump_json=lambda: '{"id":"a"}') + snapshot_b = SimpleNamespace(plugin_id="plugin-b", model_dump_json=lambda: '{"id":"b"}') + validate_mock = mocker.patch( + "core.helper.marketplace.MarketplacePluginSnapshot.model_validate", + side_effect=[snapshot_a, snapshot_b], + ) + setex_mock = mocker.patch("core.helper.marketplace.redis_client.setex") + + fetch_global_plugin_manifest("prefix:", 60) + + assert validate_mock.call_count == 2 + setex_mock.assert_any_call(name="prefix:plugin-a", time=60, value='{"id":"a"}') + setex_mock.assert_any_call(name="prefix:plugin-b", time=60, value='{"id":"b"}') diff --git a/api/tests/unit_tests/core/helper/test_moderation.py b/api/tests/unit_tests/core/helper/test_moderation.py new file mode 100644 index 00000000000..4a84099b74f --- /dev/null +++ b/api/tests/unit_tests/core/helper/test_moderation.py @@ -0,0 +1,158 @@ +from types import SimpleNamespace +from typing import cast + +import pytest +from graphon.model_runtime.errors.invoke import InvokeBadRequestError +from pytest_mock import MockerFixture + +from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity +from core.helper.moderation import check_moderation +from models.provider import ProviderType + + +def _build_model_config(provider: str = "openai") -> SimpleNamespace: + return SimpleNamespace( + provider=provider, + provider_model_bundle=SimpleNamespace( + configuration=SimpleNamespace(using_provider_type=ProviderType.SYSTEM), + ), + ) + + +def test_check_moderation_returns_false_when_feature_not_enabled(mocker: MockerFixture) -> None: + mocker.patch( + "core.helper.moderation.hosting_configuration", + SimpleNamespace(moderation_config=None, provider_map={}), + ) + + assert ( + check_moderation( + "tenant-1", + cast(ModelConfigWithCredentialsEntity, _build_model_config()), + "hello", + ) + is False + ) + + +def test_check_moderation_returns_false_when_hosting_credentials_missing(mocker: MockerFixture) -> None: + openai_provider = "langgenius/openai/openai" + mocker.patch( + "core.helper.moderation.hosting_configuration", + SimpleNamespace( + moderation_config=SimpleNamespace(enabled=True, providers={"openai"}), + provider_map={openai_provider: SimpleNamespace(enabled=True, credentials=None)}, + ), + ) + + assert ( + check_moderation( + "tenant-1", + cast(ModelConfigWithCredentialsEntity, _build_model_config()), + "hello", + ) + is False + ) + + +def test_check_moderation_returns_true_when_model_accepts_text(mocker: MockerFixture) -> None: + openai_provider = "langgenius/openai/openai" + hosting_openai = SimpleNamespace(enabled=True, credentials={"api_key": "k"}) + mocker.patch( + "core.helper.moderation.hosting_configuration", + SimpleNamespace( + moderation_config=SimpleNamespace(enabled=True, providers={"openai"}), + provider_map={openai_provider: hosting_openai}, + ), + ) + mocker.patch("core.helper.moderation.secrets.choice", return_value="chunk") + + moderation_model = SimpleNamespace(invoke=lambda **invoke_kwargs: invoke_kwargs["text"] == "chunk") + factory = SimpleNamespace(get_model_type_instance=lambda **_factory_kwargs: moderation_model) + mocker.patch("core.helper.moderation.create_plugin_model_provider_factory", return_value=factory) + + assert ( + check_moderation( + "tenant-1", + cast(ModelConfigWithCredentialsEntity, _build_model_config()), + "abc", + ) + is True + ) + + +def test_check_moderation_returns_true_when_text_is_empty(mocker: MockerFixture) -> None: + openai_provider = "langgenius/openai/openai" + hosting_openai = SimpleNamespace(enabled=True, credentials={"api_key": "k"}) + mocker.patch( + "core.helper.moderation.hosting_configuration", + SimpleNamespace( + moderation_config=SimpleNamespace(enabled=True, providers={"openai"}), + provider_map={openai_provider: hosting_openai}, + ), + ) + factory_mock = mocker.patch("core.helper.moderation.create_plugin_model_provider_factory") + choice_mock = mocker.patch("core.helper.moderation.secrets.choice") + + assert ( + check_moderation( + "tenant-1", + cast(ModelConfigWithCredentialsEntity, _build_model_config()), + "", + ) + is True + ) + factory_mock.assert_not_called() + choice_mock.assert_not_called() + + +def test_check_moderation_returns_false_when_model_rejects_text(mocker: MockerFixture) -> None: + openai_provider = "langgenius/openai/openai" + hosting_openai = SimpleNamespace(enabled=True, credentials={"api_key": "k"}) + mocker.patch( + "core.helper.moderation.hosting_configuration", + SimpleNamespace( + moderation_config=SimpleNamespace(enabled=True, providers={"openai"}), + provider_map={openai_provider: hosting_openai}, + ), + ) + mocker.patch("core.helper.moderation.secrets.choice", return_value="chunk") + + moderation_model = SimpleNamespace(invoke=lambda **_invoke_kwargs: False) + factory = SimpleNamespace(get_model_type_instance=lambda **_factory_kwargs: moderation_model) + mocker.patch("core.helper.moderation.create_plugin_model_provider_factory", return_value=factory) + + assert ( + check_moderation( + "tenant-1", + cast(ModelConfigWithCredentialsEntity, _build_model_config()), + "abc", + ) + is False + ) + + +def test_check_moderation_raises_bad_request_when_provider_call_fails(mocker: MockerFixture) -> None: + openai_provider = "langgenius/openai/openai" + hosting_openai = SimpleNamespace(enabled=True, credentials={"api_key": "k"}) + mocker.patch( + "core.helper.moderation.hosting_configuration", + SimpleNamespace( + moderation_config=SimpleNamespace(enabled=True, providers={"openai"}), + provider_map={openai_provider: hosting_openai}, + ), + ) + mocker.patch("core.helper.moderation.secrets.choice", return_value="chunk") + + failing_model = SimpleNamespace( + invoke=lambda **_invoke_kwargs: (_ for _ in ()).throw(RuntimeError("boom")), + ) + factory = SimpleNamespace(get_model_type_instance=lambda **_factory_kwargs: failing_model) + mocker.patch("core.helper.moderation.create_plugin_model_provider_factory", return_value=factory) + + with pytest.raises(InvokeBadRequestError, match="Rate limit exceeded, please try again later."): + check_moderation( + "tenant-1", + cast(ModelConfigWithCredentialsEntity, _build_model_config()), + "abc", + ) diff --git a/api/tests/unit_tests/core/helper/test_name_generator.py b/api/tests/unit_tests/core/helper/test_name_generator.py new file mode 100644 index 00000000000..37a87260f15 --- /dev/null +++ b/api/tests/unit_tests/core/helper/test_name_generator.py @@ -0,0 +1,33 @@ +from dataclasses import dataclass + +from pytest_mock import MockerFixture + +from core.helper.name_generator import generate_incremental_name, generate_provider_name +from core.plugin.entities.plugin_daemon import CredentialType + + +@dataclass +class _Provider: + name: str + + +def test_generate_incremental_name_uses_next_highest_suffix() -> None: + names = ["API KEY 1", "API KEY 3", "API KEY 2", "other", "", "API KEY x"] + + assert generate_incremental_name(names, "API KEY") == "API KEY 4" + + +def test_generate_incremental_name_returns_default_when_no_matches() -> None: + assert generate_incremental_name(["custom", " ", ""], "AUTH") == "AUTH 1" + + +def test_generate_provider_name_uses_credential_display_name() -> None: + providers = [_Provider(name="API KEY 1"), _Provider(name="API KEY 2")] + + assert generate_provider_name(providers, CredentialType.API_KEY) == "API KEY 3" + + +def test_generate_provider_name_falls_back_on_generation_error(mocker: MockerFixture) -> None: + mocker.patch("core.helper.name_generator.generate_incremental_name", side_effect=RuntimeError("boom")) + + assert generate_provider_name([], CredentialType.OAUTH2, fallback_context="ctx") == "AUTH 1" diff --git a/api/tests/unit_tests/core/helper/test_tool_parameter_cache.py b/api/tests/unit_tests/core/helper/test_tool_parameter_cache.py new file mode 100644 index 00000000000..3c8b44d0101 --- /dev/null +++ b/api/tests/unit_tests/core/helper/test_tool_parameter_cache.py @@ -0,0 +1,71 @@ +import json + +from pytest_mock import MockerFixture + +from core.helper.tool_parameter_cache import ToolParameterCache, ToolParameterCacheType + + +def test_tool_parameter_cache_get_returns_decoded_dict(mocker: MockerFixture) -> None: + redis_client_mock = mocker.patch("core.helper.tool_parameter_cache.redis_client") + cache = ToolParameterCache( + tenant_id="tenant", + provider="provider", + tool_name="tool", + cache_type=ToolParameterCacheType.PARAMETER, + identity_id="identity", + ) + payload = {"k": "v", "n": 1} + cache_key = cache.cache_key + + redis_client_mock.get.return_value = json.dumps(payload).encode("utf-8") + + assert cache.get() == payload + redis_client_mock.get.assert_called_once_with(cache_key) + + +def test_tool_parameter_cache_get_returns_none_for_invalid_json(mocker: MockerFixture) -> None: + redis_client_mock = mocker.patch("core.helper.tool_parameter_cache.redis_client") + cache = ToolParameterCache( + tenant_id="tenant", + provider="provider", + tool_name="tool", + cache_type=ToolParameterCacheType.PARAMETER, + identity_id="identity", + ) + + redis_client_mock.get.return_value = b"{invalid-json" + + assert cache.get() is None + + +def test_tool_parameter_cache_get_returns_none_when_key_is_missing(mocker: MockerFixture) -> None: + redis_client_mock = mocker.patch("core.helper.tool_parameter_cache.redis_client") + cache = ToolParameterCache( + tenant_id="tenant", + provider="provider", + tool_name="tool", + cache_type=ToolParameterCacheType.PARAMETER, + identity_id="identity", + ) + + redis_client_mock.get.return_value = None + + assert cache.get() is None + + +def test_tool_parameter_cache_set_and_delete(mocker: MockerFixture) -> None: + redis_client_mock = mocker.patch("core.helper.tool_parameter_cache.redis_client") + cache = ToolParameterCache( + tenant_id="tenant", + provider="provider", + tool_name="tool", + cache_type=ToolParameterCacheType.PARAMETER, + identity_id="identity", + ) + + params = {"a": "b"} + cache.set(params) + cache.delete() + + redis_client_mock.setex.assert_called_once_with(cache.cache_key, 86400, json.dumps(params)) + redis_client_mock.delete.assert_called_once_with(cache.cache_key) From 1873b22e96262232103a46ff7aa3b0dd77a0f68d Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Thu, 2 Apr 2026 15:06:11 +0800 Subject: [PATCH 003/549] refactor: update to tailwind v4 (#34415) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: yyh --- .npmrc | 1 + pnpm-lock.yaml | 1150 +++--- pnpm-workspace.yaml | 14 +- taze.config.js | 7 +- .../overview/long-time-range-picker.tsx | 2 +- .../time-range-picker/range-selector.tsx | 2 +- .../overview/tracing/config-button.tsx | 2 +- .../tracing/provider-config-modal.tsx | 76 +- .../overview/tracing/provider-panel.tsx | 2 +- .../[appId]/overview/tracing/tracing-icon.tsx | 2 +- .../[appId]/style.module.css | 2 + .../[datasetId]/documents/style.module.css | 2 + .../(humanInputLayout)/form/[token]/form.tsx | 12 +- .../webapp-reset-password/check-code/page.tsx | 2 +- .../webapp-reset-password/page.tsx | 2 +- .../webapp-signin/check-code/page.tsx | 2 +- .../webapp-signin/normalForm.tsx | 12 +- .../account-page/AvatarWithEdit.tsx | 4 +- .../account-page/email-change-modal.tsx | 24 +- .../(commonLayout)/account-page/index.tsx | 4 +- web/app/account/(commonLayout)/avatar.tsx | 4 +- web/app/account/(commonLayout)/header.tsx | 4 +- web/app/account/oauth/authorize/page.tsx | 4 +- web/app/activate/activateForm.tsx | 4 +- .../app-info/app-info-detail-panel.tsx | 4 +- .../app-sidebar/app-info/app-operations.tsx | 12 +- .../app-sidebar/app-sidebar-dropdown.tsx | 4 +- web/app/components/app-sidebar/basic.tsx | 6 +- .../app-sidebar/dataset-info/dropdown.tsx | 2 +- .../app-sidebar/dataset-sidebar-dropdown.tsx | 4 +- web/app/components/app-sidebar/index.tsx | 2 +- .../annotation/add-annotation-modal/index.tsx | 2 +- .../app/annotation/batch-action.tsx | 2 +- .../csv-uploader.tsx | 2 +- .../batch-add-annotation-modal/index.tsx | 2 +- .../edit-annotation-modal/index.tsx | 2 +- .../app/annotation/header-opts/index.tsx | 16 +- web/app/components/app/annotation/index.tsx | 4 +- .../view-annotation-modal/index.tsx | 4 +- .../access-control-dialog.tsx | 2 +- .../access-control-item.tsx | 4 +- .../add-member-or-group-pop.tsx | 2 +- .../components/app/app-publisher/index.tsx | 2 +- .../app/app-publisher/suggested-action.tsx | 2 +- .../base/var-highlight/style.module.css | 2 + .../warning-mask/cannot-query-dataset.tsx | 2 +- .../base/warning-mask/has-not-set-api.tsx | 4 +- .../base/warning-mask/style.module.css | 2 + .../config-prompt/advanced-prompt-input.tsx | 2 +- .../config-prompt/confirm-add-var/index.tsx | 2 +- .../conversation-history/history-panel.tsx | 2 +- .../prompt-editor-height-resize-wrap.tsx | 2 +- .../config-prompt/simple-prompt-input.tsx | 2 +- .../config-prompt/style.module.css | 2 + .../config-var/config-modal/field.tsx | 2 +- .../config-var/config-modal/index.tsx | 6 +- .../config-var/config-modal/type-select.tsx | 4 +- .../config-var/config-select/index.tsx | 2 +- .../config-var/select-var-type.tsx | 2 +- .../app/configuration/config-vision/index.tsx | 2 +- .../config/agent/agent-setting/index.tsx | 4 +- .../agent-tools/setting-built-in-tool.tsx | 6 +- .../config/assistant-type-picker/index.tsx | 2 +- .../config/automatic/get-automatic-res.tsx | 4 +- .../config/automatic/idea-output.tsx | 2 +- .../config/automatic/instruction-editor.tsx | 8 +- .../config/automatic/prompt-toast.tsx | 4 +- .../config/automatic/style.module.css | 2 + .../config/automatic/version-selector.tsx | 2 +- .../code-generator/get-code-generator-res.tsx | 4 +- .../app/configuration/config/config-audio.tsx | 2 +- .../configuration/config/config-document.tsx | 2 +- .../ctrl-btn-group/style.module.css | 2 + .../dataset-config/card-item/index.tsx | 2 +- .../dataset-config/context-var/var-picker.tsx | 2 +- .../params-config/config-content.tsx | 4 +- .../dataset-config/select-dataset/index.tsx | 2 +- .../dataset-config/settings-modal/index.tsx | 2 +- .../configuration/debug/chat-user-input.tsx | 2 +- .../model-parameter-trigger.spec.tsx | 2 +- .../model-parameter-trigger.tsx | 6 +- .../app/configuration/debug/index.tsx | 6 +- .../components/app/configuration/index.tsx | 2 +- .../prompt-value-panel/index.tsx | 2 +- .../app/configuration/style.module.css | 2 + .../tools/external-data-tool-modal.tsx | 8 +- .../app/configuration/tools/index.tsx | 6 +- .../app/create-app-dialog/app-card/index.tsx | 4 +- .../components/app/create-app-modal/index.tsx | 4 +- .../app/create-from-dsl-modal/uploader.tsx | 4 +- .../components/app/duplicate-modal/index.tsx | 2 +- web/app/components/app/log/list.tsx | 6 +- web/app/components/app/log/model-info.tsx | 4 +- web/app/components/app/log/var-panel.tsx | 2 +- web/app/components/app/overview/app-card.tsx | 8 +- web/app/components/app/overview/app-chart.tsx | 2 +- .../app/overview/customize/index.tsx | 4 +- .../app/overview/embedded/index.tsx | 2 +- .../app/overview/embedded/style.module.css | 2 + .../app/overview/settings/index.tsx | 2 +- .../components/app/overview/style.module.css | 2 + .../components/app/overview/trigger-card.tsx | 6 +- .../app/text-generate/item/index.tsx | 6 +- .../app/text-generate/saved-items/index.tsx | 2 +- .../saved-items/no-data/index.tsx | 2 +- .../components/app/type-selector/index.tsx | 2 +- web/app/components/app/workflow-log/list.tsx | 4 +- .../apps/__tests__/app-card.spec.tsx | 2 +- web/app/components/apps/app-card.tsx | 24 +- web/app/components/apps/empty.tsx | 2 +- .../components/base/action-button/index.css | 101 +- .../base/agent-log-modal/tool-call.tsx | 2 +- web/app/components/base/alert.tsx | 2 +- .../components/base/app-icon-picker/index.tsx | 2 +- .../base/app-icon-picker/style.module.css | 2 + .../base/app-icon/__tests__/index.spec.tsx | 8 +- .../base/app-icon/index.stories.tsx | 2 +- web/app/components/base/app-icon/index.tsx | 12 +- .../base/audio-btn/style.module.css | 2 + .../base/audio-gallery/AudioPlayer.tsx | 4 +- .../auto-height-textarea/index.stories.tsx | 22 +- web/app/components/base/badge.tsx | 2 +- web/app/components/base/badge/index.css | 58 +- .../base/block-input/__tests__/index.spec.tsx | 2 +- .../base/block-input/index.stories.tsx | 2 +- web/app/components/base/block-input/index.tsx | 44 +- .../base/button/add-button.stories.tsx | 2 +- web/app/components/base/button/index.css | 216 +- web/app/components/base/button/index.tsx | 2 +- .../base/button/sync-button.stories.tsx | 2 +- .../chat/chat-with-history/chat-wrapper.tsx | 2 +- .../chat-with-history/header-in-mobile.tsx | 4 +- .../header/mobile-operation-dropdown.tsx | 2 +- .../chat-with-history/header/operation.tsx | 2 +- .../base/chat/chat-with-history/index.tsx | 4 +- .../chat-with-history/inputs-form/content.tsx | 2 +- .../inputs-form/view-form-dropdown.tsx | 2 +- .../chat-with-history/sidebar/operation.tsx | 2 +- .../answer/__tests__/basic-content.spec.tsx | 2 +- .../base/chat/chat/answer/basic-content.tsx | 2 +- .../chat/answer/human-input-content/tips.tsx | 2 +- .../base/chat/chat/answer/operation.tsx | 8 +- .../base/chat/chat/answer/tool-detail.tsx | 8 +- .../chat/citation/__tests__/popup.spec.tsx | 2 +- .../base/chat/chat/citation/popup.tsx | 2 +- .../chat/chat/loading-anim/style.module.css | 2 + .../components/base/chat/chat/log/index.tsx | 2 +- .../components/base/chat/chat/question.tsx | 117 +- .../components/base/chat/chat/try-to-ask.tsx | 4 +- .../chat/embedded-chatbot/chat-wrapper.tsx | 2 +- .../base/chat/embedded-chatbot/index.tsx | 4 +- .../embedded-chatbot/inputs-form/content.tsx | 2 +- .../inputs-form/view-form-dropdown.tsx | 4 +- .../base/checkbox/index.stories.tsx | 2 +- web/app/components/base/checkbox/index.tsx | 2 +- web/app/components/base/chip/index.tsx | 10 +- web/app/components/base/confirm/index.tsx | 8 +- .../components/base/content-dialog/index.tsx | 4 +- .../base/copy-feedback/index.stories.tsx | 2 +- .../base/copy-feedback/style.module.css | 2 + .../base/copy-icon/index.stories.tsx | 2 +- .../base/corner-label/index.stories.tsx | 4 +- .../date-picker/footer.tsx | 2 +- .../date-picker/index.tsx | 4 +- .../time-picker/index.tsx | 4 +- .../components/base/dialog/index.stories.tsx | 2 +- web/app/components/base/dialog/index.tsx | 4 +- .../base/divider/__tests__/index.spec.tsx | 8 +- web/app/components/base/divider/index.tsx | 4 +- .../base/drawer-plus/__tests__/index.spec.tsx | 10 +- web/app/components/base/drawer-plus/index.tsx | 4 +- .../base/drawer/__tests__/index.spec.tsx | 2 +- web/app/components/base/drawer/index.tsx | 8 +- .../conversation-opener/modal.tsx | 4 +- .../new-feature-panel/dialog-wrapper.tsx | 12 +- .../moderation/form-generation.tsx | 4 +- .../moderation/moderation-content.tsx | 2 +- .../moderation/moderation-setting-modal.tsx | 4 +- .../text-to-speech/param-config-content.tsx | 12 +- .../base/file-thumb/image-render.tsx | 2 +- .../__tests__/file-list-in-log.spec.tsx | 2 +- .../base/file-uploader/audio-preview.tsx | 2 +- .../file-from-link-or-local/index.tsx | 8 +- .../base/file-uploader/file-image-render.tsx | 2 +- .../base/file-uploader/file-list-in-log.tsx | 2 +- .../file-image-item.tsx | 6 +- .../file-uploader-in-chat-input/file-item.tsx | 2 +- .../base/file-uploader/pdf-preview.tsx | 4 +- .../base/file-uploader/video-preview.tsx | 2 +- .../base/form/components/base/base-field.tsx | 4 +- .../field/input-type-select/index.tsx | 2 +- .../mixed-variable-text-input/placeholder.tsx | 2 +- .../base/fullscreen-modal/index.tsx | 6 +- .../base/grid-mask/__tests__/index.spec.tsx | 4 +- .../base/grid-mask/index.stories.tsx | 2 +- web/app/components/base/grid-mask/index.tsx | 4 +- .../base/grid-mask/style.module.css | 2 + .../src/image/llm/BaichuanTextCn.module.css | 2 + .../icons/src/image/llm/Minimax.module.css | 2 + .../src/image/llm/MinimaxText.module.css | 2 + .../icons/src/image/llm/Tongyi.module.css | 2 + .../icons/src/image/llm/TongyiText.module.css | 2 + .../src/image/llm/TongyiTextCn.module.css | 2 + .../base/icons/src/image/llm/Wxyy.module.css | 2 + .../icons/src/image/llm/WxyyText.module.css | 2 + .../icons/src/image/llm/WxyyTextCn.module.css | 2 + .../base/image-gallery/style.module.css | 2 + .../base/image-uploader/audio-preview.tsx | 2 +- .../image-uploader/chat-image-uploader.tsx | 4 +- .../base/image-uploader/image-link-input.tsx | 2 +- .../image-uploader/image-list.stories.tsx | 2 +- .../base/image-uploader/image-list.tsx | 8 +- .../base/image-uploader/image-preview.tsx | 2 +- .../base/image-uploader/video-preview.tsx | 2 +- .../base/inline-delete-confirm/index.tsx | 2 +- .../components/base/input-with-copy/index.tsx | 2 +- .../base/input/__tests__/index.spec.tsx | 4 +- web/app/components/base/input/index.tsx | 6 +- .../base/list-empty/index.stories.tsx | 2 +- web/app/components/base/list-empty/index.tsx | 10 +- .../components/base/logo/index.stories.tsx | 6 +- .../base/markdown-blocks/button.tsx | 2 +- .../components/base/markdown-blocks/link.tsx | 2 +- .../components/with-icon-card-item.tsx | 4 +- .../base/markdown/__tests__/index.spec.tsx | 4 +- .../base/markdown/index.stories.tsx | 2 +- web/app/components/base/markdown/index.tsx | 2 +- web/app/components/base/mermaid/index.tsx | 4 +- .../base/modal-like-wrap/index.stories.tsx | 2 +- .../base/modal/__tests__/index.spec.tsx | 2 +- web/app/components/base/modal/index.tsx | 6 +- web/app/components/base/modal/modal.tsx | 4 +- .../base/notion-connector/index.tsx | 2 +- web/app/components/base/notion-icon/index.tsx | 2 +- .../base/pagination/__tests__/index.spec.tsx | 10 +- web/app/components/base/pagination/index.tsx | 4 +- .../__tests__/score-threshold-item.spec.tsx | 2 +- web/app/components/base/popover/index.tsx | 2 +- .../components/base/premium-badge/index.css | 177 +- .../base/prompt-editor/index.stories.tsx | 4 +- .../components/base/prompt-editor/index.tsx | 4 +- .../__tests__/on-blur-or-focus-block.spec.tsx | 10 +- .../__tests__/index.spec.tsx | 22 +- .../__tests__/prompt-option.spec.tsx | 6 +- .../plugins/component-picker-block/hooks.tsx | 2 +- .../plugins/component-picker-block/index.tsx | 2 +- .../component-picker-block/prompt-option.tsx | 2 +- .../__tests__/component.spec.tsx | 4 +- .../plugins/context-block/component.tsx | 2 +- .../__tests__/component.spec.tsx | 2 +- .../plugins/history-block/component.tsx | 2 +- .../plugins/hitl-input-block/component-ui.tsx | 6 +- .../plugins/hitl-input-block/input-field.tsx | 4 +- .../query-block/__tests__/component.spec.tsx | 4 +- .../plugins/query-block/component.tsx | 2 +- .../__tests__/component.spec.tsx | 8 +- .../plugins/request-url-block/component.tsx | 2 +- .../plugins/shortcuts-popup-plugin/index.tsx | 2 +- .../base/prompt-log-modal/index.tsx | 2 +- .../base/radio-card/index.stories.tsx | 6 +- .../base/radio-card/simple/style.module.css | 2 + .../components/base/radio/style.module.css | 2 + .../search-input/__tests__/index.spec.tsx | 2 +- .../base/search-input/index.stories.tsx | 14 +- .../components/base/search-input/index.tsx | 8 +- .../base/segmented-control/index.css | 156 +- .../base/segmented-control/index.tsx | 2 +- .../components/base/select/index.stories.tsx | 4 +- web/app/components/base/select/index.tsx | 14 +- .../components/base/select/locale-signin.tsx | 6 +- web/app/components/base/select/locale.tsx | 6 +- web/app/components/base/select/pure.tsx | 2 +- .../base/simple-pie-chart/index.module.css | 2 + .../base/simple-pie-chart/index.stories.tsx | 2 +- web/app/components/base/skeleton/index.tsx | 2 +- web/app/components/base/sort/index.tsx | 4 +- web/app/components/base/spinner/index.tsx | 2 +- web/app/components/base/svg/index.stories.tsx | 2 +- web/app/components/base/svg/style.module.css | 2 + .../base/switch/__tests__/index.spec.tsx | 20 +- .../components/base/switch/index.stories.tsx | 4 +- web/app/components/base/switch/index.tsx | 16 +- web/app/components/base/switch/skeleton.tsx | 4 +- web/app/components/base/tab-slider/index.tsx | 6 +- .../base/tag-input/__tests__/index.spec.tsx | 2 +- .../base/tag-input/index.stories.tsx | 6 +- .../tag-management/__tests__/index.spec.tsx | 2 +- .../components/base/tag-management/filter.tsx | 6 +- .../base/tag-management/selector.tsx | 8 +- .../base/tag/__tests__/index.spec.tsx | 6 +- web/app/components/base/tag/index.tsx | 2 +- .../base/textarea/index.stories.tsx | 2 +- web/app/components/base/textarea/index.tsx | 2 +- web/app/components/base/theme-selector.tsx | 2 +- web/app/components/base/theme-switcher.tsx | 2 +- web/app/components/base/tooltip/index.tsx | 6 +- .../components/base/ui/alert-dialog/index.tsx | 8 +- .../components/base/ui/context-menu/index.tsx | 2 +- web/app/components/base/ui/dialog/index.tsx | 16 +- .../base/ui/dropdown-menu/index.tsx | 2 +- web/app/components/base/ui/menu-shared.ts | 8 +- .../components/base/ui/number-field/index.tsx | 20 +- web/app/components/base/ui/popover/index.tsx | 4 +- .../ui/scroll-area/__tests__/index.spec.tsx | 18 +- .../base/ui/scroll-area/index.module.css | 2 + .../base/ui/scroll-area/index.stories.tsx | 34 +- .../components/base/ui/scroll-area/index.tsx | 8 +- .../base/ui/select/__tests__/index.spec.tsx | 10 +- web/app/components/base/ui/select/index.tsx | 26 +- web/app/components/base/ui/slider/index.tsx | 22 +- .../base/ui/toast/__tests__/index.spec.tsx | 4 +- .../base/ui/toast/index.stories.tsx | 51 +- web/app/components/base/ui/toast/index.tsx | 35 +- web/app/components/base/ui/tooltip/index.tsx | 6 +- .../base/video-gallery/VideoPlayer.module.css | 2 + .../base/voice-input/index.module.css | 2 + .../base/voice-input/index.stories.tsx | 2 +- .../annotation-full/__tests__/modal.spec.tsx | 2 +- .../billing/annotation-full/modal.tsx | 2 +- .../billing/annotation-full/style.module.css | 2 + .../billing/apps-full-in-dialog/index.tsx | 2 +- .../apps-full-in-dialog/style.module.css | 2 + .../components/billing/billing-page/index.tsx | 2 +- .../billing/plan-upgrade-modal/index.tsx | 4 +- .../plan-upgrade-modal/style.module.css | 2 + web/app/components/billing/pricing/footer.tsx | 2 +- .../billing/pricing/header.module.css | 2 + .../cloud-plan-item/list/item/tooltip.tsx | 2 +- .../components/billing/progress-bar/index.tsx | 8 +- .../trigger-events-limit-modal/index.tsx | 2 +- .../components/billing/upgrade-btn/index.tsx | 2 +- .../billing/upgrade-btn/style.module.css | 2 + .../vector-space-full/style.module.css | 2 + .../components/custom/custom-page/index.tsx | 2 +- .../components/chat-preview-card.tsx | 12 +- .../components/workflow-preview-card.tsx | 4 +- .../custom/custom-web-app-brand/index.tsx | 2 +- .../custom-web-app-brand/style.module.css | 2 + web/app/components/custom/style.module.css | 2 + .../datasets/common/credential-icon.tsx | 2 +- .../datasets/common/document-picker/index.tsx | 2 +- .../preview-document-picker.tsx | 2 +- .../status-with-action.tsx | 2 +- .../datasets/common/image-previewer/index.tsx | 4 +- .../image-uploader-in-chunk/image-input.tsx | 2 +- .../image-uploader-in-chunk/image-item.tsx | 6 +- .../image-input.tsx | 2 +- .../image-item.tsx | 6 +- .../common/retrieval-method-info/index.tsx | 2 +- .../__tests__/footer.spec.tsx | 2 +- .../create-from-pipeline/list/create-card.tsx | 2 +- .../__tests__/chunk-structure-card.spec.tsx | 2 +- .../create/embedding-process/index.module.css | 2 + .../indexing-progress-item.tsx | 4 +- .../index.module.css | 2 + .../create/file-preview/index.module.css | 2 + .../create/file-uploader/index.module.css | 2 + .../datasets/create/index.module.css | 2 + .../notion-page-preview/index.module.css | 2 + .../datasets/create/step-one/index.module.css | 2 + .../datasets/create/step-one/index.tsx | 2 +- .../datasets/create/step-one/upgrade-card.tsx | 4 +- .../create/step-three/index.module.css | 2 + .../datasets/create/step-three/index.tsx | 2 +- .../datasets/create/step-two/index.module.css | 4 +- .../language-select/__tests__/index.spec.tsx | 4 +- .../create/step-two/language-select/index.tsx | 6 +- .../preview-item/__tests__/index.spec.tsx | 2 +- .../create/stepper/__tests__/index.spec.tsx | 2 +- .../stop-embedding-modal/index.module.css | 2 + .../create/stop-embedding-modal/index.tsx | 2 +- .../datasets/create/website/base/input.tsx | 2 +- .../create/website/base/url-input.tsx | 2 +- .../create/website/firecrawl/index.tsx | 2 +- .../datasets/create/website/index.module.css | 2 + .../create/website/jina-reader/index.tsx | 2 +- .../datasets/create/website/no-data.tsx | 2 +- .../datasets/create/website/preview.tsx | 2 +- .../create/website/watercrawl/index.tsx | 2 +- .../documents/components/documents-header.tsx | 4 +- .../online-drive/connect/index.tsx | 2 +- .../header/breadcrumbs/dropdown/index.tsx | 2 +- .../file-list/list/empty-folder.tsx | 2 +- .../file-list/list/empty-search-result.tsx | 2 +- .../online-drive/file-list/list/index.tsx | 2 +- .../base/__tests__/index.spec.tsx | 2 +- .../website-crawl/base/crawling.tsx | 10 +- .../website-crawl/base/error-message.tsx | 2 +- .../website-crawl/base/options/index.tsx | 2 +- .../create-from-pipeline/preview/loading.tsx | 2 +- .../processing/embedding-process/index.tsx | 4 +- .../create-from-pipeline/processing/index.tsx | 2 +- .../detail/batch-modal/csv-uploader.tsx | 2 +- .../documents/detail/batch-modal/index.tsx | 2 +- .../detail/completed/child-segment-detail.tsx | 2 +- .../detail/completed/child-segment-list.tsx | 6 +- .../detail/completed/common/batch-action.tsx | 2 +- .../detail/completed/common/chunk-content.tsx | 6 +- .../detail/completed/common/drawer.tsx | 2 +- .../completed/common/regeneration-modal.tsx | 2 +- .../completed/common/segment-index-tag.tsx | 2 +- .../detail/completed/common/summary-text.tsx | 2 +- .../detail/completed/components/menu-bar.tsx | 2 +- .../detail/completed/new-child-segment.tsx | 2 +- .../completed/segment-card/chunk-content.tsx | 2 +- .../detail/completed/segment-card/index.tsx | 2 +- .../detail/completed/segment-detail.tsx | 2 +- .../detail/completed/style.module.css | 8 +- .../detail/embedding/style.module.css | 4 +- .../datasets/documents/detail/index.tsx | 6 +- .../metadata/components/doc-type-selector.tsx | 2 +- .../detail/metadata/components/field-info.tsx | 2 +- .../detail/metadata/style.module.css | 25 +- .../datasets/documents/detail/new-segment.tsx | 2 +- .../documents/detail/segment-add/index.tsx | 10 +- .../documents/detail/style.module.css | 4 +- .../datasets/documents/style.module.css | 10 +- .../external-api/external-api-modal/index.tsx | 4 +- .../external-api/external-api-panel/index.tsx | 2 +- .../datasets/extra-info/api-access/index.tsx | 2 +- .../service-api/__tests__/card.spec.tsx | 2 +- .../datasets/extra-info/service-api/index.tsx | 2 +- .../flavours/__tests__/edit-slice.spec.tsx | 12 +- .../formatted-text/flavours/edit-slice.tsx | 6 +- .../formatted-text/flavours/shared.tsx | 2 +- .../components/__tests__/mask.spec.tsx | 2 +- .../components/chunk-detail-modal.tsx | 6 +- .../hit-testing/components/empty-records.tsx | 2 +- .../datasets/hit-testing/components/mask.tsx | 2 +- .../components/query-input/index.tsx | 4 +- .../components/query-input/textarea.tsx | 2 +- .../components/result-item-external.tsx | 4 +- .../hit-testing/components/result-item.tsx | 2 +- .../components/datasets/hit-testing/index.tsx | 6 +- .../__tests__/operations-popover.spec.tsx | 2 +- .../components/dataset-card-header.tsx | 2 +- .../components/operations-popover.tsx | 6 +- .../list/dataset-card/components/tag-area.tsx | 2 +- web/app/components/datasets/list/index.tsx | 2 +- .../datasets/metadata/base/date-picker.tsx | 2 +- .../input-has-set-multiple-value.tsx | 2 +- .../create-metadata-modal.tsx | 2 +- .../dataset-metadata-drawer.tsx | 2 +- .../select-metadata-modal.tsx | 2 +- .../__tests__/index.spec.tsx | 2 +- .../__tests__/no-data.spec.tsx | 2 +- .../metadata/metadata-document/no-data.tsx | 2 +- .../external-knowledge-section.spec.tsx | 2 +- .../__tests__/index.spec.tsx | 2 +- .../settings/permission-selector/index.tsx | 8 +- web/app/components/develop/ApiServer.tsx | 4 +- .../develop/__tests__/ApiServer.spec.tsx | 2 +- .../components/develop/__tests__/md.spec.tsx | 8 +- web/app/components/develop/code.tsx | 6 +- web/app/components/develop/md.tsx | 10 +- .../develop/secret-key/style.module.css | 2 + web/app/components/develop/toc-panel.tsx | 6 +- web/app/components/explore/app-card/index.tsx | 4 +- .../explore/app-list/style.module.css | 2 + .../components/explore/banner/banner-item.tsx | 4 +- .../explore/banner/indicator-button.tsx | 2 +- .../explore/create-app-modal/index.tsx | 2 +- .../explore/item-operation/index.tsx | 2 +- .../explore/item-operation/style.module.css | 2 + web/app/components/explore/sidebar/index.tsx | 6 +- .../explore/sidebar/no-apps/style.module.css | 2 + .../explore/try-app/__tests__/index.spec.tsx | 2 +- .../explore/try-app/app-info/index.tsx | 4 +- .../components/explore/try-app/app/chat.tsx | 2 +- .../explore/try-app/app/text-generation.tsx | 2 +- web/app/components/explore/try-app/index.tsx | 2 +- .../components/goto-anything/actions/app.tsx | 2 +- .../goto-anything/command-selector.tsx | 4 +- .../__tests__/search-input.spec.tsx | 2 +- .../goto-anything/components/result-item.tsx | 2 +- .../goto-anything/components/search-input.tsx | 6 +- web/app/components/goto-anything/index.tsx | 4 +- .../components/header/account-about/index.tsx | 2 +- .../header/account-dropdown/compliance.tsx | 89 +- .../header/account-dropdown/index.tsx | 16 +- .../header/account-dropdown/support.tsx | 2 +- .../workplace-selector/index.module.css | 2 + .../Integrations-page/index.module.css | 2 + .../api-based-extension-page/empty.tsx | 2 +- .../api-based-extension-page/modal.tsx | 8 +- .../api-based-extension-page/selector.tsx | 2 +- .../data-source-page-new/card.tsx | 4 +- .../data-source-page-new/configure.tsx | 6 +- .../install-from-marketplace.tsx | 2 +- .../data-source-page-new/item.tsx | 4 +- .../data-source-page-new/operator.tsx | 2 +- .../header/account-setting/index.tsx | 4 +- .../key-validator/KeyInput.tsx | 2 +- .../language-page/index.module.css | 2 + .../__tests__/index.spec.tsx | 2 +- .../account-setting/members-page/index.tsx | 4 +- .../members-page/invite-modal/index.tsx | 4 +- .../invited-modal/index.module.css | 2 + .../members-page/operation/index.tsx | 4 +- .../operation/transfer-ownership.tsx | 2 +- .../transfer-ownership-modal/index.tsx | 16 +- .../member-selector.tsx | 4 +- .../model-provider-page/index.tsx | 4 +- .../install-from-marketplace.tsx | 2 +- .../model-auth/add-custom-model.tsx | 2 +- .../model-auth/authorized/index.tsx | 6 +- .../model-auth/config-model.tsx | 2 +- .../model-auth/credential-selector.tsx | 2 +- .../model-provider-page/model-modal/Form.tsx | 6 +- .../model-provider-page/model-modal/Input.tsx | 2 +- .../model-modal/__tests__/Input.spec.tsx | 6 +- .../model-provider-page/model-modal/index.tsx | 2 +- .../__tests__/parameter-item.spec.tsx | 2 +- .../configuration-button.tsx | 2 +- .../model-parameter-modal/index.tsx | 2 +- .../model-parameter-modal/parameter-item.tsx | 8 +- .../presets-parameter.tsx | 2 +- .../status-indicators.tsx | 2 +- .../model-selector/feature-icon.tsx | 14 +- .../model-selector/popup-item.tsx | 12 +- .../model-selector/popup.tsx | 8 +- .../model-auth-dropdown/api-key-section.tsx | 2 +- .../credits-exhausted-alert.tsx | 2 +- .../usage-priority-section.tsx | 2 +- .../model-load-balancing-configs.tsx | 2 +- .../model-load-balancing-modal.tsx | 2 +- .../provider-card-actions.tsx | 2 +- .../quota-panel.module.css | 2 + .../provider-added-card/system-quota-card.tsx | 6 +- web/app/components/header/app-back/index.tsx | 2 +- .../components/header/app-selector/index.tsx | 8 +- web/app/components/header/header-wrapper.tsx | 2 +- web/app/components/header/index.module.css | 2 + web/app/components/header/index.tsx | 4 +- .../components/header/nav/index.module.css | 2 + web/app/components/header/nav/index.tsx | 2 +- .../header/nav/nav-selector/index.tsx | 10 +- .../components/header/plan-badge/index.tsx | 2 +- .../plugins-nav/downloading-icon.module.css | 2 + .../components/header/plugins-nav/index.tsx | 2 +- .../plugins/card/base/placeholder.tsx | 6 +- .../install-plugin/base/loading-error.tsx | 6 +- .../steps/selectPackage.tsx | 4 +- .../install-from-marketplace/index.tsx | 2 +- .../__tests__/plugin-type-switch.spec.tsx | 12 +- .../description/__tests__/index.spec.tsx | 4 +- .../plugins/marketplace/description/index.tsx | 14 +- .../empty/__tests__/index.spec.tsx | 20 +- .../plugins/marketplace/empty/index.tsx | 8 +- .../plugins/marketplace/list/card-wrapper.tsx | 2 +- .../plugins/marketplace/list/list-wrapper.tsx | 2 +- .../marketplace/plugin-type-switch.tsx | 2 +- .../search-box/__tests__/index.spec.tsx | 4 +- .../plugins/marketplace/search-box/index.tsx | 6 +- .../search-box/search-box-wrapper.tsx | 2 +- .../marketplace/search-box/tags-filter.tsx | 4 +- .../sort-dropdown/__tests__/index.spec.tsx | 4 +- .../marketplace/sort-dropdown/index.tsx | 2 +- .../authorize/add-oauth-button.tsx | 4 +- .../plugin-auth/authorize/api-key-modal.tsx | 2 +- .../plugins/plugin-auth/authorize/index.tsx | 8 +- .../authorize/oauth-client-settings.tsx | 2 +- .../plugins/plugin-auth/authorized/index.tsx | 4 +- .../plugins/plugin-auth/authorized/item.tsx | 2 +- .../app-selector/app-inputs-form.tsx | 2 +- .../app-selector/app-picker.tsx | 4 +- .../app-selector/index.tsx | 4 +- .../components/header-modals.tsx | 2 +- .../detail-header/index.tsx | 2 +- .../plugin-detail-panel/endpoint-card.tsx | 2 +- .../plugin-detail-panel/endpoint-list.tsx | 2 +- .../plugin-detail-panel/endpoint-modal.tsx | 2 +- .../plugins/plugin-detail-panel/index.tsx | 2 +- .../model-selector/index.tsx | 2 +- .../multiple-tool-selector/index.tsx | 4 +- .../plugin-detail-panel/strategy-detail.tsx | 4 +- .../create/components/modal-steps.tsx | 2 +- .../subscription-list/create/index.tsx | 4 +- .../subscription-list/delete-confirm.tsx | 2 +- .../edit/apikey-edit-modal.tsx | 2 +- .../edit/manual-edit-modal.tsx | 2 +- .../edit/oauth-edit-modal.tsx | 2 +- .../subscription-list/selector-entry.tsx | 2 +- .../subscription-list/selector-view.tsx | 2 +- .../subscription-list/subscription-card.tsx | 2 +- .../components/reasoning-config-form.tsx | 6 +- .../tool-selector/components/schema-modal.tsx | 2 +- .../tool-selector/components/tool-trigger.tsx | 2 +- .../tool-selector/index.tsx | 2 +- .../trigger/event-detail-drawer.tsx | 2 +- .../components/plugins/plugin-item/index.tsx | 2 +- .../plugins/plugin-page/empty/index.tsx | 8 +- .../filter-management/category-filter.tsx | 2 +- .../filter-management/tag-filter.tsx | 2 +- .../components/plugins/plugin-page/index.tsx | 2 +- .../plugin-page/install-plugin-dropdown.tsx | 4 +- .../plugin-page/plugin-tasks/index.tsx | 2 +- web/app/components/plugins/provider-card.tsx | 2 +- .../components/plugins/readme-panel/index.tsx | 4 +- .../__tests__/index.spec.tsx | 2 +- .../auto-update-setting/index.tsx | 2 +- .../no-plugin-selected.tsx | 2 +- .../auto-update-setting/plugins-picker.tsx | 2 +- .../auto-update-setting/strategy-picker.tsx | 2 +- .../auto-update-setting/tool-picker.tsx | 4 +- .../plugins/reference-setting-modal/index.tsx | 2 +- .../reference-setting-modal/style.module.css | 2 + .../update-plugin/plugin-version-picker.tsx | 2 +- .../__tests__/publish-toast.spec.tsx | 4 +- .../editor/form/__tests__/hooks.spec.ts | 2 +- .../editor/form/__tests__/index.spec.tsx | 12 +- .../panel/input-field/editor/index.tsx | 2 +- .../panel/input-field/preview/index.tsx | 2 +- .../preparation/__tests__/hooks.spec.ts | 2 +- .../result-preview/__tests__/index.spec.tsx | 2 +- .../test-run/result/result-preview/index.tsx | 4 +- .../result/tabs/__tests__/index.spec.tsx | 6 +- .../panel/test-run/result/tabs/tab.tsx | 2 +- .../publish-as-knowledge-pipeline-modal.tsx | 2 +- .../rag-pipeline/components/publish-toast.tsx | 2 +- .../rag-pipeline-header/publisher/index.tsx | 2 +- .../components/update-dsl-modal.tsx | 4 +- .../text-generation-sidebar.spec.tsx | 4 +- .../share/text-generation/info-modal.tsx | 2 +- .../share/text-generation/menu-dropdown.tsx | 2 +- .../run-batch/csv-reader/index.tsx | 2 +- .../share/text-generation/run-once/index.tsx | 2 +- .../text-generation-result-panel.tsx | 8 +- .../text-generation-sidebar.tsx | 2 +- web/app/components/splash.tsx | 2 +- .../config-credentials.tsx | 12 +- .../edit-custom-collection-modal/index.tsx | 6 +- .../edit-custom-collection-modal/test-api.tsx | 8 +- web/app/components/tools/labels/filter.tsx | 6 +- web/app/components/tools/labels/selector.tsx | 4 +- .../components/tools/marketplace/index.tsx | 2 +- .../tools/mcp/__tests__/modal.spec.tsx | 4 +- .../tools/mcp/detail/list-loading.tsx | 30 +- .../tools/mcp/detail/operation-dropdown.tsx | 2 +- .../tools/mcp/detail/provider-detail.tsx | 2 +- .../components/tools/mcp/detail/tool-item.tsx | 2 +- web/app/components/tools/mcp/index.tsx | 2 +- .../components/tools/mcp/mcp-server-modal.tsx | 4 +- .../components/tools/mcp/mcp-service-card.tsx | 6 +- web/app/components/tools/mcp/modal.tsx | 2 +- .../mcp/sections/authentication-section.tsx | 2 +- .../provider/__tests__/tool-item.spec.tsx | 2 +- web/app/components/tools/provider/detail.tsx | 2 +- .../components/tools/provider/tool-item.tsx | 2 +- .../setting/build-in/config-credentials.tsx | 10 +- .../__tests__/method-selector.spec.tsx | 2 +- .../components/tools/workflow-tool/index.tsx | 6 +- .../tools/workflow-tool/method-selector.tsx | 6 +- .../workflow-header/features-trigger.tsx | 2 +- .../workflow-onboarding-modal/index.tsx | 4 +- .../start-node-selection-panel.tsx | 4 +- .../__tests__/use-workflow-start-run.spec.tsx | 4 +- .../__tests__/syncing-data-modal.spec.tsx | 2 +- web/app/components/workflow/block-icon.tsx | 4 +- .../block-selector/all-start-blocks.tsx | 2 +- .../workflow/block-selector/all-tools.tsx | 2 +- .../block-selector/featured-tools.tsx | 2 +- .../block-selector/featured-triggers.tsx | 2 +- .../workflow/block-selector/main.tsx | 2 +- .../market-place-plugin/action.tsx | 2 +- .../market-place-plugin/list.tsx | 6 +- .../rag-tool-recommendations/list.tsx | 2 +- .../workflow/block-selector/tool-picker.tsx | 6 +- .../block-selector/tool/action-item.tsx | 2 +- .../workflow/block-selector/tools.tsx | 2 +- .../trigger-plugin/action-item.tsx | 2 +- web/app/components/workflow/custom-edge.tsx | 2 +- .../components/workflow/edge-contextmenu.tsx | 2 +- .../__tests__/chat-variable-button.spec.tsx | 2 +- .../header/__tests__/env-button.spec.tsx | 2 +- .../__tests__/global-variable-button.spec.tsx | 2 +- .../__tests__/version-history-button.spec.tsx | 2 +- .../workflow/header/chat-variable-button.tsx | 2 +- .../workflow/header/checklist/node-group.tsx | 2 +- .../header/checklist/plugin-group.tsx | 2 +- .../components/workflow/header/env-button.tsx | 2 +- .../header/global-variable-button.tsx | 2 +- .../workflow/header/header-in-restoring.tsx | 4 +- .../workflow/header/run-and-history.tsx | 2 +- .../workflow/header/running-title.tsx | 2 +- .../header/scroll-to-selected-node-button.tsx | 2 +- .../workflow/header/test-run-menu.tsx | 2 +- .../header/version-history-button.tsx | 2 +- .../workflow/header/view-history.tsx | 2 +- .../workflow/header/view-workflow-history.tsx | 2 +- .../components/workflow/help-line/index.tsx | 4 +- .../use-dynamic-test-run-options.spec.tsx | 6 +- .../components/workflow/node-contextmenu.tsx | 2 +- .../components/before-run-form/bool-input.tsx | 2 +- .../components/before-run-form/form-item.tsx | 4 +- .../nodes/_base/components/collapse/index.tsx | 2 +- .../components/editor/code-editor/index.tsx | 2 +- .../_base/components/editor/text-editor.tsx | 2 +- .../error-handle/error-handle-on-node.tsx | 4 +- .../error-handle-type-selector.tsx | 2 +- .../error-handle/fail-branch-card.tsx | 4 +- .../components/form-input-item.sections.tsx | 4 +- .../_base/components/form-input-item.tsx | 2 +- .../components/form-input-type-switch.tsx | 2 +- .../nodes/_base/components/info-panel.tsx | 2 +- .../components/input-number-with-slider.tsx | 2 +- .../layout/__tests__/field-title.spec.tsx | 6 +- .../_base/components/layout/field-title.tsx | 2 +- .../components/list-no-data-placeholder.tsx | 2 +- .../mixed-variable-text-input/placeholder.tsx | 2 +- .../nodes/_base/components/next-step/add.tsx | 6 +- .../_base/components/next-step/container.tsx | 2 +- .../_base/components/next-step/operator.tsx | 2 +- .../nodes/_base/components/node-control.tsx | 6 +- .../nodes/_base/components/node-handle.tsx | 16 +- .../nodes/_base/components/node-resizer.tsx | 6 +- .../_base/components/panel-operator/index.tsx | 2 +- .../nodes/_base/components/prompt/editor.tsx | 2 +- .../nodes/_base/components/remove-button.tsx | 2 +- .../_base/components/retry/style.module.css | 2 + .../components/title-description-input.tsx | 6 +- .../var-reference-picker.helpers.spec.ts | 2 +- .../__tests__/var-reference-vars.spec.tsx | 2 +- .../components/variable/constant-field.tsx | 4 +- .../variable/manage-input-field.tsx | 2 +- .../object-child-tree-panel/show/field.tsx | 2 +- .../components/variable/output-var-list.tsx | 2 +- .../variable/var-reference-picker.trigger.tsx | 4 +- .../variable/variable-label/hooks.ts | 4 +- .../variable-label-in-editor.tsx | 2 +- .../variable-label/variable-label-in-node.tsx | 2 +- .../variable-label/variable-label-in-text.tsx | 2 +- .../_base/components/workflow-panel/index.tsx | 4 +- .../workflow/nodes/_base/node-sections.tsx | 4 +- .../components/workflow/nodes/_base/node.tsx | 14 +- .../nodes/agent/components/model-bar.tsx | 4 +- .../nodes/agent/components/tool-icon.tsx | 4 +- .../components/operation-selector.tsx | 2 +- .../assigner/components/var-list/index.tsx | 2 +- .../workflow/nodes/assigner/node.tsx | 4 +- .../workflow/nodes/data-source/node.tsx | 2 +- .../nodes/http/__tests__/integration.spec.tsx | 2 +- .../nodes/http/components/api-input.tsx | 2 +- .../http/components/authorization/index.tsx | 2 +- .../nodes/http/components/curl-panel.tsx | 6 +- .../key-value/key-value-edit/input-item.tsx | 4 +- .../key-value/key-value-edit/item.tsx | 2 +- .../components/workflow/nodes/http/node.tsx | 2 +- .../__tests__/human-input.spec.tsx | 14 +- .../components/button-style-dropdown.tsx | 2 +- .../delivery-method/email-configure-modal.tsx | 8 +- .../components/delivery-method/index.tsx | 2 +- .../delivery-method/method-item.tsx | 6 +- .../delivery-method/method-selector.tsx | 16 +- .../delivery-method/recipient/email-input.tsx | 4 +- .../delivery-method/recipient/index.tsx | 6 +- .../delivery-method/recipient/member-list.tsx | 2 +- .../recipient/member-selector.tsx | 2 +- .../delivery-method/test-email-sender.tsx | 6 +- .../delivery-method/upgrade-modal.tsx | 6 +- .../human-input/components/form-content.tsx | 4 +- .../nodes/human-input/components/timeout.tsx | 2 +- .../components/variable-in-markdown.tsx | 2 +- .../workflow/nodes/human-input/node.tsx | 10 +- .../workflow/nodes/human-input/panel.tsx | 6 +- .../if-else/components/condition-add.tsx | 2 +- .../condition-list/condition-item.tsx | 2 +- .../condition-list/condition-operator.tsx | 2 +- .../condition-list/condition-var-selector.tsx | 2 +- .../components/condition-number-input.tsx | 10 +- .../if-else/components/condition-wrap.tsx | 2 +- .../workflow/nodes/if-else/node.tsx | 4 +- .../workflow/nodes/iteration-start/index.tsx | 4 +- .../workflow/nodes/iteration/add-block.tsx | 4 +- .../workflow/nodes/iteration/node.tsx | 2 +- .../chunk-structure/instruction/index.tsx | 4 +- .../knowledge-base/components/option-card.tsx | 2 +- .../workflow/nodes/knowledge-base/node.tsx | 2 +- .../workflow/nodes/knowledge-base/panel.tsx | 4 +- .../components/dataset-item.tsx | 2 +- .../components/metadata/add-condition.tsx | 2 +- .../condition-common-variable-selector.tsx | 2 +- .../condition-list/condition-item.tsx | 4 +- .../condition-list/condition-number.tsx | 4 +- .../condition-list/condition-string.tsx | 4 +- .../condition-list/condition-value-method.tsx | 4 +- .../condition-variable-selector.tsx | 2 +- .../metadata/metadata-filter/index.tsx | 2 +- .../components/extract-input.tsx | 2 +- .../components/filter-condition.tsx | 4 +- .../components/sub-variable-picker.tsx | 2 +- .../error-message.tsx | 2 +- .../json-importer.tsx | 2 +- .../json-schema-generator/index.tsx | 2 +- .../json-schema-generator/prompt-editor.tsx | 2 +- .../visual-editor/add-field.tsx | 2 +- .../edit-card/auto-width-input.tsx | 2 +- .../visual-editor/edit-card/index.tsx | 4 +- .../nodes/llm/components/structure-output.tsx | 2 +- .../components/workflow/nodes/llm/node.tsx | 2 +- .../components/workflow/nodes/llm/panel.tsx | 2 +- .../workflow/nodes/loop-start/index.tsx | 4 +- .../workflow/nodes/loop/add-block.tsx | 4 +- .../nodes/loop/components/condition-add.tsx | 2 +- .../condition-list/condition-item.tsx | 2 +- .../condition-list/condition-var-selector.tsx | 2 +- .../components/condition-number-input.tsx | 10 +- .../nodes/loop/components/condition-wrap.tsx | 2 +- .../loop/components/loop-variables/empty.tsx | 2 +- .../components/loop-variables/form-item.tsx | 2 +- .../workflow/nodes/loop/insert-block.tsx | 2 +- .../components/workflow/nodes/loop/node.tsx | 2 +- .../components/extract-parameter/item.tsx | 4 +- .../components/extract-parameter/update.tsx | 6 +- .../nodes/parameter-extractor/node.tsx | 2 +- .../nodes/parameter-extractor/panel.tsx | 2 +- .../components/class-list.tsx | 2 +- .../nodes/question-classifier/node.tsx | 6 +- .../nodes/question-classifier/panel.tsx | 2 +- .../nodes/tool/components/input-var-list.tsx | 4 +- .../mixed-variable-text-input/placeholder.tsx | 2 +- .../components/workflow/nodes/tool/node.tsx | 4 +- .../workflow/nodes/trigger-plugin/node.tsx | 2 +- .../__tests__/generic-table.spec.tsx | 2 +- .../components/generic-table.tsx | 2 +- .../components/paragraph-input.tsx | 2 +- .../workflow/nodes/trigger-webhook/panel.tsx | 6 +- .../__tests__/integration.spec.tsx | 6 +- .../components/add-variable/index.tsx | 10 +- .../components/node-group-item.tsx | 8 +- .../components/node-variable-item.tsx | 8 +- .../components/var-group-item.tsx | 4 +- .../note-editor/__tests__/editor.spec.tsx | 2 +- .../workflow/note-node/note-editor/editor.tsx | 2 +- .../plugins/link-editor-plugin/component.tsx | 4 +- .../note-node/note-editor/toolbar/divider.tsx | 2 +- .../operator/__tests__/add-block.spec.tsx | 2 +- .../workflow/operator/add-block.tsx | 2 +- .../components/workflow/operator/control.tsx | 4 +- .../components/workflow/operator/index.tsx | 4 +- .../workflow/operator/zoom-in-out.tsx | 2 +- .../components/workflow/panel-contextmenu.tsx | 2 +- .../workflow/panel/chat-record/index.tsx | 2 +- .../__tests__/variable-modal.spec.tsx | 2 +- .../components/array-bool-list.tsx | 2 +- .../components/array-value-list.tsx | 2 +- .../components/object-value-item.tsx | 4 +- .../components/variable-modal-trigger.tsx | 2 +- .../components/variable-modal.sections.tsx | 6 +- .../components/variable-type-select.tsx | 2 +- .../panel/debug-and-preview/chat-wrapper.tsx | 2 +- .../panel/debug-and-preview/index.tsx | 4 +- .../panel/debug-and-preview/user-input.tsx | 2 +- .../panel/env-panel/variable-modal.tsx | 4 +- .../panel/env-panel/variable-trigger.tsx | 2 +- web/app/components/workflow/panel/index.tsx | 2 +- .../workflow/panel/inputs-panel.tsx | 2 +- .../version-history-panel/filter/index.tsx | 2 +- .../panel/version-history-panel/index.tsx | 2 +- .../version-history-panel/loading/item.tsx | 6 +- .../version-history-item.tsx | 6 +- .../workflow/panel/workflow-preview.tsx | 20 +- .../workflow/run/agent-log/agent-log-item.tsx | 2 +- .../run/agent-log/agent-log-trigger.tsx | 4 +- web/app/components/workflow/run/index.tsx | 6 +- web/app/components/workflow/run/meta.tsx | 12 +- web/app/components/workflow/run/node.tsx | 10 +- web/app/components/workflow/run/status.tsx | 6 +- .../components/workflow/run/tracing-panel.tsx | 2 +- .../workflow/selection-contextmenu.tsx | 2 +- .../components/workflow/shortcuts-name.tsx | 2 +- .../simple-node/__tests__/index.spec.tsx | 8 +- .../components/workflow/simple-node/index.tsx | 14 +- .../workflow/syncing-data-modal.tsx | 2 +- .../components/workflow/update-dsl-modal.tsx | 4 +- .../variable-inspect/display-content.tsx | 6 +- .../workflow/variable-inspect/empty.tsx | 2 +- .../workflow/variable-inspect/index.tsx | 2 +- .../workflow/variable-inspect/listening.tsx | 6 +- .../workflow/variable-inspect/panel.tsx | 2 +- .../workflow/variable-inspect/right.tsx | 2 +- .../workflow/variable-inspect/trigger.tsx | 10 +- .../workflow-preview/__tests__/index.spec.tsx | 6 +- .../components/error-handle-on-node.tsx | 2 +- .../components/node-handle.tsx | 4 +- .../components/nodes/base.tsx | 10 +- .../components/nodes/if-else/node.tsx | 4 +- .../nodes/iteration-start/index.tsx | 2 +- .../components/nodes/iteration/node.tsx | 2 +- .../components/nodes/loop-start/index.tsx | 2 +- .../components/nodes/loop/node.tsx | 2 +- .../nodes/question-classifier/node.tsx | 2 +- .../workflow/workflow-preview/index.tsx | 4 +- .../education-apply/education-apply-page.tsx | 4 +- web/app/education-apply/search-input.tsx | 2 +- web/app/education-apply/user-info.tsx | 2 +- .../education-apply/verify-state-modal.tsx | 4 +- .../forgot-password/ChangePasswordForm.tsx | 8 +- web/app/init/InitPasswordPopup.tsx | 2 +- web/app/install/installForm.tsx | 4 +- web/app/reset-password/check-code/page.tsx | 2 +- web/app/reset-password/page.tsx | 2 +- web/app/signin/check-code/page.tsx | 2 +- web/app/signin/invite-settings/page.tsx | 2 +- web/app/signin/normal-form.tsx | 14 +- web/app/signin/page.module.css | 2 + web/app/signup/check-code/page.tsx | 2 +- web/app/styles/globals.css | 1465 ++++---- web/app/styles/markdown.css | 1 + web/app/styles/preflight.css | 8 +- web/docs/overlay-migration.md | 42 +- web/eslint-suppressions.json | 3159 ++++++++++++++--- web/eslint.config.mjs | 5 + web/package.json | 5 +- web/postcss.config.js | 3 +- web/tailwind-common-config.ts | 10 - web/tailwind-css-plugin.ts | 27 - web/typography.js | 6 +- web/vite.config.ts | 2 + 919 files changed, 6545 insertions(+), 3901 deletions(-) create mode 100644 .npmrc delete mode 100644 web/tailwind-css-plugin.ts diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000000..cffe8cdef13 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +save-exact=true diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index baa4ed6c34c..cdd69e783a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,7 +34,7 @@ catalogs: specifier: 3.0.0 version: 3.0.0 '@eslint/js': - specifier: ^10.0.1 + specifier: 10.0.1 version: 10.0.1 '@floating-ui/react': specifier: 0.27.19 @@ -144,9 +144,15 @@ catalogs: '@t3-oss/env-nextjs': specifier: 0.13.11 version: 0.13.11 + '@tailwindcss/postcss': + specifier: 4.2.2 + version: 4.2.2 '@tailwindcss/typography': specifier: 0.5.19 version: 0.5.19 + '@tailwindcss/vite': + specifier: 4.2.2 + version: 4.2.2 '@tanstack/eslint-plugin-query': specifier: 5.95.2 version: 5.95.2 @@ -198,9 +204,6 @@ catalogs: '@types/node': specifier: 25.5.0 version: 25.5.0 - '@types/postcss-js': - specifier: 4.1.0 - version: 4.1.0 '@types/qs': specifier: 6.15.0 version: 6.15.0 @@ -220,7 +223,7 @@ catalogs: specifier: 1.15.9 version: 1.15.9 '@typescript-eslint/eslint-plugin': - specifier: ^8.57.2 + specifier: 8.57.2 version: 8.57.2 '@typescript-eslint/parser': specifier: 8.57.2 @@ -246,9 +249,6 @@ catalogs: ahooks: specifier: 3.9.7 version: 3.9.7 - autoprefixer: - specifier: 10.4.27 - version: 10.4.27 class-variance-authority: specifier: 0.7.1 version: 0.7.1 @@ -414,9 +414,6 @@ catalogs: postcss: specifier: 8.5.8 version: 8.5.8 - postcss-js: - specifier: 5.1.0 - version: 5.1.0 qrcode.react: specifier: 4.2.0 version: 4.2.0 @@ -499,11 +496,11 @@ catalogs: specifier: 2.3.1 version: 2.3.1 tailwind-merge: - specifier: 2.6.1 - version: 2.6.1 + specifier: 3.5.0 + version: 3.5.0 tailwindcss: - specifier: 3.4.19 - version: 3.4.19 + specifier: 4.2.2 + version: 4.2.2 taze: specifier: 19.10.0 version: 19.10.0 @@ -752,7 +749,7 @@ importers: version: 0.13.11(typescript@5.9.3)(valibot@1.3.1(typescript@5.9.3))(zod@4.3.6) '@tailwindcss/typography': specifier: 'catalog:' - version: 0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3)) + version: 0.5.19(tailwindcss@4.2.2) '@tanstack/react-form': specifier: 'catalog:' version: 1.28.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -956,7 +953,7 @@ importers: version: 2.3.1 tailwind-merge: specifier: 'catalog:' - version: 2.6.1 + version: 3.5.0 tldts: specifier: 'catalog:' version: 7.0.27 @@ -981,16 +978,16 @@ importers: devDependencies: '@antfu/eslint-config': specifier: 'catalog:' - version: 7.7.3(@eslint-react/eslint-plugin@3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@next/eslint-plugin-next@16.2.1)(@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3))(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(@vue/compiler-sfc@3.5.31)(eslint-plugin-react-hooks@7.0.1(eslint@10.1.0(jiti@1.21.7)))(eslint-plugin-react-refresh@0.5.2(eslint@10.1.0(jiti@1.21.7)))(eslint@10.1.0(jiti@1.21.7))(oxlint@1.57.0(oxlint-tsgolint@0.17.3))(typescript@5.9.3) + version: 7.7.3(@eslint-react/eslint-plugin@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(@next/eslint-plugin-next@16.2.1)(@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3))(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(@vue/compiler-sfc@3.5.31)(eslint-plugin-react-hooks@7.0.1(eslint@10.1.0(jiti@2.6.1)))(eslint-plugin-react-refresh@0.5.2(eslint@10.1.0(jiti@2.6.1)))(eslint@10.1.0(jiti@2.6.1))(oxlint@1.57.0(oxlint-tsgolint@0.17.3))(typescript@5.9.3) '@chromatic-com/storybook': specifier: 'catalog:' version: 5.1.1(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) '@egoist/tailwindcss-icons': specifier: 'catalog:' - version: 1.9.2(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3)) + version: 1.9.2(tailwindcss@4.2.2) '@eslint-react/eslint-plugin': specifier: 'catalog:' - version: 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + version: 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) '@hono/node-server': specifier: 'catalog:' version: 1.19.11(hono@4.12.9) @@ -1020,7 +1017,7 @@ importers: version: 4.2.0 '@storybook/addon-docs': specifier: 'catalog:' - version: 10.3.3(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) + version: 10.3.3(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) '@storybook/addon-links': specifier: 'catalog:' version: 10.3.3(react@19.2.4)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) @@ -1032,13 +1029,19 @@ importers: version: 10.3.3(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) '@storybook/nextjs-vite': specifier: 'catalog:' - version: 10.3.3(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) + version: 10.3.3(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) '@storybook/react': specifier: 'catalog:' version: 10.3.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + '@tailwindcss/postcss': + specifier: 'catalog:' + version: 4.2.2 + '@tailwindcss/vite': + specifier: 'catalog:' + version: 4.2.2(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) '@tanstack/eslint-plugin-query': specifier: 'catalog:' - version: 5.95.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + version: 5.95.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) '@tanstack/react-devtools': specifier: 'catalog:' version: 0.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11) @@ -1062,13 +1065,13 @@ importers: version: 14.6.1(@testing-library/dom@10.4.1) '@tsslint/cli': specifier: 'catalog:' - version: 3.0.2(@tsslint/compat-eslint@3.0.2(jiti@1.21.7)(typescript@5.9.3))(typescript@5.9.3) + version: 3.0.2(@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@5.9.3))(typescript@5.9.3) '@tsslint/compat-eslint': specifier: 'catalog:' - version: 3.0.2(jiti@1.21.7)(typescript@5.9.3) + version: 3.0.2(jiti@2.6.1)(typescript@5.9.3) '@tsslint/config': specifier: 'catalog:' - version: 3.0.2(@tsslint/compat-eslint@3.0.2(jiti@1.21.7)(typescript@5.9.3))(typescript@5.9.3) + version: 3.0.2(@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@5.9.3))(typescript@5.9.3) '@types/js-cookie': specifier: 'catalog:' version: 3.0.6 @@ -1081,9 +1084,6 @@ importers: '@types/node': specifier: 'catalog:' version: 25.5.0 - '@types/postcss-js': - specifier: 'catalog:' - version: 4.1.0 '@types/qs': specifier: 'catalog:' version: 6.15.0 @@ -1104,58 +1104,55 @@ importers: version: 1.15.9 '@typescript-eslint/parser': specifier: 'catalog:' - version: 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + version: 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) '@typescript/native-preview': specifier: 'catalog:' version: 7.0.0-dev.20260329.1 '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) + version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.21(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(react@19.2.4) + version: 0.5.21(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(react@19.2.4) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.1(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) + version: 4.1.1(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) agentation: specifier: 'catalog:' version: 3.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - autoprefixer: - specifier: 'catalog:' - version: 10.4.27(postcss@8.5.8) code-inspector-plugin: specifier: 'catalog:' version: 1.4.5 eslint: specifier: 'catalog:' - version: 10.1.0(jiti@1.21.7) + version: 10.1.0(jiti@2.6.1) eslint-markdown: specifier: 'catalog:' - version: 0.6.0(eslint@10.1.0(jiti@1.21.7)) + version: 0.6.0(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-better-tailwindcss: specifier: 'catalog:' - version: 4.3.2(eslint@10.1.0(jiti@1.21.7))(oxlint@1.57.0(oxlint-tsgolint@0.17.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))(typescript@5.9.3) + version: 4.3.2(eslint@10.1.0(jiti@2.6.1))(oxlint@1.57.0(oxlint-tsgolint@0.17.3))(tailwindcss@4.2.2)(typescript@5.9.3) eslint-plugin-hyoban: specifier: 'catalog:' - version: 0.14.1(eslint@10.1.0(jiti@1.21.7)) + version: 0.14.1(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-markdown-preferences: specifier: 'catalog:' - version: 0.40.3(@eslint/markdown@7.5.1)(eslint@10.1.0(jiti@1.21.7)) + version: 0.40.3(@eslint/markdown@7.5.1)(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-no-barrel-files: specifier: 'catalog:' - version: 1.2.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + version: 1.2.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) eslint-plugin-react-hooks: specifier: 'catalog:' - version: 7.0.1(eslint@10.1.0(jiti@1.21.7)) + version: 7.0.1(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-react-refresh: specifier: 'catalog:' - version: 0.5.2(eslint@10.1.0(jiti@1.21.7)) + version: 0.5.2(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-sonarjs: specifier: 'catalog:' - version: 4.0.2(eslint@10.1.0(jiti@1.21.7)) + version: 4.0.2(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-storybook: specifier: 'catalog:' - version: 10.3.3(eslint@10.1.0(jiti@1.21.7))(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + version: 10.3.3(eslint@10.1.0(jiti@2.6.1))(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) happy-dom: specifier: 'catalog:' version: 20.8.9 @@ -1171,9 +1168,6 @@ importers: postcss: specifier: 'catalog:' version: 8.5.8 - postcss-js: - specifier: 'catalog:' - version: 5.1.0(postcss@8.5.8) react-server-dom-webpack: specifier: 'catalog:' version: 19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) @@ -1185,7 +1179,7 @@ importers: version: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) tailwindcss: specifier: 'catalog:' - version: 3.4.19(tsx@4.21.0)(yaml@2.8.3) + version: 4.2.2 tsx: specifier: 'catalog:' version: 4.21.0 @@ -1197,22 +1191,22 @@ importers: version: 3.19.3 vinext: specifier: 'catalog:' - version: 0.0.38(f5786d681f520e26604259e094ebaa46) + version: 0.0.38(21fde6c2677b0aab516df83ef1beed5d) vite: specifier: npm:@voidzero-dev/vite-plus-core@0.1.14 - version: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + version: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' vite-plugin-inspect: specifier: 'catalog:' - version: 12.0.0-beta.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)(ws@8.20.0) + version: 12.0.0-beta.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)(ws@8.20.0) vite-plus: specifier: 'catalog:' - version: 0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + version: 0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) vitest: specifier: npm:@voidzero-dev/vite-plus-test@0.1.14 - version: '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + version: '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' vitest-canvas-mock: specifier: 'catalog:' - version: 1.1.4(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) + version: 1.1.4(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) packages: @@ -3828,11 +3822,108 @@ packages: zod: optional: true + '@tailwindcss/node@4.2.2': + resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==} + + '@tailwindcss/oxide-android-arm64@4.2.2': + resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.2': + resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.2': + resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.2': + resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.2.2': + resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.2.2': + resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.2': + resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==} + engines: {node: '>= 20'} + + '@tailwindcss/postcss@4.2.2': + resolution: {integrity: sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==} + '@tailwindcss/typography@0.5.19': resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} peerDependencies: tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + '@tailwindcss/vite@4.2.2': + resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + '@tanstack/devtools-client@0.0.6': resolution: {integrity: sha512-f85ZJXJnDIFOoykG/BFIixuAevJovCvJF391LPs6YjBAPhGYC50NWlx1y4iF/UmK5/cCMx+/JqI5SBOz7FanQQ==} engines: {node: '>=18'} @@ -4206,9 +4297,6 @@ packages: '@types/papaparse@5.5.2': resolution: {integrity: sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==} - '@types/postcss-js@4.1.0': - resolution: {integrity: sha512-E19kBYOk2uEhzxfbam6jALzE6J1GNdny2jdftwDHo72+oWWt7bkWSGzZYVfaRK1r/UToMhAcfbKCAauBXrxi7g==} - '@types/qs@6.15.0': resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==} @@ -4769,17 +4857,10 @@ packages: any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - are-docs-informative@0.0.2: resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} engines: {node: '>=14'} - arg@5.0.2: - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -4815,13 +4896,6 @@ packages: async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - autoprefixer@10.4.27: - resolution: {integrity: sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 - bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -4848,10 +4922,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - birecord@0.1.1: resolution: {integrity: sha512-VUpsf/qykW0heRlC8LooCq28Kxn3mAqKohhDG/49rrsQ1dT1CXyj/pgXS+5BSRzFTR/3DyIBOqQOrGyZOh71Aw==} @@ -4923,10 +4993,6 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - camelcase-css@2.0.1: - resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} - engines: {node: '>= 6'} - camelize@1.0.1: resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} @@ -4998,10 +5064,6 @@ packages: chevrotain@11.1.2: resolution: {integrity: sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==} - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -5430,9 +5492,6 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - didyoumean@1.2.2: - resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} - diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5441,9 +5500,6 @@ packages: resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} engines: {node: '>=0.3.1'} - dlv@1.1.3: - resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} @@ -6053,9 +6109,6 @@ packages: react-dom: optional: true - fraction.js@5.3.4: - resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} - fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -6356,10 +6409,6 @@ packages: is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - is-builtin-module@5.0.0: resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==} engines: {node: '>=18.20'} @@ -6447,10 +6496,6 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} - jiti@1.21.7: - resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} - hasBin: true - jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -7133,10 +7178,6 @@ packages: resolution: {integrity: sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==} engines: {node: ^20.17.0 || >=22.9.0} - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - normalize-wheel@1.0.1: resolution: {integrity: sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==} @@ -7171,10 +7212,6 @@ packages: object-deep-merge@2.0.0: resolution: {integrity: sha512-3DC3UMpeffLTHiuXSy/UG4NOIYTLlY9u3V82+djSCLYClWobZiS4ivYzpIUWrRY/nfsJ8cWsKyG3QfyLePmhvg==} - object-hash@3.0.0: - resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} - engines: {node: '>= 6'} - obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} @@ -7352,10 +7389,6 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - pify@2.3.0: - resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} - engines: {node: '>=0.10.0'} - pinyin-pro@3.28.0: resolution: {integrity: sha512-mMRty6RisoyYNphJrTo3pnvp3w8OMZBrXm9YSWkxhAfxKj1KZk2y8T2PDIZlDDRsvZ0No+Hz6FI4sZpA6Ey25g==} @@ -7404,24 +7437,6 @@ packages: resolution: {integrity: sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==} engines: {node: '>= 10.12'} - postcss-import@15.1.0: - resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} - engines: {node: '>=14.0.0'} - peerDependencies: - postcss: ^8.0.0 - - postcss-js@4.1.0: - resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} - engines: {node: ^12 || ^14 || >= 16} - peerDependencies: - postcss: ^8.4.21 - - postcss-js@5.1.0: - resolution: {integrity: sha512-glrtXSrLt3eH/mgceNgP6u/6jHodqRQ/ToFht+yqwquw0KBf6Zue5qJQFgcIEfQQyYl+BCPN/TYdWyeOQh3c5Q==} - engines: {node: ^20 || ^22 || >= 24} - peerDependencies: - postcss: ^8.4.21 - postcss-load-config@6.0.1: resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} engines: {node: '>= 18'} @@ -7440,20 +7455,10 @@ packages: yaml: optional: true - postcss-nested@6.2.0: - resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.2.14 - postcss-selector-parser@6.0.10: resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} engines: {node: '>=4'} - postcss-selector-parser@6.1.2: - resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} - engines: {node: '>=4'} - postcss-selector-parser@7.1.1: resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} engines: {node: '>=4'} @@ -7709,9 +7714,6 @@ packages: react: '>=17' react-dom: '>=17' - read-cache@1.0.0: - resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} - read-package-up@12.0.0: resolution: {integrity: sha512-Q5hMVBYur/eQNWDdbF4/Wqqr9Bjvtrw2kjGxxBbKLbx8bVCL8gcArjTy8zDUuLGQicftpMuU0riQNcAsbtOVsw==} engines: {node: '>=20'} @@ -7724,10 +7726,6 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -8180,16 +8178,11 @@ packages: '@eslint/css': optional: true - tailwind-merge@2.6.1: - resolution: {integrity: sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==} - tailwind-merge@3.5.0: resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} - tailwindcss@3.4.19: - resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} - engines: {node: '>=14.0.0'} - hasBin: true + tailwindcss@4.2.2: + resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==} tapable@2.3.2: resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} @@ -9088,50 +9081,50 @@ snapshots: idb: 8.0.0 tslib: 2.8.1 - '@antfu/eslint-config@7.7.3(@eslint-react/eslint-plugin@3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@next/eslint-plugin-next@16.2.1)(@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3))(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(@vue/compiler-sfc@3.5.31)(eslint-plugin-react-hooks@7.0.1(eslint@10.1.0(jiti@1.21.7)))(eslint-plugin-react-refresh@0.5.2(eslint@10.1.0(jiti@1.21.7)))(eslint@10.1.0(jiti@1.21.7))(oxlint@1.57.0(oxlint-tsgolint@0.17.3))(typescript@5.9.3)': + '@antfu/eslint-config@7.7.3(@eslint-react/eslint-plugin@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(@next/eslint-plugin-next@16.2.1)(@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3))(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(@vue/compiler-sfc@3.5.31)(eslint-plugin-react-hooks@7.0.1(eslint@10.1.0(jiti@2.6.1)))(eslint-plugin-react-refresh@0.5.2(eslint@10.1.0(jiti@2.6.1)))(eslint@10.1.0(jiti@2.6.1))(oxlint@1.57.0(oxlint-tsgolint@0.17.3))(typescript@5.9.3)': dependencies: '@antfu/install-pkg': 1.1.0 '@clack/prompts': 1.1.0 - '@e18e/eslint-plugin': 0.2.0(eslint@10.1.0(jiti@1.21.7))(oxlint@1.57.0(oxlint-tsgolint@0.17.3)) - '@eslint-community/eslint-plugin-eslint-comments': 4.7.1(eslint@10.1.0(jiti@1.21.7)) + '@e18e/eslint-plugin': 0.2.0(eslint@10.1.0(jiti@2.6.1))(oxlint@1.57.0(oxlint-tsgolint@0.17.3)) + '@eslint-community/eslint-plugin-eslint-comments': 4.7.1(eslint@10.1.0(jiti@2.6.1)) '@eslint/markdown': 7.5.1 - '@stylistic/eslint-plugin': 5.10.0(eslint@10.1.0(jiti@1.21.7)) - '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@vitest/eslint-plugin': 1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@stylistic/eslint-plugin': 5.10.0(eslint@10.1.0(jiti@2.6.1)) + '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@vitest/eslint-plugin': 1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) ansis: 4.2.0 cac: 7.0.0 - eslint: 10.1.0(jiti@1.21.7) - eslint-config-flat-gitignore: 2.3.0(eslint@10.1.0(jiti@1.21.7)) + eslint: 10.1.0(jiti@2.6.1) + eslint-config-flat-gitignore: 2.3.0(eslint@10.1.0(jiti@2.6.1)) eslint-flat-config-utils: 3.0.2 - eslint-merge-processors: 2.0.0(eslint@10.1.0(jiti@1.21.7)) - eslint-plugin-antfu: 3.2.2(eslint@10.1.0(jiti@1.21.7)) - eslint-plugin-command: 3.5.2(@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3))(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7)) - eslint-plugin-import-lite: 0.5.2(eslint@10.1.0(jiti@1.21.7)) - eslint-plugin-jsdoc: 62.8.1(eslint@10.1.0(jiti@1.21.7)) - eslint-plugin-jsonc: 3.1.2(eslint@10.1.0(jiti@1.21.7)) - eslint-plugin-n: 17.24.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + eslint-merge-processors: 2.0.0(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-antfu: 3.2.2(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-command: 3.5.2(@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3))(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-import-lite: 0.5.2(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-jsdoc: 62.8.1(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-jsonc: 3.1.2(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-n: 17.24.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) eslint-plugin-no-only-tests: 3.3.0 - eslint-plugin-perfectionist: 5.7.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - eslint-plugin-pnpm: 1.6.0(eslint@10.1.0(jiti@1.21.7)) - eslint-plugin-regexp: 3.1.0(eslint@10.1.0(jiti@1.21.7)) - eslint-plugin-toml: 1.3.1(eslint@10.1.0(jiti@1.21.7)) - eslint-plugin-unicorn: 63.0.0(eslint@10.1.0(jiti@1.21.7)) - eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7)) - eslint-plugin-vue: 10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.1.0(jiti@1.21.7)))(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(vue-eslint-parser@10.4.0(eslint@10.1.0(jiti@1.21.7))) - eslint-plugin-yml: 3.3.1(eslint@10.1.0(jiti@1.21.7)) - eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.31)(eslint@10.1.0(jiti@1.21.7)) + eslint-plugin-perfectionist: 5.7.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-pnpm: 1.6.0(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-regexp: 3.1.0(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-toml: 1.3.1(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-unicorn: 63.0.0(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-vue: 10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.1.0(jiti@2.6.1)))(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.1.0(jiti@2.6.1))) + eslint-plugin-yml: 3.3.1(eslint@10.1.0(jiti@2.6.1)) + eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.31)(eslint@10.1.0(jiti@2.6.1)) globals: 17.4.0 local-pkg: 1.1.2 parse-gitignore: 2.0.0 toml-eslint-parser: 1.0.3 - vue-eslint-parser: 10.4.0(eslint@10.1.0(jiti@1.21.7)) + vue-eslint-parser: 10.4.0(eslint@10.1.0(jiti@2.6.1)) yaml-eslint-parser: 2.0.0 optionalDependencies: - '@eslint-react/eslint-plugin': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/eslint-plugin': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) '@next/eslint-plugin-next': 16.2.1 - eslint-plugin-react-hooks: 7.0.1(eslint@10.1.0(jiti@1.21.7)) - eslint-plugin-react-refresh: 0.5.2(eslint@10.1.0(jiti@1.21.7)) + eslint-plugin-react-hooks: 7.0.1(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-react-refresh: 0.5.2(eslint@10.1.0(jiti@2.6.1)) transitivePeerDependencies: - '@eslint/json' - '@typescript-eslint/rule-tester' @@ -9489,17 +9482,17 @@ snapshots: '@cucumber/tag-expressions@9.1.0': {} - '@e18e/eslint-plugin@0.2.0(eslint@10.1.0(jiti@1.21.7))(oxlint@1.57.0(oxlint-tsgolint@0.17.3))': + '@e18e/eslint-plugin@0.2.0(eslint@10.1.0(jiti@2.6.1))(oxlint@1.57.0(oxlint-tsgolint@0.17.3))': dependencies: - eslint-plugin-depend: 1.5.0(eslint@10.1.0(jiti@1.21.7)) + eslint-plugin-depend: 1.5.0(eslint@10.1.0(jiti@2.6.1)) optionalDependencies: - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) oxlint: 1.57.0(oxlint-tsgolint@0.17.3) - '@egoist/tailwindcss-icons@1.9.2(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))': + '@egoist/tailwindcss-icons@1.9.2(tailwindcss@4.2.2)': dependencies: '@iconify/utils': 3.1.0 - tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.3) + tailwindcss: 4.2.2 '@emnapi/core@1.9.1': dependencies: @@ -9607,100 +9600,95 @@ snapshots: '@esbuild/win32-x64@0.27.2': optional: true - '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@10.1.0(jiti@1.21.7))': + '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@10.1.0(jiti@2.6.1))': dependencies: escape-string-regexp: 4.0.0 - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) ignore: 7.0.5 - '@eslint-community/eslint-utils@4.9.1(eslint@10.1.0(jiti@1.21.7))': - dependencies: - eslint: 10.1.0(jiti@1.21.7) - eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.9.1(eslint@10.1.0(jiti@2.6.1))': dependencies: eslint: 10.1.0(jiti@2.6.1) eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.9.1(eslint@9.27.0(jiti@1.21.7))': + '@eslint-community/eslint-utils@4.9.1(eslint@9.27.0(jiti@2.6.1))': dependencies: - eslint: 9.27.0(jiti@1.21.7) + eslint: 9.27.0(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint-react/ast@3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': + '@eslint-react/ast@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.57.2 '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - eslint: 10.1.0(jiti@1.21.7) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.1.0(jiti@2.6.1) string-ts: 2.3.1 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@eslint-react/core@3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': + '@eslint-react/core@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.57.2 '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - eslint: 10.1.0(jiti@1.21.7) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.1.0(jiti@2.6.1) ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@eslint-react/eslint-plugin@3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': + '@eslint-react/eslint-plugin@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/type-utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - eslint: 10.1.0(jiti@1.21.7) - eslint-plugin-react-dom: 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - eslint-plugin-react-naming-convention: 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - eslint-plugin-react-rsc: 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - eslint-plugin-react-web-api: 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - eslint-plugin-react-x: 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.1.0(jiti@2.6.1) + eslint-plugin-react-dom: 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-naming-convention: 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-rsc: 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-web-api: 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-x: 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@eslint-react/shared@3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': + '@eslint-react/shared@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - eslint: 10.1.0(jiti@1.21.7) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.1.0(jiti@2.6.1) ts-pattern: 5.9.0 typescript: 5.9.3 zod: 4.3.6 transitivePeerDependencies: - supports-color - '@eslint-react/var@3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': + '@eslint-react/var@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.57.2 '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - eslint: 10.1.0(jiti@1.21.7) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.1.0(jiti@2.6.1) ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@eslint/compat@2.0.3(eslint@10.1.0(jiti@1.21.7))': + '@eslint/compat@2.0.3(eslint@10.1.0(jiti@2.6.1))': dependencies: '@eslint/core': 1.1.1 optionalDependencies: - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) '@eslint/config-array@0.20.1': dependencies: @@ -10011,11 +9999,11 @@ snapshots: dependencies: minipass: 7.1.3 - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.4(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)': + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.4(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)': dependencies: glob: 13.0.6 react-docgen-typescript: 2.4.0(typescript@5.9.3) - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' optionalDependencies: typescript: 5.9.3 @@ -11260,10 +11248,10 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@storybook/addon-docs@10.3.3(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': + '@storybook/addon-docs@10.3.3(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.4) - '@storybook/csf-plugin': 10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) + '@storybook/csf-plugin': 10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) '@storybook/icons': 2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@storybook/react-dom-shim': 10.3.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) react: 19.2.4 @@ -11293,25 +11281,25 @@ snapshots: storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ts-dedent: 2.2.0 - '@storybook/builder-vite@10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': + '@storybook/builder-vite@10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': dependencies: - '@storybook/csf-plugin': 10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) + '@storybook/csf-plugin': 10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ts-dedent: 2.2.0 - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' transitivePeerDependencies: - esbuild - rollup - webpack - '@storybook/csf-plugin@10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': + '@storybook/csf-plugin@10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': dependencies: storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) unplugin: 2.3.11 optionalDependencies: esbuild: 0.27.2 rollup: 4.59.0 - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' webpack: 5.105.4(esbuild@0.27.2)(uglify-js@3.19.3) '@storybook/global@5.0.0': {} @@ -11321,18 +11309,18 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@storybook/nextjs-vite@10.3.3(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': + '@storybook/nextjs-vite@10.3.3(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': dependencies: - '@storybook/builder-vite': 10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) + '@storybook/builder-vite': 10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) '@storybook/react': 10.3.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) - '@storybook/react-vite': 10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) + '@storybook/react-vite': 10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) next: 16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.4) - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' - vite-plugin-storybook-nextjs: 3.2.4(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite-plugin-storybook-nextjs: 3.2.4(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -11349,11 +11337,11 @@ snapshots: react-dom: 19.2.4(react@19.2.4) storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@storybook/react-vite@10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': + '@storybook/react-vite@10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.4(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.4(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3) '@rollup/pluginutils': 5.3.0(rollup@4.59.0) - '@storybook/builder-vite': 10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) + '@storybook/builder-vite': 10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) '@storybook/react': 10.3.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) empathic: 2.0.0 magic-string: 0.30.21 @@ -11363,7 +11351,7 @@ snapshots: resolve: 1.22.11 storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) tsconfig-paths: 4.2.0 - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' transitivePeerDependencies: - esbuild - rollup @@ -11394,11 +11382,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@stylistic/eslint-plugin@5.10.0(eslint@10.1.0(jiti@1.21.7))': + '@stylistic/eslint-plugin@5.10.0(eslint@10.1.0(jiti@2.6.1))': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) '@typescript-eslint/types': 8.57.2 - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) eslint-visitor-keys: 4.2.1 espree: 10.4.0 estraverse: 5.3.0 @@ -11428,10 +11416,86 @@ snapshots: valibot: 1.3.1(typescript@5.9.3) zod: 4.3.6 - '@tailwindcss/typography@0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))': + '@tailwindcss/node@4.2.2': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.20.1 + jiti: 2.6.1 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.2 + + '@tailwindcss/oxide-android-arm64@4.2.2': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.2': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.2': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.2': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.2': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + optional: true + + '@tailwindcss/oxide@4.2.2': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.2 + '@tailwindcss/oxide-darwin-arm64': 4.2.2 + '@tailwindcss/oxide-darwin-x64': 4.2.2 + '@tailwindcss/oxide-freebsd-x64': 4.2.2 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.2 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.2 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.2 + '@tailwindcss/oxide-linux-x64-musl': 4.2.2 + '@tailwindcss/oxide-wasm32-wasi': 4.2.2 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 + + '@tailwindcss/postcss@4.2.2': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.2.2 + '@tailwindcss/oxide': 4.2.2 + postcss: 8.5.8 + tailwindcss: 4.2.2 + + '@tailwindcss/typography@0.5.19(tailwindcss@4.2.2)': dependencies: postcss-selector-parser: 6.0.10 - tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.3) + tailwindcss: 4.2.2 + + '@tailwindcss/vite@4.2.2(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))': + dependencies: + '@tailwindcss/node': 4.2.2 + '@tailwindcss/oxide': 4.2.2 + tailwindcss: 4.2.2 + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' '@tanstack/devtools-client@0.0.6': dependencies: @@ -11477,10 +11541,10 @@ snapshots: - csstype - utf-8-validate - '@tanstack/eslint-plugin-query@5.95.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': + '@tanstack/eslint-plugin-query@5.95.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - eslint: 10.1.0(jiti@1.21.7) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.1.0(jiti@2.6.1) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -11611,10 +11675,10 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 - '@tsslint/cli@3.0.2(@tsslint/compat-eslint@3.0.2(jiti@1.21.7)(typescript@5.9.3))(typescript@5.9.3)': + '@tsslint/cli@3.0.2(@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@5.9.3))(typescript@5.9.3)': dependencies: '@clack/prompts': 0.8.2 - '@tsslint/config': 3.0.2(@tsslint/compat-eslint@3.0.2(jiti@1.21.7)(typescript@5.9.3))(typescript@5.9.3) + '@tsslint/config': 3.0.2(@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@5.9.3))(typescript@5.9.3) '@tsslint/core': 3.0.2 '@volar/language-core': 2.4.28 '@volar/language-hub': 0.0.1 @@ -11625,23 +11689,23 @@ snapshots: - '@tsslint/compat-eslint' - tsl - '@tsslint/compat-eslint@3.0.2(jiti@1.21.7)(typescript@5.9.3)': + '@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@5.9.3)': dependencies: '@tsslint/types': 3.0.2 - '@typescript-eslint/parser': 8.57.2(eslint@9.27.0(jiti@1.21.7))(typescript@5.9.3) - eslint: 9.27.0(jiti@1.21.7) + '@typescript-eslint/parser': 8.57.2(eslint@9.27.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.27.0(jiti@2.6.1) transitivePeerDependencies: - jiti - supports-color - typescript - '@tsslint/config@3.0.2(@tsslint/compat-eslint@3.0.2(jiti@1.21.7)(typescript@5.9.3))(typescript@5.9.3)': + '@tsslint/config@3.0.2(@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@5.9.3))(typescript@5.9.3)': dependencies: '@tsslint/types': 3.0.2 minimatch: 10.2.4 ts-api-utils: 2.5.0(typescript@5.9.3) optionalDependencies: - '@tsslint/compat-eslint': 3.0.2(jiti@1.21.7)(typescript@5.9.3) + '@tsslint/compat-eslint': 3.0.2(jiti@2.6.1)(typescript@5.9.3) transitivePeerDependencies: - typescript @@ -11868,10 +11932,6 @@ snapshots: dependencies: '@types/node': 25.5.0 - '@types/postcss-js@4.1.0': - dependencies: - postcss: 8.5.8 - '@types/qs@6.15.0': {} '@types/react-dom@19.2.3(@types/react@19.2.14)': @@ -11914,22 +11974,6 @@ snapshots: '@types/zen-observable@0.8.3': {} - '@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': - dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/type-utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.2 - eslint: 10.1.0(jiti@1.21.7) - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -11946,18 +11990,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.2 - debug: 4.4.3(supports-color@8.1.1) - eslint: 10.1.0(jiti@1.21.7) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.57.2 @@ -11970,14 +12002,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.57.2(eslint@9.27.0(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/parser@8.57.2(eslint@9.27.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.57.2 '@typescript-eslint/types': 8.57.2 '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.57.2 debug: 4.4.3(supports-color@8.1.1) - eslint: 9.27.0(jiti@1.21.7) + eslint: 9.27.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -11991,13 +12023,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) ajv: 6.14.0 - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) json-stable-stringify-without-jsonify: 1.0.1 lodash.merge: 4.6.2 semver: 7.7.4 @@ -12014,18 +12046,6 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - debug: 4.4.3(supports-color@8.1.1) - eslint: 10.1.0(jiti@1.21.7) - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/type-utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.57.2 @@ -12055,17 +12075,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@1.21.7)) - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) - eslint: 10.1.0(jiti@1.21.7) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) @@ -12141,12 +12150,12 @@ snapshots: '@resvg/resvg-wasm': 2.4.0 satori: 0.16.0 - '@vitejs/devtools-kit@0.1.11(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)(ws@8.20.0)': + '@vitejs/devtools-kit@0.1.11(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)(ws@8.20.0)': dependencies: '@vitejs/devtools-rpc': 0.1.11(typescript@5.9.3)(ws@8.20.0) birpc: 4.0.0 ohash: 2.0.11 - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' transitivePeerDependencies: - typescript - ws @@ -12163,12 +12172,12 @@ snapshots: transitivePeerDependencies: - typescript - '@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))': + '@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' - '@vitejs/plugin-rsc@0.5.21(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(react@19.2.4)': + '@vitejs/plugin-rsc@0.5.21(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(react@19.2.4)': dependencies: '@rolldown/pluginutils': 1.0.0-rc.5 es-module-lexer: 2.0.0 @@ -12180,12 +12189,12 @@ snapshots: srvx: 0.11.13 strip-literal: 3.1.0 turbo-stream: 3.2.0 - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' - vitefu: 1.1.2(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vitefu: 1.1.2(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) optionalDependencies: react-server-dom-webpack: 19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) - '@vitest/coverage-v8@4.1.1(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))': + '@vitest/coverage-v8@4.1.1(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.1.1 @@ -12197,7 +12206,7 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vitest: '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' '@vitest/coverage-v8@4.1.1(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3))': dependencies: @@ -12213,15 +12222,15 @@ snapshots: tinyrainbow: 3.1.0 vitest: '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)' - '@vitest/eslint-plugin@1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': + '@vitest/eslint-plugin@1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - eslint: 10.1.0(jiti@1.21.7) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.1.0(jiti@2.6.1) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) typescript: 5.9.3 - vitest: '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vitest: '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' transitivePeerDependencies: - supports-color @@ -12257,23 +12266,6 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': - dependencies: - '@oxc-project/runtime': 0.121.0 - '@oxc-project/types': 0.122.0 - lightningcss: 1.32.0 - postcss: 8.5.8 - optionalDependencies: - '@types/node': 25.5.0 - esbuild: 0.27.2 - fsevents: 2.3.3 - jiti: 1.21.7 - sass: 1.98.0 - terser: 5.46.1 - tsx: 4.21.0 - typescript: 5.9.3 - yaml: 2.8.3 - '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': dependencies: '@oxc-project/runtime': 0.121.0 @@ -12309,11 +12301,11 @@ snapshots: '@voidzero-dev/vite-plus-linux-x64-musl@0.1.14': optional: true - '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': + '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@voidzero-dev/vite-plus-core': 0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + '@voidzero-dev/vite-plus-core': 0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) es-module-lexer: 1.7.0 obug: 2.1.1 pixelmatch: 7.1.0 @@ -12323,7 +12315,7 @@ snapshots: tinybench: 2.9.0 tinyexec: 1.0.4 tinyglobby: 0.2.15 - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' ws: 8.20.0 optionalDependencies: '@types/node': 25.5.0 @@ -12598,15 +12590,8 @@ snapshots: any-promise@1.3.0: {} - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.2 - are-docs-informative@0.0.2: {} - arg@5.0.2: {} - argparse@2.0.1: {} aria-hidden@1.2.6: @@ -12641,15 +12626,6 @@ snapshots: async@3.2.6: {} - autoprefixer@10.4.27(postcss@8.5.8): - dependencies: - browserslist: 4.28.1 - caniuse-lite: 1.0.30001781 - fraction.js: 5.3.4 - picocolors: 1.1.1 - postcss: 8.5.8 - postcss-value-parser: 4.2.0 - bail@2.0.2: {} balanced-match@1.0.2: {} @@ -12665,8 +12641,6 @@ snapshots: baseline-browser-mapping@2.10.12: {} - binary-extensions@2.3.0: {} - birecord@0.1.1: {} birpc@4.0.0: {} @@ -12731,8 +12705,6 @@ snapshots: callsites@3.1.0: {} - camelcase-css@2.0.1: {} - camelize@1.0.1: {} caniuse-lite@1.0.30001781: {} @@ -12824,18 +12796,6 @@ snapshots: '@chevrotain/utils': 11.1.2 lodash-es: 4.17.23 - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -13248,14 +13208,10 @@ snapshots: dependencies: dequal: 2.0.3 - didyoumean@1.2.2: {} - diff-sequences@29.6.3: {} diff@4.0.4: {} - dlv@1.1.3: {} - doctrine@3.0.0: dependencies: esutils: 2.0.3 @@ -13415,46 +13371,46 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-compat-utils@0.5.1(eslint@10.1.0(jiti@1.21.7)): + eslint-compat-utils@0.5.1(eslint@10.1.0(jiti@2.6.1)): dependencies: - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) semver: 7.7.4 - eslint-config-flat-gitignore@2.3.0(eslint@10.1.0(jiti@1.21.7)): + eslint-config-flat-gitignore@2.3.0(eslint@10.1.0(jiti@2.6.1)): dependencies: - '@eslint/compat': 2.0.3(eslint@10.1.0(jiti@1.21.7)) - eslint: 10.1.0(jiti@1.21.7) + '@eslint/compat': 2.0.3(eslint@10.1.0(jiti@2.6.1)) + eslint: 10.1.0(jiti@2.6.1) eslint-flat-config-utils@3.0.2: dependencies: '@eslint/config-helpers': 0.5.3 pathe: 2.0.3 - eslint-json-compat-utils@0.2.3(eslint@10.1.0(jiti@1.21.7))(jsonc-eslint-parser@3.1.0): + eslint-json-compat-utils@0.2.3(eslint@10.1.0(jiti@2.6.1))(jsonc-eslint-parser@3.1.0): dependencies: - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) esquery: 1.7.0 jsonc-eslint-parser: 3.1.0 - eslint-markdown@0.6.0(eslint@10.1.0(jiti@1.21.7)): + eslint-markdown@0.6.0(eslint@10.1.0(jiti@2.6.1)): dependencies: '@eslint/markdown': 7.5.1 micromark-util-normalize-identifier: 2.0.1 parse5: 8.0.0 optionalDependencies: - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) transitivePeerDependencies: - supports-color - eslint-merge-processors@2.0.0(eslint@10.1.0(jiti@1.21.7)): + eslint-merge-processors@2.0.0(eslint@10.1.0(jiti@2.6.1)): dependencies: - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) - eslint-plugin-antfu@3.2.2(eslint@10.1.0(jiti@1.21.7)): + eslint-plugin-antfu@3.2.2(eslint@10.1.0(jiti@2.6.1)): dependencies: - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) - eslint-plugin-better-tailwindcss@4.3.2(eslint@10.1.0(jiti@1.21.7))(oxlint@1.57.0(oxlint-tsgolint@0.17.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))(typescript@5.9.3): + eslint-plugin-better-tailwindcss@4.3.2(eslint@10.1.0(jiti@2.6.1))(oxlint@1.57.0(oxlint-tsgolint@0.17.3))(tailwindcss@4.2.2)(typescript@5.9.3): dependencies: '@eslint/css-tree': 3.6.9 '@valibot/to-json-schema': 1.6.0(valibot@1.3.1(typescript@5.9.3)) @@ -13462,47 +13418,47 @@ snapshots: jiti: 2.6.1 synckit: 0.11.12 tailwind-csstree: 0.1.5 - tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.3) + tailwindcss: 4.2.2 tsconfig-paths-webpack-plugin: 4.2.0 valibot: 1.3.1(typescript@5.9.3) optionalDependencies: - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) oxlint: 1.57.0(oxlint-tsgolint@0.17.3) transitivePeerDependencies: - '@eslint/css' - typescript - eslint-plugin-command@3.5.2(@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3))(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7)): + eslint-plugin-command@3.5.2(@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3))(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1)): dependencies: '@es-joy/jsdoccomment': 0.84.0 - '@typescript-eslint/rule-tester': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/rule-tester': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - eslint: 10.1.0(jiti@1.21.7) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.1.0(jiti@2.6.1) - eslint-plugin-depend@1.5.0(eslint@10.1.0(jiti@1.21.7)): + eslint-plugin-depend@1.5.0(eslint@10.1.0(jiti@2.6.1)): dependencies: empathic: 2.0.0 - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) module-replacements: 2.11.0 semver: 7.7.4 - eslint-plugin-es-x@7.8.0(eslint@10.1.0(jiti@1.21.7)): + eslint-plugin-es-x@7.8.0(eslint@10.1.0(jiti@2.6.1)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 - eslint: 10.1.0(jiti@1.21.7) - eslint-compat-utils: 0.5.1(eslint@10.1.0(jiti@1.21.7)) + eslint: 10.1.0(jiti@2.6.1) + eslint-compat-utils: 0.5.1(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-hyoban@0.14.1(eslint@10.1.0(jiti@1.21.7)): + eslint-plugin-hyoban@0.14.1(eslint@10.1.0(jiti@2.6.1)): dependencies: - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) - eslint-plugin-import-lite@0.5.2(eslint@10.1.0(jiti@1.21.7)): + eslint-plugin-import-lite@0.5.2(eslint@10.1.0(jiti@2.6.1)): dependencies: - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) - eslint-plugin-jsdoc@62.8.1(eslint@10.1.0(jiti@1.21.7)): + eslint-plugin-jsdoc@62.8.1(eslint@10.1.0(jiti@2.6.1)): dependencies: '@es-joy/jsdoccomment': 0.84.0 '@es-joy/resolve.exports': 1.2.0 @@ -13510,7 +13466,7 @@ snapshots: comment-parser: 1.4.5 debug: 4.4.3(supports-color@8.1.1) escape-string-regexp: 4.0.0 - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) espree: 11.2.0 esquery: 1.7.0 html-entities: 2.6.0 @@ -13522,27 +13478,27 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-jsonc@3.1.2(eslint@10.1.0(jiti@1.21.7)): + eslint-plugin-jsonc@3.1.2(eslint@10.1.0(jiti@2.6.1)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) '@eslint/core': 1.1.1 '@eslint/plugin-kit': 0.6.1 '@ota-meshi/ast-token-store': 0.3.0 diff-sequences: 29.6.3 - eslint: 10.1.0(jiti@1.21.7) - eslint-json-compat-utils: 0.2.3(eslint@10.1.0(jiti@1.21.7))(jsonc-eslint-parser@3.1.0) + eslint: 10.1.0(jiti@2.6.1) + eslint-json-compat-utils: 0.2.3(eslint@10.1.0(jiti@2.6.1))(jsonc-eslint-parser@3.1.0) jsonc-eslint-parser: 3.1.0 natural-compare: 1.4.0 synckit: 0.11.12 transitivePeerDependencies: - '@eslint/json' - eslint-plugin-markdown-preferences@0.40.3(@eslint/markdown@7.5.1)(eslint@10.1.0(jiti@1.21.7)): + eslint-plugin-markdown-preferences@0.40.3(@eslint/markdown@7.5.1)(eslint@10.1.0(jiti@2.6.1)): dependencies: '@eslint/markdown': 7.5.1 diff-sequences: 29.6.3 emoji-regex-xs: 2.0.1 - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) mdast-util-from-markdown: 2.0.3 mdast-util-frontmatter: 2.0.1 mdast-util-gfm: 3.1.0 @@ -13557,12 +13513,12 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-n@17.24.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3): + eslint-plugin-n@17.24.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) enhanced-resolve: 5.20.1 - eslint: 10.1.0(jiti@1.21.7) - eslint-plugin-es-x: 7.8.0(eslint@10.1.0(jiti@1.21.7)) + eslint: 10.1.0(jiti@2.6.1) + eslint-plugin-es-x: 7.8.0(eslint@10.1.0(jiti@2.6.1)) get-tsconfig: 4.13.7 globals: 15.15.0 globrex: 0.1.2 @@ -13572,9 +13528,9 @@ snapshots: transitivePeerDependencies: - typescript - eslint-plugin-no-barrel-files@1.2.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3): + eslint-plugin-no-barrel-files@1.2.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - eslint - supports-color @@ -13582,19 +13538,19 @@ snapshots: eslint-plugin-no-only-tests@3.3.0: {} - eslint-plugin-perfectionist@5.7.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3): + eslint-plugin-perfectionist@5.7.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - eslint: 10.1.0(jiti@1.21.7) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.1.0(jiti@2.6.1) natural-orderby: 5.0.0 transitivePeerDependencies: - supports-color - typescript - eslint-plugin-pnpm@1.6.0(eslint@10.1.0(jiti@1.21.7)): + eslint-plugin-pnpm@1.6.0(eslint@10.1.0(jiti@2.6.1)): dependencies: empathic: 2.0.0 - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) jsonc-eslint-parser: 3.1.0 pathe: 2.0.3 pnpm-workspace-yaml: 1.6.0 @@ -13602,98 +13558,98 @@ snapshots: yaml: 2.8.3 yaml-eslint-parser: 2.0.0 - eslint-plugin-react-dom@3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3): + eslint-plugin-react-dom@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/core': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.57.2 '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) compare-versions: 6.1.1 - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-hooks@7.0.1(eslint@10.1.0(jiti@1.21.7)): + eslint-plugin-react-hooks@7.0.1(eslint@10.1.0(jiti@2.6.1)): dependencies: '@babel/core': 7.29.0 '@babel/parser': 7.29.2 - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) hermes-parser: 0.25.1 zod: 4.3.6 zod-validation-error: 4.0.2(zod@4.3.6) transitivePeerDependencies: - supports-color - eslint-plugin-react-naming-convention@3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3): + eslint-plugin-react-naming-convention@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/core': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/type-utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) compare-versions: 6.1.1 - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) string-ts: 2.3.1 ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.5.2(eslint@10.1.0(jiti@1.21.7)): + eslint-plugin-react-refresh@0.5.2(eslint@10.1.0(jiti@2.6.1)): dependencies: - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) - eslint-plugin-react-rsc@3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3): + eslint-plugin-react-rsc@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/type-utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - eslint: 10.1.0(jiti@1.21.7) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.1.0(jiti@2.6.1) ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-web-api@3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3): + eslint-plugin-react-web-api@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/core': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.57.2 '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) birecord: 0.1.1 - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-x@3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3): + eslint-plugin-react-x@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/core': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/type-utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) compare-versions: 6.1.1 - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) string-ts: 2.3.1 ts-api-utils: 2.5.0(typescript@5.9.3) ts-pattern: 5.9.0 @@ -13701,23 +13657,23 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-regexp@3.1.0(eslint@10.1.0(jiti@1.21.7)): + eslint-plugin-regexp@3.1.0(eslint@10.1.0(jiti@2.6.1)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 comment-parser: 1.4.6 - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) jsdoc-type-pratt-parser: 7.1.1 refa: 0.12.1 regexp-ast-analysis: 0.7.1 scslre: 0.3.0 - eslint-plugin-sonarjs@4.0.2(eslint@10.1.0(jiti@1.21.7)): + eslint-plugin-sonarjs@4.0.2(eslint@10.1.0(jiti@2.6.1)): dependencies: '@eslint-community/regexpp': 4.12.2 builtin-modules: 3.3.0 bytes: 3.1.2 - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) functional-red-black-tree: 1.0.1 globals: 17.4.0 jsx-ast-utils-x: 0.1.0 @@ -13728,35 +13684,35 @@ snapshots: ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 - eslint-plugin-storybook@10.3.3(eslint@10.1.0(jiti@1.21.7))(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3): + eslint-plugin-storybook@10.3.3(eslint@10.1.0(jiti@2.6.1))(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - eslint: 10.1.0(jiti@1.21.7) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.1.0(jiti@2.6.1) storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) transitivePeerDependencies: - supports-color - typescript - eslint-plugin-toml@1.3.1(eslint@10.1.0(jiti@1.21.7)): + eslint-plugin-toml@1.3.1(eslint@10.1.0(jiti@2.6.1)): dependencies: '@eslint/core': 1.1.1 '@eslint/plugin-kit': 0.6.1 '@ota-meshi/ast-token-store': 0.3.0 debug: 4.4.3(supports-color@8.1.1) - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) toml-eslint-parser: 1.0.3 transitivePeerDependencies: - supports-color - eslint-plugin-unicorn@63.0.0(eslint@10.1.0(jiti@1.21.7)): + eslint-plugin-unicorn@63.0.0(eslint@10.1.0(jiti@2.6.1)): dependencies: '@babel/helper-validator-identifier': 7.28.5 - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) change-case: 5.4.4 ci-info: 4.4.0 clean-regexp: 1.0.0 core-js-compat: 3.49.0 - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) find-up-simple: 1.0.1 globals: 16.5.0 indent-string: 5.0.0 @@ -13768,27 +13724,27 @@ snapshots: semver: 7.7.4 strip-indent: 4.1.1 - eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7)): + eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1)): dependencies: - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-vue@10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.1.0(jiti@1.21.7)))(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(vue-eslint-parser@10.4.0(eslint@10.1.0(jiti@1.21.7))): + eslint-plugin-vue@10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.1.0(jiti@2.6.1)))(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.1.0(jiti@2.6.1))): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@1.21.7)) - eslint: 10.1.0(jiti@1.21.7) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) + eslint: 10.1.0(jiti@2.6.1) natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 7.1.1 semver: 7.7.4 - vue-eslint-parser: 10.4.0(eslint@10.1.0(jiti@1.21.7)) + vue-eslint-parser: 10.4.0(eslint@10.1.0(jiti@2.6.1)) xml-name-validator: 4.0.0 optionalDependencies: - '@stylistic/eslint-plugin': 5.10.0(eslint@10.1.0(jiti@1.21.7)) - '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@stylistic/eslint-plugin': 5.10.0(eslint@10.1.0(jiti@2.6.1)) + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-yml@3.3.1(eslint@10.1.0(jiti@1.21.7)): + eslint-plugin-yml@3.3.1(eslint@10.1.0(jiti@2.6.1)): dependencies: '@eslint/core': 1.1.1 '@eslint/plugin-kit': 0.6.1 @@ -13796,16 +13752,16 @@ snapshots: debug: 4.4.3(supports-color@8.1.1) diff-sequences: 29.6.3 escape-string-regexp: 5.0.0 - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) natural-compare: 1.4.0 yaml-eslint-parser: 2.0.0 transitivePeerDependencies: - supports-color - eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.31)(eslint@10.1.0(jiti@1.21.7)): + eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.31)(eslint@10.1.0(jiti@2.6.1)): dependencies: '@vue/compiler-sfc': 3.5.31 - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) eslint-scope@5.1.1: dependencies: @@ -13830,43 +13786,6 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.1.0(jiti@1.21.7): - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@1.21.7)) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.3 - '@eslint/config-helpers': 0.5.3 - '@eslint/core': 1.1.1 - '@eslint/plugin-kit': 0.6.1 - '@humanfs/node': 0.16.7 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 - ajv: 6.14.0 - cross-spawn: 7.0.6 - debug: 4.4.3(supports-color@8.1.1) - escape-string-regexp: 4.0.0 - eslint-scope: 9.1.2 - eslint-visitor-keys: 5.0.1 - espree: 11.2.0 - esquery: 1.7.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.4 - natural-compare: 1.4.0 - optionator: 0.9.4 - optionalDependencies: - jiti: 1.21.7 - transitivePeerDependencies: - - supports-color - eslint@10.1.0(jiti@2.6.1): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) @@ -13904,9 +13823,9 @@ snapshots: transitivePeerDependencies: - supports-color - eslint@9.27.0(jiti@1.21.7): + eslint@9.27.0(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.27.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.27.0(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.20.1 '@eslint/config-helpers': 0.2.3 @@ -13942,7 +13861,7 @@ snapshots: natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: - jiti: 1.21.7 + jiti: 2.6.1 transitivePeerDependencies: - supports-color @@ -14127,8 +14046,6 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - fraction.js@5.3.4: {} - fs-constants@1.0.0: optional: true @@ -14481,10 +14398,6 @@ snapshots: is-alphabetical: 2.0.1 is-decimal: 2.0.1 - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - is-builtin-module@5.0.0: dependencies: builtin-modules: 5.0.0 @@ -14555,8 +14468,6 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jiti@1.21.7: {} - jiti@2.6.1: {} jotai@2.19.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4): @@ -15499,8 +15410,6 @@ snapshots: semver: 7.7.4 validate-npm-package-license: 3.0.4 - normalize-path@3.0.0: {} - normalize-wheel@1.0.1: {} nth-check@2.1.1: @@ -15518,8 +15427,6 @@ snapshots: object-deep-merge@2.0.0: {} - object-hash@3.0.0: {} - obug@2.1.1: {} ofetch@1.5.1: @@ -15799,8 +15706,6 @@ snapshots: picomatch@4.0.4: {} - pify@2.3.0: {} - pinyin-pro@3.28.0: {} pirates@4.0.7: {} @@ -15851,31 +15756,6 @@ snapshots: transitivePeerDependencies: - supports-color - postcss-import@15.1.0(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - postcss-value-parser: 4.2.0 - read-cache: 1.0.0 - resolve: 1.22.11 - - postcss-js@4.1.0(postcss@8.5.8): - dependencies: - camelcase-css: 2.0.1 - postcss: 8.5.8 - - postcss-js@5.1.0(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.3): - dependencies: - lilconfig: 3.1.3 - optionalDependencies: - jiti: 1.21.7 - postcss: 8.5.8 - tsx: 4.21.0 - yaml: 2.8.3 - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.3): dependencies: lilconfig: 3.1.3 @@ -15885,21 +15765,11 @@ snapshots: tsx: 4.21.0 yaml: 2.8.3 - postcss-nested@6.2.0(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - postcss-selector-parser: 6.1.2 - postcss-selector-parser@6.0.10: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-selector-parser@6.1.2: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - postcss-selector-parser@7.1.1: dependencies: cssesc: 3.0.0 @@ -16181,10 +16051,6 @@ snapshots: - '@types/react' - immer - read-cache@1.0.0: - dependencies: - pify: 2.3.0 - read-package-up@12.0.0: dependencies: find-up-simple: 1.0.1 @@ -16206,10 +16072,6 @@ snapshots: util-deprecate: 1.0.2 optional: true - readdirp@3.6.0: - dependencies: - picomatch: 2.3.2 - readdirp@4.1.2: {} recast@0.23.11: @@ -16804,37 +16666,9 @@ snapshots: tailwind-csstree@0.1.5: {} - tailwind-merge@2.6.1: {} - tailwind-merge@3.5.0: {} - tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3): - dependencies: - '@alloc/quick-lru': 5.2.0 - arg: 5.0.2 - chokidar: 3.6.0 - didyoumean: 1.2.2 - dlv: 1.1.3 - fast-glob: 3.3.3 - glob-parent: 6.0.2 - is-glob: 4.0.3 - jiti: 1.21.7 - lilconfig: 3.1.3 - micromatch: 4.0.8 - normalize-path: 3.0.0 - object-hash: 3.0.0 - picocolors: 1.1.1 - postcss: 8.5.8 - postcss-import: 15.1.0(postcss@8.5.8) - postcss-js: 4.1.0(postcss@8.5.8) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.3) - postcss-nested: 6.2.0(postcss@8.5.8) - postcss-selector-parser: 6.1.2 - resolve: 1.22.11 - sucrase: 3.35.1 - transitivePeerDependencies: - - tsx - - yaml + tailwindcss@4.2.2: {} tapable@2.3.2: {} @@ -17243,21 +17077,21 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vinext@0.0.38(f5786d681f520e26604259e094ebaa46): + vinext@0.0.38(21fde6c2677b0aab516df83ef1beed5d): dependencies: '@unpic/react': 1.0.2(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@vercel/og': 0.8.6 - '@vitejs/plugin-react': 6.0.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) + '@vitejs/plugin-react': 6.0.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) magic-string: 0.30.21 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) rsc-html-stream: 0.0.7 - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' vite-plugin-commonjs: 0.10.4 - vite-tsconfig-paths: 6.1.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3) + vite-tsconfig-paths: 6.1.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3) optionalDependencies: '@mdx-js/rollup': 3.1.1(rollup@4.59.0) - '@vitejs/plugin-rsc': 0.5.21(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(react@19.2.4) + '@vitejs/plugin-rsc': 0.5.21(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(react@19.2.4) react-server-dom-webpack: 19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) transitivePeerDependencies: - next @@ -17277,9 +17111,9 @@ snapshots: fast-glob: 3.3.3 magic-string: 0.30.21 - vite-plugin-inspect@12.0.0-beta.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)(ws@8.20.0): + vite-plugin-inspect@12.0.0-beta.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)(ws@8.20.0): dependencies: - '@vitejs/devtools-kit': 0.1.11(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)(ws@8.20.0) + '@vitejs/devtools-kit': 0.1.11(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)(ws@8.20.0) ansis: 4.2.0 error-stack-parser-es: 1.0.5 obug: 2.1.1 @@ -17288,12 +17122,12 @@ snapshots: perfect-debounce: 2.1.0 sirv: 3.0.2 unplugin-utils: 0.3.1 - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' transitivePeerDependencies: - typescript - ws - vite-plugin-storybook-nextjs@3.2.4(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3): + vite-plugin-storybook-nextjs@3.2.4(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3): dependencies: '@next/env': 16.0.0 image-size: 2.0.2 @@ -17302,17 +17136,17 @@ snapshots: next: 16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ts-dedent: 2.2.0 - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' - vite-tsconfig-paths: 5.1.4(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3) + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite-tsconfig-paths: 5.1.4(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3) transitivePeerDependencies: - supports-color - typescript - vite-plus@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3): + vite-plus@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3): dependencies: '@oxc-project/types': 0.122.0 - '@voidzero-dev/vite-plus-core': 0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) - '@voidzero-dev/vite-plus-test': 0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + '@voidzero-dev/vite-plus-core': 0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + '@voidzero-dev/vite-plus-test': 0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) cac: 7.0.0 cross-spawn: 7.0.6 oxfmt: 0.42.0 @@ -17404,23 +17238,23 @@ snapshots: - vite - yaml - vite-tsconfig-paths@5.1.4(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3): + vite-tsconfig-paths@5.1.4(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3): dependencies: debug: 4.4.3(supports-color@8.1.1) globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' transitivePeerDependencies: - supports-color - typescript - vite-tsconfig-paths@6.1.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3): + vite-tsconfig-paths@6.1.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3): dependencies: debug: 4.4.3(supports-color@8.1.1) globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' transitivePeerDependencies: - supports-color - typescript @@ -17445,15 +17279,15 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - vitefu@1.1.2(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)): + vitefu@1.1.2(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)): optionalDependencies: - vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' - vitest-canvas-mock@1.1.4(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)): + vitest-canvas-mock@1.1.4(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)): dependencies: cssfontparser: 1.2.1 moo-color: 1.0.3 - vitest: '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vitest: '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' void-elements@3.1.0: {} @@ -17474,10 +17308,10 @@ snapshots: vscode-uri@3.1.0: {} - vue-eslint-parser@10.4.0(eslint@10.1.0(jiti@1.21.7)): + vue-eslint-parser@10.4.0(eslint@10.1.0(jiti@2.6.1)): dependencies: debug: 4.4.3(supports-color@8.1.1) - eslint: 10.1.0(jiti@1.21.7) + eslint: 10.1.0(jiti@2.6.1) eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 espree: 11.2.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 77451f6dfc4..236d6d7ade1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,9 +1,9 @@ trustPolicy: no-downgrade -minimumReleaseAge: 1440 +minimumReleaseAge: 2880 blockExoticSubdeps: true strictDepBuilds: true allowBuilds: - '@parcel/watcher': false + "@parcel/watcher": false canvas: false esbuild: false sharp: false @@ -78,7 +78,7 @@ catalog: "@egoist/tailwindcss-icons": 1.9.2 "@emoji-mart/data": 1.2.1 "@eslint-react/eslint-plugin": 3.0.0 - "@eslint/js": ^10.0.1 + "@eslint/js": 10.0.1 "@floating-ui/react": 0.27.19 "@formatjs/intl-localematcher": 0.8.2 "@headlessui/react": 2.2.9 @@ -116,7 +116,9 @@ catalog: "@streamdown/math": 1.0.2 "@svgdotjs/svg.js": 3.2.5 "@t3-oss/env-nextjs": 0.13.11 + "@tailwindcss/postcss": 4.2.2 "@tailwindcss/typography": 0.5.19 + "@tailwindcss/vite": 4.2.2 "@tanstack/eslint-plugin-query": 5.95.2 "@tanstack/react-devtools": 0.10.0 "@tanstack/react-form": 1.28.5 @@ -141,7 +143,7 @@ catalog: "@types/react-syntax-highlighter": 15.5.13 "@types/react-window": 1.8.8 "@types/sortablejs": 1.15.9 - "@typescript-eslint/eslint-plugin": ^8.57.2 + "@typescript-eslint/eslint-plugin": 8.57.2 "@typescript-eslint/parser": 8.57.2 "@typescript/native-preview": 7.0.0-dev.20260329.1 "@vitejs/plugin-react": 6.0.1 @@ -234,8 +236,8 @@ catalog: storybook: 10.3.3 streamdown: 2.5.0 string-ts: 2.3.1 - tailwind-merge: 2.6.1 - tailwindcss: 3.4.19 + tailwind-merge: 3.5.0 + tailwindcss: 4.2.2 taze: 19.10.0 tldts: 7.0.27 tsup: ^8.5.1 diff --git a/taze.config.js b/taze.config.js index d21756e207d..cd5a9f86563 100644 --- a/taze.config.js +++ b/taze.config.js @@ -8,13 +8,8 @@ export default defineConfig({ '@types/react-window', // We can not upgrade these yet - 'tailwind-merge', - 'tailwindcss', 'typescript', ], - write: true, - install: false, - recursive: true, - interactive: true, + maturityPeriod: 2, }) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/long-time-range-picker.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/long-time-range-picker.tsx index f7178d7ac21..b5da0e4ca5e 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/long-time-range-picker.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/long-time-range-picker.tsx @@ -57,7 +57,7 @@ const LongTimeRangePicker: FC = ({ return ( ({ value: k, name: t(`filter.period.${v.name}`, { ns: 'appLog' }) }))} - className="mt-0 !w-40" + className="mt-0 w-40!" notClearable={true} onSelect={handleSelect} defaultValue="2" diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx index 986170728ff..a4bf0251393 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx @@ -70,7 +70,7 @@ const RangeSelector: FC = ({ return ( ({ ...v, name: t(`filter.period.${v.name}`, { ns: 'appLog' }) }))} - className="mt-0 !w-40" + className="mt-0 w-40!" notClearable={true} onSelect={handleSelectRange} defaultValue={0} diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx index 8429f8a3a91..17ca5d78cf5 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx @@ -51,7 +51,7 @@ const ConfigBtn: FC = ({ {children} - + diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx index cc2143faac5..72913b4934c 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx @@ -288,7 +288,7 @@ const ProviderConfigModal: FC = ({ {!isShowRemoveConfirm ? ( - +
@@ -304,7 +304,7 @@ const ProviderConfigModal: FC = ({ <> = ({ /> = ({ /> = ({ /> = ({ <> = ({ /> = ({ /> = ({ <> = ({ /> @@ -391,7 +391,7 @@ const ProviderConfigModal: FC = ({ <> = ({ /> = ({ /> = ({ <> = ({ /> = ({ /> = ({ <> = ({ /> = ({ /> = ({ <> = ({ /> = ({ /> = ({ <> = ({ <> = ({ /> = ({ /> = ({ <> = ({ /> = ({ /> = ({
- {isChosen &&
{t(`${I18N_PREFIX}.inUse`, { ns: 'app' })}
} + {isChosen &&
{t(`${I18N_PREFIX}.inUse`, { ns: 'app' })}
}
{!readOnly && (
diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx index 137fff05df3..9bf1ddc50dc 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx @@ -10,7 +10,7 @@ type Props = { } const sizeClassMap = { - lg: 'w-9 h-9 p-2 rounded-[10px]', + lg: 'w-9 h-9 p-2 radius-lg', md: 'w-6 h-6 p-1 rounded-lg', } diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/style.module.css b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/style.module.css index 45c7d197b40..1f1aca2d114 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/style.module.css +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/style.module.css @@ -1,3 +1,5 @@ +@reference "../../../../styles/globals.css"; + .app { flex-grow: 1; height: 0; diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/documents/style.module.css b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/documents/style.module.css index 67a9fe3bf5d..955f5d593b8 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/documents/style.module.css +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/documents/style.module.css @@ -1,3 +1,5 @@ +@reference "../../../../../styles/globals.css"; + .logTable td { padding: 7px 8px; box-sizing: border-box; diff --git a/web/app/(humanInputLayout)/form/[token]/form.tsx b/web/app/(humanInputLayout)/form/[token]/form.tsx index 035da6be8ab..221420aadef 100644 --- a/web/app/(humanInputLayout)/form/[token]/form.tsx +++ b/web/app/(humanInputLayout)/form/[token]/form.tsx @@ -101,7 +101,7 @@ const FormContent = () => { return (
-
+
@@ -129,7 +129,7 @@ const FormContent = () => { return (
-
+
@@ -157,7 +157,7 @@ const FormContent = () => { return (
-
+
@@ -185,7 +185,7 @@ const FormContent = () => { return (
-
+
@@ -211,7 +211,7 @@ const FormContent = () => { return (
-
+
@@ -248,7 +248,7 @@ const FormContent = () => {
{site.title}
-
+
{contentList.map((content, index) => (
-
+
router.back()} className="flex h-9 cursor-pointer items-center justify-center text-text-tertiary">
diff --git a/web/app/(shareLayout)/webapp-reset-password/page.tsx b/web/app/(shareLayout)/webapp-reset-password/page.tsx index 0cdfb4ec11c..a25b4bb4ef3 100644 --- a/web/app/(shareLayout)/webapp-reset-password/page.tsx +++ b/web/app/(shareLayout)/webapp-reset-password/page.tsx @@ -84,7 +84,7 @@ export default function CheckCode() {
-
+
diff --git a/web/app/(shareLayout)/webapp-signin/check-code/page.tsx b/web/app/(shareLayout)/webapp-signin/check-code/page.tsx index f209ad9e5c8..e2296c5d20a 100644 --- a/web/app/(shareLayout)/webapp-signin/check-code/page.tsx +++ b/web/app/(shareLayout)/webapp-signin/check-code/page.tsx @@ -127,7 +127,7 @@ export default function CheckCode() {
-
+
router.back()} className="flex h-9 cursor-pointer items-center justify-center text-text-tertiary">
diff --git a/web/app/(shareLayout)/webapp-signin/normalForm.tsx b/web/app/(shareLayout)/webapp-signin/normalForm.tsx index 7ee08d66aee..ed97e648065 100644 --- a/web/app/(shareLayout)/webapp-signin/normalForm.tsx +++ b/web/app/(shareLayout)/webapp-signin/normalForm.tsx @@ -55,7 +55,7 @@ const NormalForm = () => { return (
-
+
@@ -71,7 +71,7 @@ const NormalForm = () => { return (
-
+
@@ -87,7 +87,7 @@ const NormalForm = () => { return (
-
+
@@ -119,7 +119,7 @@ const NormalForm = () => { {showORLine && (
{t('or', { ns: 'login' })} @@ -154,7 +154,7 @@ const NormalForm = () => { } {allMethodsAreDisabled && ( <> -
+
@@ -163,7 +163,7 @@ const NormalForm = () => {
diff --git a/web/app/account/(commonLayout)/account-page/AvatarWithEdit.tsx b/web/app/account/(commonLayout)/account-page/AvatarWithEdit.tsx index 25e529a2210..ccd2dd53cc5 100644 --- a/web/app/account/(commonLayout)/account-page/AvatarWithEdit.tsx +++ b/web/app/account/(commonLayout)/account-page/AvatarWithEdit.tsx @@ -133,7 +133,7 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
!open && setIsShowAvatarPicker(false)}> - + @@ -150,7 +150,7 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => { !open && setIsShowDeleteConfirm(false)}> - +
{t('avatar.deleteTitle', { ns: 'common' })}

{t('avatar.deleteDescription', { ns: 'common' })}

diff --git a/web/app/account/(commonLayout)/account-page/email-change-modal.tsx b/web/app/account/(commonLayout)/account-page/email-change-modal.tsx index 2e2d61f2f93..9eab0477322 100644 --- a/web/app/account/(commonLayout)/account-page/email-change-modal.tsx +++ b/web/app/account/(commonLayout)/account-page/email-change-modal.tsx @@ -182,7 +182,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => { return ( !open && onClose()}> - +
@@ -203,14 +203,14 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
-
+

{t('account.account', { ns: 'common' })}

@@ -40,7 +40,7 @@ const Header = () => {

{t('account.studio', { ns: 'common' })}

-
+
diff --git a/web/app/account/oauth/authorize/page.tsx b/web/app/account/oauth/authorize/page.tsx index 670f6ec5936..2c849fd5425 100644 --- a/web/app/account/oauth/authorize/page.tsx +++ b/web/app/account/oauth/authorize/page.tsx @@ -118,14 +118,14 @@ export default function OAuthAuthorize() {
{authAppInfo?.app_icon && (
- app icon + app icon
)}
{isLoggedIn &&
{t('connect', { ns: 'oauth' })}
} -
{authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('unknownApp', { ns: 'oauth' })}
+
{authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('unknownApp', { ns: 'oauth' })}
{!isLoggedIn &&
{t('tips.notLoggedIn', { ns: 'oauth' })}
}
{isLoggedIn ? `${authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('unknownApp', { ns: 'oauth' })} ${t('tips.loggedIn', { ns: 'oauth' })}` : t('tips.needLogin', { ns: 'oauth' })}
diff --git a/web/app/activate/activateForm.tsx b/web/app/activate/activateForm.tsx index 418d3b8bb1d..d5274d52f06 100644 --- a/web/app/activate/activateForm.tsx +++ b/web/app/activate/activateForm.tsx @@ -55,11 +55,11 @@ const ActivateForm = () => { {checkRes && !checkRes.is_valid && (
-
🤷‍♂️
+
🤷‍♂️

{t('invalid', { ns: 'login' })}

diff --git a/web/app/components/app-sidebar/app-info/app-info-detail-panel.tsx b/web/app/components/app-sidebar/app-info/app-info-detail-panel.tsx index 70dcb8df704..4aacc0cdb16 100644 --- a/web/app/components/app-sidebar/app-info/app-info-detail-panel.tsx +++ b/web/app/components/app-sidebar/app-info/app-info-detail-panel.tsx @@ -97,7 +97,7 @@ const AppInfoDetailPanel = ({
@@ -116,7 +116,7 @@ const AppInfoDetailPanel = ({
{appDetail.description && ( -
+
{appDetail.description}
)} diff --git a/web/app/components/app-sidebar/app-info/app-operations.tsx b/web/app/components/app-sidebar/app-info/app-operations.tsx index 78dd6f00436..e3cf233fea2 100644 --- a/web/app/components/app-sidebar/app-info/app-operations.tsx +++ b/web/app/components/app-sidebar/app-info/app-operations.tsx @@ -130,7 +130,7 @@ const AppOperations = ({ data-targetid={operation.id} size="small" variant="secondary" - className="gap-[1px]" + className="gap-px" tabIndex={-1} > {cloneElement(operation.icon, { className: 'h-3.5 w-3.5 text-components-button-secondary-text' })} @@ -143,7 +143,7 @@ const AppOperations = ({ id="more-measure" size="small" variant="secondary" - className="gap-[1px]" + className="gap-px" tabIndex={-1} > @@ -159,7 +159,7 @@ const AppOperations = ({ data-targetid={operation.id} size="small" variant="secondary" - className="gap-[1px]" + className="gap-px" onClick={operation.onClick} > {cloneElement(operation.icon, { className: 'h-3.5 w-3.5 text-components-button-secondary-text' })} @@ -179,7 +179,7 @@ const AppOperations = ({ - -
+ +
{moreOperations.map(item => item.type === 'divider' ? (
diff --git a/web/app/components/app-sidebar/app-sidebar-dropdown.tsx b/web/app/components/app-sidebar/app-sidebar-dropdown.tsx index 87632ba6475..d1a3ec935bb 100644 --- a/web/app/components/app-sidebar/app-sidebar-dropdown.tsx +++ b/web/app/components/app-sidebar/app-sidebar-dropdown.tsx @@ -60,7 +60,7 @@ const AppSidebarDropdown = ({ navigation }: Props) => { }} > -
+
{
- +
, + app: , api: (
), - dataset: , + dataset: , webapp: (
), - notion: , + notion: , } export default function AppBasic({ icon, icon_background, name, isExternal, type, hoverTip, textStyle, isExtraInLine, mode = 'expand', iconType = 'app', hideType }: IAppBasicProps) { diff --git a/web/app/components/app-sidebar/dataset-info/dropdown.tsx b/web/app/components/app-sidebar/dataset-info/dropdown.tsx index 1d1208e7d3c..6c70f96b349 100644 --- a/web/app/components/app-sidebar/dataset-info/dropdown.tsx +++ b/web/app/components/app-sidebar/dataset-info/dropdown.tsx @@ -119,7 +119,7 @@ const DropDown = ({ - +
@@ -132,7 +132,7 @@ const DatasetSidebarDropdown = ({