diff --git a/.gitignore b/.gitignore index f703fc02e9..53dea88899 100644 --- a/.gitignore +++ b/.gitignore @@ -212,7 +212,7 @@ api/.vscode # pnpm /.pnpm-store -/node_modules +node_modules .vite-hooks/_ # plugin migrate diff --git a/.vite-hooks/pre-commit b/.vite-hooks/pre-commit index 54e09f80d6..a4b5531ab1 100755 --- a/.vite-hooks/pre-commit +++ b/.vite-hooks/pre-commit @@ -89,6 +89,12 @@ if $web_modified; then echo "No staged TypeScript changes detected, skipping type-check:tsgo" fi + echo "Running knip" + if ! pnpm run knip; then + echo "Knip check failed. Please run 'pnpm run knip' to fix the errors." + exit 1 + fi + echo "Running unit tests check" modified_files=$(git diff --cached --name-only -- utils | grep -v '\.spec\.ts$' || true) diff --git a/api/controllers/console/app/app_import.py b/api/controllers/console/app/app_import.py index 16e1fa3245..c2805f765b 100644 --- a/api/controllers/console/app/app_import.py +++ b/api/controllers/console/app/app_import.py @@ -1,6 +1,6 @@ from flask_restx import Resource, fields, marshal_with from pydantic import BaseModel, Field -from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import Session, sessionmaker from controllers.console.app.wraps import get_app_model from controllers.console.wraps import ( @@ -71,7 +71,7 @@ class AppImportApi(Resource): args = AppImportPayload.model_validate(console_ns.payload) # Create service with session - with sessionmaker(db.engine).begin() as session: + with Session(db.engine) as session: import_service = AppDslService(session) # Import app account = current_user diff --git a/api/controllers/console/app/workflow_draft_variable.py b/api/controllers/console/app/workflow_draft_variable.py index 366f145360..f6d076320c 100644 --- a/api/controllers/console/app/workflow_draft_variable.py +++ b/api/controllers/console/app/workflow_draft_variable.py @@ -193,7 +193,7 @@ workflow_draft_variable_list_model = console_ns.model( ) -def _api_prerequisite(f: Callable[..., Any]) -> Callable[..., Any]: +def _api_prerequisite[**P, R](f: Callable[P, R]) -> Callable[P, R | Response]: """Common prerequisites for all draft workflow variable APIs. It ensures the following conditions are satisfied: @@ -210,7 +210,7 @@ def _api_prerequisite(f: Callable[..., Any]) -> Callable[..., Any]: @edit_permission_required @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @wraps(f) - def wrapper(*args: Any, **kwargs: Any): + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | Response: return f(*args, **kwargs) return wrapper diff --git a/api/controllers/console/app/wraps.py b/api/controllers/console/app/wraps.py index bd6f019eac..c9cf08072a 100644 --- a/api/controllers/console/app/wraps.py +++ b/api/controllers/console/app/wraps.py @@ -1,6 +1,6 @@ from collections.abc import Callable from functools import wraps -from typing import Any +from typing import overload from sqlalchemy import select @@ -23,14 +23,30 @@ def _load_app_model_with_trial(app_id: str) -> App | None: return app_model -def get_app_model( - view: Callable[..., Any] | None = None, +@overload +def get_app_model[**P, R]( + view: Callable[P, R], *, mode: AppMode | list[AppMode] | None = None, -) -> Callable[..., Any] | Callable[[Callable[..., Any]], Callable[..., Any]]: - def decorator(view_func: Callable[..., Any]) -> Callable[..., Any]: +) -> Callable[P, R]: ... + + +@overload +def get_app_model[**P, R]( + view: None = None, + *, + mode: AppMode | list[AppMode] | None = None, +) -> Callable[[Callable[P, R]], Callable[P, R]]: ... + + +def get_app_model[**P, R]( + view: Callable[P, R] | None = None, + *, + mode: AppMode | list[AppMode] | None = None, +) -> Callable[P, R] | Callable[[Callable[P, R]], Callable[P, R]]: + def decorator(view_func: Callable[P, R]) -> Callable[P, R]: @wraps(view_func) - def decorated_view(*args: Any, **kwargs: Any): + def decorated_view(*args: P.args, **kwargs: P.kwargs) -> R: if not kwargs.get("app_id"): raise ValueError("missing app_id in path parameters") @@ -68,14 +84,30 @@ def get_app_model( return decorator(view) -def get_app_model_with_trial( - view: Callable[..., Any] | None = None, +@overload +def get_app_model_with_trial[**P, R]( + view: Callable[P, R], *, mode: AppMode | list[AppMode] | None = None, -) -> Callable[..., Any] | Callable[[Callable[..., Any]], Callable[..., Any]]: - def decorator(view_func: Callable[..., Any]) -> Callable[..., Any]: +) -> Callable[P, R]: ... + + +@overload +def get_app_model_with_trial[**P, R]( + view: None = None, + *, + mode: AppMode | list[AppMode] | None = None, +) -> Callable[[Callable[P, R]], Callable[P, R]]: ... + + +def get_app_model_with_trial[**P, R]( + view: Callable[P, R] | None = None, + *, + mode: AppMode | list[AppMode] | None = None, +) -> Callable[P, R] | Callable[[Callable[P, R]], Callable[P, R]]: + def decorator(view_func: Callable[P, R]) -> Callable[P, R]: @wraps(view_func) - def decorated_view(*args: Any, **kwargs: Any): + def decorated_view(*args: P.args, **kwargs: P.kwargs) -> R: if not kwargs.get("app_id"): raise ValueError("missing app_id in path parameters") diff --git a/api/controllers/console/datasets/data_source.py b/api/controllers/console/datasets/data_source.py index ac14349045..e623722b23 100644 --- a/api/controllers/console/datasets/data_source.py +++ b/api/controllers/console/datasets/data_source.py @@ -158,10 +158,11 @@ class DataSourceApi(Resource): @login_required @account_initialization_required def patch(self, binding_id, action: Literal["enable", "disable"]): + _, current_tenant_id = current_account_with_tenant() binding_id = str(binding_id) with sessionmaker(db.engine, expire_on_commit=False).begin() as session: data_source_binding = session.execute( - select(DataSourceOauthBinding).filter_by(id=binding_id) + select(DataSourceOauthBinding).filter_by(id=binding_id, tenant_id=current_tenant_id) ).scalar_one_or_none() if data_source_binding is None: raise NotFound("Data source binding not found.") diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py index d635dcb530..93feec0019 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py @@ -1,4 +1,5 @@ import logging +from collections.abc import Callable from typing import Any, NoReturn from flask import Response, request @@ -55,7 +56,7 @@ class WorkflowDraftVariablePatchPayload(BaseModel): register_schema_models(console_ns, WorkflowDraftVariablePatchPayload) -def _api_prerequisite(f): +def _api_prerequisite[**P, R](f: Callable[P, R]) -> Callable[P, R | Response]: """Common prerequisites for all draft workflow variable APIs. It ensures the following conditions are satisfied: @@ -70,7 +71,7 @@ def _api_prerequisite(f): @login_required @account_initialization_required @get_rag_pipeline - def wrapper(*args, **kwargs): + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | Response: if not isinstance(current_user, Account) or not current_user.has_edit_permission: raise Forbidden() return f(*args, **kwargs) diff --git a/api/controllers/service_api/wraps.py b/api/controllers/service_api/wraps.py index 2dd916bb31..b9389ccc47 100644 --- a/api/controllers/service_api/wraps.py +++ b/api/controllers/service_api/wraps.py @@ -1,9 +1,10 @@ +import inspect import logging import time from collections.abc import Callable from enum import StrEnum, auto from functools import wraps -from typing import Any, cast, overload +from typing import cast, overload from flask import current_app, request from flask_login import user_logged_in @@ -230,94 +231,73 @@ def cloud_edition_billing_rate_limit_check[**P, R]( return interceptor -def validate_dataset_token( - view: Callable[..., Any] | None = None, -) -> Callable[..., Any] | Callable[[Callable[..., Any]], Callable[..., Any]]: - def decorator(view_func: Callable[..., Any]) -> Callable[..., Any]: - @wraps(view_func) - def decorated(*args: Any, **kwargs: Any) -> Any: - api_token = validate_and_get_api_token("dataset") +def validate_dataset_token[R](view: Callable[..., R]) -> Callable[..., R]: + positional_parameters = [ + parameter + for parameter in inspect.signature(view).parameters.values() + if parameter.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD) + ] + expects_bound_instance = bool(positional_parameters and positional_parameters[0].name in {"self", "cls"}) - # get url path dataset_id from positional args or kwargs - # Flask passes URL path parameters as positional arguments - dataset_id = None + @wraps(view) + def decorated(*args: object, **kwargs: object) -> R: + api_token = validate_and_get_api_token("dataset") - # First try to get from kwargs (explicit parameter) - dataset_id = kwargs.get("dataset_id") + # Flask may pass URL path parameters positionally, so inspect both kwargs and args. + dataset_id = kwargs.get("dataset_id") - # If not in kwargs, try to extract from positional args - if not dataset_id and args: - # For class methods: args[0] is self, args[1] is dataset_id (if exists) - # Check if first arg is likely a class instance (has __dict__ or __class__) - if len(args) > 1 and hasattr(args[0], "__dict__"): - # This is a class method, dataset_id should be in args[1] - potential_id = args[1] - # Validate it's a string-like UUID, not another object - try: - # Try to convert to string and check if it's a valid UUID format - str_id = str(potential_id) - # Basic check: UUIDs are 36 chars with hyphens - if len(str_id) == 36 and str_id.count("-") == 4: - dataset_id = str_id - except Exception: - logger.exception("Failed to parse dataset_id from class method args") - elif len(args) > 0: - # Not a class method, check if args[0] looks like a UUID - potential_id = args[0] - try: - str_id = str(potential_id) - if len(str_id) == 36 and str_id.count("-") == 4: - dataset_id = str_id - except Exception: - logger.exception("Failed to parse dataset_id from positional args") + if not dataset_id and args: + potential_id = args[0] + try: + str_id = str(potential_id) + if len(str_id) == 36 and str_id.count("-") == 4: + dataset_id = str_id + except Exception: + logger.exception("Failed to parse dataset_id from positional args") - # Validate dataset if dataset_id is provided - if dataset_id: - dataset_id = str(dataset_id) - dataset = db.session.scalar( - select(Dataset) - .where( - Dataset.id == dataset_id, - Dataset.tenant_id == api_token.tenant_id, - ) - .limit(1) + if dataset_id: + dataset_id = str(dataset_id) + dataset = db.session.scalar( + select(Dataset) + .where( + Dataset.id == dataset_id, + Dataset.tenant_id == api_token.tenant_id, ) - if not dataset: - raise NotFound("Dataset not found.") - if not dataset.enable_api: - raise Forbidden("Dataset api access is not enabled.") - tenant_account_join = db.session.execute( - select(Tenant, TenantAccountJoin) - .where(Tenant.id == api_token.tenant_id) - .where(TenantAccountJoin.tenant_id == Tenant.id) - .where(TenantAccountJoin.role.in_(["owner"])) - .where(Tenant.status == TenantStatus.NORMAL) - ).one_or_none() # TODO: only owner information is required, so only one is returned. - if tenant_account_join: - tenant, ta = tenant_account_join - account = db.session.get(Account, ta.account_id) - # Login admin - if account: - account.current_tenant = tenant - current_app.login_manager._update_request_context_with_user(account) # type: ignore - user_logged_in.send(current_app._get_current_object(), user=current_user) # type: ignore - else: - raise Unauthorized("Tenant owner account does not exist.") + .limit(1) + ) + if not dataset: + raise NotFound("Dataset not found.") + if not dataset.enable_api: + raise Forbidden("Dataset api access is not enabled.") + + tenant_account_join = db.session.execute( + select(Tenant, TenantAccountJoin) + .where(Tenant.id == api_token.tenant_id) + .where(TenantAccountJoin.tenant_id == Tenant.id) + .where(TenantAccountJoin.role.in_(["owner"])) + .where(Tenant.status == TenantStatus.NORMAL) + ).one_or_none() # TODO: only owner information is required, so only one is returned. + if tenant_account_join: + tenant, ta = tenant_account_join + account = db.session.get(Account, ta.account_id) + # Login admin + if account: + account.current_tenant = tenant + current_app.login_manager._update_request_context_with_user(account) # type: ignore + user_logged_in.send(current_app._get_current_object(), user=current_user) # type: ignore else: - raise Unauthorized("Tenant does not exist.") - if args and isinstance(args[0], Resource): - return view_func(args[0], api_token.tenant_id, *args[1:], **kwargs) + raise Unauthorized("Tenant owner account does not exist.") + else: + raise Unauthorized("Tenant does not exist.") - return view_func(api_token.tenant_id, *args, **kwargs) + if expects_bound_instance: + if not args: + raise TypeError("validate_dataset_token expected a bound resource instance.") + return view(args[0], api_token.tenant_id, *args[1:], **kwargs) - return decorated + return view(api_token.tenant_id, *args, **kwargs) - if view: - return decorator(view) - - # if view is None, it means that the decorator is used without parentheses - # use the decorator as a function for method_decorators - return decorator + return decorated def validate_and_get_api_token(scope: str | None = None): diff --git a/api/controllers/trigger/webhook.py b/api/controllers/trigger/webhook.py index eb579da5d4..213704383c 100644 --- a/api/controllers/trigger/webhook.py +++ b/api/controllers/trigger/webhook.py @@ -7,7 +7,7 @@ from werkzeug.exceptions import NotFound, RequestEntityTooLarge from controllers.trigger import bp from core.trigger.debug.event_bus import TriggerDebugEventBus from core.trigger.debug.events import WebhookDebugEvent, build_webhook_pool_key -from services.trigger.webhook_service import WebhookService +from services.trigger.webhook_service import RawWebhookDataDict, WebhookService logger = logging.getLogger(__name__) @@ -23,6 +23,7 @@ def _prepare_webhook_execution(webhook_id: str, is_debug: bool = False): webhook_id, is_debug=is_debug ) + webhook_data: RawWebhookDataDict try: # Use new unified extraction and validation webhook_data = WebhookService.extract_and_validate_webhook_data(webhook_trigger, node_config) diff --git a/api/core/logging/structured_formatter.py b/api/core/logging/structured_formatter.py index 4295d2dd34..9baf6c4682 100644 --- a/api/core/logging/structured_formatter.py +++ b/api/core/logging/structured_formatter.py @@ -3,13 +3,19 @@ import logging import traceback from datetime import UTC, datetime -from typing import Any +from typing import Any, TypedDict import orjson from configs import dify_config +class IdentityDict(TypedDict, total=False): + tenant_id: str + user_id: str + user_type: str + + class StructuredJSONFormatter(logging.Formatter): """ JSON log formatter following the specified schema: @@ -84,7 +90,7 @@ class StructuredJSONFormatter(logging.Formatter): return log_dict - def _extract_identity(self, record: logging.LogRecord) -> dict[str, str] | None: + def _extract_identity(self, record: logging.LogRecord) -> IdentityDict | None: tenant_id = getattr(record, "tenant_id", None) user_id = getattr(record, "user_id", None) user_type = getattr(record, "user_type", None) @@ -92,7 +98,7 @@ class StructuredJSONFormatter(logging.Formatter): if not any([tenant_id, user_id, user_type]): return None - identity: dict[str, str] = {} + identity: IdentityDict = {} if tenant_id: identity["tenant_id"] = tenant_id if user_id: diff --git a/api/core/mcp/session/base_session.py b/api/core/mcp/session/base_session.py index e50fd42198..0b3aa79838 100644 --- a/api/core/mcp/session/base_session.py +++ b/api/core/mcp/session/base_session.py @@ -4,7 +4,7 @@ from collections.abc import Callable from concurrent.futures import Future, ThreadPoolExecutor, TimeoutError from datetime import timedelta from types import TracebackType -from typing import Any, Self, cast +from typing import Any, Self from httpx import HTTPStatusError from pydantic import BaseModel @@ -338,12 +338,11 @@ class BaseSession[ validated_request = self._receive_request_type.model_validate( message.message.root.model_dump(by_alias=True, mode="json", exclude_none=True) ) - validated_request = cast(ReceiveRequestT, validated_request) responder = RequestResponder[ReceiveRequestT, SendResultT]( request_id=message.message.root.id, request_meta=validated_request.root.params.meta if validated_request.root.params else None, - request=validated_request, + request=validated_request, # type: ignore[arg-type] # mypy can't narrow constrained TypeVar from model_validate session=self, on_complete=lambda r: self._in_flight.pop(r.request_id, None), ) @@ -359,15 +358,14 @@ class BaseSession[ notification = self._receive_notification_type.model_validate( message.message.root.model_dump(by_alias=True, mode="json", exclude_none=True) ) - notification = cast(ReceiveNotificationT, notification) # Handle cancellation notifications if isinstance(notification.root, CancelledNotification): cancelled_id = notification.root.params.requestId if cancelled_id in self._in_flight: self._in_flight[cancelled_id].cancel() else: - self._received_notification(notification) - self._handle_incoming(notification) + self._received_notification(notification) # type: ignore[arg-type] + self._handle_incoming(notification) # type: ignore[arg-type] except Exception as e: # For other validation errors, log and continue logger.warning("Failed to validate notification: %s. Message was: %s", e, message.message.root) diff --git a/api/dify_app.py b/api/dify_app.py index d6deb8e007..bbe3f33787 100644 --- a/api/dify_app.py +++ b/api/dify_app.py @@ -1,5 +1,14 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from flask import Flask +if TYPE_CHECKING: + from extensions.ext_login import DifyLoginManager + class DifyApp(Flask): - pass + """Flask application type with Dify-specific extension attributes.""" + + login_manager: DifyLoginManager diff --git a/api/extensions/ext_login.py b/api/extensions/ext_login.py index 02e50a90fc..bc59eaca63 100644 --- a/api/extensions/ext_login.py +++ b/api/extensions/ext_login.py @@ -1,7 +1,8 @@ import json +from typing import cast import flask_login -from flask import Response, request +from flask import Request, Response, request from flask_login import user_loaded_from_request, user_logged_in from sqlalchemy import select from werkzeug.exceptions import NotFound, Unauthorized @@ -16,13 +17,35 @@ from models import Account, Tenant, TenantAccountJoin from models.model import AppMCPServer, EndUser from services.account_service import AccountService -login_manager = flask_login.LoginManager() +type LoginUser = Account | EndUser + + +class DifyLoginManager(flask_login.LoginManager): + """Project-specific Flask-Login manager with a stable unauthorized contract. + + Dify registers `unauthorized_handler` below to always return a JSON `Response`. + Overriding this method lets callers rely on that narrower return type instead of + Flask-Login's broader callback contract. + """ + + def unauthorized(self) -> Response: + """Return the registered unauthorized handler result as a Flask `Response`.""" + return cast(Response, super().unauthorized()) + + def load_user_from_request_context(self) -> None: + """Populate Flask-Login's request-local user cache for the current request.""" + self._load_user() + + +login_manager = DifyLoginManager() # Flask-Login configuration @login_manager.request_loader -def load_user_from_request(request_from_flask_login): +def load_user_from_request(request_from_flask_login: Request) -> LoginUser | None: """Load user based on the request.""" + del request_from_flask_login + # Skip authentication for documentation endpoints if dify_config.SWAGGER_UI_ENABLED and request.path.endswith((dify_config.SWAGGER_UI_PATH, "/swagger.json")): return None @@ -100,10 +123,12 @@ def load_user_from_request(request_from_flask_login): raise NotFound("End user not found.") return end_user + return None + @user_logged_in.connect @user_loaded_from_request.connect -def on_user_logged_in(_sender, user): +def on_user_logged_in(_sender: object, user: LoginUser) -> None: """Called when a user logged in. Note: AccountService.load_logged_in_account will populate user.current_tenant_id @@ -114,8 +139,10 @@ def on_user_logged_in(_sender, user): @login_manager.unauthorized_handler -def unauthorized_handler(): +def unauthorized_handler() -> Response: """Handle unauthorized requests.""" + # Keep this as a concrete `Response`; `DifyLoginManager.unauthorized()` narrows + # Flask-Login's callback contract based on this override. return Response( json.dumps({"code": "unauthorized", "message": "Unauthorized."}), status=401, @@ -123,5 +150,5 @@ def unauthorized_handler(): ) -def init_app(app: DifyApp): +def init_app(app: DifyApp) -> None: login_manager.init_app(app) diff --git a/api/libs/login.py b/api/libs/login.py index 68a2050747..067597cb3c 100644 --- a/api/libs/login.py +++ b/api/libs/login.py @@ -2,19 +2,19 @@ from __future__ import annotations from collections.abc import Callable from functools import wraps -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast -from flask import current_app, g, has_request_context, request +from flask import Response, current_app, g, has_request_context, request from flask_login.config import EXEMPT_METHODS from werkzeug.local import LocalProxy from configs import dify_config +from dify_app import DifyApp +from extensions.ext_login import DifyLoginManager from libs.token import check_csrf_token from models import Account if TYPE_CHECKING: - from flask.typing import ResponseReturnValue - from models.model import EndUser @@ -29,7 +29,13 @@ def _resolve_current_user() -> EndUser | Account | None: return get_current_object() if callable(get_current_object) else user_proxy # type: ignore -def current_account_with_tenant(): +def _get_login_manager() -> DifyLoginManager: + """Return the project login manager with Dify's narrowed unauthorized contract.""" + app = cast(DifyApp, current_app) + return app.login_manager + + +def current_account_with_tenant() -> tuple[Account, str]: """ Resolve the underlying account for the current user proxy and ensure tenant context exists. Allows tests to supply plain Account mocks without the LocalProxy helper. @@ -42,7 +48,7 @@ def current_account_with_tenant(): return user, user.current_tenant_id -def login_required[**P, R](func: Callable[P, R]) -> Callable[P, R | ResponseReturnValue]: +def login_required[**P, R](func: Callable[P, R]) -> Callable[P, R | Response]: """ If you decorate a view with this, it will ensure that the current user is logged in and authenticated before calling the actual view. (If they are @@ -77,13 +83,16 @@ def login_required[**P, R](func: Callable[P, R]) -> Callable[P, R | ResponseRetu """ @wraps(func) - def decorated_view(*args: P.args, **kwargs: P.kwargs) -> R | ResponseReturnValue: + def decorated_view(*args: P.args, **kwargs: P.kwargs) -> R | Response: if request.method in EXEMPT_METHODS or dify_config.LOGIN_DISABLED: return current_app.ensure_sync(func)(*args, **kwargs) user = _resolve_current_user() if user is None or not user.is_authenticated: - return current_app.login_manager.unauthorized() # type: ignore + # `DifyLoginManager` guarantees that the registered unauthorized handler + # is surfaced here as a concrete Flask `Response`. + unauthorized_response: Response = _get_login_manager().unauthorized() + return unauthorized_response g._login_user = user # we put csrf validation here for less conflicts # TODO: maybe find a better place for it. @@ -96,7 +105,7 @@ def login_required[**P, R](func: Callable[P, R]) -> Callable[P, R | ResponseRetu def _get_user() -> EndUser | Account | None: if has_request_context(): if "_login_user" not in g: - current_app.login_manager._load_user() # type: ignore + _get_login_manager().load_user_from_request_context() return g._login_user diff --git a/api/pyproject.toml b/api/pyproject.toml index 863b61cad1..cbd9af151b 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -171,7 +171,7 @@ dev = [ "sseclient-py>=1.8.0", "pytest-timeout>=2.4.0", "pytest-xdist>=3.8.0", - "pyrefly>=0.57.1", + "pyrefly>=0.59.1", ] ############################################################ diff --git a/api/services/account_service.py b/api/services/account_service.py index 29b1444730..d02f244428 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -8,7 +8,7 @@ from hashlib import sha256 from typing import Any, TypedDict, cast from pydantic import BaseModel, TypeAdapter -from sqlalchemy import func, select +from sqlalchemy import delete, func, select from sqlalchemy.orm import Session @@ -144,22 +144,26 @@ class AccountService: @staticmethod def load_user(user_id: str) -> None | Account: - account = db.session.query(Account).filter_by(id=user_id).first() + account = db.session.get(Account, user_id) if not account: return None if account.status == AccountStatus.BANNED: raise Unauthorized("Account is banned.") - current_tenant = db.session.query(TenantAccountJoin).filter_by(account_id=account.id, current=True).first() + current_tenant = db.session.scalar( + select(TenantAccountJoin) + .where(TenantAccountJoin.account_id == account.id, TenantAccountJoin.current == True) + .limit(1) + ) if current_tenant: account.set_tenant_id(current_tenant.tenant_id) else: - available_ta = ( - db.session.query(TenantAccountJoin) - .filter_by(account_id=account.id) + available_ta = db.session.scalar( + select(TenantAccountJoin) + .where(TenantAccountJoin.account_id == account.id) .order_by(TenantAccountJoin.id.asc()) - .first() + .limit(1) ) if not available_ta: return None @@ -195,7 +199,7 @@ class AccountService: def authenticate(email: str, password: str, invite_token: str | None = None) -> Account: """authenticate account with email and password""" - account = db.session.query(Account).filter_by(email=email).first() + account = db.session.scalar(select(Account).where(Account.email == email).limit(1)) if not account: raise AccountPasswordError("Invalid email or password.") @@ -371,8 +375,10 @@ class AccountService: """Link account integrate""" try: # Query whether there is an existing binding record for the same provider - account_integrate: AccountIntegrate | None = ( - db.session.query(AccountIntegrate).filter_by(account_id=account.id, provider=provider).first() + account_integrate: AccountIntegrate | None = db.session.scalar( + select(AccountIntegrate) + .where(AccountIntegrate.account_id == account.id, AccountIntegrate.provider == provider) + .limit(1) ) if account_integrate: @@ -416,7 +422,9 @@ class AccountService: def update_account_email(account: Account, email: str) -> Account: """Update account email""" account.email = email - account_integrate = db.session.query(AccountIntegrate).filter_by(account_id=account.id).first() + account_integrate = db.session.scalar( + select(AccountIntegrate).where(AccountIntegrate.account_id == account.id).limit(1) + ) if account_integrate: db.session.delete(account_integrate) db.session.add(account) @@ -818,7 +826,7 @@ class AccountService: ) ) - account = db.session.query(Account).where(Account.email == email).first() + account = db.session.scalar(select(Account).where(Account.email == email).limit(1)) if not account: return None @@ -1018,7 +1026,7 @@ class AccountService: @staticmethod def check_email_unique(email: str) -> bool: - return db.session.query(Account).filter_by(email=email).first() is None + return db.session.scalar(select(Account).where(Account.email == email).limit(1)) is None class TenantService: @@ -1384,10 +1392,10 @@ class RegisterService: db.session.add(dify_setup) db.session.commit() except Exception as e: - db.session.query(DifySetup).delete() - db.session.query(TenantAccountJoin).delete() - db.session.query(Account).delete() - db.session.query(Tenant).delete() + db.session.execute(delete(DifySetup)) + db.session.execute(delete(TenantAccountJoin)) + db.session.execute(delete(Account)) + db.session.execute(delete(Tenant)) db.session.commit() logger.exception("Setup account failed, email: %s, name: %s", email, name) @@ -1488,7 +1496,11 @@ class RegisterService: TenantService.switch_tenant(account, tenant.id) else: TenantService.check_member_permission(tenant, inviter, account, "add") - ta = db.session.query(TenantAccountJoin).filter_by(tenant_id=tenant.id, account_id=account.id).first() + ta = db.session.scalar( + select(TenantAccountJoin) + .where(TenantAccountJoin.tenant_id == tenant.id, TenantAccountJoin.account_id == account.id) + .limit(1) + ) if not ta: TenantService.create_tenant_member(tenant, account, role) @@ -1545,21 +1557,18 @@ class RegisterService: if not invitation_data: return None - tenant = ( - db.session.query(Tenant) - .where(Tenant.id == invitation_data["workspace_id"], Tenant.status == "normal") - .first() + tenant = db.session.scalar( + select(Tenant).where(Tenant.id == invitation_data["workspace_id"], Tenant.status == "normal").limit(1) ) if not tenant: return None - tenant_account = ( - db.session.query(Account, TenantAccountJoin.role) + tenant_account = db.session.execute( + select(Account, TenantAccountJoin.role) .join(TenantAccountJoin, Account.id == TenantAccountJoin.account_id) .where(Account.email == invitation_data["email"], TenantAccountJoin.tenant_id == tenant.id) - .first() - ) + ).first() if not tenant_account: return None diff --git a/api/services/annotation_service.py b/api/services/annotation_service.py index 8ebc87a670..b472a26950 100644 --- a/api/services/annotation_service.py +++ b/api/services/annotation_service.py @@ -4,6 +4,8 @@ import uuid import pandas as pd logger = logging.getLogger(__name__) +from typing import TypedDict + from sqlalchemy import or_, select from werkzeug.datastructures import FileStorage from werkzeug.exceptions import NotFound @@ -23,6 +25,27 @@ from tasks.annotation.enable_annotation_reply_task import enable_annotation_repl from tasks.annotation.update_annotation_to_index_task import update_annotation_to_index_task +class AnnotationJobStatusDict(TypedDict): + job_id: str + job_status: str + + +class EmbeddingModelDict(TypedDict): + embedding_provider_name: str + embedding_model_name: str + + +class AnnotationSettingDict(TypedDict): + id: str + enabled: bool + score_threshold: float + embedding_model: EmbeddingModelDict | dict + + +class AnnotationSettingDisabledDict(TypedDict): + enabled: bool + + class AppAnnotationService: @classmethod def up_insert_app_annotation_from_message(cls, args: dict, app_id: str) -> MessageAnnotation: @@ -85,7 +108,7 @@ class AppAnnotationService: return annotation @classmethod - def enable_app_annotation(cls, args: dict, app_id: str): + def enable_app_annotation(cls, args: dict, app_id: str) -> AnnotationJobStatusDict: enable_app_annotation_key = f"enable_app_annotation_{str(app_id)}" cache_result = redis_client.get(enable_app_annotation_key) if cache_result is not None: @@ -109,7 +132,7 @@ class AppAnnotationService: return {"job_id": job_id, "job_status": "waiting"} @classmethod - def disable_app_annotation(cls, app_id: str): + def disable_app_annotation(cls, app_id: str) -> AnnotationJobStatusDict: _, current_tenant_id = current_account_with_tenant() disable_app_annotation_key = f"disable_app_annotation_{str(app_id)}" cache_result = redis_client.get(disable_app_annotation_key) @@ -567,7 +590,7 @@ class AppAnnotationService: db.session.commit() @classmethod - def get_app_annotation_setting_by_app_id(cls, app_id: str): + def get_app_annotation_setting_by_app_id(cls, app_id: str) -> AnnotationSettingDict | AnnotationSettingDisabledDict: _, current_tenant_id = current_account_with_tenant() # get app info app = ( @@ -602,7 +625,9 @@ class AppAnnotationService: return {"enabled": False} @classmethod - def update_app_annotation_setting(cls, app_id: str, annotation_setting_id: str, args: dict): + def update_app_annotation_setting( + cls, app_id: str, annotation_setting_id: str, args: dict + ) -> AnnotationSettingDict: current_user, current_tenant_id = current_account_with_tenant() # get app info app = ( diff --git a/api/services/billing_service.py b/api/services/billing_service.py index 3c3e4aa6d2..c3ce48b6bc 100644 --- a/api/services/billing_service.py +++ b/api/services/billing_service.py @@ -32,6 +32,11 @@ class SubscriptionPlan(TypedDict): expiration_date: int +class KnowledgeRateLimitDict(TypedDict): + limit: int + subscription_plan: str + + class BillingService: base_url = os.environ.get("BILLING_API_URL", "BILLING_API_URL") secret_key = os.environ.get("BILLING_API_SECRET_KEY", "BILLING_API_SECRET_KEY") @@ -58,7 +63,7 @@ class BillingService: return usage_info @classmethod - def get_knowledge_rate_limit(cls, tenant_id: str): + def get_knowledge_rate_limit(cls, tenant_id: str) -> KnowledgeRateLimitDict: params = {"tenant_id": tenant_id} knowledge_rate_limit = cls._send_request("GET", "/subscription/knowledge-rate-limit", params=params) diff --git a/api/services/external_knowledge_service.py b/api/services/external_knowledge_service.py index 9a522ece52..b178718baa 100644 --- a/api/services/external_knowledge_service.py +++ b/api/services/external_knowledge_service.py @@ -5,7 +5,7 @@ from urllib.parse import urlparse import httpx from graphon.nodes.http_request.exc import InvalidHttpMethodError -from sqlalchemy import select +from sqlalchemy import func, select from constants import HIDDEN_VALUE from core.helper import ssrf_proxy @@ -103,8 +103,10 @@ class ExternalDatasetService: @staticmethod def get_external_knowledge_api(external_knowledge_api_id: str, tenant_id: str) -> ExternalKnowledgeApis: - external_knowledge_api: ExternalKnowledgeApis | None = ( - db.session.query(ExternalKnowledgeApis).filter_by(id=external_knowledge_api_id, tenant_id=tenant_id).first() + external_knowledge_api: ExternalKnowledgeApis | None = db.session.scalar( + select(ExternalKnowledgeApis) + .where(ExternalKnowledgeApis.id == external_knowledge_api_id, ExternalKnowledgeApis.tenant_id == tenant_id) + .limit(1) ) if external_knowledge_api is None: raise ValueError("api template not found") @@ -112,8 +114,10 @@ class ExternalDatasetService: @staticmethod def update_external_knowledge_api(tenant_id, user_id, external_knowledge_api_id, args) -> ExternalKnowledgeApis: - external_knowledge_api: ExternalKnowledgeApis | None = ( - db.session.query(ExternalKnowledgeApis).filter_by(id=external_knowledge_api_id, tenant_id=tenant_id).first() + external_knowledge_api: ExternalKnowledgeApis | None = db.session.scalar( + select(ExternalKnowledgeApis) + .where(ExternalKnowledgeApis.id == external_knowledge_api_id, ExternalKnowledgeApis.tenant_id == tenant_id) + .limit(1) ) if external_knowledge_api is None: raise ValueError("api template not found") @@ -132,8 +136,10 @@ class ExternalDatasetService: @staticmethod def delete_external_knowledge_api(tenant_id: str, external_knowledge_api_id: str): - external_knowledge_api = ( - db.session.query(ExternalKnowledgeApis).filter_by(id=external_knowledge_api_id, tenant_id=tenant_id).first() + external_knowledge_api = db.session.scalar( + select(ExternalKnowledgeApis) + .where(ExternalKnowledgeApis.id == external_knowledge_api_id, ExternalKnowledgeApis.tenant_id == tenant_id) + .limit(1) ) if external_knowledge_api is None: raise ValueError("api template not found") @@ -144,9 +150,12 @@ class ExternalDatasetService: @staticmethod def external_knowledge_api_use_check(external_knowledge_api_id: str) -> tuple[bool, int]: count = ( - db.session.query(ExternalKnowledgeBindings) - .filter_by(external_knowledge_api_id=external_knowledge_api_id) - .count() + db.session.scalar( + select(func.count(ExternalKnowledgeBindings.id)).where( + ExternalKnowledgeBindings.external_knowledge_api_id == external_knowledge_api_id + ) + ) + or 0 ) if count > 0: return True, count @@ -154,8 +163,10 @@ class ExternalDatasetService: @staticmethod def get_external_knowledge_binding_with_dataset_id(tenant_id: str, dataset_id: str) -> ExternalKnowledgeBindings: - external_knowledge_binding: ExternalKnowledgeBindings | None = ( - db.session.query(ExternalKnowledgeBindings).filter_by(dataset_id=dataset_id, tenant_id=tenant_id).first() + external_knowledge_binding: ExternalKnowledgeBindings | None = db.session.scalar( + select(ExternalKnowledgeBindings) + .where(ExternalKnowledgeBindings.dataset_id == dataset_id, ExternalKnowledgeBindings.tenant_id == tenant_id) + .limit(1) ) if not external_knowledge_binding: raise ValueError("external knowledge binding not found") @@ -163,8 +174,10 @@ class ExternalDatasetService: @staticmethod def document_create_args_validate(tenant_id: str, external_knowledge_api_id: str, process_parameter: dict): - external_knowledge_api = ( - db.session.query(ExternalKnowledgeApis).filter_by(id=external_knowledge_api_id, tenant_id=tenant_id).first() + external_knowledge_api = db.session.scalar( + select(ExternalKnowledgeApis) + .where(ExternalKnowledgeApis.id == external_knowledge_api_id, ExternalKnowledgeApis.tenant_id == tenant_id) + .limit(1) ) if external_knowledge_api is None or external_knowledge_api.settings is None: raise ValueError("api template not found") @@ -238,12 +251,17 @@ class ExternalDatasetService: @staticmethod def create_external_dataset(tenant_id: str, user_id: str, args: dict) -> Dataset: # check if dataset name already exists - if db.session.query(Dataset).filter_by(name=args.get("name"), tenant_id=tenant_id).first(): + if db.session.scalar( + select(Dataset).where(Dataset.name == args.get("name"), Dataset.tenant_id == tenant_id).limit(1) + ): raise DatasetNameDuplicateError(f"Dataset with name {args.get('name')} already exists.") - external_knowledge_api = ( - db.session.query(ExternalKnowledgeApis) - .filter_by(id=args.get("external_knowledge_api_id"), tenant_id=tenant_id) - .first() + external_knowledge_api = db.session.scalar( + select(ExternalKnowledgeApis) + .where( + ExternalKnowledgeApis.id == args.get("external_knowledge_api_id"), + ExternalKnowledgeApis.tenant_id == tenant_id, + ) + .limit(1) ) if external_knowledge_api is None: @@ -286,16 +304,18 @@ class ExternalDatasetService: external_retrieval_parameters: dict, metadata_condition: MetadataCondition | None = None, ): - external_knowledge_binding = ( - db.session.query(ExternalKnowledgeBindings).filter_by(dataset_id=dataset_id, tenant_id=tenant_id).first() + external_knowledge_binding = db.session.scalar( + select(ExternalKnowledgeBindings) + .where(ExternalKnowledgeBindings.dataset_id == dataset_id, ExternalKnowledgeBindings.tenant_id == tenant_id) + .limit(1) ) if not external_knowledge_binding: raise ValueError("external knowledge binding not found") - external_knowledge_api = ( - db.session.query(ExternalKnowledgeApis) - .filter_by(id=external_knowledge_binding.external_knowledge_api_id) - .first() + external_knowledge_api = db.session.scalar( + select(ExternalKnowledgeApis) + .where(ExternalKnowledgeApis.id == external_knowledge_binding.external_knowledge_api_id) + .limit(1) ) if external_knowledge_api is None or external_knowledge_api.settings is None: raise ValueError("external api template not found") diff --git a/api/services/hit_testing_service.py b/api/services/hit_testing_service.py index 82e0b0f8b1..7900f6da26 100644 --- a/api/services/hit_testing_service.py +++ b/api/services/hit_testing_service.py @@ -1,7 +1,7 @@ import json import logging import time -from typing import Any +from typing import Any, TypedDict from graphon.model_runtime.entities import LLMMode @@ -18,6 +18,16 @@ from models.enums import CreatorUserRole, DatasetQuerySource logger = logging.getLogger(__name__) + +class QueryDict(TypedDict): + content: str + + +class RetrieveResponseDict(TypedDict): + query: QueryDict + records: list[dict[str, Any]] + + default_retrieval_model = { "search_method": RetrievalMethod.SEMANTIC_SEARCH, "reranking_enable": False, @@ -150,7 +160,7 @@ class HitTestingService: return dict(cls.compact_external_retrieve_response(dataset, query, all_documents)) @classmethod - def compact_retrieve_response(cls, query: str, documents: list[Document]) -> dict[Any, Any]: + def compact_retrieve_response(cls, query: str, documents: list[Document]) -> RetrieveResponseDict: records = RetrievalService.format_retrieval_documents(documents) return { @@ -161,7 +171,7 @@ class HitTestingService: } @classmethod - def compact_external_retrieve_response(cls, dataset: Dataset, query: str, documents: list) -> dict[Any, Any]: + def compact_external_retrieve_response(cls, dataset: Dataset, query: str, documents: list) -> RetrieveResponseDict: records = [] if dataset.provider == "external": for document in documents: diff --git a/api/services/metadata_service.py b/api/services/metadata_service.py index 12729278cc..672f309bac 100644 --- a/api/services/metadata_service.py +++ b/api/services/metadata_service.py @@ -1,6 +1,8 @@ import copy import logging +from sqlalchemy import delete, func, select + from core.rag.index_processor.constant.built_in_field import BuiltInField, MetadataDataSource from extensions.ext_database import db from extensions.ext_redis import redis_client @@ -25,10 +27,14 @@ class MetadataService: raise ValueError("Metadata name cannot exceed 255 characters.") current_user, current_tenant_id = current_account_with_tenant() # check if metadata name already exists - if ( - db.session.query(DatasetMetadata) - .filter_by(tenant_id=current_tenant_id, dataset_id=dataset_id, name=metadata_args.name) - .first() + if db.session.scalar( + select(DatasetMetadata) + .where( + DatasetMetadata.tenant_id == current_tenant_id, + DatasetMetadata.dataset_id == dataset_id, + DatasetMetadata.name == metadata_args.name, + ) + .limit(1) ): raise ValueError("Metadata name already exists.") for field in BuiltInField: @@ -54,10 +60,14 @@ class MetadataService: lock_key = f"dataset_metadata_lock_{dataset_id}" # check if metadata name already exists current_user, current_tenant_id = current_account_with_tenant() - if ( - db.session.query(DatasetMetadata) - .filter_by(tenant_id=current_tenant_id, dataset_id=dataset_id, name=name) - .first() + if db.session.scalar( + select(DatasetMetadata) + .where( + DatasetMetadata.tenant_id == current_tenant_id, + DatasetMetadata.dataset_id == dataset_id, + DatasetMetadata.name == name, + ) + .limit(1) ): raise ValueError("Metadata name already exists.") for field in BuiltInField: @@ -65,7 +75,11 @@ class MetadataService: raise ValueError("Metadata name already exists in Built-in fields.") try: MetadataService.knowledge_base_metadata_lock_check(dataset_id, None) - metadata = db.session.query(DatasetMetadata).filter_by(id=metadata_id, dataset_id=dataset_id).first() + metadata = db.session.scalar( + select(DatasetMetadata) + .where(DatasetMetadata.id == metadata_id, DatasetMetadata.dataset_id == dataset_id) + .limit(1) + ) if metadata is None: raise ValueError("Metadata not found.") old_name = metadata.name @@ -74,9 +88,9 @@ class MetadataService: metadata.updated_at = naive_utc_now() # update related documents - dataset_metadata_bindings = ( - db.session.query(DatasetMetadataBinding).filter_by(metadata_id=metadata_id).all() - ) + dataset_metadata_bindings = db.session.scalars( + select(DatasetMetadataBinding).where(DatasetMetadataBinding.metadata_id == metadata_id) + ).all() if dataset_metadata_bindings: document_ids = [binding.document_id for binding in dataset_metadata_bindings] documents = DocumentService.get_document_by_ids(document_ids) @@ -101,15 +115,19 @@ class MetadataService: lock_key = f"dataset_metadata_lock_{dataset_id}" try: MetadataService.knowledge_base_metadata_lock_check(dataset_id, None) - metadata = db.session.query(DatasetMetadata).filter_by(id=metadata_id, dataset_id=dataset_id).first() + metadata = db.session.scalar( + select(DatasetMetadata) + .where(DatasetMetadata.id == metadata_id, DatasetMetadata.dataset_id == dataset_id) + .limit(1) + ) if metadata is None: raise ValueError("Metadata not found.") db.session.delete(metadata) # deal related documents - dataset_metadata_bindings = ( - db.session.query(DatasetMetadataBinding).filter_by(metadata_id=metadata_id).all() - ) + dataset_metadata_bindings = db.session.scalars( + select(DatasetMetadataBinding).where(DatasetMetadataBinding.metadata_id == metadata_id) + ).all() if dataset_metadata_bindings: document_ids = [binding.document_id for binding in dataset_metadata_bindings] documents = DocumentService.get_document_by_ids(document_ids) @@ -224,16 +242,23 @@ class MetadataService: # deal metadata binding (in the same transaction as the doc_metadata update) if not operation.partial_update: - db.session.query(DatasetMetadataBinding).filter_by(document_id=operation.document_id).delete() + db.session.execute( + delete(DatasetMetadataBinding).where( + DatasetMetadataBinding.document_id == operation.document_id + ) + ) current_user, current_tenant_id = current_account_with_tenant() for metadata_value in operation.metadata_list: # check if binding already exists if operation.partial_update: - existing_binding = ( - db.session.query(DatasetMetadataBinding) - .filter_by(document_id=operation.document_id, metadata_id=metadata_value.id) - .first() + existing_binding = db.session.scalar( + select(DatasetMetadataBinding) + .where( + DatasetMetadataBinding.document_id == operation.document_id, + DatasetMetadataBinding.metadata_id == metadata_value.id, + ) + .limit(1) ) if existing_binding: continue @@ -275,9 +300,13 @@ class MetadataService: "id": item.get("id"), "name": item.get("name"), "type": item.get("type"), - "count": db.session.query(DatasetMetadataBinding) - .filter_by(metadata_id=item.get("id"), dataset_id=dataset.id) - .count(), + "count": db.session.scalar( + select(func.count(DatasetMetadataBinding.id)).where( + DatasetMetadataBinding.metadata_id == item.get("id"), + DatasetMetadataBinding.dataset_id == dataset.id, + ) + ) + or 0, } for item in dataset.doc_metadata or [] if item.get("id") != "built-in" diff --git a/api/services/rag_pipeline/rag_pipeline.py b/api/services/rag_pipeline/rag_pipeline.py index 50f34d5a8a..5b3668aebb 100644 --- a/api/services/rag_pipeline/rag_pipeline.py +++ b/api/services/rag_pipeline/rag_pipeline.py @@ -156,27 +156,27 @@ class RagPipelineService: :param template_id: template id :param template_info: template info """ - customized_template: PipelineCustomizedTemplate | None = ( - db.session.query(PipelineCustomizedTemplate) + customized_template: PipelineCustomizedTemplate | None = db.session.scalar( + select(PipelineCustomizedTemplate) .where( PipelineCustomizedTemplate.id == template_id, PipelineCustomizedTemplate.tenant_id == current_user.current_tenant_id, ) - .first() + .limit(1) ) if not customized_template: raise ValueError("Customized pipeline template not found.") # check template name is exist template_name = template_info.name if template_name: - template = ( - db.session.query(PipelineCustomizedTemplate) + template = db.session.scalar( + select(PipelineCustomizedTemplate) .where( PipelineCustomizedTemplate.name == template_name, PipelineCustomizedTemplate.tenant_id == current_user.current_tenant_id, PipelineCustomizedTemplate.id != template_id, ) - .first() + .limit(1) ) if template: raise ValueError("Template name is already exists") @@ -192,13 +192,13 @@ class RagPipelineService: """ Delete customized pipeline template. """ - customized_template: PipelineCustomizedTemplate | None = ( - db.session.query(PipelineCustomizedTemplate) + customized_template: PipelineCustomizedTemplate | None = db.session.scalar( + select(PipelineCustomizedTemplate) .where( PipelineCustomizedTemplate.id == template_id, PipelineCustomizedTemplate.tenant_id == current_user.current_tenant_id, ) - .first() + .limit(1) ) if not customized_template: raise ValueError("Customized pipeline template not found.") @@ -210,14 +210,14 @@ class RagPipelineService: Get draft workflow """ # fetch draft workflow by rag pipeline - workflow = ( - db.session.query(Workflow) + workflow = db.session.scalar( + select(Workflow) .where( Workflow.tenant_id == pipeline.tenant_id, Workflow.app_id == pipeline.id, Workflow.version == "draft", ) - .first() + .limit(1) ) # return draft workflow @@ -232,28 +232,28 @@ class RagPipelineService: return None # fetch published workflow by workflow_id - workflow = ( - db.session.query(Workflow) + workflow = db.session.scalar( + select(Workflow) .where( Workflow.tenant_id == pipeline.tenant_id, Workflow.app_id == pipeline.id, Workflow.id == pipeline.workflow_id, ) - .first() + .limit(1) ) return workflow def get_published_workflow_by_id(self, pipeline: Pipeline, workflow_id: str) -> Workflow | None: """Fetch a published workflow snapshot by ID for restore operations.""" - workflow = ( - db.session.query(Workflow) + workflow = db.session.scalar( + select(Workflow) .where( Workflow.tenant_id == pipeline.tenant_id, Workflow.app_id == pipeline.id, Workflow.id == workflow_id, ) - .first() + .limit(1) ) if workflow and workflow.version == Workflow.VERSION_DRAFT: raise IsDraftWorkflowError("source workflow must be published") @@ -974,7 +974,7 @@ class RagPipelineService: if invoke_from.value == InvokeFrom.PUBLISHED_PIPELINE: document_id = get_system_segment(variable_pool, SystemVariableKey.DOCUMENT_ID) if document_id: - document = db.session.query(Document).where(Document.id == document_id.value).first() + document = db.session.get(Document, document_id.value) if document: document.indexing_status = IndexingStatus.ERROR document.error = error @@ -1178,12 +1178,12 @@ class RagPipelineService: """ Publish customized pipeline template """ - pipeline = db.session.query(Pipeline).where(Pipeline.id == pipeline_id).first() + pipeline = db.session.get(Pipeline, pipeline_id) if not pipeline: raise ValueError("Pipeline not found") if not pipeline.workflow_id: raise ValueError("Pipeline workflow not found") - workflow = db.session.query(Workflow).where(Workflow.id == pipeline.workflow_id).first() + workflow = db.session.get(Workflow, pipeline.workflow_id) if not workflow: raise ValueError("Workflow not found") with Session(db.engine) as session: @@ -1194,21 +1194,21 @@ class RagPipelineService: # check template name is exist template_name = args.get("name") if template_name: - template = ( - db.session.query(PipelineCustomizedTemplate) + template = db.session.scalar( + select(PipelineCustomizedTemplate) .where( PipelineCustomizedTemplate.name == template_name, PipelineCustomizedTemplate.tenant_id == pipeline.tenant_id, ) - .first() + .limit(1) ) if template: raise ValueError("Template name is already exists") - max_position = ( - db.session.query(func.max(PipelineCustomizedTemplate.position)) - .where(PipelineCustomizedTemplate.tenant_id == pipeline.tenant_id) - .scalar() + max_position = db.session.scalar( + select(func.max(PipelineCustomizedTemplate.position)).where( + PipelineCustomizedTemplate.tenant_id == pipeline.tenant_id + ) ) from services.rag_pipeline.rag_pipeline_dsl_service import RagPipelineDslService @@ -1239,13 +1239,14 @@ class RagPipelineService: def is_workflow_exist(self, pipeline: Pipeline) -> bool: return ( - db.session.query(Workflow) - .where( - Workflow.tenant_id == pipeline.tenant_id, - Workflow.app_id == pipeline.id, - Workflow.version == Workflow.VERSION_DRAFT, + db.session.scalar( + select(func.count(Workflow.id)).where( + Workflow.tenant_id == pipeline.tenant_id, + Workflow.app_id == pipeline.id, + Workflow.version == Workflow.VERSION_DRAFT, + ) ) - .count() + or 0 ) > 0 def get_node_last_run( @@ -1353,11 +1354,11 @@ class RagPipelineService: def get_recommended_plugins(self, type: str) -> dict: # Query active recommended plugins - query = db.session.query(PipelineRecommendedPlugin).where(PipelineRecommendedPlugin.active == True) + stmt = select(PipelineRecommendedPlugin).where(PipelineRecommendedPlugin.active == True) if type and type != "all": - query = query.where(PipelineRecommendedPlugin.type == type) + stmt = stmt.where(PipelineRecommendedPlugin.type == type) - pipeline_recommended_plugins = query.order_by(PipelineRecommendedPlugin.position.asc()).all() + pipeline_recommended_plugins = db.session.scalars(stmt.order_by(PipelineRecommendedPlugin.position.asc())).all() if not pipeline_recommended_plugins: return { @@ -1396,14 +1397,12 @@ class RagPipelineService: """ Retry error document """ - document_pipeline_execution_log = ( - db.session.query(DocumentPipelineExecutionLog) - .where(DocumentPipelineExecutionLog.document_id == document.id) - .first() + document_pipeline_execution_log = db.session.scalar( + select(DocumentPipelineExecutionLog).where(DocumentPipelineExecutionLog.document_id == document.id).limit(1) ) if not document_pipeline_execution_log: raise ValueError("Document pipeline execution log not found") - pipeline = db.session.query(Pipeline).where(Pipeline.id == document_pipeline_execution_log.pipeline_id).first() + pipeline = db.session.get(Pipeline, document_pipeline_execution_log.pipeline_id) if not pipeline: raise ValueError("Pipeline not found") # convert to app config @@ -1432,23 +1431,23 @@ class RagPipelineService: """ Get datasource plugins """ - dataset: Dataset | None = ( - db.session.query(Dataset) + dataset: Dataset | None = db.session.scalar( + select(Dataset) .where( Dataset.id == dataset_id, Dataset.tenant_id == tenant_id, ) - .first() + .limit(1) ) if not dataset: raise ValueError("Dataset not found") - pipeline: Pipeline | None = ( - db.session.query(Pipeline) + pipeline: Pipeline | None = db.session.scalar( + select(Pipeline) .where( Pipeline.id == dataset.pipeline_id, Pipeline.tenant_id == tenant_id, ) - .first() + .limit(1) ) if not pipeline: raise ValueError("Pipeline not found") @@ -1530,23 +1529,23 @@ class RagPipelineService: """ Get pipeline """ - dataset: Dataset | None = ( - db.session.query(Dataset) + dataset: Dataset | None = db.session.scalar( + select(Dataset) .where( Dataset.id == dataset_id, Dataset.tenant_id == tenant_id, ) - .first() + .limit(1) ) if not dataset: raise ValueError("Dataset not found") - pipeline: Pipeline | None = ( - db.session.query(Pipeline) + pipeline: Pipeline | None = db.session.scalar( + select(Pipeline) .where( Pipeline.id == dataset.pipeline_id, Pipeline.tenant_id == tenant_id, ) - .first() + .limit(1) ) if not pipeline: raise ValueError("Pipeline not found") diff --git a/api/services/tools/workflow_tools_manage_service.py b/api/services/tools/workflow_tools_manage_service.py index fb6b5bea24..dc0b281e15 100644 --- a/api/services/tools/workflow_tools_manage_service.py +++ b/api/services/tools/workflow_tools_manage_service.py @@ -3,7 +3,7 @@ import logging from datetime import datetime from graphon.model_runtime.utils.encoders import jsonable_encoder -from sqlalchemy import or_, select +from sqlalchemy import delete, or_, select from sqlalchemy.orm import Session from core.tools.__base.tool_provider import ToolProviderController @@ -42,20 +42,22 @@ class WorkflowToolManageService: labels: list[str] | None = None, ): # check if the name is unique - existing_workflow_tool_provider = ( - db.session.query(WorkflowToolProvider) + existing_workflow_tool_provider = db.session.scalar( + select(WorkflowToolProvider) .where( WorkflowToolProvider.tenant_id == tenant_id, # name or app_id or_(WorkflowToolProvider.name == name, WorkflowToolProvider.app_id == workflow_app_id), ) - .first() + .limit(1) ) if existing_workflow_tool_provider is not None: raise ValueError(f"Tool with name {name} or app_id {workflow_app_id} already exists") - app: App | None = db.session.query(App).where(App.id == workflow_app_id, App.tenant_id == tenant_id).first() + app: App | None = db.session.scalar( + select(App).where(App.id == workflow_app_id, App.tenant_id == tenant_id).limit(1) + ) if app is None: raise ValueError(f"App {workflow_app_id} not found") @@ -122,30 +124,30 @@ class WorkflowToolManageService: :return: the updated tool """ # check if the name is unique - existing_workflow_tool_provider = ( - db.session.query(WorkflowToolProvider) + existing_workflow_tool_provider = db.session.scalar( + select(WorkflowToolProvider) .where( WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.name == name, WorkflowToolProvider.id != workflow_tool_id, ) - .first() + .limit(1) ) if existing_workflow_tool_provider is not None: raise ValueError(f"Tool with name {name} already exists") - workflow_tool_provider: WorkflowToolProvider | None = ( - db.session.query(WorkflowToolProvider) + workflow_tool_provider: WorkflowToolProvider | None = db.session.scalar( + select(WorkflowToolProvider) .where(WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.id == workflow_tool_id) - .first() + .limit(1) ) if workflow_tool_provider is None: raise ValueError(f"Tool {workflow_tool_id} not found") - app: App | None = ( - db.session.query(App).where(App.id == workflow_tool_provider.app_id, App.tenant_id == tenant_id).first() + app: App | None = db.session.scalar( + select(App).where(App.id == workflow_tool_provider.app_id, App.tenant_id == tenant_id).limit(1) ) if app is None: @@ -234,9 +236,11 @@ class WorkflowToolManageService: :param tenant_id: the tenant id :param workflow_tool_id: the workflow tool id """ - db.session.query(WorkflowToolProvider).where( - WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.id == workflow_tool_id - ).delete() + db.session.execute( + delete(WorkflowToolProvider).where( + WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.id == workflow_tool_id + ) + ) db.session.commit() @@ -251,10 +255,10 @@ class WorkflowToolManageService: :param workflow_tool_id: the workflow tool id :return: the tool """ - db_tool: WorkflowToolProvider | None = ( - db.session.query(WorkflowToolProvider) + db_tool: WorkflowToolProvider | None = db.session.scalar( + select(WorkflowToolProvider) .where(WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.id == workflow_tool_id) - .first() + .limit(1) ) return cls._get_workflow_tool(tenant_id, db_tool) @@ -267,10 +271,10 @@ class WorkflowToolManageService: :param workflow_app_id: the workflow app id :return: the tool """ - db_tool: WorkflowToolProvider | None = ( - db.session.query(WorkflowToolProvider) + db_tool: WorkflowToolProvider | None = db.session.scalar( + select(WorkflowToolProvider) .where(WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.app_id == workflow_app_id) - .first() + .limit(1) ) return cls._get_workflow_tool(tenant_id, db_tool) @@ -284,8 +288,8 @@ class WorkflowToolManageService: if db_tool is None: raise ValueError("Tool not found") - workflow_app: App | None = ( - db.session.query(App).where(App.id == db_tool.app_id, App.tenant_id == db_tool.tenant_id).first() + workflow_app: App | None = db.session.scalar( + select(App).where(App.id == db_tool.app_id, App.tenant_id == db_tool.tenant_id).limit(1) ) if workflow_app is None: @@ -331,10 +335,10 @@ class WorkflowToolManageService: :param workflow_tool_id: the workflow tool id :return: the list of tools """ - db_tool: WorkflowToolProvider | None = ( - db.session.query(WorkflowToolProvider) + db_tool: WorkflowToolProvider | None = db.session.scalar( + select(WorkflowToolProvider) .where(WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.id == workflow_tool_id) - .first() + .limit(1) ) if db_tool is None: diff --git a/api/services/trigger/webhook_service.py b/api/services/trigger/webhook_service.py index c03275497d..e2d14c49e5 100644 --- a/api/services/trigger/webhook_service.py +++ b/api/services/trigger/webhook_service.py @@ -3,7 +3,7 @@ import logging import mimetypes import secrets from collections.abc import Callable, Mapping, Sequence -from typing import Any +from typing import Any, TypedDict import orjson from flask import request @@ -50,6 +50,14 @@ logger = logging.getLogger(__name__) _file_access_controller = DatabaseFileAccessController() +class RawWebhookDataDict(TypedDict): + method: str + headers: dict[str, str] + query_params: dict[str, str] + body: dict[str, Any] + files: dict[str, Any] + + class WebhookService: """Service for handling webhook operations.""" @@ -145,7 +153,7 @@ class WebhookService: @classmethod def extract_and_validate_webhook_data( cls, webhook_trigger: WorkflowWebhookTrigger, node_config: NodeConfigDict - ) -> dict[str, Any]: + ) -> RawWebhookDataDict: """Extract and validate webhook data in a single unified process. Args: @@ -173,7 +181,7 @@ class WebhookService: return processed_data @classmethod - def extract_webhook_data(cls, webhook_trigger: WorkflowWebhookTrigger) -> dict[str, Any]: + def extract_webhook_data(cls, webhook_trigger: WorkflowWebhookTrigger) -> RawWebhookDataDict: """Extract raw data from incoming webhook request without type conversion. Args: @@ -189,7 +197,7 @@ class WebhookService: """ cls._validate_content_length() - data = { + data: RawWebhookDataDict = { "method": request.method, "headers": dict(request.headers), "query_params": dict(request.args), @@ -223,7 +231,7 @@ class WebhookService: return data @classmethod - def _process_and_validate_data(cls, raw_data: dict[str, Any], node_data: WebhookData) -> dict[str, Any]: + def _process_and_validate_data(cls, raw_data: RawWebhookDataDict, node_data: WebhookData) -> RawWebhookDataDict: """Process and validate webhook data according to node configuration. Args: @@ -664,7 +672,7 @@ class WebhookService: raise ValueError(f"Required header missing: {header_name}") @classmethod - def _validate_http_metadata(cls, webhook_data: dict[str, Any], node_data: WebhookData) -> dict[str, Any]: + def _validate_http_metadata(cls, webhook_data: RawWebhookDataDict, node_data: WebhookData) -> dict[str, Any]: """Validate HTTP method and content-type. Args: @@ -729,7 +737,7 @@ class WebhookService: return False @classmethod - def build_workflow_inputs(cls, webhook_data: dict[str, Any]) -> dict[str, Any]: + def build_workflow_inputs(cls, webhook_data: RawWebhookDataDict) -> dict[str, Any]: """Construct workflow inputs payload from webhook data. Args: @@ -747,7 +755,7 @@ class WebhookService: @classmethod def trigger_workflow_execution( - cls, webhook_trigger: WorkflowWebhookTrigger, webhook_data: dict[str, Any], workflow: Workflow + cls, webhook_trigger: WorkflowWebhookTrigger, webhook_data: RawWebhookDataDict, workflow: Workflow ) -> None: """Trigger workflow execution via AsyncWorkflowService. diff --git a/api/services/variable_truncator.py b/api/services/variable_truncator.py index 5427b7b3a7..4d58a9cf12 100644 --- a/api/services/variable_truncator.py +++ b/api/services/variable_truncator.py @@ -129,6 +129,7 @@ class VariableTruncator(BaseTruncator): used_size += self.calculate_json_size(key) if used_size > budget: truncated_mapping[key] = "..." + is_truncated = True continue value_budget = (budget - used_size) // (length - len(truncated_mapping)) if isinstance(value, Segment): @@ -164,9 +165,9 @@ class VariableTruncator(BaseTruncator): result = self._truncate_segment(segment, self._max_size_bytes) if result.value_size > self._max_size_bytes: - if isinstance(result.value, str): - result = self._truncate_string(result.value, self._max_size_bytes) - return TruncationResult(StringSegment(value=result.value), True) + if isinstance(result.value, StringSegment): + fallback_result = self._truncate_string(result.value.value, self._max_size_bytes) + return TruncationResult(StringSegment(value=fallback_result.value), True) # Apply final fallback - convert to JSON string and truncate json_str = dumps_with_segments(result.value, ensure_ascii=False) diff --git a/web/app/components/base/auto-height-textarea/style.module.scss b/api/tests/test_containers_integration_tests/controllers/service_api/__init__.py similarity index 100% rename from web/app/components/base/auto-height-textarea/style.module.scss rename to api/tests/test_containers_integration_tests/controllers/service_api/__init__.py diff --git a/api/tests/test_containers_integration_tests/controllers/service_api/dataset/__init__.py b/api/tests/test_containers_integration_tests/controllers/service_api/dataset/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/unit_tests/controllers/service_api/dataset/test_dataset.py b/api/tests/test_containers_integration_tests/controllers/service_api/dataset/test_dataset.py similarity index 50% rename from api/tests/unit_tests/controllers/service_api/dataset/test_dataset.py rename to api/tests/test_containers_integration_tests/controllers/service_api/dataset/test_dataset.py index 910d781cd0..77a5730cf4 100644 --- a/api/tests/unit_tests/controllers/service_api/dataset/test_dataset.py +++ b/api/tests/test_containers_integration_tests/controllers/service_api/dataset/test_dataset.py @@ -1,17 +1,16 @@ """ -Unit tests for Service API Dataset controllers. +Integration tests for Service API Dataset controllers. + +Migrated from unit_tests/controllers/service_api/dataset/test_dataset.py. Tests coverage for: - DatasetCreatePayload, DatasetUpdatePayload Pydantic models - Tag-related payloads (create, update, delete, binding) - DatasetListQuery model -- DatasetService and TagService interfaces -- Permission validation patterns +- API endpoint error handling and controller behavior -Focus on: -- Pydantic model validation -- Error type mappings -- Service method interfaces +Services (DatasetService, TagService, DocumentService) remain mocked +since these test controller-level behavior. """ import uuid @@ -19,6 +18,7 @@ from types import SimpleNamespace from unittest.mock import Mock, patch import pytest +from sqlalchemy.orm import Session from werkzeug.exceptions import Forbidden, NotFound import services @@ -36,22 +36,23 @@ from controllers.service_api.dataset.error import DatasetInUseError, DatasetName from models.account import Account from models.dataset import DatasetPermissionEnum from models.enums import TagType -from services.dataset_service import DatasetPermissionService, DatasetService, DocumentService -from services.tag_service import TagService +from models.model import Tag + +# --------------------------------------------------------------------------- +# Pydantic model validation tests +# --------------------------------------------------------------------------- class TestDatasetCreatePayload: """Test suite for DatasetCreatePayload Pydantic model.""" def test_payload_with_required_name(self): - """Test payload with required name field.""" payload = DatasetCreatePayload(name="Test Dataset") assert payload.name == "Test Dataset" assert payload.description == "" assert payload.permission == DatasetPermissionEnum.ONLY_ME def test_payload_with_all_fields(self): - """Test payload with all fields populated.""" payload = DatasetCreatePayload( name="Full Dataset", description="A comprehensive dataset description", @@ -70,28 +71,23 @@ class TestDatasetCreatePayload: assert payload.embedding_model_provider == "openai" def test_payload_name_length_validation_min(self): - """Test name minimum length validation.""" with pytest.raises(ValueError): DatasetCreatePayload(name="") def test_payload_name_length_validation_max(self): - """Test name maximum length validation (40 chars).""" with pytest.raises(ValueError): DatasetCreatePayload(name="A" * 41) def test_payload_description_max_length(self): - """Test description maximum length (400 chars).""" with pytest.raises(ValueError): DatasetCreatePayload(name="Dataset", description="A" * 401) @pytest.mark.parametrize("technique", ["high_quality", "economy"]) def test_payload_valid_indexing_techniques(self, technique): - """Test valid indexing technique values.""" payload = DatasetCreatePayload(name="Dataset", indexing_technique=technique) assert payload.indexing_technique == technique def test_payload_with_external_knowledge_settings(self): - """Test payload with external knowledge configuration.""" payload = DatasetCreatePayload( name="External Dataset", external_knowledge_api_id="api_123", external_knowledge_id="knowledge_456" ) @@ -103,20 +99,17 @@ class TestDatasetUpdatePayload: """Test suite for DatasetUpdatePayload Pydantic model.""" def test_payload_all_optional(self): - """Test payload with all fields optional.""" payload = DatasetUpdatePayload() assert payload.name is None assert payload.description is None assert payload.permission is None def test_payload_with_partial_update(self): - """Test payload with partial update fields.""" payload = DatasetUpdatePayload(name="Updated Name", description="Updated description") assert payload.name == "Updated Name" assert payload.description == "Updated description" def test_payload_with_permission_change(self): - """Test payload with permission update.""" payload = DatasetUpdatePayload( permission=DatasetPermissionEnum.PARTIAL_TEAM, partial_member_list=[{"user_id": "user_123", "role": "editor"}], @@ -125,12 +118,8 @@ class TestDatasetUpdatePayload: assert len(payload.partial_member_list) == 1 def test_payload_name_length_validation(self): - """Test name length constraints.""" - # Minimum is 1 with pytest.raises(ValueError): DatasetUpdatePayload(name="") - - # Maximum is 40 with pytest.raises(ValueError): DatasetUpdatePayload(name="A" * 41) @@ -139,7 +128,6 @@ class TestDatasetListQuery: """Test suite for DatasetListQuery Pydantic model.""" def test_query_with_defaults(self): - """Test query with default values.""" query = DatasetListQuery() assert query.page == 1 assert query.limit == 20 @@ -148,7 +136,6 @@ class TestDatasetListQuery: assert query.tag_ids == [] def test_query_with_all_filters(self): - """Test query with all filter fields.""" query = DatasetListQuery( page=3, limit=50, keyword="machine learning", include_all=True, tag_ids=["tag1", "tag2", "tag3"] ) @@ -159,7 +146,6 @@ class TestDatasetListQuery: assert len(query.tag_ids) == 3 def test_query_with_tag_filter(self): - """Test query with tag IDs filter.""" query = DatasetListQuery(tag_ids=["tag_abc", "tag_def"]) assert query.tag_ids == ["tag_abc", "tag_def"] @@ -168,22 +154,18 @@ class TestTagCreatePayload: """Test suite for TagCreatePayload Pydantic model.""" def test_payload_with_name(self): - """Test payload with required name.""" payload = TagCreatePayload(name="New Tag") assert payload.name == "New Tag" def test_payload_name_length_min(self): - """Test name minimum length (1).""" with pytest.raises(ValueError): TagCreatePayload(name="") def test_payload_name_length_max(self): - """Test name maximum length (50).""" with pytest.raises(ValueError): TagCreatePayload(name="A" * 51) def test_payload_with_unicode_name(self): - """Test payload with unicode characters.""" payload = TagCreatePayload(name="标签 🏷️ Тег") assert payload.name == "标签 🏷️ Тег" @@ -192,13 +174,11 @@ class TestTagUpdatePayload: """Test suite for TagUpdatePayload Pydantic model.""" def test_payload_with_name_and_id(self): - """Test payload with name and tag_id.""" payload = TagUpdatePayload(name="Updated Tag", tag_id="tag_123") assert payload.name == "Updated Tag" assert payload.tag_id == "tag_123" def test_payload_requires_tag_id(self): - """Test that tag_id is required.""" with pytest.raises(ValueError): TagUpdatePayload(name="Updated Tag") @@ -207,12 +187,10 @@ class TestTagDeletePayload: """Test suite for TagDeletePayload Pydantic model.""" def test_payload_with_tag_id(self): - """Test payload with tag_id.""" payload = TagDeletePayload(tag_id="tag_to_delete") assert payload.tag_id == "tag_to_delete" def test_payload_requires_tag_id(self): - """Test that tag_id is required.""" with pytest.raises(ValueError): TagDeletePayload() @@ -221,19 +199,16 @@ class TestTagBindingPayload: """Test suite for TagBindingPayload Pydantic model.""" def test_payload_with_valid_data(self): - """Test payload with valid binding data.""" payload = TagBindingPayload(tag_ids=["tag1", "tag2"], target_id="dataset_123") assert len(payload.tag_ids) == 2 assert payload.target_id == "dataset_123" def test_payload_rejects_empty_tag_ids(self): - """Test that empty tag_ids are rejected.""" with pytest.raises(ValueError) as exc_info: TagBindingPayload(tag_ids=[], target_id="dataset_123") assert "Tag IDs is required" in str(exc_info.value) def test_payload_single_tag_id(self): - """Test payload with single tag ID.""" payload = TagBindingPayload(tag_ids=["single_tag"], target_id="dataset_456") assert payload.tag_ids == ["single_tag"] @@ -242,674 +217,14 @@ class TestTagUnbindingPayload: """Test suite for TagUnbindingPayload Pydantic model.""" def test_payload_with_valid_data(self): - """Test payload with valid unbinding data.""" payload = TagUnbindingPayload(tag_id="tag_123", target_id="dataset_456") assert payload.tag_id == "tag_123" assert payload.target_id == "dataset_456" -class TestDatasetTagsApi: - """Test suite for DatasetTagsApi endpoints.""" - - @pytest.fixture - def app(self): - """Create Flask test application.""" - from flask import Flask - - app = Flask(__name__) - app.config["TESTING"] = True - return app - - @patch("controllers.service_api.dataset.dataset.current_user") - @patch("controllers.service_api.dataset.dataset.TagService") - def test_get_tags_success(self, mock_tag_service, mock_current_user, app): - """Test successful retrieval of dataset tags.""" - # Arrange - mock_current_user needs to pass isinstance(current_user, Account) - from models.account import Account - - mock_account = Mock(spec=Account) - mock_account.current_tenant_id = "tenant_123" - # Replace the mock with our properly specced one - from controllers.service_api.dataset import dataset as dataset_module - - original_current_user = dataset_module.current_user - dataset_module.current_user = mock_account - - mock_tag = Mock() - mock_tag.id = "tag_1" - mock_tag.name = "Test Tag" - mock_tag.type = TagType.KNOWLEDGE - mock_tag.binding_count = "0" # Required for Pydantic validation - must be string - mock_tag_service.get_tags.return_value = [mock_tag] - - from controllers.service_api.dataset.dataset import DatasetTagsApi - - try: - # Act - with app.test_request_context("/", method="GET"): - api = DatasetTagsApi() - response, status_code = api.get("tenant_123") - - # Assert - assert status_code == 200 - assert len(response) == 1 - assert response[0]["id"] == "tag_1" - assert response[0]["name"] == "Test Tag" - mock_tag_service.get_tags.assert_called_once_with("knowledge", "tenant_123") - finally: - dataset_module.current_user = original_current_user - - @pytest.mark.skip(reason="Production code bug: binding_count should be string, not integer") - @patch("controllers.service_api.dataset.dataset.TagService") - @patch("controllers.service_api.dataset.dataset.service_api_ns") - def test_create_tag_success(self, mock_service_api_ns, mock_tag_service, app): - """Test successful creation of a dataset tag.""" - # Arrange - from controllers.service_api.dataset import dataset as dataset_module - from models.account import Account - - mock_account = Mock(spec=Account) - mock_account.has_edit_permission = True - mock_account.is_dataset_editor = False - original_current_user = dataset_module.current_user - dataset_module.current_user = mock_account - - mock_tag = Mock() - mock_tag.id = "new_tag_1" - mock_tag.name = "New Tag" - mock_tag.type = TagType.KNOWLEDGE - mock_tag_service.save_tags.return_value = mock_tag - mock_service_api_ns.payload = {"name": "New Tag"} - - from controllers.service_api.dataset.dataset import DatasetTagsApi - - try: - # Act - with app.test_request_context("/", method="POST", json={"name": "New Tag"}): - api = DatasetTagsApi() - response, status_code = api.post("tenant_123") - - # Assert - assert status_code == 200 - assert response["id"] == "new_tag_1" - assert response["name"] == "New Tag" - assert response["binding_count"] == 0 - finally: - dataset_module.current_user = original_current_user - - def test_create_tag_forbidden(self, app): - """Test tag creation without edit permissions.""" - # Arrange - from werkzeug.exceptions import Forbidden - - from controllers.service_api.dataset import dataset as dataset_module - from models.account import Account - - mock_account = Mock(spec=Account) - mock_account.has_edit_permission = False - mock_account.is_dataset_editor = False - original_current_user = dataset_module.current_user - dataset_module.current_user = mock_account - - from controllers.service_api.dataset.dataset import DatasetTagsApi - - try: - # Act & Assert - with app.test_request_context("/", method="POST"): - api = DatasetTagsApi() - with pytest.raises(Forbidden): - api.post("tenant_123") - finally: - dataset_module.current_user = original_current_user - - @pytest.mark.skip(reason="Production code bug: binding_count should be string, not integer") - @patch("controllers.service_api.dataset.dataset.TagService") - @patch("controllers.service_api.dataset.dataset.service_api_ns") - def test_update_tag_success(self, mock_service_api_ns, mock_tag_service, app): - """Test successful update of a dataset tag.""" - # Arrange - from controllers.service_api.dataset import dataset as dataset_module - from models.account import Account - - mock_account = Mock(spec=Account) - mock_account.has_edit_permission = True - original_current_user = dataset_module.current_user - dataset_module.current_user = mock_account - - mock_tag = Mock() - mock_tag.id = "tag_1" - mock_tag.name = "Updated Tag" - mock_tag.type = TagType.KNOWLEDGE - mock_tag.binding_count = "5" - mock_tag_service.update_tags.return_value = mock_tag - mock_tag_service.get_tag_binding_count.return_value = 5 - mock_service_api_ns.payload = {"name": "Updated Tag", "tag_id": "tag_1"} - - from controllers.service_api.dataset.dataset import DatasetTagsApi - - try: - # Act - with app.test_request_context("/", method="PATCH", json={"name": "Updated Tag", "tag_id": "tag_1"}): - api = DatasetTagsApi() - response, status_code = api.patch("tenant_123") - - # Assert - assert status_code == 200 - assert response["id"] == "tag_1" - assert response["name"] == "Updated Tag" - assert response["binding_count"] == 5 - finally: - dataset_module.current_user = original_current_user - - @pytest.mark.skip(reason="Production code bug: binding_count should be string, not integer") - @patch("controllers.service_api.dataset.dataset.TagService") - @patch("controllers.service_api.dataset.dataset.service_api_ns") - def test_delete_tag_success(self, mock_service_api_ns, mock_tag_service, app): - """Test successful deletion of a dataset tag.""" - # Arrange - from controllers.service_api.dataset import dataset as dataset_module - from models.account import Account - - mock_account = Mock(spec=Account) - mock_account.has_edit_permission = True - original_current_user = dataset_module.current_user - dataset_module.current_user = mock_account - - mock_tag_service.delete_tag.return_value = None - mock_service_api_ns.payload = {"tag_id": "tag_1"} - - from controllers.service_api.dataset.dataset import DatasetTagsApi - - try: - # Act - with app.test_request_context("/", method="DELETE", json={"tag_id": "tag_1"}): - api = DatasetTagsApi() - response = api.delete("tenant_123") - - # Assert - assert response == ("", 204) - mock_tag_service.delete_tag.assert_called_once_with("tag_1") - finally: - dataset_module.current_user = original_current_user - - -class TestDatasetTagBindingApi: - """Test suite for DatasetTagBindingApi endpoints.""" - - @pytest.fixture - def app(self): - """Create Flask test application.""" - from flask import Flask - - app = Flask(__name__) - app.config["TESTING"] = True - return app - - @patch("controllers.service_api.dataset.dataset.TagService") - @patch("controllers.service_api.dataset.dataset.service_api_ns") - def test_bind_tags_success(self, mock_service_api_ns, mock_tag_service, app): - """Test successful binding of tags to dataset.""" - # Arrange - from controllers.service_api.dataset import dataset as dataset_module - from models.account import Account - - mock_account = Mock(spec=Account) - mock_account.has_edit_permission = True - mock_account.is_dataset_editor = False - original_current_user = dataset_module.current_user - dataset_module.current_user = mock_account - - mock_tag_service.save_tag_binding.return_value = None - payload = {"tag_ids": ["tag_1", "tag_2"], "target_id": "dataset_123"} - mock_service_api_ns.payload = payload - - from controllers.service_api.dataset.dataset import DatasetTagBindingApi - - try: - # Act - with app.test_request_context("/", method="POST", json=payload): - api = DatasetTagBindingApi() - response = api.post("tenant_123") - - # Assert - assert response == ("", 204) - mock_tag_service.save_tag_binding.assert_called_once_with( - {"tag_ids": ["tag_1", "tag_2"], "target_id": "dataset_123", "type": "knowledge"} - ) - finally: - dataset_module.current_user = original_current_user - - def test_bind_tags_forbidden(self, app): - """Test tag binding without edit permissions.""" - # Arrange - from werkzeug.exceptions import Forbidden - - from controllers.service_api.dataset import dataset as dataset_module - from models.account import Account - - mock_account = Mock(spec=Account) - mock_account.has_edit_permission = False - mock_account.is_dataset_editor = False - original_current_user = dataset_module.current_user - dataset_module.current_user = mock_account - - from controllers.service_api.dataset.dataset import DatasetTagBindingApi - - try: - # Act & Assert - with app.test_request_context("/", method="POST"): - api = DatasetTagBindingApi() - with pytest.raises(Forbidden): - api.post("tenant_123") - finally: - dataset_module.current_user = original_current_user - - -class TestDatasetTagUnbindingApi: - """Test suite for DatasetTagUnbindingApi endpoints.""" - - @pytest.fixture - def app(self): - """Create Flask test application.""" - from flask import Flask - - app = Flask(__name__) - app.config["TESTING"] = True - return app - - @patch("controllers.service_api.dataset.dataset.TagService") - @patch("controllers.service_api.dataset.dataset.service_api_ns") - def test_unbind_tag_success(self, mock_service_api_ns, mock_tag_service, app): - """Test successful unbinding of tag from dataset.""" - # Arrange - from controllers.service_api.dataset import dataset as dataset_module - from models.account import Account - - mock_account = Mock(spec=Account) - mock_account.has_edit_permission = True - mock_account.is_dataset_editor = False - original_current_user = dataset_module.current_user - dataset_module.current_user = mock_account - - mock_tag_service.delete_tag_binding.return_value = None - payload = {"tag_id": "tag_1", "target_id": "dataset_123"} - mock_service_api_ns.payload = payload - - from controllers.service_api.dataset.dataset import DatasetTagUnbindingApi - - try: - # Act - with app.test_request_context("/", method="POST", json=payload): - api = DatasetTagUnbindingApi() - response = api.post("tenant_123") - - # Assert - assert response == ("", 204) - mock_tag_service.delete_tag_binding.assert_called_once_with( - {"tag_id": "tag_1", "target_id": "dataset_123", "type": "knowledge"} - ) - finally: - dataset_module.current_user = original_current_user - - -class TestDatasetTagsBindingStatusApi: - """Test suite for DatasetTagsBindingStatusApi endpoints.""" - - @pytest.fixture - def app(self): - """Create Flask test application.""" - from flask import Flask - - app = Flask(__name__) - app.config["TESTING"] = True - return app - - @patch("controllers.service_api.dataset.dataset.TagService") - def test_get_dataset_tags_binding_status(self, mock_tag_service, app): - """Test retrieval of tags bound to a specific dataset.""" - # Arrange - from controllers.service_api.dataset import dataset as dataset_module - from models.account import Account - - mock_account = Mock(spec=Account) - mock_account.current_tenant_id = "tenant_123" - original_current_user = dataset_module.current_user - dataset_module.current_user = mock_account - - mock_tag = Mock() - mock_tag.id = "tag_1" - mock_tag.name = "Test Tag" - mock_tag_service.get_tags_by_target_id.return_value = [mock_tag] - - from controllers.service_api.dataset.dataset import DatasetTagsBindingStatusApi - - try: - # Act - with app.test_request_context("/", method="GET"): - api = DatasetTagsBindingStatusApi() - response, status_code = api.get("tenant_123", dataset_id="dataset_123") - - # Assert - assert status_code == 200 - assert response["data"] == [{"id": "tag_1", "name": "Test Tag"}] - assert response["total"] == 1 - mock_tag_service.get_tags_by_target_id.assert_called_once_with("knowledge", "tenant_123", "dataset_123") - finally: - dataset_module.current_user = original_current_user - - -class TestDocumentStatusApi: - """Test suite for DocumentStatusApi batch operations.""" - - @pytest.fixture - def app(self): - """Create Flask test application.""" - from flask import Flask - - app = Flask(__name__) - app.config["TESTING"] = True - return app - - @patch("controllers.service_api.dataset.dataset.DatasetService") - @patch("controllers.service_api.dataset.dataset.DocumentService") - def test_batch_enable_documents(self, mock_doc_service, mock_dataset_service, app): - """Test batch enabling documents.""" - # Arrange - mock_dataset = Mock() - mock_dataset_service.get_dataset.return_value = mock_dataset - mock_doc_service.batch_update_document_status.return_value = None - - from controllers.service_api.dataset.dataset import DocumentStatusApi - - # Act - with app.test_request_context("/", method="PATCH", json={"document_ids": ["doc_1", "doc_2"]}): - api = DocumentStatusApi() - response, status_code = api.patch("tenant_123", "dataset_123", "enable") - - # Assert - assert status_code == 200 - assert response == {"result": "success"} - mock_doc_service.batch_update_document_status.assert_called_once() - - @patch("controllers.service_api.dataset.dataset.DatasetService") - def test_batch_update_dataset_not_found(self, mock_dataset_service, app): - """Test batch update when dataset not found.""" - # Arrange - mock_dataset_service.get_dataset.return_value = None - - from werkzeug.exceptions import NotFound - - from controllers.service_api.dataset.dataset import DocumentStatusApi - - # Act & Assert - with app.test_request_context("/", method="PATCH", json={"document_ids": ["doc_1"]}): - api = DocumentStatusApi() - with pytest.raises(NotFound) as exc_info: - api.patch("tenant_123", "dataset_123", "enable") - assert "Dataset not found" in str(exc_info.value) - - @patch("controllers.service_api.dataset.dataset.DatasetService") - @patch("controllers.service_api.dataset.dataset.DocumentService") - def test_batch_update_permission_error(self, mock_doc_service, mock_dataset_service, app): - """Test batch update with permission error.""" - # Arrange - mock_dataset = Mock() - mock_dataset_service.get_dataset.return_value = mock_dataset - from services.errors.account import NoPermissionError - - mock_dataset_service.check_dataset_permission.side_effect = NoPermissionError("No permission") - - from werkzeug.exceptions import Forbidden - - from controllers.service_api.dataset.dataset import DocumentStatusApi - - # Act & Assert - with app.test_request_context("/", method="PATCH", json={"document_ids": ["doc_1"]}): - api = DocumentStatusApi() - with pytest.raises(Forbidden): - api.patch("tenant_123", "dataset_123", "enable") - - @patch("controllers.service_api.dataset.dataset.DatasetService") - @patch("controllers.service_api.dataset.dataset.DocumentService") - def test_batch_update_invalid_action(self, mock_doc_service, mock_dataset_service, app): - """Test batch update with invalid action error.""" - # Arrange - mock_dataset = Mock() - mock_dataset_service.get_dataset.return_value = mock_dataset - mock_doc_service.batch_update_document_status.side_effect = ValueError("Invalid action") - - from controllers.service_api.dataset.dataset import DocumentStatusApi - from controllers.service_api.dataset.error import InvalidActionError - - # Act & Assert - with app.test_request_context("/", method="PATCH", json={"document_ids": ["doc_1"]}): - api = DocumentStatusApi() - with pytest.raises(InvalidActionError): - api.patch("tenant_123", "dataset_123", "invalid_action") - - """Test DatasetPermissionEnum values.""" - - def test_only_me_permission(self): - """Test ONLY_ME permission value.""" - assert DatasetPermissionEnum.ONLY_ME is not None - - def test_all_team_permission(self): - """Test ALL_TEAM permission value.""" - assert DatasetPermissionEnum.ALL_TEAM is not None - - def test_partial_team_permission(self): - """Test PARTIAL_TEAM permission value.""" - assert DatasetPermissionEnum.PARTIAL_TEAM is not None - - -class TestDatasetErrors: - """Test dataset-related error types.""" - - def test_dataset_in_use_error_can_be_raised(self): - """Test DatasetInUseError can be raised.""" - error = DatasetInUseError() - assert error is not None - - def test_dataset_name_duplicate_error_can_be_raised(self): - """Test DatasetNameDuplicateError can be raised.""" - error = DatasetNameDuplicateError() - assert error is not None - - def test_invalid_action_error_can_be_raised(self): - """Test InvalidActionError can be raised.""" - error = InvalidActionError("Invalid action") - assert error is not None - - -class TestDatasetService: - """Test DatasetService interface methods.""" - - def test_get_datasets_method_exists(self): - """Test DatasetService.get_datasets exists.""" - assert hasattr(DatasetService, "get_datasets") - - def test_get_dataset_method_exists(self): - """Test DatasetService.get_dataset exists.""" - assert hasattr(DatasetService, "get_dataset") - - def test_create_empty_dataset_method_exists(self): - """Test DatasetService.create_empty_dataset exists.""" - assert hasattr(DatasetService, "create_empty_dataset") - - def test_update_dataset_method_exists(self): - """Test DatasetService.update_dataset exists.""" - assert hasattr(DatasetService, "update_dataset") - - def test_delete_dataset_method_exists(self): - """Test DatasetService.delete_dataset exists.""" - assert hasattr(DatasetService, "delete_dataset") - - def test_check_dataset_permission_method_exists(self): - """Test DatasetService.check_dataset_permission exists.""" - assert hasattr(DatasetService, "check_dataset_permission") - - def test_check_dataset_model_setting_method_exists(self): - """Test DatasetService.check_dataset_model_setting exists.""" - assert hasattr(DatasetService, "check_dataset_model_setting") - - def test_check_embedding_model_setting_method_exists(self): - """Test DatasetService.check_embedding_model_setting exists.""" - assert hasattr(DatasetService, "check_embedding_model_setting") - - @patch.object(DatasetService, "get_datasets") - def test_get_datasets_returns_tuple(self, mock_get): - """Test get_datasets returns tuple of datasets and total.""" - mock_datasets = [Mock(), Mock()] - mock_get.return_value = (mock_datasets, 2) - - datasets, total = DatasetService.get_datasets(page=1, per_page=20, tenant_id="tenant_123", user=Mock()) - assert len(datasets) == 2 - assert total == 2 - - @patch.object(DatasetService, "get_dataset") - def test_get_dataset_returns_dataset(self, mock_get): - """Test get_dataset returns dataset object.""" - mock_dataset = Mock() - mock_dataset.id = str(uuid.uuid4()) - mock_dataset.name = "Test Dataset" - mock_get.return_value = mock_dataset - - result = DatasetService.get_dataset("dataset_id") - assert result.name == "Test Dataset" - - @patch.object(DatasetService, "get_dataset") - def test_get_dataset_returns_none_when_not_found(self, mock_get): - """Test get_dataset returns None when not found.""" - mock_get.return_value = None - - result = DatasetService.get_dataset("nonexistent_id") - assert result is None - - -class TestDatasetPermissionService: - """Test DatasetPermissionService interface.""" - - def test_check_permission_method_exists(self): - """Test DatasetPermissionService.check_permission exists.""" - assert hasattr(DatasetPermissionService, "check_permission") - - def test_get_dataset_partial_member_list_method_exists(self): - """Test DatasetPermissionService.get_dataset_partial_member_list exists.""" - assert hasattr(DatasetPermissionService, "get_dataset_partial_member_list") - - def test_update_partial_member_list_method_exists(self): - """Test DatasetPermissionService.update_partial_member_list exists.""" - assert hasattr(DatasetPermissionService, "update_partial_member_list") - - def test_clear_partial_member_list_method_exists(self): - """Test DatasetPermissionService.clear_partial_member_list exists.""" - assert hasattr(DatasetPermissionService, "clear_partial_member_list") - - -class TestDocumentService: - """Test DocumentService interface.""" - - def test_batch_update_document_status_method_exists(self): - """Test DocumentService.batch_update_document_status exists.""" - assert hasattr(DocumentService, "batch_update_document_status") - - -class TestTagService: - """Test TagService interface.""" - - def test_get_tags_method_exists(self): - """Test TagService.get_tags exists.""" - assert hasattr(TagService, "get_tags") - - def test_save_tags_method_exists(self): - """Test TagService.save_tags exists.""" - assert hasattr(TagService, "save_tags") - - def test_update_tags_method_exists(self): - """Test TagService.update_tags exists.""" - assert hasattr(TagService, "update_tags") - - def test_delete_tag_method_exists(self): - """Test TagService.delete_tag exists.""" - assert hasattr(TagService, "delete_tag") - - def test_save_tag_binding_method_exists(self): - """Test TagService.save_tag_binding exists.""" - assert hasattr(TagService, "save_tag_binding") - - def test_delete_tag_binding_method_exists(self): - """Test TagService.delete_tag_binding exists.""" - assert hasattr(TagService, "delete_tag_binding") - - def test_get_tags_by_target_id_method_exists(self): - """Test TagService.get_tags_by_target_id exists.""" - assert hasattr(TagService, "get_tags_by_target_id") - - def test_get_tag_binding_count_method_exists(self): - """Test TagService.get_tag_binding_count exists.""" - assert hasattr(TagService, "get_tag_binding_count") - - @patch.object(TagService, "get_tags") - def test_get_tags_returns_list(self, mock_get): - """Test get_tags returns list of tags.""" - mock_tags = [ - Mock(id="tag1", name="Tag One", type="knowledge"), - Mock(id="tag2", name="Tag Two", type="knowledge"), - ] - mock_get.return_value = mock_tags - - result = TagService.get_tags("knowledge", "tenant_123") - assert len(result) == 2 - - @patch.object(TagService, "save_tags") - def test_save_tags_returns_tag(self, mock_save): - """Test save_tags returns created tag.""" - mock_tag = Mock() - mock_tag.id = str(uuid.uuid4()) - mock_tag.name = "New Tag" - mock_tag.type = TagType.KNOWLEDGE - mock_save.return_value = mock_tag - - result = TagService.save_tags({"name": "New Tag", "type": "knowledge"}) - assert result.name == "New Tag" - - -class TestDocumentStatusAction: - """Test document status action values.""" - - def test_enable_action(self): - """Test enable action.""" - action = "enable" - assert action in ["enable", "disable", "archive", "un_archive"] - - def test_disable_action(self): - """Test disable action.""" - action = "disable" - assert action in ["enable", "disable", "archive", "un_archive"] - - def test_archive_action(self): - """Test archive action.""" - action = "archive" - assert action in ["enable", "disable", "archive", "un_archive"] - - def test_un_archive_action(self): - """Test un_archive action.""" - action = "un_archive" - assert action in ["enable", "disable", "archive", "un_archive"] - - -# ============================================================================= -# API Endpoint Tests -# -# ``DatasetListApi`` and ``DatasetApi`` inherit from ``DatasetApiResource`` -# whose ``method_decorators`` include ``validate_dataset_token``. -# -# Decorator strategy: -# - ``@cloud_edition_billing_rate_limit_check`` preserves ``__wrapped__`` -# → call via ``_unwrap(method)(self, …)``. -# - Methods without billing decorators → call directly; only patch ``db``, -# services, ``current_user``, and ``marshal``. -# ============================================================================= +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- def _unwrap(method): @@ -920,6 +235,15 @@ def _unwrap(method): return fn +@pytest.fixture +def app(flask_app_with_containers): + # Uses the full containerised app so that Flask config, extensions, and + # blueprint registrations match production. Most tests mock the service + # layer to isolate controller logic; a few (e.g. test_list_tags_from_db) + # exercise the real DB-backed path to validate end-to-end behaviour. + return flask_app_with_containers + + @pytest.fixture def mock_tenant(): tenant = Mock() @@ -938,12 +262,13 @@ def mock_dataset(): return dataset -class TestDatasetListApiGet: - """Test suite for DatasetListApi.get() endpoint. +# --------------------------------------------------------------------------- +# API endpoint tests — DatasetListApi +# --------------------------------------------------------------------------- - ``get`` has no billing decorators but calls ``current_user``, - ``DatasetService``, ``create_plugin_provider_manager``, and ``marshal``. - """ + +class TestDatasetListApiGet: + """Test suite for DatasetListApi.get() endpoint.""" @patch("controllers.service_api.dataset.dataset.marshal") @patch("controllers.service_api.dataset.dataset.create_plugin_provider_manager") @@ -958,7 +283,6 @@ class TestDatasetListApiGet: app, mock_tenant, ): - """Test successful dataset list retrieval.""" from controllers.service_api.dataset.dataset import DatasetListApi mock_current_user.__class__ = Account @@ -981,10 +305,7 @@ class TestDatasetListApiGet: class TestDatasetListApiPost: - """Test suite for DatasetListApi.post() endpoint. - - ``post`` is wrapped by ``@cloud_edition_billing_rate_limit_check``. - """ + """Test suite for DatasetListApi.post() endpoint.""" @patch("controllers.service_api.dataset.dataset.marshal") @patch("controllers.service_api.dataset.dataset.current_user") @@ -997,7 +318,6 @@ class TestDatasetListApiPost: app, mock_tenant, ): - """Test successful dataset creation.""" from controllers.service_api.dataset.dataset import DatasetListApi mock_current_user.__class__ = Account @@ -1024,7 +344,6 @@ class TestDatasetListApiPost: app, mock_tenant, ): - """Test DatasetNameDuplicateError when name already exists.""" from controllers.service_api.dataset.dataset import DatasetListApi mock_current_user.__class__ = Account @@ -1040,12 +359,13 @@ class TestDatasetListApiPost: _unwrap(api.post)(api, tenant_id=mock_tenant.id) -class TestDatasetApiGet: - """Test suite for DatasetApi.get() endpoint. +# --------------------------------------------------------------------------- +# API endpoint tests — DatasetApi +# --------------------------------------------------------------------------- - ``get`` has no billing decorators but calls ``DatasetService``, - ``create_plugin_provider_manager``, ``marshal``, and ``current_user``. - """ + +class TestDatasetApiGet: + """Test suite for DatasetApi.get() endpoint.""" @patch("controllers.service_api.dataset.dataset.DatasetPermissionService") @patch("controllers.service_api.dataset.dataset.marshal") @@ -1062,7 +382,6 @@ class TestDatasetApiGet: app, mock_dataset, ): - """Test successful dataset retrieval.""" from controllers.service_api.dataset.dataset import DatasetApi mock_dataset_svc.get_dataset.return_value = mock_dataset @@ -1092,7 +411,6 @@ class TestDatasetApiGet: @patch("controllers.service_api.dataset.dataset.DatasetService") def test_get_dataset_not_found(self, mock_dataset_svc, app, mock_dataset): - """Test 404 when dataset not found.""" from controllers.service_api.dataset.dataset import DatasetApi mock_dataset_svc.get_dataset.return_value = None @@ -1114,7 +432,6 @@ class TestDatasetApiGet: app, mock_dataset, ): - """Test 403 when user has no permission.""" from controllers.service_api.dataset.dataset import DatasetApi mock_dataset_svc.get_dataset.return_value = mock_dataset @@ -1130,10 +447,7 @@ class TestDatasetApiGet: class TestDatasetApiDelete: - """Test suite for DatasetApi.delete() endpoint. - - ``delete`` is wrapped by ``@cloud_edition_billing_rate_limit_check``. - """ + """Test suite for DatasetApi.delete() endpoint.""" @patch("controllers.service_api.dataset.dataset.DatasetPermissionService") @patch("controllers.service_api.dataset.dataset.current_user") @@ -1146,7 +460,6 @@ class TestDatasetApiDelete: app, mock_dataset, ): - """Test successful dataset deletion.""" from controllers.service_api.dataset.dataset import DatasetApi mock_dataset_svc.delete_dataset.return_value = True @@ -1169,7 +482,6 @@ class TestDatasetApiDelete: app, mock_dataset, ): - """Test 404 when dataset not found for deletion.""" from controllers.service_api.dataset.dataset import DatasetApi mock_dataset_svc.delete_dataset.return_value = False @@ -1191,7 +503,6 @@ class TestDatasetApiDelete: app, mock_dataset, ): - """Test DatasetInUseError when dataset is in use.""" from controllers.service_api.dataset.dataset import DatasetApi mock_dataset_svc.delete_dataset.side_effect = services.errors.dataset.DatasetInUseError() @@ -1205,12 +516,13 @@ class TestDatasetApiDelete: _unwrap(api.delete)(api, _=mock_dataset.tenant_id, dataset_id=mock_dataset.id) -class TestDocumentStatusApiPatch: - """Test suite for DocumentStatusApi.patch() endpoint. +# --------------------------------------------------------------------------- +# API endpoint tests — DocumentStatusApi +# --------------------------------------------------------------------------- - ``patch`` has no billing decorators but calls ``DatasetService``, - ``DocumentService``, and ``current_user``. - """ + +class TestDocumentStatusApiPatch: + """Test suite for DocumentStatusApi.patch() endpoint.""" @patch("controllers.service_api.dataset.dataset.DocumentService") @patch("controllers.service_api.dataset.dataset.current_user") @@ -1224,7 +536,6 @@ class TestDocumentStatusApiPatch: mock_tenant, mock_dataset, ): - """Test successful batch document status update.""" from controllers.service_api.dataset.dataset import DocumentStatusApi mock_current_user.__class__ = Account @@ -1256,7 +567,6 @@ class TestDocumentStatusApiPatch: mock_tenant, mock_dataset, ): - """Test 404 when dataset not found.""" from controllers.service_api.dataset.dataset import DocumentStatusApi mock_dataset_svc.get_dataset.return_value = None @@ -1274,6 +584,39 @@ class TestDocumentStatusApiPatch: action="enable", ) + @patch("controllers.service_api.dataset.dataset.DocumentService") + @patch("controllers.service_api.dataset.dataset.current_user") + @patch("controllers.service_api.dataset.dataset.DatasetService") + def test_batch_update_status_permission_error( + self, + mock_dataset_svc, + mock_current_user, + mock_doc_svc, + app, + mock_tenant, + mock_dataset, + ): + from controllers.service_api.dataset.dataset import DocumentStatusApi + + mock_current_user.__class__ = Account + mock_dataset_svc.get_dataset.return_value = mock_dataset + mock_dataset_svc.check_dataset_permission.side_effect = services.errors.account.NoPermissionError( + "No permission" + ) + + with app.test_request_context( + f"/datasets/{mock_dataset.id}/documents/status/enable", + method="PATCH", + json={"document_ids": ["doc-1"]}, + ): + api = DocumentStatusApi() + with pytest.raises(Forbidden): + api.patch( + tenant_id=mock_tenant.id, + dataset_id=mock_dataset.id, + action="enable", + ) + @patch("controllers.service_api.dataset.dataset.DocumentService") @patch("controllers.service_api.dataset.dataset.current_user") @patch("controllers.service_api.dataset.dataset.DatasetService") @@ -1286,7 +629,6 @@ class TestDocumentStatusApiPatch: mock_tenant, mock_dataset, ): - """Test InvalidActionError when document is indexing.""" from controllers.service_api.dataset.dataset import DocumentStatusApi mock_current_user.__class__ = Account @@ -1320,7 +662,6 @@ class TestDocumentStatusApiPatch: mock_tenant, mock_dataset, ): - """Test InvalidActionError when ValueError raised.""" from controllers.service_api.dataset.dataset import DocumentStatusApi mock_current_user.__class__ = Account @@ -1343,6 +684,11 @@ class TestDocumentStatusApiPatch: ) +# --------------------------------------------------------------------------- +# API endpoint tests — Tags +# --------------------------------------------------------------------------- + + class TestDatasetTagsApiGet: """Test suite for DatasetTagsApi.get() endpoint.""" @@ -1354,7 +700,6 @@ class TestDatasetTagsApiGet: mock_tag_svc, app, ): - """Test successful tag list retrieval.""" from controllers.service_api.dataset.dataset import DatasetTagsApi mock_current_user.__class__ = Account @@ -1368,15 +713,49 @@ class TestDatasetTagsApiGet: assert status == 200 assert len(response) == 1 + mock_tag_svc.get_tags.assert_called_once_with("knowledge", "tenant-1") + + @pytest.mark.skip(reason="Production bug: DataSetTag.binding_count is str|None but DB COUNT() returns int") + @patch("controllers.service_api.dataset.dataset.current_user") + def test_list_tags_from_db( + self, + mock_current_user, + app, + db_session_with_containers: Session, + ): + """Integration test: creates real Tag rows and retrieves them + through the controller without mocking TagService.""" + from tests.test_containers_integration_tests.controllers.console.helpers import ( + create_console_account_and_tenant, + ) + + account, tenant = create_console_account_and_tenant(db_session_with_containers) + + tag = Tag( + name="Integration Tag", + type=TagType.KNOWLEDGE, + created_by=account.id, + tenant_id=tenant.id, + ) + db_session_with_containers.add(tag) + db_session_with_containers.commit() + + mock_current_user.__class__ = Account + mock_current_user.current_tenant_id = tenant.id + + from controllers.service_api.dataset.dataset import DatasetTagsApi + + with app.test_request_context("/datasets/tags", method="GET"): + api = DatasetTagsApi() + response, status = api.get(_=None) + + assert status == 200 + assert any(t["name"] == "Integration Tag" for t in response) class TestDatasetTagsApiPost: """Test suite for DatasetTagsApi.post() endpoint.""" - # BUG: dataset.py L512 passes ``binding_count=0`` (int) to - # ``DataSetTag.model_validate()``, but ``DataSetTag.binding_count`` - # is typed ``str | None`` (see fields/tag_fields.py L20). - # This causes a Pydantic ValidationError at runtime. @pytest.mark.skip(reason="Production bug: DataSetTag.binding_count is str|None but dataset.py passes int 0") @patch("controllers.service_api.dataset.dataset.TagService") @patch("controllers.service_api.dataset.dataset.current_user") @@ -1386,7 +765,6 @@ class TestDatasetTagsApiPost: mock_tag_svc, app, ): - """Test successful tag creation.""" from controllers.service_api.dataset.dataset import DatasetTagsApi mock_current_user.__class__ = Account @@ -1409,7 +787,6 @@ class TestDatasetTagsApiPost: @patch("controllers.service_api.dataset.dataset.current_user") def test_create_tag_forbidden(self, mock_current_user, app): - """Test 403 when user lacks edit permission.""" from controllers.service_api.dataset.dataset import DatasetTagsApi mock_current_user.__class__ = Account @@ -1426,6 +803,146 @@ class TestDatasetTagsApiPost: api.post(_=None) +class TestDatasetTagsApiPatch: + """Test suite for DatasetTagsApi.patch() endpoint.""" + + @pytest.mark.skip(reason="Production bug: DataSetTag.binding_count is str|None but dataset.py passes int 0") + @patch("controllers.service_api.dataset.dataset.TagService") + @patch("controllers.service_api.dataset.dataset.service_api_ns") + @patch("controllers.service_api.dataset.dataset.current_user") + def test_update_tag_success( + self, + mock_current_user, + mock_service_api_ns, + mock_tag_svc, + app, + ): + from controllers.service_api.dataset.dataset import DatasetTagsApi + + mock_current_user.__class__ = Account + mock_current_user.has_edit_permission = True + mock_current_user.is_dataset_editor = True + + mock_tag = SimpleNamespace(id="tag-1", name="Updated Tag", type="knowledge") + mock_tag_svc.update_tags.return_value = mock_tag + mock_tag_svc.get_tag_binding_count.return_value = 5 + mock_service_api_ns.payload = {"name": "Updated Tag", "tag_id": "tag-1"} + + with app.test_request_context( + "/datasets/tags", + method="PATCH", + json={"name": "Updated Tag", "tag_id": "tag-1"}, + ): + api = DatasetTagsApi() + response, status = api.patch(_=None) + + assert status == 200 + assert response["name"] == "Updated Tag" + mock_tag_svc.update_tags.assert_called_once_with({"name": "Updated Tag", "type": "knowledge"}, "tag-1") + + @patch("controllers.service_api.dataset.dataset.current_user") + def test_update_tag_forbidden(self, mock_current_user, app): + from controllers.service_api.dataset.dataset import DatasetTagsApi + + mock_current_user.__class__ = Account + mock_current_user.has_edit_permission = False + mock_current_user.is_dataset_editor = False + + with app.test_request_context( + "/datasets/tags", + method="PATCH", + json={"name": "Updated Tag", "tag_id": "tag-1"}, + ): + api = DatasetTagsApi() + with pytest.raises(Forbidden): + api.patch(_=None) + + +class TestDatasetTagsApiDelete: + """Test suite for DatasetTagsApi.delete() endpoint.""" + + @patch("controllers.service_api.dataset.dataset.TagService") + @patch("controllers.service_api.dataset.dataset.service_api_ns") + @patch("libs.login.current_user") + def test_delete_tag_success( + self, + mock_current_user, + mock_service_api_ns, + mock_tag_svc, + app, + ): + from controllers.service_api.dataset.dataset import DatasetTagsApi + + user_obj = Mock(spec=Account) + user_obj.has_edit_permission = True + mock_current_user.has_edit_permission = True + # Assign as plain lambda to avoid AsyncMock returning a coroutine + mock_current_user._get_current_object = lambda: user_obj + + mock_tag_svc.delete_tag.return_value = None + mock_service_api_ns.payload = {"tag_id": "tag-1"} + + with app.test_request_context( + "/datasets/tags", + method="DELETE", + json={"tag_id": "tag-1"}, + ): + api = DatasetTagsApi() + result = api.delete(_=None) + + assert result == ("", 204) + mock_tag_svc.delete_tag.assert_called_once_with("tag-1") + + @patch("libs.login.current_user") + def test_delete_tag_forbidden(self, mock_current_user, app): + from controllers.service_api.dataset.dataset import DatasetTagsApi + + user_obj = Mock(spec=Account) + user_obj.has_edit_permission = False + mock_current_user.has_edit_permission = False + # Assign as plain lambda to avoid AsyncMock returning a coroutine + mock_current_user._get_current_object = lambda: user_obj + + with app.test_request_context( + "/datasets/tags", + method="DELETE", + json={"tag_id": "tag-1"}, + ): + api = DatasetTagsApi() + with pytest.raises(Forbidden): + api.delete(_=None) + + +class TestDatasetTagsBindingStatusApi: + """Test suite for DatasetTagsBindingStatusApi endpoints.""" + + @patch("controllers.service_api.dataset.dataset.TagService") + @patch("controllers.service_api.dataset.dataset.current_user") + def test_get_dataset_tags_binding_status( + self, + mock_current_user, + mock_tag_svc, + app, + ): + from controllers.service_api.dataset.dataset import DatasetTagsBindingStatusApi + + mock_current_user.__class__ = Account + mock_current_user.current_tenant_id = "tenant_123" + mock_tag = Mock() + mock_tag.id = "tag_1" + mock_tag.name = "Test Tag" + mock_tag_svc.get_tags_by_target_id.return_value = [mock_tag] + + with app.test_request_context("/", method="GET"): + api = DatasetTagsBindingStatusApi() + response, status_code = api.get("tenant_123", dataset_id="dataset_123") + + assert status_code == 200 + assert response["data"] == [{"id": "tag_1", "name": "Test Tag"}] + assert response["total"] == 1 + mock_tag_svc.get_tags_by_target_id.assert_called_once_with("knowledge", "tenant_123", "dataset_123") + + class TestDatasetTagBindingApiPost: """Test suite for DatasetTagBindingApi.post() endpoint.""" @@ -1437,7 +954,6 @@ class TestDatasetTagBindingApiPost: mock_tag_svc, app, ): - """Test successful tag binding.""" from controllers.service_api.dataset.dataset import DatasetTagBindingApi mock_current_user.__class__ = Account @@ -1454,10 +970,12 @@ class TestDatasetTagBindingApiPost: result = api.post(_=None) assert result == ("", 204) + mock_tag_svc.save_tag_binding.assert_called_once_with( + {"tag_ids": ["tag-1"], "target_id": "ds-1", "type": "knowledge"} + ) @patch("controllers.service_api.dataset.dataset.current_user") def test_bind_tags_forbidden(self, mock_current_user, app): - """Test 403 when user lacks edit permission.""" from controllers.service_api.dataset.dataset import DatasetTagBindingApi mock_current_user.__class__ = Account @@ -1485,7 +1003,6 @@ class TestDatasetTagUnbindingApiPost: mock_tag_svc, app, ): - """Test successful tag unbinding.""" from controllers.service_api.dataset.dataset import DatasetTagUnbindingApi mock_current_user.__class__ = Account @@ -1502,10 +1019,12 @@ class TestDatasetTagUnbindingApiPost: result = api.post(_=None) assert result == ("", 204) + mock_tag_svc.delete_tag_binding.assert_called_once_with( + {"tag_id": "tag-1", "target_id": "ds-1", "type": "knowledge"} + ) @patch("controllers.service_api.dataset.dataset.current_user") def test_unbind_tag_forbidden(self, mock_current_user, app): - """Test 403 when user lacks edit permission.""" from controllers.service_api.dataset.dataset import DatasetTagUnbindingApi mock_current_user.__class__ = Account diff --git a/api/tests/unit_tests/controllers/console/test_workspace_account.py b/api/tests/unit_tests/controllers/console/test_workspace_account.py index 9afc1c4166..7f9fe9cbf9 100644 --- a/api/tests/unit_tests/controllers/console/test_workspace_account.py +++ b/api/tests/unit_tests/controllers/console/test_workspace_account.py @@ -20,7 +20,7 @@ def app(): app = Flask(__name__) app.config["TESTING"] = True app.config["RESTX_MASK_HEADER"] = "X-Fields" - app.login_manager = SimpleNamespace(_load_user=lambda: None) + app.login_manager = SimpleNamespace(load_user_from_request_context=lambda: None) return app diff --git a/api/tests/unit_tests/controllers/console/test_workspace_members.py b/api/tests/unit_tests/controllers/console/test_workspace_members.py index 368892b922..239fec8430 100644 --- a/api/tests/unit_tests/controllers/console/test_workspace_members.py +++ b/api/tests/unit_tests/controllers/console/test_workspace_members.py @@ -12,7 +12,7 @@ from models.account import Account, TenantAccountRole def app(): flask_app = Flask(__name__) flask_app.config["TESTING"] = True - flask_app.login_manager = SimpleNamespace(_load_user=lambda: None) + flask_app.login_manager = SimpleNamespace(load_user_from_request_context=lambda: None) return flask_app diff --git a/api/tests/unit_tests/extensions/test_ext_login.py b/api/tests/unit_tests/extensions/test_ext_login.py new file mode 100644 index 0000000000..64abc19427 --- /dev/null +++ b/api/tests/unit_tests/extensions/test_ext_login.py @@ -0,0 +1,17 @@ +import json + +from flask import Response + +from extensions.ext_login import unauthorized_handler + + +def test_unauthorized_handler_returns_json_response() -> None: + response = unauthorized_handler() + + assert isinstance(response, Response) + assert response.status_code == 401 + assert response.content_type == "application/json" + assert json.loads(response.get_data(as_text=True)) == { + "code": "unauthorized", + "message": "Unauthorized.", + } diff --git a/api/tests/unit_tests/libs/test_login.py b/api/tests/unit_tests/libs/test_login.py index 0c9e73299b..2bf2212844 100644 --- a/api/tests/unit_tests/libs/test_login.py +++ b/api/tests/unit_tests/libs/test_login.py @@ -2,11 +2,12 @@ from types import SimpleNamespace from unittest.mock import MagicMock import pytest -from flask import Flask, g -from flask_login import LoginManager, UserMixin +from flask import Flask, Response, g +from flask_login import UserMixin from pytest_mock import MockerFixture import libs.login as login_module +from extensions.ext_login import DifyLoginManager from libs.login import current_user from models.account import Account @@ -39,9 +40,12 @@ def login_app(mocker: MockerFixture) -> Flask: app = Flask(__name__) app.config["TESTING"] = True - login_manager = LoginManager() + login_manager = DifyLoginManager() login_manager.init_app(app) - login_manager.unauthorized = mocker.Mock(name="unauthorized", return_value="Unauthorized") + login_manager.unauthorized = mocker.Mock( + name="unauthorized", + return_value=Response("Unauthorized", status=401, content_type="application/json"), + ) @login_manager.user_loader def load_user(_user_id: str): @@ -109,18 +113,43 @@ class TestLoginRequired: resolved_user: MockUser | None, description: str, ): - """Test that missing or unauthenticated users are redirected.""" + """Test that missing or unauthenticated users return the manager response.""" resolve_user = resolve_current_user(resolved_user) with login_app.test_request_context(): result = protected_view() - assert result == "Unauthorized", description + assert result is login_app.login_manager.unauthorized.return_value, description + assert isinstance(result, Response) + assert result.status_code == 401 resolve_user.assert_called_once_with() login_app.login_manager.unauthorized.assert_called_once_with() csrf_check.assert_not_called() + def test_unauthorized_access_propagates_response_object( + self, + login_app: Flask, + protected_view, + csrf_check: MagicMock, + resolve_current_user, + mocker: MockerFixture, + ) -> None: + """Test that unauthorized responses are propagated as Flask Response objects.""" + resolve_user = resolve_current_user(None) + response = Response("Unauthorized", status=401, content_type="application/json") + mocker.patch.object( + login_module, "_get_login_manager", return_value=SimpleNamespace(unauthorized=lambda: response) + ) + + with login_app.test_request_context(): + result = protected_view() + + assert result is response + assert isinstance(result, Response) + resolve_user.assert_called_once_with() + csrf_check.assert_not_called() + @pytest.mark.parametrize( ("method", "login_disabled"), [ @@ -168,10 +197,14 @@ class TestGetUser: """Test that _get_user loads user if not already in g.""" mock_user = MockUser("test_user") - def _load_user() -> None: + def load_user_from_request_context() -> None: g._login_user = mock_user - load_user = mocker.patch.object(login_app.login_manager, "_load_user", side_effect=_load_user) + load_user = mocker.patch.object( + login_app.login_manager, + "load_user_from_request_context", + side_effect=load_user_from_request_context, + ) with login_app.test_request_context(): user = login_module._get_user() diff --git a/api/tests/unit_tests/services/dataset_metadata.py b/api/tests/unit_tests/services/dataset_metadata.py index 5ba18d8dc0..b825a8686a 100644 --- a/api/tests/unit_tests/services/dataset_metadata.py +++ b/api/tests/unit_tests/services/dataset_metadata.py @@ -401,10 +401,7 @@ class TestMetadataServiceCreateMetadata: metadata_args = MetadataTestDataFactory.create_metadata_args_mock(name="category", metadata_type="string") # Mock query to return None (no existing metadata with same name) - mock_query = Mock() - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None - mock_db_session.query.return_value = mock_query + mock_db_session.scalar.return_value = None # Mock BuiltInField enum iteration with patch("services.metadata_service.BuiltInField") as mock_builtin: @@ -417,10 +414,6 @@ class TestMetadataServiceCreateMetadata: assert result is not None assert isinstance(result, DatasetMetadata) - # Verify query was made to check for duplicates - mock_db_session.query.assert_called() - mock_query.filter_by.assert_called() - # Verify metadata was added and committed mock_db_session.add.assert_called_once() mock_db_session.commit.assert_called_once() @@ -468,10 +461,7 @@ class TestMetadataServiceCreateMetadata: # Mock existing metadata with same name existing_metadata = MetadataTestDataFactory.create_metadata_mock(name="category") - mock_query = Mock() - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = existing_metadata - mock_db_session.query.return_value = mock_query + mock_db_session.scalar.return_value = existing_metadata # Act & Assert with pytest.raises(ValueError, match="Metadata name already exists"): @@ -500,10 +490,7 @@ class TestMetadataServiceCreateMetadata: ) # Mock query to return None (no duplicate in database) - mock_query = Mock() - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None - mock_db_session.query.return_value = mock_query + mock_db_session.scalar.return_value = None # Mock BuiltInField to include the conflicting name with patch("services.metadata_service.BuiltInField") as mock_builtin: @@ -597,27 +584,11 @@ class TestMetadataServiceUpdateMetadataName: existing_metadata = MetadataTestDataFactory.create_metadata_mock(metadata_id=metadata_id, name="category") - # Mock query for duplicate check (no duplicate) - mock_query = Mock() - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None - mock_db_session.query.return_value = mock_query - - # Mock metadata retrieval - def query_side_effect(model): - if model == DatasetMetadata: - mock_meta_query = Mock() - mock_meta_query.filter_by.return_value = mock_meta_query - mock_meta_query.first.return_value = existing_metadata - return mock_meta_query - return mock_query - - mock_db_session.query.side_effect = query_side_effect + # Mock scalar calls: first for duplicate check (None), second for metadata retrieval + mock_db_session.scalar.side_effect = [None, existing_metadata] # Mock no metadata bindings (no documents to update) - mock_binding_query = Mock() - mock_binding_query.filter_by.return_value = mock_binding_query - mock_binding_query.all.return_value = [] + mock_db_session.scalars.return_value.all.return_value = [] # Mock BuiltInField enum with patch("services.metadata_service.BuiltInField") as mock_builtin: @@ -655,22 +626,8 @@ class TestMetadataServiceUpdateMetadataName: metadata_id = "non-existent-metadata" new_name = "updated_category" - # Mock query for duplicate check (no duplicate) - mock_query = Mock() - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None - mock_db_session.query.return_value = mock_query - - # Mock metadata retrieval to return None - def query_side_effect(model): - if model == DatasetMetadata: - mock_meta_query = Mock() - mock_meta_query.filter_by.return_value = mock_meta_query - mock_meta_query.first.return_value = None # Not found - return mock_meta_query - return mock_query - - mock_db_session.query.side_effect = query_side_effect + # Mock scalar calls: first for duplicate check (None), second for metadata retrieval (None = not found) + mock_db_session.scalar.side_effect = [None, None] # Mock BuiltInField enum with patch("services.metadata_service.BuiltInField") as mock_builtin: @@ -746,15 +703,10 @@ class TestMetadataServiceDeleteMetadata: existing_metadata = MetadataTestDataFactory.create_metadata_mock(metadata_id=metadata_id, name="category") # Mock metadata retrieval - mock_query = Mock() - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = existing_metadata - mock_db_session.query.return_value = mock_query + mock_db_session.scalar.return_value = existing_metadata # Mock no metadata bindings (no documents to update) - mock_binding_query = Mock() - mock_binding_query.filter_by.return_value = mock_binding_query - mock_binding_query.all.return_value = [] + mock_db_session.scalars.return_value.all.return_value = [] # Act result = MetadataService.delete_metadata(dataset_id, metadata_id) @@ -788,10 +740,7 @@ class TestMetadataServiceDeleteMetadata: metadata_id = "non-existent-metadata" # Mock metadata retrieval to return None - mock_query = Mock() - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None - mock_db_session.query.return_value = mock_query + mock_db_session.scalar.return_value = None # Act & Assert with pytest.raises(ValueError, match="Metadata not found"): @@ -1013,10 +962,7 @@ class TestMetadataServiceGetDatasetMetadatas: ) # Mock usage count queries - mock_query = Mock() - mock_query.filter_by.return_value = mock_query - mock_query.count.return_value = 5 # 5 documents use this metadata - mock_db_session.query.return_value = mock_query + mock_db_session.scalar.return_value = 5 # 5 documents use this metadata # Act result = MetadataService.get_dataset_metadatas(dataset) diff --git a/api/tests/unit_tests/services/external_dataset_service.py b/api/tests/unit_tests/services/external_dataset_service.py index 70bd1c73b3..5848603ab8 100644 --- a/api/tests/unit_tests/services/external_dataset_service.py +++ b/api/tests/unit_tests/services/external_dataset_service.py @@ -292,7 +292,7 @@ class TestExternalDatasetServiceCrudExternalKnowledgeApi: """ api = Mock(spec=ExternalKnowledgeApis) - mock_db_session.query.return_value.filter_by.return_value.first.return_value = api + mock_db_session.scalar.return_value = api result = ExternalDatasetService.get_external_knowledge_api("api-id", "tenant-id") assert result is api @@ -302,7 +302,7 @@ class TestExternalDatasetServiceCrudExternalKnowledgeApi: When the record is absent, a ``ValueError`` is raised. """ - mock_db_session.query.return_value.filter_by.return_value.first.return_value = None + mock_db_session.scalar.return_value = None with pytest.raises(ValueError, match="api template not found"): ExternalDatasetService.get_external_knowledge_api("missing-id", "tenant-id") @@ -320,7 +320,7 @@ class TestExternalDatasetServiceCrudExternalKnowledgeApi: existing_api = Mock(spec=ExternalKnowledgeApis) existing_api.settings_dict = {"api_key": "stored-key"} existing_api.settings = '{"api_key":"stored-key"}' - mock_db_session.query.return_value.filter_by.return_value.first.return_value = existing_api + mock_db_session.scalar.return_value = existing_api args = { "name": "New Name", @@ -340,7 +340,7 @@ class TestExternalDatasetServiceCrudExternalKnowledgeApi: Updating a non‑existent API template should raise ``ValueError``. """ - mock_db_session.query.return_value.filter_by.return_value.first.return_value = None + mock_db_session.scalar.return_value = None with pytest.raises(ValueError, match="api template not found"): ExternalDatasetService.update_external_knowledge_api( @@ -356,7 +356,7 @@ class TestExternalDatasetServiceCrudExternalKnowledgeApi: """ api = Mock(spec=ExternalKnowledgeApis) - mock_db_session.query.return_value.filter_by.return_value.first.return_value = api + mock_db_session.scalar.return_value = api ExternalDatasetService.delete_external_knowledge_api("tenant-1", "api-1") @@ -368,7 +368,7 @@ class TestExternalDatasetServiceCrudExternalKnowledgeApi: Deletion of a missing template should raise ``ValueError``. """ - mock_db_session.query.return_value.filter_by.return_value.first.return_value = None + mock_db_session.scalar.return_value = None with pytest.raises(ValueError, match="api template not found"): ExternalDatasetService.delete_external_knowledge_api("tenant-1", "missing") @@ -394,7 +394,7 @@ class TestExternalDatasetServiceUsageAndBindings: When there are bindings, ``external_knowledge_api_use_check`` returns True and count. """ - mock_db_session.query.return_value.filter_by.return_value.count.return_value = 3 + mock_db_session.scalar.return_value = 3 in_use, count = ExternalDatasetService.external_knowledge_api_use_check("api-1") @@ -406,7 +406,7 @@ class TestExternalDatasetServiceUsageAndBindings: Zero bindings should return ``(False, 0)``. """ - mock_db_session.query.return_value.filter_by.return_value.count.return_value = 0 + mock_db_session.scalar.return_value = 0 in_use, count = ExternalDatasetService.external_knowledge_api_use_check("api-1") @@ -419,7 +419,7 @@ class TestExternalDatasetServiceUsageAndBindings: """ binding = Mock(spec=ExternalKnowledgeBindings) - mock_db_session.query.return_value.filter_by.return_value.first.return_value = binding + mock_db_session.scalar.return_value = binding result = ExternalDatasetService.get_external_knowledge_binding_with_dataset_id("tenant-1", "ds-1") assert result is binding @@ -429,7 +429,7 @@ class TestExternalDatasetServiceUsageAndBindings: Missing binding should result in a ``ValueError``. """ - mock_db_session.query.return_value.filter_by.return_value.first.return_value = None + mock_db_session.scalar.return_value = None with pytest.raises(ValueError, match="external knowledge binding not found"): ExternalDatasetService.get_external_knowledge_binding_with_dataset_id("tenant-1", "ds-1") @@ -460,7 +460,7 @@ class TestExternalDatasetServiceDocumentCreateArgsValidate: '[{"document_process_setting":[{"name":"foo","required":true},{"name":"bar","required":false}]}]' ) # Raw string; the service itself calls json.loads on it - mock_db_session.query.return_value.filter_by.return_value.first.return_value = external_api + mock_db_session.scalar.return_value = external_api process_parameter = {"foo": "value", "bar": "optional"} @@ -474,7 +474,7 @@ class TestExternalDatasetServiceDocumentCreateArgsValidate: When the referenced API template is missing, a ``ValueError`` is raised. """ - mock_db_session.query.return_value.filter_by.return_value.first.return_value = None + mock_db_session.scalar.return_value = None with pytest.raises(ValueError, match="api template not found"): ExternalDatasetService.document_create_args_validate("tenant-1", "missing", {}) @@ -488,7 +488,7 @@ class TestExternalDatasetServiceDocumentCreateArgsValidate: external_api.settings = ( '[{"document_process_setting":[{"name":"foo","required":true},{"name":"bar","required":false}]}]' ) - mock_db_session.query.return_value.filter_by.return_value.first.return_value = external_api + mock_db_session.scalar.return_value = external_api process_parameter = {"bar": "present"} # missing "foo" @@ -702,7 +702,7 @@ class TestExternalDatasetServiceCreateExternalDataset: } # No existing dataset with same name. - mock_db_session.query.return_value.filter_by.return_value.first.side_effect = [ + mock_db_session.scalar.side_effect = [ None, # duplicate‑name check Mock(spec=ExternalKnowledgeApis), # external knowledge api ] @@ -724,7 +724,7 @@ class TestExternalDatasetServiceCreateExternalDataset: """ existing_dataset = Mock(spec=Dataset) - mock_db_session.query.return_value.filter_by.return_value.first.return_value = existing_dataset + mock_db_session.scalar.return_value = existing_dataset args = { "name": "Existing", @@ -744,7 +744,7 @@ class TestExternalDatasetServiceCreateExternalDataset: """ # First call: duplicate name check – not found. - mock_db_session.query.return_value.filter_by.return_value.first.side_effect = [ + mock_db_session.scalar.side_effect = [ None, None, # external knowledge api lookup ] @@ -763,8 +763,10 @@ class TestExternalDatasetServiceCreateExternalDataset: ``external_knowledge_id`` and ``external_knowledge_api_id`` are mandatory. """ - # duplicate name check - mock_db_session.query.return_value.filter_by.return_value.first.side_effect = [ + # duplicate name check — two calls to create_external_dataset, each does 2 scalar calls + mock_db_session.scalar.side_effect = [ + None, + Mock(spec=ExternalKnowledgeApis), None, Mock(spec=ExternalKnowledgeApis), ] @@ -826,7 +828,7 @@ class TestExternalDatasetServiceFetchExternalKnowledgeRetrieval: api.settings = '{"endpoint":"https://example.com","api_key":"secret"}' # First query: binding; second query: api. - mock_db_session.query.return_value.filter_by.return_value.first.side_effect = [ + mock_db_session.scalar.side_effect = [ binding, api, ] @@ -861,7 +863,7 @@ class TestExternalDatasetServiceFetchExternalKnowledgeRetrieval: Missing binding should raise ``ValueError``. """ - mock_db_session.query.return_value.filter_by.return_value.first.return_value = None + mock_db_session.scalar.return_value = None with pytest.raises(ValueError, match="external knowledge binding not found"): ExternalDatasetService.fetch_external_knowledge_retrieval( @@ -878,7 +880,7 @@ class TestExternalDatasetServiceFetchExternalKnowledgeRetrieval: """ binding = ExternalDatasetTestDataFactory.create_external_binding() - mock_db_session.query.return_value.filter_by.return_value.first.side_effect = [ + mock_db_session.scalar.side_effect = [ binding, None, ] @@ -901,7 +903,7 @@ class TestExternalDatasetServiceFetchExternalKnowledgeRetrieval: api = Mock(spec=ExternalKnowledgeApis) api.settings = '{"endpoint":"https://example.com","api_key":"secret"}' - mock_db_session.query.return_value.filter_by.return_value.first.side_effect = [ + mock_db_session.scalar.side_effect = [ binding, api, ] 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 index cb3c2d742d..f270ee0fde 100644 --- 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 @@ -117,9 +117,7 @@ def test_get_all_published_workflow_applies_limit_and_has_more(rag_pipeline_serv 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) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=None) with pytest.raises(ValueError, match="Dataset not found"): rag_pipeline_service.get_pipeline("tenant-1", "dataset-1") @@ -131,12 +129,8 @@ def test_get_pipeline_raises_when_dataset_not_found(mocker, rag_pipeline_service 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]) + # First scalar finds the template, second scalar (duplicate check) returns None + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[template, None]) 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")) @@ -152,9 +146,7 @@ def test_update_customized_pipeline_template_success(mocker) -> None: 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.db.session.scalar", return_value=None) 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")) @@ -166,9 +158,7 @@ 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.db.session.scalar", side_effect=[template, duplicate]) 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")) @@ -181,9 +171,7 @@ def test_update_customized_pipeline_template_duplicate_name(mocker) -> None: 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) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=template) delete_mock = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.delete") commit_mock = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.commit") @@ -196,9 +184,7 @@ def test_delete_customized_pipeline_template_success(mocker) -> None: 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.db.session.scalar", return_value=None) 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"): @@ -397,18 +383,14 @@ def test_get_rag_pipeline_workflow_run_delegates(mocker, rag_pipeline_service) - 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) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=1) 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) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=0) pipeline = SimpleNamespace(tenant_id="t1", id="p1") assert rag_pipeline_service.is_workflow_exist(pipeline) is False @@ -738,8 +720,7 @@ def test_get_second_step_parameters_success(mocker, rag_pipeline_service) -> Non def test_publish_customized_pipeline_template_success(mocker, rag_pipeline_service) -> None: - from models.dataset import Dataset, Pipeline, PipelineCustomizedTemplate - from models.workflow import Workflow + from models.dataset import Pipeline # 1. Setup mocks pipeline = mocker.Mock(spec=Pipeline) @@ -754,36 +735,15 @@ def test_publish_customized_pipeline_template_success(mocker, rag_pipeline_servi # 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 get() for Pipeline and Workflow PK lookups + mock_db.session.get.side_effect = [pipeline, workflow] + # Mock scalar() for template name check (None) and max position (5) + mock_db.session.scalar.side_effect = [None, 5] # 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"} @@ -839,9 +799,7 @@ def test_get_datasource_plugins_success(mocker, rag_pipeline_service) -> None: 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("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline]) mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) @@ -881,11 +839,9 @@ def test_retry_error_document_success(mocker, rag_pipeline_service) -> None: 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) + # Mock queries: Log lookup via scalar, Pipeline lookup via get + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=log) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=pipeline) mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) @@ -913,7 +869,7 @@ def test_set_datasource_variables_success(mocker, rag_pipeline_service) -> None: # 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() + mock_db.session.scalar.return_value = mocker.Mock() pipeline = mocker.Mock(spec=Pipeline) pipeline.id = "p-1" @@ -976,7 +932,7 @@ def test_get_draft_workflow_success(mocker, rag_pipeline_service) -> None: 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 + mock_db.session.scalar.return_value = workflow # 2. Run test result = rag_pipeline_service.get_draft_workflow(pipeline) @@ -998,7 +954,7 @@ def test_get_published_workflow_success(mocker, rag_pipeline_service) -> None: 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 + mock_db.session.scalar.return_value = workflow # 2. Run test result = rag_pipeline_service.get_published_workflow(pipeline) @@ -1319,11 +1275,8 @@ def test_get_rag_pipeline_workflow_run_node_executions_returns_sorted_executions 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 + mock_db.session.scalars.return_value.all.return_value = [] result = rag_pipeline_service.get_recommended_plugins("all") @@ -1336,11 +1289,8 @@ def test_get_recommended_plugins_returns_empty_when_no_active_plugins(mocker, ra 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 + mock_db.session.scalars.return_value.all.return_value = [plugin_a, plugin_b] 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", @@ -1568,9 +1518,7 @@ def test_get_second_step_parameters_filters_first_step_variables(mocker, rag_pip 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) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=None) with pytest.raises(ValueError, match="Document pipeline execution log not found"): rag_pipeline_service.retry_error_document( @@ -1581,9 +1529,7 @@ def test_retry_error_document_raises_when_execution_log_not_found(mocker, rag_pi 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("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline]) mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=None) with pytest.raises(ValueError, match="Pipeline or workflow not found"): @@ -1656,8 +1602,7 @@ def test_handle_node_run_result_marks_document_error_for_published_invoke(mocker 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) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=document) add_mock = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.add") commit_mock = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.commit") @@ -1712,9 +1657,7 @@ def test_run_datasource_node_preview_raises_for_unsupported_provider(mocker, rag 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) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=None) with pytest.raises(ValueError, match="Pipeline not found"): rag_pipeline_service.publish_customized_pipeline_template("p1", {}) @@ -1722,9 +1665,7 @@ def test_publish_customized_pipeline_template_raises_for_missing_pipeline(mocker 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) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=pipeline) with pytest.raises(ValueError, match="Pipeline workflow not found"): rag_pipeline_service.publish_customized_pipeline_template("p1", {"name": "template-name"}) @@ -1732,8 +1673,7 @@ def test_publish_customized_pipeline_template_raises_for_missing_workflow_id(moc 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) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=None) with pytest.raises(ValueError, match="Dataset not found"): rag_pipeline_service.get_pipeline("t1", "d1") @@ -1742,8 +1682,7 @@ def test_get_pipeline_raises_when_dataset_missing(mocker, rag_pipeline_service) 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) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, None]) with pytest.raises(ValueError, match="Pipeline not found"): rag_pipeline_service.get_pipeline("t1", "d1") @@ -1783,8 +1722,7 @@ def test_get_pipeline_templates_builtin_en_us_no_fallback(mocker) -> None: 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) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=template) 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")) @@ -2011,8 +1949,7 @@ def test_run_free_workflow_node_delegates_to_handle_result(mocker, rag_pipeline_ 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) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", side_effect=[pipeline, None]) with pytest.raises(ValueError, match="Workflow not found"): rag_pipeline_service.publish_customized_pipeline_template("p1", {}) @@ -2021,11 +1958,9 @@ def test_publish_customized_pipeline_template_raises_when_workflow_missing(mocke 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 + mock_db.session.get.side_effect = [pipeline, workflow] session_ctx = mocker.MagicMock() session_ctx.__enter__.return_value = SimpleNamespace() session_ctx.__exit__.return_value = False @@ -2038,11 +1973,8 @@ def test_publish_customized_pipeline_template_raises_when_dataset_missing(mocker 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 + mock_db.session.scalars.return_value.all.return_value = [plugin] 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=[]) @@ -2056,8 +1988,8 @@ def test_get_recommended_plugins_skips_manifest_when_missing(mocker, rag_pipelin 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) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=exec_log) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=None) with pytest.raises(ValueError, match="Pipeline not found"): rag_pipeline_service.retry_error_document( @@ -2069,8 +2001,8 @@ def test_retry_error_document_raises_when_workflow_missing(mocker, rag_pipeline_ 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("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=exec_log) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=pipeline) mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=None) with pytest.raises(ValueError, match="Workflow not found"): @@ -2086,8 +2018,7 @@ def test_get_datasource_plugins_returns_empty_for_non_datasource_nodes(mocker, r 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("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline]) mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) assert rag_pipeline_service.get_datasource_plugins("t1", "d1", True) == [] @@ -2250,8 +2181,7 @@ def test_get_datasource_plugins_handles_empty_datasource_data_and_non_published( 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("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline]) 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=[] @@ -2291,8 +2221,7 @@ def test_get_datasource_plugins_extracts_user_inputs_and_credentials(mocker, rag ], ) 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("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline]) mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow) mocker.patch( "services.rag_pipeline.rag_pipeline.DatasourceProviderService.list_datasource_credentials", @@ -2310,8 +2239,7 @@ def test_get_pipeline_returns_pipeline_when_found(mocker, rag_pipeline_service) 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) + mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline]) result = rag_pipeline_service.get_pipeline("t1", "d1") diff --git a/api/tests/unit_tests/services/test_account_service.py b/api/tests/unit_tests/services/test_account_service.py index dcd6785464..cc64159c5f 100644 --- a/api/tests/unit_tests/services/test_account_service.py +++ b/api/tests/unit_tests/services/test_account_service.py @@ -173,9 +173,7 @@ class TestAccountService: # Setup test data mock_account = TestAccountAssociatedDataFactory.create_account_mock() - # Setup smart database query mock - query_results = {("Account", "email", "test@example.com"): mock_account} - ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + mock_db_dependencies["db"].session.scalar.return_value = mock_account mock_password_dependencies["compare_password"].return_value = True @@ -188,9 +186,7 @@ class TestAccountService: def test_authenticate_account_not_found(self, mock_db_dependencies): """Test authentication when account does not exist.""" - # Setup smart database query mock - no matching results - query_results = {("Account", "email", "notfound@example.com"): None} - ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + mock_db_dependencies["db"].session.scalar.return_value = None # Execute test and verify exception self._assert_exception_raised( @@ -202,9 +198,7 @@ class TestAccountService: # Setup test data mock_account = TestAccountAssociatedDataFactory.create_account_mock(status="banned") - # Setup smart database query mock - query_results = {("Account", "email", "banned@example.com"): mock_account} - ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + mock_db_dependencies["db"].session.scalar.return_value = mock_account # Execute test and verify exception self._assert_exception_raised(AccountLoginError, AccountService.authenticate, "banned@example.com", "password") @@ -214,9 +208,7 @@ class TestAccountService: # Setup test data mock_account = TestAccountAssociatedDataFactory.create_account_mock() - # Setup smart database query mock - query_results = {("Account", "email", "test@example.com"): mock_account} - ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + mock_db_dependencies["db"].session.scalar.return_value = mock_account mock_password_dependencies["compare_password"].return_value = False @@ -230,9 +222,7 @@ class TestAccountService: # Setup test data mock_account = TestAccountAssociatedDataFactory.create_account_mock(status="pending") - # Setup smart database query mock - query_results = {("Account", "email", "pending@example.com"): mock_account} - ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + mock_db_dependencies["db"].session.scalar.return_value = mock_account mock_password_dependencies["compare_password"].return_value = True @@ -422,12 +412,8 @@ class TestAccountService: mock_account = TestAccountAssociatedDataFactory.create_account_mock() mock_tenant_join = TestAccountAssociatedDataFactory.create_tenant_join_mock() - # Setup smart database query mock - query_results = { - ("Account", "id", "user-123"): mock_account, - ("TenantAccountJoin", "account_id", "user-123"): mock_tenant_join, - } - ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + mock_db_dependencies["db"].session.get.return_value = mock_account + mock_db_dependencies["db"].session.scalar.return_value = mock_tenant_join # Mock datetime with patch("services.account_service.datetime") as mock_datetime: @@ -444,9 +430,7 @@ class TestAccountService: def test_load_user_not_found(self, mock_db_dependencies): """Test user loading when user does not exist.""" - # Setup smart database query mock - no matching results - query_results = {("Account", "id", "non-existent-user"): None} - ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + mock_db_dependencies["db"].session.get.return_value = None # Execute test result = AccountService.load_user("non-existent-user") @@ -459,9 +443,7 @@ class TestAccountService: # Setup test data mock_account = TestAccountAssociatedDataFactory.create_account_mock(status="banned") - # Setup smart database query mock - query_results = {("Account", "id", "user-123"): mock_account} - ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + mock_db_dependencies["db"].session.get.return_value = mock_account # Execute test and verify exception self._assert_exception_raised( @@ -476,13 +458,9 @@ class TestAccountService: mock_account = TestAccountAssociatedDataFactory.create_account_mock() mock_available_tenant = TestAccountAssociatedDataFactory.create_tenant_join_mock(current=False) - # Setup smart database query mock for complex scenario - query_results = { - ("Account", "id", "user-123"): mock_account, - ("TenantAccountJoin", "account_id", "user-123"): None, # No current tenant - ("TenantAccountJoin", "order_by", "first_available"): mock_available_tenant, # First available tenant - } - ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + mock_db_dependencies["db"].session.get.return_value = mock_account + # First scalar: current tenant (None), second scalar: available tenant + mock_db_dependencies["db"].session.scalar.side_effect = [None, mock_available_tenant] # Mock datetime with patch("services.account_service.datetime") as mock_datetime: @@ -503,13 +481,9 @@ class TestAccountService: # Setup test data mock_account = TestAccountAssociatedDataFactory.create_account_mock() - # Setup smart database query mock for no tenants scenario - query_results = { - ("Account", "id", "user-123"): mock_account, - ("TenantAccountJoin", "account_id", "user-123"): None, # No current tenant - ("TenantAccountJoin", "order_by", "first_available"): None, # No available tenants - } - ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + mock_db_dependencies["db"].session.get.return_value = mock_account + # First scalar: current tenant (None), second scalar: available tenant (None) + mock_db_dependencies["db"].session.scalar.side_effect = [None, None] # Mock datetime with patch("services.account_service.datetime") as mock_datetime: @@ -1060,7 +1034,7 @@ class TestRegisterService: ) # Verify rollback operations were called - mock_db_dependencies["db"].session.query.assert_called() + mock_db_dependencies["db"].session.execute.assert_called() # ==================== Registration Tests ==================== @@ -1625,10 +1599,8 @@ class TestRegisterService: mock_session_class.return_value.__exit__.return_value = None mock_lookup.return_value = mock_existing_account - # Mock the db.session.query for TenantAccountJoin - mock_db_query = MagicMock() - mock_db_query.filter_by.return_value.first.return_value = None # No existing member - mock_db_dependencies["db"].session.query.return_value = mock_db_query + # Mock scalar for TenantAccountJoin lookup - no existing member + mock_db_dependencies["db"].session.scalar.return_value = None # Mock TenantService methods with ( @@ -1803,14 +1775,9 @@ class TestRegisterService: } mock_get_invitation_by_token.return_value = invitation_data - # Mock database queries - complex query mocking - mock_query1 = MagicMock() - mock_query1.where.return_value.first.return_value = mock_tenant - - mock_query2 = MagicMock() - mock_query2.join.return_value.where.return_value.first.return_value = (mock_account, "normal") - - mock_db_dependencies["db"].session.query.side_effect = [mock_query1, mock_query2] + # Mock scalar for tenant lookup, execute for account+role lookup + mock_db_dependencies["db"].session.scalar.return_value = mock_tenant + mock_db_dependencies["db"].session.execute.return_value.first.return_value = (mock_account, "normal") # Execute test result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123") @@ -1842,10 +1809,8 @@ class TestRegisterService: } mock_redis_dependencies.get.return_value = json.dumps(invitation_data).encode() - # Mock database queries - no tenant found - mock_query = MagicMock() - mock_query.filter.return_value.first.return_value = None - mock_db_dependencies["db"].session.query.return_value = mock_query + # Mock scalar for tenant lookup - not found + mock_db_dependencies["db"].session.scalar.return_value = None # Execute test result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123") @@ -1868,14 +1833,9 @@ class TestRegisterService: } mock_redis_dependencies.get.return_value = json.dumps(invitation_data).encode() - # Mock database queries - mock_query1 = MagicMock() - mock_query1.filter.return_value.first.return_value = mock_tenant - - mock_query2 = MagicMock() - mock_query2.join.return_value.where.return_value.first.return_value = None # No account found - - mock_db_dependencies["db"].session.query.side_effect = [mock_query1, mock_query2] + # Mock scalar for tenant, execute for account+role + mock_db_dependencies["db"].session.scalar.return_value = mock_tenant + mock_db_dependencies["db"].session.execute.return_value.first.return_value = None # No account found # Execute test result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123") @@ -1901,14 +1861,9 @@ class TestRegisterService: } mock_redis_dependencies.get.return_value = json.dumps(invitation_data).encode() - # Mock database queries - mock_query1 = MagicMock() - mock_query1.filter.return_value.first.return_value = mock_tenant - - mock_query2 = MagicMock() - mock_query2.join.return_value.where.return_value.first.return_value = (mock_account, "normal") - - mock_db_dependencies["db"].session.query.side_effect = [mock_query1, mock_query2] + # Mock scalar for tenant, execute for account+role + mock_db_dependencies["db"].session.scalar.return_value = mock_tenant + mock_db_dependencies["db"].session.execute.return_value.first.return_value = (mock_account, "normal") # Execute test result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123") diff --git a/api/tests/unit_tests/services/test_external_dataset_service.py b/api/tests/unit_tests/services/test_external_dataset_service.py index 3709e1fa94..7c8dab5029 100644 --- a/api/tests/unit_tests/services/test_external_dataset_service.py +++ b/api/tests/unit_tests/services/test_external_dataset_service.py @@ -799,10 +799,7 @@ class TestExternalDatasetServiceGetAPI: api_id = "api-123" expected_api = factory.create_external_knowledge_api_mock(api_id=api_id) - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = expected_api + mock_db.session.scalar.return_value = expected_api # Act tenant_id = "tenant-123" @@ -810,16 +807,12 @@ class TestExternalDatasetServiceGetAPI: # Assert assert result.id == api_id - mock_query.filter_by.assert_called_once_with(id=api_id, tenant_id=tenant_id) @patch("services.external_knowledge_service.db") def test_get_external_knowledge_api_not_found(self, mock_db, factory): """Test error when API is not found.""" # Arrange - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(ValueError, match="api template not found"): @@ -848,10 +841,7 @@ class TestExternalDatasetServiceUpdateAPI: "settings": {"endpoint": "https://new.example.com", "api_key": "new-key"}, } - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = existing_api + mock_db.session.scalar.return_value = existing_api # Act result = ExternalDatasetService.update_external_knowledge_api(tenant_id, user_id, api_id, args) @@ -881,10 +871,7 @@ class TestExternalDatasetServiceUpdateAPI: "settings": {"endpoint": "https://api.example.com", "api_key": HIDDEN_VALUE}, } - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = existing_api + mock_db.session.scalar.return_value = existing_api # Act result = ExternalDatasetService.update_external_knowledge_api(tenant_id, "user-123", api_id, args) @@ -897,10 +884,7 @@ class TestExternalDatasetServiceUpdateAPI: def test_update_external_knowledge_api_not_found(self, mock_db, factory): """Test error when API is not found.""" # Arrange - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None + mock_db.session.scalar.return_value = None args = {"name": "Updated API"} @@ -912,10 +896,7 @@ class TestExternalDatasetServiceUpdateAPI: def test_update_external_knowledge_api_tenant_mismatch(self, mock_db, factory): """Test error when tenant ID doesn't match.""" # Arrange - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None + mock_db.session.scalar.return_value = None args = {"name": "Updated API"} @@ -934,10 +915,7 @@ class TestExternalDatasetServiceUpdateAPI: args = {"name": "New Name Only"} - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = existing_api + mock_db.session.scalar.return_value = existing_api # Act result = ExternalDatasetService.update_external_knowledge_api("tenant-123", "user-123", "api-123", args) @@ -958,10 +936,7 @@ class TestExternalDatasetServiceDeleteAPI: existing_api = factory.create_external_knowledge_api_mock(api_id=api_id, tenant_id=tenant_id) - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = existing_api + mock_db.session.scalar.return_value = existing_api # Act ExternalDatasetService.delete_external_knowledge_api(tenant_id, api_id) @@ -974,10 +949,7 @@ class TestExternalDatasetServiceDeleteAPI: def test_delete_external_knowledge_api_not_found(self, mock_db, factory): """Test error when API is not found.""" # Arrange - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(ValueError, match="api template not found"): @@ -987,10 +959,7 @@ class TestExternalDatasetServiceDeleteAPI: def test_delete_external_knowledge_api_tenant_mismatch(self, mock_db, factory): """Test error when tenant ID doesn't match.""" # Arrange - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(ValueError, match="api template not found"): @@ -1006,10 +975,7 @@ class TestExternalDatasetServiceAPIUseCheck: # Arrange api_id = "api-123" - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.count.return_value = 1 + mock_db.session.scalar.return_value = 1 # Act in_use, count = ExternalDatasetService.external_knowledge_api_use_check(api_id) @@ -1024,10 +990,7 @@ class TestExternalDatasetServiceAPIUseCheck: # Arrange api_id = "api-123" - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.count.return_value = 10 + mock_db.session.scalar.return_value = 10 # Act in_use, count = ExternalDatasetService.external_knowledge_api_use_check(api_id) @@ -1042,10 +1005,7 @@ class TestExternalDatasetServiceAPIUseCheck: # Arrange api_id = "api-123" - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.count.return_value = 0 + mock_db.session.scalar.return_value = 0 # Act in_use, count = ExternalDatasetService.external_knowledge_api_use_check(api_id) @@ -1067,10 +1027,7 @@ class TestExternalDatasetServiceGetBinding: expected_binding = factory.create_external_knowledge_binding_mock(tenant_id=tenant_id, dataset_id=dataset_id) - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = expected_binding + mock_db.session.scalar.return_value = expected_binding # Act result = ExternalDatasetService.get_external_knowledge_binding_with_dataset_id(tenant_id, dataset_id) @@ -1083,10 +1040,7 @@ class TestExternalDatasetServiceGetBinding: def test_get_external_knowledge_binding_not_found(self, mock_db, factory): """Test error when binding is not found.""" # Arrange - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(ValueError, match="external knowledge binding not found"): @@ -1113,10 +1067,7 @@ class TestExternalDatasetServiceDocumentValidate: api = factory.create_external_knowledge_api_mock(api_id=api_id, settings=[settings]) - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = api + mock_db.session.scalar.return_value = api process_parameter = {"param1": "value1", "param2": "value2"} @@ -1134,10 +1085,7 @@ class TestExternalDatasetServiceDocumentValidate: api = factory.create_external_knowledge_api_mock(api_id=api_id, settings=[settings]) - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = api + mock_db.session.scalar.return_value = api process_parameter = {} @@ -1149,10 +1097,7 @@ class TestExternalDatasetServiceDocumentValidate: def test_document_create_args_validate_api_not_found(self, mock_db, factory): """Test validation fails when API is not found.""" # Arrange - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(ValueError, match="api template not found"): @@ -1165,10 +1110,7 @@ class TestExternalDatasetServiceDocumentValidate: settings = {} api = factory.create_external_knowledge_api_mock(settings=[settings]) - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = api + mock_db.session.scalar.return_value = api # Act & Assert - should not raise ExternalDatasetService.document_create_args_validate("tenant-123", "api-123", {}) @@ -1186,10 +1128,7 @@ class TestExternalDatasetServiceDocumentValidate: api = factory.create_external_knowledge_api_mock(settings=[settings]) - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = api + mock_db.session.scalar.return_value = api process_parameter = {"required_param": "value"} @@ -1498,24 +1437,7 @@ class TestExternalDatasetServiceCreateDataset: api = factory.create_external_knowledge_api_mock(api_id="api-123") - # Mock database queries - mock_dataset_query = MagicMock() - mock_api_query = MagicMock() - - def query_side_effect(model): - if model == Dataset: - return mock_dataset_query - elif model == ExternalKnowledgeApis: - return mock_api_query - return MagicMock() - - mock_db.session.query.side_effect = query_side_effect - - mock_dataset_query.filter_by.return_value = mock_dataset_query - mock_dataset_query.first.return_value = None - - mock_api_query.filter_by.return_value = mock_api_query - mock_api_query.first.return_value = api + mock_db.session.scalar.side_effect = [None, api] # Act result = ExternalDatasetService.create_external_dataset(tenant_id, user_id, args) @@ -1534,10 +1456,7 @@ class TestExternalDatasetServiceCreateDataset: # Arrange existing_dataset = factory.create_dataset_mock(name="Duplicate Dataset") - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = existing_dataset + mock_db.session.scalar.return_value = existing_dataset args = {"name": "Duplicate Dataset"} @@ -1549,23 +1468,7 @@ class TestExternalDatasetServiceCreateDataset: def test_create_external_dataset_api_not_found_error(self, mock_db, factory): """Test error when external knowledge API is not found.""" # Arrange - mock_dataset_query = MagicMock() - mock_api_query = MagicMock() - - def query_side_effect(model): - if model == Dataset: - return mock_dataset_query - elif model == ExternalKnowledgeApis: - return mock_api_query - return MagicMock() - - mock_db.session.query.side_effect = query_side_effect - - mock_dataset_query.filter_by.return_value = mock_dataset_query - mock_dataset_query.first.return_value = None - - mock_api_query.filter_by.return_value = mock_api_query - mock_api_query.first.return_value = None + mock_db.session.scalar.side_effect = [None, None] args = {"name": "Test Dataset", "external_knowledge_api_id": "nonexistent-api"} @@ -1579,23 +1482,7 @@ class TestExternalDatasetServiceCreateDataset: # Arrange api = factory.create_external_knowledge_api_mock() - mock_dataset_query = MagicMock() - mock_api_query = MagicMock() - - def query_side_effect(model): - if model == Dataset: - return mock_dataset_query - elif model == ExternalKnowledgeApis: - return mock_api_query - return MagicMock() - - mock_db.session.query.side_effect = query_side_effect - - mock_dataset_query.filter_by.return_value = mock_dataset_query - mock_dataset_query.first.return_value = None - - mock_api_query.filter_by.return_value = mock_api_query - mock_api_query.first.return_value = api + mock_db.session.scalar.side_effect = [None, api] args = {"name": "Test Dataset", "external_knowledge_api_id": "api-123"} @@ -1609,23 +1496,7 @@ class TestExternalDatasetServiceCreateDataset: # Arrange api = factory.create_external_knowledge_api_mock() - mock_dataset_query = MagicMock() - mock_api_query = MagicMock() - - def query_side_effect(model): - if model == Dataset: - return mock_dataset_query - elif model == ExternalKnowledgeApis: - return mock_api_query - return MagicMock() - - mock_db.session.query.side_effect = query_side_effect - - mock_dataset_query.filter_by.return_value = mock_dataset_query - mock_dataset_query.first.return_value = None - - mock_api_query.filter_by.return_value = mock_api_query - mock_api_query.first.return_value = api + mock_db.session.scalar.side_effect = [None, api] args = {"name": "Test Dataset", "external_knowledge_id": "knowledge-123"} @@ -1651,23 +1522,7 @@ class TestExternalDatasetServiceFetchRetrieval: ) api = factory.create_external_knowledge_api_mock(api_id="api-123") - mock_binding_query = MagicMock() - mock_api_query = MagicMock() - - def query_side_effect(model): - if model == ExternalKnowledgeBindings: - return mock_binding_query - elif model == ExternalKnowledgeApis: - return mock_api_query - return MagicMock() - - mock_db.session.query.side_effect = query_side_effect - - mock_binding_query.filter_by.return_value = mock_binding_query - mock_binding_query.first.return_value = binding - - mock_api_query.filter_by.return_value = mock_api_query - mock_api_query.first.return_value = api + mock_db.session.scalar.side_effect = [binding, api] mock_response = MagicMock() mock_response.status_code = 200 @@ -1695,10 +1550,7 @@ class TestExternalDatasetServiceFetchRetrieval: def test_fetch_external_knowledge_retrieval_binding_not_found_error(self, mock_db, factory): """Test error when external knowledge binding is not found.""" # Arrange - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.filter_by.return_value = mock_query - mock_query.first.return_value = None + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(ValueError, match="external knowledge binding not found"): @@ -1712,23 +1564,7 @@ class TestExternalDatasetServiceFetchRetrieval: binding = factory.create_external_knowledge_binding_mock() api = factory.create_external_knowledge_api_mock() - mock_binding_query = MagicMock() - mock_api_query = MagicMock() - - def query_side_effect(model): - if model == ExternalKnowledgeBindings: - return mock_binding_query - elif model == ExternalKnowledgeApis: - return mock_api_query - return MagicMock() - - mock_db.session.query.side_effect = query_side_effect - - mock_binding_query.filter_by.return_value = mock_binding_query - mock_binding_query.first.return_value = binding - - mock_api_query.filter_by.return_value = mock_api_query - mock_api_query.first.return_value = api + mock_db.session.scalar.side_effect = [binding, api] mock_response = MagicMock() mock_response.status_code = 200 @@ -1751,23 +1587,7 @@ class TestExternalDatasetServiceFetchRetrieval: binding = factory.create_external_knowledge_binding_mock() api = factory.create_external_knowledge_api_mock() - mock_binding_query = MagicMock() - mock_api_query = MagicMock() - - def query_side_effect(model): - if model == ExternalKnowledgeBindings: - return mock_binding_query - elif model == ExternalKnowledgeApis: - return mock_api_query - return MagicMock() - - mock_db.session.query.side_effect = query_side_effect - - mock_binding_query.filter_by.return_value = mock_binding_query - mock_binding_query.first.return_value = binding - - mock_api_query.filter_by.return_value = mock_api_query - mock_api_query.first.return_value = api + mock_db.session.scalar.side_effect = [binding, api] mock_response = MagicMock() mock_response.status_code = 200 @@ -1799,23 +1619,7 @@ class TestExternalDatasetServiceFetchRetrieval: binding = factory.create_external_knowledge_binding_mock() api = factory.create_external_knowledge_api_mock() - mock_binding_query = MagicMock() - mock_api_query = MagicMock() - - def query_side_effect(model): - if model == ExternalKnowledgeBindings: - return mock_binding_query - elif model == ExternalKnowledgeApis: - return mock_api_query - return MagicMock() - - mock_db.session.query.side_effect = query_side_effect - - mock_binding_query.filter_by.return_value = mock_binding_query - mock_binding_query.first.return_value = binding - - mock_api_query.filter_by.return_value = mock_api_query - mock_api_query.first.return_value = api + mock_db.session.scalar.side_effect = [binding, api] mock_response = MagicMock() mock_response.status_code = 500 @@ -1856,23 +1660,7 @@ class TestExternalDatasetServiceFetchRetrieval: ) api = factory.create_external_knowledge_api_mock(api_id="api-123") - mock_binding_query = MagicMock() - mock_api_query = MagicMock() - - def query_side_effect(model): - if model == ExternalKnowledgeBindings: - return mock_binding_query - elif model == ExternalKnowledgeApis: - return mock_api_query - return MagicMock() - - mock_db.session.query.side_effect = query_side_effect - - mock_binding_query.filter_by.return_value = mock_binding_query - mock_binding_query.first.return_value = binding - - mock_api_query.filter_by.return_value = mock_api_query - mock_api_query.first.return_value = api + mock_db.session.scalar.side_effect = [binding, api] mock_response = MagicMock() mock_response.status_code = status_code @@ -1891,23 +1679,7 @@ class TestExternalDatasetServiceFetchRetrieval: binding = factory.create_external_knowledge_binding_mock() api = factory.create_external_knowledge_api_mock() - mock_binding_query = MagicMock() - mock_api_query = MagicMock() - - def query_side_effect(model): - if model == ExternalKnowledgeBindings: - return mock_binding_query - elif model == ExternalKnowledgeApis: - return mock_api_query - return MagicMock() - - mock_db.session.query.side_effect = query_side_effect - - mock_binding_query.filter_by.return_value = mock_binding_query - mock_binding_query.first.return_value = binding - - mock_api_query.filter_by.return_value = mock_api_query - mock_api_query.first.return_value = api + mock_db.session.scalar.side_effect = [binding, api] mock_response = MagicMock() mock_response.status_code = 503 diff --git a/api/tests/unit_tests/services/test_model_provider_service_sanitization.py b/api/tests/unit_tests/services/test_model_provider_service_sanitization.py index 1bd979b9ec..acf5dff634 100644 --- a/api/tests/unit_tests/services/test_model_provider_service_sanitization.py +++ b/api/tests/unit_tests/services/test_model_provider_service_sanitization.py @@ -85,3 +85,644 @@ def test_get_provider_list_strips_credentials(service_with_fake_configurations: assert len(custom_models) == 1 # The sanitizer should drop credentials in list response assert custom_models[0].credentials is None + + +# === Merged from test_model_provider_service.py === + + +from types import SimpleNamespace +from typing import Any +from unittest.mock import MagicMock + +import pytest +from graphon.model_runtime.entities.common_entities import I18nObject +from graphon.model_runtime.entities.model_entities import FetchFrom, ModelType, ParameterRule, ParameterType + +from core.entities.model_entities import ModelStatus +from models.provider import ProviderType +from services import model_provider_service as service_module +from services.errors.app_model_config import ProviderNotFoundError +from services.model_provider_service import ModelProviderService + + +def _create_service_with_mocked_manager() -> tuple[ModelProviderService, MagicMock]: + manager = MagicMock() + service = ModelProviderService() + service._get_provider_manager = MagicMock(return_value=manager) + return service, manager + + +def _build_provider_configuration( + *, + provider_name: str = "openai", + supported_model_types: list[ModelType] | None = None, + custom_models: list[Any] | None = None, + custom_config_available: bool = True, +) -> SimpleNamespace: + if supported_model_types is None: + supported_model_types = [ModelType.LLM] + return SimpleNamespace( + provider=SimpleNamespace( + provider=provider_name, + label=I18nObject(en_US=provider_name), + description=None, + icon_small=None, + icon_small_dark=None, + background=None, + help=None, + supported_model_types=supported_model_types, + configurate_methods=[], + provider_credential_schema=None, + model_credential_schema=None, + ), + preferred_provider_type=ProviderType.CUSTOM, + custom_configuration=SimpleNamespace( + provider=SimpleNamespace( + current_credential_id="cred-1", + current_credential_name="Credential 1", + available_credentials=[], + ), + models=custom_models, + can_added_models=[], + ), + system_configuration=SimpleNamespace(enabled=False, current_quota_type=None, quota_configurations=[]), + is_custom_configuration_available=lambda: custom_config_available, + ) + + +def test__get_provider_configuration_should_return_configuration_when_provider_exists() -> None: + # Arrange + service, manager = _create_service_with_mocked_manager() + provider_configuration = SimpleNamespace(name="provider-config") + manager.get_configurations.return_value = {"openai": provider_configuration} + + # Act + result = service._get_provider_configuration(tenant_id="tenant-1", provider="openai") + + # Assert + assert result is provider_configuration + + +def test__get_provider_configuration_should_raise_error_when_provider_is_missing() -> None: + # Arrange + service, manager = _create_service_with_mocked_manager() + manager.get_configurations.return_value = {} + + # Act / Assert + with pytest.raises(ProviderNotFoundError, match="does not exist"): + service._get_provider_configuration(tenant_id="tenant-1", provider="missing") + + +def test_get_provider_list_should_filter_by_model_type_and_build_no_configure_status() -> None: + # Arrange + service, manager = _create_service_with_mocked_manager() + allowed = _build_provider_configuration( + provider_name="openai", + supported_model_types=[ModelType.LLM], + custom_config_available=False, + ) + filtered = _build_provider_configuration( + provider_name="embedding", + supported_model_types=[ModelType.TEXT_EMBEDDING], + custom_config_available=True, + ) + manager.get_configurations.return_value = {"openai": allowed, "embedding": filtered} + + # Act + result = service.get_provider_list(tenant_id="tenant-1", model_type=ModelType.LLM.value) + + # Assert + assert len(result) == 1 + assert result[0].provider == "openai" + assert result[0].custom_configuration.status.value == "no-configure" + + +def test_get_models_by_provider_should_wrap_model_entities_with_tenant_context() -> None: + # Arrange + service, manager = _create_service_with_mocked_manager() + + class _Model: + def __init__(self, model_name: str) -> None: + self.model_name = model_name + + def model_dump(self) -> dict[str, Any]: + return { + "model": self.model_name, + "label": {"en_US": self.model_name}, + "model_type": ModelType.LLM, + "features": [], + "fetch_from": FetchFrom.PREDEFINED_MODEL, + "model_properties": {}, + "deprecated": False, + "status": ModelStatus.ACTIVE, + "load_balancing_enabled": False, + "has_invalid_load_balancing_configs": False, + "provider": { + "provider": "openai", + "label": {"en_US": "OpenAI"}, + "icon_small": None, + "icon_small_dark": None, + "supported_model_types": [ModelType.LLM], + }, + } + + provider_configurations = SimpleNamespace( + get_models=MagicMock(return_value=[_Model("gpt-4o"), _Model("gpt-4o-mini")]) + ) + manager.get_configurations.return_value = provider_configurations + + # Act + result = service.get_models_by_provider(tenant_id="tenant-1", provider="openai") + + # Assert + assert len(result) == 2 + assert result[0].model == "gpt-4o" + assert result[1].provider.provider == "openai" + provider_configurations.get_models.assert_called_once_with(provider="openai") + + +@pytest.mark.parametrize( + ("method_name", "method_kwargs", "provider_method_name", "provider_call_kwargs", "provider_return"), + [ + ( + "get_provider_credential", + {"tenant_id": "tenant-1", "provider": "openai", "credential_id": "cred-1"}, + "get_provider_credential", + {"credential_id": "cred-1"}, + {"token": "abc"}, + ), + ( + "validate_provider_credentials", + {"tenant_id": "tenant-1", "provider": "openai", "credentials": {"token": "abc"}}, + "validate_provider_credentials", + ({"token": "abc"},), + None, + ), + ( + "create_provider_credential", + {"tenant_id": "tenant-1", "provider": "openai", "credentials": {"token": "abc"}, "credential_name": "A"}, + "create_provider_credential", + ({"token": "abc"}, "A"), + None, + ), + ( + "update_provider_credential", + { + "tenant_id": "tenant-1", + "provider": "openai", + "credentials": {"token": "abc"}, + "credential_id": "cred-1", + "credential_name": "B", + }, + "update_provider_credential", + {"credential_id": "cred-1", "credentials": {"token": "abc"}, "credential_name": "B"}, + None, + ), + ( + "remove_provider_credential", + {"tenant_id": "tenant-1", "provider": "openai", "credential_id": "cred-1"}, + "delete_provider_credential", + {"credential_id": "cred-1"}, + None, + ), + ( + "switch_active_provider_credential", + {"tenant_id": "tenant-1", "provider": "openai", "credential_id": "cred-1"}, + "switch_active_provider_credential", + {"credential_id": "cred-1"}, + None, + ), + ], +) +def test_provider_credential_methods_should_delegate_to_provider_configuration( + method_name: str, + method_kwargs: dict[str, Any], + provider_method_name: str, + provider_call_kwargs: Any, + provider_return: Any, + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + service = ModelProviderService() + provider_configuration = MagicMock() + getattr(provider_configuration, provider_method_name).return_value = provider_return + get_provider_config_mock = MagicMock(return_value=provider_configuration) + monkeypatch.setattr(service, "_get_provider_configuration", get_provider_config_mock) + + # Act + result = getattr(service, method_name)(**method_kwargs) + + # Assert + get_provider_config_mock.assert_called_once_with("tenant-1", "openai") + provider_method = getattr(provider_configuration, provider_method_name) + if isinstance(provider_call_kwargs, tuple): + provider_method.assert_called_once_with(*provider_call_kwargs) + elif isinstance(provider_call_kwargs, dict): + provider_method.assert_called_once_with(**provider_call_kwargs) + else: + provider_method.assert_called_once_with(provider_call_kwargs) + if method_name == "get_provider_credential": + assert result == {"token": "abc"} + + +@pytest.mark.parametrize( + ("method_name", "method_kwargs", "provider_method_name", "expected_kwargs", "provider_return"), + [ + ( + "get_model_credential", + { + "tenant_id": "tenant-1", + "provider": "openai", + "model_type": ModelType.LLM.value, + "model": "gpt-4o", + "credential_id": "cred-1", + }, + "get_custom_model_credential", + {"model_type": ModelType.LLM, "model": "gpt-4o", "credential_id": "cred-1"}, + {"api_key": "x"}, + ), + ( + "validate_model_credentials", + { + "tenant_id": "tenant-1", + "provider": "openai", + "model_type": ModelType.LLM.value, + "model": "gpt-4o", + "credentials": {"api_key": "x"}, + }, + "validate_custom_model_credentials", + {"model_type": ModelType.LLM, "model": "gpt-4o", "credentials": {"api_key": "x"}}, + None, + ), + ( + "create_model_credential", + { + "tenant_id": "tenant-1", + "provider": "openai", + "model_type": ModelType.LLM.value, + "model": "gpt-4o", + "credentials": {"api_key": "x"}, + "credential_name": "cred-a", + }, + "create_custom_model_credential", + { + "model_type": ModelType.LLM, + "model": "gpt-4o", + "credentials": {"api_key": "x"}, + "credential_name": "cred-a", + }, + None, + ), + ( + "update_model_credential", + { + "tenant_id": "tenant-1", + "provider": "openai", + "model_type": ModelType.LLM.value, + "model": "gpt-4o", + "credentials": {"api_key": "x"}, + "credential_id": "cred-1", + "credential_name": "cred-b", + }, + "update_custom_model_credential", + { + "model_type": ModelType.LLM, + "model": "gpt-4o", + "credentials": {"api_key": "x"}, + "credential_id": "cred-1", + "credential_name": "cred-b", + }, + None, + ), + ( + "remove_model_credential", + { + "tenant_id": "tenant-1", + "provider": "openai", + "model_type": ModelType.LLM.value, + "model": "gpt-4o", + "credential_id": "cred-1", + }, + "delete_custom_model_credential", + {"model_type": ModelType.LLM, "model": "gpt-4o", "credential_id": "cred-1"}, + None, + ), + ( + "switch_active_custom_model_credential", + { + "tenant_id": "tenant-1", + "provider": "openai", + "model_type": ModelType.LLM.value, + "model": "gpt-4o", + "credential_id": "cred-1", + }, + "switch_custom_model_credential", + {"model_type": ModelType.LLM, "model": "gpt-4o", "credential_id": "cred-1"}, + None, + ), + ( + "add_model_credential_to_model_list", + { + "tenant_id": "tenant-1", + "provider": "openai", + "model_type": ModelType.LLM.value, + "model": "gpt-4o", + "credential_id": "cred-1", + }, + "add_model_credential_to_model", + {"model_type": ModelType.LLM, "model": "gpt-4o", "credential_id": "cred-1"}, + None, + ), + ( + "remove_model", + { + "tenant_id": "tenant-1", + "provider": "openai", + "model_type": ModelType.LLM.value, + "model": "gpt-4o", + }, + "delete_custom_model", + {"model_type": ModelType.LLM, "model": "gpt-4o"}, + None, + ), + ], +) +def test_custom_model_methods_should_convert_model_type_and_delegate( + method_name: str, + method_kwargs: dict[str, Any], + provider_method_name: str, + expected_kwargs: dict[str, Any], + provider_return: Any, + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + service = ModelProviderService() + provider_configuration = MagicMock() + getattr(provider_configuration, provider_method_name).return_value = provider_return + get_provider_config_mock = MagicMock(return_value=provider_configuration) + monkeypatch.setattr(service, "_get_provider_configuration", get_provider_config_mock) + + # Act + result = getattr(service, method_name)(**method_kwargs) + + # Assert + get_provider_config_mock.assert_called_once_with("tenant-1", "openai") + getattr(provider_configuration, provider_method_name).assert_called_once_with(**expected_kwargs) + if method_name == "get_model_credential": + assert result == {"api_key": "x"} + + +def test_get_models_by_model_type_should_group_active_non_deprecated_models() -> None: + # Arrange + service, manager = _create_service_with_mocked_manager() + openai_provider = SimpleNamespace( + provider="openai", + label=I18nObject(en_US="OpenAI"), + icon_small=None, + icon_small_dark=None, + ) + anthropic_provider = SimpleNamespace( + provider="anthropic", + label=I18nObject(en_US="Anthropic"), + icon_small=None, + icon_small_dark=None, + ) + models = [ + SimpleNamespace( + provider=openai_provider, + model="gpt-4o", + label=I18nObject(en_US="GPT-4o"), + model_type=ModelType.LLM, + features=[], + fetch_from=FetchFrom.PREDEFINED_MODEL, + model_properties={}, + status=ModelStatus.ACTIVE, + load_balancing_enabled=False, + deprecated=False, + ), + SimpleNamespace( + provider=openai_provider, + model="old-openai", + label=I18nObject(en_US="Old OpenAI"), + model_type=ModelType.LLM, + features=[], + fetch_from=FetchFrom.PREDEFINED_MODEL, + model_properties={}, + status=ModelStatus.ACTIVE, + load_balancing_enabled=False, + deprecated=True, + ), + SimpleNamespace( + provider=anthropic_provider, + model="old-anthropic", + label=I18nObject(en_US="Old Anthropic"), + model_type=ModelType.LLM, + features=[], + fetch_from=FetchFrom.PREDEFINED_MODEL, + model_properties={}, + status=ModelStatus.ACTIVE, + load_balancing_enabled=False, + deprecated=True, + ), + ] + provider_configurations = SimpleNamespace(get_models=MagicMock(return_value=models)) + manager.get_configurations.return_value = provider_configurations + + # Act + result = service.get_models_by_model_type(tenant_id="tenant-1", model_type=ModelType.LLM.value) + + # Assert + provider_configurations.get_models.assert_called_once_with(model_type=ModelType.LLM, only_active=True) + assert len(result) == 1 + assert result[0].provider == "openai" + assert len(result[0].models) == 1 + assert result[0].models[0].model == "gpt-4o" + + +@pytest.mark.parametrize( + ("credentials", "schema", "expected_count"), + [ + (None, None, 0), + ({"api_key": "x"}, None, 0), + ( + {"api_key": "x"}, + SimpleNamespace( + parameter_rules=[ + ParameterRule( + name="temperature", + label=I18nObject(en_US="Temperature"), + type=ParameterType.FLOAT, + ) + ] + ), + 1, + ), + ], +) +def test_get_model_parameter_rules_should_handle_missing_credentials_and_schema( + credentials: dict[str, Any] | None, + schema: Any, + expected_count: int, + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + service = ModelProviderService() + provider_configuration = MagicMock() + provider_configuration.get_current_credentials.return_value = credentials + provider_configuration.get_model_schema.return_value = schema + monkeypatch.setattr(service, "_get_provider_configuration", MagicMock(return_value=provider_configuration)) + + # Act + result = service.get_model_parameter_rules(tenant_id="tenant-1", provider="openai", model="gpt-4o") + + # Assert + assert len(result) == expected_count + provider_configuration.get_current_credentials.assert_called_once_with(model_type=ModelType.LLM, model="gpt-4o") + if credentials: + provider_configuration.get_model_schema.assert_called_once_with( + model_type=ModelType.LLM, + model="gpt-4o", + credentials=credentials, + ) + else: + provider_configuration.get_model_schema.assert_not_called() + + +def test_get_default_model_of_model_type_should_return_response_when_manager_returns_model() -> None: + # Arrange + service, manager = _create_service_with_mocked_manager() + manager.get_default_model.return_value = SimpleNamespace( + model="gpt-4o", + model_type=ModelType.LLM, + provider=SimpleNamespace( + provider="openai", + label=I18nObject(en_US="OpenAI"), + icon_small=None, + supported_model_types=[ModelType.LLM], + ), + ) + + # Act + result = service.get_default_model_of_model_type(tenant_id="tenant-1", model_type=ModelType.LLM.value) + + # Assert + assert result is not None + assert result.model == "gpt-4o" + assert result.provider.provider == "openai" + manager.get_default_model.assert_called_once_with(tenant_id="tenant-1", model_type=ModelType.LLM) + + +def test_get_default_model_of_model_type_should_return_none_when_manager_returns_none() -> None: + # Arrange + service, manager = _create_service_with_mocked_manager() + manager.get_default_model.return_value = None + + # Act + result = service.get_default_model_of_model_type(tenant_id="tenant-1", model_type=ModelType.LLM.value) + + # Assert + assert result is None + + +def test_get_default_model_of_model_type_should_return_none_when_manager_raises_exception() -> None: + # Arrange + service, manager = _create_service_with_mocked_manager() + manager.get_default_model.side_effect = RuntimeError("boom") + + # Act + result = service.get_default_model_of_model_type(tenant_id="tenant-1", model_type=ModelType.LLM.value) + + # Assert + assert result is None + + +def test_update_default_model_of_model_type_should_delegate_to_provider_manager() -> None: + # Arrange + service, manager = _create_service_with_mocked_manager() + + # Act + service.update_default_model_of_model_type( + tenant_id="tenant-1", + model_type=ModelType.LLM.value, + provider="openai", + model="gpt-4o", + ) + + # Assert + manager.update_default_model_record.assert_called_once_with( + tenant_id="tenant-1", + model_type=ModelType.LLM, + provider="openai", + model="gpt-4o", + ) + + +def test_get_model_provider_icon_should_fetch_icon_bytes_from_factory(monkeypatch: pytest.MonkeyPatch) -> None: + # Arrange + service = ModelProviderService() + factory_instance = MagicMock() + factory_instance.get_provider_icon.return_value = (b"icon-bytes", "image/png") + factory_constructor = MagicMock(return_value=factory_instance) + monkeypatch.setattr(service_module, "create_plugin_model_provider_factory", factory_constructor) + + # Act + result = service.get_model_provider_icon( + tenant_id="tenant-1", + provider="openai", + icon_type="icon_small", + lang="en_US", + ) + + # Assert + factory_constructor.assert_called_once_with(tenant_id="tenant-1") + factory_instance.get_provider_icon.assert_called_once_with("openai", "icon_small", "en_US") + assert result == (b"icon-bytes", "image/png") + + +def test_switch_preferred_provider_should_convert_enum_and_delegate(monkeypatch: pytest.MonkeyPatch) -> None: + # Arrange + service = ModelProviderService() + provider_configuration = MagicMock() + monkeypatch.setattr(service, "_get_provider_configuration", MagicMock(return_value=provider_configuration)) + + # Act + service.switch_preferred_provider( + tenant_id="tenant-1", + provider="openai", + preferred_provider_type=ProviderType.SYSTEM.value, + ) + + # Assert + provider_configuration.switch_preferred_provider_type.assert_called_once_with(ProviderType.SYSTEM) + + +@pytest.mark.parametrize( + ("method_name", "provider_method_name"), + [ + ("enable_model", "enable_model"), + ("disable_model", "disable_model"), + ], +) +def test_model_enablement_methods_should_convert_model_type_and_delegate( + method_name: str, + provider_method_name: str, + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + service = ModelProviderService() + provider_configuration = MagicMock() + monkeypatch.setattr(service, "_get_provider_configuration", MagicMock(return_value=provider_configuration)) + + # Act + getattr(service, method_name)( + tenant_id="tenant-1", + provider="openai", + model="gpt-4o", + model_type=ModelType.LLM.value, + ) + + # Assert + getattr(provider_configuration, provider_method_name).assert_called_once_with( + model="gpt-4o", + model_type=ModelType.LLM, + ) diff --git a/api/tests/unit_tests/services/test_recommended_app_service.py b/api/tests/unit_tests/services/test_recommended_app_service.py index 12f4c0b982..12bc84db87 100644 --- a/api/tests/unit_tests/services/test_recommended_app_service.py +++ b/api/tests/unit_tests/services/test_recommended_app_service.py @@ -316,7 +316,7 @@ class TestRecommendedAppServiceGetDetail: mock_factory_class.get_recommend_app_factory.return_value = mock_factory # Act - result = RecommendedAppService.get_recommend_app_detail(app_id) + result = _recommendation_detail(RecommendedAppService.get_recommend_app_detail(app_id)) # Assert assert result == expected_detail @@ -346,7 +346,7 @@ class TestRecommendedAppServiceGetDetail: mock_factory_class.get_recommend_app_factory.return_value = mock_factory # Act - result = RecommendedAppService.get_recommend_app_detail(app_id) + result = _recommendation_detail(RecommendedAppService.get_recommend_app_detail(app_id)) # Assert assert result["name"] == f"App from {mode}" @@ -369,7 +369,7 @@ class TestRecommendedAppServiceGetDetail: mock_factory_class.get_recommend_app_factory.return_value = mock_factory # Act - result = RecommendedAppService.get_recommend_app_detail(app_id) + result = _recommendation_detail(RecommendedAppService.get_recommend_app_detail(app_id)) # Assert assert result is None @@ -392,7 +392,7 @@ class TestRecommendedAppServiceGetDetail: mock_factory_class.get_recommend_app_factory.return_value = mock_factory # Act - result = RecommendedAppService.get_recommend_app_detail(app_id) + result = _recommendation_detail(RecommendedAppService.get_recommend_app_detail(app_id)) # Assert assert result == {} @@ -432,9 +432,197 @@ class TestRecommendedAppServiceGetDetail: mock_factory_class.get_recommend_app_factory.return_value = mock_factory # Act - result = RecommendedAppService.get_recommend_app_detail(app_id) + result = _recommendation_detail(RecommendedAppService.get_recommend_app_detail(app_id)) # Assert assert result["model_config"] == complex_model_config assert len(result["workflows"]) == 2 assert len(result["tools"]) == 3 + + +# === Merged from test_recommended_app_service_additional.py === + + +from types import SimpleNamespace +from typing import Any, cast +from unittest.mock import MagicMock + +import pytest + +from services import recommended_app_service as service_module +from services.recommended_app_service import RecommendedAppService + + +def _recommendation_detail(result: dict[str, Any] | None) -> dict[str, Any]: + return cast(dict[str, Any], result) + + +@pytest.fixture +def mocked_db_session(monkeypatch: pytest.MonkeyPatch) -> MagicMock: + # Arrange + session = MagicMock() + monkeypatch.setattr(service_module, "db", SimpleNamespace(session=session)) + + # Assert + return session + + +def _mock_factory_for_apps( + monkeypatch: pytest.MonkeyPatch, + *, + mode: str, + result: dict[str, Any], + fallback_result: dict[str, Any] | None = None, +) -> tuple[MagicMock, MagicMock]: + retrieval_instance = MagicMock() + retrieval_instance.get_recommended_apps_and_categories.return_value = result + retrieval_factory = MagicMock(return_value=retrieval_instance) + monkeypatch.setattr(service_module.dify_config, "HOSTED_FETCH_APP_TEMPLATES_MODE", mode, raising=False) + monkeypatch.setattr( + service_module.RecommendAppRetrievalFactory, + "get_recommend_app_factory", + MagicMock(return_value=retrieval_factory), + ) + + builtin_instance = MagicMock() + if fallback_result is not None: + builtin_instance.fetch_recommended_apps_from_builtin.return_value = fallback_result + monkeypatch.setattr( + service_module.RecommendAppRetrievalFactory, + "get_buildin_recommend_app_retrieval", + MagicMock(return_value=builtin_instance), + ) + return retrieval_instance, builtin_instance + + +def test_get_recommended_apps_and_categories_should_not_query_trial_table_when_trial_feature_disabled( + monkeypatch: pytest.MonkeyPatch, + mocked_db_session: MagicMock, +) -> None: + # Arrange + expected = {"recommended_apps": [{"app_id": "app-1"}], "categories": ["all"]} + retrieval_instance, builtin_instance = _mock_factory_for_apps( + monkeypatch, + mode="remote", + result=expected, + ) + monkeypatch.setattr( + service_module.FeatureService, + "get_system_features", + MagicMock(return_value=SimpleNamespace(enable_trial_app=False)), + ) + + # Act + result = RecommendedAppService.get_recommended_apps_and_categories("en-US") + + # Assert + assert result == expected + retrieval_instance.get_recommended_apps_and_categories.assert_called_once_with("en-US") + builtin_instance.fetch_recommended_apps_from_builtin.assert_not_called() + mocked_db_session.scalar.assert_not_called() + + +def test_get_recommended_apps_and_categories_should_fallback_and_enrich_can_trial_when_trial_feature_enabled( + monkeypatch: pytest.MonkeyPatch, + mocked_db_session: MagicMock, +) -> None: + # Arrange + remote_result = {"recommended_apps": [], "categories": []} + fallback_result = {"recommended_apps": [{"app_id": "app-1"}, {"app_id": "app-2"}], "categories": ["all"]} + _, builtin_instance = _mock_factory_for_apps( + monkeypatch, + mode="remote", + result=remote_result, + fallback_result=fallback_result, + ) + monkeypatch.setattr( + service_module.FeatureService, + "get_system_features", + MagicMock(return_value=SimpleNamespace(enable_trial_app=True)), + ) + mocked_db_session.scalar.side_effect = [SimpleNamespace(id="trial-app"), None] + + # Act + result = RecommendedAppService.get_recommended_apps_and_categories("ja-JP") + + # Assert + builtin_instance.fetch_recommended_apps_from_builtin.assert_called_once_with("en-US") + assert result["recommended_apps"][0]["can_trial"] is True + assert result["recommended_apps"][1]["can_trial"] is False + assert mocked_db_session.scalar.call_count == 2 + + +@pytest.mark.parametrize( + ("trial_query_result", "expected_can_trial"), + [ + (SimpleNamespace(id="trial"), True), + (None, False), + ], +) +def test_get_recommend_app_detail_should_set_can_trial_when_trial_feature_enabled( + monkeypatch: pytest.MonkeyPatch, + mocked_db_session: MagicMock, + trial_query_result: Any, + expected_can_trial: bool, +) -> None: + # Arrange + detail = {"id": "app-1", "name": "Test App"} + retrieval_instance = MagicMock() + retrieval_instance.get_recommend_app_detail.return_value = detail + retrieval_factory = MagicMock(return_value=retrieval_instance) + monkeypatch.setattr(service_module.dify_config, "HOSTED_FETCH_APP_TEMPLATES_MODE", "remote", raising=False) + monkeypatch.setattr( + service_module.RecommendAppRetrievalFactory, + "get_recommend_app_factory", + MagicMock(return_value=retrieval_factory), + ) + monkeypatch.setattr( + service_module.FeatureService, + "get_system_features", + MagicMock(return_value=SimpleNamespace(enable_trial_app=True)), + ) + mocked_db_session.scalar.return_value = trial_query_result + + # Act + result = cast(dict[str, Any], RecommendedAppService.get_recommend_app_detail("app-1")) + + # Assert + assert result["id"] == "app-1" + assert result["can_trial"] is expected_can_trial + mocked_db_session.scalar.assert_called_once() + + +def test_add_trial_app_record_should_increment_count_when_existing_record_found( + mocked_db_session: MagicMock, +) -> None: + # Arrange + existing_record = SimpleNamespace(count=3) + mocked_db_session.scalar.return_value = existing_record + + # Act + RecommendedAppService.add_trial_app_record("app-1", "account-1") + + # Assert + assert existing_record.count == 4 + mocked_db_session.scalar.assert_called_once() + mocked_db_session.commit.assert_called_once() + mocked_db_session.add.assert_not_called() + + +def test_add_trial_app_record_should_create_new_record_when_no_existing_record( + mocked_db_session: MagicMock, +) -> None: + # Arrange + mocked_db_session.scalar.return_value = None + + # Act + RecommendedAppService.add_trial_app_record("app-2", "account-2") + + # Assert + mocked_db_session.scalar.assert_called_once() + mocked_db_session.add.assert_called_once() + added = mocked_db_session.add.call_args.args[0] + assert added.app_id == "app-2" + assert added.account_id == "account-2" + assert added.count == 1 + mocked_db_session.commit.assert_called_once() diff --git a/api/tests/unit_tests/services/test_schedule_service.py b/api/tests/unit_tests/services/test_schedule_service.py index e28965ea2c..2a78876da6 100644 --- a/api/tests/unit_tests/services/test_schedule_service.py +++ b/api/tests/unit_tests/services/test_schedule_service.py @@ -1,12 +1,15 @@ import unittest from datetime import UTC, datetime +from types import SimpleNamespace +from typing import Any, cast from unittest.mock import MagicMock, Mock, patch import pytest from sqlalchemy.orm import Session +from core.trigger.constants import TRIGGER_SCHEDULE_NODE_TYPE from core.workflow.nodes.trigger_schedule.entities import ScheduleConfig, SchedulePlanUpdate, VisualConfig -from core.workflow.nodes.trigger_schedule.exc import ScheduleConfigError +from core.workflow.nodes.trigger_schedule.exc import ScheduleConfigError, ScheduleNotFoundError from events.event_handlers.sync_workflow_schedule_when_app_published import ( sync_schedule_from_workflow, ) @@ -14,6 +17,8 @@ from libs.schedule_utils import calculate_next_run_at, convert_12h_to_24h from models.account import Account, TenantAccountJoin from models.trigger import WorkflowSchedulePlan from models.workflow import Workflow +from services.errors.account import AccountNotFoundError +from services.trigger import schedule_service as service_module from services.trigger.schedule_service import ScheduleService @@ -775,5 +780,158 @@ class TestSyncScheduleFromWorkflow(unittest.TestCase): mock_session.commit.assert_called_once() +@pytest.fixture +def session_mock() -> MagicMock: + return MagicMock(spec=Session) + + +def _workflow(**kwargs: Any) -> Workflow: + return cast(Workflow, SimpleNamespace(**kwargs)) + + +def test_update_schedule_should_update_only_node_id_without_recomputing_time( + session_mock: MagicMock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + schedule = MagicMock(spec=WorkflowSchedulePlan) + schedule.cron_expression = "0 10 * * *" + schedule.timezone = "UTC" + session_mock.get.return_value = schedule + + next_run_mock = MagicMock(return_value=datetime(2026, 1, 1, 10, 0, tzinfo=UTC)) + monkeypatch.setattr(service_module, "calculate_next_run_at", next_run_mock) + + # Act + result = ScheduleService.update_schedule( + session=session_mock, + schedule_id="schedule-1", + updates=SchedulePlanUpdate(node_id="node-new"), + ) + + # Assert + assert result is schedule + assert schedule.node_id == "node-new" + next_run_mock.assert_not_called() + session_mock.flush.assert_called_once() + + +def test_get_tenant_owner_should_raise_when_account_record_missing(session_mock: MagicMock) -> None: + # Arrange + join = SimpleNamespace(account_id="account-404") + session_mock.execute.return_value.scalar_one_or_none.return_value = join + session_mock.get.return_value = None + + # Act / Assert + with pytest.raises(AccountNotFoundError, match="Account not found: account-404"): + ScheduleService.get_tenant_owner(session=session_mock, tenant_id="tenant-1") + + +def test_get_tenant_owner_should_raise_when_no_owner_or_admin_found(session_mock: MagicMock) -> None: + # Arrange + session_mock.execute.return_value.scalar_one_or_none.side_effect = [None, None] + + # Act / Assert + with pytest.raises(AccountNotFoundError, match="Account not found for tenant: tenant-1"): + ScheduleService.get_tenant_owner(session=session_mock, tenant_id="tenant-1") + + +def test_update_next_run_at_should_raise_when_schedule_not_found(session_mock: MagicMock) -> None: + # Arrange + session_mock.get.return_value = None + + # Act / Assert + with pytest.raises(ScheduleNotFoundError, match="Schedule not found: schedule-1"): + ScheduleService.update_next_run_at(session=session_mock, schedule_id="schedule-1") + + +def test_to_schedule_config_should_build_from_cron_mode() -> None: + # Arrange + node_config: dict[str, Any] = { + "id": "node-1", + "data": { + "mode": "cron", + "cron_expression": "0 12 * * *", + "timezone": "Asia/Kolkata", + }, + } + + # Act + result = ScheduleService.to_schedule_config(node_config=node_config) + + # Assert + assert result.node_id == "node-1" + assert result.cron_expression == "0 12 * * *" + assert result.timezone == "Asia/Kolkata" + + +def test_to_schedule_config_should_raise_for_cron_mode_without_expression() -> None: + # Arrange + node_config = {"id": "node-1", "data": {"mode": "cron", "cron_expression": ""}} + + # Act / Assert + with pytest.raises(ScheduleConfigError, match="Cron expression is required for cron mode"): + ScheduleService.to_schedule_config(node_config=node_config) + + +def test_to_schedule_config_should_build_from_visual_mode(monkeypatch: pytest.MonkeyPatch) -> None: + # Arrange + node_config = { + "id": "node-1", + "data": { + "mode": "visual", + "frequency": "daily", + "visual_config": {"time": "9:30 AM"}, + "timezone": "UTC", + }, + } + monkeypatch.setattr(ScheduleService, "visual_to_cron", MagicMock(return_value="30 9 * * *")) + + # Act + result = ScheduleService.to_schedule_config(node_config=node_config) + + # Assert + assert result.cron_expression == "30 9 * * *" + + +def test_to_schedule_config_should_raise_for_invalid_mode() -> None: + # Arrange + node_config = {"id": "node-1", "data": {"mode": "manual"}} + + # Act / Assert + with pytest.raises(ScheduleConfigError, match="Invalid schedule mode: manual"): + ScheduleService.to_schedule_config(node_config=node_config) + + +def test_extract_schedule_config_should_raise_when_graph_is_empty() -> None: + # Arrange + workflow = _workflow(graph_dict={}) + + # Act / Assert + with pytest.raises(ScheduleConfigError, match="Workflow graph is empty"): + ScheduleService.extract_schedule_config(workflow=workflow) + + +def test_extract_schedule_config_should_raise_when_mode_invalid() -> None: + # Arrange + workflow = _workflow( + graph_dict={ + "nodes": [ + { + "id": "schedule-1", + "data": { + "type": TRIGGER_SCHEDULE_NODE_TYPE, + "mode": "invalid", + }, + } + ] + } + ) + + # Act / Assert + with pytest.raises(ScheduleConfigError, match="Invalid schedule mode: invalid"): + ScheduleService.extract_schedule_config(workflow=workflow) + + if __name__ == "__main__": unittest.main() diff --git a/api/tests/unit_tests/services/test_variable_truncator.py b/api/tests/unit_tests/services/test_variable_truncator.py index 9c23135225..27602bb1cc 100644 --- a/api/tests/unit_tests/services/test_variable_truncator.py +++ b/api/tests/unit_tests/services/test_variable_truncator.py @@ -12,6 +12,7 @@ This test suite covers all functionality of the current VariableTruncator includ import functools import json import uuid +from collections.abc import Mapping from typing import Any from uuid import uuid4 @@ -199,14 +200,14 @@ class TestArrayTruncation: def test_small_array_no_truncation(self, small_truncator: VariableTruncator): """Test that small arrays are not truncated.""" - small_array = [1, 2] + small_array: list[object] = [1, 2] result = small_truncator._truncate_array(small_array, 1000) assert result.value == small_array assert result.truncated is False def test_array_element_limit_truncation(self, small_truncator: VariableTruncator): """Test that arrays over element limit are truncated.""" - large_array = [1, 2, 3, 4, 5, 6] # Exceeds limit of 3 + large_array: list[object] = [1, 2, 3, 4, 5, 6] # Exceeds limit of 3 result = small_truncator._truncate_array(large_array, 1000) assert result.truncated is True @@ -215,7 +216,7 @@ class TestArrayTruncation: def test_array_size_budget_truncation(self, small_truncator: VariableTruncator): """Test array truncation due to size budget constraints.""" # Create array with strings that will exceed size budget - large_strings = ["very long string " * 5, "another long string " * 5] + large_strings: list[object] = ["very long string " * 5, "another long string " * 5] result = small_truncator._truncate_array(large_strings, 50) assert result.truncated is True @@ -276,10 +277,10 @@ class TestObjectTruncation: # Values should be truncated if they exist for key, value in result.value.items(): - if isinstance(value, str): - original_value = obj_with_long_values[key] - # Value should be same or smaller - assert len(value) <= len(original_value) + assert isinstance(value, str) + original_value = obj_with_long_values[key] + # Value should be same or smaller + assert len(value) <= len(original_value) def test_object_key_dropping(self, small_truncator): """Test object truncation where keys are dropped due to size constraints.""" @@ -506,10 +507,9 @@ class TestEdgeCases: truncator = VariableTruncator(string_length_limit=10) # Unicode characters - unicode_text = "🌍🚀🌍🚀🌍🚀🌍🚀🌍🚀" # Each emoji counts as 1 character + unicode_text = "你好世界你好世界你好世界" # Multi-byte UTF-8 characters result = truncator.truncate(StringSegment(value=unicode_text)) - if len(unicode_text) > 10: - assert result.truncated is True + assert result.truncated is True # Special JSON characters special_chars = '{"key": "value with \\"quotes\\" and \\n newlines"}' @@ -631,13 +631,12 @@ class TestIntegrationScenarios: result = truncator.truncate(segment) assert isinstance(result, TruncationResult) - # Should handle all data types appropriately - if result.truncated: - # Verify the result is smaller or equal than original - original_size = truncator.calculate_json_size(mixed_data) - if isinstance(result.result, ObjectSegment): - result_size = truncator.calculate_json_size(result.result.value) - assert result_size <= original_size + assert result.truncated is True + assert isinstance(result.result, ObjectSegment) + # Verify the result is smaller or equal than original + original_size = truncator.calculate_json_size(mixed_data) + result_size = truncator.calculate_json_size(result.result.value) + assert result_size <= original_size def test_file_and_array_file_variable_mapping(self, file): truncator = VariableTruncator(string_length_limit=30, array_element_limit=3, max_size_bytes=300) @@ -675,3 +674,229 @@ def test_dummy_variable_truncator_methods(): assert isinstance(result, TruncationResult) assert result.result == segment assert result.truncated is False + + +# === Merged from test_variable_truncator_additional.py === + + +from typing import Any + +import pytest +from graphon.nodes.variable_assigner.common.helpers import UpdatedVariable +from graphon.variables.segments import IntegerSegment, ObjectSegment, StringSegment +from graphon.variables.types import SegmentType + +from services import variable_truncator as truncator_module +from services.variable_truncator import BaseTruncator, TruncationResult, VariableTruncator + + +class _AbstractPassthrough(BaseTruncator): + def truncate(self, segment: Any) -> TruncationResult: + # Arrange / Act + return super().truncate(segment) # type: ignore[misc] + + def truncate_variable_mapping(self, v: Mapping[str, Any]) -> tuple[Mapping[str, Any], bool]: + # Arrange / Act + return super().truncate_variable_mapping(v) # type: ignore[misc] + + +def test_base_truncator_methods_should_execute_abstract_placeholders() -> None: + # Arrange + passthrough = _AbstractPassthrough() + + # Act + truncate_result = passthrough.truncate(StringSegment(value="x")) + mapping_result = passthrough.truncate_variable_mapping({"a": 1}) + + # Assert + assert truncate_result is None + assert mapping_result is None + + +def test_default_should_use_dify_config_limits(monkeypatch: pytest.MonkeyPatch) -> None: + # Arrange + monkeypatch.setattr(truncator_module.dify_config, "WORKFLOW_VARIABLE_TRUNCATION_MAX_SIZE", 111) + monkeypatch.setattr(truncator_module.dify_config, "WORKFLOW_VARIABLE_TRUNCATION_ARRAY_LENGTH", 7) + monkeypatch.setattr(truncator_module.dify_config, "WORKFLOW_VARIABLE_TRUNCATION_STRING_LENGTH", 33) + + # Act + truncator = VariableTruncator.default() + + # Assert + assert truncator._max_size_bytes == 111 + assert truncator._array_element_limit == 7 + assert truncator._string_length_limit == 33 + + +def test_truncate_variable_mapping_should_mark_over_budget_keys_with_ellipsis() -> None: + # Arrange + truncator = VariableTruncator(max_size_bytes=5) + mapping = {"very_long_key": "value"} + + # Act + result, truncated = truncator.truncate_variable_mapping(mapping) + + # Assert + assert result == {"very_long_key": "..."} + assert truncated is True + + +def test_truncate_variable_mapping_should_handle_segment_values() -> None: + # Arrange + truncator = VariableTruncator(max_size_bytes=100) + mapping = {"seg": StringSegment(value="hello")} + + # Act + result, truncated = truncator.truncate_variable_mapping(mapping) + + # Assert + assert isinstance(result["seg"], StringSegment) + assert result["seg"].value == "hello" + assert truncated is False + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (None, False), + (True, False), + (1, False), + (1.5, False), + ("x", True), + ({"k": "v"}, True), + ], +) +def test_json_value_needs_truncation_should_match_expected_rules(value: Any, expected: bool) -> None: + # Arrange + + # Act + result = VariableTruncator._json_value_needs_truncation(value) + + # Assert + assert result is expected + + +def test_truncate_should_use_string_fallback_when_truncated_value_size_exceeds_limit( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + truncator = VariableTruncator(max_size_bytes=10) + forced_result = truncator_module._PartResult( + value=StringSegment(value="this is too long"), + value_size=100, + truncated=True, + ) + monkeypatch.setattr(truncator, "_truncate_segment", lambda *_args, **_kwargs: forced_result) + + # Act + result = truncator.truncate(StringSegment(value="input")) + + # Assert + assert result.truncated is True + assert isinstance(result.result, StringSegment) + assert not result.result.value.startswith('"') + + +def test_truncate_segment_should_raise_assertion_for_unexpected_truncatable_segment( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + truncator = VariableTruncator() + monkeypatch.setattr(VariableTruncator, "_segment_need_truncation", lambda _segment: True) + + # Act / Assert + with pytest.raises(AssertionError): + truncator._truncate_segment(IntegerSegment(value=1), 10) + + +def test_calculate_json_size_should_unwrap_segment_values() -> None: + # Arrange + segment = StringSegment(value="abc") + + # Act + size = VariableTruncator.calculate_json_size(segment) + + # Assert + assert size == VariableTruncator.calculate_json_size("abc") + + +def test_calculate_json_size_should_handle_updated_variable_instances() -> None: + # Arrange + updated = UpdatedVariable(name="n", selector=["node", "var"], value_type=SegmentType.STRING, new_value="v") + + # Act + size = VariableTruncator.calculate_json_size(updated) + + # Assert + assert size > 0 + + +def test_maybe_qa_structure_should_validate_shape() -> None: + # Arrange + + # Act / Assert + assert VariableTruncator._maybe_qa_structure({"qa_chunks": []}) is True + assert VariableTruncator._maybe_qa_structure({"qa_chunks": "not-list"}) is False + assert VariableTruncator._maybe_qa_structure({}) is False + + +def test_maybe_parent_child_structure_should_validate_shape() -> None: + # Arrange + + # Act / Assert + assert VariableTruncator._maybe_parent_child_structure({"parent_mode": "full", "parent_child_chunks": []}) is True + assert VariableTruncator._maybe_parent_child_structure({"parent_mode": 1, "parent_child_chunks": []}) is False + assert ( + VariableTruncator._maybe_parent_child_structure({"parent_mode": "full", "parent_child_chunks": "bad"}) is False + ) + + +def test_truncate_object_should_truncate_segment_values_inside_object() -> None: + # Arrange + truncator = VariableTruncator(string_length_limit=8, max_size_bytes=30) + mapping = {"s": StringSegment(value="long-content")} + + # Act + result = truncator._truncate_object(mapping, 20) + + # Assert + assert result.truncated is True + assert isinstance(result.value["s"], StringSegment) + + +def test_truncate_json_primitives_should_handle_updated_variable_input() -> None: + # Arrange + truncator = VariableTruncator(max_size_bytes=100) + updated = UpdatedVariable(name="n", selector=["node", "var"], value_type=SegmentType.STRING, new_value="v") + + # Act + result = truncator._truncate_json_primitives(updated, 100) + + # Assert + assert isinstance(result.value, dict) + + +def test_truncate_json_primitives_should_raise_assertion_for_unsupported_value_type() -> None: + # Arrange + truncator = VariableTruncator() + + # Act / Assert + with pytest.raises(AssertionError): + truncator._truncate_json_primitives(object(), 100) # type: ignore[arg-type] + + +def test_truncate_should_apply_json_string_fallback_for_large_non_string_segment( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + truncator = VariableTruncator(max_size_bytes=10) + forced_segment = ObjectSegment(value={"k": "v"}) + forced_result = truncator_module._PartResult(value=forced_segment, value_size=100, truncated=True) + monkeypatch.setattr(truncator, "_truncate_segment", lambda *_args, **_kwargs: forced_result) + + # Act + result = truncator.truncate(ObjectSegment(value={"a": "b"})) + + # Assert + assert result.truncated is True + assert isinstance(result.result, StringSegment) diff --git a/api/tests/unit_tests/services/test_webhook_service.py b/api/tests/unit_tests/services/test_webhook_service.py index ffdcc046f9..78049182ad 100644 --- a/api/tests/unit_tests/services/test_webhook_service.py +++ b/api/tests/unit_tests/services/test_webhook_service.py @@ -559,3 +559,757 @@ class TestWebhookServiceUnit: result = _prepare_webhook_execution("test_webhook", is_debug=True) assert result == (mock_trigger, mock_workflow, mock_config, mock_data, None) + + +# === Merged from test_webhook_service_additional.py === + + +from types import SimpleNamespace +from typing import Any, cast +from unittest.mock import MagicMock + +import pytest +from flask import Flask +from graphon.variables.types import SegmentType +from werkzeug.datastructures import FileStorage +from werkzeug.exceptions import RequestEntityTooLarge + +from core.workflow.nodes.trigger_webhook.entities import ( + ContentType, + WebhookBodyParameter, + WebhookData, + WebhookParameter, +) +from models.enums import AppTriggerStatus +from models.model import App +from models.trigger import WorkflowWebhookTrigger +from models.workflow import Workflow +from services.errors.app import QuotaExceededError +from services.trigger import webhook_service as service_module +from services.trigger.webhook_service import WebhookService + + +class _FakeQuery: + def __init__(self, result: Any) -> None: + self._result = result + + def where(self, *args: Any, **kwargs: Any) -> "_FakeQuery": + return self + + def filter(self, *args: Any, **kwargs: Any) -> "_FakeQuery": + return self + + def order_by(self, *args: Any, **kwargs: Any) -> "_FakeQuery": + return self + + def first(self) -> Any: + return self._result + + +class _SessionContext: + def __init__(self, session: Any) -> None: + self._session = session + + def __enter__(self) -> Any: + return self._session + + def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> bool: + return False + + +@pytest.fixture +def flask_app() -> Flask: + return Flask(__name__) + + +def _patch_session(monkeypatch: pytest.MonkeyPatch, session: Any) -> None: + monkeypatch.setattr(service_module, "db", SimpleNamespace(engine=MagicMock(), session=MagicMock())) + monkeypatch.setattr(service_module, "Session", lambda *args, **kwargs: _SessionContext(session)) + + +def _workflow_trigger(**kwargs: Any) -> WorkflowWebhookTrigger: + return cast(WorkflowWebhookTrigger, SimpleNamespace(**kwargs)) + + +def _workflow(**kwargs: Any) -> Workflow: + return cast(Workflow, SimpleNamespace(**kwargs)) + + +def _app(**kwargs: Any) -> App: + return cast(App, SimpleNamespace(**kwargs)) + + +def test_get_webhook_trigger_and_workflow_should_raise_when_webhook_not_found(monkeypatch: pytest.MonkeyPatch) -> None: + # Arrange + fake_session = MagicMock() + fake_session.query.return_value = _FakeQuery(None) + _patch_session(monkeypatch, fake_session) + + # Act / Assert + with pytest.raises(ValueError, match="Webhook not found"): + WebhookService.get_webhook_trigger_and_workflow("webhook-1") + + +def test_get_webhook_trigger_and_workflow_should_raise_when_app_trigger_not_found( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + webhook_trigger = SimpleNamespace(app_id="app-1", node_id="node-1") + fake_session = MagicMock() + fake_session.query.side_effect = [_FakeQuery(webhook_trigger), _FakeQuery(None)] + _patch_session(monkeypatch, fake_session) + + # Act / Assert + with pytest.raises(ValueError, match="App trigger not found"): + WebhookService.get_webhook_trigger_and_workflow("webhook-1") + + +def test_get_webhook_trigger_and_workflow_should_raise_when_app_trigger_rate_limited( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + webhook_trigger = SimpleNamespace(app_id="app-1", node_id="node-1") + app_trigger = SimpleNamespace(status=AppTriggerStatus.RATE_LIMITED) + fake_session = MagicMock() + fake_session.query.side_effect = [_FakeQuery(webhook_trigger), _FakeQuery(app_trigger)] + _patch_session(monkeypatch, fake_session) + + # Act / Assert + with pytest.raises(ValueError, match="rate limited"): + WebhookService.get_webhook_trigger_and_workflow("webhook-1") + + +def test_get_webhook_trigger_and_workflow_should_raise_when_app_trigger_disabled( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + webhook_trigger = SimpleNamespace(app_id="app-1", node_id="node-1") + app_trigger = SimpleNamespace(status=AppTriggerStatus.DISABLED) + fake_session = MagicMock() + fake_session.query.side_effect = [_FakeQuery(webhook_trigger), _FakeQuery(app_trigger)] + _patch_session(monkeypatch, fake_session) + + # Act / Assert + with pytest.raises(ValueError, match="disabled"): + WebhookService.get_webhook_trigger_and_workflow("webhook-1") + + +def test_get_webhook_trigger_and_workflow_should_raise_when_workflow_not_found(monkeypatch: pytest.MonkeyPatch) -> None: + # Arrange + webhook_trigger = SimpleNamespace(app_id="app-1", node_id="node-1") + app_trigger = SimpleNamespace(status=AppTriggerStatus.ENABLED) + fake_session = MagicMock() + fake_session.query.side_effect = [_FakeQuery(webhook_trigger), _FakeQuery(app_trigger), _FakeQuery(None)] + _patch_session(monkeypatch, fake_session) + + # Act / Assert + with pytest.raises(ValueError, match="Workflow not found"): + WebhookService.get_webhook_trigger_and_workflow("webhook-1") + + +def test_get_webhook_trigger_and_workflow_should_return_values_for_non_debug_mode( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + webhook_trigger = SimpleNamespace(app_id="app-1", node_id="node-1") + app_trigger = SimpleNamespace(status=AppTriggerStatus.ENABLED) + workflow = MagicMock() + workflow.get_node_config_by_id.return_value = {"data": {"key": "value"}} + + fake_session = MagicMock() + fake_session.query.side_effect = [_FakeQuery(webhook_trigger), _FakeQuery(app_trigger), _FakeQuery(workflow)] + _patch_session(monkeypatch, fake_session) + + # Act + got_trigger, got_workflow, got_node_config = WebhookService.get_webhook_trigger_and_workflow("webhook-1") + + # Assert + assert got_trigger is webhook_trigger + assert got_workflow is workflow + assert got_node_config == {"data": {"key": "value"}} + + +def test_get_webhook_trigger_and_workflow_should_return_values_for_debug_mode(monkeypatch: pytest.MonkeyPatch) -> None: + # Arrange + webhook_trigger = SimpleNamespace(app_id="app-1", node_id="node-1") + workflow = MagicMock() + workflow.get_node_config_by_id.return_value = {"data": {"mode": "debug"}} + + fake_session = MagicMock() + fake_session.query.side_effect = [_FakeQuery(webhook_trigger), _FakeQuery(workflow)] + _patch_session(monkeypatch, fake_session) + + # Act + got_trigger, got_workflow, got_node_config = WebhookService.get_webhook_trigger_and_workflow( + "webhook-1", is_debug=True + ) + + # Assert + assert got_trigger is webhook_trigger + assert got_workflow is workflow + assert got_node_config == {"data": {"mode": "debug"}} + + +def test_extract_webhook_data_should_use_text_fallback_for_unknown_content_type( + flask_app: Flask, + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + warning_mock = MagicMock() + monkeypatch.setattr(service_module.logger, "warning", warning_mock) + webhook_trigger = MagicMock() + + # Act + with flask_app.test_request_context( + "/webhook", + method="POST", + headers={"Content-Type": "application/vnd.custom"}, + data="plain content", + ): + result = WebhookService.extract_webhook_data(webhook_trigger) + + # Assert + assert result["body"] == {"raw": "plain content"} + warning_mock.assert_called_once() + + +def test_extract_webhook_data_should_raise_for_request_too_large( + flask_app: Flask, + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + monkeypatch.setattr(service_module.dify_config, "WEBHOOK_REQUEST_BODY_MAX_SIZE", 1) + + # Act / Assert + with flask_app.test_request_context("/webhook", method="POST", data="ab"): + with pytest.raises(RequestEntityTooLarge): + WebhookService.extract_webhook_data(MagicMock()) + + +def test_extract_octet_stream_body_should_return_none_when_empty_payload(flask_app: Flask) -> None: + # Arrange + webhook_trigger = MagicMock() + + # Act + with flask_app.test_request_context("/webhook", method="POST", data=b""): + body, files = WebhookService._extract_octet_stream_body(webhook_trigger) + + # Assert + assert body == {"raw": None} + assert files == {} + + +def test_extract_octet_stream_body_should_return_none_when_processing_raises( + flask_app: Flask, + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + webhook_trigger = MagicMock() + monkeypatch.setattr(WebhookService, "_detect_binary_mimetype", MagicMock(return_value="application/octet-stream")) + monkeypatch.setattr(WebhookService, "_create_file_from_binary", MagicMock(side_effect=RuntimeError("boom"))) + + # Act + with flask_app.test_request_context("/webhook", method="POST", data=b"abc"): + body, files = WebhookService._extract_octet_stream_body(webhook_trigger) + + # Assert + assert body == {"raw": None} + assert files == {} + + +def test_extract_text_body_should_return_empty_string_when_request_read_fails( + flask_app: Flask, + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + monkeypatch.setattr("flask.wrappers.Request.get_data", MagicMock(side_effect=RuntimeError("read error"))) + + # Act + with flask_app.test_request_context("/webhook", method="POST", data="abc"): + body, files = WebhookService._extract_text_body() + + # Assert + assert body == {"raw": ""} + assert files == {} + + +def test_detect_binary_mimetype_should_fallback_when_magic_raises(monkeypatch: pytest.MonkeyPatch) -> None: + # Arrange + fake_magic = MagicMock() + fake_magic.from_buffer.side_effect = RuntimeError("magic failed") + monkeypatch.setattr(service_module, "magic", fake_magic) + + # Act + result = WebhookService._detect_binary_mimetype(b"binary") + + # Assert + assert result == "application/octet-stream" + + +def test_process_file_uploads_should_use_octet_stream_fallback_when_mimetype_unknown( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + webhook_trigger = _workflow_trigger(created_by="user-1", tenant_id="tenant-1") + file_obj = MagicMock() + file_obj.to_dict.return_value = {"id": "f-1"} + monkeypatch.setattr(WebhookService, "_create_file_from_binary", MagicMock(return_value=file_obj)) + monkeypatch.setattr(service_module.mimetypes, "guess_type", MagicMock(return_value=(None, None))) + + uploaded = MagicMock() + uploaded.filename = "file.unknown" + uploaded.content_type = None + uploaded.read.return_value = b"content" + + # Act + result = WebhookService._process_file_uploads({"f": uploaded}, webhook_trigger) + + # Assert + assert result == {"f": {"id": "f-1"}} + + +def test_create_file_from_binary_should_call_tool_file_manager_and_file_factory( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + webhook_trigger = _workflow_trigger(created_by="user-1", tenant_id="tenant-1") + manager = MagicMock() + manager.create_file_by_raw.return_value = SimpleNamespace(id="tool-file-1") + monkeypatch.setattr(service_module, "ToolFileManager", MagicMock(return_value=manager)) + expected_file = MagicMock() + monkeypatch.setattr(service_module.file_factory, "build_from_mapping", MagicMock(return_value=expected_file)) + + # Act + result = WebhookService._create_file_from_binary(b"abc", "text/plain", webhook_trigger) + + # Assert + assert result is expected_file + manager.create_file_by_raw.assert_called_once() + + +@pytest.mark.parametrize( + ("raw_value", "param_type", "expected"), + [ + ("42", SegmentType.NUMBER, 42), + ("3.14", SegmentType.NUMBER, 3.14), + ("yes", SegmentType.BOOLEAN, True), + ("no", SegmentType.BOOLEAN, False), + ], +) +def test_convert_form_value_should_convert_supported_types( + raw_value: str, + param_type: str, + expected: Any, +) -> None: + # Arrange + + # Act + result = WebhookService._convert_form_value("param", raw_value, param_type) + + # Assert + assert result == expected + + +def test_convert_form_value_should_raise_for_unsupported_type() -> None: + # Arrange + + # Act / Assert + with pytest.raises(ValueError, match="Unsupported type"): + WebhookService._convert_form_value("p", "x", SegmentType.FILE) + + +def test_validate_json_value_should_return_original_for_unmapped_supported_segment_type( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + warning_mock = MagicMock() + monkeypatch.setattr(service_module.logger, "warning", warning_mock) + + # Act + result = WebhookService._validate_json_value("param", {"x": 1}, "unsupported-type") + + # Assert + assert result == {"x": 1} + warning_mock.assert_called_once() + + +def test_validate_and_convert_value_should_wrap_conversion_errors() -> None: + # Arrange + + # Act / Assert + with pytest.raises(ValueError, match="validation failed"): + WebhookService._validate_and_convert_value("param", "bad", SegmentType.NUMBER, is_form_data=True) + + +def test_process_parameters_should_raise_when_required_parameter_missing() -> None: + # Arrange + raw_params = {"optional": "x"} + config = [WebhookParameter(name="required_param", type=SegmentType.STRING, required=True)] + + # Act / Assert + with pytest.raises(ValueError, match="Required parameter missing"): + WebhookService._process_parameters(raw_params, config, is_form_data=True) + + +def test_process_parameters_should_include_unconfigured_parameters() -> None: + # Arrange + raw_params = {"known": "1", "unknown": "x"} + config = [WebhookParameter(name="known", type=SegmentType.NUMBER, required=False)] + + # Act + result = WebhookService._process_parameters(raw_params, config, is_form_data=True) + + # Assert + assert result == {"known": 1, "unknown": "x"} + + +def test_process_body_parameters_should_raise_when_required_text_raw_is_missing() -> None: + # Arrange + + # Act / Assert + with pytest.raises(ValueError, match="Required body content missing"): + WebhookService._process_body_parameters( + raw_body={"raw": ""}, + body_configs=[WebhookBodyParameter(name="raw", required=True)], + content_type=ContentType.TEXT, + ) + + +def test_process_body_parameters_should_skip_file_config_for_multipart_form_data() -> None: + # Arrange + raw_body = {"message": "hello", "extra": "x"} + body_configs = [ + WebhookBodyParameter(name="upload", type=SegmentType.FILE, required=True), + WebhookBodyParameter(name="message", type=SegmentType.STRING, required=True), + ] + + # Act + result = WebhookService._process_body_parameters(raw_body, body_configs, ContentType.FORM_DATA) + + # Assert + assert result == {"message": "hello", "extra": "x"} + + +def test_validate_required_headers_should_accept_sanitized_header_names() -> None: + # Arrange + headers = {"x_api_key": "123"} + configs = [WebhookParameter(name="x-api-key", required=True)] + + # Act + WebhookService._validate_required_headers(headers, configs) + + # Assert + assert True + + +def test_validate_required_headers_should_raise_when_required_header_missing() -> None: + # Arrange + headers = {"x-other": "123"} + configs = [WebhookParameter(name="x-api-key", required=True)] + + # Act / Assert + with pytest.raises(ValueError, match="Required header missing"): + WebhookService._validate_required_headers(headers, configs) + + +def test_validate_http_metadata_should_return_content_type_mismatch_error() -> None: + # Arrange + webhook_data = {"method": "POST", "headers": {"Content-Type": "application/json"}} + node_data = WebhookData(method="post", content_type=ContentType.TEXT) + + # Act + result = WebhookService._validate_http_metadata(webhook_data, node_data) + + # Assert + assert result["valid"] is False + assert "Content-type mismatch" in result["error"] + + +def test_extract_content_type_should_fallback_to_lowercase_header_key() -> None: + # Arrange + headers = {"content-type": "application/json; charset=utf-8"} + + # Act + result = WebhookService._extract_content_type(headers) + + # Assert + assert result == "application/json" + + +def test_build_workflow_inputs_should_include_expected_keys() -> None: + # Arrange + webhook_data = {"headers": {"h": "v"}, "query_params": {"q": 1}, "body": {"b": 2}} + + # Act + result = WebhookService.build_workflow_inputs(webhook_data) + + # Assert + assert result["webhook_data"] == webhook_data + assert result["webhook_headers"] == {"h": "v"} + assert result["webhook_query_params"] == {"q": 1} + assert result["webhook_body"] == {"b": 2} + + +def test_trigger_workflow_execution_should_trigger_async_workflow_successfully(monkeypatch: pytest.MonkeyPatch) -> None: + # Arrange + webhook_trigger = _workflow_trigger( + app_id="app-1", + node_id="node-1", + tenant_id="tenant-1", + webhook_id="webhook-1", + ) + workflow = _workflow(id="wf-1") + webhook_data = {"body": {"x": 1}} + + session = MagicMock() + _patch_session(monkeypatch, session) + + end_user = SimpleNamespace(id="end-user-1") + monkeypatch.setattr( + service_module.EndUserService, "get_or_create_end_user_by_type", MagicMock(return_value=end_user) + ) + quota_type = SimpleNamespace(TRIGGER=SimpleNamespace(consume=MagicMock())) + monkeypatch.setattr(service_module, "QuotaType", quota_type) + trigger_async_mock = MagicMock() + monkeypatch.setattr(service_module.AsyncWorkflowService, "trigger_workflow_async", trigger_async_mock) + + # Act + WebhookService.trigger_workflow_execution(webhook_trigger, webhook_data, workflow) + + # Assert + trigger_async_mock.assert_called_once() + + +def test_trigger_workflow_execution_should_mark_tenant_rate_limited_when_quota_exceeded( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + webhook_trigger = _workflow_trigger( + app_id="app-1", + node_id="node-1", + tenant_id="tenant-1", + webhook_id="webhook-1", + ) + workflow = _workflow(id="wf-1") + + session = MagicMock() + _patch_session(monkeypatch, session) + + monkeypatch.setattr( + service_module.EndUserService, + "get_or_create_end_user_by_type", + MagicMock(return_value=SimpleNamespace(id="end-user-1")), + ) + quota_type = SimpleNamespace( + TRIGGER=SimpleNamespace( + consume=MagicMock(side_effect=QuotaExceededError(feature="trigger", tenant_id="tenant-1", required=1)) + ) + ) + monkeypatch.setattr(service_module, "QuotaType", quota_type) + mark_rate_limited_mock = MagicMock() + monkeypatch.setattr(service_module.AppTriggerService, "mark_tenant_triggers_rate_limited", mark_rate_limited_mock) + + # Act / Assert + with pytest.raises(QuotaExceededError): + WebhookService.trigger_workflow_execution(webhook_trigger, {"body": {}}, workflow) + mark_rate_limited_mock.assert_called_once_with("tenant-1") + + +def test_trigger_workflow_execution_should_log_and_reraise_unexpected_errors(monkeypatch: pytest.MonkeyPatch) -> None: + # Arrange + webhook_trigger = _workflow_trigger( + app_id="app-1", + node_id="node-1", + tenant_id="tenant-1", + webhook_id="webhook-1", + ) + workflow = _workflow(id="wf-1") + + session = MagicMock() + _patch_session(monkeypatch, session) + + monkeypatch.setattr( + service_module.EndUserService, "get_or_create_end_user_by_type", MagicMock(side_effect=RuntimeError("boom")) + ) + logger_exception_mock = MagicMock() + monkeypatch.setattr(service_module.logger, "exception", logger_exception_mock) + + # Act / Assert + with pytest.raises(RuntimeError, match="boom"): + WebhookService.trigger_workflow_execution(webhook_trigger, {"body": {}}, workflow) + logger_exception_mock.assert_called_once() + + +def test_sync_webhook_relationships_should_raise_when_workflow_exceeds_node_limit() -> None: + # Arrange + app = _app(id="app-1", tenant_id="tenant-1", created_by="user-1") + workflow = _workflow( + walk_nodes=lambda _node_type: [ + (f"node-{i}", {}) for i in range(WebhookService.MAX_WEBHOOK_NODES_PER_WORKFLOW + 1) + ] + ) + + # Act / Assert + with pytest.raises(ValueError, match="maximum webhook node limit"): + WebhookService.sync_webhook_relationships(app, workflow) + + +def test_sync_webhook_relationships_should_raise_when_lock_not_acquired(monkeypatch: pytest.MonkeyPatch) -> None: + # Arrange + app = _app(id="app-1", tenant_id="tenant-1", created_by="user-1") + workflow = _workflow(walk_nodes=lambda _node_type: [("node-1", {})]) + + lock = MagicMock() + lock.acquire.return_value = False + monkeypatch.setattr(service_module.redis_client, "get", MagicMock(return_value=None)) + monkeypatch.setattr(service_module.redis_client, "lock", MagicMock(return_value=lock)) + + # Act / Assert + with pytest.raises(RuntimeError, match="Failed to acquire lock"): + WebhookService.sync_webhook_relationships(app, workflow) + + +def test_sync_webhook_relationships_should_create_missing_records_and_delete_stale_records( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + app = _app(id="app-1", tenant_id="tenant-1", created_by="user-1") + workflow = _workflow(walk_nodes=lambda _node_type: [("node-new", {})]) + + class _WorkflowWebhookTrigger: + app_id = "app_id" + tenant_id = "tenant_id" + webhook_id = "webhook_id" + node_id = "node_id" + + def __init__(self, app_id: str, tenant_id: str, node_id: str, webhook_id: str, created_by: str) -> None: + self.id = None + self.app_id = app_id + self.tenant_id = tenant_id + self.node_id = node_id + self.webhook_id = webhook_id + self.created_by = created_by + + class _Select: + def where(self, *args: Any, **kwargs: Any) -> "_Select": + return self + + class _Session: + def __init__(self) -> None: + self.added: list[Any] = [] + self.deleted: list[Any] = [] + self.commit_count = 0 + self.existing_records = [SimpleNamespace(node_id="node-stale")] + + def scalars(self, _stmt: Any) -> Any: + return SimpleNamespace(all=lambda: self.existing_records) + + def add(self, obj: Any) -> None: + self.added.append(obj) + + def flush(self) -> None: + for idx, obj in enumerate(self.added, start=1): + if obj.id is None: + obj.id = f"rec-{idx}" + + def commit(self) -> None: + self.commit_count += 1 + + def delete(self, obj: Any) -> None: + self.deleted.append(obj) + + lock = MagicMock() + lock.acquire.return_value = True + lock.release.return_value = None + + fake_session = _Session() + + monkeypatch.setattr(service_module, "WorkflowWebhookTrigger", _WorkflowWebhookTrigger) + monkeypatch.setattr(service_module, "select", MagicMock(return_value=_Select())) + monkeypatch.setattr(service_module.redis_client, "get", MagicMock(return_value=None)) + monkeypatch.setattr(service_module.redis_client, "lock", MagicMock(return_value=lock)) + redis_set_mock = MagicMock() + redis_delete_mock = MagicMock() + monkeypatch.setattr(service_module.redis_client, "set", redis_set_mock) + monkeypatch.setattr(service_module.redis_client, "delete", redis_delete_mock) + monkeypatch.setattr(WebhookService, "generate_webhook_id", MagicMock(return_value="generated-webhook-id")) + _patch_session(monkeypatch, fake_session) + + # Act + WebhookService.sync_webhook_relationships(app, workflow) + + # Assert + assert len(fake_session.added) == 1 + assert len(fake_session.deleted) == 1 + assert fake_session.commit_count == 2 + redis_set_mock.assert_called_once() + redis_delete_mock.assert_called_once() + lock.release.assert_called_once() + + +def test_sync_webhook_relationships_should_log_when_lock_release_fails(monkeypatch: pytest.MonkeyPatch) -> None: + # Arrange + app = _app(id="app-1", tenant_id="tenant-1", created_by="user-1") + workflow = _workflow(walk_nodes=lambda _node_type: []) + + class _Select: + def where(self, *args: Any, **kwargs: Any) -> "_Select": + return self + + class _Session: + def scalars(self, _stmt: Any) -> Any: + return SimpleNamespace(all=lambda: []) + + def commit(self) -> None: + return None + + lock = MagicMock() + lock.acquire.return_value = True + lock.release.side_effect = RuntimeError("release failed") + + logger_exception_mock = MagicMock() + + monkeypatch.setattr(service_module, "select", MagicMock(return_value=_Select())) + monkeypatch.setattr(service_module.redis_client, "get", MagicMock(return_value=None)) + monkeypatch.setattr(service_module.redis_client, "lock", MagicMock(return_value=lock)) + monkeypatch.setattr(service_module.logger, "exception", logger_exception_mock) + _patch_session(monkeypatch, _Session()) + + # Act + WebhookService.sync_webhook_relationships(app, workflow) + + # Assert + assert logger_exception_mock.call_count == 1 + + +def test_generate_webhook_response_should_fallback_when_response_body_is_not_json() -> None: + # Arrange + node_config = {"data": {"status_code": 200, "response_body": "{bad-json"}} + + # Act + body, status = WebhookService.generate_webhook_response(node_config) + + # Assert + assert status == 200 + assert "message" in body + + +def test_generate_webhook_id_should_return_24_character_identifier() -> None: + # Arrange + + # Act + webhook_id = WebhookService.generate_webhook_id() + + # Assert + assert isinstance(webhook_id, str) + assert len(webhook_id) == 24 + + +def test_sanitize_key_should_return_original_value_for_non_string_input() -> None: + # Arrange + + # Act + result = WebhookService._sanitize_key(123) # type: ignore[arg-type] + + # Assert + assert result == 123 diff --git a/api/tests/unit_tests/services/test_workflow_run_service_pause.py b/api/tests/unit_tests/services/test_workflow_run_service_pause.py index a62c9f4555..64b21317ab 100644 --- a/api/tests/unit_tests/services/test_workflow_run_service_pause.py +++ b/api/tests/unit_tests/services/test_workflow_run_service_pause.py @@ -176,3 +176,300 @@ class TestWorkflowRunService: service = WorkflowRunService(session_factory) assert service._session_factory == session_factory + + +# === Merged from test_workflow_run_service.py === + + +from types import SimpleNamespace +from typing import Any, cast +from unittest.mock import MagicMock + +import pytest + +from models import Account, App, EndUser, WorkflowRunTriggeredFrom +from services import workflow_run_service as service_module +from services.workflow_run_service import WorkflowRunService + + +@pytest.fixture +def repository_factory_mocks(monkeypatch: pytest.MonkeyPatch) -> tuple[MagicMock, MagicMock, Any]: + # Arrange + node_repo = MagicMock() + workflow_run_repo = MagicMock() + factory = SimpleNamespace( + create_api_workflow_node_execution_repository=MagicMock(return_value=node_repo), + create_api_workflow_run_repository=MagicMock(return_value=workflow_run_repo), + ) + monkeypatch.setattr(service_module, "DifyAPIRepositoryFactory", factory) + + # Assert + return node_repo, workflow_run_repo, factory + + +def _app_model(**kwargs: Any) -> App: + return cast(App, SimpleNamespace(**kwargs)) + + +def _account(**kwargs: Any) -> Account: + return cast(Account, SimpleNamespace(**kwargs)) + + +def _end_user(**kwargs: Any) -> EndUser: + return cast(EndUser, SimpleNamespace(**kwargs)) + + +def test___init___should_create_sessionmaker_from_db_engine_when_session_factory_missing( + monkeypatch: pytest.MonkeyPatch, + repository_factory_mocks: tuple[MagicMock, MagicMock, Any], +) -> None: + # Arrange + session_factory = MagicMock(name="session_factory") + sessionmaker_mock = MagicMock(return_value=session_factory) + monkeypatch.setattr(service_module, "sessionmaker", sessionmaker_mock) + monkeypatch.setattr(service_module, "db", SimpleNamespace(engine="db-engine")) + + # Act + service = WorkflowRunService() + + # Assert + sessionmaker_mock.assert_called_once_with(bind="db-engine", expire_on_commit=False) + assert service._session_factory is session_factory + + +def test___init___should_create_sessionmaker_when_engine_is_provided( + monkeypatch: pytest.MonkeyPatch, + repository_factory_mocks: tuple[MagicMock, MagicMock, Any], +) -> None: + # Arrange + class FakeEngine: + pass + + session_factory = MagicMock(name="session_factory") + sessionmaker_mock = MagicMock(return_value=session_factory) + monkeypatch.setattr(service_module, "Engine", FakeEngine) + monkeypatch.setattr(service_module, "sessionmaker", sessionmaker_mock) + engine = cast(Engine, FakeEngine()) + + # Act + service = WorkflowRunService(session_factory=engine) + + # Assert + sessionmaker_mock.assert_called_once_with(bind=engine, expire_on_commit=False) + assert service._session_factory is session_factory + + +def test___init___should_keep_provided_sessionmaker_and_create_repositories( + repository_factory_mocks: tuple[MagicMock, MagicMock, Any], +) -> None: + # Arrange + node_repo, workflow_run_repo, factory = repository_factory_mocks + session_factory = MagicMock(name="session_factory") + + # Act + service = WorkflowRunService(session_factory=session_factory) + + # Assert + assert service._session_factory is session_factory + assert service._node_execution_service_repo is node_repo + assert service._workflow_run_repo is workflow_run_repo + factory.create_api_workflow_node_execution_repository.assert_called_once_with(session_factory) + factory.create_api_workflow_run_repository.assert_called_once_with(session_factory) + + +def test_get_paginate_workflow_runs_should_forward_filters_and_parse_limit( + repository_factory_mocks: tuple[MagicMock, MagicMock, Any], +) -> None: + # Arrange + _, workflow_run_repo, _ = repository_factory_mocks + service = WorkflowRunService(session_factory=MagicMock(name="session_factory")) + app_model = _app_model(tenant_id="tenant-1", id="app-1") + expected = MagicMock(name="pagination") + workflow_run_repo.get_paginated_workflow_runs.return_value = expected + args = {"limit": "7", "last_id": "last-1", "status": "succeeded"} + + # Act + result = service.get_paginate_workflow_runs( + app_model=app_model, + args=args, + triggered_from=WorkflowRunTriggeredFrom.APP_RUN, + ) + + # Assert + assert result is expected + workflow_run_repo.get_paginated_workflow_runs.assert_called_once_with( + tenant_id="tenant-1", + app_id="app-1", + triggered_from=WorkflowRunTriggeredFrom.APP_RUN, + limit=7, + last_id="last-1", + status="succeeded", + ) + + +def test_get_paginate_advanced_chat_workflow_runs_should_attach_message_fields_when_message_exists( + repository_factory_mocks: tuple[MagicMock, MagicMock, Any], + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + service = WorkflowRunService(session_factory=MagicMock(name="session_factory")) + app_model = _app_model(tenant_id="tenant-1", id="app-1") + run_with_message = SimpleNamespace( + id="run-1", + status="running", + message=SimpleNamespace(id="msg-1", conversation_id="conv-1"), + ) + run_without_message = SimpleNamespace(id="run-2", status="succeeded", message=None) + pagination = SimpleNamespace(data=[run_with_message, run_without_message]) + monkeypatch.setattr(service, "get_paginate_workflow_runs", MagicMock(return_value=pagination)) + + # Act + result = service.get_paginate_advanced_chat_workflow_runs(app_model=app_model, args={"limit": "2"}) + + # Assert + assert result is pagination + assert len(result.data) == 2 + assert result.data[0].message_id == "msg-1" + assert result.data[0].conversation_id == "conv-1" + assert result.data[0].status == "running" + assert not hasattr(result.data[1], "message_id") + assert result.data[1].id == "run-2" + + +def test_get_workflow_run_should_delegate_to_repository_by_tenant_and_app( + repository_factory_mocks: tuple[MagicMock, MagicMock, Any], +) -> None: + # Arrange + _, workflow_run_repo, _ = repository_factory_mocks + service = WorkflowRunService(session_factory=MagicMock(name="session_factory")) + app_model = _app_model(tenant_id="tenant-1", id="app-1") + expected = MagicMock(name="workflow_run") + workflow_run_repo.get_workflow_run_by_id.return_value = expected + + # Act + result = service.get_workflow_run(app_model=app_model, run_id="run-1") + + # Assert + assert result is expected + workflow_run_repo.get_workflow_run_by_id.assert_called_once_with( + tenant_id="tenant-1", + app_id="app-1", + run_id="run-1", + ) + + +def test_get_workflow_runs_count_should_forward_optional_filters( + repository_factory_mocks: tuple[MagicMock, MagicMock, Any], +) -> None: + # Arrange + _, workflow_run_repo, _ = repository_factory_mocks + service = WorkflowRunService(session_factory=MagicMock(name="session_factory")) + app_model = _app_model(tenant_id="tenant-1", id="app-1") + expected = {"total": 3, "succeeded": 2} + workflow_run_repo.get_workflow_runs_count.return_value = expected + + # Act + result = service.get_workflow_runs_count( + app_model=app_model, + status="succeeded", + time_range="7d", + triggered_from=WorkflowRunTriggeredFrom.APP_RUN, + ) + + # Assert + assert result == expected + workflow_run_repo.get_workflow_runs_count.assert_called_once_with( + tenant_id="tenant-1", + app_id="app-1", + triggered_from=WorkflowRunTriggeredFrom.APP_RUN, + status="succeeded", + time_range="7d", + ) + + +def test_get_workflow_run_node_executions_should_return_empty_list_when_run_not_found( + repository_factory_mocks: tuple[MagicMock, MagicMock, Any], + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + service = WorkflowRunService(session_factory=MagicMock(name="session_factory")) + monkeypatch.setattr(service, "get_workflow_run", MagicMock(return_value=None)) + app_model = _app_model(id="app-1") + user = _account(current_tenant_id="tenant-1") + + # Act + result = service.get_workflow_run_node_executions(app_model=app_model, run_id="run-1", user=user) + + # Assert + assert result == [] + + +def test_get_workflow_run_node_executions_should_use_end_user_tenant_id( + repository_factory_mocks: tuple[MagicMock, MagicMock, Any], + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + node_repo, _, _ = repository_factory_mocks + service = WorkflowRunService(session_factory=MagicMock(name="session_factory")) + monkeypatch.setattr(service, "get_workflow_run", MagicMock(return_value=SimpleNamespace(id="run-1"))) + + class FakeEndUser: + def __init__(self, tenant_id: str) -> None: + self.tenant_id = tenant_id + + monkeypatch.setattr(service_module, "EndUser", FakeEndUser) + user = cast(EndUser, FakeEndUser(tenant_id="tenant-end-user")) + app_model = _app_model(id="app-1") + expected = [SimpleNamespace(id="exec-1")] + node_repo.get_executions_by_workflow_run.return_value = expected + + # Act + result = service.get_workflow_run_node_executions(app_model=app_model, run_id="run-1", user=user) + + # Assert + assert result == expected + node_repo.get_executions_by_workflow_run.assert_called_once_with( + tenant_id="tenant-end-user", + app_id="app-1", + workflow_run_id="run-1", + ) + + +def test_get_workflow_run_node_executions_should_use_account_current_tenant_id( + repository_factory_mocks: tuple[MagicMock, MagicMock, Any], + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + node_repo, _, _ = repository_factory_mocks + service = WorkflowRunService(session_factory=MagicMock(name="session_factory")) + monkeypatch.setattr(service, "get_workflow_run", MagicMock(return_value=SimpleNamespace(id="run-1"))) + app_model = _app_model(id="app-1") + user = _account(current_tenant_id="tenant-account") + expected = [SimpleNamespace(id="exec-1"), SimpleNamespace(id="exec-2")] + node_repo.get_executions_by_workflow_run.return_value = expected + + # Act + result = service.get_workflow_run_node_executions(app_model=app_model, run_id="run-1", user=user) + + # Assert + assert result == expected + node_repo.get_executions_by_workflow_run.assert_called_once_with( + tenant_id="tenant-account", + app_id="app-1", + workflow_run_id="run-1", + ) + + +def test_get_workflow_run_node_executions_should_raise_when_resolved_tenant_id_is_none( + repository_factory_mocks: tuple[MagicMock, MagicMock, Any], + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + service = WorkflowRunService(session_factory=MagicMock(name="session_factory")) + monkeypatch.setattr(service, "get_workflow_run", MagicMock(return_value=SimpleNamespace(id="run-1"))) + app_model = _app_model(id="app-1") + user = _account(current_tenant_id=None) + + # Act / Assert + with pytest.raises(ValueError, match="tenant_id cannot be None"): + service.get_workflow_run_node_executions(app_model=app_model, run_id="run-1", user=user) diff --git a/api/tests/unit_tests/services/workflow/test_workflow_converter_additional.py b/api/tests/unit_tests/services/workflow/test_workflow_converter_additional.py new file mode 100644 index 0000000000..2aaf3bdf1d --- /dev/null +++ b/api/tests/unit_tests/services/workflow/test_workflow_converter_additional.py @@ -0,0 +1,831 @@ +from __future__ import annotations + +import json +from types import SimpleNamespace +from typing import Any, cast +from unittest.mock import MagicMock + +import pytest + +from core.app.app_config.entities import ( + AdvancedChatMessageEntity, + AdvancedChatPromptTemplateEntity, + AdvancedCompletionPromptTemplateEntity, + DatasetEntity, + DatasetRetrieveConfigEntity, + ExternalDataVariableEntity, + ModelConfigEntity, + PromptTemplateEntity, +) +from core.helper import encrypter +from core.prompt.utils.prompt_template_parser import PromptTemplateParser +from models.api_based_extension import APIBasedExtension, APIBasedExtensionPoint +from models.model import Account, App, AppMode, AppModelConfig +from services.workflow import workflow_converter as converter_module +from services.workflow.workflow_converter import WorkflowConverter + +try: + from graphon.enums import BuiltinNodeTypes + from graphon.model_runtime.entities.llm_entities import LLMMode + from graphon.model_runtime.entities.message_entities import PromptMessageRole + from graphon.variables.input_entities import VariableEntity, VariableEntityType +except ModuleNotFoundError: + from dify_graph.enums import BuiltinNodeTypes + from dify_graph.model_runtime.entities.llm_entities import LLMMode + from dify_graph.model_runtime.entities.message_entities import PromptMessageRole + from dify_graph.variables.input_entities import VariableEntity, VariableEntityType + + +@pytest.fixture +def converter() -> WorkflowConverter: + return WorkflowConverter() + + +def _app_model(**kwargs: Any) -> App: + return cast(App, SimpleNamespace(**kwargs)) + + +def _account(**kwargs: Any) -> Account: + return cast(Account, SimpleNamespace(**kwargs)) + + +def _app_model_config(**kwargs: Any) -> AppModelConfig: + return cast(AppModelConfig, SimpleNamespace(**kwargs)) + + +def _build_start_graph() -> dict[str, Any]: + return { + "nodes": [ + { + "id": "start", + "position": None, + "data": {"type": BuiltinNodeTypes.START, "variables": [{"variable": "name"}, {"variable": "city"}]}, + } + ], + "edges": [], + } + + +def _build_model_config(mode: str | LLMMode) -> ModelConfigEntity: + return ModelConfigEntity(provider="openai", model="gpt-4", mode=mode, parameters={}, stop=[]) + + +@pytest.fixture +def default_variables() -> list[VariableEntity]: + return [ + VariableEntity(variable="text_input", label="text-input", type=VariableEntityType.TEXT_INPUT), + VariableEntity(variable="paragraph", label="paragraph", type=VariableEntityType.PARAGRAPH), + VariableEntity(variable="select", label="select", type=VariableEntityType.SELECT), + ] + + +def test__convert_to_start_node(default_variables: list[VariableEntity]) -> None: + result = WorkflowConverter()._convert_to_start_node(default_variables) + + assert result["id"] == "start" + assert result["data"]["type"] == BuiltinNodeTypes.START + assert result["data"]["variables"][0]["type"] == "text-input" + assert result["data"]["variables"][0]["variable"] == "text_input" + + +def test__convert_to_http_request_node_for_chatbot(default_variables: list[VariableEntity]) -> None: + app_model = MagicMock() + app_model.id = "app_id" + app_model.tenant_id = "tenant_id" + app_model.mode = AppMode.CHAT + + extension = APIBasedExtension( + tenant_id="tenant_id", + name="api-1", + api_key="encrypted_api_key", + api_endpoint="https://dify.ai", + ) + extension.id = "api_based_extension_id" + + workflow_converter = WorkflowConverter() + workflow_converter._get_api_based_extension = MagicMock(return_value=extension) + encrypter.decrypt_token = MagicMock(return_value="api_key") + + external_data_variables = [ + ExternalDataVariableEntity( + variable="external_variable", + type="api", + config={"api_based_extension_id": "api_based_extension_id"}, + ), + ] + + nodes, mapping = workflow_converter._convert_to_http_request_node( + app_model=app_model, + variables=default_variables, + external_data_variables=external_data_variables, + ) + + assert len(nodes) == 2 + assert nodes[0]["data"]["type"] == BuiltinNodeTypes.HTTP_REQUEST + assert nodes[1]["data"]["type"] == BuiltinNodeTypes.CODE + body = json.loads(nodes[0]["data"]["body"]["data"]) + assert body["point"] == APIBasedExtensionPoint.APP_EXTERNAL_DATA_TOOL_QUERY + assert body["params"]["query"] == "{{#sys.query#}}" + assert body["params"]["inputs"]["text_input"] == "{{#start.text_input#}}" + assert mapping == {"external_variable": "code_1"} + + +def test__convert_to_http_request_node_for_workflow_app(default_variables: list[VariableEntity]) -> None: + app_model = MagicMock() + app_model.id = "app_id" + app_model.tenant_id = "tenant_id" + app_model.mode = AppMode.WORKFLOW + + extension = APIBasedExtension( + tenant_id="tenant_id", + name="api-1", + api_key="encrypted_api_key", + api_endpoint="https://dify.ai", + ) + extension.id = "api_based_extension_id" + + workflow_converter = WorkflowConverter() + workflow_converter._get_api_based_extension = MagicMock(return_value=extension) + encrypter.decrypt_token = MagicMock(return_value="api_key") + + external_data_variables = [ + ExternalDataVariableEntity( + variable="external_variable", + type="api", + config={"api_based_extension_id": "api_based_extension_id"}, + ), + ] + + nodes, _ = workflow_converter._convert_to_http_request_node( + app_model=app_model, + variables=default_variables, + external_data_variables=external_data_variables, + ) + + body = json.loads(nodes[0]["data"]["body"]["data"]) + assert body["params"]["query"] == "" + + +def test__convert_to_knowledge_retrieval_node_for_chatbot() -> None: + dataset_config = DatasetEntity( + dataset_ids=["dataset_id_1", "dataset_id_2"], + retrieve_config=DatasetRetrieveConfigEntity( + retrieve_strategy=DatasetRetrieveConfigEntity.RetrieveStrategy.MULTIPLE, + top_k=5, + score_threshold=0.8, + reranking_model={"reranking_provider_name": "cohere", "reranking_model_name": "rerank-english-v2.0"}, + reranking_enabled=True, + ), + ) + model_config = ModelConfigEntity(provider="openai", model="gpt-4", mode="chat", parameters={}, stop=[]) + + node = WorkflowConverter()._convert_to_knowledge_retrieval_node( + new_app_mode=AppMode.ADVANCED_CHAT, + dataset_config=dataset_config, + model_config=model_config, + ) + + assert node is not None + assert node["data"]["query_variable_selector"] == ["sys", "query"] + assert node["data"]["multiple_retrieval_config"]["top_k"] == 5 + + +def test__convert_to_knowledge_retrieval_node_for_workflow_app() -> None: + dataset_config = DatasetEntity( + dataset_ids=["dataset_id_1", "dataset_id_2"], + retrieve_config=DatasetRetrieveConfigEntity( + query_variable="query", + retrieve_strategy=DatasetRetrieveConfigEntity.RetrieveStrategy.MULTIPLE, + top_k=5, + score_threshold=0.8, + reranking_model={"reranking_provider_name": "cohere", "reranking_model_name": "rerank-english-v2.0"}, + reranking_enabled=True, + ), + ) + model_config = ModelConfigEntity(provider="openai", model="gpt-4", mode="chat", parameters={}, stop=[]) + + node = WorkflowConverter()._convert_to_knowledge_retrieval_node( + new_app_mode=AppMode.WORKFLOW, + dataset_config=dataset_config, + model_config=model_config, + ) + + assert node is not None + assert node["data"]["query_variable_selector"] == ["start", "query"] + + +def test__convert_to_llm_node_for_chatbot_simple_chat_model(default_variables: list[VariableEntity]) -> None: + workflow_converter = WorkflowConverter() + graph = {"nodes": [workflow_converter._convert_to_start_node(default_variables)], "edges": []} + model_config = ModelConfigEntity(provider="openai", model="gpt-4", mode=LLMMode.CHAT.value, parameters={}, stop=[]) + prompt_template = PromptTemplateEntity( + prompt_type=PromptTemplateEntity.PromptType.SIMPLE, + simple_prompt_template="You are a helper for {{text_input}} and {{paragraph}}", + ) + + node = workflow_converter._convert_to_llm_node( + original_app_mode=AppMode.CHAT, + new_app_mode=AppMode.ADVANCED_CHAT, + model_config=model_config, + graph=graph, + prompt_template=prompt_template, + ) + + assert node["data"]["type"] == BuiltinNodeTypes.LLM + assert node["data"]["memory"] is not None + assert node["data"]["prompt_template"][0]["role"] == "user" + assert "{{#start.text_input#}}" in node["data"]["prompt_template"][0]["text"] + + +def test__convert_to_llm_node_for_chatbot_simple_chat_model_with_empty_template( + default_variables: list[VariableEntity], + monkeypatch: pytest.MonkeyPatch, +) -> None: + workflow_converter = WorkflowConverter() + graph = {"nodes": [workflow_converter._convert_to_start_node(default_variables)], "edges": []} + model_config = ModelConfigEntity(provider="openai", model="gpt-4", mode=LLMMode.CHAT.value, parameters={}, stop=[]) + prompt_template = PromptTemplateEntity( + prompt_type=PromptTemplateEntity.PromptType.SIMPLE, + simple_prompt_template="ignored", + ) + monkeypatch.setattr( + converter_module.SimplePromptTransform, + "get_prompt_template", + lambda self, **kwargs: {"prompt_template": PromptTemplateParser(""), "prompt_rules": {}}, + ) + + node = workflow_converter._convert_to_llm_node( + original_app_mode=AppMode.CHAT, + new_app_mode=AppMode.ADVANCED_CHAT, + model_config=model_config, + graph=graph, + prompt_template=prompt_template, + ) + + assert node["data"]["prompt_template"] == [] + + +def test__convert_to_llm_node_for_chatbot_advanced_chat_model(default_variables: list[VariableEntity]) -> None: + workflow_converter = WorkflowConverter() + graph = {"nodes": [workflow_converter._convert_to_start_node(default_variables)], "edges": []} + model_config = ModelConfigEntity(provider="openai", model="gpt-4", mode=LLMMode.CHAT.value, parameters={}, stop=[]) + prompt_template = PromptTemplateEntity( + prompt_type=PromptTemplateEntity.PromptType.ADVANCED, + advanced_chat_prompt_template=AdvancedChatPromptTemplateEntity( + messages=[AdvancedChatMessageEntity(text="Hello {{text_input}}", role=PromptMessageRole.USER)] + ), + ) + + node = workflow_converter._convert_to_llm_node( + original_app_mode=AppMode.CHAT, + new_app_mode=AppMode.ADVANCED_CHAT, + model_config=model_config, + graph=graph, + prompt_template=prompt_template, + ) + + assert isinstance(node["data"]["prompt_template"], list) + assert node["data"]["prompt_template"][0]["role"] == PromptMessageRole.USER.value + + +def test__convert_to_llm_node_for_chatbot_advanced_chat_model_without_template( + default_variables: list[VariableEntity], +) -> None: + workflow_converter = WorkflowConverter() + graph = {"nodes": [workflow_converter._convert_to_start_node(default_variables)], "edges": []} + model_config = ModelConfigEntity(provider="openai", model="gpt-4", mode=LLMMode.CHAT.value, parameters={}, stop=[]) + prompt_template = PromptTemplateEntity( + prompt_type=PromptTemplateEntity.PromptType.ADVANCED, + advanced_chat_prompt_template=None, + ) + + node = workflow_converter._convert_to_llm_node( + original_app_mode=AppMode.CHAT, + new_app_mode=AppMode.WORKFLOW, + model_config=model_config, + graph=graph, + prompt_template=prompt_template, + ) + + assert node["data"]["prompt_template"] == [] + assert node["data"]["memory"] is None + + +def test__convert_to_llm_node_for_workflow_advanced_completion_model(default_variables: list[VariableEntity]) -> None: + workflow_converter = WorkflowConverter() + graph = {"nodes": [workflow_converter._convert_to_start_node(default_variables)], "edges": []} + model_config = ModelConfigEntity( + provider="openai", + model="gpt-3.5-turbo-instruct", + mode=LLMMode.COMPLETION.value, + parameters={}, + stop=[], + ) + prompt_template = PromptTemplateEntity( + prompt_type=PromptTemplateEntity.PromptType.ADVANCED, + advanced_completion_prompt_template=AdvancedCompletionPromptTemplateEntity( + prompt="Hello {{text_input}} and {{#query#}}", + role_prefix=AdvancedCompletionPromptTemplateEntity.RolePrefixEntity(user="Human", assistant="Assistant"), + ), + ) + + node = workflow_converter._convert_to_llm_node( + original_app_mode=AppMode.COMPLETION, + new_app_mode=AppMode.ADVANCED_CHAT, + model_config=model_config, + graph=graph, + prompt_template=prompt_template, + ) + + assert node["data"]["prompt_template"]["text"].find("{{#sys.query#}}") != -1 + assert node["data"]["memory"]["role_prefix"]["user"] == "Human" + + +def test__convert_to_end_node() -> None: + node = WorkflowConverter()._convert_to_end_node() + assert node["id"] == "end" + assert node["data"]["type"] == BuiltinNodeTypes.END + + +def test__convert_to_answer_node() -> None: + node = WorkflowConverter()._convert_to_answer_node() + assert node["id"] == "answer" + assert node["data"]["type"] == BuiltinNodeTypes.ANSWER + + +def test_convert_to_workflow_should_raise_when_app_model_config_is_missing(converter: WorkflowConverter) -> None: + app_model = _app_model(app_model_config=None) + + with pytest.raises(ValueError, match="App model config is required"): + converter.convert_to_workflow( + app_model=app_model, + account=_account(id="account-1"), + name="new-app", + icon_type="emoji", + icon="robot", + icon_background="#fff", + ) + + +@pytest.mark.parametrize( + ("source_mode", "expected_mode"), + [ + (AppMode.CHAT, AppMode.ADVANCED_CHAT), + (AppMode.COMPLETION, AppMode.WORKFLOW), + ], +) +def test_convert_to_workflow_should_create_new_app_with_fallback_fields( + converter: WorkflowConverter, + monkeypatch: pytest.MonkeyPatch, + source_mode: AppMode, + expected_mode: AppMode, +) -> None: + class FakeApp: + def __init__(self) -> None: + self.id = "new-app-id" + + workflow = SimpleNamespace(app_id=None) + monkeypatch.setattr(converter, "convert_app_model_config_to_workflow", MagicMock(return_value=workflow)) + monkeypatch.setattr(converter_module, "App", FakeApp) + + db_session = SimpleNamespace(add=MagicMock(), flush=MagicMock(), commit=MagicMock()) + monkeypatch.setattr(converter_module, "db", SimpleNamespace(session=db_session)) + + send_mock = MagicMock() + monkeypatch.setattr(converter_module.app_was_created, "send", send_mock) + + account = _account(id="account-1") + app_model = _app_model( + tenant_id="tenant-1", + name="Source App", + mode=source_mode, + icon_type="emoji", + icon="sparkles", + icon_background="#123456", + enable_site=True, + enable_api=True, + api_rpm=10, + api_rph=100, + is_public=False, + app_model_config=_app_model_config(id="config-1"), + ) + + new_app = converter.convert_to_workflow( + app_model=app_model, + account=account, + name="", + icon_type="", + icon="", + icon_background="", + ) + + assert new_app.name == "Source App(workflow)" + assert new_app.mode == expected_mode + assert new_app.icon_type == "emoji" + assert new_app.icon == "sparkles" + assert new_app.icon_background == "#123456" + assert new_app.created_by == "account-1" + assert workflow.app_id == "new-app-id" + db_session.add.assert_called_once() + db_session.flush.assert_called_once() + db_session.commit.assert_called_once() + send_mock.assert_called_once_with(new_app, account=account) + + +def test_convert_app_model_config_to_workflow_should_build_advanced_chat_graph_and_features( + converter: WorkflowConverter, + monkeypatch: pytest.MonkeyPatch, +) -> None: + app_model = _app_model(id="app-1", tenant_id="tenant-1", mode=AppMode.CHAT) + app_config = SimpleNamespace( + variables=[SimpleNamespace(variable="name")], + external_data_variables=[SimpleNamespace(variable="ext")], + dataset=SimpleNamespace(id="dataset"), + model=SimpleNamespace(), + prompt_template=SimpleNamespace(), + additional_features=SimpleNamespace(file_upload=SimpleNamespace()), + app_model_config_dict={ + "opening_statement": "hello", + "suggested_questions": ["q1"], + "suggested_questions_after_answer": True, + "speech_to_text": True, + "text_to_speech": {"enabled": True}, + "file_upload": {"enabled": True}, + "sensitive_word_avoidance": {"enabled": True}, + "retriever_resource": {"enabled": True}, + }, + ) + + class FakeWorkflow: + VERSION_DRAFT = "draft" + + def __init__(self, **kwargs: Any) -> None: + self.__dict__.update(kwargs) + + monkeypatch.setattr(converter, "_get_new_app_mode", MagicMock(return_value=AppMode.ADVANCED_CHAT)) + monkeypatch.setattr(converter, "_convert_to_app_config", MagicMock(return_value=app_config)) + monkeypatch.setattr( + converter, + "_convert_to_start_node", + MagicMock( + return_value={"id": "start", "position": None, "data": {"type": BuiltinNodeTypes.START, "variables": []}} + ), + ) + monkeypatch.setattr( + converter, + "_convert_to_http_request_node", + MagicMock( + return_value=( + [{"id": "http", "position": None, "data": {"type": BuiltinNodeTypes.HTTP_REQUEST}}], + {"ext": "code_1"}, + ) + ), + ) + monkeypatch.setattr( + converter, + "_convert_to_knowledge_retrieval_node", + MagicMock( + return_value={"id": "knowledge", "position": None, "data": {"type": BuiltinNodeTypes.KNOWLEDGE_RETRIEVAL}} + ), + ) + monkeypatch.setattr( + converter, + "_convert_to_llm_node", + MagicMock(return_value={"id": "llm", "position": None, "data": {"type": BuiltinNodeTypes.LLM}}), + ) + monkeypatch.setattr( + converter, + "_convert_to_answer_node", + MagicMock(return_value={"id": "answer", "position": None, "data": {"type": BuiltinNodeTypes.ANSWER}}), + ) + monkeypatch.setattr(converter_module, "Workflow", FakeWorkflow) + + db_session = SimpleNamespace(add=MagicMock(), commit=MagicMock()) + monkeypatch.setattr(converter_module, "db", SimpleNamespace(session=db_session)) + + workflow = converter.convert_app_model_config_to_workflow( + app_model=app_model, + app_model_config=_app_model_config(id="cfg"), + account_id="account-1", + ) + + graph = json.loads(workflow.graph) + node_ids = [node["id"] for node in graph["nodes"]] + assert node_ids == ["start", "http", "knowledge", "llm", "answer"] + + features = json.loads(workflow.features) + assert "opening_statement" in features + assert "retriever_resource" in features + db_session.add.assert_called_once() + db_session.commit.assert_called_once() + + +def test_convert_app_model_config_to_workflow_should_build_workflow_mode_with_end_node( + converter: WorkflowConverter, + monkeypatch: pytest.MonkeyPatch, +) -> None: + app_model = _app_model(id="app-1", tenant_id="tenant-1", mode=AppMode.COMPLETION) + app_config = SimpleNamespace( + variables=[SimpleNamespace(variable="name")], + external_data_variables=[], + dataset=SimpleNamespace(id="dataset"), + model=SimpleNamespace(), + prompt_template=SimpleNamespace(), + additional_features=None, + app_model_config_dict={ + "text_to_speech": {"enabled": False}, + "file_upload": {"enabled": False}, + "sensitive_word_avoidance": {"enabled": False}, + }, + ) + + class FakeWorkflow: + VERSION_DRAFT = "draft" + + def __init__(self, **kwargs: Any) -> None: + self.__dict__.update(kwargs) + + monkeypatch.setattr(converter, "_get_new_app_mode", MagicMock(return_value=AppMode.WORKFLOW)) + monkeypatch.setattr(converter, "_convert_to_app_config", MagicMock(return_value=app_config)) + monkeypatch.setattr( + converter, + "_convert_to_start_node", + MagicMock( + return_value={"id": "start", "position": None, "data": {"type": BuiltinNodeTypes.START, "variables": []}} + ), + ) + monkeypatch.setattr(converter, "_convert_to_knowledge_retrieval_node", MagicMock(return_value=None)) + monkeypatch.setattr( + converter, + "_convert_to_llm_node", + MagicMock(return_value={"id": "llm", "position": None, "data": {"type": BuiltinNodeTypes.LLM}}), + ) + monkeypatch.setattr( + converter, + "_convert_to_end_node", + MagicMock(return_value={"id": "end", "position": None, "data": {"type": BuiltinNodeTypes.END}}), + ) + monkeypatch.setattr(converter_module, "Workflow", FakeWorkflow) + + db_session = SimpleNamespace(add=MagicMock(), commit=MagicMock()) + monkeypatch.setattr(converter_module, "db", SimpleNamespace(session=db_session)) + + workflow = converter.convert_app_model_config_to_workflow( + app_model=app_model, + app_model_config=_app_model_config(id="cfg"), + account_id="account-1", + ) + + graph = json.loads(workflow.graph) + node_ids = [node["id"] for node in graph["nodes"]] + assert node_ids == ["start", "llm", "end"] + + features = json.loads(workflow.features) + assert set(features.keys()) == {"text_to_speech", "file_upload", "sensitive_word_avoidance"} + + +def test_convert_to_app_config_should_route_to_correct_manager( + converter: WorkflowConverter, + monkeypatch: pytest.MonkeyPatch, +) -> None: + agent_result = SimpleNamespace(kind="agent") + chat_result = SimpleNamespace(kind="chat") + completion_result = SimpleNamespace(kind="completion") + monkeypatch.setattr( + converter_module.AgentChatAppConfigManager, "get_app_config", MagicMock(return_value=agent_result) + ) + monkeypatch.setattr(converter_module.ChatAppConfigManager, "get_app_config", MagicMock(return_value=chat_result)) + monkeypatch.setattr( + converter_module.CompletionAppConfigManager, + "get_app_config", + MagicMock(return_value=completion_result), + ) + + from_agent_mode = converter._convert_to_app_config( + app_model=_app_model(mode=AppMode.AGENT_CHAT, is_agent=False), + app_model_config=_app_model_config(id="cfg-1"), + ) + from_agent_flag = converter._convert_to_app_config( + app_model=_app_model(mode=AppMode.CHAT, is_agent=True), + app_model_config=_app_model_config(id="cfg-2"), + ) + from_chat_mode = converter._convert_to_app_config( + app_model=_app_model(mode=AppMode.CHAT, is_agent=False), + app_model_config=_app_model_config(id="cfg-3"), + ) + from_completion_mode = converter._convert_to_app_config( + app_model=_app_model(mode=AppMode.COMPLETION, is_agent=False), + app_model_config=_app_model_config(id="cfg-4"), + ) + + assert from_agent_mode is agent_result + assert from_agent_flag is agent_result + assert from_chat_mode is chat_result + assert from_completion_mode is completion_result + + +def test_convert_to_app_config_should_raise_for_invalid_app_mode(converter: WorkflowConverter) -> None: + app_model = _app_model(mode=AppMode.WORKFLOW, is_agent=False) + + with pytest.raises(ValueError, match="Invalid app mode"): + converter._convert_to_app_config(app_model=app_model, app_model_config=_app_model_config(id="cfg")) + + +def test_convert_to_http_request_node_should_skip_non_api_and_missing_extension_id( + converter: WorkflowConverter, +) -> None: + app_model = _app_model(id="app-1", tenant_id="tenant-1", mode=AppMode.CHAT) + external_data_variables = [ + ExternalDataVariableEntity(variable="skip_type", type="dataset", config={"api_based_extension_id": "x"}), + ExternalDataVariableEntity(variable="skip_config", type="api", config={}), + ] + + nodes, mapping = converter._convert_to_http_request_node( + app_model=app_model, + variables=[], + external_data_variables=external_data_variables, + ) + + assert nodes == [] + assert mapping == {} + + +def test_convert_to_knowledge_retrieval_node_should_return_none_for_workflow_without_query_variable( + converter: WorkflowConverter, +) -> None: + dataset_config = DatasetEntity( + dataset_ids=["ds-1"], + retrieve_config=DatasetRetrieveConfigEntity( + query_variable=None, + retrieve_strategy=DatasetRetrieveConfigEntity.RetrieveStrategy.MULTIPLE, + ), + ) + model_config = _build_model_config(mode=LLMMode.CHAT) + + node = converter._convert_to_knowledge_retrieval_node( + new_app_mode=AppMode.WORKFLOW, + dataset_config=dataset_config, + model_config=model_config, + ) + + assert node is None + + +def test_convert_to_llm_node_should_raise_when_simple_chat_template_missing( + converter: WorkflowConverter, +) -> None: + graph = _build_start_graph() + model_config = _build_model_config(mode=LLMMode.CHAT) + prompt_template = PromptTemplateEntity(prompt_type=PromptTemplateEntity.PromptType.SIMPLE) + + with pytest.raises(ValueError, match="Simple prompt template is required"): + converter._convert_to_llm_node( + original_app_mode=AppMode.CHAT, + new_app_mode=AppMode.ADVANCED_CHAT, + graph=graph, + model_config=model_config, + prompt_template=prompt_template, + ) + + +def test_convert_to_llm_node_should_raise_when_prompt_template_parser_type_is_invalid_for_chat( + converter: WorkflowConverter, + monkeypatch: pytest.MonkeyPatch, +) -> None: + graph = _build_start_graph() + model_config = _build_model_config(mode=LLMMode.CHAT) + prompt_template = PromptTemplateEntity( + prompt_type=PromptTemplateEntity.PromptType.SIMPLE, + simple_prompt_template="Hello {{name}}", + ) + monkeypatch.setattr( + converter_module.SimplePromptTransform, + "get_prompt_template", + lambda self, **kwargs: {"prompt_template": "invalid"}, + ) + + with pytest.raises(TypeError, match="Expected PromptTemplateParser"): + converter._convert_to_llm_node( + original_app_mode=AppMode.CHAT, + new_app_mode=AppMode.ADVANCED_CHAT, + graph=graph, + model_config=model_config, + prompt_template=prompt_template, + ) + + +def test_convert_to_llm_node_should_raise_when_simple_completion_template_missing( + converter: WorkflowConverter, +) -> None: + graph = _build_start_graph() + model_config = _build_model_config(mode=LLMMode.COMPLETION) + prompt_template = PromptTemplateEntity(prompt_type=PromptTemplateEntity.PromptType.SIMPLE) + + with pytest.raises(ValueError, match="Simple prompt template is required"): + converter._convert_to_llm_node( + original_app_mode=AppMode.COMPLETION, + new_app_mode=AppMode.WORKFLOW, + graph=graph, + model_config=model_config, + prompt_template=prompt_template, + ) + + +def test_convert_to_llm_node_should_raise_when_completion_prompt_rules_type_is_invalid( + converter: WorkflowConverter, + monkeypatch: pytest.MonkeyPatch, +) -> None: + graph = _build_start_graph() + model_config = _build_model_config(mode=LLMMode.COMPLETION) + prompt_template = PromptTemplateEntity( + prompt_type=PromptTemplateEntity.PromptType.SIMPLE, + simple_prompt_template="Hello {{name}}", + ) + monkeypatch.setattr( + converter_module.SimplePromptTransform, + "get_prompt_template", + lambda self, **kwargs: {"prompt_template": PromptTemplateParser("Hello {{name}}"), "prompt_rules": "invalid"}, + ) + + with pytest.raises(TypeError, match="Expected dict for prompt_rules"): + converter._convert_to_llm_node( + original_app_mode=AppMode.COMPLETION, + new_app_mode=AppMode.ADVANCED_CHAT, + graph=graph, + model_config=model_config, + prompt_template=prompt_template, + ) + + +def test_convert_to_llm_node_should_use_empty_text_for_advanced_completion_without_template( + converter: WorkflowConverter, +) -> None: + graph = _build_start_graph() + model_config = _build_model_config(mode=LLMMode.COMPLETION) + prompt_template = PromptTemplateEntity( + prompt_type=PromptTemplateEntity.PromptType.ADVANCED, + advanced_completion_prompt_template=None, + ) + + llm_node = converter._convert_to_llm_node( + original_app_mode=AppMode.COMPLETION, + new_app_mode=AppMode.WORKFLOW, + graph=graph, + model_config=model_config, + prompt_template=prompt_template, + ) + + assert llm_node["data"]["prompt_template"]["text"] == "" + assert llm_node["data"]["memory"] is None + + +def test_replace_template_variables_should_replace_start_and_external_references(converter: WorkflowConverter) -> None: + template = "Hello {{name}} from {{city}} with {{weather}}" + variables = [{"variable": "name"}, {"variable": "city"}] + external_mapping = {"weather": "code_1"} + + result = converter._replace_template_variables(template, variables, external_mapping) + + assert result == "Hello {{#start.name#}} from {{#start.city#}} with {{#code_1.result#}}" + + +def test_graph_helpers_should_create_edges_append_nodes_and_choose_mode(converter: WorkflowConverter) -> None: + graph = {"nodes": [{"id": "start", "position": None, "data": {"type": BuiltinNodeTypes.START}}], "edges": []} + node = {"id": "llm", "position": None, "data": {"type": BuiltinNodeTypes.LLM}} + + edge = converter._create_edge("start", "llm") + updated_graph = converter._append_node(graph, node) + workflow_mode = converter._get_new_app_mode(_app_model(mode=AppMode.COMPLETION)) + advanced_chat_mode = converter._get_new_app_mode(_app_model(mode=AppMode.CHAT)) + + assert edge == {"id": "start-llm", "source": "start", "target": "llm"} + assert updated_graph["nodes"][-1]["id"] == "llm" + assert updated_graph["edges"][-1]["source"] == "start" + assert workflow_mode == AppMode.WORKFLOW + assert advanced_chat_mode == AppMode.ADVANCED_CHAT + + +def test_get_api_based_extension_should_raise_when_extension_not_found( + converter: WorkflowConverter, + monkeypatch: pytest.MonkeyPatch, +) -> None: + db_session = SimpleNamespace(scalar=MagicMock(return_value=None)) + monkeypatch.setattr(converter_module, "db", SimpleNamespace(session=db_session)) + + with pytest.raises(ValueError, match="API Based Extension not found"): + converter._get_api_based_extension(tenant_id="tenant-1", api_based_extension_id="ext-1") + db_session.scalar.assert_called_once() + + +def test_get_api_based_extension_should_return_entity_when_found( + converter: WorkflowConverter, + monkeypatch: pytest.MonkeyPatch, +) -> None: + extension = SimpleNamespace(id="ext-1") + db_session = SimpleNamespace(scalar=MagicMock(return_value=extension)) + monkeypatch.setattr(converter_module, "db", SimpleNamespace(session=db_session)) + + result = converter._get_api_based_extension(tenant_id="tenant-1", api_based_extension_id="ext-1") + + assert result is extension + db_session.scalar.assert_called_once() diff --git a/api/tests/unit_tests/services/workflow/test_workflow_event_snapshot_service.py b/api/tests/unit_tests/services/workflow/test_workflow_event_snapshot_service.py index 077a7c27a2..b8b073f75c 100644 --- a/api/tests/unit_tests/services/workflow/test_workflow_event_snapshot_service.py +++ b/api/tests/unit_tests/services/workflow/test_workflow_event_snapshot_service.py @@ -1,10 +1,9 @@ -from __future__ import annotations - import json import queue from collections.abc import Sequence from dataclasses import dataclass from datetime import UTC, datetime +from itertools import cycle from threading import Event import pytest @@ -224,3 +223,577 @@ def test_resolve_task_id_priority(context_task_id, buffered_task_id, expected) - buffer_state.task_id_ready.set() task_id = _resolve_task_id(resumption_context, buffer_state, "run-1", wait_timeout=0.0) assert task_id == expected + + +# === Merged from test_workflow_event_snapshot_service_additional.py === + + +import json +import queue +from collections.abc import Mapping +from dataclasses import dataclass +from datetime import UTC, datetime +from threading import Event +from types import SimpleNamespace +from typing import Any, cast +from unittest.mock import MagicMock + +import pytest +from graphon.enums import WorkflowExecutionStatus +from graphon.runtime import GraphRuntimeState, VariablePool +from sqlalchemy.orm import Session, sessionmaker + +from core.app.app_config.entities import WorkflowUIBasedAppConfig +from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerateEntity +from core.app.entities.task_entities import StreamEvent +from core.app.layers.pause_state_persist_layer import WorkflowResumptionContext, _WorkflowGenerateEntityWrapper +from models.enums import CreatorUserRole +from models.model import AppMode +from models.workflow import WorkflowRun +from repositories.entities.workflow_pause import WorkflowPauseEntity +from services import workflow_event_snapshot_service as service_module +from services.workflow_event_snapshot_service import BufferState, MessageContext, build_workflow_event_stream + + +def _build_workflow_run_additional(status: WorkflowExecutionStatus = WorkflowExecutionStatus.RUNNING) -> WorkflowRun: + return WorkflowRun( + id="run-1", + tenant_id="tenant-1", + app_id="app-1", + workflow_id="workflow-1", + type="workflow", + triggered_from="app-run", + version="v1", + graph=None, + inputs=json.dumps({"query": "hello"}), + status=status, + outputs=json.dumps({}), + error=None, + elapsed_time=1.2, + total_tokens=5, + total_steps=2, + created_by_role=CreatorUserRole.END_USER, + created_by="user-1", + created_at=datetime(2024, 1, 1, tzinfo=UTC), + ) + + +def _build_resumption_context_additional(task_id: str) -> WorkflowResumptionContext: + app_config = WorkflowUIBasedAppConfig( + tenant_id="tenant-1", + app_id="app-1", + app_mode=AppMode.WORKFLOW, + workflow_id="workflow-1", + ) + generate_entity = WorkflowAppGenerateEntity( + task_id=task_id, + app_config=app_config, + inputs={}, + files=[], + user_id="user-1", + stream=True, + invoke_from=InvokeFrom.EXPLORE, + call_depth=0, + workflow_execution_id="run-1", + ) + runtime_state = GraphRuntimeState(variable_pool=VariablePool(), start_at=0.0) + runtime_state.outputs = {"answer": "ok"} + wrapper = _WorkflowGenerateEntityWrapper(entity=generate_entity) + return WorkflowResumptionContext( + generate_entity=wrapper, + serialized_graph_runtime_state=runtime_state.dumps(), + ) + + +class _SessionContext: + def __init__(self, session: Any) -> None: + self._session = session + + def __enter__(self) -> Any: + return self._session + + def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> bool: + return False + + +class _SessionMaker: + def __init__(self, session: Any) -> None: + self._session = session + + def __call__(self) -> _SessionContext: + return _SessionContext(self._session) + + +class _SubscriptionContext: + def __init__(self, subscription: Any) -> None: + self._subscription = subscription + + def __enter__(self) -> Any: + return self._subscription + + def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> bool: + return False + + +class _Topic: + def __init__(self, subscription: Any) -> None: + self._subscription = subscription + + def subscribe(self) -> _SubscriptionContext: + return _SubscriptionContext(self._subscription) + + +class _StaticSubscription: + def receive(self, timeout: int = 1) -> None: + return None + + +@dataclass(frozen=True) +class _PauseEntity(WorkflowPauseEntity): + state: bytes + + @property + def id(self) -> str: + return "pause-1" + + @property + def workflow_execution_id(self) -> str: + return "run-1" + + @property + def resumed_at(self) -> datetime | None: + return None + + @property + def paused_at(self) -> datetime: + return datetime(2024, 1, 1, tzinfo=UTC) + + def get_state(self) -> bytes: + return self.state + + def get_pause_reasons(self) -> list[Any]: + return [] + + +def test_get_message_context_should_return_none_when_no_message() -> None: + # Arrange + session = SimpleNamespace(scalar=MagicMock(return_value=None)) + session_maker = _SessionMaker(session) + + # Act + result = service_module._get_message_context(cast(sessionmaker[Session], session_maker), "run-1") + + # Assert + assert result is None + + +def test_get_message_context_should_default_created_at_to_zero_when_message_has_no_timestamp() -> None: + # Arrange + message = SimpleNamespace( + id="msg-1", + conversation_id="conv-1", + created_at=None, + answer="answer", + ) + session = SimpleNamespace(scalar=MagicMock(return_value=message)) + session_maker = _SessionMaker(session) + + # Act + result = service_module._get_message_context(cast(sessionmaker[Session], session_maker), "run-1") + + # Assert + assert result is not None + assert result.created_at == 0 + assert result.message_id == "msg-1" + assert result.conversation_id == "conv-1" + assert result.answer == "answer" + + +def test_load_resumption_context_should_return_none_when_pause_entity_missing() -> None: + # Arrange + + # Act + result = service_module._load_resumption_context(None) + + # Assert + assert result is None + + +def test_load_resumption_context_should_return_none_when_pause_entity_state_is_invalid() -> None: + # Arrange + pause_entity = _PauseEntity(state=b"not-a-valid-state") + + # Act + result = service_module._load_resumption_context(pause_entity) + + # Assert + assert result is None + + +def test_load_resumption_context_should_parse_valid_state_into_context() -> None: + # Arrange + context = _build_resumption_context_additional(task_id="task-ctx") + pause_entity = _PauseEntity(state=context.dumps().encode()) + + # Act + result = service_module._load_resumption_context(pause_entity) + + # Assert + assert result is not None + assert result.get_generate_entity().task_id == "task-ctx" + + +def test_resolve_task_id_should_return_workflow_run_id_when_buffer_state_is_missing() -> None: + # Arrange + + # Act + result = service_module._resolve_task_id( + resumption_context=None, + buffer_state=None, + workflow_run_id="run-1", + ) + + # Assert + assert result == "run-1" + + +@pytest.mark.parametrize( + ("payload", "expected"), + [ + (b'{"event":"node_started"}', {"event": "node_started"}), + (b"invalid-json", None), + (b"[]", None), + ], +) +def test_parse_event_message_should_parse_only_json_object( + payload: bytes, + expected: dict[str, Any] | None, +) -> None: + # Arrange + + # Act + result = service_module._parse_event_message(payload) + + # Assert + assert result == expected + + +def test_is_terminal_event_should_recognize_finished_and_optional_paused_events() -> None: + # Arrange + finished_event = {"event": StreamEvent.WORKFLOW_FINISHED.value} + paused_event = {"event": StreamEvent.WORKFLOW_PAUSED.value} + + # Act + is_finished = service_module._is_terminal_event(finished_event, include_paused=False) + paused_without_flag = service_module._is_terminal_event(paused_event, include_paused=False) + paused_with_flag = service_module._is_terminal_event(paused_event, include_paused=True) + + # Assert + assert is_finished is True + assert paused_without_flag is False + assert paused_with_flag is True + assert service_module._is_terminal_event(StreamEvent.PING.value, include_paused=True) is False + + +def test_apply_message_context_should_update_payload_when_context_exists() -> None: + # Arrange + payload: dict[str, Any] = {"event": "workflow_started"} + context = MessageContext(conversation_id="conv-1", message_id="msg-1", created_at=1700000000) + + # Act + service_module._apply_message_context(payload, context) + + # Assert + assert payload["conversation_id"] == "conv-1" + assert payload["message_id"] == "msg-1" + assert payload["created_at"] == 1700000000 + + +def test_start_buffering_should_capture_task_id_and_enqueue_event() -> None: + # Arrange + class Subscription: + def __init__(self) -> None: + self._calls = 0 + + def receive(self, timeout: int = 1) -> bytes | None: + self._calls += 1 + if self._calls == 1: + return b'{"event":"node_started","task_id":"task-1"}' + return None + + subscription = Subscription() + + # Act + buffer_state = service_module._start_buffering(subscription) + ready = buffer_state.task_id_ready.wait(timeout=1) + event = buffer_state.queue.get(timeout=1) + buffer_state.stop_event.set() + finished = buffer_state.done_event.wait(timeout=1) + + # Assert + assert ready is True + assert finished is True + assert buffer_state.task_id_hint == "task-1" + assert event["event"] == "node_started" + + +def test_start_buffering_should_drop_old_event_when_queue_is_full( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + class QueueWithSingleFull: + def __init__(self) -> None: + self._first_put = True + self.items: list[dict[str, Any]] = [{"event": "old"}] + + def put_nowait(self, item: dict[str, Any]) -> None: + if self._first_put: + self._first_put = False + raise queue.Full + self.items.append(item) + + def get_nowait(self) -> dict[str, Any]: + if not self.items: + raise queue.Empty + return self.items.pop(0) + + def empty(self) -> bool: + return len(self.items) == 0 + + fake_queue = QueueWithSingleFull() + monkeypatch.setattr(service_module.queue, "Queue", lambda maxsize=2048: fake_queue) + + class Subscription: + def __init__(self) -> None: + self._calls = 0 + + def receive(self, timeout: int = 1) -> bytes | None: + self._calls += 1 + if self._calls == 1: + return b'{"event":"node_started","task_id":"task-2"}' + return None + + subscription = Subscription() + + # Act + buffer_state = service_module._start_buffering(subscription) + ready = buffer_state.task_id_ready.wait(timeout=1) + buffer_state.stop_event.set() + finished = buffer_state.done_event.wait(timeout=1) + + # Assert + assert ready is True + assert finished is True + assert fake_queue.items[-1]["task_id"] == "task-2" + + +def test_start_buffering_should_set_done_event_when_subscription_raises() -> None: + # Arrange + class Subscription: + def receive(self, timeout: int = 1) -> bytes | None: + raise RuntimeError("subscription failure") + + subscription = Subscription() + + # Act + buffer_state = service_module._start_buffering(subscription) + finished = buffer_state.done_event.wait(timeout=1) + + # Assert + assert finished is True + + +def test_build_workflow_event_stream_should_emit_ping_and_terminal_snapshot_event( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + workflow_run = _build_workflow_run_additional(status=WorkflowExecutionStatus.RUNNING) + topic = _Topic(_StaticSubscription()) + workflow_run_repo = SimpleNamespace(get_workflow_pause=MagicMock()) + node_repo = SimpleNamespace(get_execution_snapshots_by_workflow_run=MagicMock(return_value=[])) + factory = SimpleNamespace( + create_api_workflow_run_repository=MagicMock(return_value=workflow_run_repo), + create_api_workflow_node_execution_repository=MagicMock(return_value=node_repo), + ) + monkeypatch.setattr(service_module, "DifyAPIRepositoryFactory", factory) + monkeypatch.setattr(service_module.MessageGenerator, "get_response_topic", MagicMock(return_value=topic)) + monkeypatch.setattr( + service_module, + "_get_message_context", + MagicMock(return_value=MessageContext("conv-1", "msg-1", 1700000000)), + ) + monkeypatch.setattr(service_module, "_load_resumption_context", MagicMock(return_value=None)) + buffer_state = BufferState( + queue=queue.Queue(), + stop_event=Event(), + done_event=Event(), + task_id_ready=Event(), + task_id_hint="task-1", + ) + monkeypatch.setattr(service_module, "_start_buffering", MagicMock(return_value=buffer_state)) + monkeypatch.setattr(service_module, "_resolve_task_id", MagicMock(return_value="task-1")) + monkeypatch.setattr( + service_module, + "_build_snapshot_events", + MagicMock(return_value=[{"event": StreamEvent.WORKFLOW_FINISHED.value, "task_id": "task-1"}]), + ) + + # Act + events = list( + build_workflow_event_stream( + app_mode=AppMode.ADVANCED_CHAT, + workflow_run=workflow_run, + tenant_id="tenant-1", + app_id="app-1", + session_maker=MagicMock(), + ) + ) + + # Assert + assert events[0] == StreamEvent.PING.value + finished_event = cast(Mapping[str, Any], events[1]) + assert finished_event["event"] == StreamEvent.WORKFLOW_FINISHED.value + assert buffer_state.stop_event.is_set() is True + node_repo.get_execution_snapshots_by_workflow_run.assert_called_once() + called_kwargs = node_repo.get_execution_snapshots_by_workflow_run.call_args.kwargs + assert called_kwargs["workflow_run_id"] == "run-1" + + +def test_build_workflow_event_stream_should_emit_periodic_ping_and_stop_after_idle_timeout( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + workflow_run = _build_workflow_run_additional(status=WorkflowExecutionStatus.RUNNING) + topic = _Topic(_StaticSubscription()) + workflow_run_repo = SimpleNamespace(get_workflow_pause=MagicMock()) + node_repo = SimpleNamespace(get_execution_snapshots_by_workflow_run=MagicMock(return_value=[])) + factory = SimpleNamespace( + create_api_workflow_run_repository=MagicMock(return_value=workflow_run_repo), + create_api_workflow_node_execution_repository=MagicMock(return_value=node_repo), + ) + monkeypatch.setattr(service_module, "DifyAPIRepositoryFactory", factory) + monkeypatch.setattr(service_module.MessageGenerator, "get_response_topic", MagicMock(return_value=topic)) + monkeypatch.setattr(service_module, "_load_resumption_context", MagicMock(return_value=None)) + monkeypatch.setattr(service_module, "_build_snapshot_events", MagicMock(return_value=[])) + monkeypatch.setattr(service_module, "_resolve_task_id", MagicMock(return_value="task-1")) + + class AlwaysEmptyQueue: + def empty(self) -> bool: + return False + + def get(self, timeout: int = 1) -> None: + raise queue.Empty + + buffer_state = BufferState( + queue=AlwaysEmptyQueue(), # type: ignore[arg-type] + stop_event=Event(), + done_event=Event(), + task_id_ready=Event(), + task_id_hint="task-1", + ) + monkeypatch.setattr(service_module, "_start_buffering", MagicMock(return_value=buffer_state)) + time_values = cycle([0.0, 6.0, 21.0, 26.0]) + monkeypatch.setattr(service_module.time, "time", lambda: next(time_values)) + + # Act + events = list( + build_workflow_event_stream( + app_mode=AppMode.WORKFLOW, + workflow_run=workflow_run, + tenant_id="tenant-1", + app_id="app-1", + session_maker=MagicMock(), + idle_timeout=20.0, + ping_interval=5.0, + ) + ) + + # Assert + assert events == [StreamEvent.PING.value, StreamEvent.PING.value] + assert buffer_state.stop_event.is_set() is True + + +def test_build_workflow_event_stream_should_exit_when_buffer_done_and_empty( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + workflow_run = _build_workflow_run_additional(status=WorkflowExecutionStatus.RUNNING) + topic = _Topic(_StaticSubscription()) + workflow_run_repo = SimpleNamespace(get_workflow_pause=MagicMock()) + node_repo = SimpleNamespace(get_execution_snapshots_by_workflow_run=MagicMock(return_value=[])) + factory = SimpleNamespace( + create_api_workflow_run_repository=MagicMock(return_value=workflow_run_repo), + create_api_workflow_node_execution_repository=MagicMock(return_value=node_repo), + ) + monkeypatch.setattr(service_module, "DifyAPIRepositoryFactory", factory) + monkeypatch.setattr(service_module.MessageGenerator, "get_response_topic", MagicMock(return_value=topic)) + monkeypatch.setattr(service_module, "_load_resumption_context", MagicMock(return_value=None)) + monkeypatch.setattr(service_module, "_build_snapshot_events", MagicMock(return_value=[])) + monkeypatch.setattr(service_module, "_resolve_task_id", MagicMock(return_value="task-1")) + buffer_state = BufferState( + queue=queue.Queue(), + stop_event=Event(), + done_event=Event(), + task_id_ready=Event(), + task_id_hint="task-1", + ) + buffer_state.done_event.set() + monkeypatch.setattr(service_module, "_start_buffering", MagicMock(return_value=buffer_state)) + + # Act + events = list( + build_workflow_event_stream( + app_mode=AppMode.WORKFLOW, + workflow_run=workflow_run, + tenant_id="tenant-1", + app_id="app-1", + session_maker=MagicMock(), + ) + ) + + # Assert + assert events == [StreamEvent.PING.value] + assert buffer_state.stop_event.is_set() is True + + +def test_build_workflow_event_stream_should_continue_when_pause_loading_fails( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Arrange + workflow_run = _build_workflow_run_additional(status=WorkflowExecutionStatus.PAUSED) + topic = _Topic(_StaticSubscription()) + workflow_run_repo = SimpleNamespace(get_workflow_pause=MagicMock(side_effect=RuntimeError("boom"))) + node_repo = SimpleNamespace(get_execution_snapshots_by_workflow_run=MagicMock(return_value=[])) + factory = SimpleNamespace( + create_api_workflow_run_repository=MagicMock(return_value=workflow_run_repo), + create_api_workflow_node_execution_repository=MagicMock(return_value=node_repo), + ) + monkeypatch.setattr(service_module, "DifyAPIRepositoryFactory", factory) + monkeypatch.setattr(service_module.MessageGenerator, "get_response_topic", MagicMock(return_value=topic)) + monkeypatch.setattr(service_module, "_load_resumption_context", MagicMock(return_value=None)) + monkeypatch.setattr(service_module, "_resolve_task_id", MagicMock(return_value="task-1")) + snapshot_builder = MagicMock(return_value=[{"event": StreamEvent.WORKFLOW_FINISHED.value}]) + monkeypatch.setattr(service_module, "_build_snapshot_events", snapshot_builder) + buffer_state = BufferState( + queue=queue.Queue(), + stop_event=Event(), + done_event=Event(), + task_id_ready=Event(), + task_id_hint="task-1", + ) + monkeypatch.setattr(service_module, "_start_buffering", MagicMock(return_value=buffer_state)) + + # Act + events = list( + build_workflow_event_stream( + app_mode=AppMode.WORKFLOW, + workflow_run=workflow_run, + tenant_id="tenant-1", + app_id="app-1", + session_maker=MagicMock(), + ) + ) + + # Assert + assert events[0] == StreamEvent.PING.value + assert snapshot_builder.call_args.kwargs["pause_entity"] is None diff --git a/api/tests/unit_tests/tasks/test_dataset_indexing_task.py b/api/tests/unit_tests/tasks/test_dataset_indexing_task.py index 0b189ebae2..34e474c921 100644 --- a/api/tests/unit_tests/tasks/test_dataset_indexing_task.py +++ b/api/tests/unit_tests/tasks/test_dataset_indexing_task.py @@ -10,6 +10,8 @@ This module tests the document indexing task functionality including: """ import uuid +from contextlib import nullcontext +from types import SimpleNamespace from unittest.mock import MagicMock, Mock, patch import pytest @@ -1113,13 +1115,17 @@ class TestAdvancedScenarios: _document_indexing_with_tenant_queue(tenant_id, dataset_id, document_ids, mock_task) # Assert - # Verify delete was called to clean up task key - mock_redis.delete.assert_called_once() + expected_task_key = f"tenant_document_indexing_task:{tenant_id}" - # Verify the correct key was deleted (contains tenant_id and "document_indexing") - delete_call_args = mock_redis.delete.call_args[0][0] - assert tenant_id in delete_call_args - assert "document_indexing" in delete_call_args + # Verify the task key for this tenant was deleted (do not assert call count; fixtures may be shared). + mock_redis.delete.assert_any_call(expected_task_key) + + deleted_keys = [delete_call.args[0] for delete_call in mock_redis.delete.call_args_list if delete_call.args] + assert expected_task_key in deleted_keys + + deleted_task_key = next(key for key in deleted_keys if key == expected_task_key) + assert tenant_id in deleted_task_key + assert "document_indexing" in deleted_task_key def test_billing_disabled_skips_limit_checks( self, dataset_id, document_ids, mock_db_session, mock_dataset, mock_indexing_runner, mock_feature_service @@ -1510,3 +1516,475 @@ class TestRobustness: # Verify the exception message assert "Feature service" in str(exc_info.value) or isinstance(exc_info.value, Exception) + + +class _SessionContext: + def __init__(self, session: MagicMock) -> None: + self._session = session + + def __enter__(self) -> MagicMock: + return self._session + + def __exit__(self, exc_type, exc, tb) -> None: # type: ignore[override] + return None + + +class TestDocumentIndexingTaskSummaryFlow: + """Additional coverage for summary and tenant queue branches.""" + + def test_should_return_when_dataset_missing(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test early return when dataset does not exist.""" + # Arrange + session = MagicMock() + dataset_query = MagicMock() + dataset_query.where.return_value = dataset_query + dataset_query.first.return_value = None + session.query.side_effect = lambda model: dataset_query + + create_session_mock = MagicMock(return_value=_SessionContext(session)) + monkeypatch.setattr("tasks.document_indexing_task.session_factory.create_session", create_session_mock) + features_mock = MagicMock() + monkeypatch.setattr("tasks.document_indexing_task.FeatureService.get_features", features_mock) + + # Act + _document_indexing("dataset-1", ["doc-1"]) + + # Assert + features_mock.assert_not_called() + + def test_should_mark_documents_error_when_batch_upload_limit_exceeded( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Test batch upload limit triggers error handling.""" + # Arrange + dataset = SimpleNamespace(id="dataset-1", tenant_id="tenant-1") + document = SimpleNamespace(id="doc-1", indexing_status=None, error=None, stopped_at=None) + + dataset_query = MagicMock() + dataset_query.where.return_value = dataset_query + dataset_query.first.return_value = dataset + + document_query = MagicMock() + document_query.where.return_value = document_query + document_query.first.return_value = document + + session = MagicMock() + session.query.side_effect = lambda model: dataset_query if model is Dataset else document_query + + monkeypatch.setattr( + "tasks.document_indexing_task.session_factory.create_session", + MagicMock(return_value=_SessionContext(session)), + ) + + features = SimpleNamespace( + billing=SimpleNamespace( + enabled=True, + subscription=SimpleNamespace(plan=CloudPlan.PROFESSIONAL), + ), + vector_space=SimpleNamespace(limit=0, size=0), + ) + monkeypatch.setattr( + "tasks.document_indexing_task.FeatureService.get_features", MagicMock(return_value=features) + ) + monkeypatch.setattr("tasks.document_indexing_task.dify_config.BATCH_UPLOAD_LIMIT", "1") + + # Act + _document_indexing("dataset-1", ["doc-1", "doc-2"]) + + # Assert + assert document.indexing_status == "error" + assert "batch upload limit" in document.error + session.commit.assert_called_once() + + def test_should_queue_summary_generation_for_completed_documents(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test summary generation is queued for eligible documents.""" + # Arrange + dataset = SimpleNamespace( + id="dataset-1", + tenant_id="tenant-1", + indexing_technique="high_quality", + summary_index_setting={"enable": True}, + ) + + doc_eligible = SimpleNamespace( + id="doc-1", + indexing_status="completed", + doc_form="text", + need_summary=True, + ) + doc_skip_form = SimpleNamespace( + id="doc-2", + indexing_status="completed", + doc_form="qa_model", + need_summary=True, + ) + doc_skip_status = SimpleNamespace( + id="doc-3", + indexing_status="processing", + doc_form="text", + need_summary=True, + ) + + dataset_query = MagicMock() + dataset_query.where.return_value = dataset_query + dataset_query.first.return_value = dataset + + phase1_docs = [SimpleNamespace(id="doc-1"), SimpleNamespace(id="doc-2"), SimpleNamespace(id="doc-3")] + phase1_document_query = MagicMock() + phase1_document_query.where.return_value = phase1_document_query + phase1_document_query.all.return_value = phase1_docs + + summary_document_query = MagicMock() + summary_document_query.where.return_value = summary_document_query + summary_document_query.all.return_value = [doc_eligible, doc_skip_form, doc_skip_status] + + session1 = MagicMock() + session2 = MagicMock() + session2.begin.return_value = nullcontext() + session3 = MagicMock() + + session1.query.side_effect = lambda model: dataset_query + session2.query.side_effect = lambda model: phase1_document_query + session3.query.side_effect = lambda model: summary_document_query if model is Document else dataset_query + + create_session_mock = MagicMock( + side_effect=[_SessionContext(session1), _SessionContext(session2), _SessionContext(session3)] + ) + monkeypatch.setattr("tasks.document_indexing_task.session_factory.create_session", create_session_mock) + + features = SimpleNamespace( + billing=SimpleNamespace(enabled=False), + vector_space=SimpleNamespace(limit=0, size=0), + ) + monkeypatch.setattr( + "tasks.document_indexing_task.FeatureService.get_features", MagicMock(return_value=features) + ) + + indexing_runner = MagicMock() + monkeypatch.setattr("tasks.document_indexing_task.IndexingRunner", MagicMock(return_value=indexing_runner)) + delay_mock = MagicMock() + monkeypatch.setattr("tasks.document_indexing_task.generate_summary_index_task.delay", delay_mock) + + # Act + _document_indexing("dataset-1", ["doc-1", "doc-2", "doc-3"]) + + # Assert + delay_mock.assert_called_once_with("dataset-1", "doc-1", None) + + def test_should_continue_when_summary_queue_fails(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test summary queueing errors are swallowed.""" + # Arrange + dataset = SimpleNamespace( + id="dataset-1", + tenant_id="tenant-1", + indexing_technique="high_quality", + summary_index_setting={"enable": True}, + ) + + doc_eligible = SimpleNamespace( + id="doc-1", + indexing_status="completed", + doc_form="text", + need_summary=True, + ) + + dataset_query = MagicMock() + dataset_query.where.return_value = dataset_query + dataset_query.first.return_value = dataset + + phase1_query = MagicMock() + phase1_query.where.return_value = phase1_query + phase1_query.all.return_value = [SimpleNamespace(id="doc-1")] + + summary_query = MagicMock() + summary_query.where.return_value = summary_query + summary_query.all.return_value = [doc_eligible] + + session1 = MagicMock() + session2 = MagicMock() + session2.begin.return_value = nullcontext() + session3 = MagicMock() + session1.query.side_effect = lambda model: dataset_query + session2.query.side_effect = lambda model: phase1_query + session3.query.side_effect = lambda model: summary_query if model is Document else dataset_query + + monkeypatch.setattr( + "tasks.document_indexing_task.session_factory.create_session", + MagicMock(side_effect=[_SessionContext(session1), _SessionContext(session2), _SessionContext(session3)]), + ) + + features = SimpleNamespace( + billing=SimpleNamespace(enabled=False), + vector_space=SimpleNamespace(limit=0, size=0), + ) + monkeypatch.setattr( + "tasks.document_indexing_task.FeatureService.get_features", MagicMock(return_value=features) + ) + + indexing_runner = MagicMock() + monkeypatch.setattr("tasks.document_indexing_task.IndexingRunner", MagicMock(return_value=indexing_runner)) + delay_mock = MagicMock(side_effect=Exception("boom")) + monkeypatch.setattr("tasks.document_indexing_task.generate_summary_index_task.delay", delay_mock) + + # Act + _document_indexing("dataset-1", ["doc-1"]) + + # Assert + delay_mock.assert_called_once_with("dataset-1", "doc-1", None) + + def test_should_return_when_dataset_missing_after_indexing(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test early return when dataset is missing after indexing.""" + # Arrange + dataset = SimpleNamespace(id="dataset-1", tenant_id="tenant-1") + dataset_query = MagicMock() + dataset_query.where.return_value = dataset_query + dataset_query.first.side_effect = [dataset, None] + + document_query = MagicMock() + document_query.where.return_value = document_query + document_query.all.return_value = [SimpleNamespace(id="doc-1")] + + session1 = MagicMock() + session2 = MagicMock() + session2.begin.return_value = nullcontext() + session3 = MagicMock() + session1.query.side_effect = lambda model: dataset_query + session2.query.side_effect = lambda model: document_query + session3.query.side_effect = lambda model: dataset_query + + monkeypatch.setattr( + "tasks.document_indexing_task.session_factory.create_session", + MagicMock(side_effect=[_SessionContext(session1), _SessionContext(session2), _SessionContext(session3)]), + ) + + features = SimpleNamespace( + billing=SimpleNamespace(enabled=False), + vector_space=SimpleNamespace(limit=0, size=0), + ) + monkeypatch.setattr( + "tasks.document_indexing_task.FeatureService.get_features", MagicMock(return_value=features) + ) + monkeypatch.setattr("tasks.document_indexing_task.IndexingRunner", MagicMock(return_value=MagicMock())) + + # Act + _document_indexing("dataset-1", ["doc-1"]) + + # Assert + session3.query.assert_called() + + def test_should_skip_summary_when_not_high_quality(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test summary generation skipped when indexing_technique is not high_quality.""" + # Arrange + dataset = SimpleNamespace( + id="dataset-1", + tenant_id="tenant-1", + indexing_technique="economy", + summary_index_setting={"enable": True}, + ) + dataset_query = MagicMock() + dataset_query.where.return_value = dataset_query + dataset_query.first.return_value = dataset + + document_query = MagicMock() + document_query.where.return_value = document_query + document_query.all.return_value = [SimpleNamespace(id="doc-1")] + + session1 = MagicMock() + session2 = MagicMock() + session2.begin.return_value = nullcontext() + session3 = MagicMock() + session1.query.side_effect = lambda model: dataset_query + session2.query.side_effect = lambda model: document_query + session3.query.side_effect = lambda model: dataset_query + + monkeypatch.setattr( + "tasks.document_indexing_task.session_factory.create_session", + MagicMock(side_effect=[_SessionContext(session1), _SessionContext(session2), _SessionContext(session3)]), + ) + + features = SimpleNamespace( + billing=SimpleNamespace(enabled=False), + vector_space=SimpleNamespace(limit=0, size=0), + ) + monkeypatch.setattr( + "tasks.document_indexing_task.FeatureService.get_features", MagicMock(return_value=features) + ) + monkeypatch.setattr("tasks.document_indexing_task.IndexingRunner", MagicMock(return_value=MagicMock())) + + delay_mock = MagicMock() + monkeypatch.setattr("tasks.document_indexing_task.generate_summary_index_task.delay", delay_mock) + + # Act + _document_indexing("dataset-1", ["doc-1"]) + + # Assert + delay_mock.assert_not_called() + + def test_should_skip_summary_generation_when_indexing_paused(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test summary generation is skipped when indexing is paused.""" + # Arrange + dataset = SimpleNamespace(id="dataset-1", tenant_id="tenant-1") + dataset_query = MagicMock() + dataset_query.where.return_value = dataset_query + dataset_query.first.return_value = dataset + + document_query = MagicMock() + document_query.where.return_value = document_query + document_query.all.return_value = [SimpleNamespace(id="doc-1")] + + session1 = MagicMock() + session2 = MagicMock() + session2.begin.return_value = nullcontext() + session1.query.side_effect = lambda model: dataset_query + session2.query.side_effect = lambda model: document_query + + create_session_mock = MagicMock(side_effect=[_SessionContext(session1), _SessionContext(session2)]) + monkeypatch.setattr("tasks.document_indexing_task.session_factory.create_session", create_session_mock) + + features = SimpleNamespace( + billing=SimpleNamespace(enabled=False), + vector_space=SimpleNamespace(limit=0, size=0), + ) + monkeypatch.setattr( + "tasks.document_indexing_task.FeatureService.get_features", MagicMock(return_value=features) + ) + + runner = MagicMock() + runner.run.side_effect = DocumentIsPausedError("paused") + monkeypatch.setattr("tasks.document_indexing_task.IndexingRunner", MagicMock(return_value=runner)) + delay_mock = MagicMock() + monkeypatch.setattr("tasks.document_indexing_task.generate_summary_index_task.delay", delay_mock) + + # Act + _document_indexing("dataset-1", ["doc-1"]) + + # Assert + delay_mock.assert_not_called() + + def test_should_handle_indexing_runner_exception(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test generic indexing runner exception is handled.""" + # Arrange + dataset = SimpleNamespace(id="dataset-1", tenant_id="tenant-1") + dataset_query = MagicMock() + dataset_query.where.return_value = dataset_query + dataset_query.first.return_value = dataset + + document_query = MagicMock() + document_query.where.return_value = document_query + document_query.all.return_value = [SimpleNamespace(id="doc-1")] + + session1 = MagicMock() + session2 = MagicMock() + session2.begin.return_value = nullcontext() + session1.query.side_effect = lambda model: dataset_query + session2.query.side_effect = lambda model: document_query + + monkeypatch.setattr( + "tasks.document_indexing_task.session_factory.create_session", + MagicMock(side_effect=[_SessionContext(session1), _SessionContext(session2)]), + ) + + features = SimpleNamespace( + billing=SimpleNamespace(enabled=False), + vector_space=SimpleNamespace(limit=0, size=0), + ) + monkeypatch.setattr( + "tasks.document_indexing_task.FeatureService.get_features", MagicMock(return_value=features) + ) + + runner = MagicMock() + runner.run.side_effect = RuntimeError("boom") + monkeypatch.setattr("tasks.document_indexing_task.IndexingRunner", MagicMock(return_value=runner)) + + delay_mock = MagicMock() + monkeypatch.setattr("tasks.document_indexing_task.generate_summary_index_task.delay", delay_mock) + + # Act + _document_indexing("dataset-1", ["doc-1"]) + + # Assert + delay_mock.assert_not_called() + + def test_should_log_missing_document_entry_in_summary_list(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test falsey document entries are handled in summary iteration.""" + + # Arrange + class _FalseyDocument: + def __init__(self, doc_id: str) -> None: + self.id = doc_id + + def __bool__(self) -> bool: + return False + + dataset = SimpleNamespace( + id="dataset-1", + tenant_id="tenant-1", + indexing_technique="high_quality", + summary_index_setting={"enable": True}, + ) + dataset_query = MagicMock() + dataset_query.where.return_value = dataset_query + dataset_query.first.return_value = dataset + + phase1_query = MagicMock() + phase1_query.where.return_value = phase1_query + phase1_query.all.return_value = [SimpleNamespace(id="doc-1")] + + summary_query = MagicMock() + summary_query.where.return_value = summary_query + summary_query.all.return_value = [_FalseyDocument("missing-doc")] + + session1 = MagicMock() + session2 = MagicMock() + session2.begin.return_value = nullcontext() + session3 = MagicMock() + session1.query.side_effect = lambda model: dataset_query + session2.query.side_effect = lambda model: phase1_query + session3.query.side_effect = lambda model: summary_query if model is Document else dataset_query + + monkeypatch.setattr( + "tasks.document_indexing_task.session_factory.create_session", + MagicMock(side_effect=[_SessionContext(session1), _SessionContext(session2), _SessionContext(session3)]), + ) + + features = SimpleNamespace( + billing=SimpleNamespace(enabled=False), + vector_space=SimpleNamespace(limit=0, size=0), + ) + monkeypatch.setattr( + "tasks.document_indexing_task.FeatureService.get_features", MagicMock(return_value=features) + ) + monkeypatch.setattr("tasks.document_indexing_task.IndexingRunner", MagicMock(return_value=MagicMock())) + + delay_mock = MagicMock() + monkeypatch.setattr("tasks.document_indexing_task.generate_summary_index_task.delay", delay_mock) + + # Act + _document_indexing("dataset-1", ["doc-1"]) + + # Assert + delay_mock.assert_not_called() + + def test_normal_document_indexing_task_should_delegate(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test normal indexing task delegates to tenant queue handler.""" + # Arrange + handler = MagicMock() + monkeypatch.setattr("tasks.document_indexing_task._document_indexing_with_tenant_queue", handler) + + # Act + normal_document_indexing_task("tenant-1", "dataset-1", ["doc-1"]) + + # Assert + handler.assert_called_once_with("tenant-1", "dataset-1", ["doc-1"], normal_document_indexing_task) + + def test_priority_document_indexing_task_should_delegate(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test priority indexing task delegates to tenant queue handler.""" + # Arrange + handler = MagicMock() + monkeypatch.setattr("tasks.document_indexing_task._document_indexing_with_tenant_queue", handler) + + # Act + priority_document_indexing_task("tenant-1", "dataset-1", ["doc-1"]) + + # Assert + handler.assert_called_once_with("tenant-1", "dataset-1", ["doc-1"], priority_document_indexing_task) diff --git a/api/uv.lock b/api/uv.lock index 9381fabb40..d171483d37 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -53,23 +53,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/45/4a/064321452809dae953c1ed6e017504e72551a26b6f5708a5a80e4bf556ff/aiohttp-3.13.4.tar.gz", hash = "sha256:d97a6d09c66087890c2ab5d49069e1e570583f7ac0314ecf98294c1b6aaebd38", size = 7859748, upload-time = "2026-03-28T17:19:40.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/7e/cb94129302d78c46662b47f9897d642fd0b33bdfef4b73b20c6ced35aa4c/aiohttp-3.13.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8ea0c64d1bcbf201b285c2246c51a0c035ba3bbd306640007bc5844a3b4658c1", size = 760027, upload-time = "2026-03-28T17:15:33.022Z" }, - { url = "https://files.pythonhosted.org/packages/5e/cd/2db3c9397c3bd24216b203dd739945b04f8b87bb036c640da7ddb63c75ef/aiohttp-3.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6f742e1fa45c0ed522b00ede565e18f97e4cf8d1883a712ac42d0339dfb0cce7", size = 508325, upload-time = "2026-03-28T17:15:34.714Z" }, - { url = "https://files.pythonhosted.org/packages/36/a3/d28b2722ec13107f2e37a86b8a169897308bab6a3b9e071ecead9d67bd9b/aiohttp-3.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dcfb50ee25b3b7a1222a9123be1f9f89e56e67636b561441f0b304e25aaef8f", size = 502402, upload-time = "2026-03-28T17:15:36.409Z" }, - { url = "https://files.pythonhosted.org/packages/fa/d6/acd47b5f17c4430e555590990a4746efbcb2079909bb865516892bf85f37/aiohttp-3.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3262386c4ff370849863ea93b9ea60fd59c6cf56bf8f93beac625cf4d677c04d", size = 1771224, upload-time = "2026-03-28T17:15:38.223Z" }, - { url = "https://files.pythonhosted.org/packages/98/af/af6e20113ba6a48fd1cd9e5832c4851e7613ef50c7619acdaee6ec5f1aff/aiohttp-3.13.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:473bb5aa4218dd254e9ae4834f20e31f5a0083064ac0136a01a62ddbae2eaa42", size = 1731530, upload-time = "2026-03-28T17:15:39.988Z" }, - { url = "https://files.pythonhosted.org/packages/81/16/78a2f5d9c124ad05d5ce59a9af94214b6466c3491a25fb70760e98e9f762/aiohttp-3.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56423766399b4c77b965f6aaab6c9546617b8994a956821cc507d00b91d978c", size = 1827925, upload-time = "2026-03-28T17:15:41.944Z" }, - { url = "https://files.pythonhosted.org/packages/2a/1f/79acf0974ced805e0e70027389fccbb7d728e6f30fcac725fb1071e63075/aiohttp-3.13.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8af249343fafd5ad90366a16d230fc265cf1149f26075dc9fe93cfd7c7173942", size = 1923579, upload-time = "2026-03-28T17:15:44.071Z" }, - { url = "https://files.pythonhosted.org/packages/af/53/29f9e2054ea6900413f3b4c3eb9d8331f60678ec855f13ba8714c47fd48d/aiohttp-3.13.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bc0a5cf4f10ef5a2c94fdde488734b582a3a7a000b131263e27c9295bd682d9", size = 1767655, upload-time = "2026-03-28T17:15:45.911Z" }, - { url = "https://files.pythonhosted.org/packages/f3/57/462fe1d3da08109ba4aa8590e7aed57c059af2a7e80ec21f4bac5cfe1094/aiohttp-3.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5c7ff1028e3c9fc5123a865ce17df1cb6424d180c503b8517afbe89aa566e6be", size = 1630439, upload-time = "2026-03-28T17:15:48.11Z" }, - { url = "https://files.pythonhosted.org/packages/d7/4b/4813344aacdb8127263e3eec343d24e973421143826364fa9fc847f6283f/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ba5cf98b5dcb9bddd857da6713a503fa6d341043258ca823f0f5ab7ab4a94ee8", size = 1745557, upload-time = "2026-03-28T17:15:50.13Z" }, - { url = "https://files.pythonhosted.org/packages/d4/01/1ef1adae1454341ec50a789f03cfafe4c4ac9c003f6a64515ecd32fe4210/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d85965d3ba21ee4999e83e992fecb86c4614d6920e40705501c0a1f80a583c12", size = 1741796, upload-time = "2026-03-28T17:15:52.351Z" }, - { url = "https://files.pythonhosted.org/packages/22/04/8cdd99af988d2aa6922714d957d21383c559835cbd43fbf5a47ddf2e0f05/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:49f0b18a9b05d79f6f37ddd567695943fcefb834ef480f17a4211987302b2dc7", size = 1805312, upload-time = "2026-03-28T17:15:54.407Z" }, - { url = "https://files.pythonhosted.org/packages/fb/7f/b48d5577338d4b25bbdbae35c75dbfd0493cb8886dc586fbfb2e90862239/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7f78cb080c86fbf765920e5f1ef35af3f24ec4314d6675d0a21eaf41f6f2679c", size = 1621751, upload-time = "2026-03-28T17:15:56.564Z" }, - { url = "https://files.pythonhosted.org/packages/bc/89/4eecad8c1858e6d0893c05929e22343e0ebe3aec29a8a399c65c3cc38311/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:67a3ec705534a614b68bbf1c70efa777a21c3da3895d1c44510a41f5a7ae0453", size = 1826073, upload-time = "2026-03-28T17:15:58.489Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5c/9dc8293ed31b46c39c9c513ac7ca152b3c3d38e0ea111a530ad12001b827/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6630ec917e85c5356b2295744c8a97d40f007f96a1c76bf1928dc2e27465393", size = 1760083, upload-time = "2026-03-28T17:16:00.677Z" }, - { url = "https://files.pythonhosted.org/packages/1e/19/8bbf6a4994205d96831f97b7d21a0feed120136e6267b5b22d229c6dc4dc/aiohttp-3.13.4-cp311-cp311-win32.whl", hash = "sha256:54049021bc626f53a5394c29e8c444f726ee5a14b6e89e0ad118315b1f90f5e3", size = 439690, upload-time = "2026-03-28T17:16:02.902Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f5/ac409ecd1007528d15c3e8c3a57d34f334c70d76cfb7128a28cffdebd4c1/aiohttp-3.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:c033f2bc964156030772d31cbf7e5defea181238ce1f87b9455b786de7d30145", size = 463824, upload-time = "2026-03-28T17:16:05.058Z" }, { url = "https://files.pythonhosted.org/packages/1e/bd/ede278648914cabbabfdf95e436679b5d4156e417896a9b9f4587169e376/aiohttp-3.13.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ee62d4471ce86b108b19c3364db4b91180d13fe3510144872d6bad5401957360", size = 752158, upload-time = "2026-03-28T17:16:06.901Z" }, { url = "https://files.pythonhosted.org/packages/90/de/581c053253c07b480b03785196ca5335e3c606a37dc73e95f6527f1591fe/aiohttp-3.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c0fd8f41b54b58636402eb493afd512c23580456f022c1ba2db0f810c959ed0d", size = 501037, upload-time = "2026-03-28T17:16:08.82Z" }, { url = "https://files.pythonhosted.org/packages/fa/f9/a5ede193c08f13cc42c0a5b50d1e246ecee9115e4cf6e900d8dbd8fd6acb/aiohttp-3.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4baa48ce49efd82d6b1a0be12d6a36b35e5594d1dd42f8bfba96ea9f8678b88c", size = 501556, upload-time = "2026-03-28T17:16:10.63Z" }, @@ -1586,7 +1569,7 @@ dev = [ { name = "lxml-stubs", specifier = "~=0.5.1" }, { name = "mypy", specifier = "~=1.19.1" }, { name = "pandas-stubs", specifier = "~=3.0.0" }, - { name = "pyrefly", specifier = ">=0.57.1" }, + { name = "pyrefly", specifier = ">=0.59.1" }, { name = "pytest", specifier = "~=9.0.2" }, { name = "pytest-benchmark", specifier = "~=5.2.3" }, { name = "pytest-cov", specifier = "~=7.1.0" }, @@ -4839,18 +4822,19 @@ wheels = [ [[package]] name = "pyrefly" -version = "0.57.1" +version = "0.59.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/c1/c17211e5bbd2b90a24447484713da7cc2cee4e9455e57b87016ffc69d426/pyrefly-0.57.1.tar.gz", hash = "sha256:b05f6f5ee3a6a5d502ca19d84cb9ab62d67f05083819964a48c1510f2993efc6", size = 5310800, upload-time = "2026-03-18T18:42:35.614Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/ce/7882c2af92b2ff6505fcd3430eff8048ece6c6254cc90bdc76ecee12dfab/pyrefly-0.59.1.tar.gz", hash = "sha256:bf1675b0c38d45df2c8f8618cbdfa261a1b92430d9d31eba16e0282b551e210f", size = 5475432, upload-time = "2026-04-01T22:04:04.11Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/58/8af37856c8d45b365ece635a6728a14b0356b08d1ff1ac601d7120def1e0/pyrefly-0.57.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:91974bfbe951eebf5a7bc959c1f3921f0371c789cad84761511d695e9ab2265f", size = 12681847, upload-time = "2026-03-18T18:42:10.963Z" }, - { url = "https://files.pythonhosted.org/packages/5f/d7/fae6dd9d0355fc5b8df7793f1423b7433ca8e10b698ea934c35f0e4e6522/pyrefly-0.57.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:808087298537c70f5e7cdccb5bbaad482e7e056e947c0adf00fb612cbace9fdc", size = 12219634, upload-time = "2026-03-18T18:42:13.469Z" }, - { url = "https://files.pythonhosted.org/packages/29/8f/9511ae460f0690e837b9ba0f7e5e192079e16ff9a9ba8a272450e81f11f8/pyrefly-0.57.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b01f454fa5539e070c0cba17ddec46b3d2107d571d519bd8eca8f3142ba02a6", size = 34947757, upload-time = "2026-03-18T18:42:17.152Z" }, - { url = "https://files.pythonhosted.org/packages/07/43/f053bf9c65218f70e6a49561e9942c7233f8c3e4da8d42e5fe2aae50b3d2/pyrefly-0.57.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02ad59ea722191f51635f23e37574662116b82ca9d814529f7cb5528f041f381", size = 37621018, upload-time = "2026-03-18T18:42:20.79Z" }, - { url = "https://files.pythonhosted.org/packages/0e/76/9cea46de01665bbc125e4f215340c9365c8d56cda6198ff238a563ea8e75/pyrefly-0.57.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54bc0afe56776145e37733ff763e7e9679ee8a76c467b617dc3f227d4124a9e2", size = 40203649, upload-time = "2026-03-18T18:42:24.519Z" }, - { url = "https://files.pythonhosted.org/packages/fd/8b/2fb4a96d75e2a57df698a43e2970e441ba2704e3906cdc0386a055daa05a/pyrefly-0.57.1-py3-none-win32.whl", hash = "sha256:468e5839144b25bb0dce839bfc5fd879c9f38e68ebf5de561f30bed9ae19d8ca", size = 11732953, upload-time = "2026-03-18T18:42:27.379Z" }, - { url = "https://files.pythonhosted.org/packages/13/5a/4a197910fe2e9b102b15ae5e7687c45b7b5981275a11a564b41e185dd907/pyrefly-0.57.1-py3-none-win_amd64.whl", hash = "sha256:46db9c97093673c4fb7fab96d610e74d140661d54688a92d8e75ad885a56c141", size = 12537319, upload-time = "2026-03-18T18:42:30.196Z" }, - { url = "https://files.pythonhosted.org/packages/b5/c6/bc442874be1d9b63da1f9debb4f04b7d0c590a8dc4091921f3c288207242/pyrefly-0.57.1-py3-none-win_arm64.whl", hash = "sha256:feb1bbe3b0d8d5a70121dcdf1476e6a99cc056a26a49379a156f040729244dcb", size = 12013455, upload-time = "2026-03-18T18:42:32.928Z" }, + { url = "https://files.pythonhosted.org/packages/d0/10/04a0e05b08fc855b6fe38c3df549925fc3c2c6e750506870de7335d3e1f7/pyrefly-0.59.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:390db3cd14aa7e0268e847b60cd9ee18b04273eddfa38cf341ed3bb43f3fef2a", size = 12868133, upload-time = "2026-04-01T22:03:39.436Z" }, + { url = "https://files.pythonhosted.org/packages/c7/78/fa7be227c3e3fcacee501c1562278dd026186ffd1b5b5beb51d3941a3aed/pyrefly-0.59.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d246d417b6187c1650d7f855f61c68fbfd6d6155dc846d4e4d273a3e6b5175cb", size = 12379325, upload-time = "2026-04-01T22:03:42.046Z" }, + { url = "https://files.pythonhosted.org/packages/bb/13/6828ce1c98171b5f8388f33c4b0b9ea2ab8c49abe0ef8d793c31e30a05cb/pyrefly-0.59.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:575ac67b04412dc651a7143d27e38a40fbdd3c831c714d5520d0e9d4c8631ab4", size = 35826408, upload-time = "2026-04-01T22:03:45.067Z" }, + { url = "https://files.pythonhosted.org/packages/23/56/79ed8ece9a7ecad0113c394a06a084107db3ad8f1fefe19e7ded43c51245/pyrefly-0.59.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:062e6262ce1064d59dcad81ac0499bb7a3ad501e9bc8a677a50dc630ff0bf862", size = 38532699, upload-time = "2026-04-01T22:03:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/18/7d/ecc025e0f0e3f295b497f523cc19cefaa39e57abede8fc353d29445d174b/pyrefly-0.59.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ef4247f9e6f734feb93e1f2b75335b943629956e509f545cc9cdcccd76dd20", size = 36743570, upload-time = "2026-04-01T22:03:51.362Z" }, + { url = "https://files.pythonhosted.org/packages/2f/03/b1ce882ebcb87c673165c00451fbe4df17bf96ccfde18c75880dc87c5f5e/pyrefly-0.59.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59a2d01723b84d042f4fa6ec871ffd52d0a7e83b0ea791c2e0bb0ff750abce56", size = 41236246, upload-time = "2026-04-01T22:03:54.361Z" }, + { url = "https://files.pythonhosted.org/packages/17/af/5e9c7afd510e7dd64a2204be0ed39e804089cbc4338675a28615c7176acb/pyrefly-0.59.1-py3-none-win32.whl", hash = "sha256:4ea70c780848f8376411e787643ae5d2d09da8a829362332b7b26d15ebcbaf56", size = 11884747, upload-time = "2026-04-01T22:03:56.776Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c1/7db1077627453fd1068f0761f059a9512645c00c4c20acfb9f0c24ac02ec/pyrefly-0.59.1-py3-none-win_amd64.whl", hash = "sha256:67e6a08cfd129a0d2788d5e40a627f9860e0fe91a876238d93d5c63ff4af68ae", size = 12720608, upload-time = "2026-04-01T22:03:59.252Z" }, + { url = "https://files.pythonhosted.org/packages/07/16/4bb6e5fce5a9cf0992932d9435d964c33e507aaaf96fdfbb1be493078a4a/pyrefly-0.59.1-py3-none-win_arm64.whl", hash = "sha256:01179cb215cf079e8223a064f61a074f7079aa97ea705cbbc68af3d6713afd15", size = 12223158, upload-time = "2026-04-01T22:04:01.869Z" }, ] [[package]] diff --git a/package.json b/package.json index 48c3acef02..ce3180214b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,6 @@ "prepare": "vp config" }, "devDependencies": { - "taze": "catalog:", "vite-plus": "catalog:" }, "engines": { diff --git a/web/app/components/base/icons/assets/public/avatar/robot.svg b/packages/iconify-collections/assets/public/avatar/robot.svg similarity index 100% rename from web/app/components/base/icons/assets/public/avatar/robot.svg rename to packages/iconify-collections/assets/public/avatar/robot.svg diff --git a/web/app/components/base/icons/assets/public/avatar/user.svg b/packages/iconify-collections/assets/public/avatar/user.svg similarity index 100% rename from web/app/components/base/icons/assets/public/avatar/user.svg rename to packages/iconify-collections/assets/public/avatar/user.svg diff --git a/web/app/components/base/icons/assets/public/billing/ar-cube-1.svg b/packages/iconify-collections/assets/public/billing/ar-cube-1.svg similarity index 100% rename from web/app/components/base/icons/assets/public/billing/ar-cube-1.svg rename to packages/iconify-collections/assets/public/billing/ar-cube-1.svg diff --git a/web/app/components/base/icons/assets/public/billing/asterisk.svg b/packages/iconify-collections/assets/public/billing/asterisk.svg similarity index 100% rename from web/app/components/base/icons/assets/public/billing/asterisk.svg rename to packages/iconify-collections/assets/public/billing/asterisk.svg diff --git a/web/app/components/base/icons/assets/public/billing/aws-marketplace-dark.svg b/packages/iconify-collections/assets/public/billing/aws-marketplace-dark.svg similarity index 100% rename from web/app/components/base/icons/assets/public/billing/aws-marketplace-dark.svg rename to packages/iconify-collections/assets/public/billing/aws-marketplace-dark.svg diff --git a/web/app/components/base/icons/assets/public/billing/aws-marketplace-light.svg b/packages/iconify-collections/assets/public/billing/aws-marketplace-light.svg similarity index 100% rename from web/app/components/base/icons/assets/public/billing/aws-marketplace-light.svg rename to packages/iconify-collections/assets/public/billing/aws-marketplace-light.svg diff --git a/web/app/components/base/icons/assets/public/billing/azure.svg b/packages/iconify-collections/assets/public/billing/azure.svg similarity index 100% rename from web/app/components/base/icons/assets/public/billing/azure.svg rename to packages/iconify-collections/assets/public/billing/azure.svg diff --git a/web/app/components/base/icons/assets/public/billing/buildings.svg b/packages/iconify-collections/assets/public/billing/buildings.svg similarity index 100% rename from web/app/components/base/icons/assets/public/billing/buildings.svg rename to packages/iconify-collections/assets/public/billing/buildings.svg diff --git a/web/app/components/base/icons/assets/public/billing/diamond.svg b/packages/iconify-collections/assets/public/billing/diamond.svg similarity index 100% rename from web/app/components/base/icons/assets/public/billing/diamond.svg rename to packages/iconify-collections/assets/public/billing/diamond.svg diff --git a/web/app/components/base/icons/assets/public/billing/google-cloud.svg b/packages/iconify-collections/assets/public/billing/google-cloud.svg similarity index 100% rename from web/app/components/base/icons/assets/public/billing/google-cloud.svg rename to packages/iconify-collections/assets/public/billing/google-cloud.svg diff --git a/web/app/components/base/icons/assets/public/billing/group-2.svg b/packages/iconify-collections/assets/public/billing/group-2.svg similarity index 100% rename from web/app/components/base/icons/assets/public/billing/group-2.svg rename to packages/iconify-collections/assets/public/billing/group-2.svg diff --git a/web/app/components/base/icons/assets/public/billing/keyframe.svg b/packages/iconify-collections/assets/public/billing/keyframe.svg similarity index 100% rename from web/app/components/base/icons/assets/public/billing/keyframe.svg rename to packages/iconify-collections/assets/public/billing/keyframe.svg diff --git a/web/app/components/base/icons/assets/public/billing/sparkles-soft.svg b/packages/iconify-collections/assets/public/billing/sparkles-soft.svg similarity index 100% rename from web/app/components/base/icons/assets/public/billing/sparkles-soft.svg rename to packages/iconify-collections/assets/public/billing/sparkles-soft.svg diff --git a/web/app/components/base/icons/assets/public/billing/sparkles.svg b/packages/iconify-collections/assets/public/billing/sparkles.svg similarity index 100% rename from web/app/components/base/icons/assets/public/billing/sparkles.svg rename to packages/iconify-collections/assets/public/billing/sparkles.svg diff --git a/web/app/components/base/icons/assets/public/common/d.svg b/packages/iconify-collections/assets/public/common/d.svg similarity index 100% rename from web/app/components/base/icons/assets/public/common/d.svg rename to packages/iconify-collections/assets/public/common/d.svg diff --git a/web/app/components/base/icons/assets/public/common/diagonal-dividing-line.svg b/packages/iconify-collections/assets/public/common/diagonal-dividing-line.svg similarity index 100% rename from web/app/components/base/icons/assets/public/common/diagonal-dividing-line.svg rename to packages/iconify-collections/assets/public/common/diagonal-dividing-line.svg diff --git a/web/app/components/base/icons/assets/public/common/dify.svg b/packages/iconify-collections/assets/public/common/dify.svg similarity index 100% rename from web/app/components/base/icons/assets/public/common/dify.svg rename to packages/iconify-collections/assets/public/common/dify.svg diff --git a/web/app/components/base/icons/assets/public/common/gdpr.svg b/packages/iconify-collections/assets/public/common/gdpr.svg similarity index 100% rename from web/app/components/base/icons/assets/public/common/gdpr.svg rename to packages/iconify-collections/assets/public/common/gdpr.svg diff --git a/web/app/components/base/icons/assets/public/common/github.svg b/packages/iconify-collections/assets/public/common/github.svg similarity index 100% rename from web/app/components/base/icons/assets/public/common/github.svg rename to packages/iconify-collections/assets/public/common/github.svg diff --git a/web/app/components/base/icons/assets/public/common/highlight.svg b/packages/iconify-collections/assets/public/common/highlight.svg similarity index 100% rename from web/app/components/base/icons/assets/public/common/highlight.svg rename to packages/iconify-collections/assets/public/common/highlight.svg diff --git a/web/app/components/base/icons/assets/public/common/iso.svg b/packages/iconify-collections/assets/public/common/iso.svg similarity index 100% rename from web/app/components/base/icons/assets/public/common/iso.svg rename to packages/iconify-collections/assets/public/common/iso.svg diff --git a/web/app/components/base/icons/assets/public/common/line-3.svg b/packages/iconify-collections/assets/public/common/line-3.svg similarity index 100% rename from web/app/components/base/icons/assets/public/common/line-3.svg rename to packages/iconify-collections/assets/public/common/line-3.svg diff --git a/web/app/components/base/icons/assets/public/common/lock.svg b/packages/iconify-collections/assets/public/common/lock.svg similarity index 100% rename from web/app/components/base/icons/assets/public/common/lock.svg rename to packages/iconify-collections/assets/public/common/lock.svg diff --git a/web/app/components/base/icons/assets/public/common/message-chat-square.svg b/packages/iconify-collections/assets/public/common/message-chat-square.svg similarity index 100% rename from web/app/components/base/icons/assets/public/common/message-chat-square.svg rename to packages/iconify-collections/assets/public/common/message-chat-square.svg diff --git a/web/app/components/base/icons/assets/public/common/multi-path-retrieval.svg b/packages/iconify-collections/assets/public/common/multi-path-retrieval.svg similarity index 100% rename from web/app/components/base/icons/assets/public/common/multi-path-retrieval.svg rename to packages/iconify-collections/assets/public/common/multi-path-retrieval.svg diff --git a/web/app/components/base/icons/assets/public/common/n-to-1-retrieval.svg b/packages/iconify-collections/assets/public/common/n-to-1-retrieval.svg similarity index 100% rename from web/app/components/base/icons/assets/public/common/n-to-1-retrieval.svg rename to packages/iconify-collections/assets/public/common/n-to-1-retrieval.svg diff --git a/web/app/components/base/icons/assets/public/common/notion.svg b/packages/iconify-collections/assets/public/common/notion.svg similarity index 100% rename from web/app/components/base/icons/assets/public/common/notion.svg rename to packages/iconify-collections/assets/public/common/notion.svg diff --git a/web/app/components/base/icons/assets/public/common/soc2.svg b/packages/iconify-collections/assets/public/common/soc2.svg similarity index 100% rename from web/app/components/base/icons/assets/public/common/soc2.svg rename to packages/iconify-collections/assets/public/common/soc2.svg diff --git a/web/app/components/base/icons/assets/public/common/sparkles-soft-accent.svg b/packages/iconify-collections/assets/public/common/sparkles-soft-accent.svg similarity index 100% rename from web/app/components/base/icons/assets/public/common/sparkles-soft-accent.svg rename to packages/iconify-collections/assets/public/common/sparkles-soft-accent.svg diff --git a/web/app/components/base/icons/assets/public/common/sparkles-soft.svg b/packages/iconify-collections/assets/public/common/sparkles-soft.svg similarity index 100% rename from web/app/components/base/icons/assets/public/common/sparkles-soft.svg rename to packages/iconify-collections/assets/public/common/sparkles-soft.svg diff --git a/web/app/components/base/icons/assets/public/education/triangle.svg b/packages/iconify-collections/assets/public/education/triangle.svg similarity index 100% rename from web/app/components/base/icons/assets/public/education/triangle.svg rename to packages/iconify-collections/assets/public/education/triangle.svg diff --git a/web/app/components/base/icons/assets/public/files/csv.svg b/packages/iconify-collections/assets/public/files/csv.svg similarity index 100% rename from web/app/components/base/icons/assets/public/files/csv.svg rename to packages/iconify-collections/assets/public/files/csv.svg diff --git a/web/app/components/base/icons/assets/public/files/doc.svg b/packages/iconify-collections/assets/public/files/doc.svg similarity index 100% rename from web/app/components/base/icons/assets/public/files/doc.svg rename to packages/iconify-collections/assets/public/files/doc.svg diff --git a/web/app/components/base/icons/assets/public/files/docx.svg b/packages/iconify-collections/assets/public/files/docx.svg similarity index 100% rename from web/app/components/base/icons/assets/public/files/docx.svg rename to packages/iconify-collections/assets/public/files/docx.svg diff --git a/web/app/components/base/icons/assets/public/files/html.svg b/packages/iconify-collections/assets/public/files/html.svg similarity index 100% rename from web/app/components/base/icons/assets/public/files/html.svg rename to packages/iconify-collections/assets/public/files/html.svg diff --git a/web/app/components/base/icons/assets/public/files/json.svg b/packages/iconify-collections/assets/public/files/json.svg similarity index 100% rename from web/app/components/base/icons/assets/public/files/json.svg rename to packages/iconify-collections/assets/public/files/json.svg diff --git a/web/app/components/base/icons/assets/public/files/md.svg b/packages/iconify-collections/assets/public/files/md.svg similarity index 100% rename from web/app/components/base/icons/assets/public/files/md.svg rename to packages/iconify-collections/assets/public/files/md.svg diff --git a/web/app/components/base/icons/assets/public/files/pdf.svg b/packages/iconify-collections/assets/public/files/pdf.svg similarity index 100% rename from web/app/components/base/icons/assets/public/files/pdf.svg rename to packages/iconify-collections/assets/public/files/pdf.svg diff --git a/web/app/components/base/icons/assets/public/files/txt.svg b/packages/iconify-collections/assets/public/files/txt.svg similarity index 100% rename from web/app/components/base/icons/assets/public/files/txt.svg rename to packages/iconify-collections/assets/public/files/txt.svg diff --git a/web/app/components/base/icons/assets/public/files/unknown.svg b/packages/iconify-collections/assets/public/files/unknown.svg similarity index 100% rename from web/app/components/base/icons/assets/public/files/unknown.svg rename to packages/iconify-collections/assets/public/files/unknown.svg diff --git a/web/app/components/base/icons/assets/public/files/xlsx.svg b/packages/iconify-collections/assets/public/files/xlsx.svg similarity index 100% rename from web/app/components/base/icons/assets/public/files/xlsx.svg rename to packages/iconify-collections/assets/public/files/xlsx.svg diff --git a/web/app/components/base/icons/assets/public/files/yaml.svg b/packages/iconify-collections/assets/public/files/yaml.svg similarity index 100% rename from web/app/components/base/icons/assets/public/files/yaml.svg rename to packages/iconify-collections/assets/public/files/yaml.svg diff --git a/web/app/components/base/icons/assets/public/knowledge/dataset-card/external-knowledge-base.svg b/packages/iconify-collections/assets/public/knowledge/dataset-card/external-knowledge-base.svg similarity index 100% rename from web/app/components/base/icons/assets/public/knowledge/dataset-card/external-knowledge-base.svg rename to packages/iconify-collections/assets/public/knowledge/dataset-card/external-knowledge-base.svg diff --git a/web/app/components/base/icons/assets/public/knowledge/dataset-card/general.svg b/packages/iconify-collections/assets/public/knowledge/dataset-card/general.svg similarity index 100% rename from web/app/components/base/icons/assets/public/knowledge/dataset-card/general.svg rename to packages/iconify-collections/assets/public/knowledge/dataset-card/general.svg diff --git a/web/app/components/base/icons/assets/public/knowledge/dataset-card/graph.svg b/packages/iconify-collections/assets/public/knowledge/dataset-card/graph.svg similarity index 100% rename from web/app/components/base/icons/assets/public/knowledge/dataset-card/graph.svg rename to packages/iconify-collections/assets/public/knowledge/dataset-card/graph.svg diff --git a/web/app/components/base/icons/assets/public/knowledge/dataset-card/parent-child.svg b/packages/iconify-collections/assets/public/knowledge/dataset-card/parent-child.svg similarity index 100% rename from web/app/components/base/icons/assets/public/knowledge/dataset-card/parent-child.svg rename to packages/iconify-collections/assets/public/knowledge/dataset-card/parent-child.svg diff --git a/web/app/components/base/icons/assets/public/knowledge/dataset-card/qa.svg b/packages/iconify-collections/assets/public/knowledge/dataset-card/qa.svg similarity index 100% rename from web/app/components/base/icons/assets/public/knowledge/dataset-card/qa.svg rename to packages/iconify-collections/assets/public/knowledge/dataset-card/qa.svg diff --git a/web/app/components/base/icons/assets/public/knowledge/file.svg b/packages/iconify-collections/assets/public/knowledge/file.svg similarity index 100% rename from web/app/components/base/icons/assets/public/knowledge/file.svg rename to packages/iconify-collections/assets/public/knowledge/file.svg diff --git a/web/app/components/base/icons/assets/public/knowledge/online-drive/buckets-blue.svg b/packages/iconify-collections/assets/public/knowledge/online-drive/buckets-blue.svg similarity index 100% rename from web/app/components/base/icons/assets/public/knowledge/online-drive/buckets-blue.svg rename to packages/iconify-collections/assets/public/knowledge/online-drive/buckets-blue.svg diff --git a/web/app/components/base/icons/assets/public/knowledge/online-drive/buckets-gray.svg b/packages/iconify-collections/assets/public/knowledge/online-drive/buckets-gray.svg similarity index 100% rename from web/app/components/base/icons/assets/public/knowledge/online-drive/buckets-gray.svg rename to packages/iconify-collections/assets/public/knowledge/online-drive/buckets-gray.svg diff --git a/web/app/components/base/icons/assets/public/knowledge/online-drive/folder.svg b/packages/iconify-collections/assets/public/knowledge/online-drive/folder.svg similarity index 100% rename from web/app/components/base/icons/assets/public/knowledge/online-drive/folder.svg rename to packages/iconify-collections/assets/public/knowledge/online-drive/folder.svg diff --git a/web/app/components/base/icons/assets/public/knowledge/option-card-effect-blue-light.svg b/packages/iconify-collections/assets/public/knowledge/option-card-effect-blue-light.svg similarity index 100% rename from web/app/components/base/icons/assets/public/knowledge/option-card-effect-blue-light.svg rename to packages/iconify-collections/assets/public/knowledge/option-card-effect-blue-light.svg diff --git a/web/app/components/base/icons/assets/public/knowledge/option-card-effect-blue.svg b/packages/iconify-collections/assets/public/knowledge/option-card-effect-blue.svg similarity index 100% rename from web/app/components/base/icons/assets/public/knowledge/option-card-effect-blue.svg rename to packages/iconify-collections/assets/public/knowledge/option-card-effect-blue.svg diff --git a/web/app/components/base/icons/assets/public/knowledge/option-card-effect-orange.svg b/packages/iconify-collections/assets/public/knowledge/option-card-effect-orange.svg similarity index 100% rename from web/app/components/base/icons/assets/public/knowledge/option-card-effect-orange.svg rename to packages/iconify-collections/assets/public/knowledge/option-card-effect-orange.svg diff --git a/web/app/components/base/icons/assets/public/knowledge/option-card-effect-purple.svg b/packages/iconify-collections/assets/public/knowledge/option-card-effect-purple.svg similarity index 100% rename from web/app/components/base/icons/assets/public/knowledge/option-card-effect-purple.svg rename to packages/iconify-collections/assets/public/knowledge/option-card-effect-purple.svg diff --git a/web/app/components/base/icons/assets/public/knowledge/option-card-effect-teal.svg b/packages/iconify-collections/assets/public/knowledge/option-card-effect-teal.svg similarity index 100% rename from web/app/components/base/icons/assets/public/knowledge/option-card-effect-teal.svg rename to packages/iconify-collections/assets/public/knowledge/option-card-effect-teal.svg diff --git a/web/app/components/base/icons/assets/public/knowledge/selection-mod.svg b/packages/iconify-collections/assets/public/knowledge/selection-mod.svg similarity index 100% rename from web/app/components/base/icons/assets/public/knowledge/selection-mod.svg rename to packages/iconify-collections/assets/public/knowledge/selection-mod.svg diff --git a/web/app/components/base/icons/assets/public/knowledge/watercrawl.svg b/packages/iconify-collections/assets/public/knowledge/watercrawl.svg similarity index 100% rename from web/app/components/base/icons/assets/public/knowledge/watercrawl.svg rename to packages/iconify-collections/assets/public/knowledge/watercrawl.svg diff --git a/web/app/components/base/icons/assets/public/llm/Anthropic-dark.svg b/packages/iconify-collections/assets/public/llm/Anthropic-dark.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/Anthropic-dark.svg rename to packages/iconify-collections/assets/public/llm/Anthropic-dark.svg diff --git a/web/app/components/base/icons/assets/public/llm/Anthropic-light.svg b/packages/iconify-collections/assets/public/llm/Anthropic-light.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/Anthropic-light.svg rename to packages/iconify-collections/assets/public/llm/Anthropic-light.svg diff --git a/web/app/components/base/icons/assets/public/llm/Tongyi.svg b/packages/iconify-collections/assets/public/llm/Tongyi.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/Tongyi.svg rename to packages/iconify-collections/assets/public/llm/Tongyi.svg diff --git a/web/app/components/base/icons/assets/public/llm/anthropic-short-light.svg b/packages/iconify-collections/assets/public/llm/anthropic-short-light.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/anthropic-short-light.svg rename to packages/iconify-collections/assets/public/llm/anthropic-short-light.svg diff --git a/web/app/components/base/icons/assets/public/llm/anthropic-text.svg b/packages/iconify-collections/assets/public/llm/anthropic-text.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/anthropic-text.svg rename to packages/iconify-collections/assets/public/llm/anthropic-text.svg diff --git a/web/app/components/base/icons/assets/public/llm/anthropic.svg b/packages/iconify-collections/assets/public/llm/anthropic.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/anthropic.svg rename to packages/iconify-collections/assets/public/llm/anthropic.svg diff --git a/web/app/components/base/icons/assets/public/llm/azure-openai-service-text.svg b/packages/iconify-collections/assets/public/llm/azure-openai-service-text.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/azure-openai-service-text.svg rename to packages/iconify-collections/assets/public/llm/azure-openai-service-text.svg diff --git a/web/app/components/base/icons/assets/public/llm/azure-openai-service.svg b/packages/iconify-collections/assets/public/llm/azure-openai-service.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/azure-openai-service.svg rename to packages/iconify-collections/assets/public/llm/azure-openai-service.svg diff --git a/web/app/components/base/icons/assets/public/llm/azureai-text.svg b/packages/iconify-collections/assets/public/llm/azureai-text.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/azureai-text.svg rename to packages/iconify-collections/assets/public/llm/azureai-text.svg diff --git a/web/app/components/base/icons/assets/public/llm/azureai.svg b/packages/iconify-collections/assets/public/llm/azureai.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/azureai.svg rename to packages/iconify-collections/assets/public/llm/azureai.svg diff --git a/web/app/components/base/icons/assets/public/llm/baichuan-text.svg b/packages/iconify-collections/assets/public/llm/baichuan-text.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/baichuan-text.svg rename to packages/iconify-collections/assets/public/llm/baichuan-text.svg diff --git a/web/app/components/base/icons/assets/public/llm/baichuan.svg b/packages/iconify-collections/assets/public/llm/baichuan.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/baichuan.svg rename to packages/iconify-collections/assets/public/llm/baichuan.svg diff --git a/web/app/components/base/icons/assets/public/llm/chatglm-text.svg b/packages/iconify-collections/assets/public/llm/chatglm-text.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/chatglm-text.svg rename to packages/iconify-collections/assets/public/llm/chatglm-text.svg diff --git a/web/app/components/base/icons/assets/public/llm/chatglm.svg b/packages/iconify-collections/assets/public/llm/chatglm.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/chatglm.svg rename to packages/iconify-collections/assets/public/llm/chatglm.svg diff --git a/web/app/components/base/icons/assets/public/llm/cohere-text.svg b/packages/iconify-collections/assets/public/llm/cohere-text.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/cohere-text.svg rename to packages/iconify-collections/assets/public/llm/cohere-text.svg diff --git a/web/app/components/base/icons/assets/public/llm/cohere.svg b/packages/iconify-collections/assets/public/llm/cohere.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/cohere.svg rename to packages/iconify-collections/assets/public/llm/cohere.svg diff --git a/web/app/components/base/icons/assets/public/llm/deepseek.svg b/packages/iconify-collections/assets/public/llm/deepseek.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/deepseek.svg rename to packages/iconify-collections/assets/public/llm/deepseek.svg diff --git a/web/app/components/base/icons/assets/public/llm/gemini.svg b/packages/iconify-collections/assets/public/llm/gemini.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/gemini.svg rename to packages/iconify-collections/assets/public/llm/gemini.svg diff --git a/web/app/components/base/icons/assets/public/llm/gpt-3.svg b/packages/iconify-collections/assets/public/llm/gpt-3.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/gpt-3.svg rename to packages/iconify-collections/assets/public/llm/gpt-3.svg diff --git a/web/app/components/base/icons/assets/public/llm/gpt-4.svg b/packages/iconify-collections/assets/public/llm/gpt-4.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/gpt-4.svg rename to packages/iconify-collections/assets/public/llm/gpt-4.svg diff --git a/web/app/components/base/icons/assets/public/llm/grok.svg b/packages/iconify-collections/assets/public/llm/grok.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/grok.svg rename to packages/iconify-collections/assets/public/llm/grok.svg diff --git a/web/app/components/base/icons/assets/public/llm/huggingface-text-hub.svg b/packages/iconify-collections/assets/public/llm/huggingface-text-hub.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/huggingface-text-hub.svg rename to packages/iconify-collections/assets/public/llm/huggingface-text-hub.svg diff --git a/web/app/components/base/icons/assets/public/llm/huggingface-text.svg b/packages/iconify-collections/assets/public/llm/huggingface-text.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/huggingface-text.svg rename to packages/iconify-collections/assets/public/llm/huggingface-text.svg diff --git a/web/app/components/base/icons/assets/public/llm/huggingface.svg b/packages/iconify-collections/assets/public/llm/huggingface.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/huggingface.svg rename to packages/iconify-collections/assets/public/llm/huggingface.svg diff --git a/web/app/components/base/icons/assets/public/llm/iflytek-spark-text-cn.svg b/packages/iconify-collections/assets/public/llm/iflytek-spark-text-cn.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/iflytek-spark-text-cn.svg rename to packages/iconify-collections/assets/public/llm/iflytek-spark-text-cn.svg diff --git a/web/app/components/base/icons/assets/public/llm/iflytek-spark-text.svg b/packages/iconify-collections/assets/public/llm/iflytek-spark-text.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/iflytek-spark-text.svg rename to packages/iconify-collections/assets/public/llm/iflytek-spark-text.svg diff --git a/web/app/components/base/icons/assets/public/llm/iflytek-spark.svg b/packages/iconify-collections/assets/public/llm/iflytek-spark.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/iflytek-spark.svg rename to packages/iconify-collections/assets/public/llm/iflytek-spark.svg diff --git a/web/app/components/base/icons/assets/public/llm/jina-text.svg b/packages/iconify-collections/assets/public/llm/jina-text.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/jina-text.svg rename to packages/iconify-collections/assets/public/llm/jina-text.svg diff --git a/web/app/components/base/icons/assets/public/llm/jina.svg b/packages/iconify-collections/assets/public/llm/jina.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/jina.svg rename to packages/iconify-collections/assets/public/llm/jina.svg diff --git a/web/app/components/base/icons/assets/public/llm/localai-text.svg b/packages/iconify-collections/assets/public/llm/localai-text.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/localai-text.svg rename to packages/iconify-collections/assets/public/llm/localai-text.svg diff --git a/web/app/components/base/icons/assets/public/llm/localai.svg b/packages/iconify-collections/assets/public/llm/localai.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/localai.svg rename to packages/iconify-collections/assets/public/llm/localai.svg diff --git a/web/app/components/base/icons/assets/public/llm/microsoft.svg b/packages/iconify-collections/assets/public/llm/microsoft.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/microsoft.svg rename to packages/iconify-collections/assets/public/llm/microsoft.svg diff --git a/web/app/components/base/icons/assets/public/llm/openai-black.svg b/packages/iconify-collections/assets/public/llm/openai-black.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/openai-black.svg rename to packages/iconify-collections/assets/public/llm/openai-black.svg diff --git a/web/app/components/base/icons/assets/public/llm/openai-blue.svg b/packages/iconify-collections/assets/public/llm/openai-blue.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/openai-blue.svg rename to packages/iconify-collections/assets/public/llm/openai-blue.svg diff --git a/web/app/components/base/icons/assets/public/llm/openai-green.svg b/packages/iconify-collections/assets/public/llm/openai-green.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/openai-green.svg rename to packages/iconify-collections/assets/public/llm/openai-green.svg diff --git a/web/app/components/base/icons/assets/public/llm/openai-small.svg b/packages/iconify-collections/assets/public/llm/openai-small.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/openai-small.svg rename to packages/iconify-collections/assets/public/llm/openai-small.svg diff --git a/web/app/components/base/icons/assets/public/llm/openai-teal.svg b/packages/iconify-collections/assets/public/llm/openai-teal.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/openai-teal.svg rename to packages/iconify-collections/assets/public/llm/openai-teal.svg diff --git a/web/app/components/base/icons/assets/public/llm/openai-text.svg b/packages/iconify-collections/assets/public/llm/openai-text.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/openai-text.svg rename to packages/iconify-collections/assets/public/llm/openai-text.svg diff --git a/web/app/components/base/icons/assets/public/llm/openai-transparent.svg b/packages/iconify-collections/assets/public/llm/openai-transparent.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/openai-transparent.svg rename to packages/iconify-collections/assets/public/llm/openai-transparent.svg diff --git a/web/app/components/base/icons/assets/public/llm/openai-violet.svg b/packages/iconify-collections/assets/public/llm/openai-violet.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/openai-violet.svg rename to packages/iconify-collections/assets/public/llm/openai-violet.svg diff --git a/web/app/components/base/icons/assets/public/llm/openai-yellow.svg b/packages/iconify-collections/assets/public/llm/openai-yellow.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/openai-yellow.svg rename to packages/iconify-collections/assets/public/llm/openai-yellow.svg diff --git a/web/app/components/base/icons/assets/public/llm/openllm-text.svg b/packages/iconify-collections/assets/public/llm/openllm-text.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/openllm-text.svg rename to packages/iconify-collections/assets/public/llm/openllm-text.svg diff --git a/web/app/components/base/icons/assets/public/llm/openllm.svg b/packages/iconify-collections/assets/public/llm/openllm.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/openllm.svg rename to packages/iconify-collections/assets/public/llm/openllm.svg diff --git a/web/app/components/base/icons/assets/public/llm/replicate-text.svg b/packages/iconify-collections/assets/public/llm/replicate-text.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/replicate-text.svg rename to packages/iconify-collections/assets/public/llm/replicate-text.svg diff --git a/web/app/components/base/icons/assets/public/llm/replicate.svg b/packages/iconify-collections/assets/public/llm/replicate.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/replicate.svg rename to packages/iconify-collections/assets/public/llm/replicate.svg diff --git a/web/app/components/base/icons/assets/public/llm/xorbits-inference-text.svg b/packages/iconify-collections/assets/public/llm/xorbits-inference-text.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/xorbits-inference-text.svg rename to packages/iconify-collections/assets/public/llm/xorbits-inference-text.svg diff --git a/web/app/components/base/icons/assets/public/llm/xorbits-inference.svg b/packages/iconify-collections/assets/public/llm/xorbits-inference.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/xorbits-inference.svg rename to packages/iconify-collections/assets/public/llm/xorbits-inference.svg diff --git a/web/app/components/base/icons/assets/public/llm/zhipuai-text-cn.svg b/packages/iconify-collections/assets/public/llm/zhipuai-text-cn.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/zhipuai-text-cn.svg rename to packages/iconify-collections/assets/public/llm/zhipuai-text-cn.svg diff --git a/web/app/components/base/icons/assets/public/llm/zhipuai-text.svg b/packages/iconify-collections/assets/public/llm/zhipuai-text.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/zhipuai-text.svg rename to packages/iconify-collections/assets/public/llm/zhipuai-text.svg diff --git a/web/app/components/base/icons/assets/public/llm/zhipuai.svg b/packages/iconify-collections/assets/public/llm/zhipuai.svg similarity index 100% rename from web/app/components/base/icons/assets/public/llm/zhipuai.svg rename to packages/iconify-collections/assets/public/llm/zhipuai.svg diff --git a/web/app/components/base/icons/assets/public/model/checked.svg b/packages/iconify-collections/assets/public/model/checked.svg similarity index 100% rename from web/app/components/base/icons/assets/public/model/checked.svg rename to packages/iconify-collections/assets/public/model/checked.svg diff --git a/web/app/components/base/icons/assets/public/other/Icon-3-dots.svg b/packages/iconify-collections/assets/public/other/Icon-3-dots.svg similarity index 100% rename from web/app/components/base/icons/assets/public/other/Icon-3-dots.svg rename to packages/iconify-collections/assets/public/other/Icon-3-dots.svg diff --git a/web/app/components/base/icons/assets/public/other/default-tool-icon.svg b/packages/iconify-collections/assets/public/other/default-tool-icon.svg similarity index 100% rename from web/app/components/base/icons/assets/public/other/default-tool-icon.svg rename to packages/iconify-collections/assets/public/other/default-tool-icon.svg diff --git a/web/app/components/base/icons/assets/public/other/message-3-fill.svg b/packages/iconify-collections/assets/public/other/message-3-fill.svg similarity index 100% rename from web/app/components/base/icons/assets/public/other/message-3-fill.svg rename to packages/iconify-collections/assets/public/other/message-3-fill.svg diff --git a/web/app/components/base/icons/assets/public/other/row-struct.svg b/packages/iconify-collections/assets/public/other/row-struct.svg similarity index 100% rename from web/app/components/base/icons/assets/public/other/row-struct.svg rename to packages/iconify-collections/assets/public/other/row-struct.svg diff --git a/web/app/components/base/icons/assets/public/other/slack.svg b/packages/iconify-collections/assets/public/other/slack.svg similarity index 100% rename from web/app/components/base/icons/assets/public/other/slack.svg rename to packages/iconify-collections/assets/public/other/slack.svg diff --git a/web/app/components/base/icons/assets/public/other/teams.svg b/packages/iconify-collections/assets/public/other/teams.svg similarity index 100% rename from web/app/components/base/icons/assets/public/other/teams.svg rename to packages/iconify-collections/assets/public/other/teams.svg diff --git a/web/app/components/base/icons/assets/public/plugins/google.svg b/packages/iconify-collections/assets/public/plugins/google.svg similarity index 100% rename from web/app/components/base/icons/assets/public/plugins/google.svg rename to packages/iconify-collections/assets/public/plugins/google.svg diff --git a/web/app/components/base/icons/assets/public/plugins/partner-dark.svg b/packages/iconify-collections/assets/public/plugins/partner-dark.svg similarity index 100% rename from web/app/components/base/icons/assets/public/plugins/partner-dark.svg rename to packages/iconify-collections/assets/public/plugins/partner-dark.svg diff --git a/web/app/components/base/icons/assets/public/plugins/partner-light.svg b/packages/iconify-collections/assets/public/plugins/partner-light.svg similarity index 100% rename from web/app/components/base/icons/assets/public/plugins/partner-light.svg rename to packages/iconify-collections/assets/public/plugins/partner-light.svg diff --git a/web/app/components/base/icons/assets/public/plugins/verified-dark.svg b/packages/iconify-collections/assets/public/plugins/verified-dark.svg similarity index 100% rename from web/app/components/base/icons/assets/public/plugins/verified-dark.svg rename to packages/iconify-collections/assets/public/plugins/verified-dark.svg diff --git a/web/app/components/base/icons/assets/public/plugins/verified-light.svg b/packages/iconify-collections/assets/public/plugins/verified-light.svg similarity index 100% rename from web/app/components/base/icons/assets/public/plugins/verified-light.svg rename to packages/iconify-collections/assets/public/plugins/verified-light.svg diff --git a/web/app/components/base/icons/assets/public/plugins/web-reader.svg b/packages/iconify-collections/assets/public/plugins/web-reader.svg similarity index 100% rename from web/app/components/base/icons/assets/public/plugins/web-reader.svg rename to packages/iconify-collections/assets/public/plugins/web-reader.svg diff --git a/web/app/components/base/icons/assets/public/plugins/wikipedia.svg b/packages/iconify-collections/assets/public/plugins/wikipedia.svg similarity index 100% rename from web/app/components/base/icons/assets/public/plugins/wikipedia.svg rename to packages/iconify-collections/assets/public/plugins/wikipedia.svg diff --git a/web/app/components/base/icons/assets/public/thought/data-set.svg b/packages/iconify-collections/assets/public/thought/data-set.svg similarity index 100% rename from web/app/components/base/icons/assets/public/thought/data-set.svg rename to packages/iconify-collections/assets/public/thought/data-set.svg diff --git a/web/app/components/base/icons/assets/public/thought/loading.svg b/packages/iconify-collections/assets/public/thought/loading.svg similarity index 100% rename from web/app/components/base/icons/assets/public/thought/loading.svg rename to packages/iconify-collections/assets/public/thought/loading.svg diff --git a/web/app/components/base/icons/assets/public/thought/search.svg b/packages/iconify-collections/assets/public/thought/search.svg similarity index 100% rename from web/app/components/base/icons/assets/public/thought/search.svg rename to packages/iconify-collections/assets/public/thought/search.svg diff --git a/web/app/components/base/icons/assets/public/thought/thought-list.svg b/packages/iconify-collections/assets/public/thought/thought-list.svg similarity index 100% rename from web/app/components/base/icons/assets/public/thought/thought-list.svg rename to packages/iconify-collections/assets/public/thought/thought-list.svg diff --git a/web/app/components/base/icons/assets/public/thought/web-reader.svg b/packages/iconify-collections/assets/public/thought/web-reader.svg similarity index 100% rename from web/app/components/base/icons/assets/public/thought/web-reader.svg rename to packages/iconify-collections/assets/public/thought/web-reader.svg diff --git a/web/app/components/base/icons/assets/public/tracing/aliyun-icon-big.svg b/packages/iconify-collections/assets/public/tracing/aliyun-icon-big.svg similarity index 100% rename from web/app/components/base/icons/assets/public/tracing/aliyun-icon-big.svg rename to packages/iconify-collections/assets/public/tracing/aliyun-icon-big.svg diff --git a/web/app/components/base/icons/assets/public/tracing/aliyun-icon.svg b/packages/iconify-collections/assets/public/tracing/aliyun-icon.svg similarity index 100% rename from web/app/components/base/icons/assets/public/tracing/aliyun-icon.svg rename to packages/iconify-collections/assets/public/tracing/aliyun-icon.svg diff --git a/web/app/components/base/icons/assets/public/tracing/arize-icon-big.svg b/packages/iconify-collections/assets/public/tracing/arize-icon-big.svg similarity index 100% rename from web/app/components/base/icons/assets/public/tracing/arize-icon-big.svg rename to packages/iconify-collections/assets/public/tracing/arize-icon-big.svg diff --git a/web/app/components/base/icons/assets/public/tracing/arize-icon.svg b/packages/iconify-collections/assets/public/tracing/arize-icon.svg similarity index 100% rename from web/app/components/base/icons/assets/public/tracing/arize-icon.svg rename to packages/iconify-collections/assets/public/tracing/arize-icon.svg diff --git a/web/app/components/base/icons/assets/public/tracing/databricks-icon-big.svg b/packages/iconify-collections/assets/public/tracing/databricks-icon-big.svg similarity index 100% rename from web/app/components/base/icons/assets/public/tracing/databricks-icon-big.svg rename to packages/iconify-collections/assets/public/tracing/databricks-icon-big.svg diff --git a/web/app/components/base/icons/assets/public/tracing/databricks-icon.svg b/packages/iconify-collections/assets/public/tracing/databricks-icon.svg similarity index 100% rename from web/app/components/base/icons/assets/public/tracing/databricks-icon.svg rename to packages/iconify-collections/assets/public/tracing/databricks-icon.svg diff --git a/web/app/components/base/icons/assets/public/tracing/langfuse-icon-big.svg b/packages/iconify-collections/assets/public/tracing/langfuse-icon-big.svg similarity index 100% rename from web/app/components/base/icons/assets/public/tracing/langfuse-icon-big.svg rename to packages/iconify-collections/assets/public/tracing/langfuse-icon-big.svg diff --git a/web/app/components/base/icons/assets/public/tracing/langfuse-icon.svg b/packages/iconify-collections/assets/public/tracing/langfuse-icon.svg similarity index 100% rename from web/app/components/base/icons/assets/public/tracing/langfuse-icon.svg rename to packages/iconify-collections/assets/public/tracing/langfuse-icon.svg diff --git a/web/app/components/base/icons/assets/public/tracing/langsmith-icon-big.svg b/packages/iconify-collections/assets/public/tracing/langsmith-icon-big.svg similarity index 100% rename from web/app/components/base/icons/assets/public/tracing/langsmith-icon-big.svg rename to packages/iconify-collections/assets/public/tracing/langsmith-icon-big.svg diff --git a/web/app/components/base/icons/assets/public/tracing/langsmith-icon.svg b/packages/iconify-collections/assets/public/tracing/langsmith-icon.svg similarity index 100% rename from web/app/components/base/icons/assets/public/tracing/langsmith-icon.svg rename to packages/iconify-collections/assets/public/tracing/langsmith-icon.svg diff --git a/web/app/components/base/icons/assets/public/tracing/mlflow-icon-big.svg b/packages/iconify-collections/assets/public/tracing/mlflow-icon-big.svg similarity index 100% rename from web/app/components/base/icons/assets/public/tracing/mlflow-icon-big.svg rename to packages/iconify-collections/assets/public/tracing/mlflow-icon-big.svg diff --git a/web/app/components/base/icons/assets/public/tracing/mlflow-icon.svg b/packages/iconify-collections/assets/public/tracing/mlflow-icon.svg similarity index 100% rename from web/app/components/base/icons/assets/public/tracing/mlflow-icon.svg rename to packages/iconify-collections/assets/public/tracing/mlflow-icon.svg diff --git a/web/app/components/base/icons/assets/public/tracing/opik-icon-big.svg b/packages/iconify-collections/assets/public/tracing/opik-icon-big.svg similarity index 100% rename from web/app/components/base/icons/assets/public/tracing/opik-icon-big.svg rename to packages/iconify-collections/assets/public/tracing/opik-icon-big.svg diff --git a/web/app/components/base/icons/assets/public/tracing/opik-icon.svg b/packages/iconify-collections/assets/public/tracing/opik-icon.svg similarity index 100% rename from web/app/components/base/icons/assets/public/tracing/opik-icon.svg rename to packages/iconify-collections/assets/public/tracing/opik-icon.svg diff --git a/web/app/components/base/icons/assets/public/tracing/phoenix-icon-big.svg b/packages/iconify-collections/assets/public/tracing/phoenix-icon-big.svg similarity index 100% rename from web/app/components/base/icons/assets/public/tracing/phoenix-icon-big.svg rename to packages/iconify-collections/assets/public/tracing/phoenix-icon-big.svg diff --git a/web/app/components/base/icons/assets/public/tracing/phoenix-icon.svg b/packages/iconify-collections/assets/public/tracing/phoenix-icon.svg similarity index 100% rename from web/app/components/base/icons/assets/public/tracing/phoenix-icon.svg rename to packages/iconify-collections/assets/public/tracing/phoenix-icon.svg diff --git a/web/app/components/base/icons/assets/public/tracing/tencent-icon-big.svg b/packages/iconify-collections/assets/public/tracing/tencent-icon-big.svg similarity index 100% rename from web/app/components/base/icons/assets/public/tracing/tencent-icon-big.svg rename to packages/iconify-collections/assets/public/tracing/tencent-icon-big.svg diff --git a/web/app/components/base/icons/assets/public/tracing/tencent-icon.svg b/packages/iconify-collections/assets/public/tracing/tencent-icon.svg similarity index 100% rename from web/app/components/base/icons/assets/public/tracing/tencent-icon.svg rename to packages/iconify-collections/assets/public/tracing/tencent-icon.svg diff --git a/web/app/components/base/icons/assets/public/tracing/tracing-icon.svg b/packages/iconify-collections/assets/public/tracing/tracing-icon.svg similarity index 100% rename from web/app/components/base/icons/assets/public/tracing/tracing-icon.svg rename to packages/iconify-collections/assets/public/tracing/tracing-icon.svg diff --git a/web/app/components/base/icons/assets/public/tracing/weave-icon-big.svg b/packages/iconify-collections/assets/public/tracing/weave-icon-big.svg similarity index 100% rename from web/app/components/base/icons/assets/public/tracing/weave-icon-big.svg rename to packages/iconify-collections/assets/public/tracing/weave-icon-big.svg diff --git a/web/app/components/base/icons/assets/public/tracing/weave-icon.svg b/packages/iconify-collections/assets/public/tracing/weave-icon.svg similarity index 100% rename from web/app/components/base/icons/assets/public/tracing/weave-icon.svg rename to packages/iconify-collections/assets/public/tracing/weave-icon.svg diff --git a/web/app/components/base/icons/assets/vender/features/citations.svg b/packages/iconify-collections/assets/vender/features/citations.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/features/citations.svg rename to packages/iconify-collections/assets/vender/features/citations.svg diff --git a/web/app/components/base/icons/assets/vender/features/content-moderation.svg b/packages/iconify-collections/assets/vender/features/content-moderation.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/features/content-moderation.svg rename to packages/iconify-collections/assets/vender/features/content-moderation.svg diff --git a/web/app/components/base/icons/assets/vender/features/document.svg b/packages/iconify-collections/assets/vender/features/document.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/features/document.svg rename to packages/iconify-collections/assets/vender/features/document.svg diff --git a/web/app/components/base/icons/assets/vender/features/folder-upload.svg b/packages/iconify-collections/assets/vender/features/folder-upload.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/features/folder-upload.svg rename to packages/iconify-collections/assets/vender/features/folder-upload.svg diff --git a/web/app/components/base/icons/assets/vender/features/love-message.svg b/packages/iconify-collections/assets/vender/features/love-message.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/features/love-message.svg rename to packages/iconify-collections/assets/vender/features/love-message.svg diff --git a/web/app/components/base/icons/assets/vender/features/message-fast.svg b/packages/iconify-collections/assets/vender/features/message-fast.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/features/message-fast.svg rename to packages/iconify-collections/assets/vender/features/message-fast.svg diff --git a/web/app/components/base/icons/assets/vender/features/microphone-01.svg b/packages/iconify-collections/assets/vender/features/microphone-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/features/microphone-01.svg rename to packages/iconify-collections/assets/vender/features/microphone-01.svg diff --git a/web/app/components/base/icons/assets/vender/features/text-to-audio.svg b/packages/iconify-collections/assets/vender/features/text-to-audio.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/features/text-to-audio.svg rename to packages/iconify-collections/assets/vender/features/text-to-audio.svg diff --git a/web/app/components/base/icons/assets/vender/features/virtual-assistant.svg b/packages/iconify-collections/assets/vender/features/virtual-assistant.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/features/virtual-assistant.svg rename to packages/iconify-collections/assets/vender/features/virtual-assistant.svg diff --git a/web/app/components/base/icons/assets/vender/features/vision.svg b/packages/iconify-collections/assets/vender/features/vision.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/features/vision.svg rename to packages/iconify-collections/assets/vender/features/vision.svg diff --git a/web/app/components/base/icons/assets/vender/knowledge/add-chunks.svg b/packages/iconify-collections/assets/vender/knowledge/add-chunks.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/knowledge/add-chunks.svg rename to packages/iconify-collections/assets/vender/knowledge/add-chunks.svg diff --git a/web/app/components/base/icons/assets/vender/knowledge/api-aggregate.svg b/packages/iconify-collections/assets/vender/knowledge/api-aggregate.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/knowledge/api-aggregate.svg rename to packages/iconify-collections/assets/vender/knowledge/api-aggregate.svg diff --git a/web/app/components/base/icons/assets/vender/knowledge/arrow-shape.svg b/packages/iconify-collections/assets/vender/knowledge/arrow-shape.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/knowledge/arrow-shape.svg rename to packages/iconify-collections/assets/vender/knowledge/arrow-shape.svg diff --git a/web/app/components/base/icons/assets/vender/knowledge/chunk.svg b/packages/iconify-collections/assets/vender/knowledge/chunk.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/knowledge/chunk.svg rename to packages/iconify-collections/assets/vender/knowledge/chunk.svg diff --git a/web/app/components/base/icons/assets/vender/knowledge/collapse.svg b/packages/iconify-collections/assets/vender/knowledge/collapse.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/knowledge/collapse.svg rename to packages/iconify-collections/assets/vender/knowledge/collapse.svg diff --git a/web/app/components/base/icons/assets/vender/knowledge/divider.svg b/packages/iconify-collections/assets/vender/knowledge/divider.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/knowledge/divider.svg rename to packages/iconify-collections/assets/vender/knowledge/divider.svg diff --git a/web/app/components/base/icons/assets/vender/knowledge/economic.svg b/packages/iconify-collections/assets/vender/knowledge/economic.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/knowledge/economic.svg rename to packages/iconify-collections/assets/vender/knowledge/economic.svg diff --git a/web/app/components/base/icons/assets/vender/knowledge/full-text-search.svg b/packages/iconify-collections/assets/vender/knowledge/full-text-search.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/knowledge/full-text-search.svg rename to packages/iconify-collections/assets/vender/knowledge/full-text-search.svg diff --git a/web/app/components/base/icons/assets/vender/knowledge/general-chunk.svg b/packages/iconify-collections/assets/vender/knowledge/general-chunk.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/knowledge/general-chunk.svg rename to packages/iconify-collections/assets/vender/knowledge/general-chunk.svg diff --git a/web/app/components/base/icons/assets/vender/knowledge/high-quality.svg b/packages/iconify-collections/assets/vender/knowledge/high-quality.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/knowledge/high-quality.svg rename to packages/iconify-collections/assets/vender/knowledge/high-quality.svg diff --git a/web/app/components/base/icons/assets/vender/knowledge/hybrid-search.svg b/packages/iconify-collections/assets/vender/knowledge/hybrid-search.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/knowledge/hybrid-search.svg rename to packages/iconify-collections/assets/vender/knowledge/hybrid-search.svg diff --git a/web/app/components/base/icons/assets/vender/knowledge/parent-child-chunk.svg b/packages/iconify-collections/assets/vender/knowledge/parent-child-chunk.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/knowledge/parent-child-chunk.svg rename to packages/iconify-collections/assets/vender/knowledge/parent-child-chunk.svg diff --git a/web/app/components/base/icons/assets/vender/knowledge/question-and-answer.svg b/packages/iconify-collections/assets/vender/knowledge/question-and-answer.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/knowledge/question-and-answer.svg rename to packages/iconify-collections/assets/vender/knowledge/question-and-answer.svg diff --git a/web/app/components/base/icons/assets/vender/knowledge/search-lines-sparkle.svg b/packages/iconify-collections/assets/vender/knowledge/search-lines-sparkle.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/knowledge/search-lines-sparkle.svg rename to packages/iconify-collections/assets/vender/knowledge/search-lines-sparkle.svg diff --git a/web/app/components/base/icons/assets/vender/knowledge/search-menu.svg b/packages/iconify-collections/assets/vender/knowledge/search-menu.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/knowledge/search-menu.svg rename to packages/iconify-collections/assets/vender/knowledge/search-menu.svg diff --git a/web/app/components/base/icons/assets/vender/knowledge/vector-search.svg b/packages/iconify-collections/assets/vender/knowledge/vector-search.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/knowledge/vector-search.svg rename to packages/iconify-collections/assets/vender/knowledge/vector-search.svg diff --git a/web/app/components/base/icons/assets/vender/line/alertsAndFeedback/alert-triangle.svg b/packages/iconify-collections/assets/vender/line/alertsAndFeedback/alert-triangle.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/alertsAndFeedback/alert-triangle.svg rename to packages/iconify-collections/assets/vender/line/alertsAndFeedback/alert-triangle.svg diff --git a/web/app/components/base/icons/assets/vender/line/alertsAndFeedback/thumbs-down.svg b/packages/iconify-collections/assets/vender/line/alertsAndFeedback/thumbs-down.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/alertsAndFeedback/thumbs-down.svg rename to packages/iconify-collections/assets/vender/line/alertsAndFeedback/thumbs-down.svg diff --git a/web/app/components/base/icons/assets/vender/line/alertsAndFeedback/thumbs-up.svg b/packages/iconify-collections/assets/vender/line/alertsAndFeedback/thumbs-up.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/alertsAndFeedback/thumbs-up.svg rename to packages/iconify-collections/assets/vender/line/alertsAndFeedback/thumbs-up.svg diff --git a/web/app/components/base/icons/assets/vender/line/alertsAndFeedback/warning.svg b/packages/iconify-collections/assets/vender/line/alertsAndFeedback/warning.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/alertsAndFeedback/warning.svg rename to packages/iconify-collections/assets/vender/line/alertsAndFeedback/warning.svg diff --git a/web/app/components/base/icons/assets/vender/line/arrows/IconR.svg b/packages/iconify-collections/assets/vender/line/arrows/IconR.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/arrows/IconR.svg rename to packages/iconify-collections/assets/vender/line/arrows/IconR.svg diff --git a/web/app/components/base/icons/assets/vender/line/arrows/arrow-narrow-left.svg b/packages/iconify-collections/assets/vender/line/arrows/arrow-narrow-left.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/arrows/arrow-narrow-left.svg rename to packages/iconify-collections/assets/vender/line/arrows/arrow-narrow-left.svg diff --git a/web/app/components/base/icons/assets/vender/line/arrows/arrow-up-right.svg b/packages/iconify-collections/assets/vender/line/arrows/arrow-up-right.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/arrows/arrow-up-right.svg rename to packages/iconify-collections/assets/vender/line/arrows/arrow-up-right.svg diff --git a/web/app/components/base/icons/assets/vender/line/arrows/chevron-down-double.svg b/packages/iconify-collections/assets/vender/line/arrows/chevron-down-double.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/arrows/chevron-down-double.svg rename to packages/iconify-collections/assets/vender/line/arrows/chevron-down-double.svg diff --git a/web/app/components/base/icons/assets/vender/line/arrows/chevron-right.svg b/packages/iconify-collections/assets/vender/line/arrows/chevron-right.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/arrows/chevron-right.svg rename to packages/iconify-collections/assets/vender/line/arrows/chevron-right.svg diff --git a/web/app/components/base/icons/assets/vender/line/arrows/chevron-selector-vertical.svg b/packages/iconify-collections/assets/vender/line/arrows/chevron-selector-vertical.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/arrows/chevron-selector-vertical.svg rename to packages/iconify-collections/assets/vender/line/arrows/chevron-selector-vertical.svg diff --git a/web/app/components/base/icons/assets/vender/line/arrows/refresh-ccw-01.svg b/packages/iconify-collections/assets/vender/line/arrows/refresh-ccw-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/arrows/refresh-ccw-01.svg rename to packages/iconify-collections/assets/vender/line/arrows/refresh-ccw-01.svg diff --git a/web/app/components/base/icons/assets/vender/line/arrows/refresh-cw-05.svg b/packages/iconify-collections/assets/vender/line/arrows/refresh-cw-05.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/arrows/refresh-cw-05.svg rename to packages/iconify-collections/assets/vender/line/arrows/refresh-cw-05.svg diff --git a/web/app/components/base/icons/assets/vender/line/arrows/reverse-left.svg b/packages/iconify-collections/assets/vender/line/arrows/reverse-left.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/arrows/reverse-left.svg rename to packages/iconify-collections/assets/vender/line/arrows/reverse-left.svg diff --git a/web/app/components/base/icons/assets/vender/line/communication/ai-text.svg b/packages/iconify-collections/assets/vender/line/communication/ai-text.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/communication/ai-text.svg rename to packages/iconify-collections/assets/vender/line/communication/ai-text.svg diff --git a/web/app/components/base/icons/assets/vender/line/communication/chat-bot-slim.svg b/packages/iconify-collections/assets/vender/line/communication/chat-bot-slim.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/communication/chat-bot-slim.svg rename to packages/iconify-collections/assets/vender/line/communication/chat-bot-slim.svg diff --git a/web/app/components/base/icons/assets/vender/line/communication/chat-bot.svg b/packages/iconify-collections/assets/vender/line/communication/chat-bot.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/communication/chat-bot.svg rename to packages/iconify-collections/assets/vender/line/communication/chat-bot.svg diff --git a/web/app/components/base/icons/assets/vender/line/communication/cute-robot.svg b/packages/iconify-collections/assets/vender/line/communication/cute-robot.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/communication/cute-robot.svg rename to packages/iconify-collections/assets/vender/line/communication/cute-robot.svg diff --git a/web/app/components/base/icons/assets/vender/line/communication/message-check-remove.svg b/packages/iconify-collections/assets/vender/line/communication/message-check-remove.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/communication/message-check-remove.svg rename to packages/iconify-collections/assets/vender/line/communication/message-check-remove.svg diff --git a/web/app/components/base/icons/assets/vender/line/communication/message-fast-plus.svg b/packages/iconify-collections/assets/vender/line/communication/message-fast-plus.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/communication/message-fast-plus.svg rename to packages/iconify-collections/assets/vender/line/communication/message-fast-plus.svg diff --git a/web/app/components/base/icons/assets/vender/line/development/artificial-brain.svg b/packages/iconify-collections/assets/vender/line/development/artificial-brain.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/development/artificial-brain.svg rename to packages/iconify-collections/assets/vender/line/development/artificial-brain.svg diff --git a/web/app/components/base/icons/assets/vender/line/development/bar-chart-square-02.svg b/packages/iconify-collections/assets/vender/line/development/bar-chart-square-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/development/bar-chart-square-02.svg rename to packages/iconify-collections/assets/vender/line/development/bar-chart-square-02.svg diff --git a/web/app/components/base/icons/assets/vender/line/development/brackets-x.svg b/packages/iconify-collections/assets/vender/line/development/brackets-x.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/development/brackets-x.svg rename to packages/iconify-collections/assets/vender/line/development/brackets-x.svg diff --git a/web/app/components/base/icons/assets/vender/line/development/code-browser.svg b/packages/iconify-collections/assets/vender/line/development/code-browser.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/development/code-browser.svg rename to packages/iconify-collections/assets/vender/line/development/code-browser.svg diff --git a/web/app/components/base/icons/assets/vender/line/development/container.svg b/packages/iconify-collections/assets/vender/line/development/container.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/development/container.svg rename to packages/iconify-collections/assets/vender/line/development/container.svg diff --git a/web/app/components/base/icons/assets/vender/line/development/database-01.svg b/packages/iconify-collections/assets/vender/line/development/database-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/development/database-01.svg rename to packages/iconify-collections/assets/vender/line/development/database-01.svg diff --git a/web/app/components/base/icons/assets/vender/line/development/database-03.svg b/packages/iconify-collections/assets/vender/line/development/database-03.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/development/database-03.svg rename to packages/iconify-collections/assets/vender/line/development/database-03.svg diff --git a/web/app/components/base/icons/assets/vender/line/development/file-heart-02.svg b/packages/iconify-collections/assets/vender/line/development/file-heart-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/development/file-heart-02.svg rename to packages/iconify-collections/assets/vender/line/development/file-heart-02.svg diff --git a/web/app/components/base/icons/assets/vender/line/development/git-branch-01.svg b/packages/iconify-collections/assets/vender/line/development/git-branch-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/development/git-branch-01.svg rename to packages/iconify-collections/assets/vender/line/development/git-branch-01.svg diff --git a/web/app/components/base/icons/assets/vender/line/development/prompt-engineering.svg b/packages/iconify-collections/assets/vender/line/development/prompt-engineering.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/development/prompt-engineering.svg rename to packages/iconify-collections/assets/vender/line/development/prompt-engineering.svg diff --git a/web/app/components/base/icons/assets/vender/line/development/puzzle-piece-01.svg b/packages/iconify-collections/assets/vender/line/development/puzzle-piece-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/development/puzzle-piece-01.svg rename to packages/iconify-collections/assets/vender/line/development/puzzle-piece-01.svg diff --git a/web/app/components/base/icons/assets/vender/line/development/terminal-square.svg b/packages/iconify-collections/assets/vender/line/development/terminal-square.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/development/terminal-square.svg rename to packages/iconify-collections/assets/vender/line/development/terminal-square.svg diff --git a/web/app/components/base/icons/assets/vender/line/development/variable.svg b/packages/iconify-collections/assets/vender/line/development/variable.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/development/variable.svg rename to packages/iconify-collections/assets/vender/line/development/variable.svg diff --git a/web/app/components/base/icons/assets/vender/line/development/webhooks.svg b/packages/iconify-collections/assets/vender/line/development/webhooks.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/development/webhooks.svg rename to packages/iconify-collections/assets/vender/line/development/webhooks.svg diff --git a/web/app/components/base/icons/assets/vender/line/editor/align-left.svg b/packages/iconify-collections/assets/vender/line/editor/align-left.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/editor/align-left.svg rename to packages/iconify-collections/assets/vender/line/editor/align-left.svg diff --git a/web/app/components/base/icons/assets/vender/line/editor/bezier-curve-03.svg b/packages/iconify-collections/assets/vender/line/editor/bezier-curve-03.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/editor/bezier-curve-03.svg rename to packages/iconify-collections/assets/vender/line/editor/bezier-curve-03.svg diff --git a/web/app/components/base/icons/assets/vender/line/editor/collapse.svg b/packages/iconify-collections/assets/vender/line/editor/collapse.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/editor/collapse.svg rename to packages/iconify-collections/assets/vender/line/editor/collapse.svg diff --git a/web/app/components/base/icons/assets/vender/line/editor/colors.svg b/packages/iconify-collections/assets/vender/line/editor/colors.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/editor/colors.svg rename to packages/iconify-collections/assets/vender/line/editor/colors.svg diff --git a/web/app/components/base/icons/assets/vender/line/editor/image-indent-left.svg b/packages/iconify-collections/assets/vender/line/editor/image-indent-left.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/editor/image-indent-left.svg rename to packages/iconify-collections/assets/vender/line/editor/image-indent-left.svg diff --git a/web/app/components/base/icons/assets/vender/line/editor/left-indent-02.svg b/packages/iconify-collections/assets/vender/line/editor/left-indent-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/editor/left-indent-02.svg rename to packages/iconify-collections/assets/vender/line/editor/left-indent-02.svg diff --git a/web/app/components/base/icons/assets/vender/line/editor/letter-spacing-01.svg b/packages/iconify-collections/assets/vender/line/editor/letter-spacing-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/editor/letter-spacing-01.svg rename to packages/iconify-collections/assets/vender/line/editor/letter-spacing-01.svg diff --git a/web/app/components/base/icons/assets/vender/line/editor/type-square.svg b/packages/iconify-collections/assets/vender/line/editor/type-square.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/editor/type-square.svg rename to packages/iconify-collections/assets/vender/line/editor/type-square.svg diff --git a/web/app/components/base/icons/assets/vender/line/education/book-open-01.svg b/packages/iconify-collections/assets/vender/line/education/book-open-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/education/book-open-01.svg rename to packages/iconify-collections/assets/vender/line/education/book-open-01.svg diff --git a/web/app/components/base/icons/assets/vender/line/files/copy-check.svg b/packages/iconify-collections/assets/vender/line/files/copy-check.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/files/copy-check.svg rename to packages/iconify-collections/assets/vender/line/files/copy-check.svg diff --git a/web/app/components/base/icons/assets/vender/line/files/copy.svg b/packages/iconify-collections/assets/vender/line/files/copy.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/files/copy.svg rename to packages/iconify-collections/assets/vender/line/files/copy.svg diff --git a/web/app/components/base/icons/assets/vender/line/files/file-02.svg b/packages/iconify-collections/assets/vender/line/files/file-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/files/file-02.svg rename to packages/iconify-collections/assets/vender/line/files/file-02.svg diff --git a/web/app/components/base/icons/assets/vender/line/files/file-arrow-01.svg b/packages/iconify-collections/assets/vender/line/files/file-arrow-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/files/file-arrow-01.svg rename to packages/iconify-collections/assets/vender/line/files/file-arrow-01.svg diff --git a/web/app/components/base/icons/assets/vender/line/files/file-check-02.svg b/packages/iconify-collections/assets/vender/line/files/file-check-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/files/file-check-02.svg rename to packages/iconify-collections/assets/vender/line/files/file-check-02.svg diff --git a/web/app/components/base/icons/assets/vender/line/files/file-download-02.svg b/packages/iconify-collections/assets/vender/line/files/file-download-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/files/file-download-02.svg rename to packages/iconify-collections/assets/vender/line/files/file-download-02.svg diff --git a/web/app/components/base/icons/assets/vender/line/files/file-plus-01.svg b/packages/iconify-collections/assets/vender/line/files/file-plus-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/files/file-plus-01.svg rename to packages/iconify-collections/assets/vender/line/files/file-plus-01.svg diff --git a/web/app/components/base/icons/assets/vender/line/files/file-plus-02.svg b/packages/iconify-collections/assets/vender/line/files/file-plus-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/files/file-plus-02.svg rename to packages/iconify-collections/assets/vender/line/files/file-plus-02.svg diff --git a/web/app/components/base/icons/assets/vender/line/files/file-text.svg b/packages/iconify-collections/assets/vender/line/files/file-text.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/files/file-text.svg rename to packages/iconify-collections/assets/vender/line/files/file-text.svg diff --git a/web/app/components/base/icons/assets/vender/line/files/file-upload.svg b/packages/iconify-collections/assets/vender/line/files/file-upload.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/files/file-upload.svg rename to packages/iconify-collections/assets/vender/line/files/file-upload.svg diff --git a/web/app/components/base/icons/assets/vender/line/files/folder.svg b/packages/iconify-collections/assets/vender/line/files/folder.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/files/folder.svg rename to packages/iconify-collections/assets/vender/line/files/folder.svg diff --git a/web/app/components/base/icons/assets/vender/line/financeAndECommerce/balance.svg b/packages/iconify-collections/assets/vender/line/financeAndECommerce/balance.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/financeAndECommerce/balance.svg rename to packages/iconify-collections/assets/vender/line/financeAndECommerce/balance.svg diff --git a/web/app/components/base/icons/assets/vender/line/financeAndECommerce/coins-stacked-01.svg b/packages/iconify-collections/assets/vender/line/financeAndECommerce/coins-stacked-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/financeAndECommerce/coins-stacked-01.svg rename to packages/iconify-collections/assets/vender/line/financeAndECommerce/coins-stacked-01.svg diff --git a/web/app/components/base/icons/assets/vender/line/financeAndECommerce/credits-coin.svg b/packages/iconify-collections/assets/vender/line/financeAndECommerce/credits-coin.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/financeAndECommerce/credits-coin.svg rename to packages/iconify-collections/assets/vender/line/financeAndECommerce/credits-coin.svg diff --git a/web/app/components/base/icons/assets/vender/line/financeAndECommerce/gold-coin.svg b/packages/iconify-collections/assets/vender/line/financeAndECommerce/gold-coin.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/financeAndECommerce/gold-coin.svg rename to packages/iconify-collections/assets/vender/line/financeAndECommerce/gold-coin.svg diff --git a/web/app/components/base/icons/assets/vender/line/financeAndECommerce/receipt-list.svg b/packages/iconify-collections/assets/vender/line/financeAndECommerce/receipt-list.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/financeAndECommerce/receipt-list.svg rename to packages/iconify-collections/assets/vender/line/financeAndECommerce/receipt-list.svg diff --git a/web/app/components/base/icons/assets/vender/line/financeAndECommerce/tag-01.svg b/packages/iconify-collections/assets/vender/line/financeAndECommerce/tag-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/financeAndECommerce/tag-01.svg rename to packages/iconify-collections/assets/vender/line/financeAndECommerce/tag-01.svg diff --git a/web/app/components/base/icons/assets/vender/line/financeAndECommerce/tag-03.svg b/packages/iconify-collections/assets/vender/line/financeAndECommerce/tag-03.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/financeAndECommerce/tag-03.svg rename to packages/iconify-collections/assets/vender/line/financeAndECommerce/tag-03.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/at-sign.svg b/packages/iconify-collections/assets/vender/line/general/at-sign.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/at-sign.svg rename to packages/iconify-collections/assets/vender/line/general/at-sign.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/bookmark.svg b/packages/iconify-collections/assets/vender/line/general/bookmark.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/bookmark.svg rename to packages/iconify-collections/assets/vender/line/general/bookmark.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/check-done-01.svg b/packages/iconify-collections/assets/vender/line/general/check-done-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/check-done-01.svg rename to packages/iconify-collections/assets/vender/line/general/check-done-01.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/check.svg b/packages/iconify-collections/assets/vender/line/general/check.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/check.svg rename to packages/iconify-collections/assets/vender/line/general/check.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/checklist-square.svg b/packages/iconify-collections/assets/vender/line/general/checklist-square.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/checklist-square.svg rename to packages/iconify-collections/assets/vender/line/general/checklist-square.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/code-assistant.svg b/packages/iconify-collections/assets/vender/line/general/code-assistant.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/code-assistant.svg rename to packages/iconify-collections/assets/vender/line/general/code-assistant.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/dots-grid.svg b/packages/iconify-collections/assets/vender/line/general/dots-grid.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/dots-grid.svg rename to packages/iconify-collections/assets/vender/line/general/dots-grid.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/edit-02.svg b/packages/iconify-collections/assets/vender/line/general/edit-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/edit-02.svg rename to packages/iconify-collections/assets/vender/line/general/edit-02.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/edit-04.svg b/packages/iconify-collections/assets/vender/line/general/edit-04.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/edit-04.svg rename to packages/iconify-collections/assets/vender/line/general/edit-04.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/edit-05.svg b/packages/iconify-collections/assets/vender/line/general/edit-05.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/edit-05.svg rename to packages/iconify-collections/assets/vender/line/general/edit-05.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/hash-02.svg b/packages/iconify-collections/assets/vender/line/general/hash-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/hash-02.svg rename to packages/iconify-collections/assets/vender/line/general/hash-02.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/info-circle.svg b/packages/iconify-collections/assets/vender/line/general/info-circle.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/info-circle.svg rename to packages/iconify-collections/assets/vender/line/general/info-circle.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/link-03.svg b/packages/iconify-collections/assets/vender/line/general/link-03.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/link-03.svg rename to packages/iconify-collections/assets/vender/line/general/link-03.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/link-external-02.svg b/packages/iconify-collections/assets/vender/line/general/link-external-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/link-external-02.svg rename to packages/iconify-collections/assets/vender/line/general/link-external-02.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/log-in-04.svg b/packages/iconify-collections/assets/vender/line/general/log-in-04.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/log-in-04.svg rename to packages/iconify-collections/assets/vender/line/general/log-in-04.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/log-out-01.svg b/packages/iconify-collections/assets/vender/line/general/log-out-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/log-out-01.svg rename to packages/iconify-collections/assets/vender/line/general/log-out-01.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/log-out-04.svg b/packages/iconify-collections/assets/vender/line/general/log-out-04.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/log-out-04.svg rename to packages/iconify-collections/assets/vender/line/general/log-out-04.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/magic-edit.svg b/packages/iconify-collections/assets/vender/line/general/magic-edit.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/magic-edit.svg rename to packages/iconify-collections/assets/vender/line/general/magic-edit.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/menu-01.svg b/packages/iconify-collections/assets/vender/line/general/menu-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/menu-01.svg rename to packages/iconify-collections/assets/vender/line/general/menu-01.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/pin-01.svg b/packages/iconify-collections/assets/vender/line/general/pin-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/pin-01.svg rename to packages/iconify-collections/assets/vender/line/general/pin-01.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/pin-02.svg b/packages/iconify-collections/assets/vender/line/general/pin-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/pin-02.svg rename to packages/iconify-collections/assets/vender/line/general/pin-02.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/plus-02.svg b/packages/iconify-collections/assets/vender/line/general/plus-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/plus-02.svg rename to packages/iconify-collections/assets/vender/line/general/plus-02.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/refresh.svg b/packages/iconify-collections/assets/vender/line/general/refresh.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/refresh.svg rename to packages/iconify-collections/assets/vender/line/general/refresh.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/search-menu.svg b/packages/iconify-collections/assets/vender/line/general/search-menu.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/search-menu.svg rename to packages/iconify-collections/assets/vender/line/general/search-menu.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/settings-01.svg b/packages/iconify-collections/assets/vender/line/general/settings-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/settings-01.svg rename to packages/iconify-collections/assets/vender/line/general/settings-01.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/settings-04.svg b/packages/iconify-collections/assets/vender/line/general/settings-04.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/settings-04.svg rename to packages/iconify-collections/assets/vender/line/general/settings-04.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/target-04.svg b/packages/iconify-collections/assets/vender/line/general/target-04.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/target-04.svg rename to packages/iconify-collections/assets/vender/line/general/target-04.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/upload-03.svg b/packages/iconify-collections/assets/vender/line/general/upload-03.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/upload-03.svg rename to packages/iconify-collections/assets/vender/line/general/upload-03.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/upload-cloud-01.svg b/packages/iconify-collections/assets/vender/line/general/upload-cloud-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/upload-cloud-01.svg rename to packages/iconify-collections/assets/vender/line/general/upload-cloud-01.svg diff --git a/web/app/components/base/icons/assets/vender/line/general/x.svg b/packages/iconify-collections/assets/vender/line/general/x.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/general/x.svg rename to packages/iconify-collections/assets/vender/line/general/x.svg diff --git a/web/app/components/base/icons/assets/vender/line/images/image-plus.svg b/packages/iconify-collections/assets/vender/line/images/image-plus.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/images/image-plus.svg rename to packages/iconify-collections/assets/vender/line/images/image-plus.svg diff --git a/web/app/components/base/icons/assets/vender/line/layout/align-left-01.svg b/packages/iconify-collections/assets/vender/line/layout/align-left-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/layout/align-left-01.svg rename to packages/iconify-collections/assets/vender/line/layout/align-left-01.svg diff --git a/web/app/components/base/icons/assets/vender/line/layout/align-right-01.svg b/packages/iconify-collections/assets/vender/line/layout/align-right-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/layout/align-right-01.svg rename to packages/iconify-collections/assets/vender/line/layout/align-right-01.svg diff --git a/web/app/components/base/icons/assets/vender/line/layout/grid-01.svg b/packages/iconify-collections/assets/vender/line/layout/grid-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/layout/grid-01.svg rename to packages/iconify-collections/assets/vender/line/layout/grid-01.svg diff --git a/web/app/components/base/icons/assets/vender/line/layout/layout-grid-02.svg b/packages/iconify-collections/assets/vender/line/layout/layout-grid-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/layout/layout-grid-02.svg rename to packages/iconify-collections/assets/vender/line/layout/layout-grid-02.svg diff --git a/web/app/components/base/icons/assets/vender/line/mediaAndDevices/microphone-01.svg b/packages/iconify-collections/assets/vender/line/mediaAndDevices/microphone-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/mediaAndDevices/microphone-01.svg rename to packages/iconify-collections/assets/vender/line/mediaAndDevices/microphone-01.svg diff --git a/web/app/components/base/icons/assets/vender/line/mediaAndDevices/play-circle.svg b/packages/iconify-collections/assets/vender/line/mediaAndDevices/play-circle.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/mediaAndDevices/play-circle.svg rename to packages/iconify-collections/assets/vender/line/mediaAndDevices/play-circle.svg diff --git a/web/app/components/base/icons/assets/vender/line/mediaAndDevices/sliders-h.svg b/packages/iconify-collections/assets/vender/line/mediaAndDevices/sliders-h.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/mediaAndDevices/sliders-h.svg rename to packages/iconify-collections/assets/vender/line/mediaAndDevices/sliders-h.svg diff --git a/web/app/components/base/icons/assets/vender/line/mediaAndDevices/speaker.svg b/packages/iconify-collections/assets/vender/line/mediaAndDevices/speaker.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/mediaAndDevices/speaker.svg rename to packages/iconify-collections/assets/vender/line/mediaAndDevices/speaker.svg diff --git a/web/app/components/base/icons/assets/vender/line/mediaAndDevices/stop-circle.svg b/packages/iconify-collections/assets/vender/line/mediaAndDevices/stop-circle.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/mediaAndDevices/stop-circle.svg rename to packages/iconify-collections/assets/vender/line/mediaAndDevices/stop-circle.svg diff --git a/web/app/components/base/icons/assets/vender/line/mediaAndDevices/stop.svg b/packages/iconify-collections/assets/vender/line/mediaAndDevices/stop.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/mediaAndDevices/stop.svg rename to packages/iconify-collections/assets/vender/line/mediaAndDevices/stop.svg diff --git a/web/app/components/base/icons/assets/vender/line/others/bubble-x.svg b/packages/iconify-collections/assets/vender/line/others/bubble-x.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/others/bubble-x.svg rename to packages/iconify-collections/assets/vender/line/others/bubble-x.svg diff --git a/web/app/components/base/icons/assets/vender/line/others/colors.svg b/packages/iconify-collections/assets/vender/line/others/colors.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/others/colors.svg rename to packages/iconify-collections/assets/vender/line/others/colors.svg diff --git a/web/app/components/base/icons/assets/vender/line/others/drag-handle.svg b/packages/iconify-collections/assets/vender/line/others/drag-handle.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/others/drag-handle.svg rename to packages/iconify-collections/assets/vender/line/others/drag-handle.svg diff --git a/web/app/components/base/icons/assets/vender/line/others/env.svg b/packages/iconify-collections/assets/vender/line/others/env.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/others/env.svg rename to packages/iconify-collections/assets/vender/line/others/env.svg diff --git a/web/app/components/base/icons/assets/vender/line/others/global-variable.svg b/packages/iconify-collections/assets/vender/line/others/global-variable.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/others/global-variable.svg rename to packages/iconify-collections/assets/vender/line/others/global-variable.svg diff --git a/web/app/components/base/icons/assets/vender/line/others/icon-3-dots.svg b/packages/iconify-collections/assets/vender/line/others/icon-3-dots.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/others/icon-3-dots.svg rename to packages/iconify-collections/assets/vender/line/others/icon-3-dots.svg diff --git a/web/app/components/base/icons/assets/vender/line/others/long-arrow-left.svg b/packages/iconify-collections/assets/vender/line/others/long-arrow-left.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/others/long-arrow-left.svg rename to packages/iconify-collections/assets/vender/line/others/long-arrow-left.svg diff --git a/web/app/components/base/icons/assets/vender/line/others/long-arrow-right.svg b/packages/iconify-collections/assets/vender/line/others/long-arrow-right.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/others/long-arrow-right.svg rename to packages/iconify-collections/assets/vender/line/others/long-arrow-right.svg diff --git a/web/app/components/base/icons/assets/vender/line/others/search-menu.svg b/packages/iconify-collections/assets/vender/line/others/search-menu.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/others/search-menu.svg rename to packages/iconify-collections/assets/vender/line/others/search-menu.svg diff --git a/web/app/components/base/icons/assets/vender/line/others/tools.svg b/packages/iconify-collections/assets/vender/line/others/tools.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/others/tools.svg rename to packages/iconify-collections/assets/vender/line/others/tools.svg diff --git a/web/app/components/base/icons/assets/vender/line/shapes/cube-outline.svg b/packages/iconify-collections/assets/vender/line/shapes/cube-outline.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/shapes/cube-outline.svg rename to packages/iconify-collections/assets/vender/line/shapes/cube-outline.svg diff --git a/web/app/components/base/icons/assets/vender/line/time/clock-fast-forward.svg b/packages/iconify-collections/assets/vender/line/time/clock-fast-forward.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/time/clock-fast-forward.svg rename to packages/iconify-collections/assets/vender/line/time/clock-fast-forward.svg diff --git a/web/app/components/base/icons/assets/vender/line/time/clock-play-slim.svg b/packages/iconify-collections/assets/vender/line/time/clock-play-slim.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/time/clock-play-slim.svg rename to packages/iconify-collections/assets/vender/line/time/clock-play-slim.svg diff --git a/web/app/components/base/icons/assets/vender/line/time/clock-play.svg b/packages/iconify-collections/assets/vender/line/time/clock-play.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/time/clock-play.svg rename to packages/iconify-collections/assets/vender/line/time/clock-play.svg diff --git a/web/app/components/base/icons/assets/vender/line/time/clock-refresh.svg b/packages/iconify-collections/assets/vender/line/time/clock-refresh.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/time/clock-refresh.svg rename to packages/iconify-collections/assets/vender/line/time/clock-refresh.svg diff --git a/web/app/components/base/icons/assets/vender/line/users/user-01.svg b/packages/iconify-collections/assets/vender/line/users/user-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/users/user-01.svg rename to packages/iconify-collections/assets/vender/line/users/user-01.svg diff --git a/web/app/components/base/icons/assets/vender/line/users/users-01.svg b/packages/iconify-collections/assets/vender/line/users/users-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/users/users-01.svg rename to packages/iconify-collections/assets/vender/line/users/users-01.svg diff --git a/web/app/components/base/icons/assets/vender/line/weather/stars-02.svg b/packages/iconify-collections/assets/vender/line/weather/stars-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/line/weather/stars-02.svg rename to packages/iconify-collections/assets/vender/line/weather/stars-02.svg diff --git a/web/app/components/base/icons/assets/vender/other/anthropic-text.svg b/packages/iconify-collections/assets/vender/other/anthropic-text.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/other/anthropic-text.svg rename to packages/iconify-collections/assets/vender/other/anthropic-text.svg diff --git a/web/app/components/base/icons/assets/vender/other/generator.svg b/packages/iconify-collections/assets/vender/other/generator.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/other/generator.svg rename to packages/iconify-collections/assets/vender/other/generator.svg diff --git a/web/app/components/base/icons/assets/vender/other/group.svg b/packages/iconify-collections/assets/vender/other/group.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/other/group.svg rename to packages/iconify-collections/assets/vender/other/group.svg diff --git a/web/app/components/base/icons/assets/vender/other/hourglass-shape.svg b/packages/iconify-collections/assets/vender/other/hourglass-shape.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/other/hourglass-shape.svg rename to packages/iconify-collections/assets/vender/other/hourglass-shape.svg diff --git a/web/app/components/base/icons/assets/vender/other/mcp.svg b/packages/iconify-collections/assets/vender/other/mcp.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/other/mcp.svg rename to packages/iconify-collections/assets/vender/other/mcp.svg diff --git a/web/app/components/base/icons/assets/vender/other/no-tool-placeholder.svg b/packages/iconify-collections/assets/vender/other/no-tool-placeholder.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/other/no-tool-placeholder.svg rename to packages/iconify-collections/assets/vender/other/no-tool-placeholder.svg diff --git a/web/app/components/base/icons/assets/vender/other/openai.svg b/packages/iconify-collections/assets/vender/other/openai.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/other/openai.svg rename to packages/iconify-collections/assets/vender/other/openai.svg diff --git a/web/app/components/base/icons/assets/vender/other/replay-line.svg b/packages/iconify-collections/assets/vender/other/replay-line.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/other/replay-line.svg rename to packages/iconify-collections/assets/vender/other/replay-line.svg diff --git a/web/app/components/base/icons/assets/vender/other/square-checklist.svg b/packages/iconify-collections/assets/vender/other/square-checklist.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/other/square-checklist.svg rename to packages/iconify-collections/assets/vender/other/square-checklist.svg diff --git a/web/app/components/base/icons/assets/vender/pipeline/input-field.svg b/packages/iconify-collections/assets/vender/pipeline/input-field.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/pipeline/input-field.svg rename to packages/iconify-collections/assets/vender/pipeline/input-field.svg diff --git a/web/app/components/base/icons/assets/vender/pipeline/pipeline-fill.svg b/packages/iconify-collections/assets/vender/pipeline/pipeline-fill.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/pipeline/pipeline-fill.svg rename to packages/iconify-collections/assets/vender/pipeline/pipeline-fill.svg diff --git a/web/app/components/base/icons/assets/vender/pipeline/pipeline-line.svg b/packages/iconify-collections/assets/vender/pipeline/pipeline-line.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/pipeline/pipeline-line.svg rename to packages/iconify-collections/assets/vender/pipeline/pipeline-line.svg diff --git a/web/app/components/base/icons/assets/vender/plugin/box-sparkle-fill.svg b/packages/iconify-collections/assets/vender/plugin/box-sparkle-fill.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/plugin/box-sparkle-fill.svg rename to packages/iconify-collections/assets/vender/plugin/box-sparkle-fill.svg diff --git a/web/app/components/base/icons/assets/vender/plugin/left-corner.svg b/packages/iconify-collections/assets/vender/plugin/left-corner.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/plugin/left-corner.svg rename to packages/iconify-collections/assets/vender/plugin/left-corner.svg diff --git a/web/app/components/base/icons/assets/vender/plugin/trigger.svg b/packages/iconify-collections/assets/vender/plugin/trigger.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/plugin/trigger.svg rename to packages/iconify-collections/assets/vender/plugin/trigger.svg diff --git a/web/app/components/base/icons/assets/vender/solid/FinanceAndECommerce/gold-coin.svg b/packages/iconify-collections/assets/vender/solid/FinanceAndECommerce/gold-coin.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/FinanceAndECommerce/gold-coin.svg rename to packages/iconify-collections/assets/vender/solid/FinanceAndECommerce/gold-coin.svg diff --git a/web/app/components/base/icons/assets/vender/solid/FinanceAndECommerce/scales-02.svg b/packages/iconify-collections/assets/vender/solid/FinanceAndECommerce/scales-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/FinanceAndECommerce/scales-02.svg rename to packages/iconify-collections/assets/vender/solid/FinanceAndECommerce/scales-02.svg diff --git a/web/app/components/base/icons/assets/vender/solid/alertsAndFeedback/alert-triangle.svg b/packages/iconify-collections/assets/vender/solid/alertsAndFeedback/alert-triangle.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/alertsAndFeedback/alert-triangle.svg rename to packages/iconify-collections/assets/vender/solid/alertsAndFeedback/alert-triangle.svg diff --git a/web/app/components/base/icons/assets/vender/solid/arrows/arrow-down-double-line.svg b/packages/iconify-collections/assets/vender/solid/arrows/arrow-down-double-line.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/arrows/arrow-down-double-line.svg rename to packages/iconify-collections/assets/vender/solid/arrows/arrow-down-double-line.svg diff --git a/web/app/components/base/icons/assets/vender/solid/arrows/arrow-down-round-fill.svg b/packages/iconify-collections/assets/vender/solid/arrows/arrow-down-round-fill.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/arrows/arrow-down-round-fill.svg rename to packages/iconify-collections/assets/vender/solid/arrows/arrow-down-round-fill.svg diff --git a/web/app/components/base/icons/assets/vender/solid/arrows/arrow-up-double-line.svg b/packages/iconify-collections/assets/vender/solid/arrows/arrow-up-double-line.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/arrows/arrow-up-double-line.svg rename to packages/iconify-collections/assets/vender/solid/arrows/arrow-up-double-line.svg diff --git a/web/app/components/base/icons/assets/vender/solid/arrows/chevron-down.svg b/packages/iconify-collections/assets/vender/solid/arrows/chevron-down.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/arrows/chevron-down.svg rename to packages/iconify-collections/assets/vender/solid/arrows/chevron-down.svg diff --git a/web/app/components/base/icons/assets/vender/solid/arrows/high-priority.svg b/packages/iconify-collections/assets/vender/solid/arrows/high-priority.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/arrows/high-priority.svg rename to packages/iconify-collections/assets/vender/solid/arrows/high-priority.svg diff --git a/web/app/components/base/icons/assets/vender/solid/communication/ai-text.svg b/packages/iconify-collections/assets/vender/solid/communication/ai-text.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/communication/ai-text.svg rename to packages/iconify-collections/assets/vender/solid/communication/ai-text.svg diff --git a/web/app/components/base/icons/assets/vender/solid/communication/bubble-text-mod.svg b/packages/iconify-collections/assets/vender/solid/communication/bubble-text-mod.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/communication/bubble-text-mod.svg rename to packages/iconify-collections/assets/vender/solid/communication/bubble-text-mod.svg diff --git a/web/app/components/base/icons/assets/vender/solid/communication/chat-bot.svg b/packages/iconify-collections/assets/vender/solid/communication/chat-bot.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/communication/chat-bot.svg rename to packages/iconify-collections/assets/vender/solid/communication/chat-bot.svg diff --git a/web/app/components/base/icons/assets/vender/solid/communication/cute-robot.svg b/packages/iconify-collections/assets/vender/solid/communication/cute-robot.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/communication/cute-robot.svg rename to packages/iconify-collections/assets/vender/solid/communication/cute-robot.svg diff --git a/web/app/components/base/icons/assets/vender/solid/communication/edit-list.svg b/packages/iconify-collections/assets/vender/solid/communication/edit-list.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/communication/edit-list.svg rename to packages/iconify-collections/assets/vender/solid/communication/edit-list.svg diff --git a/web/app/components/base/icons/assets/vender/solid/communication/list-sparkle.svg b/packages/iconify-collections/assets/vender/solid/communication/list-sparkle.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/communication/list-sparkle.svg rename to packages/iconify-collections/assets/vender/solid/communication/list-sparkle.svg diff --git a/web/app/components/base/icons/assets/vender/solid/communication/logic.svg b/packages/iconify-collections/assets/vender/solid/communication/logic.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/communication/logic.svg rename to packages/iconify-collections/assets/vender/solid/communication/logic.svg diff --git a/web/app/components/base/icons/assets/vender/solid/communication/message-dots-circle.svg b/packages/iconify-collections/assets/vender/solid/communication/message-dots-circle.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/communication/message-dots-circle.svg rename to packages/iconify-collections/assets/vender/solid/communication/message-dots-circle.svg diff --git a/web/app/components/base/icons/assets/vender/solid/communication/message-fast.svg b/packages/iconify-collections/assets/vender/solid/communication/message-fast.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/communication/message-fast.svg rename to packages/iconify-collections/assets/vender/solid/communication/message-fast.svg diff --git a/web/app/components/base/icons/assets/vender/solid/communication/message-heart-circle.svg b/packages/iconify-collections/assets/vender/solid/communication/message-heart-circle.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/communication/message-heart-circle.svg rename to packages/iconify-collections/assets/vender/solid/communication/message-heart-circle.svg diff --git a/web/app/components/base/icons/assets/vender/solid/communication/message-smile-square.svg b/packages/iconify-collections/assets/vender/solid/communication/message-smile-square.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/communication/message-smile-square.svg rename to packages/iconify-collections/assets/vender/solid/communication/message-smile-square.svg diff --git a/web/app/components/base/icons/assets/vender/solid/communication/send-03.svg b/packages/iconify-collections/assets/vender/solid/communication/send-03.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/communication/send-03.svg rename to packages/iconify-collections/assets/vender/solid/communication/send-03.svg diff --git a/web/app/components/base/icons/assets/vender/solid/development/api-connection-mod.svg b/packages/iconify-collections/assets/vender/solid/development/api-connection-mod.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/development/api-connection-mod.svg rename to packages/iconify-collections/assets/vender/solid/development/api-connection-mod.svg diff --git a/web/app/components/base/icons/assets/vender/solid/development/api-connection.svg b/packages/iconify-collections/assets/vender/solid/development/api-connection.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/development/api-connection.svg rename to packages/iconify-collections/assets/vender/solid/development/api-connection.svg diff --git a/web/app/components/base/icons/assets/vender/solid/development/bar-chart-square-02.svg b/packages/iconify-collections/assets/vender/solid/development/bar-chart-square-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/development/bar-chart-square-02.svg rename to packages/iconify-collections/assets/vender/solid/development/bar-chart-square-02.svg diff --git a/web/app/components/base/icons/assets/vender/solid/development/container.svg b/packages/iconify-collections/assets/vender/solid/development/container.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/development/container.svg rename to packages/iconify-collections/assets/vender/solid/development/container.svg diff --git a/web/app/components/base/icons/assets/vender/solid/development/database-02.svg b/packages/iconify-collections/assets/vender/solid/development/database-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/development/database-02.svg rename to packages/iconify-collections/assets/vender/solid/development/database-02.svg diff --git a/web/app/components/base/icons/assets/vender/solid/development/database-03.svg b/packages/iconify-collections/assets/vender/solid/development/database-03.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/development/database-03.svg rename to packages/iconify-collections/assets/vender/solid/development/database-03.svg diff --git a/web/app/components/base/icons/assets/vender/solid/development/file-heart-02.svg b/packages/iconify-collections/assets/vender/solid/development/file-heart-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/development/file-heart-02.svg rename to packages/iconify-collections/assets/vender/solid/development/file-heart-02.svg diff --git a/web/app/components/base/icons/assets/vender/solid/development/pattern-recognition.svg b/packages/iconify-collections/assets/vender/solid/development/pattern-recognition.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/development/pattern-recognition.svg rename to packages/iconify-collections/assets/vender/solid/development/pattern-recognition.svg diff --git a/web/app/components/base/icons/assets/vender/solid/development/prompt-engineering.svg b/packages/iconify-collections/assets/vender/solid/development/prompt-engineering.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/development/prompt-engineering.svg rename to packages/iconify-collections/assets/vender/solid/development/prompt-engineering.svg diff --git a/web/app/components/base/icons/assets/vender/solid/development/puzzle-piece-01.svg b/packages/iconify-collections/assets/vender/solid/development/puzzle-piece-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/development/puzzle-piece-01.svg rename to packages/iconify-collections/assets/vender/solid/development/puzzle-piece-01.svg diff --git a/web/app/components/base/icons/assets/vender/solid/development/semantic.svg b/packages/iconify-collections/assets/vender/solid/development/semantic.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/development/semantic.svg rename to packages/iconify-collections/assets/vender/solid/development/semantic.svg diff --git a/web/app/components/base/icons/assets/vender/solid/development/terminal-square.svg b/packages/iconify-collections/assets/vender/solid/development/terminal-square.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/development/terminal-square.svg rename to packages/iconify-collections/assets/vender/solid/development/terminal-square.svg diff --git a/web/app/components/base/icons/assets/vender/solid/development/variable-02.svg b/packages/iconify-collections/assets/vender/solid/development/variable-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/development/variable-02.svg rename to packages/iconify-collections/assets/vender/solid/development/variable-02.svg diff --git a/web/app/components/base/icons/assets/vender/solid/editor/brush-01.svg b/packages/iconify-collections/assets/vender/solid/editor/brush-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/editor/brush-01.svg rename to packages/iconify-collections/assets/vender/solid/editor/brush-01.svg diff --git a/web/app/components/base/icons/assets/vender/solid/editor/citations.svg b/packages/iconify-collections/assets/vender/solid/editor/citations.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/editor/citations.svg rename to packages/iconify-collections/assets/vender/solid/editor/citations.svg diff --git a/web/app/components/base/icons/assets/vender/solid/editor/colors.svg b/packages/iconify-collections/assets/vender/solid/editor/colors.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/editor/colors.svg rename to packages/iconify-collections/assets/vender/solid/editor/colors.svg diff --git a/web/app/components/base/icons/assets/vender/solid/editor/paragraph.svg b/packages/iconify-collections/assets/vender/solid/editor/paragraph.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/editor/paragraph.svg rename to packages/iconify-collections/assets/vender/solid/editor/paragraph.svg diff --git a/web/app/components/base/icons/assets/vender/solid/editor/type-square.svg b/packages/iconify-collections/assets/vender/solid/editor/type-square.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/editor/type-square.svg rename to packages/iconify-collections/assets/vender/solid/editor/type-square.svg diff --git a/web/app/components/base/icons/assets/vender/solid/education/beaker-02.svg b/packages/iconify-collections/assets/vender/solid/education/beaker-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/education/beaker-02.svg rename to packages/iconify-collections/assets/vender/solid/education/beaker-02.svg diff --git a/web/app/components/base/icons/assets/vender/solid/education/bubble-text.svg b/packages/iconify-collections/assets/vender/solid/education/bubble-text.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/education/bubble-text.svg rename to packages/iconify-collections/assets/vender/solid/education/bubble-text.svg diff --git a/web/app/components/base/icons/assets/vender/solid/education/heart-02.svg b/packages/iconify-collections/assets/vender/solid/education/heart-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/education/heart-02.svg rename to packages/iconify-collections/assets/vender/solid/education/heart-02.svg diff --git a/web/app/components/base/icons/assets/vender/solid/education/unblur.svg b/packages/iconify-collections/assets/vender/solid/education/unblur.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/education/unblur.svg rename to packages/iconify-collections/assets/vender/solid/education/unblur.svg diff --git a/web/app/components/base/icons/assets/vender/solid/files/file-05.svg b/packages/iconify-collections/assets/vender/solid/files/file-05.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/files/file-05.svg rename to packages/iconify-collections/assets/vender/solid/files/file-05.svg diff --git a/web/app/components/base/icons/assets/vender/solid/files/file-search-02.svg b/packages/iconify-collections/assets/vender/solid/files/file-search-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/files/file-search-02.svg rename to packages/iconify-collections/assets/vender/solid/files/file-search-02.svg diff --git a/web/app/components/base/icons/assets/vender/solid/files/file-zip.svg b/packages/iconify-collections/assets/vender/solid/files/file-zip.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/files/file-zip.svg rename to packages/iconify-collections/assets/vender/solid/files/file-zip.svg diff --git a/web/app/components/base/icons/assets/vender/solid/files/folder.svg b/packages/iconify-collections/assets/vender/solid/files/folder.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/files/folder.svg rename to packages/iconify-collections/assets/vender/solid/files/folder.svg diff --git a/web/app/components/base/icons/assets/vender/solid/general/answer-triangle.svg b/packages/iconify-collections/assets/vender/solid/general/answer-triangle.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/general/answer-triangle.svg rename to packages/iconify-collections/assets/vender/solid/general/answer-triangle.svg diff --git a/web/app/components/base/icons/assets/vender/solid/general/arrow-down-round-fill.svg b/packages/iconify-collections/assets/vender/solid/general/arrow-down-round-fill.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/general/arrow-down-round-fill.svg rename to packages/iconify-collections/assets/vender/solid/general/arrow-down-round-fill.svg diff --git a/web/app/components/base/icons/assets/vender/solid/general/check-circle.svg b/packages/iconify-collections/assets/vender/solid/general/check-circle.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/general/check-circle.svg rename to packages/iconify-collections/assets/vender/solid/general/check-circle.svg diff --git a/web/app/components/base/icons/assets/vender/solid/general/check-done-01.svg b/packages/iconify-collections/assets/vender/solid/general/check-done-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/general/check-done-01.svg rename to packages/iconify-collections/assets/vender/solid/general/check-done-01.svg diff --git a/web/app/components/base/icons/assets/vender/solid/general/download-02.svg b/packages/iconify-collections/assets/vender/solid/general/download-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/general/download-02.svg rename to packages/iconify-collections/assets/vender/solid/general/download-02.svg diff --git a/web/app/components/base/icons/assets/vender/solid/general/edit-03.svg b/packages/iconify-collections/assets/vender/solid/general/edit-03.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/general/edit-03.svg rename to packages/iconify-collections/assets/vender/solid/general/edit-03.svg diff --git a/web/app/components/base/icons/assets/vender/solid/general/edit-04.svg b/packages/iconify-collections/assets/vender/solid/general/edit-04.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/general/edit-04.svg rename to packages/iconify-collections/assets/vender/solid/general/edit-04.svg diff --git a/web/app/components/base/icons/assets/vender/solid/general/eye.svg b/packages/iconify-collections/assets/vender/solid/general/eye.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/general/eye.svg rename to packages/iconify-collections/assets/vender/solid/general/eye.svg diff --git a/web/app/components/base/icons/assets/vender/solid/general/github.svg b/packages/iconify-collections/assets/vender/solid/general/github.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/general/github.svg rename to packages/iconify-collections/assets/vender/solid/general/github.svg diff --git a/web/app/components/base/icons/assets/vender/solid/general/message-clock-circle.svg b/packages/iconify-collections/assets/vender/solid/general/message-clock-circle.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/general/message-clock-circle.svg rename to packages/iconify-collections/assets/vender/solid/general/message-clock-circle.svg diff --git a/web/app/components/base/icons/assets/vender/solid/general/plus-circle.svg b/packages/iconify-collections/assets/vender/solid/general/plus-circle.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/general/plus-circle.svg rename to packages/iconify-collections/assets/vender/solid/general/plus-circle.svg diff --git a/web/app/components/base/icons/assets/vender/solid/general/question-triangle.svg b/packages/iconify-collections/assets/vender/solid/general/question-triangle.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/general/question-triangle.svg rename to packages/iconify-collections/assets/vender/solid/general/question-triangle.svg diff --git a/web/app/components/base/icons/assets/vender/solid/general/search-md.svg b/packages/iconify-collections/assets/vender/solid/general/search-md.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/general/search-md.svg rename to packages/iconify-collections/assets/vender/solid/general/search-md.svg diff --git a/web/app/components/base/icons/assets/vender/solid/general/target-04.svg b/packages/iconify-collections/assets/vender/solid/general/target-04.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/general/target-04.svg rename to packages/iconify-collections/assets/vender/solid/general/target-04.svg diff --git a/web/app/components/base/icons/assets/vender/solid/general/tool-03.svg b/packages/iconify-collections/assets/vender/solid/general/tool-03.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/general/tool-03.svg rename to packages/iconify-collections/assets/vender/solid/general/tool-03.svg diff --git a/web/app/components/base/icons/assets/vender/solid/general/x-circle.svg b/packages/iconify-collections/assets/vender/solid/general/x-circle.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/general/x-circle.svg rename to packages/iconify-collections/assets/vender/solid/general/x-circle.svg diff --git a/web/app/components/base/icons/assets/vender/solid/general/zap-fast.svg b/packages/iconify-collections/assets/vender/solid/general/zap-fast.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/general/zap-fast.svg rename to packages/iconify-collections/assets/vender/solid/general/zap-fast.svg diff --git a/web/app/components/base/icons/assets/vender/solid/general/zap-narrow.svg b/packages/iconify-collections/assets/vender/solid/general/zap-narrow.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/general/zap-narrow.svg rename to packages/iconify-collections/assets/vender/solid/general/zap-narrow.svg diff --git a/web/app/components/base/icons/assets/vender/solid/layout/grid-01.svg b/packages/iconify-collections/assets/vender/solid/layout/grid-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/layout/grid-01.svg rename to packages/iconify-collections/assets/vender/solid/layout/grid-01.svg diff --git a/web/app/components/base/icons/assets/vender/solid/mediaAndDevices/audio-support-icon.svg b/packages/iconify-collections/assets/vender/solid/mediaAndDevices/audio-support-icon.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/mediaAndDevices/audio-support-icon.svg rename to packages/iconify-collections/assets/vender/solid/mediaAndDevices/audio-support-icon.svg diff --git a/web/app/components/base/icons/assets/vender/solid/mediaAndDevices/document-support-icon.svg b/packages/iconify-collections/assets/vender/solid/mediaAndDevices/document-support-icon.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/mediaAndDevices/document-support-icon.svg rename to packages/iconify-collections/assets/vender/solid/mediaAndDevices/document-support-icon.svg diff --git a/web/app/components/base/icons/assets/vender/solid/mediaAndDevices/magic-box.svg b/packages/iconify-collections/assets/vender/solid/mediaAndDevices/magic-box.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/mediaAndDevices/magic-box.svg rename to packages/iconify-collections/assets/vender/solid/mediaAndDevices/magic-box.svg diff --git a/web/app/components/base/icons/assets/vender/solid/mediaAndDevices/magic-eyes.svg b/packages/iconify-collections/assets/vender/solid/mediaAndDevices/magic-eyes.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/mediaAndDevices/magic-eyes.svg rename to packages/iconify-collections/assets/vender/solid/mediaAndDevices/magic-eyes.svg diff --git a/web/app/components/base/icons/assets/vender/solid/mediaAndDevices/magic-wand.svg b/packages/iconify-collections/assets/vender/solid/mediaAndDevices/magic-wand.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/mediaAndDevices/magic-wand.svg rename to packages/iconify-collections/assets/vender/solid/mediaAndDevices/magic-wand.svg diff --git a/web/app/components/base/icons/assets/vender/solid/mediaAndDevices/microphone-01.svg b/packages/iconify-collections/assets/vender/solid/mediaAndDevices/microphone-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/mediaAndDevices/microphone-01.svg rename to packages/iconify-collections/assets/vender/solid/mediaAndDevices/microphone-01.svg diff --git a/web/app/components/base/icons/assets/vender/solid/mediaAndDevices/play.svg b/packages/iconify-collections/assets/vender/solid/mediaAndDevices/play.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/mediaAndDevices/play.svg rename to packages/iconify-collections/assets/vender/solid/mediaAndDevices/play.svg diff --git a/web/app/components/base/icons/assets/vender/solid/mediaAndDevices/robot.svg b/packages/iconify-collections/assets/vender/solid/mediaAndDevices/robot.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/mediaAndDevices/robot.svg rename to packages/iconify-collections/assets/vender/solid/mediaAndDevices/robot.svg diff --git a/web/app/components/base/icons/assets/vender/solid/mediaAndDevices/sliders-02.svg b/packages/iconify-collections/assets/vender/solid/mediaAndDevices/sliders-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/mediaAndDevices/sliders-02.svg rename to packages/iconify-collections/assets/vender/solid/mediaAndDevices/sliders-02.svg diff --git a/web/app/components/base/icons/assets/vender/solid/mediaAndDevices/speaker.svg b/packages/iconify-collections/assets/vender/solid/mediaAndDevices/speaker.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/mediaAndDevices/speaker.svg rename to packages/iconify-collections/assets/vender/solid/mediaAndDevices/speaker.svg diff --git a/web/app/components/base/icons/assets/vender/solid/mediaAndDevices/stop-circle.svg b/packages/iconify-collections/assets/vender/solid/mediaAndDevices/stop-circle.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/mediaAndDevices/stop-circle.svg rename to packages/iconify-collections/assets/vender/solid/mediaAndDevices/stop-circle.svg diff --git a/web/app/components/base/icons/assets/vender/solid/mediaAndDevices/video-support-icon.svg b/packages/iconify-collections/assets/vender/solid/mediaAndDevices/video-support-icon.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/mediaAndDevices/video-support-icon.svg rename to packages/iconify-collections/assets/vender/solid/mediaAndDevices/video-support-icon.svg diff --git a/web/app/components/base/icons/assets/vender/solid/security/lock-01.svg b/packages/iconify-collections/assets/vender/solid/security/lock-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/security/lock-01.svg rename to packages/iconify-collections/assets/vender/solid/security/lock-01.svg diff --git a/web/app/components/base/icons/assets/vender/solid/shapes/corner.svg b/packages/iconify-collections/assets/vender/solid/shapes/corner.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/shapes/corner.svg rename to packages/iconify-collections/assets/vender/solid/shapes/corner.svg diff --git a/web/app/components/base/icons/assets/vender/solid/shapes/star-04.svg b/packages/iconify-collections/assets/vender/solid/shapes/star-04.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/shapes/star-04.svg rename to packages/iconify-collections/assets/vender/solid/shapes/star-04.svg diff --git a/web/app/components/base/icons/assets/vender/solid/shapes/star-06.svg b/packages/iconify-collections/assets/vender/solid/shapes/star-06.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/shapes/star-06.svg rename to packages/iconify-collections/assets/vender/solid/shapes/star-06.svg diff --git a/web/app/components/base/icons/assets/vender/solid/users/user-01.svg b/packages/iconify-collections/assets/vender/solid/users/user-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/users/user-01.svg rename to packages/iconify-collections/assets/vender/solid/users/user-01.svg diff --git a/web/app/components/base/icons/assets/vender/solid/users/user-edit-02.svg b/packages/iconify-collections/assets/vender/solid/users/user-edit-02.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/users/user-edit-02.svg rename to packages/iconify-collections/assets/vender/solid/users/user-edit-02.svg diff --git a/web/app/components/base/icons/assets/vender/solid/users/users-01.svg b/packages/iconify-collections/assets/vender/solid/users/users-01.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/users/users-01.svg rename to packages/iconify-collections/assets/vender/solid/users/users-01.svg diff --git a/web/app/components/base/icons/assets/vender/solid/users/users-plus.svg b/packages/iconify-collections/assets/vender/solid/users/users-plus.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/solid/users/users-plus.svg rename to packages/iconify-collections/assets/vender/solid/users/users-plus.svg diff --git a/web/app/components/base/icons/assets/vender/system/auto-update-line.svg b/packages/iconify-collections/assets/vender/system/auto-update-line.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/system/auto-update-line.svg rename to packages/iconify-collections/assets/vender/system/auto-update-line.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/agent.svg b/packages/iconify-collections/assets/vender/workflow/agent.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/agent.svg rename to packages/iconify-collections/assets/vender/workflow/agent.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/answer.svg b/packages/iconify-collections/assets/vender/workflow/answer.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/answer.svg rename to packages/iconify-collections/assets/vender/workflow/answer.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/api-aggregate.svg b/packages/iconify-collections/assets/vender/workflow/api-aggregate.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/api-aggregate.svg rename to packages/iconify-collections/assets/vender/workflow/api-aggregate.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/assigner.svg b/packages/iconify-collections/assets/vender/workflow/assigner.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/assigner.svg rename to packages/iconify-collections/assets/vender/workflow/assigner.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/asterisk.svg b/packages/iconify-collections/assets/vender/workflow/asterisk.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/asterisk.svg rename to packages/iconify-collections/assets/vender/workflow/asterisk.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/calendar-check-line.svg b/packages/iconify-collections/assets/vender/workflow/calendar-check-line.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/calendar-check-line.svg rename to packages/iconify-collections/assets/vender/workflow/calendar-check-line.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/code.svg b/packages/iconify-collections/assets/vender/workflow/code.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/code.svg rename to packages/iconify-collections/assets/vender/workflow/code.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/datasource.svg b/packages/iconify-collections/assets/vender/workflow/datasource.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/datasource.svg rename to packages/iconify-collections/assets/vender/workflow/datasource.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/docs-extractor.svg b/packages/iconify-collections/assets/vender/workflow/docs-extractor.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/docs-extractor.svg rename to packages/iconify-collections/assets/vender/workflow/docs-extractor.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/end.svg b/packages/iconify-collections/assets/vender/workflow/end.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/end.svg rename to packages/iconify-collections/assets/vender/workflow/end.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/home.svg b/packages/iconify-collections/assets/vender/workflow/home.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/home.svg rename to packages/iconify-collections/assets/vender/workflow/home.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/http.svg b/packages/iconify-collections/assets/vender/workflow/http.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/http.svg rename to packages/iconify-collections/assets/vender/workflow/http.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/human-in-loop.svg b/packages/iconify-collections/assets/vender/workflow/human-in-loop.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/human-in-loop.svg rename to packages/iconify-collections/assets/vender/workflow/human-in-loop.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/if-else.svg b/packages/iconify-collections/assets/vender/workflow/if-else.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/if-else.svg rename to packages/iconify-collections/assets/vender/workflow/if-else.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/iteration-start.svg b/packages/iconify-collections/assets/vender/workflow/iteration-start.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/iteration-start.svg rename to packages/iconify-collections/assets/vender/workflow/iteration-start.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/iteration.svg b/packages/iconify-collections/assets/vender/workflow/iteration.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/iteration.svg rename to packages/iconify-collections/assets/vender/workflow/iteration.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/jinja.svg b/packages/iconify-collections/assets/vender/workflow/jinja.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/jinja.svg rename to packages/iconify-collections/assets/vender/workflow/jinja.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/knowledge-base.svg b/packages/iconify-collections/assets/vender/workflow/knowledge-base.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/knowledge-base.svg rename to packages/iconify-collections/assets/vender/workflow/knowledge-base.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/knowledge-retrieval.svg b/packages/iconify-collections/assets/vender/workflow/knowledge-retrieval.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/knowledge-retrieval.svg rename to packages/iconify-collections/assets/vender/workflow/knowledge-retrieval.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/list-filter.svg b/packages/iconify-collections/assets/vender/workflow/list-filter.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/list-filter.svg rename to packages/iconify-collections/assets/vender/workflow/list-filter.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/llm.svg b/packages/iconify-collections/assets/vender/workflow/llm.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/llm.svg rename to packages/iconify-collections/assets/vender/workflow/llm.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/loop-end.svg b/packages/iconify-collections/assets/vender/workflow/loop-end.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/loop-end.svg rename to packages/iconify-collections/assets/vender/workflow/loop-end.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/loop.svg b/packages/iconify-collections/assets/vender/workflow/loop.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/loop.svg rename to packages/iconify-collections/assets/vender/workflow/loop.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/parameter-extractor.svg b/packages/iconify-collections/assets/vender/workflow/parameter-extractor.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/parameter-extractor.svg rename to packages/iconify-collections/assets/vender/workflow/parameter-extractor.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/question-classifier.svg b/packages/iconify-collections/assets/vender/workflow/question-classifier.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/question-classifier.svg rename to packages/iconify-collections/assets/vender/workflow/question-classifier.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/schedule.svg b/packages/iconify-collections/assets/vender/workflow/schedule.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/schedule.svg rename to packages/iconify-collections/assets/vender/workflow/schedule.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/templating-transform.svg b/packages/iconify-collections/assets/vender/workflow/templating-transform.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/templating-transform.svg rename to packages/iconify-collections/assets/vender/workflow/templating-transform.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/trigger-all.svg b/packages/iconify-collections/assets/vender/workflow/trigger-all.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/trigger-all.svg rename to packages/iconify-collections/assets/vender/workflow/trigger-all.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/variable-x.svg b/packages/iconify-collections/assets/vender/workflow/variable-x.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/variable-x.svg rename to packages/iconify-collections/assets/vender/workflow/variable-x.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/webhook-line.svg b/packages/iconify-collections/assets/vender/workflow/webhook-line.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/webhook-line.svg rename to packages/iconify-collections/assets/vender/workflow/webhook-line.svg diff --git a/web/app/components/base/icons/assets/vender/workflow/window-cursor.svg b/packages/iconify-collections/assets/vender/workflow/window-cursor.svg similarity index 100% rename from web/app/components/base/icons/assets/vender/workflow/window-cursor.svg rename to packages/iconify-collections/assets/vender/workflow/window-cursor.svg diff --git a/packages/iconify-collections/custom-public/chars.json b/packages/iconify-collections/custom-public/chars.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/packages/iconify-collections/custom-public/chars.json @@ -0,0 +1 @@ +{} diff --git a/packages/iconify-collections/custom-public/icons.json b/packages/iconify-collections/custom-public/icons.json new file mode 100644 index 0000000000..347b6145e2 --- /dev/null +++ b/packages/iconify-collections/custom-public/icons.json @@ -0,0 +1,572 @@ +{ + "prefix": "custom-public", + "lastModified": 1775115796, + "icons": { + "avatar-user": { + "body": "", + "width": 512, + "height": 512 + }, + "billing-ar-cube-1": { + "body": "", + "width": 28 + }, + "billing-asterisk": { + "body": "", + "width": 28 + }, + "billing-aws-marketplace-dark": { + "body": "", + "width": 126, + "height": 25 + }, + "billing-aws-marketplace-light": { + "body": "", + "width": 126, + "height": 24 + }, + "billing-azure": { + "body": "", + "width": 21, + "height": 20 + }, + "billing-buildings": { + "body": "" + }, + "billing-diamond": { + "body": "" + }, + "billing-google-cloud": { + "body": "", + "width": 22, + "height": 18 + }, + "billing-group-2": { + "body": "" + }, + "billing-keyframe": { + "body": "" + }, + "billing-sparkles-soft": { + "body": "", + "width": 13, + "height": 13 + }, + "common-d": { + "body": "" + }, + "common-diagonal-dividing-line": { + "body": "", + "width": 7, + "height": 20 + }, + "common-dify": { + "body": "", + "width": 50, + "height": 26 + }, + "common-gdpr": { + "body": "", + "width": 23, + "height": 28 + }, + "common-github": { + "body": "", + "width": 18, + "height": 18 + }, + "common-highlight": { + "body": "", + "width": 46, + "height": 24 + }, + "common-iso": { + "body": "", + "width": 64, + "height": 64 + }, + "common-line-3": { + "body": "", + "width": 5, + "height": 12 + }, + "common-lock": { + "body": "" + }, + "common-message-chat-square": { + "body": "" + }, + "common-multi-path-retrieval": { + "body": "", + "width": 36, + "height": 36 + }, + "common-n-to-1-retrieval": { + "body": "", + "width": 36, + "height": 36 + }, + "common-notion": { + "body": "", + "width": 20, + "height": 20 + }, + "common-soc2": { + "body": "", + "width": 28, + "height": 28 + }, + "common-sparkles-soft": { + "body": "", + "width": 14, + "height": 14 + }, + "common-sparkles-soft-accent": { + "body": "" + }, + "education-triangle": { + "body": "", + "height": 22 + }, + "files-csv": { + "body": "" + }, + "files-doc": { + "body": "" + }, + "files-docx": { + "body": "" + }, + "files-html": { + "body": "" + }, + "files-json": { + "body": "" + }, + "files-md": { + "body": "" + }, + "files-pdf": { + "body": "" + }, + "files-txt": { + "body": "" + }, + "files-unknown": { + "body": "" + }, + "files-xlsx": { + "body": "", + "width": 24, + "height": 26 + }, + "files-yaml": { + "body": "", + "width": 24, + "height": 26 + }, + "knowledge-file": { + "body": "", + "width": 16, + "height": 16 + }, + "knowledge-option-card-effect-blue": { + "body": "", + "width": 214, + "height": 124 + }, + "knowledge-option-card-effect-blue-light": { + "body": "", + "width": 212, + "height": 74 + }, + "knowledge-option-card-effect-orange": { + "body": "" + }, + "knowledge-option-card-effect-purple": { + "body": "" + }, + "knowledge-option-card-effect-teal": { + "body": "", + "width": 212, + "height": 92 + }, + "knowledge-selection-mod": { + "body": "", + "width": 10, + "height": 10 + }, + "knowledge-watercrawl": { + "body": "", + "width": 500, + "height": 500 + }, + "knowledge-dataset-card-external-knowledge-base": { + "body": "" + }, + "knowledge-dataset-card-general": { + "body": "" + }, + "knowledge-dataset-card-graph": { + "body": "" + }, + "knowledge-dataset-card-parent-child": { + "body": "" + }, + "knowledge-dataset-card-qa": { + "body": "" + }, + "knowledge-online-drive-buckets-blue": { + "body": "", + "height": 21 + }, + "knowledge-online-drive-buckets-gray": { + "body": "", + "width": 18 + }, + "knowledge-online-drive-folder": { + "body": "" + }, + "llm-anthropic": { + "body": "" + }, + "llm-anthropic-dark": { + "body": "", + "width": 90, + "height": 10 + }, + "llm-anthropic-light": { + "body": "", + "width": 90, + "height": 10 + }, + "llm-anthropic-short-light": { + "body": "", + "width": 40, + "height": 40 + }, + "llm-anthropic-text": { + "body": "", + "width": 90, + "height": 20 + }, + "llm-azure-openai-service": { + "body": "", + "width": 56 + }, + "llm-azure-openai-service-text": { + "body": "", + "width": 212 + }, + "llm-azureai": { + "body": "" + }, + "llm-azureai-text": { + "body": "", + "width": 92 + }, + "llm-baichuan": { + "body": "" + }, + "llm-baichuan-text": { + "body": "", + "width": 130 + }, + "llm-chatglm": { + "body": "" + }, + "llm-chatglm-text": { + "body": "", + "width": 100 + }, + "llm-cohere": { + "body": "", + "width": 22, + "height": 22 + }, + "llm-cohere-text": { + "body": "", + "width": 120 + }, + "llm-deepseek": { + "body": "", + "width": 40, + "height": 40 + }, + "llm-gemini": { + "body": "", + "width": 40, + "height": 40 + }, + "llm-gpt-3": { + "body": "" + }, + "llm-gpt-4": { + "body": "" + }, + "llm-grok": { + "body": "", + "width": 40, + "height": 40 + }, + "llm-huggingface": { + "body": "" + }, + "llm-huggingface-text": { + "body": "", + "width": 120 + }, + "llm-huggingface-text-hub": { + "body": "", + "width": 151 + }, + "llm-iflytek-spark": { + "body": "" + }, + "llm-iflytek-spark-text": { + "body": "", + "width": 150 + }, + "llm-iflytek-spark-text-cn": { + "body": "", + "width": 84 + }, + "llm-jina": { + "body": "" + }, + "llm-jina-text": { + "body": "", + "width": 58 + }, + "llm-microsoft": { + "body": "", + "width": 21, + "height": 22 + }, + "llm-openai-black": { + "body": "" + }, + "llm-openai-blue": { + "body": "" + }, + "llm-openai-green": { + "body": "" + }, + "llm-openai-teal": { + "body": "" + }, + "llm-openai-text": { + "body": "", + "width": 52, + "height": 20 + }, + "llm-openai-transparent": { + "body": "" + }, + "llm-openai-violet": { + "body": "" + }, + "llm-openai-yellow": { + "body": "" + }, + "llm-openllm": { + "body": "" + }, + "llm-openllm-text": { + "body": "", + "width": 92, + "height": 25 + }, + "llm-replicate": { + "body": "" + }, + "llm-replicate-text": { + "body": "", + "width": 92 + }, + "llm-xorbits-inference": { + "body": "" + }, + "llm-xorbits-inference-text": { + "body": "", + "width": 152 + }, + "llm-zhipuai": { + "body": "" + }, + "llm-zhipuai-text": { + "body": "", + "width": 89, + "height": 32 + }, + "llm-zhipuai-text-cn": { + "body": "", + "width": 86, + "height": 32 + }, + "model-checked": { + "body": "" + }, + "other-default-tool-icon": { + "body": "" + }, + "other-icon-3-dots": { + "body": "", + "width": 16, + "height": 16 + }, + "other-message-3-fill": { + "body": "" + }, + "other-row-struct": { + "body": "", + "width": 624, + "height": 48 + }, + "other-slack": { + "body": "", + "width": 27, + "height": 27 + }, + "other-teams": { + "body": "", + "width": 28, + "height": 28 + }, + "plugins-google": { + "body": "", + "width": 24, + "height": 24 + }, + "plugins-partner-dark": { + "body": "" + }, + "plugins-partner-light": { + "body": "" + }, + "plugins-verified-dark": { + "body": "" + }, + "plugins-verified-light": { + "body": "" + }, + "plugins-web-reader": { + "body": "", + "width": 24, + "height": 24 + }, + "plugins-wikipedia": { + "body": "", + "width": 24, + "height": 24 + }, + "thought-data-set": { + "body": "" + }, + "thought-loading": { + "body": "" + }, + "thought-search": { + "body": "" + }, + "thought-thought-list": { + "body": "" + }, + "thought-web-reader": { + "body": "" + }, + "tracing-aliyun-icon": { + "body": "", + "width": 65 + }, + "tracing-aliyun-icon-big": { + "body": "", + "width": 96, + "height": 24 + }, + "tracing-arize-icon": { + "body": "" + }, + "tracing-arize-icon-big": { + "body": "", + "width": 111, + "height": 24 + }, + "tracing-databricks-icon": { + "body": "", + "width": 100 + }, + "tracing-databricks-icon-big": { + "body": "", + "width": 151, + "height": 24 + }, + "tracing-langfuse-icon": { + "body": "" + }, + "tracing-langfuse-icon-big": { + "body": "", + "width": 111, + "height": 24 + }, + "tracing-langsmith-icon": { + "body": "", + "width": 84, + "height": 14 + }, + "tracing-langsmith-icon-big": { + "body": "", + "width": 124, + "height": 20 + }, + "tracing-mlflow-icon": { + "body": "", + "width": 43 + }, + "tracing-mlflow-icon-big": { + "body": "", + "width": 65, + "height": 24 + }, + "tracing-opik-icon": { + "body": "", + "width": 47.134 + }, + "tracing-opik-icon-big": { + "body": "", + "width": 70.701, + "height": 24 + }, + "tracing-phoenix-icon": { + "body": "" + }, + "tracing-phoenix-icon-big": { + "body": "", + "width": 111, + "height": 24 + }, + "tracing-tencent-icon": { + "body": "", + "width": 80, + "height": 18 + }, + "tracing-tencent-icon-big": { + "body": "", + "width": 80, + "height": 18 + }, + "tracing-tracing-icon": { + "body": "", + "width": 20, + "height": 20 + }, + "tracing-weave-icon": { + "body": "", + "width": 120 + }, + "tracing-weave-icon-big": { + "body": "", + "width": 120 + } + } +} diff --git a/packages/iconify-collections/custom-public/index.d.ts b/packages/iconify-collections/custom-public/index.d.ts new file mode 100644 index 0000000000..ecca5633d4 --- /dev/null +++ b/packages/iconify-collections/custom-public/index.d.ts @@ -0,0 +1,55 @@ +export interface IconifyJSON { + prefix: string + icons: Record + aliases?: Record + width?: number + height?: number + lastModified?: number +} + +export interface IconifyIcon { + body: string + left?: number + top?: number + width?: number + height?: number + rotate?: 0 | 1 | 2 | 3 + hFlip?: boolean + vFlip?: boolean +} + +export interface IconifyAlias extends Omit { + parent: string +} + +export interface IconifyInfo { + prefix: string + name: string + total: number + version: string + author?: { + name: string + url?: string + } + license?: { + title: string + spdx?: string + url?: string + } + samples?: string[] + palette?: boolean +} + +export interface IconifyMetaData { + [key: string]: unknown +} + +export interface IconifyChars { + [key: string]: string +} + +export declare const icons: IconifyJSON +export declare const info: IconifyInfo +export declare const metadata: IconifyMetaData +export declare const chars: IconifyChars + diff --git a/packages/iconify-collections/custom-public/index.js b/packages/iconify-collections/custom-public/index.js new file mode 100644 index 0000000000..81c1d0f5c4 --- /dev/null +++ b/packages/iconify-collections/custom-public/index.js @@ -0,0 +1,9 @@ +'use strict' + +const icons = require('./icons.json') +const info = require('./info.json') +const metadata = require('./metadata.json') +const chars = require('./chars.json') + +module.exports = { icons, info, metadata, chars } + diff --git a/packages/iconify-collections/custom-public/index.mjs b/packages/iconify-collections/custom-public/index.mjs new file mode 100644 index 0000000000..6c1108a92d --- /dev/null +++ b/packages/iconify-collections/custom-public/index.mjs @@ -0,0 +1,7 @@ +import icons from './icons.json' with { type: 'json' } +import info from './info.json' with { type: 'json' } +import metadata from './metadata.json' with { type: 'json' } +import chars from './chars.json' with { type: 'json' } + +export { icons, info, metadata, chars } + diff --git a/packages/iconify-collections/custom-public/info.json b/packages/iconify-collections/custom-public/info.json new file mode 100644 index 0000000000..8b5572de6f --- /dev/null +++ b/packages/iconify-collections/custom-public/info.json @@ -0,0 +1,24 @@ +{ + "prefix": "custom-public", + "name": "Dify Custom Public", + "total": 142, + "version": "0.0.0-private", + "author": { + "name": "LangGenius, Inc.", + "url": "https://github.com/langgenius/dify" + }, + "license": { + "title": "Modified Apache 2.0", + "spdx": "Apache-2.0", + "url": "https://github.com/langgenius/dify/blob/main/LICENSE" + }, + "samples": [ + "avatar-user", + "billing-ar-cube-1", + "billing-asterisk", + "billing-aws-marketplace-dark", + "billing-aws-marketplace-light", + "billing-azure" + ], + "palette": false +} diff --git a/packages/iconify-collections/custom-public/metadata.json b/packages/iconify-collections/custom-public/metadata.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/packages/iconify-collections/custom-public/metadata.json @@ -0,0 +1 @@ +{} diff --git a/packages/iconify-collections/custom-vender/chars.json b/packages/iconify-collections/custom-vender/chars.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/packages/iconify-collections/custom-vender/chars.json @@ -0,0 +1 @@ +{} diff --git a/packages/iconify-collections/custom-vender/icons.json b/packages/iconify-collections/custom-vender/icons.json new file mode 100644 index 0000000000..a7dc8e75e0 --- /dev/null +++ b/packages/iconify-collections/custom-vender/icons.json @@ -0,0 +1,1098 @@ +{ + "prefix": "custom-vender", + "lastModified": 1775115796, + "icons": { + "features-citations": { + "body": "" + }, + "features-content-moderation": { + "body": "" + }, + "features-document": { + "body": "" + }, + "features-folder-upload": { + "body": "" + }, + "features-love-message": { + "body": "" + }, + "features-message-fast": { + "body": "" + }, + "features-microphone-01": { + "body": "" + }, + "features-text-to-audio": { + "body": "" + }, + "features-virtual-assistant": { + "body": "" + }, + "features-vision": { + "body": "" + }, + "knowledge-add-chunks": { + "body": "", + "width": 20, + "height": 20 + }, + "knowledge-api-aggregate": { + "body": "", + "width": 16 + }, + "knowledge-arrow-shape": { + "body": "", + "width": 24, + "height": 11 + }, + "knowledge-chunk": { + "body": "", + "width": 10, + "height": 10 + }, + "knowledge-collapse": { + "body": "", + "width": 16 + }, + "knowledge-divider": { + "body": "", + "width": 6, + "height": 30 + }, + "knowledge-economic": { + "body": "", + "height": 18 + }, + "knowledge-full-text-search": { + "body": "", + "width": 15 + }, + "knowledge-general-chunk": { + "body": "", + "height": 18 + }, + "knowledge-high-quality": { + "body": "", + "height": 18 + }, + "knowledge-hybrid-search": { + "body": "", + "width": 16 + }, + "knowledge-parent-child-chunk": { + "body": "", + "height": 18 + }, + "knowledge-question-and-answer": { + "body": "", + "height": 18 + }, + "knowledge-search-lines-sparkle": { + "body": "", + "width": 16 + }, + "knowledge-search-menu": { + "body": "", + "width": 32, + "height": 33 + }, + "knowledge-vector-search": { + "body": "", + "width": 16 + }, + "line-alertsAndFeedback-alert-triangle": { + "body": "" + }, + "line-alertsAndFeedback-thumbs-down": { + "body": "" + }, + "line-alertsAndFeedback-thumbs-up": { + "body": "" + }, + "line-alertsAndFeedback-warning": { + "body": "", + "width": 12, + "height": 12 + }, + "line-arrows-arrow-narrow-left": { + "body": "", + "width": 17, + "height": 16 + }, + "line-arrows-arrow-up-right": { + "body": "" + }, + "line-arrows-chevron-down-double": { + "body": "", + "width": 12, + "height": 13 + }, + "line-arrows-chevron-right": { + "body": "" + }, + "line-arrows-chevron-selector-vertical": { + "body": "", + "width": 24, + "height": 24 + }, + "line-arrows-iconr": { + "body": "" + }, + "line-arrows-refresh-ccw-01": { + "body": "", + "width": 24, + "height": 24 + }, + "line-arrows-refresh-cw-05": { + "body": "", + "width": 16, + "height": 16 + }, + "line-arrows-reverse-left": { + "body": "", + "width": 16, + "height": 16 + }, + "line-communication-ai-text": { + "body": "" + }, + "line-communication-chat-bot": { + "body": "" + }, + "line-communication-chat-bot-slim": { + "body": "", + "width": 48, + "height": 48 + }, + "line-communication-cute-robot": { + "body": "" + }, + "line-communication-message-check-remove": { + "body": "", + "width": 24, + "height": 24 + }, + "line-communication-message-fast-plus": { + "body": "", + "width": 24, + "height": 24 + }, + "line-development-artificial-brain": { + "body": "", + "width": 24, + "height": 24 + }, + "line-development-bar-chart-square-02": { + "body": "" + }, + "line-development-brackets-x": { + "body": "", + "width": 24, + "height": 24 + }, + "line-development-code-browser": { + "body": "", + "width": 24, + "height": 24 + }, + "line-development-container": { + "body": "" + }, + "line-development-database-01": { + "body": "", + "width": 17 + }, + "line-development-database-03": { + "body": "" + }, + "line-development-file-heart-02": { + "body": "" + }, + "line-development-git-branch-01": { + "body": "" + }, + "line-development-prompt-engineering": { + "body": "" + }, + "line-development-puzzle-piece-01": { + "body": "" + }, + "line-development-terminal-square": { + "body": "", + "width": 24, + "height": 24 + }, + "line-development-variable": { + "body": "" + }, + "line-development-webhooks": { + "body": "" + }, + "line-editor-align-left": { + "body": "" + }, + "line-editor-bezier-curve-03": { + "body": "", + "width": 12, + "height": 12 + }, + "line-editor-collapse": { + "body": "", + "width": 16, + "height": 16 + }, + "line-editor-colors": { + "body": "" + }, + "line-editor-image-indent-left": { + "body": "" + }, + "line-editor-left-indent-02": { + "body": "" + }, + "line-editor-letter-spacing-01": { + "body": "" + }, + "line-editor-type-square": { + "body": "", + "width": 12, + "height": 12 + }, + "line-education-book-open-01": { + "body": "", + "width": 12, + "height": 12 + }, + "line-files-copy": { + "body": "" + }, + "line-files-copy-check": { + "body": "" + }, + "line-files-file-02": { + "body": "" + }, + "line-files-file-arrow-01": { + "body": "" + }, + "line-files-file-check-02": { + "body": "" + }, + "line-files-file-download-02": { + "body": "", + "width": 24, + "height": 24 + }, + "line-files-file-plus-01": { + "body": "" + }, + "line-files-file-plus-02": { + "body": "" + }, + "line-files-file-text": { + "body": "", + "width": 24, + "height": 24 + }, + "line-files-file-upload": { + "body": "", + "width": 24, + "height": 24 + }, + "line-files-folder": { + "body": "", + "width": 14, + "height": 14 + }, + "line-financeAndECommerce-balance": { + "body": "" + }, + "line-financeAndECommerce-coins-stacked-01": { + "body": "" + }, + "line-financeAndECommerce-credits-coin": { + "body": "", + "width": 10, + "height": 10 + }, + "line-financeAndECommerce-gold-coin": { + "body": "", + "width": 16, + "height": 16 + }, + "line-financeAndECommerce-receipt-list": { + "body": "" + }, + "line-financeAndECommerce-tag-01": { + "body": "", + "width": 14, + "height": 14 + }, + "line-financeAndECommerce-tag-03": { + "body": "", + "width": 16, + "height": 16 + }, + "line-general-at-sign": { + "body": "" + }, + "line-general-bookmark": { + "body": "", + "width": 24, + "height": 24 + }, + "line-general-check": { + "body": "" + }, + "line-general-check-done-01": { + "body": "", + "width": 24, + "height": 24 + }, + "line-general-checklist-square": { + "body": "", + "width": 32, + "height": 32 + }, + "line-general-code-assistant": { + "body": "", + "width": 24, + "height": 24 + }, + "line-general-dots-grid": { + "body": "", + "width": 14, + "height": 14 + }, + "line-general-edit-02": { + "body": "", + "width": 14, + "height": 14 + }, + "line-general-edit-04": { + "body": "", + "width": 24, + "height": 24 + }, + "line-general-edit-05": { + "body": "" + }, + "line-general-hash-02": { + "body": "", + "width": 12, + "height": 12 + }, + "line-general-info-circle": { + "body": "", + "width": 12, + "height": 12 + }, + "line-general-link-03": { + "body": "", + "width": 17 + }, + "line-general-link-external-02": { + "body": "", + "width": 12, + "height": 12 + }, + "line-general-log-in-04": { + "body": "" + }, + "line-general-log-out-01": { + "body": "", + "width": 14, + "height": 14 + }, + "line-general-log-out-04": { + "body": "" + }, + "line-general-magic-edit": { + "body": "", + "width": 24, + "height": 24 + }, + "line-general-menu-01": { + "body": "" + }, + "line-general-pin-01": { + "body": "" + }, + "line-general-pin-02": { + "body": "", + "width": 24, + "height": 24 + }, + "line-general-plus-02": { + "body": "", + "width": 10, + "height": 10 + }, + "line-general-refresh": { + "body": "", + "width": 24, + "height": 24 + }, + "line-general-search-menu": { + "body": "", + "width": 32, + "height": 32 + }, + "line-general-settings-01": { + "body": "", + "width": 14, + "height": 14 + }, + "line-general-settings-04": { + "body": "", + "width": 14, + "height": 14 + }, + "line-general-target-04": { + "body": "", + "width": 12, + "height": 12 + }, + "line-general-upload-03": { + "body": "" + }, + "line-general-upload-cloud-01": { + "body": "", + "width": 24, + "height": 24 + }, + "line-general-x": { + "body": "" + }, + "line-images-image-plus": { + "body": "" + }, + "line-layout-align-left-01": { + "body": "" + }, + "line-layout-align-right-01": { + "body": "" + }, + "line-layout-grid-01": { + "body": "", + "width": 17, + "height": 16 + }, + "line-layout-layout-grid-02": { + "body": "" + }, + "line-mediaAndDevices-microphone-01": { + "body": "" + }, + "line-mediaAndDevices-play-circle": { + "body": "" + }, + "line-mediaAndDevices-sliders-h": { + "body": "", + "width": 24, + "height": 24 + }, + "line-mediaAndDevices-speaker": { + "body": "" + }, + "line-mediaAndDevices-stop": { + "body": "", + "width": 12, + "height": 12 + }, + "line-mediaAndDevices-stop-circle": { + "body": "", + "width": 17 + }, + "line-others-bubble-x": { + "body": "" + }, + "line-others-colors": { + "body": "", + "width": 14, + "height": 14 + }, + "line-others-drag-handle": { + "body": "" + }, + "line-others-env": { + "body": "" + }, + "line-others-global-variable": { + "body": "" + }, + "line-others-icon-3-dots": { + "body": "" + }, + "line-others-long-arrow-left": { + "body": "", + "width": 21, + "height": 8 + }, + "line-others-long-arrow-right": { + "body": "", + "width": 26, + "height": 8 + }, + "line-others-search-menu": { + "body": "", + "width": 32, + "height": 32 + }, + "line-others-tools": { + "body": "", + "height": 17 + }, + "line-shapes-cube-outline": { + "body": "", + "height": 17 + }, + "line-time-clock-fast-forward": { + "body": "", + "width": 24, + "height": 24 + }, + "line-time-clock-play": { + "body": "" + }, + "line-time-clock-play-slim": { + "body": "", + "width": 32, + "height": 32 + }, + "line-time-clock-refresh": { + "body": "", + "width": 12, + "height": 12 + }, + "line-users-user-01": { + "body": "" + }, + "line-users-users-01": { + "body": "" + }, + "line-weather-stars-02": { + "body": "", + "width": 24, + "height": 24 + }, + "other-anthropic-text": { + "body": "", + "width": 90, + "height": 20 + }, + "other-generator": { + "body": "" + }, + "other-group": { + "body": "", + "height": 16 + }, + "other-hourglass-shape": { + "body": "", + "width": 8 + }, + "other-mcp": { + "body": "", + "width": 16, + "height": 16 + }, + "other-no-tool-placeholder": { + "body": "", + "width": 204, + "height": 36 + }, + "other-openai": { + "body": "", + "width": 80, + "height": 22 + }, + "other-replay-line": { + "body": "", + "width": 20, + "height": 20 + }, + "other-square-checklist": { + "body": "", + "width": 24, + "height": 24 + }, + "pipeline-input-field": { + "body": "", + "width": 16, + "height": 16 + }, + "pipeline-pipeline-fill": { + "body": "" + }, + "pipeline-pipeline-line": { + "body": "" + }, + "plugin-box-sparkle-fill": { + "body": "", + "width": 14, + "height": 14 + }, + "plugin-left-corner": { + "body": "", + "width": 13, + "height": 20 + }, + "plugin-trigger": { + "body": "" + }, + "solid-FinanceAndECommerce-gold-coin": { + "body": "" + }, + "solid-FinanceAndECommerce-scales-02": { + "body": "" + }, + "solid-alertsAndFeedback-alert-triangle": { + "body": "", + "width": 12, + "height": 12 + }, + "solid-arrows-arrow-down-double-line": { + "body": "" + }, + "solid-arrows-arrow-down-round-fill": { + "body": "" + }, + "solid-arrows-arrow-up-double-line": { + "body": "" + }, + "solid-arrows-chevron-down": { + "body": "", + "width": 24, + "height": 24 + }, + "solid-arrows-high-priority": { + "body": "", + "width": 24, + "height": 24 + }, + "solid-communication-ai-text": { + "body": "" + }, + "solid-communication-bubble-text-mod": { + "body": "" + }, + "solid-communication-chat-bot": { + "body": "", + "width": 13, + "height": 12 + }, + "solid-communication-cute-robot": { + "body": "" + }, + "solid-communication-edit-list": { + "body": "" + }, + "solid-communication-list-sparkle": { + "body": "" + }, + "solid-communication-logic": { + "body": "" + }, + "solid-communication-message-dots-circle": { + "body": "" + }, + "solid-communication-message-fast": { + "body": "" + }, + "solid-communication-message-heart-circle": { + "body": "", + "width": 16, + "height": 16 + }, + "solid-communication-message-smile-square": { + "body": "", + "width": 16, + "height": 16 + }, + "solid-communication-send-03": { + "body": "", + "width": 20, + "height": 20 + }, + "solid-development-api-connection": { + "body": "", + "width": 24, + "height": 24 + }, + "solid-development-api-connection-mod": { + "body": "" + }, + "solid-development-bar-chart-square-02": { + "body": "" + }, + "solid-development-container": { + "body": "", + "width": 17 + }, + "solid-development-database-02": { + "body": "", + "width": 17 + }, + "solid-development-database-03": { + "body": "" + }, + "solid-development-file-heart-02": { + "body": "" + }, + "solid-development-pattern-recognition": { + "body": "", + "width": 24, + "height": 24 + }, + "solid-development-prompt-engineering": { + "body": "" + }, + "solid-development-puzzle-piece-01": { + "body": "", + "width": 17 + }, + "solid-development-semantic": { + "body": "", + "width": 24, + "height": 24 + }, + "solid-development-terminal-square": { + "body": "", + "width": 12, + "height": 12 + }, + "solid-development-variable-02": { + "body": "", + "width": 24, + "height": 24 + }, + "solid-editor-brush-01": { + "body": "" + }, + "solid-editor-citations": { + "body": "", + "width": 16, + "height": 16 + }, + "solid-editor-colors": { + "body": "" + }, + "solid-editor-paragraph": { + "body": "" + }, + "solid-editor-type-square": { + "body": "" + }, + "solid-education-beaker-02": { + "body": "", + "width": 12, + "height": 12 + }, + "solid-education-bubble-text": { + "body": "" + }, + "solid-education-heart-02": { + "body": "" + }, + "solid-education-unblur": { + "body": "" + }, + "solid-files-file-05": { + "body": "" + }, + "solid-files-file-search-02": { + "body": "" + }, + "solid-files-file-zip": { + "body": "" + }, + "solid-files-folder": { + "body": "" + }, + "solid-general-answer-triangle": { + "body": "", + "width": 8, + "height": 12 + }, + "solid-general-arrow-down-round-fill": { + "body": "", + "width": 16, + "height": 16 + }, + "solid-general-check-circle": { + "body": "", + "width": 16, + "height": 16 + }, + "solid-general-check-done-01": { + "body": "" + }, + "solid-general-download-02": { + "body": "" + }, + "solid-general-edit-03": { + "body": "", + "width": 12, + "height": 12 + }, + "solid-general-edit-04": { + "body": "" + }, + "solid-general-eye": { + "body": "" + }, + "solid-general-github": { + "body": "", + "width": 16, + "height": 16 + }, + "solid-general-message-clock-circle": { + "body": "", + "width": 16, + "height": 16 + }, + "solid-general-plus-circle": { + "body": "" + }, + "solid-general-question-triangle": { + "body": "", + "width": 8, + "height": 12 + }, + "solid-general-search-md": { + "body": "" + }, + "solid-general-target-04": { + "body": "" + }, + "solid-general-tool-03": { + "body": "", + "width": 16, + "height": 16 + }, + "solid-general-x-circle": { + "body": "", + "width": 16, + "height": 16 + }, + "solid-general-zap-fast": { + "body": "", + "width": 12, + "height": 12 + }, + "solid-general-zap-narrow": { + "body": "", + "width": 12, + "height": 12 + }, + "solid-layout-grid-01": { + "body": "" + }, + "solid-mediaAndDevices-audio-support-icon": { + "body": "" + }, + "solid-mediaAndDevices-document-support-icon": { + "body": "" + }, + "solid-mediaAndDevices-magic-box": { + "body": "" + }, + "solid-mediaAndDevices-magic-eyes": { + "body": "" + }, + "solid-mediaAndDevices-magic-wand": { + "body": "" + }, + "solid-mediaAndDevices-microphone-01": { + "body": "", + "width": 16, + "height": 16 + }, + "solid-mediaAndDevices-play": { + "body": "" + }, + "solid-mediaAndDevices-robot": { + "body": "" + }, + "solid-mediaAndDevices-sliders-02": { + "body": "", + "width": 24, + "height": 24 + }, + "solid-mediaAndDevices-speaker": { + "body": "", + "width": 16, + "height": 16 + }, + "solid-mediaAndDevices-stop-circle": { + "body": "", + "width": 20, + "height": 20 + }, + "solid-mediaAndDevices-video-support-icon": { + "body": "" + }, + "solid-security-lock-01": { + "body": "", + "width": 12, + "height": 12 + }, + "solid-shapes-corner": { + "body": "", + "width": 13, + "height": 20 + }, + "solid-shapes-star-04": { + "body": "", + "width": 11, + "height": 10 + }, + "solid-shapes-star-06": { + "body": "" + }, + "solid-users-user-01": { + "body": "" + }, + "solid-users-user-edit-02": { + "body": "", + "width": 14, + "height": 14 + }, + "solid-users-users-01": { + "body": "" + }, + "solid-users-users-plus": { + "body": "", + "width": 24, + "height": 24 + }, + "system-auto-update-line": { + "body": "", + "width": 24, + "height": 24 + }, + "workflow-agent": { + "body": "", + "width": 16, + "height": 16 + }, + "workflow-answer": { + "body": "" + }, + "workflow-api-aggregate": { + "body": "", + "width": 16, + "height": 16 + }, + "workflow-assigner": { + "body": "", + "width": 16, + "height": 16 + }, + "workflow-asterisk": { + "body": "" + }, + "workflow-calendar-check-line": { + "body": "" + }, + "workflow-code": { + "body": "" + }, + "workflow-datasource": { + "body": "" + }, + "workflow-docs-extractor": { + "body": "", + "width": 16, + "height": 16 + }, + "workflow-end": { + "body": "" + }, + "workflow-home": { + "body": "" + }, + "workflow-http": { + "body": "" + }, + "workflow-human-in-loop": { + "body": "", + "width": 16, + "height": 16 + }, + "workflow-if-else": { + "body": "" + }, + "workflow-iteration": { + "body": "" + }, + "workflow-iteration-start": { + "body": "", + "width": 12, + "height": 12 + }, + "workflow-jinja": { + "body": "", + "width": 24, + "height": 12 + }, + "workflow-knowledge-base": { + "body": "" + }, + "workflow-knowledge-retrieval": { + "body": "", + "width": 16, + "height": 16 + }, + "workflow-list-filter": { + "body": "", + "width": 16, + "height": 16 + }, + "workflow-llm": { + "body": "" + }, + "workflow-loop": { + "body": "", + "width": 18, + "height": 16 + }, + "workflow-loop-end": { + "body": "", + "width": 16, + "height": 16 + }, + "workflow-parameter-extractor": { + "body": "" + }, + "workflow-question-classifier": { + "body": "" + }, + "workflow-schedule": { + "body": "", + "width": 16, + "height": 16 + }, + "workflow-templating-transform": { + "body": "" + }, + "workflow-trigger-all": { + "body": "" + }, + "workflow-variable-x": { + "body": "" + }, + "workflow-webhook-line": { + "body": "", + "width": 16, + "height": 16 + }, + "workflow-window-cursor": { + "body": "", + "width": 16, + "height": 16 + } + } +} diff --git a/packages/iconify-collections/custom-vender/index.d.ts b/packages/iconify-collections/custom-vender/index.d.ts new file mode 100644 index 0000000000..ecca5633d4 --- /dev/null +++ b/packages/iconify-collections/custom-vender/index.d.ts @@ -0,0 +1,55 @@ +export interface IconifyJSON { + prefix: string + icons: Record + aliases?: Record + width?: number + height?: number + lastModified?: number +} + +export interface IconifyIcon { + body: string + left?: number + top?: number + width?: number + height?: number + rotate?: 0 | 1 | 2 | 3 + hFlip?: boolean + vFlip?: boolean +} + +export interface IconifyAlias extends Omit { + parent: string +} + +export interface IconifyInfo { + prefix: string + name: string + total: number + version: string + author?: { + name: string + url?: string + } + license?: { + title: string + spdx?: string + url?: string + } + samples?: string[] + palette?: boolean +} + +export interface IconifyMetaData { + [key: string]: unknown +} + +export interface IconifyChars { + [key: string]: string +} + +export declare const icons: IconifyJSON +export declare const info: IconifyInfo +export declare const metadata: IconifyMetaData +export declare const chars: IconifyChars + diff --git a/packages/iconify-collections/custom-vender/index.js b/packages/iconify-collections/custom-vender/index.js new file mode 100644 index 0000000000..81c1d0f5c4 --- /dev/null +++ b/packages/iconify-collections/custom-vender/index.js @@ -0,0 +1,9 @@ +'use strict' + +const icons = require('./icons.json') +const info = require('./info.json') +const metadata = require('./metadata.json') +const chars = require('./chars.json') + +module.exports = { icons, info, metadata, chars } + diff --git a/packages/iconify-collections/custom-vender/index.mjs b/packages/iconify-collections/custom-vender/index.mjs new file mode 100644 index 0000000000..6c1108a92d --- /dev/null +++ b/packages/iconify-collections/custom-vender/index.mjs @@ -0,0 +1,7 @@ +import icons from './icons.json' with { type: 'json' } +import info from './info.json' with { type: 'json' } +import metadata from './metadata.json' with { type: 'json' } +import chars from './chars.json' with { type: 'json' } + +export { icons, info, metadata, chars } + diff --git a/packages/iconify-collections/custom-vender/info.json b/packages/iconify-collections/custom-vender/info.json new file mode 100644 index 0000000000..0a84c45bbd --- /dev/null +++ b/packages/iconify-collections/custom-vender/info.json @@ -0,0 +1,24 @@ +{ + "prefix": "custom-vender", + "name": "Dify Custom Vender", + "total": 277, + "version": "0.0.0-private", + "author": { + "name": "LangGenius, Inc.", + "url": "https://github.com/langgenius/dify" + }, + "license": { + "title": "Modified Apache 2.0", + "spdx": "Apache-2.0", + "url": "https://github.com/langgenius/dify/blob/main/LICENSE" + }, + "samples": [ + "features-citations", + "features-content-moderation", + "features-document", + "features-folder-upload", + "features-love-message", + "features-message-fast" + ], + "palette": false +} diff --git a/packages/iconify-collections/custom-vender/metadata.json b/packages/iconify-collections/custom-vender/metadata.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/packages/iconify-collections/custom-vender/metadata.json @@ -0,0 +1 @@ +{} diff --git a/packages/iconify-collections/package.json b/packages/iconify-collections/package.json new file mode 100644 index 0000000000..3bd7285f1a --- /dev/null +++ b/packages/iconify-collections/package.json @@ -0,0 +1,31 @@ +{ + "name": "@dify/iconify-collections", + "private": true, + "version": "0.0.0-private", + "exports": { + "./custom-public": { + "types": "./custom-public/index.d.ts", + "require": "./custom-public/index.js", + "import": "./custom-public/index.mjs" + }, + "./custom-public/icons.json": "./custom-public/icons.json", + "./custom-public/info.json": "./custom-public/info.json", + "./custom-public/metadata.json": "./custom-public/metadata.json", + "./custom-public/chars.json": "./custom-public/chars.json", + "./custom-vender": { + "types": "./custom-vender/index.d.ts", + "require": "./custom-vender/index.js", + "import": "./custom-vender/index.mjs" + }, + "./custom-vender/icons.json": "./custom-vender/icons.json", + "./custom-vender/info.json": "./custom-vender/info.json", + "./custom-vender/metadata.json": "./custom-vender/metadata.json", + "./custom-vender/chars.json": "./custom-vender/chars.json" + }, + "scripts": { + "generate": "node ./scripts/generate-collections.mjs" + }, + "devDependencies": { + "iconify-import-svg": "catalog:" + } +} diff --git a/packages/iconify-collections/scripts/generate-collections.mjs b/packages/iconify-collections/scripts/generate-collections.mjs new file mode 100644 index 0000000000..1c734731e6 --- /dev/null +++ b/packages/iconify-collections/scripts/generate-collections.mjs @@ -0,0 +1,178 @@ +import { mkdir, readFile, rm, writeFile } from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { importSvgCollections } from 'iconify-import-svg' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const packageDir = path.resolve(__dirname, '..') + +const parseColorOptions = { + fallback: () => 'currentColor', +} +const svgOptimizeConfig = { + cleanupSVG: true, + deOptimisePaths: true, + runSVGO: true, + parseColors: parseColorOptions, +} + +const customPublicCollections = importSvgCollections({ + source: path.resolve(packageDir, 'assets/public'), + prefix: 'custom-public', + ignoreImportErrors: true, + ...svgOptimizeConfig, +}) + +const customVenderCollections = importSvgCollections({ + source: path.resolve(packageDir, 'assets/vender'), + prefix: 'custom-vender', + ignoreImportErrors: true, + ...svgOptimizeConfig, +}) + +const packageJson = JSON.parse(await readFile(path.resolve(packageDir, 'package.json'), 'utf8')) + +const flattenCollections = (collections, prefix) => { + const icons = {} + const aliases = {} + let lastModified = 0 + + for (const [collectionKey, collection] of Object.entries(collections)) { + const segment = collectionKey.slice(prefix.length + 1) + const namePrefix = segment + ? `${segment}-` + : '' + + for (const [iconName, iconData] of Object.entries(collection.icons ?? {})) + icons[`${namePrefix}${iconName}`] = iconData + + for (const [aliasName, aliasData] of Object.entries(collection.aliases ?? {})) + aliases[`${namePrefix}${aliasName}`] = aliasData + + if (typeof collection.lastModified === 'number') + lastModified = Math.max(lastModified, collection.lastModified) + } + + return { + prefix, + ...(lastModified ? { lastModified } : {}), + icons, + ...(Object.keys(aliases).length ? { aliases } : {}), + } +} + +const createCollectionInfo = (prefix, name, icons) => ({ + prefix, + name, + total: Object.keys(icons).length, + version: packageJson.version, + author: { + name: 'LangGenius, Inc.', + url: 'https://github.com/langgenius/dify', + }, + license: { + title: 'Modified Apache 2.0', + spdx: 'Apache-2.0', + url: 'https://github.com/langgenius/dify/blob/main/LICENSE', + }, + samples: Object.keys(icons).slice(0, 6), + palette: false, +}) + +const createIndexMjs = () => `import icons from './icons.json' with { type: 'json' } +import info from './info.json' with { type: 'json' } +import metadata from './metadata.json' with { type: 'json' } +import chars from './chars.json' with { type: 'json' } + +export { icons, info, metadata, chars } +` + +const createIndexJs = () => `'use strict' + +const icons = require('./icons.json') +const info = require('./info.json') +const metadata = require('./metadata.json') +const chars = require('./chars.json') + +module.exports = { icons, info, metadata, chars } +` + +const createIndexTypes = () => `export interface IconifyJSON { + prefix: string + icons: Record + aliases?: Record + width?: number + height?: number + lastModified?: number +} + +export interface IconifyIcon { + body: string + left?: number + top?: number + width?: number + height?: number + rotate?: 0 | 1 | 2 | 3 + hFlip?: boolean + vFlip?: boolean +} + +export interface IconifyAlias extends Omit { + parent: string +} + +export interface IconifyInfo { + prefix: string + name: string + total: number + version: string + author?: { + name: string + url?: string + } + license?: { + title: string + spdx?: string + url?: string + } + samples?: string[] + palette?: boolean +} + +export interface IconifyMetaData { + [key: string]: unknown +} + +export interface IconifyChars { + [key: string]: string +} + +export declare const icons: IconifyJSON +export declare const info: IconifyInfo +export declare const metadata: IconifyMetaData +export declare const chars: IconifyChars +` + +const writeCollectionPackage = async (directoryName, collection, name) => { + const targetDir = path.resolve(packageDir, directoryName) + const info = createCollectionInfo(collection.prefix, name, collection.icons) + + await mkdir(targetDir, { recursive: true }) + await writeFile(path.resolve(targetDir, 'icons.json'), `${JSON.stringify(collection, null, 2)}\n`) + await writeFile(path.resolve(targetDir, 'info.json'), `${JSON.stringify(info, null, 2)}\n`) + await writeFile(path.resolve(targetDir, 'metadata.json'), '{}\n') + await writeFile(path.resolve(targetDir, 'chars.json'), '{}\n') + await writeFile(path.resolve(targetDir, 'index.mjs'), `${createIndexMjs()}\n`) + await writeFile(path.resolve(targetDir, 'index.js'), `${createIndexJs()}\n`) + await writeFile(path.resolve(targetDir, 'index.d.ts'), `${createIndexTypes()}\n`) +} + +const mergedCustomPublicCollection = flattenCollections(customPublicCollections, 'custom-public') +const mergedCustomVenderCollection = flattenCollections(customVenderCollections, 'custom-vender') + +await rm(path.resolve(packageDir, 'src'), { recursive: true, force: true }) +await rm(path.resolve(packageDir, 'custom-public'), { recursive: true, force: true }) +await rm(path.resolve(packageDir, 'custom-vender'), { recursive: true, force: true }) + +await writeCollectionPackage('custom-public', mergedCustomPublicCollection, 'Dify Custom Public') +await writeCollectionPackage('custom-vender', mergedCustomVenderCollection, 'Dify Custom Vender') diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cdd69e783a..57ca2e70bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,14 +7,14 @@ settings: catalogs: default: '@amplitude/analytics-browser': - specifier: 2.38.0 - version: 2.38.0 + specifier: 2.38.1 + version: 2.38.1 '@amplitude/plugin-session-replay-browser': - specifier: 1.27.5 - version: 1.27.5 + specifier: 1.27.6 + version: 1.27.6 '@antfu/eslint-config': - specifier: 7.7.3 - version: 7.7.3 + specifier: 8.0.0 + version: 8.0.0 '@base-ui/react': specifier: 1.3.0 version: 1.3.0 @@ -49,8 +49,8 @@ catalogs: specifier: 2.2.0 version: 2.2.0 '@hono/node-server': - specifier: 1.19.11 - version: 1.19.11 + specifier: 1.19.12 + version: 1.19.12 '@iconify-json/heroicons': specifier: 1.2.3 version: 1.2.3 @@ -88,11 +88,11 @@ catalogs: specifier: 4.7.0 version: 4.7.0 '@next/eslint-plugin-next': - specifier: 16.2.1 - version: 16.2.1 + specifier: 16.2.2 + version: 16.2.2 '@next/mdx': - specifier: 16.2.1 - version: 16.2.1 + specifier: 16.2.2 + version: 16.2.2 '@orpc/client': specifier: 1.13.13 version: 1.13.13 @@ -106,8 +106,8 @@ catalogs: specifier: 1.13.13 version: 1.13.13 '@playwright/test': - specifier: 1.58.2 - version: 1.58.2 + specifier: 1.59.1 + version: 1.59.1 '@remixicon/react': specifier: 4.9.0 version: 4.9.0 @@ -115,26 +115,26 @@ catalogs: specifier: 4.2.0 version: 4.2.0 '@sentry/react': - specifier: 10.46.0 - version: 10.46.0 + specifier: 10.47.0 + version: 10.47.0 '@storybook/addon-docs': - specifier: 10.3.3 - version: 10.3.3 + specifier: 10.3.4 + version: 10.3.4 '@storybook/addon-links': - specifier: 10.3.3 - version: 10.3.3 + specifier: 10.3.4 + version: 10.3.4 '@storybook/addon-onboarding': - specifier: 10.3.3 - version: 10.3.3 + specifier: 10.3.4 + version: 10.3.4 '@storybook/addon-themes': - specifier: 10.3.3 - version: 10.3.3 + specifier: 10.3.4 + version: 10.3.4 '@storybook/nextjs-vite': - specifier: 10.3.3 - version: 10.3.3 + specifier: 10.3.4 + version: 10.3.4 '@storybook/react': - specifier: 10.3.3 - version: 10.3.3 + specifier: 10.3.4 + version: 10.3.4 '@streamdown/math': specifier: 1.0.2 version: 1.0.2 @@ -154,23 +154,26 @@ catalogs: specifier: 4.2.2 version: 4.2.2 '@tanstack/eslint-plugin-query': - specifier: 5.95.2 - version: 5.95.2 + specifier: 5.96.1 + version: 5.96.1 '@tanstack/react-devtools': - specifier: 0.10.0 - version: 0.10.0 + specifier: 0.10.1 + version: 0.10.1 '@tanstack/react-form': - specifier: 1.28.5 - version: 1.28.5 + specifier: 1.28.6 + version: 1.28.6 '@tanstack/react-form-devtools': - specifier: 0.2.19 - version: 0.2.19 + specifier: 0.2.20 + version: 0.2.20 '@tanstack/react-query': - specifier: 5.95.2 - version: 5.95.2 + specifier: 5.96.1 + version: 5.96.1 '@tanstack/react-query-devtools': - specifier: 5.95.2 - version: 5.95.2 + specifier: 5.96.1 + version: 5.96.1 + '@tanstack/react-virtual': + specifier: 3.13.23 + version: 3.13.23 '@testing-library/dom': specifier: 10.4.1 version: 10.4.1 @@ -213,24 +216,18 @@ catalogs: '@types/react-dom': specifier: 19.2.3 version: 19.2.3 - '@types/react-syntax-highlighter': - specifier: 15.5.13 - version: 15.5.13 - '@types/react-window': - specifier: 1.8.8 - version: 1.8.8 '@types/sortablejs': specifier: 1.15.9 version: 1.15.9 '@typescript-eslint/eslint-plugin': - specifier: 8.57.2 - version: 8.57.2 + specifier: 8.58.0 + version: 8.58.0 '@typescript-eslint/parser': - specifier: 8.57.2 - version: 8.57.2 + specifier: 8.58.0 + version: 8.58.0 '@typescript/native-preview': - specifier: 7.0.0-dev.20260329.1 - version: 7.0.0-dev.20260329.1 + specifier: 7.0.0-dev.20260401.1 + version: 7.0.0-dev.20260401.1 '@vitejs/plugin-react': specifier: 6.0.1 version: 6.0.1 @@ -238,8 +235,8 @@ catalogs: specifier: 0.5.21 version: 0.5.21 '@vitest/coverage-v8': - specifier: 4.1.1 - version: 4.1.1 + specifier: 4.1.2 + version: 4.1.2 abcjs: specifier: 6.6.2 version: 6.6.2 @@ -259,8 +256,8 @@ catalogs: specifier: 1.1.1 version: 1.1.1 code-inspector-plugin: - specifier: 1.4.5 - version: 1.4.5 + specifier: 1.5.1 + version: 1.5.1 copy-to-clipboard: specifier: 3.3.3 version: 3.3.3 @@ -310,14 +307,11 @@ catalogs: specifier: 0.14.1 version: 0.14.1 eslint-plugin-markdown-preferences: - specifier: 0.40.3 - version: 0.40.3 + specifier: 0.41.0 + version: 0.41.0 eslint-plugin-no-barrel-files: specifier: 1.2.2 version: 1.2.2 - eslint-plugin-react-hooks: - specifier: 7.0.1 - version: 7.0.1 eslint-plugin-react-refresh: specifier: 0.5.2 version: 0.5.2 @@ -325,8 +319,8 @@ catalogs: specifier: 4.0.2 version: 4.0.2 eslint-plugin-storybook: - specifier: 10.3.3 - version: 10.3.3 + specifier: 10.3.4 + version: 10.3.4 fast-deep-equal: specifier: 3.1.3 version: 3.1.3 @@ -336,9 +330,12 @@ catalogs: happy-dom: specifier: 20.8.9 version: 20.8.9 + hast-util-to-jsx-runtime: + specifier: 2.3.6 + version: 2.3.6 hono: - specifier: 4.12.9 - version: 4.12.9 + specifier: 4.12.10 + version: 4.12.10 html-entities: specifier: 2.6.0 version: 2.6.0 @@ -346,8 +343,8 @@ catalogs: specifier: 1.11.13 version: 1.11.13 i18next: - specifier: 25.10.10 - version: 25.10.10 + specifier: 26.0.3 + version: 26.0.3 i18next-resources-to-backend: specifier: 1.2.1 version: 1.2.1 @@ -376,8 +373,8 @@ catalogs: specifier: 0.16.44 version: 0.16.44 knip: - specifier: 6.1.0 - version: 6.1.0 + specifier: 6.2.0 + version: 6.2.0 ky: specifier: 1.14.3 version: 1.14.3 @@ -388,8 +385,8 @@ catalogs: specifier: 0.42.0 version: 0.42.0 mermaid: - specifier: 11.13.0 - version: 11.13.0 + specifier: 11.14.0 + version: 11.14.0 mime: specifier: 4.1.0 version: 4.1.0 @@ -400,8 +397,8 @@ catalogs: specifier: 1.0.0 version: 1.0.0 next: - specifier: 16.2.1 - version: 16.2.1 + specifier: 16.2.2 + version: 16.2.2 next-themes: specifier: 0.4.6 version: 0.4.6 @@ -436,8 +433,8 @@ catalogs: specifier: 5.2.4 version: 5.2.4 react-i18next: - specifier: 16.6.6 - version: 16.6.6 + specifier: 17.0.2 + version: 17.0.2 react-multi-email: specifier: 1.0.25 version: 1.0.25 @@ -453,15 +450,9 @@ catalogs: react-sortablejs: specifier: 6.1.4 version: 6.1.4 - react-syntax-highlighter: - specifier: 15.6.6 - version: 15.6.6 react-textarea-autosize: specifier: 8.5.9 version: 8.5.9 - react-window: - specifier: 1.8.11 - version: 1.8.11 reactflow: specifier: 11.11.4 version: 11.11.4 @@ -471,15 +462,15 @@ catalogs: remark-directive: specifier: 4.0.0 version: 4.0.0 - sass: - specifier: 1.98.0 - version: 1.98.0 scheduler: specifier: 0.27.0 version: 0.27.0 sharp: specifier: 0.34.5 version: 0.34.5 + shiki: + specifier: 4.0.2 + version: 4.0.2 sortablejs: specifier: 1.15.7 version: 1.15.7 @@ -487,8 +478,8 @@ catalogs: specifier: 1.0.8 version: 1.0.8 storybook: - specifier: 10.3.3 - version: 10.3.3 + specifier: 10.3.4 + version: 10.3.4 streamdown: specifier: 2.5.0 version: 2.5.0 @@ -501,21 +492,15 @@ catalogs: tailwindcss: specifier: 4.2.2 version: 4.2.2 - taze: - specifier: 19.10.0 - version: 19.10.0 tldts: specifier: 7.0.27 version: 7.0.27 - tsup: - specifier: ^8.5.1 - version: 8.5.1 tsx: specifier: 4.21.0 version: 4.21.0 typescript: - specifier: 5.9.3 - version: 5.9.3 + specifier: 6.0.2 + version: 6.0.2 uglify-js: specifier: 3.19.3 version: 3.19.3 @@ -529,14 +514,14 @@ catalogs: specifier: 13.0.0 version: 13.0.0 vinext: - specifier: 0.0.38 - version: 0.0.38 + specifier: 0.0.39 + version: 0.0.39 vite-plugin-inspect: specifier: 12.0.0-beta.1 version: 12.0.0-beta.1 vite-plus: - specifier: 0.1.14 - version: 0.1.14 + specifier: 0.1.15 + version: 0.1.15 vitest-canvas-mock: specifier: 1.1.4 version: 1.1.4 @@ -561,7 +546,7 @@ overrides: array.prototype.flatmap: npm:@nolyfill/array.prototype.flatmap@^1.0.44 array.prototype.tosorted: npm:@nolyfill/array.prototype.tosorted@^1.0.44 assert: npm:@nolyfill/assert@^1.0.26 - brace-expansion@<2.0.2: 2.0.2 + brace-expansion@>=2.0.0 <2.0.3: 2.0.3 canvas: ^3.2.2 devalue@<5.3.2: 5.3.2 dompurify@>=3.1.3 <=3.3.1: 3.3.2 @@ -575,6 +560,8 @@ overrides: is-generator-function: npm:@nolyfill/is-generator-function@^1.0.44 is-typed-array: npm:@nolyfill/is-typed-array@^1.0.44 isarray: npm:@nolyfill/isarray@^1.0.44 + lodash@>=4.0.0 <= 4.17.23: 4.18.0 + lodash-es@>=4.0.0 <= 4.17.23: 4.18.0 object.assign: npm:@nolyfill/object.assign@^1.0.44 object.entries: npm:@nolyfill/object.entries@^1.0.44 object.fromentries: npm:@nolyfill/object.fromentries@^1.0.44 @@ -602,8 +589,8 @@ overrides: tar@<=7.5.10: 7.5.11 typed-array-buffer: npm:@nolyfill/typed-array-buffer@^1.0.44 undici@>=7.0.0 <7.24.0: 7.24.0 - vite: npm:@voidzero-dev/vite-plus-core@0.1.14 - vitest: npm:@voidzero-dev/vite-plus-test@0.1.14 + vite: npm:@voidzero-dev/vite-plus-core@0.1.15 + vitest: npm:@voidzero-dev/vite-plus-test@0.1.15 which-typed-array: npm:@nolyfill/which-typed-array@^1.0.44 yaml@>=2.0.0 <2.8.3: 2.8.3 yauzl@<3.2.1: 3.2.1 @@ -612,12 +599,9 @@ importers: .: devDependencies: - taze: - specifier: 'catalog:' - version: 19.10.0 vite-plus: specifier: 'catalog:' - version: 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) + version: 0.1.15(@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@6.0.2)(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) e2e: devDependencies: @@ -626,7 +610,7 @@ importers: version: 12.7.0 '@playwright/test': specifier: 'catalog:' - version: 1.58.2 + version: 1.59.1 '@types/node': specifier: 'catalog:' version: 25.5.0 @@ -635,10 +619,16 @@ importers: version: 4.21.0 typescript: specifier: 'catalog:' - version: 5.9.3 + version: 6.0.2 vite-plus: specifier: 'catalog:' - version: 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) + version: 0.1.15(@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@6.0.2)(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) + + packages/iconify-collections: + devDependencies: + iconify-import-svg: + specifier: 'catalog:' + version: 0.1.2 sdks/nodejs-client: devDependencies: @@ -650,34 +640,34 @@ importers: version: 25.5.0 '@typescript-eslint/eslint-plugin': specifier: 'catalog:' - version: 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) + version: 8.58.0(@typescript-eslint/parser@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) '@typescript-eslint/parser': specifier: 'catalog:' - version: 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + version: 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) '@vitest/coverage-v8': specifier: 'catalog:' - version: 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)) + version: 4.1.2(@voidzero-dev/vite-plus-test@0.1.15(@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@6.0.2)(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)) eslint: specifier: 'catalog:' version: 10.1.0(jiti@2.6.1) - tsup: - specifier: 'catalog:' - version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) typescript: specifier: 'catalog:' - version: 5.9.3 + version: 6.0.2 + vite-plus: + specifier: 'catalog:' + version: 0.1.15(@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@6.0.2)(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: - specifier: npm:@voidzero-dev/vite-plus-test@0.1.14 - version: '@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)' + specifier: npm:@voidzero-dev/vite-plus-test@0.1.15 + version: '@voidzero-dev/vite-plus-test@0.1.15(@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@6.0.2)(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)' web: dependencies: '@amplitude/analytics-browser': specifier: 'catalog:' - version: 2.38.0 + version: 2.38.1 '@amplitude/plugin-session-replay-browser': specifier: 'catalog:' - version: 1.27.5(@amplitude/rrweb@2.0.0-alpha.37)(rollup@4.59.0) + version: 1.27.6(@amplitude/rrweb@2.0.0-alpha.37)(rollup@4.59.0) '@base-ui/react': specifier: 'catalog:' version: 1.3.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -731,13 +721,13 @@ importers: version: 1.13.13 '@orpc/tanstack-query': specifier: 'catalog:' - version: 1.13.13(@orpc/client@1.13.13)(@tanstack/query-core@5.95.2) + version: 1.13.13(@orpc/client@1.13.13)(@tanstack/query-core@5.96.1) '@remixicon/react': specifier: 'catalog:' version: 4.9.0(react@19.2.4) '@sentry/react': specifier: 'catalog:' - version: 10.46.0(react@19.2.4) + version: 10.47.0(react@19.2.4) '@streamdown/math': specifier: 'catalog:' version: 1.0.2(react@19.2.4) @@ -746,16 +736,19 @@ importers: version: 3.2.5 '@t3-oss/env-nextjs': specifier: 'catalog:' - version: 0.13.11(typescript@5.9.3)(valibot@1.3.1(typescript@5.9.3))(zod@4.3.6) + version: 0.13.11(typescript@6.0.2)(valibot@1.3.1(typescript@6.0.2))(zod@4.3.6) '@tailwindcss/typography': specifier: 'catalog:' 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) + version: 1.28.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-query': specifier: 'catalog:' - version: 5.95.2(react@19.2.4) + version: 5.96.1(react@19.2.4) + '@tanstack/react-virtual': + specifier: 'catalog:' + version: 3.13.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4) abcjs: specifier: 'catalog:' version: 6.6.2 @@ -813,6 +806,9 @@ importers: foxact: specifier: 'catalog:' version: 0.3.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + hast-util-to-jsx-runtime: + specifier: 'catalog:' + version: 2.3.6 html-entities: specifier: 'catalog:' version: 2.6.0 @@ -821,7 +817,7 @@ importers: version: 1.11.13 i18next: specifier: 'catalog:' - version: 25.10.10(typescript@5.9.3) + version: 26.0.3(typescript@6.0.2) i18next-resources-to-backend: specifier: 'catalog:' version: 1.2.1 @@ -857,7 +853,7 @@ importers: version: 0.42.0 mermaid: specifier: 'catalog:' - version: 11.13.0 + version: 11.14.0 mime: specifier: 'catalog:' version: 4.1.0 @@ -869,13 +865,13 @@ importers: version: 1.0.0 next: specifier: 'catalog:' - version: 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) + version: 16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) next-themes: specifier: 'catalog:' version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) nuqs: specifier: 'catalog:' - version: 2.8.9(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) + version: 2.8.9(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react@19.2.4) pinyin-pro: specifier: 'catalog:' version: 3.28.0 @@ -902,7 +898,7 @@ importers: version: 5.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-i18next: specifier: 'catalog:' - version: 16.6.6(i18next@25.10.10(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + version: 17.0.2(i18next@26.0.3(typescript@6.0.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@6.0.2) react-multi-email: specifier: 'catalog:' version: 1.0.25(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -915,15 +911,9 @@ importers: react-sortablejs: specifier: 'catalog:' version: 6.1.4(@types/sortablejs@1.15.9)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sortablejs@1.15.7) - react-syntax-highlighter: - specifier: 'catalog:' - version: 15.6.6(react@19.2.4) react-textarea-autosize: specifier: 'catalog:' version: 8.5.9(@types/react@19.2.14)(react@19.2.4) - react-window: - specifier: 'catalog:' - version: 1.8.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4) reactflow: specifier: 'catalog:' version: 11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -939,6 +929,9 @@ importers: sharp: specifier: 'catalog:' version: 0.34.5 + shiki: + specifier: 'catalog:' + version: 4.0.2 sortablejs: specifier: 'catalog:' version: 1.15.7 @@ -978,19 +971,22 @@ importers: devDependencies: '@antfu/eslint-config': specifier: 'catalog:' - 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) + version: 8.0.0(@eslint-react/eslint-plugin@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(@next/eslint-plugin-next@16.2.2)(@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(@typescript-eslint/typescript-estree@8.58.0(typescript@6.0.2))(@typescript-eslint/utils@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(@voidzero-dev/vite-plus-test@0.1.15(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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@6.0.2)(yaml@2.8.3))(@vue/compiler-sfc@3.5.31)(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.58.0(oxlint-tsgolint@0.18.1))(typescript@6.0.2) '@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)) + version: 5.1.1(storybook@10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + '@dify/iconify-collections': + specifier: workspace:* + version: link:../packages/iconify-collections '@egoist/tailwindcss-icons': specifier: 'catalog:' version: 1.9.2(tailwindcss@4.2.2) '@eslint-react/eslint-plugin': specifier: 'catalog:' - version: 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + version: 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) '@hono/node-server': specifier: 'catalog:' - version: 1.19.11(hono@4.12.9) + version: 1.19.12(hono@4.12.10) '@iconify-json/heroicons': specifier: 'catalog:' version: 1.2.3 @@ -1008,49 +1004,49 @@ importers: version: 3.1.1(rollup@4.59.0) '@next/eslint-plugin-next': specifier: 'catalog:' - version: 16.2.1 + version: 16.2.2 '@next/mdx': specifier: 'catalog:' - version: 16.2.1(@mdx-js/loader@3.1.1(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.4)) + version: 16.2.2(@mdx-js/loader@3.1.1(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.4)) '@rgrove/parse-xml': specifier: 'catalog:' 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@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)) + version: 10.3.4(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.4(@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)) + version: 10.3.4(react@19.2.4)(storybook@10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) '@storybook/addon-onboarding': specifier: 'catalog:' - 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)) + version: 10.3.4(storybook@10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) '@storybook/addon-themes': specifier: 'catalog:' - 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)) + version: 10.3.4(storybook@10.3.4(@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@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)) + version: 10.3.4(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(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.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2)(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) + version: 10.3.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2) '@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)) + version: 4.2.2(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3)) '@tanstack/eslint-plugin-query': specifier: 'catalog:' - version: 5.95.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + version: 5.96.1(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) '@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) + version: 0.10.1(@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) '@tanstack/react-form-devtools': specifier: 'catalog:' - version: 0.2.19(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11) + version: 0.2.20(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11) '@tanstack/react-query-devtools': specifier: 'catalog:' - version: 5.95.2(@tanstack/react-query@5.95.2(react@19.2.4))(react@19.2.4) + version: 5.96.1(@tanstack/react-query@5.96.1(react@19.2.4))(react@19.2.4) '@testing-library/dom': specifier: 'catalog:' version: 10.4.1 @@ -1065,13 +1061,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@2.6.1)(typescript@5.9.3))(typescript@5.9.3) + version: 3.0.2(@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@6.0.2))(typescript@6.0.2) '@tsslint/compat-eslint': specifier: 'catalog:' - version: 3.0.2(jiti@2.6.1)(typescript@5.9.3) + version: 3.0.2(jiti@2.6.1)(typescript@6.0.2) '@tsslint/config': specifier: 'catalog:' - version: 3.0.2(@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@5.9.3))(typescript@5.9.3) + version: 3.0.2(@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@6.0.2))(typescript@6.0.2) '@types/js-cookie': specifier: 'catalog:' version: 3.0.6 @@ -1093,36 +1089,30 @@ importers: '@types/react-dom': specifier: 'catalog:' version: 19.2.3(@types/react@19.2.14) - '@types/react-syntax-highlighter': - specifier: 'catalog:' - version: 15.5.13 - '@types/react-window': - specifier: 'catalog:' - version: 1.8.8 '@types/sortablejs': specifier: 'catalog:' version: 1.15.9 '@typescript-eslint/parser': specifier: 'catalog:' - version: 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + version: 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260329.1 + version: 7.0.0-dev.20260401.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@2.6.1)(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.15(@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@6.0.2)(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@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) + version: 0.5.21(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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@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)) + version: 4.1.2(@voidzero-dev/vite-plus-test@0.1.15(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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@6.0.2)(yaml@2.8.3)) agentation: specifier: 'catalog:' version: 3.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) code-inspector-plugin: specifier: 'catalog:' - version: 1.4.5 + version: 1.5.1 eslint: specifier: 'catalog:' version: 10.1.0(jiti@2.6.1) @@ -1131,19 +1121,16 @@ importers: 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@2.6.1))(oxlint@1.57.0(oxlint-tsgolint@0.17.3))(tailwindcss@4.2.2)(typescript@5.9.3) + version: 4.3.2(eslint@10.1.0(jiti@2.6.1))(oxlint@1.58.0(oxlint-tsgolint@0.18.1))(tailwindcss@4.2.2)(typescript@6.0.2) eslint-plugin-hyoban: specifier: 'catalog:' 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@2.6.1)) + version: 0.41.0(@eslint/markdown@8.0.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@2.6.1))(typescript@5.9.3) - eslint-plugin-react-hooks: - specifier: 'catalog:' - version: 7.0.1(eslint@10.1.0(jiti@2.6.1)) + version: 1.2.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) eslint-plugin-react-refresh: specifier: 'catalog:' version: 0.5.2(eslint@10.1.0(jiti@2.6.1)) @@ -1152,31 +1139,25 @@ importers: 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@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) + version: 10.3.4(eslint@10.1.0(jiti@2.6.1))(storybook@10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2) happy-dom: specifier: 'catalog:' version: 20.8.9 hono: specifier: 'catalog:' - version: 4.12.9 - iconify-import-svg: - specifier: 'catalog:' - version: 0.1.2 + version: 4.12.10 knip: specifier: 'catalog:' - version: 6.1.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + version: 6.2.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) postcss: specifier: 'catalog:' version: 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)) - sass: - specifier: 'catalog:' - version: 1.98.0 storybook: specifier: 'catalog:' - version: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) tailwindcss: specifier: 'catalog:' version: 4.2.2 @@ -1185,28 +1166,28 @@ importers: version: 4.21.0 typescript: specifier: 'catalog:' - version: 5.9.3 + version: 6.0.2 uglify-js: specifier: 'catalog:' version: 3.19.3 vinext: specifier: 'catalog:' - version: 0.0.38(21fde6c2677b0aab516df83ef1beed5d) + version: 0.0.39(bf3f106951d7257e1336e695c204ec32) 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@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + specifier: npm:@voidzero-dev/vite-plus-core@0.1.15 + version: '@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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@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) + version: 12.0.0-beta.1(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3))(typescript@6.0.2)(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@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) + version: 0.1.15(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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@6.0.2)(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@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)' + specifier: npm:@voidzero-dev/vite-plus-test@0.1.15 + version: '@voidzero-dev/vite-plus-test@0.1.15(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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@6.0.2)(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@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)) + version: 1.1.4(@voidzero-dev/vite-plus-test@0.1.15(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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@6.0.2)(yaml@2.8.3)) packages: @@ -1217,17 +1198,17 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@amplitude/analytics-browser@2.38.0': - resolution: {integrity: sha512-MhqyEkr1gGAR4s4GSSflDhFVheIx9Nv3FfElQu9NlNrXB2Hh3BEOyVgdK7hgfi6NJwFyfw30+t5lym+njtA8hA==} + '@amplitude/analytics-browser@2.38.1': + resolution: {integrity: sha512-8E3WDuCz5pmVysw7iwT9MjltzaO7Sqy9jWNaXovO30Z8sXs5Ncl32qv6o14kwlpl3wRSaaAKDe0Z3Grjx3dYYQ==} - '@amplitude/analytics-client-common@2.4.41': - resolution: {integrity: sha512-+GbvtvhsUROotPBwfAxbrqovKePhC0oQKXtxjbeNQleOHjBjsAs5jEOCHpJenCKtaRpucg/FuK3NVOS09MfW7Q==} + '@amplitude/analytics-client-common@2.4.42': + resolution: {integrity: sha512-pEpE6s8GsXTlD9Jj4b/wplCQD8fT2ml/VZSnQ1E5sU0goaeZaYQKMTXGpbA2aE40ABZMwQSopxJn+puBrJc8eg==} '@amplitude/analytics-connector@1.6.4': resolution: {integrity: sha512-SpIv0IQMNIq6SH3UqFGiaZyGSc7PBZwRdq7lvP0pBxW8i4Ny+8zwI0pV+VMfMHQwWY3wdIbWw5WQphNjpdq1/Q==} - '@amplitude/analytics-core@2.44.0': - resolution: {integrity: sha512-z9QuTxLqEQ8KIeAT6Vmy6K48rP9TUmjnb4GwUMYoV/fxu3B9ClTaN18zqXQMmDw9HwUiIreHiVbwTb7OQRN5aA==} + '@amplitude/analytics-core@2.44.1': + resolution: {integrity: sha512-bx8RAYneoEyT/gsCpcktEgBMUs5vIb2piA/Kof88BaNKAWEpIa9B4Ogg4vNPqmEgNIx/wztSduFMHHw2pLcncg==} '@amplitude/analytics-types@2.11.1': resolution: {integrity: sha512-wFEgb0t99ly2uJKm5oZ28Lti0Kh5RecR5XBkwfUpDzn84IoCIZ8GJTsMw/nThu8FZFc7xFDA4UAt76zhZKrs9A==} @@ -1235,26 +1216,26 @@ packages: '@amplitude/experiment-core@0.7.2': resolution: {integrity: sha512-Wc2NWvgQ+bLJLeF0A9wBSPIaw0XuqqgkPKsoNFQrmS7r5Djd56um75In05tqmVntPJZRvGKU46pAp8o5tdf4mA==} - '@amplitude/plugin-autocapture-browser@1.25.0': - resolution: {integrity: sha512-YuWsz8XmJuKu3NlMxkvlhLey/5tGCeOwwfsROHficR0yDWO9gNG0WtHl7A0Pw1PUc9iaXjqfG2AjYumAtiq16Q==} + '@amplitude/plugin-autocapture-browser@1.25.1': + resolution: {integrity: sha512-eIaPO7eUH2W0OWe0JoqUVvMPUGDeOn4JQa7zdClEbvHnPxfGS1RHIFNsBk5ofgEWxhUo2Ka/Z0Wl86k9FMaa7w==} - '@amplitude/plugin-custom-enrichment-browser@0.1.2': - resolution: {integrity: sha512-ZX9BKqs1E1OI7l7QCGu9JnB/1kqLN+zqIePgM2tuEhZNFQJaw4NhAMUaMRqvNnaCkHlmpVRISzSj/4D3tWMRtA==} + '@amplitude/plugin-custom-enrichment-browser@0.1.3': + resolution: {integrity: sha512-iKZkqkI5CpLb62cGNgvqTVEUj8i5UBFWJc0aQMZZBqc+vmzHBaqvjeAU0dwO8KA623YfT5I+/Vp1MnqvEXGJFg==} - '@amplitude/plugin-network-capture-browser@1.9.11': - resolution: {integrity: sha512-49o3zYnKUmRdrxgAEcr1iHnXR1um40e1icO0hzugSq04k19hs27zcl3zpEk9geO+nNKwO744ryE1q93gqVbHrQ==} + '@amplitude/plugin-network-capture-browser@1.9.12': + resolution: {integrity: sha512-/8x+GDqE25pTvsU9Po7Ur+V8pUuX4IG5p2xHPM9N/APfyc3D1zLTkC8FKo8wfPpg4Wu97mSzy1JnvPDqbJcJyw==} - '@amplitude/plugin-page-url-enrichment-browser@0.7.3': - resolution: {integrity: sha512-3UZq/zKg4lcsRgziWAPSEeaUsNsbyjjxmsAE9kSDi/hIj5RaWnwWhY6TGhv45UAReugTA4vVZyFRg9btf3c/Fg==} + '@amplitude/plugin-page-url-enrichment-browser@0.7.4': + resolution: {integrity: sha512-gF7V1ypkYB7FTwKlqjbO+7Z+Wvf72RfA64aREj9aplZdRJ0EY3qSEYMA3L2v0U5ztYchiy5MJraSaaxKfzXdJg==} - '@amplitude/plugin-page-view-tracking-browser@2.9.4': - resolution: {integrity: sha512-J16zmEadnzNpkHSmzpTiQN2q9pGJ/4SkHONA9O8KxUsMU/MYTDgof3rAYY/w5B5rmvdxfMRCjqWtvnkizzgZ6w==} + '@amplitude/plugin-page-view-tracking-browser@2.9.5': + resolution: {integrity: sha512-fWewMrgo0T7AyKnrZn6ox0ER5Ibw/IFTkX0GrQ8DxcsXrmUuSWUTsxZaA7YPDzuWPbd4AX9/AWZF2i6A9Ybtfg==} - '@amplitude/plugin-session-replay-browser@1.27.5': - resolution: {integrity: sha512-tf0Ty1nNF8OJ5QQ5scEqdGfzdgIaqkRf2MSzQfHbGcTIoYuVmAKuCgn3yMLk62MKnwgG3IsTIugMdRRv7l85PA==} + '@amplitude/plugin-session-replay-browser@1.27.6': + resolution: {integrity: sha512-wHv9b/Qzu9qg0thE+qo23/KpYGiADnAj42I1C1goQAJG7XNOk62F0sdejVvnQIV9NsLe0ItoS+tg3eqlBE7Exg==} - '@amplitude/plugin-web-vitals-browser@1.1.26': - resolution: {integrity: sha512-wiD4vy+f2fepr+8Lnn26TYYjDEnWsmlGhJog99x+xfbZ/D+stGdaCIOz5AOjU1TpTRvxvamEu2XuOh+8EZOCSA==} + '@amplitude/plugin-web-vitals-browser@1.1.27': + resolution: {integrity: sha512-jh/dWMsthx5E+ensNTwj7nkqi8iG8wyJc1HryOdY49w9zTgcbZmJwE2uumLBXBasn7l62a5EdqRkwctGL53fHw==} '@amplitude/rrdom@2.0.0-alpha.37': resolution: {integrity: sha512-u4dSnBtlbJ8oU5P/Ywl2RLqvjqWbkl4ScMUbvQA7in4pWcx+0NRN+VVjLZXQcd8Fn7E/rcxjeUh7e7HfwvdasQ==} @@ -1288,20 +1269,20 @@ packages: '@amplitude/rrweb@2.0.0-alpha.37': resolution: {integrity: sha512-jJkSpPYiVgOZB422pb2jOJJn3pvb5E5f9vKK8CEmUlk2mVAl6kPQzW98mb05M65OJFj5nn9tSe9h5r5+Cl93ag==} - '@amplitude/session-replay-browser@1.35.0': - resolution: {integrity: sha512-aGqu807oC8UIMmP+g1jBYsgN+/VeR/ThtK6fpxuZCugEogx7EZ9sXDEeudUmyvkQQfWmD+nLmrhYPX8FpROT5w==} + '@amplitude/session-replay-browser@1.35.1': + resolution: {integrity: sha512-7X6T+niZaG+zpvcFOwdkbTNUWzD6T9/rQ7POYkTK+C/6FtvJ0fpHXNHdHT8fozKox2UXL/wwZvoQWFriHSe1dA==} '@amplitude/targeting@0.2.0': resolution: {integrity: sha512-/50ywTrC4hfcfJVBbh5DFbqMPPfaIOivZeb5Gb+OGM03QrA+lsUqdvtnKLNuWtceD4H6QQ2KFzPJ5aAJLyzVDA==} - '@antfu/eslint-config@7.7.3': - resolution: {integrity: sha512-BtroDxTvmWtvr3yJkdWVCvwsKlnEdkreoeOyrdNezc/W5qaiQNf2xjcsQ3N5Yy0x27h+0WFfW8rG8YlVioG6dw==} + '@antfu/eslint-config@8.0.0': + resolution: {integrity: sha512-IKiCfsa1vRgj8srB2azqiN3nOAcVyP/TZ5Ibiz0TDW9NoQPizTvkmRTSi1vo4ax0SL9TH/8uJLK6uCfd6bQzLA==} hasBin: true peerDependencies: '@angular-eslint/eslint-plugin': ^21.1.0 '@angular-eslint/eslint-plugin-template': ^21.1.0 '@angular-eslint/template-parser': ^21.1.0 - '@eslint-react/eslint-plugin': ^2.11.0 + '@eslint-react/eslint-plugin': ^3.0.0 '@next/eslint-plugin-next': '>=15.0.0' '@prettier/plugin-xml': ^3.4.1 '@unocss/eslint-plugin': '>=0.50.0' @@ -1310,7 +1291,6 @@ packages: eslint-plugin-astro: ^1.2.0 eslint-plugin-format: '>=0.1.0' eslint-plugin-jsx-a11y: '>=6.10.2' - eslint-plugin-react-hooks: ^7.0.0 eslint-plugin-react-refresh: ^0.5.0 eslint-plugin-solid: ^0.14.3 eslint-plugin-svelte: '>=2.35.1' @@ -1341,8 +1321,6 @@ packages: optional: true eslint-plugin-jsx-a11y: optional: true - eslint-plugin-react-hooks: - optional: true eslint-plugin-react-refresh: optional: true eslint-plugin-solid: @@ -1361,11 +1339,6 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@antfu/ni@28.3.0': - resolution: {integrity: sha512-JbRijiCNAGcQcyPfV0EXOJYwV27e/srXfTvETqzbbh4jzHBV2pDYiBz8rj5SyzX27aTbCK+qXR3x6g2WKokcrA==} - engines: {node: '>=20'} - hasBin: true - '@antfu/utils@8.1.1': resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==} @@ -1492,32 +1465,32 @@ packages: '@clack/core@0.3.5': resolution: {integrity: sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ==} - '@clack/core@1.1.0': - resolution: {integrity: sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA==} + '@clack/core@1.2.0': + resolution: {integrity: sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg==} '@clack/prompts@0.8.2': resolution: {integrity: sha512-6b9Ab2UiZwJYA9iMyboYyW9yJvAO9V753ZhS+DHKEjZRKAxPPOb7MXXu84lsPFG+vZt6FRFniZ8rXi+zCIw4yQ==} - '@clack/prompts@1.1.0': - resolution: {integrity: sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g==} + '@clack/prompts@1.2.0': + resolution: {integrity: sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w==} - '@code-inspector/core@1.4.5': - resolution: {integrity: sha512-wskkSRX13TAqJG65d5sq0bRZ4kYktas/iE70xqXMOeqW/A6n2Zqhw5QRHANmEmlBvB9bP/bse+9iBkNN3Q2Skw==} + '@code-inspector/core@1.5.1': + resolution: {integrity: sha512-Y9JdgoxVh93xRMupTa1lT/v+UlcBEpM7Y1BTxQy924wSe6VVEXsJ1nPJ/Ob2HPMUAA6F568aHALi2KDUhA2kzg==} - '@code-inspector/esbuild@1.4.5': - resolution: {integrity: sha512-KBwq7waqZ3L1CW7N9ff7aS0HxzamrslR08i5ovkLQe1p6tH9Axe9zzCrBnvgmB0UZsT2r/5wKLOWyEpq5+VYKw==} + '@code-inspector/esbuild@1.5.1': + resolution: {integrity: sha512-Z/WZVCG6WaB9HTcDC8l15RpgEsfFj/WKLLr6cKNX/JzAYBroadLPw1N0sbUJUIQnow5cCo7KYpHrC1T27WVMnw==} - '@code-inspector/mako@1.4.5': - resolution: {integrity: sha512-yrHgE5+b4ZL29Xt+y0H/9xrXSbRskq7dFhmE9GYFWCcgdWNCMD25hZd7xZVije94++H65Vw6Bu/abfqEx0peog==} + '@code-inspector/mako@1.5.1': + resolution: {integrity: sha512-EQmqQnnyW8tf3EBRlYyRYv1n3W1PUcfaYuuXXAfBdfJIGMwJjj0PcrDsdiI5MNyFmIx3QdMREhWmPMx1LoAANg==} - '@code-inspector/turbopack@1.4.5': - resolution: {integrity: sha512-IG39ikmQthdx/oAxhpV7zsIQZ3Jpycl88JzH+UXHq0ZpfHwa1KdNc/9erP3kFMY4+ANmkmerqBk57knmRTGMRQ==} + '@code-inspector/turbopack@1.5.1': + resolution: {integrity: sha512-PeLbcDtKDoSrKPsWnwQc+Yj9KgCa3xbHxEwXa/aGVykilvfvYP9AH1z5BRyZLDgB21diSV75BPNpF+o/FQRYug==} - '@code-inspector/vite@1.4.5': - resolution: {integrity: sha512-vBtH91afwYL7JV4zWcJJTFd65LJ4SZz5E9AwGgCF30/L1mdDx7U29D+M+JpaxSgsMB6monKSZh+ubbqYe0ixpQ==} + '@code-inspector/vite@1.5.1': + resolution: {integrity: sha512-gkfmSmawYb1yDDuCft4DESXCAD3JxPt59dGiRoD78GhQzSYHk3tnLPZMH/GLBpdeFNbKHi1FtEMbAAECIJG9xg==} - '@code-inspector/webpack@1.4.5': - resolution: {integrity: sha512-lwUv+X1FNSUWz+FKcUsE2dT2pg6VFRRXKt16hg/m+Lwtdet2adfi6BFLZmNz3OPIEGbRB5Kjx6bfaghZhbDCCg==} + '@code-inspector/webpack@1.5.1': + resolution: {integrity: sha512-8i3QI/bSirORDF/0P16T6NhNy1RxO7soip8sWeV/2btLbYCwyiaDnqT4Bw3JaM8MNz0N8NaA2qItUrrKE7TtCg==} '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} @@ -1581,11 +1554,11 @@ packages: '@cucumber/tag-expressions@9.1.0': resolution: {integrity: sha512-bvHjcRFZ+J1TqIa9eFNO1wGHqwx4V9ZKV3hYgkuK/VahHx73uiP4rKV3JVrvWSMrwrFvJG6C8aEwnCWSvbyFdQ==} - '@e18e/eslint-plugin@0.2.0': - resolution: {integrity: sha512-mXgODVwhuDjTJ+UT+XSvmMmCidtGKfrV5nMIv1UtpWex2pYLsIM3RSpT8HWIMAebS9qANbXPKlSX4BE7ZvuCgA==} + '@e18e/eslint-plugin@0.3.0': + resolution: {integrity: sha512-hHgfpxsrZ2UYHcicA+tGZnmk19uJTaye9VH79O+XS8R4ona2Hx3xjhXghclNW58uXMk3xXlbYEOMr8thsoBmWg==} peerDependencies: eslint: ^9.0.0 || ^10.0.0 - oxlint: ^1.41.0 + oxlint: ^1.55.0 peerDependenciesMeta: eslint: optional: true @@ -1890,6 +1863,10 @@ packages: resolution: {integrity: sha512-R8uZemG9dKTbru/DQRPblbJyXpObwKzo8rv1KYGGuPUPtjM4LXBYM9q5CIZAComzZupws3tWbDwam5AFpPLyJQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/markdown@8.0.1': + resolution: {integrity: sha512-WWKmld/EyNdEB8GMq7JMPX1SDWgyJAM1uhtCi5ySrqYQM4HQjmg11EX/q3ZpnpRXHfdccFtli3NBvvGaYjWyQw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@eslint/object-schema@2.1.7': resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1950,16 +1927,13 @@ packages: react: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc - '@henrygd/queue@1.2.0': - resolution: {integrity: sha512-jW/BLSTpcvExDhqJGxtIPgGr2O0IFF8XUNDwEbfCfhrXT8a4xztQ9Lv6U/vbYzYC0xVWn+3zv6YnLUh3bEFUKA==} - '@heroicons/react@2.2.0': resolution: {integrity: sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==} peerDependencies: react: '>= 16 || ^19.0.0-rc' - '@hono/node-server@1.19.11': - resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} + '@hono/node-server@1.19.12': + resolution: {integrity: sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw==} engines: {node: '>=18.14.1'} peerDependencies: hono: ^4 @@ -2155,11 +2129,11 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.4': - resolution: {integrity: sha512-6PyZBYKnnVNqOSB0YFly+62R7dmov8segT27A+RVTBVd4iAE6kbW9QBJGlyR2yG4D4ohzhZSTIu7BK1UTtmFFA==} + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0': + resolution: {integrity: sha512-qvsTEwEFefhdirGOPnu9Wp6ChfIwy2dBCRuETU3uE+4cC+PFoxMSiiEhxk4lOluA34eARHA0OxqsEUYDqRMgeQ==} peerDependencies: typescript: '>= 4.3.x' - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: typescript: optional: true @@ -2279,8 +2253,8 @@ packages: peerDependencies: rollup: 4.59.0 - '@mermaid-js/parser@1.0.1': - resolution: {integrity: sha512-opmV19kN1JsK0T6HhhokHpcVkqKpF+x2pPDKKM2ThHtZAB5F4PROopk0amuVYK5qMrIA4erzpNm8gmPNJgMDxQ==} + '@mermaid-js/parser@1.1.0': + resolution: {integrity: sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==} '@monaco-editor/loader@1.7.0': resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==} @@ -2304,14 +2278,14 @@ packages: '@next/env@16.0.0': resolution: {integrity: sha512-s5j2iFGp38QsG1LWRQaE2iUY3h1jc014/melHFfLdrsMJPqxqDQwWNwyQTcNoUSGZlCVZuM7t7JDMmSyRilsnA==} - '@next/env@16.2.1': - resolution: {integrity: sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg==} + '@next/env@16.2.2': + resolution: {integrity: sha512-LqSGz5+xGk9EL/iBDr2yo/CgNQV6cFsNhRR2xhSXYh7B/hb4nePCxlmDvGEKG30NMHDFf0raqSyOZiQrO7BkHQ==} - '@next/eslint-plugin-next@16.2.1': - resolution: {integrity: sha512-r0epZGo24eT4g08jJlg2OEryBphXqO8aL18oajoTKLzHJ6jVr6P6FI58DLMug04MwD3j8Fj0YK0slyzneKVyzA==} + '@next/eslint-plugin-next@16.2.2': + resolution: {integrity: sha512-IOPbWzDQ+76AtjZioaCjpIY72xNSDMnarZ2GMQ4wjNLvnJEJHqxQwGFhgnIWLV9klb4g/+amg88Tk5OXVpyLTw==} - '@next/mdx@16.2.1': - resolution: {integrity: sha512-w0YOkOc+WEnsTJ8uxzBOvpe3R+9BnJOxWCE7qcI/62CzJiUEd8JKtF25e3R8cW5BGsKyRW8p4zE2JLyXKa8xdw==} + '@next/mdx@16.2.2': + resolution: {integrity: sha512-2CbRTXE6sJ7zDAaKXknb5FrrPs46iJeMPzuoBXsAOV/XVnxABGD4mSDusn0VuCoII/KjUZ+zsuo2VFbchYQXng==} peerDependencies: '@mdx-js/loader': '>=0.15.0' '@mdx-js/react': '>=0.15.0' @@ -2321,54 +2295,54 @@ packages: '@mdx-js/react': optional: true - '@next/swc-darwin-arm64@16.2.1': - resolution: {integrity: sha512-BwZ8w8YTaSEr2HIuXLMLxIdElNMPvY9fLqb20LX9A9OMGtJilhHLbCL3ggyd0TwjmMcTxi0XXt+ur1vWUoxj2Q==} + '@next/swc-darwin-arm64@16.2.2': + resolution: {integrity: sha512-B92G3ulrwmkDSEJEp9+XzGLex5wC1knrmCSIylyVeiAtCIfvEJYiN3v5kXPlYt5R4RFlsfO/v++aKV63Acrugg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.2.1': - resolution: {integrity: sha512-/vrcE6iQSJq3uL3VGVHiXeaKbn8Es10DGTGRJnRZlkNQQk3kaNtAJg8Y6xuAlrx/6INKVjkfi5rY0iEXorZ6uA==} + '@next/swc-darwin-x64@16.2.2': + resolution: {integrity: sha512-7ZwSgNKJNQiwW0CKhNm9B1WS2L1Olc4B2XY0hPYCAL3epFnugMhuw5TMWzMilQ3QCZcCHoYm9NGWTHbr5REFxw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.2.1': - resolution: {integrity: sha512-uLn+0BK+C31LTVbQ/QU+UaVrV0rRSJQ8RfniQAHPghDdgE+SlroYqcmFnO5iNjNfVWCyKZHYrs3Nl0mUzWxbBw==} + '@next/swc-linux-arm64-gnu@16.2.2': + resolution: {integrity: sha512-c3m8kBHMziMgo2fICOP/cd/5YlrxDU5YYjAJeQLyFsCqVF8xjOTH/QYG4a2u48CvvZZSj1eHQfBCbyh7kBr30Q==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.2.1': - resolution: {integrity: sha512-ssKq6iMRnHdnycGp9hCuGnXJZ0YPr4/wNwrfE5DbmvEcgl9+yv97/Kq3TPVDfYome1SW5geciLB9aiEqKXQjlQ==} + '@next/swc-linux-arm64-musl@16.2.2': + resolution: {integrity: sha512-VKLuscm0P/mIfzt+SDdn2+8TNNJ7f0qfEkA+az7OqQbjzKdBxAHs0UvuiVoCtbwX+dqMEL9U54b5wQ/aN3dHeg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.2.1': - resolution: {integrity: sha512-HQm7SrHRELJ30T1TSmT706IWovFFSRGxfgUkyWJZF/RKBMdbdRWJuFrcpDdE5vy9UXjFOx6L3mRdqH04Mmx0hg==} + '@next/swc-linux-x64-gnu@16.2.2': + resolution: {integrity: sha512-kU3OPHJq6sBUjOk7wc5zJ7/lipn8yGldMoAv4z67j6ov6Xo/JvzA7L7LCsyzzsXmgLEhk3Qkpwqaq/1+XpNR3g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.2.1': - resolution: {integrity: sha512-aV2iUaC/5HGEpbBkE+4B8aHIudoOy5DYekAKOMSHoIYQ66y/wIVeaRx8MS2ZMdxe/HIXlMho4ubdZs/J8441Tg==} + '@next/swc-linux-x64-musl@16.2.2': + resolution: {integrity: sha512-CKXRILyErMtUftp+coGcZ38ZwE/Aqq45VMCcRLr2I4OXKrgxIBDXHnBgeX/UMil0S09i2JXaDL3Q+TN8D/cKmg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.2.1': - resolution: {integrity: sha512-IXdNgiDHaSk0ZUJ+xp0OQTdTgnpx1RCfRTalhn3cjOP+IddTMINwA7DXZrwTmGDO8SUr5q2hdP/du4DcrB1GxA==} + '@next/swc-win32-arm64-msvc@16.2.2': + resolution: {integrity: sha512-sS/jSk5VUoShUqINJFvNjVT7JfR5ORYj/+/ZpOYbbIohv/lQfduWnGAycq2wlknbOql2xOR0DoV0s6Xfcy49+g==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.2.1': - resolution: {integrity: sha512-qvU+3a39Hay+ieIztkGSbF7+mccbbg1Tk25hc4JDylf8IHjYmY/Zm64Qq1602yPyQqvie+vf5T/uPwNxDNIoeg==} + '@next/swc-win32-x64-msvc@16.2.2': + resolution: {integrity: sha512-aHaKceJgdySReT7qeck5oShucxWRiiEuwCGK8HHALe6yZga8uyFpLkPgaRw3kkF04U7ROogL/suYCNt/+CuXGA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -2560,8 +2534,8 @@ packages: cpu: [x64] os: [win32] - '@oxc-project/runtime@0.121.0': - resolution: {integrity: sha512-p0bQukD8OEHxzY4T9OlANBbEFGnOnjo1CYi50HES7OD36UO2yPh6T+uOJKLtlg06eclxroipRCpQGMpeH8EJ/g==} + '@oxc-project/runtime@0.122.0': + resolution: {integrity: sha512-vevyz3bNjevQFCV2Yg5o6Sp9BSoiYiJVymMrzA3S1ZGj4J8ak4YiywhFyQMueQ3UNlJU6HZOZYDy70TUc99aHw==} engines: {node: ^20.19.0 || >=22.12.0} '@oxc-project/types@0.121.0': @@ -2678,276 +2652,276 @@ packages: cpu: [x64] os: [win32] - '@oxfmt/binding-android-arm-eabi@0.42.0': - resolution: {integrity: sha512-dsqPTYsozeokRjlrt/b4E7Pj0z3eS3Eg74TWQuuKbjY4VttBmA88rB7d50Xrd+TZ986qdXCNeZRPEzZHAe+jow==} + '@oxfmt/binding-android-arm-eabi@0.43.0': + resolution: {integrity: sha512-CgU2s+/9hHZgo0IxVxrbMPrMj+tJ6VM3mD7Mr/4oiz4FNTISLoCvRmB5nk4wAAle045RtRjd86m673jwPyb1OQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxfmt/binding-android-arm64@0.42.0': - resolution: {integrity: sha512-t+aAjHxcr5eOBphFHdg1ouQU9qmZZoRxnX7UOJSaTwSoKsb6TYezNKO0YbWytGXCECObRqNcUxPoPr0KaraAIg==} + '@oxfmt/binding-android-arm64@0.43.0': + resolution: {integrity: sha512-T9OfRwjA/EdYxAqbvR7TtqLv5nIrwPXuCtTwOHtS7aR9uXyn74ZYgzgTo6/ZwvTq9DY4W+DsV09hB2EXgn9EbA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxfmt/binding-darwin-arm64@0.42.0': - resolution: {integrity: sha512-ulpSEYMKg61C5bRMZinFHrKJYRoKGVbvMEXA5zM1puX3O9T6Q4XXDbft20yrDijpYWeuG59z3Nabt+npeTsM1A==} + '@oxfmt/binding-darwin-arm64@0.43.0': + resolution: {integrity: sha512-o3i49ZUSJWANzXMAAVY1wnqb65hn4JVzwlRQ5qfcwhRzIA8lGVaud31Q3by5ALHPrksp5QEaKCQF9aAS3TXpZA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxfmt/binding-darwin-x64@0.42.0': - resolution: {integrity: sha512-ttxLKhQYPdFiM8I/Ri37cvqChE4Xa562nNOsZFcv1CKTVLeEozXjKuYClNvxkXmNlcF55nzM80P+CQkdFBu+uQ==} + '@oxfmt/binding-darwin-x64@0.43.0': + resolution: {integrity: sha512-vWECzzCFkb0kK6jaHjbtC5sC3adiNWtqawFCxhpvsWlzVeKmv5bNvkB4nux+o4JKWTpHCM57NDK/MeXt44txmA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxfmt/binding-freebsd-x64@0.42.0': - resolution: {integrity: sha512-Og7QS3yI3tdIKYZ58SXik0rADxIk2jmd+/YvuHRyKULWpG4V2fR5V4hvKm624Mc0cQET35waPXiCQWvjQEjwYQ==} + '@oxfmt/binding-freebsd-x64@0.43.0': + resolution: {integrity: sha512-rgz8JpkKiI/umOf7fl9gwKyQasC8bs5SYHy6g7e4SunfLBY3+8ATcD5caIg8KLGEtKFm5ujKaH8EfjcmnhzTLg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxfmt/binding-linux-arm-gnueabihf@0.42.0': - resolution: {integrity: sha512-jwLOw/3CW4H6Vxcry4/buQHk7zm9Ne2YsidzTL1kpiMe4qqrRCwev3dkyWe2YkFmP+iZCQ7zku4KwjcLRoh8ew==} + '@oxfmt/binding-linux-arm-gnueabihf@0.43.0': + resolution: {integrity: sha512-nWYnF3vIFzT4OM1qL/HSf1Yuj96aBuKWSaObXHSWliwAk2rcj7AWd6Lf7jowEBQMo4wCZVnueIGw/7C4u0KTBQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm-musleabihf@0.42.0': - resolution: {integrity: sha512-XwXu2vkMtiq2h7tfvN+WA/9/5/1IoGAVCFPiiQUvcAuG3efR97KNcRGM8BetmbYouFotQ2bDal3yyjUx6IPsTg==} + '@oxfmt/binding-linux-arm-musleabihf@0.43.0': + resolution: {integrity: sha512-sFg+NWJbLfupYTF4WELHAPSnLPOn1jiDZ33Z1jfDnTaA+cC3iB35x0FMMZTFdFOz3icRIArncwCcemJFGXu6TQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm64-gnu@0.42.0': - resolution: {integrity: sha512-ea7s/XUJoT7ENAtUQDudFe3nkSM3e3Qpz4nJFRdzO2wbgXEcjnchKLEsV3+t4ev3r8nWxIYr9NRjPWtnyIFJVA==} + '@oxfmt/binding-linux-arm64-gnu@0.43.0': + resolution: {integrity: sha512-MelWqv68tX6wZEILDrTc9yewiGXe7im62+5x0bNXlCYFOZdA+VnYiJfAihbROsZ5fm90p9C3haFrqjj43XnlAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-arm64-musl@0.42.0': - resolution: {integrity: sha512-+JA0YMlSdDqmacygGi2REp57c3fN+tzARD8nwsukx9pkCHK+6DkbAA9ojS4lNKsiBjIW8WWa0pBrBWhdZEqfuw==} + '@oxfmt/binding-linux-arm64-musl@0.43.0': + resolution: {integrity: sha512-ROaWfYh+6BSJ1Arwy5ujijTlwnZetxDxzBpDc1oBR4d7rfrPBqzeyjd5WOudowzQUgyavl2wEpzn1hw3jWcqLA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-ppc64-gnu@0.42.0': - resolution: {integrity: sha512-VfnET0j4Y5mdfCzh5gBt0NK28lgn5DKx+8WgSMLYYeSooHhohdbzwAStLki9pNuGy51y4I7IoW8bqwAaCMiJQg==} + '@oxfmt/binding-linux-ppc64-gnu@0.43.0': + resolution: {integrity: sha512-PJRs/uNxmFipJJ8+SyKHh7Y7VZIKQicqrrBzvfyM5CtKi8D7yZKTwUOZV3ffxmiC2e7l1SDJpkBEOyue5NAFsg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-gnu@0.42.0': - resolution: {integrity: sha512-gVlCbmBkB0fxBWbhBj9rcxezPydsQHf4MFKeHoTSPicOQ+8oGeTQgQ8EeesSybWeiFPVRx3bgdt4IJnH6nOjAA==} + '@oxfmt/binding-linux-riscv64-gnu@0.43.0': + resolution: {integrity: sha512-j6biGAgzIhj+EtHXlbNumvwG7XqOIdiU4KgIWRXAEj/iUbHKukKW8eXa4MIwpQwW1YkxovduKtzEAPnjlnAhVQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-musl@0.42.0': - resolution: {integrity: sha512-zN5OfstL0avgt/IgvRu0zjQzVh/EPkcLzs33E9LMAzpqlLWiPWeMDZyMGFlSRGOdDjuNmlZBCgj0pFnK5u32TQ==} + '@oxfmt/binding-linux-riscv64-musl@0.43.0': + resolution: {integrity: sha512-RYWxAcslKxvy7yri24Xm9cmD0RiANaiEPs007EFG6l9h1ChM69Q5SOzACaCoz4Z9dEplnhhneeBaTWMEdpgIbA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-s390x-gnu@0.42.0': - resolution: {integrity: sha512-9X6+H2L0qMc2sCAgO9HS03bkGLMKvOFjmEdchaFlany3vNZOjnVui//D8k/xZAtQv2vaCs1reD5KAgPoIU4msA==} + '@oxfmt/binding-linux-s390x-gnu@0.43.0': + resolution: {integrity: sha512-DT6Q8zfQQy3jxpezAsBACEHNUUixKSYTwdXeXojNHe4DQOoxjPdjr3Szu6BRNjxLykZM/xMNmp9ElOIyDppwtw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-gnu@0.42.0': - resolution: {integrity: sha512-BajxJ6KQvMMdpXGPWhBGyjb2Jvx4uec0w+wi6TJZ6Tv7+MzPwe0pO8g5h1U0jyFgoaF7mDl6yKPW3ykWcbUJRw==} + '@oxfmt/binding-linux-x64-gnu@0.43.0': + resolution: {integrity: sha512-R8Yk7iYcuZORXmCfFZClqbDxRZgZ9/HEidUuBNdoX8Ptx07cMePnMVJ/woB84lFIDjh2ROHVaOP40Ds3rBXFqg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-musl@0.42.0': - resolution: {integrity: sha512-0wV284I6vc5f0AqAhgAbHU2935B4bVpncPoe5n/WzVZY/KnHgqxC8iSFGeSyLWEgstFboIcWkOPck7tqbdHkzA==} + '@oxfmt/binding-linux-x64-musl@0.43.0': + resolution: {integrity: sha512-F2YYqyvnQNvi320RWZNAvsaWEHwmW3k4OwNJ1hZxRKXupY63expbBaNp6jAgvYs7y/g546vuQnGHQuCBhslhLQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxfmt/binding-openharmony-arm64@0.42.0': - resolution: {integrity: sha512-p4BG6HpGnhfgHk1rzZfyR6zcWkE7iLrWxyehHfXUy4Qa5j3e0roglFOdP/Nj5cJJ58MA3isQ5dlfkW2nNEpolw==} + '@oxfmt/binding-openharmony-arm64@0.43.0': + resolution: {integrity: sha512-OE6TdietLXV3F6c7pNIhx/9YC1/2YFwjU9DPc/fbjxIX19hNIaP1rS0cFjCGJlGX+cVJwIKWe8Mos+LdQ1yAJw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxfmt/binding-win32-arm64-msvc@0.42.0': - resolution: {integrity: sha512-mn//WV60A+IetORDxYieYGAoQso4KnVRRjORDewMcod4irlRe0OSC7YPhhwaexYNPQz/GCFk+v9iUcZ2W22yxQ==} + '@oxfmt/binding-win32-arm64-msvc@0.43.0': + resolution: {integrity: sha512-0nWK6a7pGkbdoypfVicmV9k/N1FwjPZENoqhlTU+5HhZnAhpIO3za30nEE33u6l6tuy9OVfpdXUqxUgZ+4lbZw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.42.0': - resolution: {integrity: sha512-3gWltUrvuz4LPJXWivoAxZ28Of2O4N7OGuM5/X3ubPXCEV8hmgECLZzjz7UYvSDUS3grfdccQwmjynm+51EFpw==} + '@oxfmt/binding-win32-ia32-msvc@0.43.0': + resolution: {integrity: sha512-9aokTR4Ft+tRdvgN/pKzSkVy2ksc4/dCpDm9L/xFrbIw0yhLtASLbvoG/5WOTUh/BRPPnfGTsWznEqv0dlOmhA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxfmt/binding-win32-x64-msvc@0.42.0': - resolution: {integrity: sha512-Wg4TMAfQRL9J9AZevJ/ZNy3uyyDztDYQtGr4P8UyyzIhLhFrdSmz1J/9JT+rv0fiCDLaFOBQnj3f3K3+a5PzDQ==} + '@oxfmt/binding-win32-x64-msvc@0.43.0': + resolution: {integrity: sha512-4bPgdQux2ZLWn3bf2TTXXMHcJB4lenmuxrLqygPmvCJ104Yqzj1UctxSRzR31TiJ4MLaG22RK8dUsVpJtrCz5g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxlint-tsgolint/darwin-arm64@0.17.3': - resolution: {integrity: sha512-5aDl4mxXWs+Bj02pNrX6YY6v9KMZjLIytXoqolLEo0dfBNVeZUonZgJAa/w0aUmijwIRrBhxEzb42oLuUtfkGw==} + '@oxlint-tsgolint/darwin-arm64@0.18.1': + resolution: {integrity: sha512-CxSd15ZwHn70UJFTXVvy76bZ9zwI097cVyjvUFmYRJwvkQF3VnrTf2oe1gomUacErksvtqLgn9OKvZhLMYwvog==} cpu: [arm64] os: [darwin] - '@oxlint-tsgolint/darwin-x64@0.17.3': - resolution: {integrity: sha512-gPBy4DS5ueCgXzko20XsNZzDe/Cxde056B+QuPLGvz05CGEAtmRfpImwnyY2lAXXjPL+SmnC/OYexu8zI12yHQ==} + '@oxlint-tsgolint/darwin-x64@0.18.1': + resolution: {integrity: sha512-LE7VW/T/VcKhl3Z1ev5BusrxdlQ3DWweSeOB+qpBeur2h8+vCWq+M7tCO29C7lveBDfx1+rNwj4aiUVlA+Qs+g==} cpu: [x64] os: [darwin] - '@oxlint-tsgolint/linux-arm64@0.17.3': - resolution: {integrity: sha512-+pkunvCfB6pB0G9qHVVXUao3nqzXQPo4O3DReIi+5nGa+bOU3J3Srgy+Zb8VyOL+WDsSMJ+U7+r09cKHWhz3hg==} + '@oxlint-tsgolint/linux-arm64@0.18.1': + resolution: {integrity: sha512-2AG8YIXVJJbnM0rcsJmzzWOjZXBu5REwowgUpbHZueF7OYM3wR7Xu8pXEpAojEHAtYYZ3X4rpPoetomkJx7kCw==} cpu: [arm64] os: [linux] - '@oxlint-tsgolint/linux-x64@0.17.3': - resolution: {integrity: sha512-/kW5oXtBThu4FjmgIBthdmMjWLzT3M1TEDQhxDu7hQU5xDeTd60CDXb2SSwKCbue9xu7MbiFoJu83LN0Z/d38g==} + '@oxlint-tsgolint/linux-x64@0.18.1': + resolution: {integrity: sha512-f8vDYPEdiwpA2JaDEkadTXfuqIgweQ8zcL4SX75EN2kkW2oAynjN7cd8m86uXDgB0JrcyOywbRtwnXdiIzXn2A==} cpu: [x64] os: [linux] - '@oxlint-tsgolint/win32-arm64@0.17.3': - resolution: {integrity: sha512-NMELRvbz4Ed4dxg8WiqZxtu3k4OJEp2B9KInZW+BMfqEqbwZdEJY83tbqz2hD1EjKO2akrqBQ0GpRUJEkd8kKw==} + '@oxlint-tsgolint/win32-arm64@0.18.1': + resolution: {integrity: sha512-fBdML05KMDAL9ebWeoHIzkyI86Eq6r9YH5UDRuXJ9vAIo1EnKo0ti7hLUxNdc2dy2FF/T4k98p5wkkXvLyXqfA==} cpu: [arm64] os: [win32] - '@oxlint-tsgolint/win32-x64@0.17.3': - resolution: {integrity: sha512-+pJ7r8J3SLPws5uoidVplZc8R/lpKyKPE6LoPGv9BME00Y1VjT6jWGx/dtUN8PWvcu3iTC6k+8u3ojFSJNmWTg==} + '@oxlint-tsgolint/win32-x64@0.18.1': + resolution: {integrity: sha512-cYZMhNrsq9ZZ3OUWHyawqiS+c8HfieYG0zuZP2LbEuWWPfdZM/22iAlo608J+27G1s9RXQhvgX6VekwWbXbD7A==} cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.57.0': - resolution: {integrity: sha512-C7EiyfAJG4B70496eV543nKiq5cH0o/xIh/ufbjQz3SIvHhlDDsyn+mRFh+aW8KskTyUpyH2LGWL8p2oN6bl1A==} + '@oxlint/binding-android-arm-eabi@1.58.0': + resolution: {integrity: sha512-1T7UN3SsWWxpWyWGn1cT3ASNJOo+pI3eUkmEl7HgtowapcV8kslYpFQcYn431VuxghXakPNlbjRwhqmR37PFOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.57.0': - resolution: {integrity: sha512-9i80AresjZ/FZf5xK8tKFbhQnijD4s1eOZw6/FHUwD59HEZbVLRc2C88ADYJfLZrF5XofWDiRX/Ja9KefCLy7w==} + '@oxlint/binding-android-arm64@1.58.0': + resolution: {integrity: sha512-GryzujxuiRv2YFF7bRy8mKcxlbuAN+euVUtGJt9KKbLT8JBUIosamVhcthLh+VEr6KE6cjeVMAQxKAzJcoN7dg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.57.0': - resolution: {integrity: sha512-0eUfhRz5L2yKa9I8k3qpyl37XK3oBS5BvrgdVIx599WZK63P8sMbg+0s4IuxmIiZuBK68Ek+Z+gcKgeYf0otsg==} + '@oxlint/binding-darwin-arm64@1.58.0': + resolution: {integrity: sha512-7/bRSJIwl4GxeZL9rPZ11anNTyUO9epZrfEJH/ZMla3+/gbQ6xZixh9nOhsZ0QwsTW7/5J2A/fHbD1udC5DQQA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.57.0': - resolution: {integrity: sha512-UvrSuzBaYOue+QMAcuDITe0k/Vhj6KZGjfnI6x+NkxBTke/VoM7ZisaxgNY0LWuBkTnd1OmeQfEQdQ48fRjkQg==} + '@oxlint/binding-darwin-x64@1.58.0': + resolution: {integrity: sha512-EqdtJSiHweS2vfILNrpyJ6HUwpEq2g7+4Zx1FPi4hu3Hu7tC3znF6ufbXO8Ub2LD4mGgznjI7kSdku9NDD1Mkg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.57.0': - resolution: {integrity: sha512-wtQq0dCoiw4bUwlsNVDJJ3pxJA218fOezpgtLKrbQqUtQJcM9yP8z+I9fu14aHg0uyAxIY+99toL6uBa2r7nxA==} + '@oxlint/binding-freebsd-x64@1.58.0': + resolution: {integrity: sha512-VQt5TH4M42mY20F545G637RKxV/yjwVtKk2vfXuazfReSIiuvWBnv+FVSvIV5fKVTJNjt3GSJibh6JecbhGdBw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.57.0': - resolution: {integrity: sha512-qxFWl2BBBFcT4djKa+OtMdnLgoHEJXpqjyGwz8OhW35ImoCwR5qtAGqApNYce5260FQqoAHW8S8eZTjiX67Tsg==} + '@oxlint/binding-linux-arm-gnueabihf@1.58.0': + resolution: {integrity: sha512-fBYcj4ucwpAtjJT3oeBdFBYKvNyjRSK+cyuvBOTQjh0jvKp4yeA4S/D0IsCHus/VPaNG5L48qQkh+Vjy3HL2/Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.57.0': - resolution: {integrity: sha512-SQoIsBU7J0bDW15/f0/RvxHfY3Y0+eB/caKBQtNFbuerTiA6JCYx9P1MrrFTwY2dTm/lMgTSgskvCEYk2AtG/Q==} + '@oxlint/binding-linux-arm-musleabihf@1.58.0': + resolution: {integrity: sha512-0BeuFfwlUHlJ1xpEdSD1YO3vByEFGPg36uLjK1JgFaxFb4W6w17F8ET8sz5cheZ4+x5f2xzdnRrrWv83E3Yd8g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.57.0': - resolution: {integrity: sha512-jqxYd1W6WMeozsCmqe9Rzbu3SRrGTyGDAipRlRggetyYbUksJqJKvUNTQtZR/KFoJPb+grnSm5SHhdWrywv3RQ==} + '@oxlint/binding-linux-arm64-gnu@1.58.0': + resolution: {integrity: sha512-TXlZgnPTlxrQzxG9ZXU7BNwx1Ilrr17P3GwZY0If2EzrinqRH3zXPc3HrRcBJgcsoZNMuNL5YivtkJYgp467UQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.57.0': - resolution: {integrity: sha512-i66WyEPVEvq9bxRUCJ/MP5EBfnTDN3nhwEdFZFTO5MmLLvzngfWEG3NSdXQzTT3vk5B9i6C2XSIYBh+aG6uqyg==} + '@oxlint/binding-linux-arm64-musl@1.58.0': + resolution: {integrity: sha512-zSoYRo5dxHLcUx93Stl2hW3hSNjPt99O70eRVWt5A1zwJ+FPjeCCANCD2a9R4JbHsdcl11TIQOjyigcRVOH2mw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.57.0': - resolution: {integrity: sha512-oMZDCwz4NobclZU3pH+V1/upVlJZiZvne4jQP+zhJwt+lmio4XXr4qG47CehvrW1Lx2YZiIHuxM2D4YpkG3KVA==} + '@oxlint/binding-linux-ppc64-gnu@1.58.0': + resolution: {integrity: sha512-NQ0U/lqxH2/VxBYeAIvMNUK1y0a1bJ3ZicqkF2c6wfakbEciP9jvIE4yNzCFpZaqeIeRYaV7AVGqEO1yrfVPjA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.57.0': - resolution: {integrity: sha512-uoBnjJ3MMEBbfnWC1jSFr7/nSCkcQYa72NYoNtLl1imshDnWSolYCjzb8LVCwYCCfLJXD+0gBLD7fyC14c0+0g==} + '@oxlint/binding-linux-riscv64-gnu@1.58.0': + resolution: {integrity: sha512-X9J+kr3gIC9FT8GuZt0ekzpNUtkBVzMVU4KiKDSlocyQuEgi3gBbXYN8UkQiV77FTusLDPsovjo95YedHr+3yg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.57.0': - resolution: {integrity: sha512-BdrwD7haPZ8a9KrZhKJRSj6jwCor+Z8tHFZ3PT89Y3Jq5v3LfMfEePeAmD0LOTWpiTmzSzdmyw9ijneapiVHKQ==} + '@oxlint/binding-linux-riscv64-musl@1.58.0': + resolution: {integrity: sha512-CDze3pi1OO3Wvb/QsXjmLEY4XPKGM6kIo82ssNOgmcl1IdndF9VSGAE38YLhADWmOac7fjqhBw82LozuUVxD0Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-s390x-gnu@1.57.0': - resolution: {integrity: sha512-BNs+7ZNsRstVg2tpNxAXfMX/Iv5oZh204dVyb8Z37+/gCh+yZqNTlg6YwCLIMPSk5wLWIGOaQjT0GUOahKYImw==} + '@oxlint/binding-linux-s390x-gnu@1.58.0': + resolution: {integrity: sha512-b/89glbxFaEAcA6Uf1FvCNecBJEgcUTsV1quzrqXM/o4R1M4u+2KCVuyGCayN2UpsRWtGGLb+Ver0tBBpxaPog==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.57.0': - resolution: {integrity: sha512-AghS18w+XcENcAX0+BQGLiqjpqpaxKJa4cWWP0OWNLacs27vHBxu7TYkv9LUSGe5w8lOJHeMxcYfZNOAPqw2bg==} + '@oxlint/binding-linux-x64-gnu@1.58.0': + resolution: {integrity: sha512-0/yYpkq9VJFCEcuRlrViGj8pJUFFvNS4EkEREaN7CB1EcLXJIaVSSa5eCihwBGXtOZxhnblWgxks9juRdNQI7w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.57.0': - resolution: {integrity: sha512-E/FV3GB8phu/Rpkhz5T96hAiJlGzn91qX5yj5gU754P5cmVGXY1Jw/VSjDSlZBCY3VHjsVLdzgdkJaomEmcNOg==} + '@oxlint/binding-linux-x64-musl@1.58.0': + resolution: {integrity: sha512-hr6FNvmcAXiH+JxSvaJ4SJ1HofkdqEElXICW9sm3/Rd5eC3t7kzvmLyRAB3NngKO2wzXRCAm4Z/mGWfrsS4X8w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.57.0': - resolution: {integrity: sha512-xvZ2yZt0nUVfU14iuGv3V25jpr9pov5N0Wr28RXnHFxHCRxNDMtYPHV61gGLhN9IlXM96gI4pyYpLSJC5ClLCQ==} + '@oxlint/binding-openharmony-arm64@1.58.0': + resolution: {integrity: sha512-R+O368VXgRql1K6Xar+FEo7NEwfo13EibPMoTv3sesYQedRXd6m30Dh/7lZMxnrQVFfeo4EOfYIP4FpcgWQNHg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.57.0': - resolution: {integrity: sha512-Z4D8Pd0AyHBKeazhdIXeUUy5sIS3Mo0veOlzlDECg6PhRRKgEsBJCCV1n+keUZtQ04OP+i7+itS3kOykUyNhDg==} + '@oxlint/binding-win32-arm64-msvc@1.58.0': + resolution: {integrity: sha512-Q0FZiAY/3c4YRj4z3h9K1PgaByrifrfbBoODSeX7gy97UtB7pySPUQfC2B/GbxWU6k7CzQrRy5gME10PltLAFQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.57.0': - resolution: {integrity: sha512-StOZ9nFMVKvevicbQfql6Pouu9pgbeQnu60Fvhz2S6yfMaii+wnueLnqQ5I1JPgNF0Syew4voBlAaHD13wH6tw==} + '@oxlint/binding-win32-ia32-msvc@1.58.0': + resolution: {integrity: sha512-Y8FKBABrSPp9H0QkRLHDHOSUgM/309a3IvOVgPcVxYcX70wxJrk608CuTg7w+C6vEd724X5wJoNkBcGYfH7nNQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.57.0': - resolution: {integrity: sha512-6PuxhYgth8TuW0+ABPOIkGdBYw+qYGxgIdXPHSVpiCDm+hqTTWCmC739St1Xni0DJBt8HnSHTG67i1y6gr8qrA==} + '@oxlint/binding-win32-x64-msvc@1.58.0': + resolution: {integrity: sha512-bCn5rbiz5My+Bj7M09sDcnqW0QJyINRVxdZ65x1/Y2tGrMwherwK/lpk+HRQCKvXa8pcaQdF5KY5j54VGZLwNg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -3044,8 +3018,8 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.58.2': - resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + '@playwright/test@1.59.1': + resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} engines: {node: '>=18'} hasBin: true @@ -3055,9 +3029,6 @@ packages: '@preact/signals-core@1.14.0': resolution: {integrity: sha512-AowtCcCU/33lFlh1zRFf/u+12rfrhtNakj7UpaGEsmMwUKpKWMVvcktOGcwBBNiB4lWrZWc01LhiyyzVklJyaQ==} - '@quansync/fs@1.0.0': - resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} - '@radix-ui/primitive@1.1.3': resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} @@ -3597,36 +3568,67 @@ packages: cpu: [x64] os: [win32] - '@sentry-internal/browser-utils@10.46.0': - resolution: {integrity: sha512-WB1gBT9G13V02ekZ6NpUhoI1aGHV2eNfjEPthkU2bGBvFpQKnstwzjg7waIRGR7cu+YSW2Q6UI6aQLgBeOPD1g==} + '@sentry-internal/browser-utils@10.47.0': + resolution: {integrity: sha512-bVFRAeJWMBcBCvJKIFCMJ1/yQToL4vPGqfmlnDZeypcxkqUDKQ/Y3ziLHXoDL2sx0lagcgU2vH1QhCQ67Aujjw==} engines: {node: '>=18'} - '@sentry-internal/feedback@10.46.0': - resolution: {integrity: sha512-c4pI/z9nZCQXe9GYEw/hE/YTY9AxGBp8/wgKI+T8zylrN35SGHaXv63szzE1WbI8lacBY8lBF7rstq9bQVCaHw==} + '@sentry-internal/feedback@10.47.0': + resolution: {integrity: sha512-pdvMmi4dQpX5S/vAAzrhHPIw3T3HjUgDNgUiCBrlp7N9/6zGO2gNPhUnNekP+CjgI/z0rvf49RLqlDenpNrMOg==} engines: {node: '>=18'} - '@sentry-internal/replay-canvas@10.46.0': - resolution: {integrity: sha512-ub314MWUsekVCuoH0/HJbbimlI24SkV745UW2pj9xRbxOAEf1wjkmIzxKrMDbTgJGuEunug02XZVdJFJUzOcDw==} + '@sentry-internal/replay-canvas@10.47.0': + resolution: {integrity: sha512-A5OY8friSe6g8WAK4L8IeOPiEd9D3Ps40DzRH5j2f6SUja0t90mKMvHRcRf8zq0d4BkdB+JM7tjOkwxpuv8heA==} engines: {node: '>=18'} - '@sentry-internal/replay@10.46.0': - resolution: {integrity: sha512-JBsWeXG6bRbxBFK8GzWymWGOB9QE7Kl57BeF3jzgdHTuHSWZ2mRnAmb1K05T4LU+gVygk6yW0KmdC8Py9Qzg9A==} + '@sentry-internal/replay@10.47.0': + resolution: {integrity: sha512-ScdovxP7hJxgMt70+7hFvwT02GIaIUAxdEM/YPsayZBeCoAukPW8WiwztJfoKtsfPyKJ5A6f0H3PIxTPcA9Row==} engines: {node: '>=18'} - '@sentry/browser@10.46.0': - resolution: {integrity: sha512-80DmGlTk5Z2/OxVOzLNxwolMyouuAYKqG8KUcoyintZqHbF6kO1RulI610HmyUt3OagKeBCqt9S7w0VIfCRL+Q==} + '@sentry/browser@10.47.0': + resolution: {integrity: sha512-rC0agZdxKA5XWfL4VwPOr/rJMogXDqZgnVzr93YWpFn9DMZT/7LzxSJVPIJwRUjx3bFEby3PcTa3YaX7pxm1AA==} engines: {node: '>=18'} - '@sentry/core@10.46.0': - resolution: {integrity: sha512-N3fj4zqBQOhXliS1Ne9euqIKuciHCGOJfPGQLwBoW9DNz03jF+NB8+dUKtrJ79YLoftjVgf8nbgwtADK7NR+2Q==} + '@sentry/core@10.47.0': + resolution: {integrity: sha512-nsYRAx3EWezDut+Zl+UwwP07thh9uY7CfSAi2whTdcJl5hu1nSp2z8bba7Vq/MGbNLnazkd3A+GITBEML924JA==} engines: {node: '>=18'} - '@sentry/react@10.46.0': - resolution: {integrity: sha512-Rb1S+9OuUPVwsz7GWnQ6Kgf3azbsseUymIegg3JZHNcW/fM1nPpaljzTBnuineia113DH0pgMBcdrrZDLaosFQ==} + '@sentry/react@10.47.0': + resolution: {integrity: sha512-ZtJV6xxF8jUVE9e3YQUG3Do0XapG1GjniyLyqMPgN6cNvs/HaRJODf7m60By+VGqcl5XArEjEPTvx8CdPUXDfA==} engines: {node: '>=18'} peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x + '@shikijs/core@4.0.2': + resolution: {integrity: sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==} + engines: {node: '>=20'} + + '@shikijs/engine-javascript@4.0.2': + resolution: {integrity: sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==} + engines: {node: '>=20'} + + '@shikijs/engine-oniguruma@4.0.2': + resolution: {integrity: sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==} + engines: {node: '>=20'} + + '@shikijs/langs@4.0.2': + resolution: {integrity: sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==} + engines: {node: '>=20'} + + '@shikijs/primitive@4.0.2': + resolution: {integrity: sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==} + engines: {node: '>=20'} + + '@shikijs/themes@4.0.2': + resolution: {integrity: sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==} + engines: {node: '>=20'} + + '@shikijs/types@4.0.2': + resolution: {integrity: sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==} + engines: {node: '>=20'} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@shuding/opentype.js@1.4.0-beta.0': resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==} engines: {node: '>= 8.0.0'} @@ -3672,42 +3674,42 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@storybook/addon-docs@10.3.3': - resolution: {integrity: sha512-trJQTpOtuOEuNv1Rn8X2Sopp5hSPpb0u0soEJ71BZAbxe4d2Y1d/1MYcxBdRKwncum6sCTsnxTpqQ/qvSJKlTQ==} + '@storybook/addon-docs@10.3.4': + resolution: {integrity: sha512-ohS8fX8UIP3LN6+mDZJLCDS4Qd2rsmGwes6V6fD0sbLOmIyCVY5y68r6NHMMGJKFRwadDQOmtOt8Vc6snExrIQ==} peerDependencies: - storybook: ^10.3.3 + storybook: ^10.3.4 - '@storybook/addon-links@10.3.3': - resolution: {integrity: sha512-tazBHlB+YbU62bde5DWsq0lnxZjcAsPB3YRUpN2hSMfAySsudRingyWrgu5KeOxXhJvKJj0ohjQvGcMx/wgQUA==} + '@storybook/addon-links@10.3.4': + resolution: {integrity: sha512-4Kcdv0U5WEyteN08Mv4oAUXTigF8OHMLA7Bpf1VEQrtJfQsxoUjXzItOHhCyBvphufkZzbU0n6wCC8upEb7X7w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.3 + storybook: ^10.3.4 peerDependenciesMeta: react: optional: true - '@storybook/addon-onboarding@10.3.3': - resolution: {integrity: sha512-HZiHfXdcLc29WkYFW+1VAMtJCeAZOOLRYPvs97woJUcZqW8yfWEJ9MWH+j++736SFAv2aqZWNmP47OdBJ/kMkw==} + '@storybook/addon-onboarding@10.3.4': + resolution: {integrity: sha512-59BgxVX3FJxS+cruvk7gfm/0azRJokDeHNNubEhgKIS8ih0ol9vJ7YbAJEP7QWOf22RghWtQSO0yMmTyZTPTyQ==} peerDependencies: - storybook: ^10.3.3 + storybook: ^10.3.4 - '@storybook/addon-themes@10.3.3': - resolution: {integrity: sha512-6PgH1o7yNnWRVj4lAT1DNcX/eZXKgzjhfmzgWh3oFpPfDDvUzpFxx+MClM5f/ZieIbyQscxEuq8li7+e/F5VEQ==} + '@storybook/addon-themes@10.3.4': + resolution: {integrity: sha512-5734o52qtW8svu2vhKPncISWLr1FZrXZoN+u1q0BjTrbL6qTNE1AzIMCBEwn0TNdn16vC3ZsDJOj1dW4dD13cw==} peerDependencies: - storybook: ^10.3.3 + storybook: ^10.3.4 - '@storybook/builder-vite@10.3.3': - resolution: {integrity: sha512-awspKCTZvXyeV3KabL0id62mFbxR5u/5yyGQultwCiSb2/yVgBfip2MAqLyS850pvTiB6QFVM9deOyd2/G/bEA==} + '@storybook/builder-vite@10.3.4': + resolution: {integrity: sha512-dNQyBZpBKvwmhSTpjrsuxxY8FqFCh0hgu5+46h2WbgQ2Te3pO458heWkGb+QO7mC6FmkXO6j6zgYzXticD6F2A==} peerDependencies: - storybook: ^10.3.3 + storybook: ^10.3.4 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@storybook/csf-plugin@10.3.3': - resolution: {integrity: sha512-Utlh7zubm+4iOzBBfzLW4F4vD99UBtl2Do4edlzK2F7krQIcFvR2ontjAE8S1FQVLZAC3WHalCOS+Ch8zf3knA==} + '@storybook/csf-plugin@10.3.4': + resolution: {integrity: sha512-WPP0Z39o82WiohPkhPOs6z+9yJ+bVvqPz4d+QUPfE6FMvOOBLojlwOcGx6Xmclyn5H/CKwywFrjuz4mBO/nHhA==} peerDependencies: esbuild: 0.27.2 rollup: 4.59.0 - storybook: ^10.3.3 + storybook: ^10.3.4 vite: '*' webpack: '*' peerDependenciesMeta: @@ -3729,40 +3731,40 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@storybook/nextjs-vite@10.3.3': - resolution: {integrity: sha512-/OzOo0dSd0eFIAF9ft+ptwaXHa5Xj01cw3NXEtmPdODZXl0eiPmTvWYIJeP26UEPzI2FFSm4fK64ZZJluKpGOA==} + '@storybook/nextjs-vite@10.3.4': + resolution: {integrity: sha512-TgfAgyAx2SvhLPMXFFHskQWMfOeGEY5kDXRC+SqDnuHapUiFignqvuq1Dh8I+Al1QNrFSa/q/TQiFPDHXPVm9g==} peerDependencies: next: ^14.1.0 || ^15.0.0 || ^16.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.3 + storybook: ^10.3.4 typescript: '*' vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: typescript: optional: true - '@storybook/react-dom-shim@10.3.3': - resolution: {integrity: sha512-lkhuh4G3UTreU9M3Iz5Dt32c6U+l/4XuvqLtbe1sDHENZH6aPj7y0b5FwnfHyvuTvYRhtbo29xZrF5Bp9kCC0w==} + '@storybook/react-dom-shim@10.3.4': + resolution: {integrity: sha512-VIm9YzreGubnOtQOZ6iqEfj6KncHvAkrCR/IilqnJq7DidPWuykrFszyajTASRMiY+p+TElOW+O1PGpv55qNGw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.3 + storybook: ^10.3.4 - '@storybook/react-vite@10.3.3': - resolution: {integrity: sha512-qHdlBe1hjqFAGXa8JL7bWTLbP/gDqXbWDm+SYCB646NHh5yvVDkZLwigP5Y+UL7M2ASfqFtosnroUK9tcCM2dw==} + '@storybook/react-vite@10.3.4': + resolution: {integrity: sha512-xaMt7NdvlAb+CwXn5TOiluQ+0WkkMN3mZhCThocpblWGoyfmHH7bgQ5ZwzT+IIp8DGOsAi/HkNmSyS7Z8HRLJg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.3 + storybook: ^10.3.4 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@storybook/react@10.3.3': - resolution: {integrity: sha512-cGG5TbR8Tdx9zwlpsWyBEfWrejm5iWdYF26EwIhwuKq9GFUTAVrQzo0Rs7Tqc3ZyVhRS/YfsRiWSEH+zmq2JiQ==} + '@storybook/react@10.3.4': + resolution: {integrity: sha512-I5ifYqjrqyuhOFjalpy47kMKMXX7QU/qmHj0h/547s9Bg6sEU7xRhJnneXx1RJsEJTySjC4SmGfEU+FJz4Foiw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.3 + storybook: ^10.3.4 typescript: '>= 4.9.x' peerDependenciesMeta: typescript: @@ -3965,15 +3967,15 @@ packages: vue: optional: true - '@tanstack/devtools@0.11.0': - resolution: {integrity: sha512-ARRAnEm0HYjKlB2adC9YyDG3fbq5LVjpxPe6Jz583SanXRM1aKrZIGHIA//oRldX3mWIpM4kB6mCyd+CXCLqhA==} + '@tanstack/devtools@0.11.1': + resolution: {integrity: sha512-g3nHgVP76kT9190d6O32AjANoEnujLEB+51PDtBzlah8hvKeEygK53cunN+HXhjlfhM4PoOCi8/B96cdJVSnLg==} engines: {node: '>=18'} hasBin: true peerDependencies: solid-js: 1.9.11 - '@tanstack/eslint-plugin-query@5.95.2': - resolution: {integrity: sha512-EYUFRaqjBep4EHMPpZR12sXP7Kr5qv9iDIlq93NfbhHwhITaW6Txu3ROO6dLFz5r84T8p+oZXBG77pa2Wuok7A==} + '@tanstack/eslint-plugin-query@5.96.1': + resolution: {integrity: sha512-BDJU+Q+zESjarSSFmbzpCBh+1wDxwW+DyQlvwIukF24MHYOoRPH4ouJRTlDdbp3BnIkeylZaHHSgIvxY9lgI/g==} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ^5.4.0 @@ -3981,11 +3983,11 @@ packages: typescript: optional: true - '@tanstack/form-core@1.28.5': - resolution: {integrity: sha512-8lYnduHHfP6uaXF9+2OLnh3Fo27tH4TdtekWLG2b/Bp26ynbrWG6L4qhBgEb7VcvTpJw/RjvJF/JyFhZkG3pfQ==} + '@tanstack/form-core@1.28.6': + resolution: {integrity: sha512-4zroxL6VDj5O+w7l3dYZnUeL/h30KtNSV7UWzKAL7cl+8clMFdISPDlDlluS37As7oqvPVKo8B83VlIBvgmRog==} - '@tanstack/form-devtools@0.2.19': - resolution: {integrity: sha512-AmIq5MBcop+gYKFutowGU7py9idorJkp4a4lsR2ZIZ5qa4ekl4jWqj6Vu+kvRPpJiBl3QpiFbm9bjBvO2DueFA==} + '@tanstack/form-devtools@0.2.20': + resolution: {integrity: sha512-4cW/eU5DBTrWP53mxwHKp4NQWTIQ3XCA91pMWK7dFNNClIwFnxoSJoKwyUa6b8kRIO6uq1Sjk2mhkAtj5kB22A==} peerDependencies: solid-js: 1.9.11 @@ -3993,14 +3995,14 @@ packages: resolution: {integrity: sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==} engines: {node: '>=18'} - '@tanstack/query-core@5.95.2': - resolution: {integrity: sha512-o4T8vZHZET4Bib3jZ/tCW9/7080urD4c+0/AUaYVpIqOsr7y0reBc1oX3ttNaSW5mYyvZHctiQ/UOP2PfdmFEQ==} + '@tanstack/query-core@5.96.1': + resolution: {integrity: sha512-u1yBgtavSy+N8wgtW3PiER6UpxcplMje65yXnnVgiHTqiMwLlxiw4WvQDrXyn+UD6lnn8kHaxmerJUzQcV/MMg==} - '@tanstack/query-devtools@5.95.2': - resolution: {integrity: sha512-QfaoqBn9uAZ+ICkA8brd1EHj+qBF6glCFgt94U8XP5BT6ppSsDBI8IJ00BU+cAGjQzp6wcKJL2EmRYvxy0TWIg==} + '@tanstack/query-devtools@5.96.1': + resolution: {integrity: sha512-A4+uQTWbiqZDgrLeyjpFYLfMaWaKWpkwTkR1cUfocVj6vPYgym7QTG2se9A01WSxceDdmgxOqvn1ivcTvgWD8w==} - '@tanstack/react-devtools@0.10.0': - resolution: {integrity: sha512-cUMzOQb1IHmkb8MsD0TrxHT8EL92Rx3G0Huq+IFkWeoaZPGlIiaIcGTpS5VvQDeI4BVUT+ZGt6CQTpx8oSTECg==} + '@tanstack/react-devtools@0.10.1': + resolution: {integrity: sha512-cvcd0EqN7Q2LYatQXxFhOkEa9RUQXZlhXnM1mwuibxmyRX+CMyohUZcgjodtIfgh+RT0Pmvt49liTdZby5ovZw==} engines: {node: '>=18'} peerDependencies: '@types/react': '>=16.8' @@ -4008,13 +4010,13 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-form-devtools@0.2.19': - resolution: {integrity: sha512-bILhij/Ye4T1YtyvNctmIShBL0gBp1jnWq0/9KASDFxjXjDUTmFE4TwAzYnwXbARjf6x8ZUW5MuJbi7VjpIGFw==} + '@tanstack/react-form-devtools@0.2.20': + resolution: {integrity: sha512-aXtorJ7p3TbzOapjaxbjGX/c0uQh/wbYSwgzFt3qatNMb1xL4HM/j00Bx7hDENZNBCf8MF8YEEtvpBmnGb4rnQ==} peerDependencies: react: ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/react-form@1.28.5': - resolution: {integrity: sha512-CL8IeWkeXnEEDsHt5wBuIOZvSYrKiLRtsC9ca0LzfJJ22SYCma9cBmh1UX1EBX0o3gH2U21PmUf+y5f9OJNoEQ==} + '@tanstack/react-form@1.28.6': + resolution: {integrity: sha512-dRxwKeNW3uuJvf0sXsIQ2compFMnIJNk9B436Lx0fqkqK+CBvA1tNmEdX+faoCpuQ5Wua3c8ahVibJ65cpkijA==} peerDependencies: '@tanstack/react-start': '*' react: ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -4022,14 +4024,14 @@ packages: '@tanstack/react-start': optional: true - '@tanstack/react-query-devtools@5.95.2': - resolution: {integrity: sha512-AFQFmbznVkbtfpx8VJ2DylW17wWagQel/qLstVLkYmNRo2CmJt3SNej5hvl6EnEeljJIdC3BTB+W7HZtpsH+3g==} + '@tanstack/react-query-devtools@5.96.1': + resolution: {integrity: sha512-3ZZ58fupIXtJFM0evj8YvWrauaZPUrQEqRYaq9e4ER/WPqTKeWEucqWCXn+KJLgWlcot5JIIUtQNynbovGjTTA==} peerDependencies: - '@tanstack/react-query': ^5.95.2 + '@tanstack/react-query': ^5.96.1 react: ^18 || ^19 - '@tanstack/react-query@5.95.2': - resolution: {integrity: sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA==} + '@tanstack/react-query@5.96.1': + resolution: {integrity: sha512-2X7KYK5KKWUKGeWCVcqxXAkYefJtrKB7tSKWgeG++b0H6BRHxQaLSSi8AxcgjmUnnosHuh9WsFZqvE16P1WCzA==} peerDependencies: react: ^18 || ^19 @@ -4258,9 +4260,6 @@ packages: '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} - '@types/hast@2.3.10': - resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} - '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -4305,12 +4304,6 @@ packages: peerDependencies: '@types/react': ^19.2.0 - '@types/react-syntax-highlighter@15.5.13': - resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==} - - '@types/react-window@1.8.8': - resolution: {integrity: sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==} - '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} @@ -4341,13 +4334,13 @@ packages: '@types/zen-observable@0.8.3': resolution: {integrity: sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw==} - '@typescript-eslint/eslint-plugin@8.57.2': - resolution: {integrity: sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==} + '@typescript-eslint/eslint-plugin@8.58.0': + resolution: {integrity: sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.57.2 + '@typescript-eslint/parser': ^8.58.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' '@typescript-eslint/parser@8.57.2': resolution: {integrity: sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==} @@ -4356,12 +4349,25 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/parser@8.58.0': + resolution: {integrity: sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/project-service@8.57.2': resolution: {integrity: sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/project-service@8.58.0': + resolution: {integrity: sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/rule-tester@8.57.2': resolution: {integrity: sha512-cb5m0irr1449waTuYzGi4KD3SGUH3khL4ta/o9lzShvT7gnIwR5qVhU0VM0p966kCrtFId8hwmkvz1fOElsxTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4372,29 +4378,49 @@ packages: resolution: {integrity: sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.58.0': + resolution: {integrity: sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.57.2': resolution: {integrity: sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.57.2': - resolution: {integrity: sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==} + '@typescript-eslint/tsconfig-utils@8.58.0': + resolution: {integrity: sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.58.0': + resolution: {integrity: sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' '@typescript-eslint/types@8.57.2': resolution: {integrity: sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.58.0': + resolution: {integrity: sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.57.2': resolution: {integrity: sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/typescript-estree@8.58.0': + resolution: {integrity: sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/utils@8.57.2': resolution: {integrity: sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4402,47 +4428,58 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/utils@8.58.0': + resolution: {integrity: sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/visitor-keys@8.57.2': resolution: {integrity: sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260329.1': - resolution: {integrity: sha512-zS1thDk7luD82nXVwvMd97F7FgxAE6jGtSmnHeXdaQ+6hJQcQLOVkfUdaehhdodqKDapWA2jEURxQAYjDGvv3g==} + '@typescript-eslint/visitor-keys@8.58.0': + resolution: {integrity: sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260401.1': + resolution: {integrity: sha512-9PCc1D4/zLic30g1upOw6ZmUl98fnrXYRv5wIZ6fxm1zZAObieRKUX3Jbr8M9N8iQQFxPIZPniIScsxAbmbJvw==} cpu: [arm64] os: [darwin] - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260329.1': - resolution: {integrity: sha512-3IJ2qmpjQ1OXpZNUhJRjF1+SbDuqGC14Ug8DjWJlPBp06isi1fcJph90f5qW//FxEsNnJPYRcNwpP0A2RbTASg==} + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260401.1': + resolution: {integrity: sha512-wwzca1KrjSVC6ApXfITsg/wF4GGbhVYebc7zChpuyi+phrHfw6ThTPB5XFUH4nA32vqw0Hn/6KACipMgzg8GPA==} cpu: [x64] os: [darwin] - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260329.1': - resolution: {integrity: sha512-gQb6SjB5JlUKDaDuz6mv/m+/OBWVDlcjHINFOykBZZYZtgxBx6nEDjLrT8TiJRjmHEG6hSbv+yisUL9IThWycA==} + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260401.1': + resolution: {integrity: sha512-1hgKibGi4QZF1J0hKltgY4nj4yKDmI4Ang5ar80I+YeUdGxV/fP2kU3bJang7QtHuSso6W+a52SF62zgqbzdow==} cpu: [arm64] os: [linux] - '@typescript/native-preview-linux-arm@7.0.0-dev.20260329.1': - resolution: {integrity: sha512-WKSSJrH611DFFAg6YCkgbnkdy0a4RRpzvDpNXtPzLTbMYC5oJdq3Dpvncx5nrJvGh4J4yvzXoMxraGPyygqGLw==} + '@typescript/native-preview-linux-arm@7.0.0-dev.20260401.1': + resolution: {integrity: sha512-bbIkRZYjtyoyCJ3wFES7qn3EwYO5Go1hxArL5X5oWiBmUHq5gMIxTZDv5mpWxopVf9Eyh4ErHefXjf1s4J+6Ag==} cpu: [arm] os: [linux] - '@typescript/native-preview-linux-x64@7.0.0-dev.20260329.1': - resolution: {integrity: sha512-kg4r+ssxoEWruBynUg9bFMdcMpo5NupzAPqNBlV8uWbmYGZjaPLonFWAi9ZZMiVJY/x5ZQ9GBl6xskwLdd3PJQ==} + '@typescript/native-preview-linux-x64@7.0.0-dev.20260401.1': + resolution: {integrity: sha512-1ysZ4c/Wa3RYIlrwVceYlhb1m1hxQ4P2x92valZXH0bNWEPb+oiQ4Yf35O/vi5h8zDdX/ZQ59vivYl27cF1VVA==} cpu: [x64] os: [linux] - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260329.1': - resolution: {integrity: sha512-Qi4lddVxl5MG7Tk67gYhCFnoqqLGd4TvaI8RN4qHFjt3GV8s6c+0cQGsJXJnVgMx27qbyDTdsyAa2pvb42rYcQ==} + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260401.1': + resolution: {integrity: sha512-fZYLCRe36y1BuzRFFpU2/RQ212l6Y1dccRMh8oTB8HlAVAAwtbkb6cjEn0Ablj4Dy16+Ih8R9uHsxPLNhtKw1Q==} cpu: [arm64] os: [win32] - '@typescript/native-preview-win32-x64@7.0.0-dev.20260329.1': - resolution: {integrity: sha512-+k5+usuB8HZ6Xc+enLdb95ZJd25bQqsnI1zXxfRCHP+RS9mxs70Mi9ezQz3lKOLZFFXShSH7iW9iulm8KwVzCQ==} + '@typescript/native-preview-win32-x64@7.0.0-dev.20260401.1': + resolution: {integrity: sha512-I6ses4SjWvpbvSpm1BPFRrDeqrzu7JTchJG/a26iwwmTLv4fAGLc5/o6Kv9Naygozop1W3KOcVM5i3A9oxiIjQ==} cpu: [x64] os: [win32] - '@typescript/native-preview@7.0.0-dev.20260329.1': - resolution: {integrity: sha512-v5lJ0TgSt2m9yVk2xoj9+NH/gTDeWTLaWGPx6MJsUKOYd6bmCJhHbMcWmb8d/zlfhE9ffpixUKYj62CdYfriqA==} + '@typescript/native-preview@7.0.0-dev.20260401.1': + resolution: {integrity: sha512-xJcN9WlY/P6xKjCMH4+DTzZSj/EKR6H9avuqUKs4eKyPEvaQ4bX+9Ys3Vl2qhlUaD7IRWY7HN7db0LHAGlWRSA==} hasBin: true '@ungap/structured-clone@1.3.0': @@ -4510,17 +4547,17 @@ packages: react-server-dom-webpack: optional: true - '@vitest/coverage-v8@4.1.1': - resolution: {integrity: sha512-nZ4RWwGCoGOQRMmU/Q9wlUY540RVRxJZ9lxFsFfy0QV7Zmo5VVBhB6Sl9Xa0KIp2iIs3zWfPlo9LcY1iqbpzCw==} + '@vitest/coverage-v8@4.1.2': + resolution: {integrity: sha512-sPK//PHO+kAkScb8XITeB1bf7fsk85Km7+rt4eeuRR3VS1/crD47cmV5wicisJmjNdfeokTZwjMk4Mj2d58Mgg==} peerDependencies: - '@vitest/browser': 4.1.1 - vitest: 4.1.1 + '@vitest/browser': 4.1.2 + vitest: 4.1.2 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/eslint-plugin@1.6.13': - resolution: {integrity: sha512-ui7JGWBoQpS5NKKW0FDb1eTuFEZ5EupEv2Psemuyfba7DfA5K52SeDLelt6P4pQJJ/4UGkker/BgMk/KrjH3WQ==} + '@vitest/eslint-plugin@1.6.14': + resolution: {integrity: sha512-PXZ5ysw4eHU9h8nDtBvVcGC7Z2C/T9CFdheqSw1NNXFYqViojub0V9bgdYI67iBTOcra2mwD0EYldlY9bGPf2Q==} engines: {node: '>=18'} peerDependencies: '@typescript-eslint/eslint-plugin': '*' @@ -4541,8 +4578,8 @@ packages: '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - '@vitest/pretty-format@4.1.1': - resolution: {integrity: sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ==} + '@vitest/pretty-format@4.1.2': + resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==} '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} @@ -4550,16 +4587,16 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@vitest/utils@4.1.1': - resolution: {integrity: sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==} + '@vitest/utils@4.1.2': + resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} - '@voidzero-dev/vite-plus-core@0.1.14': - resolution: {integrity: sha512-CCWzdkfW0fo0cQNlIsYp5fOuH2IwKuPZEb2UY2Z8gXcp5pG74A82H2Pthj0heAuvYTAnfT7kEC6zM+RbiBgQbg==} + '@voidzero-dev/vite-plus-core@0.1.15': + resolution: {integrity: sha512-0qAbqwcvQwiC8xGKSSuFtsjJUEM4LZzpXF7dffRazghGEQ8HH8NAvVryp/PiMSFwreJlV3rujwL4amKjnwCHpg==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@arethetypeswrong/core': ^0.18.1 - '@tsdown/css': 0.21.4 - '@tsdown/exe': 0.21.4 + '@tsdown/css': 0.21.7 + '@tsdown/exe': 0.21.7 '@types/node': ^20.19.0 || >=22.12.0 '@vitejs/devtools': ^0.1.0 esbuild: 0.27.2 @@ -4572,7 +4609,7 @@ packages: sugarss: ^5.0.0 terser: ^5.16.0 tsx: ^4.8.1 - typescript: ^5.0.0 + typescript: ^5.0.0 || ^6.0.0 unplugin-unused: ^0.5.0 yaml: 2.8.3 peerDependenciesMeta: @@ -4613,54 +4650,54 @@ packages: yaml: optional: true - '@voidzero-dev/vite-plus-darwin-arm64@0.1.14': - resolution: {integrity: sha512-q2ESUSbapwsxVRe/KevKATahNRraoX5nti3HT9S3266OHT5sMroBY14jaxTv74ekjQc9E6EPhyLGQWuWQuuBRw==} + '@voidzero-dev/vite-plus-darwin-arm64@0.1.15': + resolution: {integrity: sha512-arFq8phXg96rQ5J+FYvkBYdEGxIhP1ePAXlUeQY2hV8hJPzse+CdxusWxcjfpTgvFi+dpsKzE4KSNS22PyBo7w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@voidzero-dev/vite-plus-darwin-x64@0.1.14': - resolution: {integrity: sha512-UpcDZc9G99E/4HDRoobvYHxMvFOG5uv3RwEcq0HF70u4DsnEMl1z8RaJLeWV7a09LGwj9Q+YWC3Z4INWnTLs8g==} + '@voidzero-dev/vite-plus-darwin-x64@0.1.15': + resolution: {integrity: sha512-2eY+gTEIZvLH33nQmcL2tKlf+iHfClaqaSMYIlUpTp/CN+xqh4Ir4y2vN1XGEuFDIW0FshSZTg3ulPtduneEDA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.14': - resolution: {integrity: sha512-GIjn35RABUEDB9gHD26nRq7T72Te+Qy2+NIzogwEaUE728PvPkatF5gMCeF4sigCoc8c4qxDwsG+A2A2LYGnDg==} + '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.15': + resolution: {integrity: sha512-jJgz84pp61oHeXAYIUXKsVwQsMQ7NHK0+dBe6v1Q+Z034xXsyBrxi/JASSeVmCpAd6CN+xzOCsfMyn3whVTTxQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.14': - resolution: {integrity: sha512-qo2RToGirG0XCcxZ2AEOuonLM256z6dNbJzDDIo5gWYA+cIKigFQJbkPyr25zsT1tsP2aY0OTxt2038XbVlRkQ==} + '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.15': + resolution: {integrity: sha512-F0Wig+We0ERhGecf3fDIwM/kfqT0vP2htH0vKUnV/inHIVbPc1MsrjcExX1eJ6KFSp5YTfchRN8HGecqtsudPA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.14': - resolution: {integrity: sha512-BsMWKZfdfGcYLxxLyaePpg6NW54xqzzcfq8sFUwKfwby0kgOKQ4WymUXyBvO9nnBb0ZPsJQrV0sx+Onac/LTaw==} + '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.15': + resolution: {integrity: sha512-aT5Yr2GphvRjoc2URmELDqjWwhe5VPvyy15Tzum+jPhEjY4I/lPXxKXEROjQe3TIv6MmFSHCe3oNCSaFdUE1pA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@voidzero-dev/vite-plus-linux-x64-musl@0.1.14': - resolution: {integrity: sha512-mOrEpj7ntW9RopGbcOYG/L0pOs0qHzUG4Vz7NXbuf4dbOSlY4JjyoMOIWxjKQORQht02Hzuf8YrMGNwa6AjVSQ==} + '@voidzero-dev/vite-plus-linux-x64-musl@0.1.15': + resolution: {integrity: sha512-Q6qMBMdVp5v84YVzFvMUpzVIHLfJuwZQR/KUtAOn/hzpfNITigKR2GrZZDgQvszFW+0CPhDFcK3kqLkxlJCdFg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@voidzero-dev/vite-plus-test@0.1.14': - resolution: {integrity: sha512-rjF+qpYD+5+THOJZ3gbE3+cxsk5sW7nJ0ODK7y6ZKeS4amREUMedEDYykzKBwR7OZDC/WwE90A0iLWCr6qAXhA==} + '@voidzero-dev/vite-plus-test@0.1.15': + resolution: {integrity: sha512-jxMUEX6PDpzMUz+KOVOoB8HiODMf5mWjH19pof0k9l/RZT4iLDyVXB+p9PoWjbVrEMMGzq9BTOVob7wfOZeZEA==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/ui': 4.1.1 + '@vitest/ui': 4.1.2 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -4678,14 +4715,14 @@ packages: jsdom: optional: true - '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.14': - resolution: {integrity: sha512-7iC+Ig+8D/zACy0IJf7w/vQ7duTjux9Ttmm3KOBdVWH4dl3JihydA7+SQVMhz71a4WiqJ6nPidoG8D6hUP4MVQ==} + '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.15': + resolution: {integrity: sha512-EePrs+NIUy3gE60qaXPXzj8mw+JAXEBfGKsfweYBgNK6jo9ZXZto5ViKTuQsVVuWLVaELZSjoudbkzXB8wnJoQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.14': - resolution: {integrity: sha512-yRJ/8yAYFluNHx0Ej6Kevx65MIeM3wFKklnxosVZRlz2ZRL1Ea1Qh3tWATr3Ipk1ciRxBv8KJgp6zXqjxtZSoQ==} + '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.15': + resolution: {integrity: sha512-vfYfwOG/5a/WUtgGrbUCatRkc5x0Rq/9GDlCzQQIAFGDB5BfyIjGbdCOqamQWOh+yQbeOHwvgAhqjZ7Dv1oo/w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -4762,6 +4799,9 @@ packages: '@webassemblyjs/wast-printer@1.14.1': resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + '@webcontainer/env@1.1.1': + resolution: {integrity: sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng==} + '@xstate/fsm@1.6.5': resolution: {integrity: sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==} @@ -4934,8 +4974,8 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@1.1.13: + resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} brace-expansion@5.0.5: resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} @@ -4971,20 +5011,10 @@ packages: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} - bundle-require@5.1.0: - resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - peerDependencies: - esbuild: 0.27.2 - bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - cac@7.0.0: resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} engines: {node: '>=20.19.0'} @@ -5027,21 +5057,12 @@ packages: character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} - character-entities-legacy@1.1.4: - resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} - character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} - character-entities@1.2.4: - resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} - character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} - character-reference-invalid@1.1.4: - resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} - character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} @@ -5128,8 +5149,8 @@ packages: react: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc - code-inspector-plugin@1.4.5: - resolution: {integrity: sha512-yp3zHd5AZhtVoBNOzKQuJVo1wZe7AIO2vAiVhF8WIAK02IwM9+gY+Pr9deajx+XyJLbzMW+3CgdfLIh+xxW2Hg==} + code-inspector-plugin@1.5.1: + resolution: {integrity: sha512-7gOqqBurKCucnls1ZHw0KWb7Z5u7gg3Q2pFSY9rrttFmwRaFJfJiscKEbm7X9IKmeEvkFRtNvNrHbSVQ67L8pQ==} collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} @@ -5141,9 +5162,6 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - comma-separated-tokens@1.0.8: - resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} - comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -5162,10 +5180,6 @@ packages: commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} @@ -5185,16 +5199,15 @@ packages: compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} confbox@0.2.4: resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} - consola@3.4.2: - resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} - engines: {node: ^14.18.0 || >=16.10.0} - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -5469,9 +5482,6 @@ packages: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} - defu@6.1.4: - resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} - delaunator@5.1.0: resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} @@ -5479,9 +5489,6 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} - destr@2.0.5: - resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -5656,8 +5663,8 @@ packages: peerDependencies: eslint: ^9.5.0 || ^10.0.0 - eslint-flat-config-utils@3.0.2: - resolution: {integrity: sha512-mPvevWSDQFwgABvyCurwIu6ZdKxGI5NW22/BGDwA1T49NO6bXuxbV9VfJK/tkQoNyPogT6Yu1d57iM0jnZVWmg==} + eslint-flat-config-utils@3.1.0: + resolution: {integrity: sha512-lM+Nwo2CzpuTS/RASQExlEIwk/BQoKqJWX6VbDlLMb/mveqvt9MMrRXFEkG3bseuK6g8noKZLeX82epkILtv4A==} eslint-json-compat-utils@0.2.3: resolution: {integrity: sha512-RbBmDFyu7FqnjE8F0ZxPNzx5UaptdeS9Uu50r7A+D7s/+FCX+ybiyViYEgFUaFIFqSWJgZRTpL5d8Kanxxl2lQ==} @@ -5726,11 +5733,11 @@ packages: peerDependencies: eslint: '*' - eslint-plugin-import-lite@0.5.2: - resolution: {integrity: sha512-XvfdWOC5dSLEI9krIPRlNmKSI2ViIE9pVylzfV9fCq0ZpDaNeUk6o0wZv0OzN83QdadgXp1NsY0qjLINxwYCsw==} + eslint-plugin-import-lite@0.6.0: + resolution: {integrity: sha512-80vevx2A7i3H7n1/6pqDO8cc5wRz6OwLDvIyVl9UflBV1N1f46e9Ihzi65IOLYoSxM6YykK2fTw1xm0Ixx6aTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: '>=9.0.0' + eslint: ^9.0.0 || ^10.0.0 eslint-plugin-jsdoc@62.8.1: resolution: {integrity: sha512-e9358PdHgvcMF98foNd3L7hVCw70Lt+YcSL7JzlJebB8eT5oRJtW6bHMQKoAwJtw6q0q0w/fRIr2kwnHdFDI6A==} @@ -5744,11 +5751,11 @@ packages: peerDependencies: eslint: '>=9.38.0' - eslint-plugin-markdown-preferences@0.40.3: - resolution: {integrity: sha512-R3CCAEFwnnYXukTdtvdsamGjbTgVs9UZKqMKhNeWNXzFtOP1Frc89bgbd56lJUN7ASaxgvzc5fUpKvDCOTtDpg==} + eslint-plugin-markdown-preferences@0.41.0: + resolution: {integrity: sha512-Pu150jKH1Cf5sW/Igck0VbuT0A9qFpIPG1dDvyAt2lG8tA3VzPDkwxBusO8JqQ9NRIrm3pat0X6cfanSki3WZQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@eslint/markdown': ^7.4.0 + '@eslint/markdown': ^7.4.0 || ^8.0.0 eslint: '>=9.0.0' eslint-plugin-n@17.24.0: @@ -5782,12 +5789,6 @@ packages: eslint: ^10.0.0 typescript: '*' - eslint-plugin-react-hooks@7.0.1: - resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} - engines: {node: '>=18'} - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - eslint-plugin-react-naming-convention@3.0.0: resolution: {integrity: sha512-pAtOZST5/NhWIa/I5yz7H1HEZTtCY7LHMhzmN9zvaOdTWyZYtz2g9pxPRDBnkR9uSmHsNt44gj+2JSAD4xwgew==} engines: {node: '>=22.0.0'} @@ -5832,11 +5833,11 @@ packages: peerDependencies: eslint: ^8.0.0 || ^9.0.0 || ^10.0.0 - eslint-plugin-storybook@10.3.3: - resolution: {integrity: sha512-jo8wZvKaJlxxrNvf4hCsROJP3CdlpaLiYewAs5Ww+PJxCrLelIi5XVHWOAgBvvr3H9WDKvUw8xuvqPYqAlpkFg==} + eslint-plugin-storybook@10.3.4: + resolution: {integrity: sha512-6jRb9ucYWKRkbuxpN+83YA3wAWuKn6rp+OVXivy0FPa82v8eciHG8OidbznmzrfcRJYkNWUb7GrPjG/rf4Vqaw==} peerDependencies: eslint: '>=8' - storybook: ^10.3.3 + storybook: ^10.3.4 eslint-plugin-toml@1.3.1: resolution: {integrity: sha512-1l00fBP03HIt9IPV7ZxBi7x0y0NMdEZmakL1jBD6N/FoKBvfKxPw5S8XkmzBecOnFBTn5Z8sNJtL5vdf9cpRMQ==} @@ -5844,8 +5845,8 @@ packages: peerDependencies: eslint: '>=9.38.0' - eslint-plugin-unicorn@63.0.0: - resolution: {integrity: sha512-Iqecl9118uQEXYh7adylgEmGfkn5es3/mlQTLLkd4pXkIk9CTGrAbeUux+YljSa2ohXCBmQQ0+Ej1kZaFgcfkA==} + eslint-plugin-unicorn@64.0.0: + resolution: {integrity: sha512-rNZwalHh8i0UfPlhNwg5BTUO1CMdKNmjqe+TgzOTZnpKoi8VBgsW7u9qCHIdpxEzZ1uwrJrPF0uRb7l//K38gA==} engines: {node: ^20.10.0 || >=21.0.0} peerDependencies: eslint: '>=9.38.0' @@ -6025,15 +6026,21 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-string-truncated-width@1.2.1: + resolution: {integrity: sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow==} + + fast-string-width@1.1.0: + resolution: {integrity: sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-wrap-ansi@0.1.6: + resolution: {integrity: sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} - fault@1.0.4: - resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==} - fault@2.0.1: resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} @@ -6079,9 +6086,6 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} - fix-dts-default-cjs-exports@1.0.1: - resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} - flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -6125,9 +6129,6 @@ packages: functional-red-black-tree@1.0.1: resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} - fzf@0.5.2: - resolution: {integrity: sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==} - gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -6180,10 +6181,6 @@ packages: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} - globals@16.5.0: - resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} - engines: {node: '>=18'} - globals@17.4.0: resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==} engines: {node: '>=18'} @@ -6229,9 +6226,6 @@ packages: hast-util-is-element@3.0.0: resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} - hast-util-parse-selector@2.2.5: - resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==} - hast-util-parse-selector@4.0.0: resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} @@ -6244,6 +6238,9 @@ packages: hast-util-to-estree@3.1.3: resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + hast-util-to-jsx-runtime@2.3.6: resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} @@ -6256,30 +6253,15 @@ packages: hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} - hastscript@6.0.0: - resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==} - hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} - hermes-estree@0.25.1: - resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} - - hermes-parser@0.25.1: - resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} - hex-rgb@4.3.0: resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} engines: {node: '>=6'} - highlight.js@10.7.3: - resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} - - highlightjs-vue@1.0.0: - resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==} - - hono@4.12.9: - resolution: {integrity: sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==} + hono@4.12.10: + resolution: {integrity: sha512-mx/p18PLy5og9ufies2GOSUqep98Td9q4i/EF6X7yJgAiIopxqdfIO3jbqsi3jRgTgw88jMDEzVKi+V2EF+27w==} engines: {node: '>=16.9.0'} hosted-git-info@9.0.2: @@ -6310,8 +6292,8 @@ packages: i18next-resources-to-backend@1.2.1: resolution: {integrity: sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw==} - i18next@25.10.10: - resolution: {integrity: sha512-cqUW2Z3EkRx7NqSyywjkgCLK7KLCL6IFVFcONG7nVYIJ3ekZ1/N5jUsihHV6Bq37NfhgtczxJcxduELtjTwkuQ==} + i18next@26.0.3: + resolution: {integrity: sha512-1571kXINxHKY7LksWp8wP+zP0YqHSSpl/OW0Y0owFEf2H3s8gCAffWaZivcz14rMkOvn3R/psiQxVsR9t2Nafg==} peerDependencies: typescript: ^5 || ^6 peerDependenciesMeta: @@ -6397,15 +6379,9 @@ packages: resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==} deprecated: The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019. - is-alphabetical@1.0.4: - resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} - is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} - is-alphanumerical@1.0.4: - resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} - is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} @@ -6413,9 +6389,6 @@ packages: resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==} engines: {node: '>=18.20'} - is-decimal@1.0.4: - resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} - is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} @@ -6432,9 +6405,6 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - is-hexadecimal@1.0.4: - resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} - is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} @@ -6518,10 +6488,6 @@ packages: react: optional: true - joycon@3.1.1: - resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} - engines: {node: '>=10'} - js-audio-recorder@1.0.7: resolution: {integrity: sha512-JiDODCElVHGrFyjGYwYyNi7zCbKk9va9C77w+zCPMmi4C6ix7zsX2h3ddHugmo4dOTOTCym9++b/wVW9nC0IaA==} @@ -6578,6 +6544,9 @@ packages: resolution: {integrity: sha512-75EA7EWZExL/j+MDKQrRbdzcRI2HOkRlmUw8fZJc1ioqFEOvBsq7Rt+A6yCxOt9w/TYNpkt52gC6nm/g5tFIng==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} @@ -6598,8 +6567,8 @@ packages: khroma@2.1.0: resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} - knip@6.1.0: - resolution: {integrity: sha512-n5eVbJP7HXmwTsiJcELWJe2O1ESxyCTNxJzRTIECDYDTM465qnqk7fL2dv6ae3NUFvFWorZvGlh9mcwxwJ5Xgw==} + knip@6.2.0: + resolution: {integrity: sha512-4OMUMJARvNble8e8TeFv12flp4fKzAITrQec1eKO4g2eA4HnNqEa8CXy2UOPLjuYuAETpe0N0r25jF9yY9FLig==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -6721,20 +6690,9 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} - lilconfig@3.1.3: - resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} - engines: {node: '>=14'} - linebreak@1.1.0: resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - load-tsconfig@0.2.5: - resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - loader-runner@4.3.1: resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} engines: {node: '>=6.11.5'} @@ -6747,8 +6705,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash-es@4.17.23: - resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + lodash-es@4.18.0: + resolution: {integrity: sha512-koAgswPPA+UTaPN64Etp+PGP+WT6oqOS2NMi5yDkMaiGw9qY4VxQbQF0mtKMyr4BlTznWyzePV5UpECTJQmSUA==} + deprecated: Bad release. Please use lodash-es@4.17.23 instead. lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -6759,8 +6718,9 @@ packages: lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} - lodash@4.17.23: - resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + lodash@4.18.0: + resolution: {integrity: sha512-l1mfj2atMqndAHI3ls7XqPxEjV2J9ZkcNyHpoZA3r2T1LLwDB69jgkMWh71YKwhBbK0G2f4WSn05ahmQXVxupA==} + deprecated: Bad release. Please use lodash@4.17.21 instead. longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -6775,9 +6735,6 @@ packages: lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} - lowlight@1.20.0: - resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} - lru-cache@11.2.7: resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} engines: {node: 20 || >=22} @@ -6894,9 +6851,6 @@ packages: mdn-data@2.23.0: resolution: {integrity: sha512-786vq1+4079JSeu2XdcDjrhi/Ry7BWtjDl9WtGPWLiIHb2T66GvIVflZTBoSNZ5JqTtJGYEVMuFA/lbQlMOyDQ==} - memoize-one@5.2.1: - resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} - merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -6904,8 +6858,8 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - mermaid@11.13.0: - resolution: {integrity: sha512-fEnci+Immw6lKMFI8sqzjlATTyjLkRa6axrEgLV2yHTfv8r+h1wjFbV6xeRtd4rUV1cS4EpR9rwp3Rci7TRWDw==} + mermaid@11.14.0: + resolution: {integrity: sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==} micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -7043,10 +6997,6 @@ packages: engines: {node: '>=16'} hasBin: true - mimic-function@5.0.1: - resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} - engines: {node: '>=18'} - mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -7137,8 +7087,8 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@16.2.1: - resolution: {integrity: sha512-VaChzNL7o9rbfdt60HUj8tev4m6d7iC1igAy157526+cJlXOQu5LzsBXNT+xaJnTP/k+utSX5vMv7m0G+zKH+Q==} + next@16.2.2: + resolution: {integrity: sha512-i6AJdyVa4oQjyvX/6GeER8dpY/xlIV+4NMv/svykcLtURJSy/WzDnnUk/TM4d0uewFHK7xSQz4TbIwPgjky+3A==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -7168,9 +7118,6 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - node-fetch-native@1.6.7: - resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} - node-releases@2.0.36: resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} @@ -7215,18 +7162,17 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - ofetch@1.5.1: - resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} - ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - onetime@7.0.0: - resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} - engines: {node: '>=18'} + oniguruma-parser@0.12.1: + resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} + + oniguruma-to-es@4.3.5: + resolution: {integrity: sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==} open@10.2.0: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} @@ -7250,21 +7196,21 @@ packages: oxc-resolver@11.19.1: resolution: {integrity: sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==} - oxfmt@0.42.0: - resolution: {integrity: sha512-QhejGErLSMReNuZ6vxgFHDyGoPbjTRNi6uGHjy0cvIjOQFqD6xmr/T+3L41ixR3NIgzcNiJ6ylQKpvShTgDfqg==} + oxfmt@0.43.0: + resolution: {integrity: sha512-KTYNG5ISfHSdmeZ25Xzb3qgz9EmQvkaGAxgBY/p38+ZiAet3uZeu7FnMwcSQJg152Qwl0wnYAxDc+Z/H6cvrwA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - oxlint-tsgolint@0.17.3: - resolution: {integrity: sha512-1eh4bcpOMw0e7+YYVxmhFc2mo/V6hJ2+zfukqf+GprvVn3y94b69M/xNrYLmx5A+VdYe0i/bJ2xOs6Hp/jRmRA==} + oxlint-tsgolint@0.18.1: + resolution: {integrity: sha512-Hgb0wMfuXBYL0ddY+1hAG8IIfC40ADwPnBuUaC6ENAuCtTF4dHwsy7mCYtQ2e7LoGvfoSJRY0+kqQRiembJ/jQ==} hasBin: true - oxlint@1.57.0: - resolution: {integrity: sha512-DGFsuBX5MFZX9yiDdtKjTrYPq45CZ8Fft6qCltJITYZxfwYjVdGf/6wycGYTACloauwIPxUnYhBVeZbHvleGhw==} + oxlint@1.58.0: + resolution: {integrity: sha512-t4s9leczDMqlvOSjnbCQe7gtoLkWgBGZ7sBdCJ9EOj5IXFSG/X7OAzK4yuH4iW+4cAYe8kLFbC8tuYMwWZm+Cg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - oxlint-tsgolint: '>=0.15.0' + oxlint-tsgolint: '>=0.18.0' peerDependenciesMeta: oxlint-tsgolint: optional: true @@ -7301,9 +7247,6 @@ packages: parse-css-color@0.2.1: resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==} - parse-entities@2.0.0: - resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} - parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} @@ -7392,10 +7335,6 @@ packages: pinyin-pro@3.28.0: resolution: {integrity: sha512-mMRty6RisoyYNphJrTo3pnvp3w8OMZBrXm9YSWkxhAfxKj1KZk2y8T2PDIZlDDRsvZ0No+Hz6FI4sZpA6Ey25g==} - pirates@4.0.7: - resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} - engines: {node: '>= 6'} - pixelmatch@7.1.0: resolution: {integrity: sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==} hasBin: true @@ -7406,13 +7345,13 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} - playwright-core@1.58.2: - resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} engines: {node: '>=18'} hasBin: true - playwright@1.58.2: - resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + playwright@1.59.1: + resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} engines: {node: '>=18'} hasBin: true @@ -7437,24 +7376,6 @@ packages: resolution: {integrity: sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==} engines: {node: '>= 10.12'} - postcss-load-config@6.0.1: - resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} - engines: {node: '>= 18'} - peerDependencies: - jiti: '>=1.21.0' - postcss: '>=8.0.9' - tsx: ^4.8.1 - yaml: 2.8.3 - peerDependenciesMeta: - jiti: - optional: true - postcss: - optional: true - tsx: - optional: true - yaml: - optional: true - postcss-selector-parser@6.0.10: resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} engines: {node: '>=4'} @@ -7492,10 +7413,6 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - prismjs@1.30.0: - resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} - engines: {node: '>=6'} - progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -7506,9 +7423,6 @@ packages: property-expr@2.0.6: resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} - property-information@5.6.0: - resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} - property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} @@ -7531,9 +7445,6 @@ packages: quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} - quansync@1.0.0: - resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} - queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -7596,10 +7507,10 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' - react-i18next@16.6.6: - resolution: {integrity: sha512-ZgL2HUoW34UKUkOV7uSQFE1CDnRPD+tCR3ywSuWH7u2iapnz86U8Bi3Vrs620qNDzCf1F47NxglCEkchCTDOHw==} + react-i18next@17.0.2: + resolution: {integrity: sha512-shBftH2vaTWK2Bsp7FiL+cevx3xFJlvFxmsDFQSrJc+6twHkP0tv/bGa01VVWzpreUVVwU+3Hev5iFqRg65RwA==} peerDependencies: - i18next: '>= 25.10.9' + i18next: '>= 26.0.1' react: '>= 16.8.0' react-dom: '*' react-native: '*' @@ -7686,24 +7597,12 @@ packages: '@types/react': optional: true - react-syntax-highlighter@15.6.6: - resolution: {integrity: sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==} - peerDependencies: - react: '>= 0.14.0' - react-textarea-autosize@8.5.9: resolution: {integrity: sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==} engines: {node: '>=10'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-window@1.8.11: - resolution: {integrity: sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==} - engines: {node: '>8.0.0'} - peerDependencies: - react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react@19.2.4: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} @@ -7759,8 +7658,14 @@ packages: reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} - refractor@3.6.0: - resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==} + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} regexp-ast-analysis@0.7.1: resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==} @@ -7841,10 +7746,6 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -7853,10 +7754,6 @@ packages: engines: {node: '>= 0.4'} hasBin: true - restore-cursor@5.1.0: - resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} - engines: {node: '>=18'} - reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -7961,9 +7858,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} + shiki@4.0.2: + resolution: {integrity: sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==} + engines: {node: '>=20'} simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} @@ -8006,9 +7903,6 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} - space-separated-tokens@1.1.5: - resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} - space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -8045,8 +7939,8 @@ packages: resolution: {integrity: sha512-9SN0XIjBBXCT6ZXXVnScJN4KP2RyFg6B8sEoFlugVHMANysfaEni4LTWlvUQQ/R0wgZl1Ovt9KBQbzn21kHoZA==} engines: {node: '>=20.19.0'} - storybook@10.3.3: - resolution: {integrity: sha512-tMoRAts9EVqf+mEMPLC6z1DPyHbcPe+CV1MhLN55IKsl0HxNjvVGK44rVPSePbltPE6vIsn4bdRj6CCUt8SJwQ==} + storybook@10.3.4: + resolution: {integrity: sha512-866YXZy9k59tLPl9SN3KZZOFeBC/swxkuBVtW8iQjJIzfCrvk7zXQd8RSQ4ignmCdArVvY4lGMCAT4yNaZSt1g==} hasBin: true peerDependencies: prettier: ^2 || ^3 @@ -8136,11 +8030,6 @@ packages: stylis@4.3.6: resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} - sucrase@3.35.1: - resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -8199,10 +8088,6 @@ packages: resolution: {integrity: sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==} engines: {node: '>=18'} - taze@19.10.0: - resolution: {integrity: sha512-pylMr+Yl8m4ZXu5LwWdtfCOJhLW69NuoeZTLtRzTekfheQ1ix5wOWjQlTb8S3SSxLlDcYFuajQOWllO5iyE0jg==} - hasBin: true - terser-webpack-plugin@5.4.0: resolution: {integrity: sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==} engines: {node: '>= 10.13.0'} @@ -8246,9 +8131,6 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyexec@1.0.4: resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} engines: {node: '>=18'} @@ -8302,10 +8184,6 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} - tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true - trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -8330,9 +8208,6 @@ packages: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} - ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - ts-pattern@5.9.0: resolution: {integrity: sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==} @@ -8363,25 +8238,6 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsup@8.5.1: - resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} - engines: {node: '>=18'} - hasBin: true - peerDependencies: - '@microsoft/api-extractor': ^7.36.0 - '@swc/core': ^1 - postcss: ^8.4.12 - typescript: '>=4.5.0' - peerDependenciesMeta: - '@microsoft/api-extractor': - optional: true - '@swc/core': - optional: true - postcss: - optional: true - typescript: - optional: true - tsx@4.21.0: resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} @@ -8409,8 +8265,8 @@ packages: resolution: {integrity: sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==} engines: {node: '>=20'} - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + typescript@6.0.2: + resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} engines: {node: '>=14.17'} hasBin: true @@ -8426,12 +8282,6 @@ packages: resolution: {integrity: sha512-X2wH19RAPZE3+ldGicOkoj/SIA83OIxcJ6Cuaw23hf8Xc6fQpvZXY0SftE2JgS0QhYLUG4uwodSI3R53keyh7w==} engines: {node: '>=14'} - unconfig-core@7.5.0: - resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} - - unconfig@7.5.0: - resolution: {integrity: sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA==} - undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} @@ -8595,8 +8445,8 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vinext@0.0.38: - resolution: {integrity: sha512-zlQswirXCApDgAFq1eoO/YbRlavGE+Bnowz5vXoQa2EmbFhYg52+T8SZs1QWdOqkbZMhpLIV/iaWvHtkRv2t4Q==} + vinext@0.0.39: + resolution: {integrity: sha512-2WCW5wKy4YkCGFtoUGDmfxMBPULGOXEqd7Rczwi4cMEx2SG8UnY5/56XXGAnjf8aIIrx8ggOVWJSebH96QdjZg==} engines: {node: '>=22'} hasBin: true peerDependencies: @@ -8638,8 +8488,8 @@ packages: storybook: ^0.0.0-0 || ^9.0.0 || ^10.0.0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - vite-plus@0.1.14: - resolution: {integrity: sha512-p4pWlpZZNiEsHxPWNdeIU9iuPix3ydm3ficb0dXPggoyIkdotfXtvn2NPX9KwfiQImU72EVEs4+VYBZYNcUYrw==} + vite-plus@0.1.15: + resolution: {integrity: sha512-PBUvTq4D4BJcuusCA3mrSQmXcGVdPX9CIPpS7Y6+T+LbDsrmAZ+ITl9FzuE6zXvpT6Nht9cpHtwOLJw7m3adog==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -8826,10 +8676,6 @@ packages: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} - xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -8874,12 +8720,6 @@ packages: zimmerframe@1.1.4: resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} - zod-validation-error@4.0.2: - resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} - engines: {node: '>=18.0.0'} - peerDependencies: - zod: ^3.25.0 || ^4.0.0 - zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} @@ -8933,27 +8773,27 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@amplitude/analytics-browser@2.38.0': + '@amplitude/analytics-browser@2.38.1': dependencies: - '@amplitude/analytics-core': 2.44.0 - '@amplitude/plugin-autocapture-browser': 1.25.0 - '@amplitude/plugin-custom-enrichment-browser': 0.1.2 - '@amplitude/plugin-network-capture-browser': 1.9.11 - '@amplitude/plugin-page-url-enrichment-browser': 0.7.3 - '@amplitude/plugin-page-view-tracking-browser': 2.9.4 - '@amplitude/plugin-web-vitals-browser': 1.1.26 + '@amplitude/analytics-core': 2.44.1 + '@amplitude/plugin-autocapture-browser': 1.25.1 + '@amplitude/plugin-custom-enrichment-browser': 0.1.3 + '@amplitude/plugin-network-capture-browser': 1.9.12 + '@amplitude/plugin-page-url-enrichment-browser': 0.7.4 + '@amplitude/plugin-page-view-tracking-browser': 2.9.5 + '@amplitude/plugin-web-vitals-browser': 1.1.27 tslib: 2.8.1 - '@amplitude/analytics-client-common@2.4.41': + '@amplitude/analytics-client-common@2.4.42': dependencies: '@amplitude/analytics-connector': 1.6.4 - '@amplitude/analytics-core': 2.44.0 + '@amplitude/analytics-core': 2.44.1 '@amplitude/analytics-types': 2.11.1 tslib: 2.8.1 '@amplitude/analytics-connector@1.6.4': {} - '@amplitude/analytics-core@2.44.0': + '@amplitude/analytics-core@2.44.1': dependencies: '@amplitude/analytics-connector': 1.6.4 '@types/zen-observable': 0.8.3 @@ -8967,48 +8807,48 @@ snapshots: dependencies: js-base64: 3.7.8 - '@amplitude/plugin-autocapture-browser@1.25.0': + '@amplitude/plugin-autocapture-browser@1.25.1': dependencies: - '@amplitude/analytics-core': 2.44.0 + '@amplitude/analytics-core': 2.44.1 tslib: 2.8.1 - '@amplitude/plugin-custom-enrichment-browser@0.1.2': + '@amplitude/plugin-custom-enrichment-browser@0.1.3': dependencies: - '@amplitude/analytics-core': 2.44.0 + '@amplitude/analytics-core': 2.44.1 tslib: 2.8.1 - '@amplitude/plugin-network-capture-browser@1.9.11': + '@amplitude/plugin-network-capture-browser@1.9.12': dependencies: - '@amplitude/analytics-core': 2.44.0 + '@amplitude/analytics-core': 2.44.1 tslib: 2.8.1 - '@amplitude/plugin-page-url-enrichment-browser@0.7.3': + '@amplitude/plugin-page-url-enrichment-browser@0.7.4': dependencies: - '@amplitude/analytics-core': 2.44.0 + '@amplitude/analytics-core': 2.44.1 tslib: 2.8.1 - '@amplitude/plugin-page-view-tracking-browser@2.9.4': + '@amplitude/plugin-page-view-tracking-browser@2.9.5': dependencies: - '@amplitude/analytics-core': 2.44.0 + '@amplitude/analytics-core': 2.44.1 tslib: 2.8.1 - '@amplitude/plugin-session-replay-browser@1.27.5(@amplitude/rrweb@2.0.0-alpha.37)(rollup@4.59.0)': + '@amplitude/plugin-session-replay-browser@1.27.6(@amplitude/rrweb@2.0.0-alpha.37)(rollup@4.59.0)': dependencies: - '@amplitude/analytics-client-common': 2.4.41 - '@amplitude/analytics-core': 2.44.0 + '@amplitude/analytics-client-common': 2.4.42 + '@amplitude/analytics-core': 2.44.1 '@amplitude/analytics-types': 2.11.1 '@amplitude/rrweb-plugin-console-record': 2.0.0-alpha.36(@amplitude/rrweb@2.0.0-alpha.37) '@amplitude/rrweb-record': 2.0.0-alpha.36 - '@amplitude/session-replay-browser': 1.35.0(@amplitude/rrweb@2.0.0-alpha.37)(rollup@4.59.0) + '@amplitude/session-replay-browser': 1.35.1(@amplitude/rrweb@2.0.0-alpha.37)(rollup@4.59.0) idb-keyval: 6.2.2 tslib: 2.8.1 transitivePeerDependencies: - '@amplitude/rrweb' - rollup - '@amplitude/plugin-web-vitals-browser@1.1.26': + '@amplitude/plugin-web-vitals-browser@1.1.27': dependencies: - '@amplitude/analytics-core': 2.44.0 + '@amplitude/analytics-core': 2.44.1 tslib: 2.8.1 web-vitals: 5.1.0 @@ -9053,10 +8893,10 @@ snapshots: base64-arraybuffer: 1.0.2 mitt: 3.0.1 - '@amplitude/session-replay-browser@1.35.0(@amplitude/rrweb@2.0.0-alpha.37)(rollup@4.59.0)': + '@amplitude/session-replay-browser@1.35.1(@amplitude/rrweb@2.0.0-alpha.37)(rollup@4.59.0)': dependencies: - '@amplitude/analytics-client-common': 2.4.41 - '@amplitude/analytics-core': 2.44.0 + '@amplitude/analytics-client-common': 2.4.42 + '@amplitude/analytics-core': 2.44.1 '@amplitude/analytics-types': 2.11.1 '@amplitude/experiment-core': 0.7.2 '@amplitude/rrweb-packer': 2.0.0-alpha.36 @@ -9074,44 +8914,44 @@ snapshots: '@amplitude/targeting@0.2.0': dependencies: - '@amplitude/analytics-client-common': 2.4.41 - '@amplitude/analytics-core': 2.44.0 + '@amplitude/analytics-client-common': 2.4.42 + '@amplitude/analytics-core': 2.44.1 '@amplitude/analytics-types': 2.11.1 '@amplitude/experiment-core': 0.7.2 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@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)': + '@antfu/eslint-config@8.0.0(@eslint-react/eslint-plugin@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(@next/eslint-plugin-next@16.2.2)(@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(@typescript-eslint/typescript-estree@8.58.0(typescript@6.0.2))(@typescript-eslint/utils@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(@voidzero-dev/vite-plus-test@0.1.15(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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@6.0.2)(yaml@2.8.3))(@vue/compiler-sfc@3.5.31)(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.58.0(oxlint-tsgolint@0.18.1))(typescript@6.0.2)': dependencies: '@antfu/install-pkg': 1.1.0 - '@clack/prompts': 1.1.0 - '@e18e/eslint-plugin': 0.2.0(eslint@10.1.0(jiti@2.6.1))(oxlint@1.57.0(oxlint-tsgolint@0.17.3)) + '@clack/prompts': 1.2.0 + '@e18e/eslint-plugin': 0.3.0(eslint@10.1.0(jiti@2.6.1))(oxlint@1.58.0(oxlint-tsgolint@0.18.1)) '@eslint-community/eslint-plugin-eslint-comments': 4.7.1(eslint@10.1.0(jiti@2.6.1)) - '@eslint/markdown': 7.5.1 + '@eslint/markdown': 8.0.1 '@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) + '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/parser': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@vitest/eslint-plugin': 1.6.14(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(@voidzero-dev/vite-plus-test@0.1.15(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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@6.0.2)(yaml@2.8.3))(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) ansis: 4.2.0 cac: 7.0.0 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-flat-config-utils: 3.1.0 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-command: 3.5.2(@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(@typescript-eslint/typescript-estree@8.58.0(typescript@6.0.2))(@typescript-eslint/utils@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-import-lite: 0.6.0(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-n: 17.24.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) eslint-plugin-no-only-tests: 3.3.0 - eslint-plugin-perfectionist: 5.7.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-perfectionist: 5.7.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) 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-unicorn: 64.0.0(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(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.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(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 @@ -9121,9 +8961,8 @@ snapshots: 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@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@2.6.1)) + '@eslint-react/eslint-plugin': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@next/eslint-plugin-next': 16.2.2 eslint-plugin-react-refresh: 0.5.2(eslint@10.1.0(jiti@2.6.1)) transitivePeerDependencies: - '@eslint/json' @@ -9141,14 +8980,6 @@ snapshots: package-manager-detector: 1.6.0 tinyexec: 1.0.4 - '@antfu/ni@28.3.0': - dependencies: - ansis: 4.2.0 - fzf: 0.5.2 - package-manager-detector: 1.6.0 - tinyexec: 1.0.4 - tinyglobby: 0.2.15 - '@antfu/utils@8.1.1': {} '@babel/code-frame@7.29.0': @@ -9285,12 +9116,12 @@ snapshots: dependencies: '@chevrotain/gast': 11.1.2 '@chevrotain/types': 11.1.2 - lodash-es: 4.17.23 + lodash-es: 4.18.0 '@chevrotain/gast@11.1.2': dependencies: '@chevrotain/types': 11.1.2 - lodash-es: 4.17.23 + lodash-es: 4.18.0 '@chevrotain/regexp-to-ast@11.1.2': {} @@ -9298,13 +9129,13 @@ snapshots: '@chevrotain/utils@11.1.2': {} - '@chromatic-com/storybook@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))': + '@chromatic-com/storybook@5.1.1(storybook@10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': dependencies: '@neoconfetti/react': 1.0.0 chromatic: 13.3.5 filesize: 10.1.6 jsonfile: 6.2.0 - storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) strip-ansi: 7.2.0 transitivePeerDependencies: - '@chromatic-com/cypress' @@ -9315,8 +9146,9 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 - '@clack/core@1.1.0': + '@clack/core@1.2.0': dependencies: + fast-wrap-ansi: 0.1.6 sisteransi: 1.0.5 '@clack/prompts@0.8.2': @@ -9325,12 +9157,14 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 - '@clack/prompts@1.1.0': + '@clack/prompts@1.2.0': dependencies: - '@clack/core': 1.1.0 + '@clack/core': 1.2.0 + fast-string-width: 1.1.0 + fast-wrap-ansi: 0.1.6 sisteransi: 1.0.5 - '@code-inspector/core@1.4.5': + '@code-inspector/core@1.5.1': dependencies: '@vue/compiler-dom': 3.5.31 chalk: 4.1.2 @@ -9340,35 +9174,35 @@ snapshots: transitivePeerDependencies: - supports-color - '@code-inspector/esbuild@1.4.5': + '@code-inspector/esbuild@1.5.1': dependencies: - '@code-inspector/core': 1.4.5 + '@code-inspector/core': 1.5.1 transitivePeerDependencies: - supports-color - '@code-inspector/mako@1.4.5': + '@code-inspector/mako@1.5.1': dependencies: - '@code-inspector/core': 1.4.5 + '@code-inspector/core': 1.5.1 transitivePeerDependencies: - supports-color - '@code-inspector/turbopack@1.4.5': + '@code-inspector/turbopack@1.5.1': dependencies: - '@code-inspector/core': 1.4.5 - '@code-inspector/webpack': 1.4.5 + '@code-inspector/core': 1.5.1 + '@code-inspector/webpack': 1.5.1 transitivePeerDependencies: - supports-color - '@code-inspector/vite@1.4.5': + '@code-inspector/vite@1.5.1': dependencies: - '@code-inspector/core': 1.4.5 + '@code-inspector/core': 1.5.1 chalk: 4.1.1 transitivePeerDependencies: - supports-color - '@code-inspector/webpack@1.4.5': + '@code-inspector/webpack@1.5.1': dependencies: - '@code-inspector/core': 1.4.5 + '@code-inspector/core': 1.5.1 transitivePeerDependencies: - supports-color @@ -9482,12 +9316,12 @@ snapshots: '@cucumber/tag-expressions@9.1.0': {} - '@e18e/eslint-plugin@0.2.0(eslint@10.1.0(jiti@2.6.1))(oxlint@1.57.0(oxlint-tsgolint@0.17.3))': + '@e18e/eslint-plugin@0.3.0(eslint@10.1.0(jiti@2.6.1))(oxlint@1.58.0(oxlint-tsgolint@0.18.1))': dependencies: eslint-plugin-depend: 1.5.0(eslint@10.1.0(jiti@2.6.1)) optionalDependencies: eslint: 10.1.0(jiti@2.6.1) - oxlint: 1.57.0(oxlint-tsgolint@0.17.3) + oxlint: 1.58.0(oxlint-tsgolint@0.18.1) '@egoist/tailwindcss-icons@1.9.2(tailwindcss@4.2.2)': dependencies: @@ -9515,7 +9349,7 @@ snapshots: '@es-joy/jsdoccomment@0.84.0': dependencies: '@types/estree': 1.0.8 - '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/types': 8.58.0 comment-parser: 1.4.5 esquery: 1.7.0 jsdoc-type-pratt-parser: 7.1.1 @@ -9618,69 +9452,69 @@ snapshots: '@eslint-community/regexpp@4.12.2': {} - '@eslint-react/ast@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/ast@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)': 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@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) eslint: 10.1.0(jiti@2.6.1) string-ts: 2.3.1 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@eslint-react/core@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@6.0.2)': dependencies: - '@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@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) eslint: 10.1.0(jiti@2.6.1) ts-pattern: 5.9.0 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@eslint-react/eslint-plugin@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/eslint-plugin@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: - '@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@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/utils': 8.57.2(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@6.0.2) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/type-utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) 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 + eslint-plugin-react-dom: 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + eslint-plugin-react-naming-convention: 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + eslint-plugin-react-rsc: 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + eslint-plugin-react-web-api: 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + eslint-plugin-react-x: 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@eslint-react/shared@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@6.0.2)': dependencies: - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) eslint: 10.1.0(jiti@2.6.1) ts-pattern: 5.9.0 - typescript: 5.9.3 + typescript: 6.0.2 zod: 4.3.6 transitivePeerDependencies: - supports-color - '@eslint-react/var@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@6.0.2)': dependencies: - '@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@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) eslint: 10.1.0(jiti@2.6.1) ts-pattern: 5.9.0 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color @@ -9767,6 +9601,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@eslint/markdown@8.0.1': + dependencies: + '@eslint/core': 1.1.1 + '@eslint/plugin-kit': 0.6.1 + github-slugger: 2.0.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-frontmatter: 2.0.1 + mdast-util-gfm: 3.1.0 + mdast-util-math: 3.0.0 + micromark-extension-frontmatter: 2.0.0 + micromark-extension-gfm: 3.0.0 + micromark-extension-math: 3.1.0 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + '@eslint/object-schema@2.1.7': {} '@eslint/object-schema@3.0.3': {} @@ -9835,15 +9685,13 @@ snapshots: react-dom: 19.2.4(react@19.2.4) use-sync-external-store: 1.6.0(react@19.2.4) - '@henrygd/queue@1.2.0': {} - '@heroicons/react@2.2.0(react@19.2.4)': dependencies: react: 19.2.4 - '@hono/node-server@1.19.11(hono@4.12.9)': + '@hono/node-server@1.19.12(hono@4.12.10)': dependencies: - hono: 4.12.9 + hono: 4.12.10 '@humanfs/core@0.19.1': {} @@ -9999,13 +9847,13 @@ 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@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)': + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3))(typescript@6.0.2)': 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@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + react-docgen-typescript: 2.4.0(typescript@6.0.2) + vite: '@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3)' optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.2 '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -10243,7 +10091,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@mermaid-js/parser@1.0.1': + '@mermaid-js/parser@1.1.0': dependencies: langium: 4.2.1 @@ -10269,41 +10117,41 @@ snapshots: '@next/env@16.0.0': {} - '@next/env@16.2.1': {} + '@next/env@16.2.2': {} - '@next/eslint-plugin-next@16.2.1': + '@next/eslint-plugin-next@16.2.2': dependencies: fast-glob: 3.3.1 - '@next/mdx@16.2.1(@mdx-js/loader@3.1.1(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.4))': + '@next/mdx@16.2.2(@mdx-js/loader@3.1.1(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.4))': dependencies: source-map: 0.7.6 optionalDependencies: '@mdx-js/loader': 3.1.1(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.4) - '@next/swc-darwin-arm64@16.2.1': + '@next/swc-darwin-arm64@16.2.2': optional: true - '@next/swc-darwin-x64@16.2.1': + '@next/swc-darwin-x64@16.2.2': optional: true - '@next/swc-linux-arm64-gnu@16.2.1': + '@next/swc-linux-arm64-gnu@16.2.2': optional: true - '@next/swc-linux-arm64-musl@16.2.1': + '@next/swc-linux-arm64-musl@16.2.2': optional: true - '@next/swc-linux-x64-gnu@16.2.1': + '@next/swc-linux-x64-gnu@16.2.2': optional: true - '@next/swc-linux-x64-musl@16.2.1': + '@next/swc-linux-x64-musl@16.2.2': optional: true - '@next/swc-win32-arm64-msvc@16.2.1': + '@next/swc-win32-arm64-msvc@16.2.2': optional: true - '@next/swc-win32-x64-msvc@16.2.1': + '@next/swc-win32-x64-msvc@16.2.2': optional: true '@nodelib/fs.scandir@2.1.5': @@ -10376,11 +10224,11 @@ snapshots: transitivePeerDependencies: - '@opentelemetry/api' - '@orpc/tanstack-query@1.13.13(@orpc/client@1.13.13)(@tanstack/query-core@5.95.2)': + '@orpc/tanstack-query@1.13.13(@orpc/client@1.13.13)(@tanstack/query-core@5.96.1)': dependencies: '@orpc/client': 1.13.13 '@orpc/shared': 1.13.13 - '@tanstack/query-core': 5.95.2 + '@tanstack/query-core': 5.96.1 transitivePeerDependencies: - '@opentelemetry/api' @@ -10451,7 +10299,7 @@ snapshots: '@oxc-parser/binding-win32-x64-msvc@0.121.0': optional: true - '@oxc-project/runtime@0.121.0': {} + '@oxc-project/runtime@0.122.0': {} '@oxc-project/types@0.121.0': {} @@ -10522,136 +10370,136 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.19.1': optional: true - '@oxfmt/binding-android-arm-eabi@0.42.0': + '@oxfmt/binding-android-arm-eabi@0.43.0': optional: true - '@oxfmt/binding-android-arm64@0.42.0': + '@oxfmt/binding-android-arm64@0.43.0': optional: true - '@oxfmt/binding-darwin-arm64@0.42.0': + '@oxfmt/binding-darwin-arm64@0.43.0': optional: true - '@oxfmt/binding-darwin-x64@0.42.0': + '@oxfmt/binding-darwin-x64@0.43.0': optional: true - '@oxfmt/binding-freebsd-x64@0.42.0': + '@oxfmt/binding-freebsd-x64@0.43.0': optional: true - '@oxfmt/binding-linux-arm-gnueabihf@0.42.0': + '@oxfmt/binding-linux-arm-gnueabihf@0.43.0': optional: true - '@oxfmt/binding-linux-arm-musleabihf@0.42.0': + '@oxfmt/binding-linux-arm-musleabihf@0.43.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.42.0': + '@oxfmt/binding-linux-arm64-gnu@0.43.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.42.0': + '@oxfmt/binding-linux-arm64-musl@0.43.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.42.0': + '@oxfmt/binding-linux-ppc64-gnu@0.43.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.42.0': + '@oxfmt/binding-linux-riscv64-gnu@0.43.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.42.0': + '@oxfmt/binding-linux-riscv64-musl@0.43.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.42.0': + '@oxfmt/binding-linux-s390x-gnu@0.43.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.42.0': + '@oxfmt/binding-linux-x64-gnu@0.43.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.42.0': + '@oxfmt/binding-linux-x64-musl@0.43.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.42.0': + '@oxfmt/binding-openharmony-arm64@0.43.0': optional: true - '@oxfmt/binding-win32-arm64-msvc@0.42.0': + '@oxfmt/binding-win32-arm64-msvc@0.43.0': optional: true - '@oxfmt/binding-win32-ia32-msvc@0.42.0': + '@oxfmt/binding-win32-ia32-msvc@0.43.0': optional: true - '@oxfmt/binding-win32-x64-msvc@0.42.0': + '@oxfmt/binding-win32-x64-msvc@0.43.0': optional: true - '@oxlint-tsgolint/darwin-arm64@0.17.3': + '@oxlint-tsgolint/darwin-arm64@0.18.1': optional: true - '@oxlint-tsgolint/darwin-x64@0.17.3': + '@oxlint-tsgolint/darwin-x64@0.18.1': optional: true - '@oxlint-tsgolint/linux-arm64@0.17.3': + '@oxlint-tsgolint/linux-arm64@0.18.1': optional: true - '@oxlint-tsgolint/linux-x64@0.17.3': + '@oxlint-tsgolint/linux-x64@0.18.1': optional: true - '@oxlint-tsgolint/win32-arm64@0.17.3': + '@oxlint-tsgolint/win32-arm64@0.18.1': optional: true - '@oxlint-tsgolint/win32-x64@0.17.3': + '@oxlint-tsgolint/win32-x64@0.18.1': optional: true - '@oxlint/binding-android-arm-eabi@1.57.0': + '@oxlint/binding-android-arm-eabi@1.58.0': optional: true - '@oxlint/binding-android-arm64@1.57.0': + '@oxlint/binding-android-arm64@1.58.0': optional: true - '@oxlint/binding-darwin-arm64@1.57.0': + '@oxlint/binding-darwin-arm64@1.58.0': optional: true - '@oxlint/binding-darwin-x64@1.57.0': + '@oxlint/binding-darwin-x64@1.58.0': optional: true - '@oxlint/binding-freebsd-x64@1.57.0': + '@oxlint/binding-freebsd-x64@1.58.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.57.0': + '@oxlint/binding-linux-arm-gnueabihf@1.58.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.57.0': + '@oxlint/binding-linux-arm-musleabihf@1.58.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.57.0': + '@oxlint/binding-linux-arm64-gnu@1.58.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.57.0': + '@oxlint/binding-linux-arm64-musl@1.58.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.57.0': + '@oxlint/binding-linux-ppc64-gnu@1.58.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.57.0': + '@oxlint/binding-linux-riscv64-gnu@1.58.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.57.0': + '@oxlint/binding-linux-riscv64-musl@1.58.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.57.0': + '@oxlint/binding-linux-s390x-gnu@1.58.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.57.0': + '@oxlint/binding-linux-x64-gnu@1.58.0': optional: true - '@oxlint/binding-linux-x64-musl@1.57.0': + '@oxlint/binding-linux-x64-musl@1.58.0': optional: true - '@oxlint/binding-openharmony-arm64@1.57.0': + '@oxlint/binding-openharmony-arm64@1.58.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.57.0': + '@oxlint/binding-win32-arm64-msvc@1.58.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.57.0': + '@oxlint/binding-win32-ia32-msvc@1.58.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.57.0': + '@oxlint/binding-win32-x64-msvc@1.58.0': optional: true '@parcel/watcher-android-arm64@2.5.6': @@ -10717,18 +10565,14 @@ snapshots: '@pkgr/core@0.2.9': {} - '@playwright/test@1.58.2': + '@playwright/test@1.59.1': dependencies: - playwright: 1.58.2 + playwright: 1.59.1 '@polka/url@1.0.0-next.29': {} '@preact/signals-core@1.14.0': {} - '@quansync/fs@1.0.0': - dependencies: - quansync: 1.0.0 - '@radix-ui/primitive@1.1.3': {} '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.4)': @@ -11169,40 +11013,80 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.59.0': optional: true - '@sentry-internal/browser-utils@10.46.0': + '@sentry-internal/browser-utils@10.47.0': dependencies: - '@sentry/core': 10.46.0 + '@sentry/core': 10.47.0 - '@sentry-internal/feedback@10.46.0': + '@sentry-internal/feedback@10.47.0': dependencies: - '@sentry/core': 10.46.0 + '@sentry/core': 10.47.0 - '@sentry-internal/replay-canvas@10.46.0': + '@sentry-internal/replay-canvas@10.47.0': dependencies: - '@sentry-internal/replay': 10.46.0 - '@sentry/core': 10.46.0 + '@sentry-internal/replay': 10.47.0 + '@sentry/core': 10.47.0 - '@sentry-internal/replay@10.46.0': + '@sentry-internal/replay@10.47.0': dependencies: - '@sentry-internal/browser-utils': 10.46.0 - '@sentry/core': 10.46.0 + '@sentry-internal/browser-utils': 10.47.0 + '@sentry/core': 10.47.0 - '@sentry/browser@10.46.0': + '@sentry/browser@10.47.0': dependencies: - '@sentry-internal/browser-utils': 10.46.0 - '@sentry-internal/feedback': 10.46.0 - '@sentry-internal/replay': 10.46.0 - '@sentry-internal/replay-canvas': 10.46.0 - '@sentry/core': 10.46.0 + '@sentry-internal/browser-utils': 10.47.0 + '@sentry-internal/feedback': 10.47.0 + '@sentry-internal/replay': 10.47.0 + '@sentry-internal/replay-canvas': 10.47.0 + '@sentry/core': 10.47.0 - '@sentry/core@10.46.0': {} + '@sentry/core@10.47.0': {} - '@sentry/react@10.46.0(react@19.2.4)': + '@sentry/react@10.47.0(react@19.2.4)': dependencies: - '@sentry/browser': 10.46.0 - '@sentry/core': 10.46.0 + '@sentry/browser': 10.47.0 + '@sentry/core': 10.47.0 react: 19.2.4 + '@shikijs/core@4.0.2': + dependencies: + '@shikijs/primitive': 4.0.2 + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.5 + + '@shikijs/engine-oniguruma@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + + '@shikijs/primitive@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/themes@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + + '@shikijs/types@4.0.2': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + '@shuding/opentype.js@1.4.0-beta.0': dependencies: fflate: 0.7.4 @@ -11248,15 +11132,15 @@ 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@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-docs@10.3.4(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.4(@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@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/csf-plugin': 10.3.4(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.4(@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)) + '@storybook/react-dom-shim': 10.3.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) 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) + storybook: 10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' @@ -11265,41 +11149,41 @@ snapshots: - vite - webpack - '@storybook/addon-links@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))': + '@storybook/addon-links@10.3.4(react@19.2.4)(storybook@10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': dependencies: '@storybook/global': 5.0.0 - storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) optionalDependencies: react: 19.2.4 - '@storybook/addon-onboarding@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/addon-onboarding@10.3.4(storybook@10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': dependencies: - storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@storybook/addon-themes@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/addon-themes@10.3.4(storybook@10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': dependencies: - storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.4(@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@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/builder-vite@10.3.4(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.4(@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@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) + '@storybook/csf-plugin': 10.3.4(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.4(@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.4(@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@2.6.1)(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.15(@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@6.0.2)(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@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/csf-plugin@10.3.4(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.4(@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) + storybook: 10.3.4(@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@2.6.1)(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.15(@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@6.0.2)(yaml@2.8.3)' webpack: 5.105.4(esbuild@0.27.2)(uglify-js@3.19.3) '@storybook/global@5.0.0': {} @@ -11309,20 +11193,20 @@ 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@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/nextjs-vite@10.3.4(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(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.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2)(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@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@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) + '@storybook/builder-vite': 10.3.4(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.4(@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.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2) + '@storybook/react-vite': 10.3.4(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) + next: 16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(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) + storybook: 10.3.4(@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@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) + vite: '@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3)' + vite-plugin-storybook-nextjs: 3.2.4(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3))(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(storybook@10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2) optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -11331,27 +11215,27 @@ snapshots: - supports-color - webpack - '@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))': + '@storybook/react-dom-shim@10.3.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': dependencies: 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) + storybook: 10.3.4(@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@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))': + '@storybook/react-vite@10.3.4(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2)(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@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) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3))(typescript@6.0.2) '@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@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/builder-vite': 10.3.4(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.4(@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.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2) empathic: 2.0.0 magic-string: 0.30.21 react: 19.2.4 react-docgen: 8.0.3 react-dom: 19.2.4(react@19.2.4) 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) + storybook: 10.3.4(@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@2.6.1)(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.15(@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@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: - esbuild - rollup @@ -11359,17 +11243,17 @@ snapshots: - typescript - webpack - '@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@10.3.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2)': dependencies: '@storybook/global': 5.0.0 - '@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)) + '@storybook/react-dom-shim': 10.3.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) react: 19.2.4 react-docgen: 8.0.3 - react-docgen-typescript: 2.4.0(typescript@5.9.3) + react-docgen-typescript: 2.4.0(typescript@6.0.2) 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: 10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color @@ -11385,7 +11269,7 @@ snapshots: '@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@2.6.1)) - '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/types': 8.58.0 eslint: 10.1.0(jiti@2.6.1) eslint-visitor-keys: 4.2.1 espree: 10.4.0 @@ -11402,18 +11286,18 @@ snapshots: dependencies: tslib: 2.8.1 - '@t3-oss/env-core@0.13.11(typescript@5.9.3)(valibot@1.3.1(typescript@5.9.3))(zod@4.3.6)': + '@t3-oss/env-core@0.13.11(typescript@6.0.2)(valibot@1.3.1(typescript@6.0.2))(zod@4.3.6)': optionalDependencies: - typescript: 5.9.3 - valibot: 1.3.1(typescript@5.9.3) + typescript: 6.0.2 + valibot: 1.3.1(typescript@6.0.2) zod: 4.3.6 - '@t3-oss/env-nextjs@0.13.11(typescript@5.9.3)(valibot@1.3.1(typescript@5.9.3))(zod@4.3.6)': + '@t3-oss/env-nextjs@0.13.11(typescript@6.0.2)(valibot@1.3.1(typescript@6.0.2))(zod@4.3.6)': dependencies: - '@t3-oss/env-core': 0.13.11(typescript@5.9.3)(valibot@1.3.1(typescript@5.9.3))(zod@4.3.6) + '@t3-oss/env-core': 0.13.11(typescript@6.0.2)(valibot@1.3.1(typescript@6.0.2))(zod@4.3.6) optionalDependencies: - typescript: 5.9.3 - valibot: 1.3.1(typescript@5.9.3) + typescript: 6.0.2 + valibot: 1.3.1(typescript@6.0.2) zod: 4.3.6 '@tailwindcss/node@4.2.2': @@ -11490,12 +11374,12 @@ snapshots: postcss-selector-parser: 6.0.10 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))': + '@tailwindcss/vite@4.2.2(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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)' + vite: '@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3)' '@tanstack/devtools-client@0.0.6': dependencies: @@ -11525,7 +11409,7 @@ snapshots: react: 19.2.4 solid-js: 1.9.11 - '@tanstack/devtools@0.11.0(csstype@3.2.3)(solid-js@1.9.11)': + '@tanstack/devtools@0.11.1(csstype@3.2.3)(solid-js@1.9.11)': dependencies: '@solid-primitives/event-listener': 2.4.5(solid-js@1.9.11) '@solid-primitives/keyboard': 1.3.5(solid-js@1.9.11) @@ -11541,26 +11425,26 @@ snapshots: - csstype - utf-8-validate - '@tanstack/eslint-plugin-query@5.95.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': + '@tanstack/eslint-plugin-query@5.96.1(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) eslint: 10.1.0(jiti@2.6.1) optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@tanstack/form-core@1.28.5': + '@tanstack/form-core@1.28.6': dependencies: '@tanstack/devtools-event-client': 0.4.3 '@tanstack/pacer-lite': 0.1.1 '@tanstack/store': 0.9.3 - '@tanstack/form-devtools@0.2.19(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11)': + '@tanstack/form-devtools@0.2.20(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11)': dependencies: '@tanstack/devtools-ui': 0.5.1(csstype@3.2.3)(solid-js@1.9.11) '@tanstack/devtools-utils': 0.4.0(@types/react@19.2.14)(react@19.2.4)(solid-js@1.9.11) - '@tanstack/form-core': 1.28.5 + '@tanstack/form-core': 1.28.6 clsx: 2.1.1 dayjs: 1.11.20 goober: 2.1.18(csstype@3.2.3) @@ -11574,13 +11458,13 @@ snapshots: '@tanstack/pacer-lite@0.1.1': {} - '@tanstack/query-core@5.95.2': {} + '@tanstack/query-core@5.96.1': {} - '@tanstack/query-devtools@5.95.2': {} + '@tanstack/query-devtools@5.96.1': {} - '@tanstack/react-devtools@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)': + '@tanstack/react-devtools@0.10.1(@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)': dependencies: - '@tanstack/devtools': 0.11.0(csstype@3.2.3)(solid-js@1.9.11) + '@tanstack/devtools': 0.11.1(csstype@3.2.3)(solid-js@1.9.11) '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) react: 19.2.4 @@ -11591,10 +11475,10 @@ snapshots: - solid-js - utf-8-validate - '@tanstack/react-form-devtools@0.2.19(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11)': + '@tanstack/react-form-devtools@0.2.20(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11)': dependencies: '@tanstack/devtools-utils': 0.4.0(@types/react@19.2.14)(react@19.2.4)(solid-js@1.9.11) - '@tanstack/form-devtools': 0.2.19(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11) + '@tanstack/form-devtools': 0.2.20(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11) react: 19.2.4 transitivePeerDependencies: - '@types/react' @@ -11603,23 +11487,23 @@ snapshots: - solid-js - vue - '@tanstack/react-form@1.28.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tanstack/react-form@1.28.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/form-core': 1.28.5 + '@tanstack/form-core': 1.28.6 '@tanstack/react-store': 0.9.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 transitivePeerDependencies: - react-dom - '@tanstack/react-query-devtools@5.95.2(@tanstack/react-query@5.95.2(react@19.2.4))(react@19.2.4)': + '@tanstack/react-query-devtools@5.96.1(@tanstack/react-query@5.96.1(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/query-devtools': 5.95.2 - '@tanstack/react-query': 5.95.2(react@19.2.4) + '@tanstack/query-devtools': 5.96.1 + '@tanstack/react-query': 5.96.1(react@19.2.4) react: 19.2.4 - '@tanstack/react-query@5.95.2(react@19.2.4)': + '@tanstack/react-query@5.96.1(react@19.2.4)': dependencies: - '@tanstack/query-core': 5.95.2 + '@tanstack/query-core': 5.96.1 react: 19.2.4 '@tanstack/react-store@0.9.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': @@ -11675,37 +11559,37 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 - '@tsslint/cli@3.0.2(@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@5.9.3))(typescript@5.9.3)': + '@tsslint/cli@3.0.2(@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@6.0.2))(typescript@6.0.2)': dependencies: '@clack/prompts': 0.8.2 - '@tsslint/config': 3.0.2(@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@5.9.3))(typescript@5.9.3) + '@tsslint/config': 3.0.2(@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@6.0.2))(typescript@6.0.2) '@tsslint/core': 3.0.2 '@volar/language-core': 2.4.28 '@volar/language-hub': 0.0.1 '@volar/typescript': 2.4.28 minimatch: 10.2.4 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - '@tsslint/compat-eslint' - tsl - '@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@5.9.3)': + '@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@6.0.2)': dependencies: '@tsslint/types': 3.0.2 - '@typescript-eslint/parser': 8.57.2(eslint@9.27.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.58.0(eslint@9.27.0(jiti@2.6.1))(typescript@6.0.2) 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@2.6.1)(typescript@5.9.3))(typescript@5.9.3)': + '@tsslint/config@3.0.2(@tsslint/compat-eslint@3.0.2(jiti@2.6.1)(typescript@6.0.2))(typescript@6.0.2)': dependencies: '@tsslint/types': 3.0.2 minimatch: 10.2.4 - ts-api-utils: 2.5.0(typescript@5.9.3) + ts-api-utils: 2.5.0(typescript@6.0.2) optionalDependencies: - '@tsslint/compat-eslint': 3.0.2(jiti@2.6.1)(typescript@5.9.3) + '@tsslint/compat-eslint': 3.0.2(jiti@2.6.1)(typescript@6.0.2) transitivePeerDependencies: - typescript @@ -11896,10 +11780,6 @@ snapshots: '@types/geojson@7946.0.16': {} - '@types/hast@2.3.10': - dependencies: - '@types/unist': 2.0.11 - '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -11938,14 +11818,6 @@ snapshots: dependencies: '@types/react': 19.2.14 - '@types/react-syntax-highlighter@15.5.13': - dependencies: - '@types/react': 19.2.14 - - '@types/react-window@1.8.8': - dependencies: - '@types/react': 19.2.14 - '@types/react@19.2.14': dependencies: csstype: 3.2.3 @@ -11974,60 +11846,81 @@ 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@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.57.2(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@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.2 + '@typescript-eslint/parser': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/type-utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.58.0 eslint: 10.1.0(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.57.2(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@6.0.2)': 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/typescript-estree': 8.57.2(typescript@6.0.2) '@typescript-eslint/visitor-keys': 8.57.2 debug: 4.4.3(supports-color@8.1.1) eslint: 10.1.0(jiti@2.6.1) - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.57.2(eslint@9.27.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)': 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 + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.58.0 + debug: 4.4.3(supports-color@8.1.1) + eslint: 10.1.0(jiti@2.6.1) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.58.0(eslint@9.27.0(jiti@2.6.1))(typescript@6.0.2)': + dependencies: + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.58.0 debug: 4.4.3(supports-color@8.1.1) eslint: 9.27.0(jiti@2.6.1) - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.57.2(typescript@5.9.3)': + '@typescript-eslint/project-service@8.57.2(typescript@6.0.2)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.9.3) - '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@6.0.2) + '@typescript-eslint/types': 8.58.0 debug: 4.4.3(supports-color@8.1.1) - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/project-service@8.58.0(typescript@6.0.2)': dependencies: - '@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@2.6.1))(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@6.0.2) + '@typescript-eslint/types': 8.58.0 + debug: 4.4.3(supports-color@8.1.1) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)': + dependencies: + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/typescript-estree': 8.57.2(typescript@6.0.2) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) ajv: 6.14.0 eslint: 10.1.0(jiti@2.6.1) json-stable-stringify-without-jsonify: 1.0.1 @@ -12042,47 +11935,84 @@ snapshots: '@typescript-eslint/types': 8.57.2 '@typescript-eslint/visitor-keys': 8.57.2 - '@typescript-eslint/tsconfig-utils@8.57.2(typescript@5.9.3)': + '@typescript-eslint/scope-manager@8.58.0': dependencies: - typescript: 5.9.3 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/visitor-keys': 8.58.0 - '@typescript-eslint/type-utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.57.2(typescript@6.0.2)': 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@2.6.1))(typescript@5.9.3) + typescript: 6.0.2 + + '@typescript-eslint/tsconfig-utils@8.58.0(typescript@6.0.2)': + dependencies: + typescript: 6.0.2 + + '@typescript-eslint/type-utils@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)': + dependencies: + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) debug: 4.4.3(supports-color@8.1.1) eslint: 10.1.0(jiti@2.6.1) - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color '@typescript-eslint/types@8.57.2': {} - '@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3)': + '@typescript-eslint/types@8.58.0': {} + + '@typescript-eslint/typescript-estree@8.57.2(typescript@6.0.2)': dependencies: - '@typescript-eslint/project-service': 8.57.2(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.9.3) + '@typescript-eslint/project-service': 8.57.2(typescript@6.0.2) + '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@6.0.2) '@typescript-eslint/types': 8.57.2 '@typescript-eslint/visitor-keys': 8.57.2 debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.4 semver: 7.7.4 tinyglobby: 0.2.15 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.58.0(typescript@6.0.2)': + dependencies: + '@typescript-eslint/project-service': 8.58.0(typescript@6.0.2) + '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@6.0.2) + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/visitor-keys': 8.58.0 + debug: 4.4.3(supports-color@8.1.1) + minimatch: 10.2.4 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) '@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/typescript-estree': 8.57.2(typescript@6.0.2) eslint: 10.1.0(jiti@2.6.1) - typescript: 5.9.3 + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2) + eslint: 10.1.0(jiti@2.6.1) + typescript: 6.0.2 transitivePeerDependencies: - supports-color @@ -12091,36 +12021,41 @@ snapshots: '@typescript-eslint/types': 8.57.2 eslint-visitor-keys: 5.0.1 - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260329.1': + '@typescript-eslint/visitor-keys@8.58.0': + dependencies: + '@typescript-eslint/types': 8.58.0 + eslint-visitor-keys: 5.0.1 + + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260401.1': optional: true - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260329.1': + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260401.1': optional: true - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260329.1': + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260401.1': optional: true - '@typescript/native-preview-linux-arm@7.0.0-dev.20260329.1': + '@typescript/native-preview-linux-arm@7.0.0-dev.20260401.1': optional: true - '@typescript/native-preview-linux-x64@7.0.0-dev.20260329.1': + '@typescript/native-preview-linux-x64@7.0.0-dev.20260401.1': optional: true - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260329.1': + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260401.1': optional: true - '@typescript/native-preview-win32-x64@7.0.0-dev.20260329.1': + '@typescript/native-preview-win32-x64@7.0.0-dev.20260401.1': optional: true - '@typescript/native-preview@7.0.0-dev.20260329.1': + '@typescript/native-preview@7.0.0-dev.20260401.1': optionalDependencies: - '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260329.1 - '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260329.1 - '@typescript/native-preview-linux-arm': 7.0.0-dev.20260329.1 - '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260329.1 - '@typescript/native-preview-linux-x64': 7.0.0-dev.20260329.1 - '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260329.1 - '@typescript/native-preview-win32-x64': 7.0.0-dev.20260329.1 + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260401.1 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260401.1 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20260401.1 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260401.1 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20260401.1 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260401.1 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20260401.1 '@ungap/structured-clone@1.3.0': {} @@ -12128,56 +12063,56 @@ snapshots: dependencies: unpic: 4.2.2 - '@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)': + '@unpic/react@1.0.2(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(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)': dependencies: '@unpic/core': 1.0.3 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) optionalDependencies: - 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) + next: 16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) '@upsetjs/venn.js@2.0.0': optionalDependencies: d3-selection: 3.0.0 d3-transition: 3.0.1(d3-selection@3.0.0) - '@valibot/to-json-schema@1.6.0(valibot@1.3.1(typescript@5.9.3))': + '@valibot/to-json-schema@1.6.0(valibot@1.3.1(typescript@6.0.2))': dependencies: - valibot: 1.3.1(typescript@5.9.3) + valibot: 1.3.1(typescript@6.0.2) '@vercel/og@0.8.6': dependencies: '@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@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)': + '@vitejs/devtools-kit@0.1.11(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3))(typescript@6.0.2)(ws@8.20.0)': dependencies: - '@vitejs/devtools-rpc': 0.1.11(typescript@5.9.3)(ws@8.20.0) + '@vitejs/devtools-rpc': 0.1.11(typescript@6.0.2)(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@2.6.1)(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.15(@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@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: - typescript - ws - '@vitejs/devtools-rpc@0.1.11(typescript@5.9.3)(ws@8.20.0)': + '@vitejs/devtools-rpc@0.1.11(typescript@6.0.2)(ws@8.20.0)': dependencies: birpc: 4.0.0 ohash: 2.0.11 p-limit: 7.3.0 structured-clone-es: 2.0.0 - valibot: 1.3.1(typescript@5.9.3) + valibot: 1.3.1(typescript@6.0.2) optionalDependencies: ws: 8.20.0 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@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-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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@2.6.1)(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.15(@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@6.0.2)(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@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)': + '@vitejs/plugin-rsc@0.5.21(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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 @@ -12189,15 +12124,15 @@ 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@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)) + vite: '@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3)' + vitefu: 1.1.2(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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@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.2(@voidzero-dev/vite-plus-test@0.1.15(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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@6.0.2)(yaml@2.8.3))': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.1 + '@vitest/utils': 4.1.2 ast-v8-to-istanbul: 1.0.0 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 @@ -12206,12 +12141,12 @@ 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@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: '@voidzero-dev/vite-plus-test@0.1.15(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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@6.0.2)(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))': + '@vitest/coverage-v8@4.1.2(@voidzero-dev/vite-plus-test@0.1.15(@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@6.0.2)(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: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.1 + '@vitest/utils': 4.1.2 ast-v8-to-istanbul: 1.0.0 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 @@ -12220,17 +12155,17 @@ 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)(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: '@voidzero-dev/vite-plus-test@0.1.15(@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@6.0.2)(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@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)': + '@vitest/eslint-plugin@1.6.14(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(@voidzero-dev/vite-plus-test@0.1.15(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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@6.0.2)(yaml@2.8.3))(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) 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@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@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)' + '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + typescript: 6.0.2 + vitest: '@voidzero-dev/vite-plus-test@0.1.15(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: - supports-color @@ -12246,7 +12181,7 @@ snapshots: dependencies: tinyrainbow: 2.0.0 - '@vitest/pretty-format@4.1.1': + '@vitest/pretty-format@4.1.2': dependencies: tinyrainbow: 3.1.0 @@ -12260,15 +12195,15 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@vitest/utils@4.1.1': + '@vitest/utils@4.1.2': dependencies: - '@vitest/pretty-format': 4.1.1 + '@vitest/pretty-format': 4.1.2 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@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-core@0.1.15(@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@6.0.2)(yaml@2.8.3)': dependencies: - '@oxc-project/runtime': 0.121.0 + '@oxc-project/runtime': 0.122.0 '@oxc-project/types': 0.122.0 lightningcss: 1.32.0 postcss: 8.5.8 @@ -12280,32 +12215,32 @@ snapshots: sass: 1.98.0 terser: 5.46.1 tsx: 4.21.0 - typescript: 5.9.3 + typescript: 6.0.2 yaml: 2.8.3 - '@voidzero-dev/vite-plus-darwin-arm64@0.1.14': + '@voidzero-dev/vite-plus-darwin-arm64@0.1.15': optional: true - '@voidzero-dev/vite-plus-darwin-x64@0.1.14': + '@voidzero-dev/vite-plus-darwin-x64@0.1.15': optional: true - '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.14': + '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.15': optional: true - '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.14': + '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.15': optional: true - '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.14': + '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.15': optional: true - '@voidzero-dev/vite-plus-linux-x64-musl@0.1.14': + '@voidzero-dev/vite-plus-linux-x64-musl@0.1.15': 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@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)': + '@voidzero-dev/vite-plus-test@0.1.15(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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@6.0.2)(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@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-core': 0.1.15(@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@6.0.2)(yaml@2.8.3) es-module-lexer: 1.7.0 obug: 2.1.1 pixelmatch: 7.1.0 @@ -12315,7 +12250,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@2.6.1)(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.15(@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@6.0.2)(yaml@2.8.3)' ws: 8.20.0 optionalDependencies: '@types/node': 25.5.0 @@ -12341,11 +12276,11 @@ snapshots: - utf-8-validate - yaml - '@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)': + '@voidzero-dev/vite-plus-test@0.1.15(@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@6.0.2)(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: '@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@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-core': 0.1.15(@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@6.0.2)(yaml@2.8.3) es-module-lexer: 1.7.0 obug: 2.1.1 pixelmatch: 7.1.0 @@ -12381,10 +12316,10 @@ snapshots: - utf-8-validate - yaml - '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.14': + '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.15': optional: true - '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.14': + '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.15': optional: true '@volar/language-core@2.4.28': @@ -12509,6 +12444,8 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 + '@webcontainer/env@1.1.1': {} + '@xstate/fsm@1.6.5': {} '@xtuc/ieee754@1.2.0': {} @@ -12543,7 +12480,7 @@ snapshots: dayjs: 1.11.20 intersection-observer: 0.12.2 js-cookie: 3.0.5 - lodash: 4.17.23 + lodash: 4.18.0 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) react-fast-compare: 3.2.2 @@ -12654,9 +12591,10 @@ snapshots: boolbase@1.0.0: {} - brace-expansion@2.0.2: + brace-expansion@1.1.13: dependencies: balanced-match: 1.0.2 + concat-map: 0.0.1 brace-expansion@5.0.5: dependencies: @@ -12692,15 +12630,8 @@ snapshots: dependencies: run-applescript: 7.1.0 - bundle-require@5.1.0(esbuild@0.27.2): - dependencies: - esbuild: 0.27.2 - load-tsconfig: 0.2.5 - bytes@3.1.2: {} - cac@6.7.14: {} - cac@7.0.0: {} callsites@3.1.0: {} @@ -12745,16 +12676,10 @@ snapshots: character-entities-html4@2.1.0: {} - character-entities-legacy@1.1.4: {} - character-entities-legacy@3.0.0: {} - character-entities@1.2.4: {} - character-entities@2.0.2: {} - character-reference-invalid@1.1.4: {} - character-reference-invalid@2.0.1: {} check-error@2.1.3: {} @@ -12785,7 +12710,7 @@ snapshots: chevrotain-allstar@0.3.1(chevrotain@11.1.2): dependencies: chevrotain: 11.1.2 - lodash-es: 4.17.23 + lodash-es: 4.18.0 chevrotain@11.1.2: dependencies: @@ -12794,11 +12719,12 @@ snapshots: '@chevrotain/regexp-to-ast': 11.1.2 '@chevrotain/types': 11.1.2 '@chevrotain/utils': 11.1.2 - lodash-es: 4.17.23 + lodash-es: 4.18.0 chokidar@4.0.3: dependencies: readdirp: 4.1.2 + optional: true chownr@1.1.4: optional: true @@ -12847,14 +12773,14 @@ snapshots: - '@types/react' - '@types/react-dom' - code-inspector-plugin@1.4.5: + code-inspector-plugin@1.5.1: dependencies: - '@code-inspector/core': 1.4.5 - '@code-inspector/esbuild': 1.4.5 - '@code-inspector/mako': 1.4.5 - '@code-inspector/turbopack': 1.4.5 - '@code-inspector/vite': 1.4.5 - '@code-inspector/webpack': 1.4.5 + '@code-inspector/core': 1.5.1 + '@code-inspector/esbuild': 1.5.1 + '@code-inspector/mako': 1.5.1 + '@code-inspector/turbopack': 1.5.1 + '@code-inspector/vite': 1.5.1 + '@code-inspector/webpack': 1.5.1 chalk: 4.1.1 transitivePeerDependencies: - supports-color @@ -12867,8 +12793,6 @@ snapshots: color-name@1.1.4: {} - comma-separated-tokens@1.0.8: {} - comma-separated-tokens@2.0.3: {} commander@14.0.0: {} @@ -12879,8 +12803,6 @@ snapshots: commander@2.20.3: {} - commander@4.1.1: {} - commander@7.2.0: {} commander@8.3.0: {} @@ -12891,12 +12813,12 @@ snapshots: compare-versions@6.1.1: {} + concat-map@0.0.1: {} + confbox@0.1.8: {} confbox@0.2.4: {} - consola@3.4.2: {} - convert-source-map@2.0.0: {} copy-to-clipboard@3.3.3: @@ -13153,7 +13075,7 @@ snapshots: dagre-d3-es@7.0.14: dependencies: d3: 7.9.0 - lodash-es: 4.17.23 + lodash-es: 4.18.0 dayjs@1.11.20: {} @@ -13190,16 +13112,12 @@ snapshots: define-lazy-prop@3.0.0: {} - defu@6.1.4: {} - delaunator@5.1.0: dependencies: robust-predicates: 3.0.3 dequal@2.0.3: {} - destr@2.0.5: {} - detect-libc@2.1.2: {} detect-node-es@1.1.0: {} @@ -13381,7 +13299,7 @@ snapshots: '@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: + eslint-flat-config-utils@3.1.0: dependencies: '@eslint/config-helpers': 0.5.3 pathe: 2.0.3 @@ -13410,30 +13328,30 @@ snapshots: dependencies: eslint: 10.1.0(jiti@2.6.1) - 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): + eslint-plugin-better-tailwindcss@4.3.2(eslint@10.1.0(jiti@2.6.1))(oxlint@1.58.0(oxlint-tsgolint@0.18.1))(tailwindcss@4.2.2)(typescript@6.0.2): dependencies: '@eslint/css-tree': 3.6.9 - '@valibot/to-json-schema': 1.6.0(valibot@1.3.1(typescript@5.9.3)) + '@valibot/to-json-schema': 1.6.0(valibot@1.3.1(typescript@6.0.2)) enhanced-resolve: 5.20.1 jiti: 2.6.1 synckit: 0.11.12 tailwind-csstree: 0.1.5 tailwindcss: 4.2.2 tsconfig-paths-webpack-plugin: 4.2.0 - valibot: 1.3.1(typescript@5.9.3) + valibot: 1.3.1(typescript@6.0.2) optionalDependencies: eslint: 10.1.0(jiti@2.6.1) - oxlint: 1.57.0(oxlint-tsgolint@0.17.3) + oxlint: 1.58.0(oxlint-tsgolint@0.18.1) transitivePeerDependencies: - '@eslint/css' - typescript - 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-command@3.5.2(@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(@typescript-eslint/typescript-estree@8.58.0(typescript@6.0.2))(@typescript-eslint/utils@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(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@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) + '@typescript-eslint/rule-tester': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) eslint: 10.1.0(jiti@2.6.1) eslint-plugin-depend@1.5.0(eslint@10.1.0(jiti@2.6.1)): @@ -13454,7 +13372,7 @@ snapshots: dependencies: 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-import-lite@0.6.0(eslint@10.1.0(jiti@2.6.1)): dependencies: eslint: 10.1.0(jiti@2.6.1) @@ -13493,9 +13411,9 @@ snapshots: transitivePeerDependencies: - '@eslint/json' - eslint-plugin-markdown-preferences@0.40.3(@eslint/markdown@7.5.1)(eslint@10.1.0(jiti@2.6.1)): + eslint-plugin-markdown-preferences@0.41.0(@eslint/markdown@8.0.1)(eslint@10.1.0(jiti@2.6.1)): dependencies: - '@eslint/markdown': 7.5.1 + '@eslint/markdown': 8.0.1 diff-sequences: 29.6.3 emoji-regex-xs: 2.0.1 eslint: 10.1.0(jiti@2.6.1) @@ -13513,7 +13431,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-n@17.24.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-n@17.24.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) enhanced-resolve: 5.20.1 @@ -13524,13 +13442,13 @@ snapshots: globrex: 0.1.2 ignore: 5.3.2 semver: 7.7.4 - ts-declaration-location: 1.0.7(typescript@5.9.3) + ts-declaration-location: 1.0.7(typescript@6.0.2) transitivePeerDependencies: - typescript - eslint-plugin-no-barrel-files@1.2.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-no-barrel-files@1.2.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2): dependencies: - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) transitivePeerDependencies: - eslint - supports-color @@ -13538,9 +13456,9 @@ snapshots: eslint-plugin-no-only-tests@3.3.0: {} - eslint-plugin-perfectionist@5.7.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-perfectionist@5.7.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2): dependencies: - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) eslint: 10.1.0(jiti@2.6.1) natural-orderby: 5.0.0 transitivePeerDependencies: @@ -13558,48 +13476,37 @@ snapshots: yaml: 2.8.3 yaml-eslint-parser: 2.0.0 - eslint-plugin-react-dom@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-dom@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2): dependencies: - '@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@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/core': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) compare-versions: 6.1.1 eslint: 10.1.0(jiti@2.6.1) ts-pattern: 5.9.0 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - eslint-plugin-react-hooks@7.0.1(eslint@10.1.0(jiti@2.6.1)): + eslint-plugin-react-naming-convention@3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2): dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.2 - 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@2.6.1))(typescript@5.9.3): - dependencies: - '@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@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/core': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/type-utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) compare-versions: 6.1.1 eslint: 10.1.0(jiti@2.6.1) string-ts: 2.3.1 ts-pattern: 5.9.0 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color @@ -13607,53 +13514,53 @@ snapshots: dependencies: eslint: 10.1.0(jiti@2.6.1) - eslint-plugin-react-rsc@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@6.0.2): dependencies: - '@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@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/type-utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) eslint: 10.1.0(jiti@2.6.1) ts-pattern: 5.9.0 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - eslint-plugin-react-web-api@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@6.0.2): dependencies: - '@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@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/core': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) birecord: 0.1.1 eslint: 10.1.0(jiti@2.6.1) ts-pattern: 5.9.0 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - eslint-plugin-react-x@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@6.0.2): dependencies: - '@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@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/core': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/type-utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) compare-versions: 6.1.1 eslint: 10.1.0(jiti@2.6.1) string-ts: 2.3.1 - ts-api-utils: 2.5.0(typescript@5.9.3) + ts-api-utils: 2.5.0(typescript@6.0.2) ts-pattern: 5.9.0 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color @@ -13681,14 +13588,14 @@ snapshots: minimatch: 10.2.4 scslre: 0.3.0 semver: 7.7.4 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 - 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): + eslint-plugin-storybook@10.3.4(eslint@10.1.0(jiti@2.6.1))(storybook@10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2): dependencies: - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) 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) + storybook: 10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) transitivePeerDependencies: - supports-color - typescript @@ -13704,7 +13611,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-unicorn@63.0.0(eslint@10.1.0(jiti@2.6.1)): + eslint-plugin-unicorn@64.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@2.6.1)) @@ -13714,7 +13621,7 @@ snapshots: core-js-compat: 3.49.0 eslint: 10.1.0(jiti@2.6.1) find-up-simple: 1.0.1 - globals: 16.5.0 + globals: 17.4.0 indent-string: 5.0.0 is-builtin-module: 5.0.0 jsesc: 3.1.0 @@ -13724,13 +13631,13 @@ 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@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-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.1.0(jiti@2.6.1)): dependencies: 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@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) - 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-vue@10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.1.0(jiti@2.6.1)))(@typescript-eslint/parser@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(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@2.6.1)) eslint: 10.1.0(jiti@2.6.1) @@ -13742,7 +13649,7 @@ snapshots: xml-name-validator: 4.0.0 optionalDependencies: '@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) + '@typescript-eslint/parser': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) eslint-plugin-yml@3.3.1(eslint@10.1.0(jiti@2.6.1)): dependencies: @@ -13971,16 +13878,22 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-string-truncated-width@1.2.1: {} + + fast-string-width@1.1.0: + dependencies: + fast-string-truncated-width: 1.2.1 + fast-uri@3.1.0: {} + fast-wrap-ansi@0.1.6: + dependencies: + fast-string-width: 1.1.0 + fastq@1.20.1: dependencies: reusify: 1.1.0 - fault@1.0.4: - dependencies: - format: 0.2.2 - fault@2.0.1: dependencies: format: 0.2.2 @@ -14018,12 +13931,6 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 - fix-dts-default-cjs-exports@1.0.1: - dependencies: - magic-string: 0.30.21 - mlly: 1.8.2 - rollup: 4.59.0 - flat-cache@4.0.1: dependencies: flatted: 3.4.2 @@ -14057,8 +13964,6 @@ snapshots: functional-red-black-tree@1.0.1: {} - fzf@0.5.2: {} - gensync@1.0.0-beta.2: {} get-east-asian-width@1.5.0: {} @@ -14102,8 +14007,6 @@ snapshots: globals@15.15.0: {} - globals@16.5.0: {} - globals@17.4.0: {} globrex@0.1.2: {} @@ -14171,8 +14074,6 @@ snapshots: dependencies: '@types/hast': 3.0.4 - hast-util-parse-selector@2.2.5: {} - hast-util-parse-selector@4.0.0: dependencies: '@types/hast': 3.0.4 @@ -14220,6 +14121,20 @@ snapshots: transitivePeerDependencies: - supports-color + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + hast-util-to-jsx-runtime@2.3.6: dependencies: '@types/estree': 1.0.8 @@ -14261,14 +14176,6 @@ snapshots: dependencies: '@types/hast': 3.0.4 - hastscript@6.0.0: - dependencies: - '@types/hast': 2.3.10 - comma-separated-tokens: 1.0.8 - hast-util-parse-selector: 2.2.5 - property-information: 5.6.0 - space-separated-tokens: 1.1.5 - hastscript@9.0.1: dependencies: '@types/hast': 3.0.4 @@ -14277,19 +14184,9 @@ snapshots: property-information: 7.1.0 space-separated-tokens: 2.0.2 - hermes-estree@0.25.1: {} - - hermes-parser@0.25.1: - dependencies: - hermes-estree: 0.25.1 - hex-rgb@4.3.0: {} - highlight.js@10.7.3: {} - - highlightjs-vue@1.0.0: {} - - hono@4.12.9: {} + hono@4.12.10: {} hosted-git-info@9.0.2: dependencies: @@ -14320,11 +14217,11 @@ snapshots: dependencies: '@babel/runtime': 7.29.2 - i18next@25.10.10(typescript@5.9.3): + i18next@26.0.3(typescript@6.0.2): dependencies: '@babel/runtime': 7.29.2 optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.2 iconify-import-svg@0.1.2: dependencies: @@ -14353,7 +14250,8 @@ snapshots: immer@11.1.4: {} - immutable@5.1.5: {} + immutable@5.1.5: + optional: true import-fresh@3.3.1: dependencies: @@ -14384,15 +14282,8 @@ snapshots: intersection-observer@0.12.2: {} - is-alphabetical@1.0.4: {} - is-alphabetical@2.0.1: {} - is-alphanumerical@1.0.4: - dependencies: - is-alphabetical: 1.0.4 - is-decimal: 1.0.4 - is-alphanumerical@2.0.1: dependencies: is-alphabetical: 2.0.1 @@ -14402,8 +14293,6 @@ snapshots: dependencies: builtin-modules: 5.0.0 - is-decimal@1.0.4: {} - is-decimal@2.0.1: {} is-docker@3.0.0: {} @@ -14414,8 +14303,6 @@ snapshots: dependencies: is-extglob: 2.1.1 - is-hexadecimal@1.0.4: {} - is-hexadecimal@2.0.1: {} is-in-ssh@1.0.0: {} @@ -14477,8 +14364,6 @@ snapshots: '@types/react': 19.2.14 react: 19.2.4 - joycon@3.1.1: {} - js-audio-recorder@1.0.7: {} js-base64@3.7.8: {} @@ -14517,6 +14402,8 @@ snapshots: eslint-visitor-keys: 5.0.1 semver: 7.7.4 + jsonc-parser@3.3.1: {} + jsonfile@6.2.0: dependencies: universalify: 2.0.1 @@ -14537,7 +14424,7 @@ snapshots: khroma@2.1.0: {} - knip@6.1.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): + knip@6.2.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): dependencies: '@nodelib/fs.walk': 1.2.8 fast-glob: 3.3.3 @@ -14652,17 +14539,11 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 - lilconfig@3.1.3: {} - linebreak@1.1.0: dependencies: base64-js: 0.0.8 unicode-trie: 2.0.0 - lines-and-columns@1.2.4: {} - - load-tsconfig@0.2.5: {} - loader-runner@4.3.1: {} local-pkg@1.1.2: @@ -14675,7 +14556,7 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash-es@4.17.23: {} + lodash-es@4.18.0: {} lodash.merge@4.6.2: {} @@ -14683,7 +14564,7 @@ snapshots: lodash.sortby@4.7.0: {} - lodash@4.17.23: {} + lodash@4.18.0: {} longest-streak@3.1.0: {} @@ -14697,11 +14578,6 @@ snapshots: dependencies: tslib: 2.8.1 - lowlight@1.20.0: - dependencies: - fault: 1.0.4 - highlight.js: 10.7.3 - lru-cache@11.2.7: {} lru-cache@5.1.1: @@ -14947,17 +14823,15 @@ snapshots: mdn-data@2.23.0: {} - memoize-one@5.2.1: {} - merge-stream@2.0.0: {} merge2@1.4.1: {} - mermaid@11.13.0: + mermaid@11.14.0: dependencies: '@braintree/sanitize-url': 7.1.2 '@iconify/utils': 3.1.0 - '@mermaid-js/parser': 1.0.1 + '@mermaid-js/parser': 1.1.0 '@types/d3': 7.4.3 '@upsetjs/venn.js': 2.0.0 cytoscape: 3.33.1 @@ -14970,7 +14844,7 @@ snapshots: dompurify: 3.3.2 katex: 0.16.44 khroma: 2.1.0 - lodash-es: 4.17.23 + lodash-es: 4.18.0 marked: 16.4.2 roughjs: 4.6.6 stylis: 4.3.6 @@ -15283,8 +15157,6 @@ snapshots: mime@4.1.0: {} - mimic-function@5.0.1: {} - mimic-response@3.1.0: optional: true @@ -15296,7 +15168,7 @@ snapshots: minimatch@3.1.5: dependencies: - brace-expansion: 2.0.2 + brace-expansion: 1.1.13 minimist@1.2.8: {} @@ -15361,9 +15233,9 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - 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): + next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0): dependencies: - '@next/env': 16.2.1 + '@next/env': 16.2.2 '@swc/helpers': 0.5.15 baseline-browser-mapping: 2.10.12 caniuse-lite: 1.0.30001781 @@ -15372,15 +15244,15 @@ snapshots: react-dom: 19.2.4(react@19.2.4) styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.4) optionalDependencies: - '@next/swc-darwin-arm64': 16.2.1 - '@next/swc-darwin-x64': 16.2.1 - '@next/swc-linux-arm64-gnu': 16.2.1 - '@next/swc-linux-arm64-musl': 16.2.1 - '@next/swc-linux-x64-gnu': 16.2.1 - '@next/swc-linux-x64-musl': 16.2.1 - '@next/swc-win32-arm64-msvc': 16.2.1 - '@next/swc-win32-x64-msvc': 16.2.1 - '@playwright/test': 1.58.2 + '@next/swc-darwin-arm64': 16.2.2 + '@next/swc-darwin-x64': 16.2.2 + '@next/swc-linux-arm64-gnu': 16.2.2 + '@next/swc-linux-arm64-musl': 16.2.2 + '@next/swc-linux-x64-gnu': 16.2.2 + '@next/swc-linux-x64-musl': 16.2.2 + '@next/swc-win32-arm64-msvc': 16.2.2 + '@next/swc-win32-x64-msvc': 16.2.2 + '@playwright/test': 1.59.1 sass: 1.98.0 sharp: 0.34.5 transitivePeerDependencies: @@ -15400,8 +15272,6 @@ snapshots: node-addon-api@7.1.1: optional: true - node-fetch-native@1.6.7: {} - node-releases@2.0.36: {} normalize-package-data@8.0.0: @@ -15416,12 +15286,12 @@ snapshots: dependencies: boolbase: 1.0.0 - nuqs@2.8.9(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): + nuqs@2.8.9(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react@19.2.4): dependencies: '@standard-schema/spec': 1.0.0 react: 19.2.4 optionalDependencies: - 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) + next: 16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) object-assign@4.1.1: {} @@ -15429,21 +15299,19 @@ snapshots: obug@2.1.1: {} - ofetch@1.5.1: - dependencies: - destr: 2.0.5 - node-fetch-native: 1.6.7 - ufo: 1.6.3 - ohash@2.0.11: {} once@1.4.0: dependencies: wrappy: 1.0.2 - onetime@7.0.0: + oniguruma-parser@0.12.1: {} + + oniguruma-to-es@4.3.5: dependencies: - mimic-function: 5.0.1 + oniguruma-parser: 0.12.1 + regex: 6.1.0 + regex-recursion: 6.0.2 open@10.2.0: dependencies: @@ -15526,61 +15394,61 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - oxfmt@0.42.0: + oxfmt@0.43.0: dependencies: tinypool: 2.1.0 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.42.0 - '@oxfmt/binding-android-arm64': 0.42.0 - '@oxfmt/binding-darwin-arm64': 0.42.0 - '@oxfmt/binding-darwin-x64': 0.42.0 - '@oxfmt/binding-freebsd-x64': 0.42.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.42.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.42.0 - '@oxfmt/binding-linux-arm64-gnu': 0.42.0 - '@oxfmt/binding-linux-arm64-musl': 0.42.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.42.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.42.0 - '@oxfmt/binding-linux-riscv64-musl': 0.42.0 - '@oxfmt/binding-linux-s390x-gnu': 0.42.0 - '@oxfmt/binding-linux-x64-gnu': 0.42.0 - '@oxfmt/binding-linux-x64-musl': 0.42.0 - '@oxfmt/binding-openharmony-arm64': 0.42.0 - '@oxfmt/binding-win32-arm64-msvc': 0.42.0 - '@oxfmt/binding-win32-ia32-msvc': 0.42.0 - '@oxfmt/binding-win32-x64-msvc': 0.42.0 + '@oxfmt/binding-android-arm-eabi': 0.43.0 + '@oxfmt/binding-android-arm64': 0.43.0 + '@oxfmt/binding-darwin-arm64': 0.43.0 + '@oxfmt/binding-darwin-x64': 0.43.0 + '@oxfmt/binding-freebsd-x64': 0.43.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.43.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.43.0 + '@oxfmt/binding-linux-arm64-gnu': 0.43.0 + '@oxfmt/binding-linux-arm64-musl': 0.43.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.43.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.43.0 + '@oxfmt/binding-linux-riscv64-musl': 0.43.0 + '@oxfmt/binding-linux-s390x-gnu': 0.43.0 + '@oxfmt/binding-linux-x64-gnu': 0.43.0 + '@oxfmt/binding-linux-x64-musl': 0.43.0 + '@oxfmt/binding-openharmony-arm64': 0.43.0 + '@oxfmt/binding-win32-arm64-msvc': 0.43.0 + '@oxfmt/binding-win32-ia32-msvc': 0.43.0 + '@oxfmt/binding-win32-x64-msvc': 0.43.0 - oxlint-tsgolint@0.17.3: + oxlint-tsgolint@0.18.1: optionalDependencies: - '@oxlint-tsgolint/darwin-arm64': 0.17.3 - '@oxlint-tsgolint/darwin-x64': 0.17.3 - '@oxlint-tsgolint/linux-arm64': 0.17.3 - '@oxlint-tsgolint/linux-x64': 0.17.3 - '@oxlint-tsgolint/win32-arm64': 0.17.3 - '@oxlint-tsgolint/win32-x64': 0.17.3 + '@oxlint-tsgolint/darwin-arm64': 0.18.1 + '@oxlint-tsgolint/darwin-x64': 0.18.1 + '@oxlint-tsgolint/linux-arm64': 0.18.1 + '@oxlint-tsgolint/linux-x64': 0.18.1 + '@oxlint-tsgolint/win32-arm64': 0.18.1 + '@oxlint-tsgolint/win32-x64': 0.18.1 - oxlint@1.57.0(oxlint-tsgolint@0.17.3): + oxlint@1.58.0(oxlint-tsgolint@0.18.1): optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.57.0 - '@oxlint/binding-android-arm64': 1.57.0 - '@oxlint/binding-darwin-arm64': 1.57.0 - '@oxlint/binding-darwin-x64': 1.57.0 - '@oxlint/binding-freebsd-x64': 1.57.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.57.0 - '@oxlint/binding-linux-arm-musleabihf': 1.57.0 - '@oxlint/binding-linux-arm64-gnu': 1.57.0 - '@oxlint/binding-linux-arm64-musl': 1.57.0 - '@oxlint/binding-linux-ppc64-gnu': 1.57.0 - '@oxlint/binding-linux-riscv64-gnu': 1.57.0 - '@oxlint/binding-linux-riscv64-musl': 1.57.0 - '@oxlint/binding-linux-s390x-gnu': 1.57.0 - '@oxlint/binding-linux-x64-gnu': 1.57.0 - '@oxlint/binding-linux-x64-musl': 1.57.0 - '@oxlint/binding-openharmony-arm64': 1.57.0 - '@oxlint/binding-win32-arm64-msvc': 1.57.0 - '@oxlint/binding-win32-ia32-msvc': 1.57.0 - '@oxlint/binding-win32-x64-msvc': 1.57.0 - oxlint-tsgolint: 0.17.3 + '@oxlint/binding-android-arm-eabi': 1.58.0 + '@oxlint/binding-android-arm64': 1.58.0 + '@oxlint/binding-darwin-arm64': 1.58.0 + '@oxlint/binding-darwin-x64': 1.58.0 + '@oxlint/binding-freebsd-x64': 1.58.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.58.0 + '@oxlint/binding-linux-arm-musleabihf': 1.58.0 + '@oxlint/binding-linux-arm64-gnu': 1.58.0 + '@oxlint/binding-linux-arm64-musl': 1.58.0 + '@oxlint/binding-linux-ppc64-gnu': 1.58.0 + '@oxlint/binding-linux-riscv64-gnu': 1.58.0 + '@oxlint/binding-linux-riscv64-musl': 1.58.0 + '@oxlint/binding-linux-s390x-gnu': 1.58.0 + '@oxlint/binding-linux-x64-gnu': 1.58.0 + '@oxlint/binding-linux-x64-musl': 1.58.0 + '@oxlint/binding-openharmony-arm64': 1.58.0 + '@oxlint/binding-win32-arm64-msvc': 1.58.0 + '@oxlint/binding-win32-ia32-msvc': 1.58.0 + '@oxlint/binding-win32-x64-msvc': 1.58.0 + oxlint-tsgolint: 0.18.1 p-limit@3.1.0: dependencies: @@ -15613,15 +15481,6 @@ snapshots: color-name: 1.1.4 hex-rgb: 4.3.0 - parse-entities@2.0.0: - dependencies: - character-entities: 1.2.4 - character-entities-legacy: 1.1.4 - character-reference-invalid: 1.1.4 - is-alphanumerical: 1.0.4 - is-decimal: 1.0.4 - is-hexadecimal: 1.0.4 - parse-entities@4.0.2: dependencies: '@types/unist': 2.0.11 @@ -15708,8 +15567,6 @@ snapshots: pinyin-pro@3.28.0: {} - pirates@4.0.7: {} - pixelmatch@7.1.0: dependencies: pngjs: 7.0.0 @@ -15726,11 +15583,11 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 - playwright-core@1.58.2: {} + playwright-core@1.59.1: {} - playwright@1.58.2: + playwright@1.59.1: dependencies: - playwright-core: 1.58.2 + playwright-core: 1.59.1 optionalDependencies: fsevents: 2.3.2 @@ -15756,15 +15613,6 @@ snapshots: transitivePeerDependencies: - supports-color - 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 - optionalDependencies: - jiti: 2.6.1 - postcss: 8.5.8 - tsx: 4.21.0 - yaml: 2.8.3 - postcss-selector-parser@6.0.10: dependencies: cssesc: 3.0.0 @@ -15815,8 +15663,6 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 - prismjs@1.30.0: {} - progress@2.0.3: {} prop-types@15.8.1: @@ -15827,10 +15673,6 @@ snapshots: property-expr@2.0.6: {} - property-information@5.6.0: - dependencies: - xtend: 4.0.2 - property-information@7.1.0: {} pump@3.0.4: @@ -15850,8 +15692,6 @@ snapshots: quansync@0.2.11: {} - quansync@1.0.0: {} - queue-microtask@1.2.3: {} radash@12.1.1: {} @@ -15874,9 +15714,9 @@ snapshots: prop-types: 15.8.1 react: 19.2.4 - react-docgen-typescript@2.4.0(typescript@5.9.3): + react-docgen-typescript@2.4.0(typescript@6.0.2): dependencies: - typescript: 5.9.3 + typescript: 6.0.2 react-docgen@8.0.3: dependencies: @@ -15923,16 +15763,16 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - react-i18next@16.6.6(i18next@25.10.10(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3): + react-i18next@17.0.2(i18next@26.0.3(typescript@6.0.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@6.0.2): dependencies: '@babel/runtime': 7.29.2 html-parse-stringify: 3.0.1 - i18next: 25.10.10(typescript@5.9.3) + i18next: 26.0.3(typescript@6.0.2) react: 19.2.4 use-sync-external-store: 1.6.0(react@19.2.4) optionalDependencies: react-dom: 19.2.4(react@19.2.4) - typescript: 5.9.3 + typescript: 6.0.2 react-is@16.13.1: {} @@ -16009,16 +15849,6 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 - react-syntax-highlighter@15.6.6(react@19.2.4): - dependencies: - '@babel/runtime': 7.29.2 - highlight.js: 10.7.3 - highlightjs-vue: 1.0.0 - lowlight: 1.20.0 - prismjs: 1.30.0 - react: 19.2.4 - refractor: 3.6.0 - react-textarea-autosize@8.5.9(@types/react@19.2.14)(react@19.2.4): dependencies: '@babel/runtime': 7.29.2 @@ -16028,13 +15858,6 @@ snapshots: transitivePeerDependencies: - '@types/react' - react-window@1.8.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4): - dependencies: - '@babel/runtime': 7.29.2 - memoize-one: 5.2.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react@19.2.4: {} reactflow@11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): @@ -16072,7 +15895,8 @@ snapshots: util-deprecate: 1.0.2 optional: true - readdirp@4.1.2: {} + readdirp@4.1.2: + optional: true recast@0.23.11: dependencies: @@ -16122,11 +15946,15 @@ snapshots: reflect-metadata@0.2.2: {} - refractor@3.6.0: + regex-recursion@6.0.2: dependencies: - hastscript: 6.0.0 - parse-entities: 2.0.0 - prismjs: 1.30.0 + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 regexp-ast-analysis@0.7.1: dependencies: @@ -16255,8 +16083,6 @@ snapshots: resolve-from@4.0.0: {} - resolve-from@5.0.0: {} - resolve-pkg-maps@1.0.0: {} resolve@1.22.11: @@ -16265,11 +16091,6 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - restore-cursor@5.1.0: - dependencies: - onetime: 7.0.0 - signal-exit: 4.1.0 - reusify@1.1.0: {} robust-predicates@3.0.3: {} @@ -16358,6 +16179,7 @@ snapshots: source-map-js: 1.2.1 optionalDependencies: '@parcel/watcher': 2.5.6 + optional: true satori@0.16.0: dependencies: @@ -16443,7 +16265,16 @@ snapshots: shebang-regex@3.0.0: {} - signal-exit@4.1.0: {} + shiki@4.0.2: + dependencies: + '@shikijs/core': 4.0.2 + '@shikijs/engine-javascript': 4.0.2 + '@shikijs/engine-oniguruma': 4.0.2 + '@shikijs/langs': 4.0.2 + '@shikijs/themes': 4.0.2 + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 simple-concat@1.0.1: optional: true @@ -16486,8 +16317,6 @@ snapshots: source-map@0.7.6: {} - space-separated-tokens@1.1.5: {} - space-separated-tokens@2.0.2: {} spdx-correct@3.2.0: @@ -16519,7 +16348,7 @@ snapshots: std-semver@1.0.8: {} - storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + storybook@10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -16527,6 +16356,7 @@ snapshots: '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) '@vitest/expect': 3.2.4 '@vitest/spy': 3.2.4 + '@webcontainer/env': 1.1.1 esbuild: 0.27.2 open: 10.2.0 recast: 0.23.11 @@ -16546,7 +16376,7 @@ snapshots: hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 marked: 17.0.5 - mermaid: 11.13.0 + mermaid: 11.14.0 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) rehype-harden: 1.1.8 @@ -16626,16 +16456,6 @@ snapshots: stylis@4.3.6: {} - sucrase@3.35.1: - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - commander: 4.1.1 - lines-and-columns: 1.2.4 - mz: 2.7.0 - pirates: 4.0.7 - tinyglobby: 0.2.15 - ts-interface-checker: 0.1.13 - supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -16697,22 +16517,6 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - taze@19.10.0: - dependencies: - '@antfu/ni': 28.3.0 - '@henrygd/queue': 1.2.0 - cac: 7.0.0 - find-up-simple: 1.0.1 - ofetch: 1.5.1 - package-manager-detector: 1.6.0 - pathe: 2.0.3 - pnpm-workspace-yaml: 1.6.0 - restore-cursor: 5.1.0 - tinyexec: 1.0.4 - tinyglobby: 0.2.15 - unconfig: 7.5.0 - yaml: 2.8.3 - terser-webpack-plugin@5.4.0(esbuild@0.27.2)(uglify-js@3.19.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -16749,8 +16553,6 @@ snapshots: tinybench@2.9.0: {} - tinyexec@0.3.2: {} - tinyexec@1.0.4: {} tinyglobby@0.2.15: @@ -16791,32 +16593,28 @@ snapshots: totalist@3.0.1: {} - tree-kill@1.2.2: {} - trim-lines@3.0.1: {} trough@2.2.0: {} - ts-api-utils@2.5.0(typescript@5.9.3): + ts-api-utils@2.5.0(typescript@6.0.2): dependencies: - typescript: 5.9.3 + typescript: 6.0.2 ts-debounce@4.0.0: {} - ts-declaration-location@1.0.7(typescript@5.9.3): + ts-declaration-location@1.0.7(typescript@6.0.2): dependencies: picomatch: 4.0.4 - typescript: 5.9.3 + typescript: 6.0.2 ts-dedent@2.2.0: {} - ts-interface-checker@0.1.13: {} - ts-pattern@5.9.0: {} - tsconfck@3.1.6(typescript@5.9.3): + tsconfck@3.1.6(typescript@6.0.2): optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.2 tsconfig-paths-webpack-plugin@4.2.0: dependencies: @@ -16837,34 +16635,6 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3): - dependencies: - bundle-require: 5.1.0(esbuild@0.27.2) - cac: 6.7.14 - chokidar: 4.0.3 - consola: 3.4.2 - debug: 4.4.3(supports-color@8.1.1) - esbuild: 0.27.2 - fix-dts-default-cjs-exports: 1.0.1 - joycon: 3.1.1 - picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.3) - resolve-from: 5.0.0 - rollup: 4.59.0 - source-map: 0.7.6 - sucrase: 3.35.1 - tinyexec: 0.3.2 - tinyglobby: 0.2.15 - tree-kill: 1.2.2 - optionalDependencies: - postcss: 8.5.8 - typescript: 5.9.3 - transitivePeerDependencies: - - jiti - - supports-color - - tsx - - yaml - tsx@4.21.0: dependencies: esbuild: 0.27.2 @@ -16891,7 +16661,7 @@ snapshots: dependencies: tagged-tag: 1.0.0 - typescript@5.9.3: {} + typescript@6.0.2: {} ufo@1.6.3: {} @@ -16899,19 +16669,6 @@ snapshots: unbash@2.2.0: {} - unconfig-core@7.5.0: - dependencies: - '@quansync/fs': 1.0.0 - quansync: 1.0.0 - - unconfig@7.5.0: - dependencies: - '@quansync/fs': 1.0.0 - defu: 6.1.4 - jiti: 2.6.1 - quansync: 1.0.0 - unconfig-core: 7.5.0 - undici-types@7.18.2: {} undici@7.24.0: {} @@ -17053,9 +16810,9 @@ snapshots: uuid@13.0.0: {} - valibot@1.3.1(typescript@5.9.3): + valibot@1.3.1(typescript@6.0.2): optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.2 validate-npm-package-license@3.0.4: dependencies: @@ -17077,21 +16834,21 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vinext@0.0.38(21fde6c2677b0aab516df83ef1beed5d): + vinext@0.0.39(bf3f106951d7257e1336e695c204ec32): 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) + '@unpic/react': 1.0.2(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(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@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-react': 6.0.1(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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@2.6.1)(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.15(@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@6.0.2)(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@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) + vite-tsconfig-paths: 6.1.1(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3))(typescript@6.0.2) 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@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) + '@vitejs/plugin-rsc': 0.5.21(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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 @@ -17111,9 +16868,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@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-plugin-inspect@12.0.0-beta.1(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3))(typescript@6.0.2)(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@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) + '@vitejs/devtools-kit': 0.1.11(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3))(typescript@6.0.2)(ws@8.20.0) ansis: 4.2.0 error-stack-parser-es: 1.0.5 obug: 2.1.1 @@ -17122,46 +16879,47 @@ 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@2.6.1)(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.15(@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@6.0.2)(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@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): + vite-plugin-storybook-nextjs@3.2.4(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3))(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(storybook@10.3.4(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2): dependencies: '@next/env': 16.0.0 image-size: 2.0.2 magic-string: 0.30.21 module-alias: 2.3.4 - 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) + next: 16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) + storybook: 10.3.4(@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@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) + vite: '@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3)' + vite-tsconfig-paths: 5.1.4(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3))(typescript@6.0.2) 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@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): + vite-plus@0.1.15(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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@6.0.2)(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@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) + '@voidzero-dev/vite-plus-core': 0.1.15(@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@6.0.2)(yaml@2.8.3) + '@voidzero-dev/vite-plus-test': 0.1.15(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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@6.0.2)(yaml@2.8.3) cac: 7.0.0 cross-spawn: 7.0.6 - oxfmt: 0.42.0 - oxlint: 1.57.0(oxlint-tsgolint@0.17.3) - oxlint-tsgolint: 0.17.3 + jsonc-parser: 3.3.1 + oxfmt: 0.43.0 + oxlint: 1.58.0(oxlint-tsgolint@0.18.1) + oxlint-tsgolint: 0.18.1 picocolors: 1.1.1 optionalDependencies: - '@voidzero-dev/vite-plus-darwin-arm64': 0.1.14 - '@voidzero-dev/vite-plus-darwin-x64': 0.1.14 - '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.1.14 - '@voidzero-dev/vite-plus-linux-arm64-musl': 0.1.14 - '@voidzero-dev/vite-plus-linux-x64-gnu': 0.1.14 - '@voidzero-dev/vite-plus-linux-x64-musl': 0.1.14 - '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.1.14 - '@voidzero-dev/vite-plus-win32-x64-msvc': 0.1.14 + '@voidzero-dev/vite-plus-darwin-arm64': 0.1.15 + '@voidzero-dev/vite-plus-darwin-x64': 0.1.15 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.1.15 + '@voidzero-dev/vite-plus-linux-arm64-musl': 0.1.15 + '@voidzero-dev/vite-plus-linux-x64-gnu': 0.1.15 + '@voidzero-dev/vite-plus-linux-x64-musl': 0.1.15 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.1.15 + '@voidzero-dev/vite-plus-win32-x64-msvc': 0.1.15 transitivePeerDependencies: - '@arethetypeswrong/core' - '@edge-runtime/vm' @@ -17190,26 +16948,27 @@ snapshots: - vite - yaml - vite-plus@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): + vite-plus@0.1.15(@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@6.0.2)(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: '@oxc-project/types': 0.122.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) - '@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) + '@voidzero-dev/vite-plus-core': 0.1.15(@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@6.0.2)(yaml@2.8.3) + '@voidzero-dev/vite-plus-test': 0.1.15(@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@6.0.2)(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) cac: 7.0.0 cross-spawn: 7.0.6 - oxfmt: 0.42.0 - oxlint: 1.57.0(oxlint-tsgolint@0.17.3) - oxlint-tsgolint: 0.17.3 + jsonc-parser: 3.3.1 + oxfmt: 0.43.0 + oxlint: 1.58.0(oxlint-tsgolint@0.18.1) + oxlint-tsgolint: 0.18.1 picocolors: 1.1.1 optionalDependencies: - '@voidzero-dev/vite-plus-darwin-arm64': 0.1.14 - '@voidzero-dev/vite-plus-darwin-x64': 0.1.14 - '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.1.14 - '@voidzero-dev/vite-plus-linux-arm64-musl': 0.1.14 - '@voidzero-dev/vite-plus-linux-x64-gnu': 0.1.14 - '@voidzero-dev/vite-plus-linux-x64-musl': 0.1.14 - '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.1.14 - '@voidzero-dev/vite-plus-win32-x64-msvc': 0.1.14 + '@voidzero-dev/vite-plus-darwin-arm64': 0.1.15 + '@voidzero-dev/vite-plus-darwin-x64': 0.1.15 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.1.15 + '@voidzero-dev/vite-plus-linux-arm64-musl': 0.1.15 + '@voidzero-dev/vite-plus-linux-x64-gnu': 0.1.15 + '@voidzero-dev/vite-plus-linux-x64-musl': 0.1.15 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.1.15 + '@voidzero-dev/vite-plus-win32-x64-msvc': 0.1.15 transitivePeerDependencies: - '@arethetypeswrong/core' - '@edge-runtime/vm' @@ -17238,23 +16997,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@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): + vite-tsconfig-paths@5.1.4(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3))(typescript@6.0.2): dependencies: debug: 4.4.3(supports-color@8.1.1) globrex: 0.1.2 - tsconfck: 3.1.6(typescript@5.9.3) + tsconfck: 3.1.6(typescript@6.0.2) optionalDependencies: - 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: '@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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@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): + vite-tsconfig-paths@6.1.1(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3))(typescript@6.0.2): 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@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + tsconfck: 3.1.6(typescript@6.0.2) + vite: '@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: - supports-color - typescript @@ -17279,15 +17038,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@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.15(@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@6.0.2)(yaml@2.8.3)): optionalDependencies: - 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: '@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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)): + vitest-canvas-mock@1.1.4(@voidzero-dev/vite-plus-test@0.1.15(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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@6.0.2)(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@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: '@voidzero-dev/vite-plus-test@0.1.15(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.15(@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@6.0.2)(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@6.0.2)(yaml@2.8.3)' void-elements@3.1.0: {} @@ -17398,8 +17157,6 @@ snapshots: xmlbuilder@15.1.1: {} - xtend@4.0.2: {} - yallist@3.1.1: {} yallist@5.0.0: {} @@ -17437,10 +17194,6 @@ snapshots: zimmerframe@1.1.4: {} - zod-validation-error@4.0.2(zod@4.3.6): - dependencies: - zod: 4.3.6 - zod@4.3.6: {} zrender@6.0.0: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 236d6d7ade..585256b4a7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,9 @@ +catalogMode: prefer trustPolicy: no-downgrade -minimumReleaseAge: 2880 +trustPolicyExclude: + - chokidar@4.0.3 + - reselect@5.1.1 + - semver@6.3.1 blockExoticSubdeps: true strictDepBuilds: true allowBuilds: @@ -11,6 +15,7 @@ packages: - web - e2e - sdks/nodejs-client + - packages/* overrides: "@lexical/code": npm:lexical-code-no-prism@0.41.0 "@monaco-editor/loader": 1.7.0 @@ -22,7 +27,7 @@ overrides: array.prototype.flatmap: npm:@nolyfill/array.prototype.flatmap@^1.0.44 array.prototype.tosorted: npm:@nolyfill/array.prototype.tosorted@^1.0.44 assert: npm:@nolyfill/assert@^1.0.26 - brace-expansion@<2.0.2: 2.0.2 + brace-expansion@>=2.0.0 <2.0.3: 2.0.3 canvas: ^3.2.2 devalue@<5.3.2: 5.3.2 dompurify@>=3.1.3 <=3.3.1: 3.3.2 @@ -36,6 +41,8 @@ overrides: is-generator-function: npm:@nolyfill/is-generator-function@^1.0.44 is-typed-array: npm:@nolyfill/is-typed-array@^1.0.44 isarray: npm:@nolyfill/isarray@^1.0.44 + lodash@>=4.0.0 <= 4.17.23: 4.18.0 + lodash-es@>=4.0.0 <= 4.17.23: 4.18.0 object.assign: npm:@nolyfill/object.assign@^1.0.44 object.entries: npm:@nolyfill/object.entries@^1.0.44 object.fromentries: npm:@nolyfill/object.fromentries@^1.0.44 @@ -63,15 +70,15 @@ overrides: tar@<=7.5.10: 7.5.11 typed-array-buffer: npm:@nolyfill/typed-array-buffer@^1.0.44 undici@>=7.0.0 <7.24.0: 7.24.0 - vite: npm:@voidzero-dev/vite-plus-core@0.1.14 - vitest: npm:@voidzero-dev/vite-plus-test@0.1.14 + vite: npm:@voidzero-dev/vite-plus-core@0.1.15 + vitest: npm:@voidzero-dev/vite-plus-test@0.1.15 which-typed-array: npm:@nolyfill/which-typed-array@^1.0.44 yaml@>=2.0.0 <2.8.3: 2.8.3 yauzl@<3.2.1: 3.2.1 catalog: - "@amplitude/analytics-browser": 2.38.0 - "@amplitude/plugin-session-replay-browser": 1.27.5 - "@antfu/eslint-config": 7.7.3 + "@amplitude/analytics-browser": 2.38.1 + "@amplitude/plugin-session-replay-browser": 1.27.6 + "@antfu/eslint-config": 8.0.0 "@base-ui/react": 1.3.0 "@chromatic-com/storybook": 5.1.1 "@cucumber/cucumber": 12.7.0 @@ -83,7 +90,7 @@ catalog: "@formatjs/intl-localematcher": 0.8.2 "@headlessui/react": 2.2.9 "@heroicons/react": 2.2.0 - "@hono/node-server": 1.19.11 + "@hono/node-server": 1.19.12 "@iconify-json/heroicons": 1.2.3 "@iconify-json/ri": 1.2.10 "@lexical/code": 0.42.0 @@ -97,34 +104,35 @@ catalog: "@mdx-js/react": 3.1.1 "@mdx-js/rollup": 3.1.1 "@monaco-editor/react": 4.7.0 - "@next/eslint-plugin-next": 16.2.1 - "@next/mdx": 16.2.1 + "@next/eslint-plugin-next": 16.2.2 + "@next/mdx": 16.2.2 "@orpc/client": 1.13.13 "@orpc/contract": 1.13.13 "@orpc/openapi-client": 1.13.13 "@orpc/tanstack-query": 1.13.13 - "@playwright/test": 1.58.2 + "@playwright/test": 1.59.1 "@remixicon/react": 4.9.0 "@rgrove/parse-xml": 4.2.0 - "@sentry/react": 10.46.0 - "@storybook/addon-docs": 10.3.3 - "@storybook/addon-links": 10.3.3 - "@storybook/addon-onboarding": 10.3.3 - "@storybook/addon-themes": 10.3.3 - "@storybook/nextjs-vite": 10.3.3 - "@storybook/react": 10.3.3 + "@sentry/react": 10.47.0 + "@storybook/addon-docs": 10.3.4 + "@storybook/addon-links": 10.3.4 + "@storybook/addon-onboarding": 10.3.4 + "@storybook/addon-themes": 10.3.4 + "@storybook/nextjs-vite": 10.3.4 + "@storybook/react": 10.3.4 "@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 - "@tanstack/react-form-devtools": 0.2.19 - "@tanstack/react-query": 5.95.2 - "@tanstack/react-query-devtools": 5.95.2 + "@tanstack/eslint-plugin-query": 5.96.1 + "@tanstack/react-devtools": 0.10.1 + "@tanstack/react-form": 1.28.6 + "@tanstack/react-form-devtools": 0.2.20 + "@tanstack/react-query": 5.96.1 + "@tanstack/react-query-devtools": 5.96.1 + "@tanstack/react-virtual": 3.13.23 "@testing-library/dom": 10.4.1 "@testing-library/jest-dom": 6.9.1 "@testing-library/react": 16.3.2 @@ -140,15 +148,13 @@ catalog: "@types/qs": 6.15.0 "@types/react": 19.2.14 "@types/react-dom": 19.2.3 - "@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/parser": 8.57.2 - "@typescript/native-preview": 7.0.0-dev.20260329.1 + "@typescript-eslint/eslint-plugin": 8.58.0 + "@typescript-eslint/parser": 8.58.0 + "@typescript/native-preview": 7.0.0-dev.20260401.1 "@vitejs/plugin-react": 6.0.1 "@vitejs/plugin-rsc": 0.5.21 - "@vitest/coverage-v8": 4.1.1 + "@vitest/coverage-v8": 4.1.2 abcjs: 6.6.2 agentation: 3.0.2 ahooks: 3.9.7 @@ -156,7 +162,7 @@ catalog: class-variance-authority: 0.7.1 clsx: 2.1.1 cmdk: 1.1.1 - code-inspector-plugin: 1.4.5 + code-inspector-plugin: 1.5.1 copy-to-clipboard: 3.3.3 cron-parser: 5.5.0 dayjs: 1.11.20 @@ -173,19 +179,19 @@ catalog: eslint-markdown: 0.6.0 eslint-plugin-better-tailwindcss: 4.3.2 eslint-plugin-hyoban: 0.14.1 - eslint-plugin-markdown-preferences: 0.40.3 + eslint-plugin-markdown-preferences: 0.41.0 eslint-plugin-no-barrel-files: 1.2.2 - eslint-plugin-react-hooks: 7.0.1 eslint-plugin-react-refresh: 0.5.2 eslint-plugin-sonarjs: 4.0.2 - eslint-plugin-storybook: 10.3.3 + eslint-plugin-storybook: 10.3.4 fast-deep-equal: 3.1.3 foxact: 0.3.0 happy-dom: 20.8.9 - hono: 4.12.9 + hast-util-to-jsx-runtime: 2.3.6 + hono: 4.12.10 html-entities: 2.6.0 html-to-image: 1.11.13 - i18next: 25.10.10 + i18next: 26.0.3 i18next-resources-to-backend: 1.2.1 iconify-import-svg: 0.1.2 immer: 11.1.4 @@ -195,15 +201,15 @@ catalog: js-yaml: 4.1.1 jsonschema: 1.5.0 katex: 0.16.44 - knip: 6.1.0 + knip: 6.2.0 ky: 1.14.3 lamejs: 1.2.1 lexical: 0.42.0 - mermaid: 11.13.0 + mermaid: 11.14.0 mime: 4.1.0 mitt: 3.0.1 negotiator: 1.0.0 - next: 16.2.1 + next: 16.2.2 next-themes: 0.4.6 nuqs: 2.8.9 pinyin-pro: 3.28.0 @@ -216,42 +222,39 @@ catalog: react-dom: 19.2.4 react-easy-crop: 5.5.7 react-hotkeys-hook: 5.2.4 - react-i18next: 16.6.6 + react-i18next: 17.0.2 react-multi-email: 1.0.25 react-papaparse: 4.4.0 react-pdf-highlighter: 8.0.0-rc.0 react-server-dom-webpack: 19.2.4 react-sortablejs: 6.1.4 - react-syntax-highlighter: 15.6.6 react-textarea-autosize: 8.5.9 - react-window: 1.8.11 reactflow: 11.11.4 remark-breaks: 4.0.0 remark-directive: 4.0.0 - sass: 1.98.0 scheduler: 0.27.0 sharp: 0.34.5 + shiki: 4.0.2 sortablejs: 1.15.7 std-semver: 1.0.8 - storybook: 10.3.3 + storybook: 10.3.4 streamdown: 2.5.0 string-ts: 2.3.1 tailwind-merge: 3.5.0 tailwindcss: 4.2.2 - taze: 19.10.0 tldts: 7.0.27 - tsup: ^8.5.1 + tsdown: 0.21.7 tsx: 4.21.0 - typescript: 5.9.3 + typescript: 6.0.2 uglify-js: 3.19.3 unist-util-visit: 5.1.0 use-context-selector: 2.0.0 uuid: 13.0.0 - vinext: 0.0.38 - vite: npm:@voidzero-dev/vite-plus-core@0.1.14 + vinext: 0.0.39 + vite: npm:@voidzero-dev/vite-plus-core@0.1.15 vite-plugin-inspect: 12.0.0-beta.1 - vite-plus: 0.1.14 - vitest: npm:@voidzero-dev/vite-plus-test@0.1.14 + vite-plus: 0.1.15 + vitest: npm:@voidzero-dev/vite-plus-test@0.1.15 vitest-canvas-mock: 1.1.4 zod: 4.3.6 zundo: 2.3.0 diff --git a/sdks/nodejs-client/package.json b/sdks/nodejs-client/package.json index d487c3abb3..da9f7353ac 100644 --- a/sdks/nodejs-client/package.json +++ b/sdks/nodejs-client/package.json @@ -45,12 +45,12 @@ "homepage": "https://dify.ai", "license": "MIT", "scripts": { - "build": "tsup", + "build": "vp pack", "lint": "eslint", "lint:fix": "eslint --fix", "type-check": "tsc -p tsconfig.json --noEmit", - "test": "vitest run", - "test:coverage": "vitest run --coverage", + "test": "vp test", + "test:coverage": "vp test --coverage", "publish:check": "./scripts/publish.sh --dry-run", "publish:npm": "./scripts/publish.sh" }, @@ -61,8 +61,8 @@ "@typescript-eslint/parser": "catalog:", "@vitest/coverage-v8": "catalog:", "eslint": "catalog:", - "tsup": "catalog:", "typescript": "catalog:", + "vite-plus": "catalog:", "vitest": "catalog:" } } diff --git a/sdks/nodejs-client/tsconfig.json b/sdks/nodejs-client/tsconfig.json index f6fb5e0555..46055447be 100644 --- a/sdks/nodejs-client/tsconfig.json +++ b/sdks/nodejs-client/tsconfig.json @@ -11,7 +11,8 @@ "strict": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, - "skipLibCheck": true + "skipLibCheck": true, + "types": ["node"] }, "include": ["src/**/*.ts", "tests/**/*.ts"] } diff --git a/sdks/nodejs-client/tsup.config.ts b/sdks/nodejs-client/tsup.config.ts deleted file mode 100644 index 522382c2a5..0000000000 --- a/sdks/nodejs-client/tsup.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from "tsup"; - -export default defineConfig({ - entry: ["src/index.ts"], - format: ["esm"], - dts: true, - clean: true, - sourcemap: true, - splitting: false, - treeshake: true, - outDir: "dist", -}); diff --git a/sdks/nodejs-client/vitest.config.ts b/sdks/nodejs-client/vite.config.ts similarity index 53% rename from sdks/nodejs-client/vitest.config.ts rename to sdks/nodejs-client/vite.config.ts index c3132e9ecf..8d89508682 100644 --- a/sdks/nodejs-client/vitest.config.ts +++ b/sdks/nodejs-client/vite.config.ts @@ -1,6 +1,17 @@ -import { defineConfig } from "vitest/config"; +import { defineConfig } from "vite-plus"; export default defineConfig({ + pack: { + entry: ["src/index.ts"], + format: ["esm"], + dts: true, + clean: true, + sourcemap: true, + // splitting: false, + treeshake: true, + outDir: "dist", + target: false, + }, test: { environment: "node", include: ["**/*.test.ts"], diff --git a/taze.config.js b/taze.config.js deleted file mode 100644 index cd5a9f8656..0000000000 --- a/taze.config.js +++ /dev/null @@ -1,15 +0,0 @@ -import { defineConfig } from 'taze' - -export default defineConfig({ - exclude: [ - // We are going to replace these - 'react-syntax-highlighter', - 'react-window', - '@types/react-window', - - // We can not upgrade these yet - 'typescript', - ], - - maturityPeriod: 2, -}) diff --git a/web/__mocks__/@tanstack/react-virtual.ts b/web/__mocks__/@tanstack/react-virtual.ts new file mode 100644 index 0000000000..59cca5e33f --- /dev/null +++ b/web/__mocks__/@tanstack/react-virtual.ts @@ -0,0 +1,36 @@ +import { vi } from 'vitest' + +const mockVirtualizer = ({ + count, + estimateSize, +}: { + count: number + estimateSize?: (index: number) => number +}) => { + const getSize = (index: number) => estimateSize?.(index) ?? 0 + + return { + getTotalSize: () => Array.from({ length: count }).reduce((total, _, index) => total + getSize(index), 0), + getVirtualItems: () => { + let start = 0 + + return Array.from({ length: count }).map((_, index) => { + const size = getSize(index) + const virtualItem = { + end: start + size, + index, + key: index, + size, + start, + } + + start += size + return virtualItem + }) + }, + measureElement: vi.fn(), + scrollToIndex: vi.fn(), + } +} + +export { mockVirtualizer as useVirtualizer } diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx index 0c87fd1a4d..d3f15bdf46 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx @@ -35,7 +35,7 @@ const TagManagementModal = dynamic(() => import('@/app/components/base/tag-manag ssr: false, }) -export type IAppDetailLayoutProps = { +type IAppDetailLayoutProps = { children: React.ReactNode appId: string } diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx index 26373bd42a..fb2edf0102 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx @@ -25,7 +25,7 @@ import { useAppWorkflow } from '@/service/use-workflow' import { AppModeEnum } from '@/types/app' import { asyncRunSafe } from '@/utils' -export type ICardViewProps = { +type ICardViewProps = { appId: string isInPanel?: boolean className?: string diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chart-view.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chart-view.tsx index b6e902f456..0d33de2972 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chart-view.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chart-view.tsx @@ -27,7 +27,7 @@ const TIME_PERIOD_MAPPING: { value: number, name: TimePeriodName }[] = [ const queryDateFormat = 'YYYY-MM-DD HH:mm' -export type IChartViewProps = { +type IChartViewProps = { appId: string headerRight: React.ReactNode } diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/style.module.css b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/style.module.css index 1f1aca2d11..45c7d197b4 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/style.module.css +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/style.module.css @@ -1,5 +1,3 @@ -@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 deleted file mode 100644 index 955f5d593b..0000000000 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/documents/style.module.css +++ /dev/null @@ -1,11 +0,0 @@ -@reference "../../../../../styles/globals.css"; - -.logTable td { - padding: 7px 8px; - box-sizing: border-box; - max-width: 200px; -} - -.pagination li { - list-style: none; -} diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx index 730b76ee19..092e47278f 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx @@ -26,7 +26,7 @@ import { usePathname } from '@/next/navigation' import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset' import { cn } from '@/utils/classnames' -export type IAppDetailLayoutProps = { +type IAppDetailLayoutProps = { children: React.ReactNode datasetId: string } diff --git a/web/app/account/(commonLayout)/avatar.tsx b/web/app/account/(commonLayout)/avatar.tsx index 5461ce739c..36a510cf63 100644 --- a/web/app/account/(commonLayout)/avatar.tsx +++ b/web/app/account/(commonLayout)/avatar.tsx @@ -13,10 +13,6 @@ import { useProviderContext } from '@/context/provider-context' import { useRouter } from '@/next/navigation' import { useLogout, useUserProfile } from '@/service/use-common' -export type IAppSelector = { - isMobile: boolean -} - export default function AppSelector() { const router = useRouter() const { t } = useTranslation() diff --git a/web/app/components/app-sidebar/app-info/index.tsx b/web/app/components/app-sidebar/app-info/index.tsx index 2530add2dc..a0628ec786 100644 --- a/web/app/components/app-sidebar/app-info/index.tsx +++ b/web/app/components/app-sidebar/app-info/index.tsx @@ -5,7 +5,7 @@ import AppInfoModals from './app-info-modals' import AppInfoTrigger from './app-info-trigger' import { useAppInfoActions } from './use-app-info-actions' -export type IAppInfoProps = { +type IAppInfoProps = { expand: boolean onlyShowDetail?: boolean openState?: boolean diff --git a/web/app/components/app-sidebar/basic.tsx b/web/app/components/app-sidebar/basic.tsx index 24746aa687..29a08f8a01 100644 --- a/web/app/components/app-sidebar/basic.tsx +++ b/web/app/components/app-sidebar/basic.tsx @@ -7,7 +7,7 @@ import { import Tooltip from '@/app/components/base/tooltip' import AppIcon from '../base/app-icon' -export type IAppBasicProps = { +type IAppBasicProps = { iconType?: 'app' | 'api' | 'dataset' | 'webapp' | 'notion' icon?: string icon_background?: string | null diff --git a/web/app/components/app-sidebar/index.tsx b/web/app/components/app-sidebar/index.tsx index bbf71c6cf9..f86cd617e3 100644 --- a/web/app/components/app-sidebar/index.tsx +++ b/web/app/components/app-sidebar/index.tsx @@ -17,7 +17,7 @@ import DatasetSidebarDropdown from './dataset-sidebar-dropdown' import NavLink from './nav-link' import ToggleButton from './toggle-button' -export type IAppDetailNavProps = { +type IAppDetailNavProps = { iconType?: 'app' | 'dataset' navigation: Array<{ name: string diff --git a/web/app/components/app/__tests__/store.spec.ts b/web/app/components/app/__tests__/store.spec.ts new file mode 100644 index 0000000000..204d659fdd --- /dev/null +++ b/web/app/components/app/__tests__/store.spec.ts @@ -0,0 +1,69 @@ +import { useStore } from '../store' + +const resetStore = () => { + useStore.setState({ + appDetail: undefined, + appSidebarExpand: '', + currentLogItem: undefined, + currentLogModalActiveTab: 'DETAIL', + showPromptLogModal: false, + showAgentLogModal: false, + showMessageLogModal: false, + showAppConfigureFeaturesModal: false, + }) +} + +describe('app store', () => { + beforeEach(() => { + resetStore() + }) + + it('should expose the default state', () => { + expect(useStore.getState()).toEqual(expect.objectContaining({ + appDetail: undefined, + appSidebarExpand: '', + currentLogItem: undefined, + currentLogModalActiveTab: 'DETAIL', + showPromptLogModal: false, + showAgentLogModal: false, + showMessageLogModal: false, + showAppConfigureFeaturesModal: false, + })) + }) + + it('should update every mutable field through its actions', () => { + const appDetail = { id: 'app-1' } as ReturnType['appDetail'] + const currentLogItem = { id: 'message-1' } as ReturnType['currentLogItem'] + + useStore.getState().setAppDetail(appDetail) + useStore.getState().setAppSidebarExpand('logs') + useStore.getState().setCurrentLogItem(currentLogItem) + useStore.getState().setCurrentLogModalActiveTab('MESSAGE') + useStore.getState().setShowPromptLogModal(true) + useStore.getState().setShowAgentLogModal(true) + useStore.getState().setShowAppConfigureFeaturesModal(true) + + expect(useStore.getState()).toEqual(expect.objectContaining({ + appDetail, + appSidebarExpand: 'logs', + currentLogItem, + currentLogModalActiveTab: 'MESSAGE', + showPromptLogModal: true, + showAgentLogModal: true, + showAppConfigureFeaturesModal: true, + })) + }) + + it('should reset the active tab when the message log modal closes', () => { + useStore.getState().setCurrentLogModalActiveTab('TRACE') + useStore.getState().setShowMessageLogModal(true) + + expect(useStore.getState().showMessageLogModal).toBe(true) + expect(useStore.getState().currentLogModalActiveTab).toBe('TRACE') + + useStore.getState().setShowMessageLogModal(false) + + expect(useStore.getState().showMessageLogModal).toBe(false) + expect(useStore.getState().currentLogModalActiveTab).toBe('DETAIL') + }) +}) diff --git a/web/app/components/app/annotation/batch-action.spec.tsx b/web/app/components/app/annotation/__tests__/batch-action.spec.tsx similarity index 96% rename from web/app/components/app/annotation/batch-action.spec.tsx rename to web/app/components/app/annotation/__tests__/batch-action.spec.tsx index 8d56dde14a..95dddd4b23 100644 --- a/web/app/components/app/annotation/batch-action.spec.tsx +++ b/web/app/components/app/annotation/__tests__/batch-action.spec.tsx @@ -1,6 +1,6 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import * as React from 'react' -import BatchAction from './batch-action' +import BatchAction from '../batch-action' describe('BatchAction', () => { const baseProps = { diff --git a/web/app/components/app/annotation/empty-element.spec.tsx b/web/app/components/app/annotation/__tests__/empty-element.spec.tsx similarity index 91% rename from web/app/components/app/annotation/empty-element.spec.tsx rename to web/app/components/app/annotation/__tests__/empty-element.spec.tsx index 89ba7e9ff8..7dcf902a9e 100644 --- a/web/app/components/app/annotation/empty-element.spec.tsx +++ b/web/app/components/app/annotation/__tests__/empty-element.spec.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react' import * as React from 'react' -import EmptyElement from './empty-element' +import EmptyElement from '../empty-element' describe('EmptyElement', () => { it('should render the empty state copy and supporting icon', () => { diff --git a/web/app/components/app/annotation/filter.spec.tsx b/web/app/components/app/annotation/__tests__/filter.spec.tsx similarity index 99% rename from web/app/components/app/annotation/filter.spec.tsx rename to web/app/components/app/annotation/__tests__/filter.spec.tsx index 7bb39bd444..8b69494e3f 100644 --- a/web/app/components/app/annotation/filter.spec.tsx +++ b/web/app/components/app/annotation/__tests__/filter.spec.tsx @@ -1,12 +1,12 @@ import type { UseQueryResult } from '@tanstack/react-query' import type { Mock } from 'vitest' -import type { QueryParam } from './filter' +import type { QueryParam } from '../filter' import type { AnnotationsCountResponse } from '@/models/log' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' import * as useLogModule from '@/service/use-log' -import Filter from './filter' +import Filter from '../filter' vi.mock('@/service/use-log') diff --git a/web/app/components/app/annotation/index.spec.tsx b/web/app/components/app/annotation/__tests__/index.spec.tsx similarity index 53% rename from web/app/components/app/annotation/index.spec.tsx rename to web/app/components/app/annotation/__tests__/index.spec.tsx index 5f5e9f74c0..cd9b127c7f 100644 --- a/web/app/components/app/annotation/index.spec.tsx +++ b/web/app/components/app/annotation/__tests__/index.spec.tsx @@ -1,5 +1,6 @@ +/* eslint-disable ts/no-explicit-any */ import type { Mock } from 'vitest' -import type { AnnotationItem } from './type' +import type { AnnotationItem } from '../type' import type { App } from '@/types/app' import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import * as React from 'react' @@ -9,13 +10,16 @@ import { addAnnotation, delAnnotation, delAnnotations, + editAnnotation, fetchAnnotationConfig, fetchAnnotationList, queryAnnotationJobStatus, + updateAnnotationScore, + updateAnnotationStatus, } from '@/service/annotation' import { AppModeEnum } from '@/types/app' -import Annotation from './index' -import { JobStatus } from './type' +import Annotation from '../index' +import { AnnotationEnableStatus, JobStatus } from '../type' vi.mock('ahooks', () => ({ useDebounce: (value: any) => value, @@ -37,29 +41,32 @@ vi.mock('@/context/provider-context', () => ({ useProviderContext: vi.fn(), })) -vi.mock('./filter', () => ({ +vi.mock('../filter', () => ({ default: ({ children }: { children: React.ReactNode }) => (
{children}
), })) -vi.mock('./empty-element', () => ({ +vi.mock('../empty-element', () => ({ default: () =>
, })) -vi.mock('./header-opts', () => ({ +vi.mock('../header-opts', () => ({ default: (props: any) => (
+
), })) let latestListProps: any -vi.mock('./list', () => ({ +vi.mock('../list', () => ({ default: (props: any) => { latestListProps = props if (!props.list.length) @@ -74,7 +81,7 @@ vi.mock('./list', () => ({ }, })) -vi.mock('./view-annotation-modal', () => ({ +vi.mock('../view-annotation-modal', () => ({ default: (props: any) => { if (!props.isShow) return null @@ -82,14 +89,40 @@ vi.mock('./view-annotation-modal', () => ({
{props.item.question}
+
) }, })) -vi.mock('@/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal', () => ({ default: (props: any) => props.isShow ?
: null })) -vi.mock('@/app/components/billing/annotation-full/modal', () => ({ default: (props: any) => props.show ?
: null })) +vi.mock('@/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal', () => ({ + default: (props: any) => props.isShow + ? ( +
+ + +
+ ) + : null, +})) +vi.mock('@/app/components/billing/annotation-full/modal', () => ({ + default: (props: any) => props.show + ? ( +
+ +
+ ) + : null, +})) const mockNotify = vi.fn() vi.spyOn(toast, 'success').mockImplementation((message, options) => { @@ -111,9 +144,12 @@ vi.spyOn(toast, 'info').mockImplementation((message, options) => { const addAnnotationMock = addAnnotation as Mock const delAnnotationMock = delAnnotation as Mock const delAnnotationsMock = delAnnotations as Mock +const editAnnotationMock = editAnnotation as Mock const fetchAnnotationConfigMock = fetchAnnotationConfig as Mock const fetchAnnotationListMock = fetchAnnotationList as Mock const queryAnnotationJobStatusMock = queryAnnotationJobStatus as Mock +const updateAnnotationScoreMock = updateAnnotationScore as Mock +const updateAnnotationStatusMock = updateAnnotationStatus as Mock const useProviderContextMock = useProviderContext as Mock const appDetail = { @@ -146,6 +182,9 @@ describe('Annotation', () => { }) fetchAnnotationListMock.mockResolvedValue({ data: [], total: 0 }) queryAnnotationJobStatusMock.mockResolvedValue({ job_status: JobStatus.completed }) + updateAnnotationStatusMock.mockResolvedValue({ job_id: 'job-1' }) + updateAnnotationScoreMock.mockResolvedValue(undefined) + editAnnotationMock.mockResolvedValue(undefined) useProviderContextMock.mockReturnValue({ plan: { usage: { annotatedResponse: 0 }, @@ -251,4 +290,166 @@ describe('Annotation', () => { expect(latestListProps.selectedIds).toEqual([annotation.id]) }) }) + + it('should show the annotation-full modal when enabling annotations exceeds the plan quota', async () => { + useProviderContextMock.mockReturnValue({ + plan: { + usage: { annotatedResponse: 10 }, + total: { annotatedResponse: 10 }, + }, + enableBilling: true, + }) + + renderComponent() + + const toggle = await screen.findByRole('switch') + fireEvent.click(toggle) + + expect(screen.getByTestId('annotation-full-modal')).toBeInTheDocument() + + fireEvent.click(screen.getByTestId('hide-annotation-full-modal')) + expect(screen.queryByTestId('annotation-full-modal')).not.toBeInTheDocument() + }) + + it('should disable annotations and refetch config after the async job completes', async () => { + fetchAnnotationConfigMock.mockResolvedValueOnce({ + id: 'config-id', + enabled: true, + embedding_model: { + embedding_model_name: 'model', + embedding_provider_name: 'provider', + }, + score_threshold: 0.5, + }).mockResolvedValueOnce({ + id: 'config-id', + enabled: false, + embedding_model: { + embedding_model_name: 'model', + embedding_provider_name: 'provider', + }, + score_threshold: 0.5, + }) + + renderComponent() + + const toggle = await screen.findByRole('switch') + await waitFor(() => { + expect(toggle).toHaveAttribute('aria-checked', 'true') + }) + fireEvent.click(toggle) + + await waitFor(() => { + expect(updateAnnotationStatusMock).toHaveBeenCalledWith( + appDetail.id, + AnnotationEnableStatus.disable, + expect.objectContaining({ + embedding_model_name: 'model', + embedding_provider_name: 'provider', + }), + 0.5, + ) + expect(queryAnnotationJobStatusMock).toHaveBeenCalledWith(appDetail.id, AnnotationEnableStatus.disable, 'job-1') + expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ + message: 'common.api.actionSuccess', + type: 'success', + })) + }) + }) + + it('should save annotation config changes and update the score when the modal confirms', async () => { + fetchAnnotationConfigMock.mockResolvedValue({ + id: 'config-id', + enabled: false, + embedding_model: { + embedding_model_name: 'model', + embedding_provider_name: 'provider', + }, + score_threshold: 0.5, + }) + + renderComponent() + + const toggle = await screen.findByRole('switch') + fireEvent.click(toggle) + + expect(screen.getByTestId('config-modal')).toBeInTheDocument() + fireEvent.click(screen.getByTestId('config-save')) + + await waitFor(() => { + expect(updateAnnotationStatusMock).toHaveBeenCalledWith( + appDetail.id, + AnnotationEnableStatus.enable, + { + embedding_model_name: 'next-model', + embedding_provider_name: 'next-provider', + }, + 0.7, + ) + expect(updateAnnotationScoreMock).toHaveBeenCalledWith(appDetail.id, 'config-id', 0.7) + expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ + message: 'common.api.actionSuccess', + type: 'success', + })) + }) + }) + + it('should refresh the list from the header shortcut and allow saving or closing the view modal', async () => { + const annotation = createAnnotation() + fetchAnnotationListMock.mockResolvedValue({ data: [annotation], total: 1 }) + + renderComponent() + + await screen.findByTestId('list') + fireEvent.click(screen.getByTestId('list-view')) + + fireEvent.click(screen.getByTestId('view-modal-save')) + + await waitFor(() => { + expect(editAnnotationMock).toHaveBeenCalledWith(appDetail.id, annotation.id, { + question: 'Edited question', + answer: 'Edited answer', + }) + }) + + fireEvent.click(screen.getByTestId('view-modal-close')) + expect(screen.queryByTestId('view-modal')).not.toBeInTheDocument() + + fireEvent.click(screen.getByTestId('trigger-added')) + + expect(fetchAnnotationListMock).toHaveBeenCalled() + }) + + it('should clear selections on cancel and hide the config modal when requested', async () => { + const annotation = createAnnotation() + fetchAnnotationConfigMock.mockResolvedValue({ + id: 'config-id', + enabled: true, + embedding_model: { + embedding_model_name: 'model', + embedding_provider_name: 'provider', + }, + score_threshold: 0.5, + }) + fetchAnnotationListMock.mockResolvedValue({ data: [annotation], total: 1 }) + + renderComponent() + + await screen.findByTestId('list') + + await act(async () => { + latestListProps.onSelectedIdsChange([annotation.id]) + }) + await act(async () => { + latestListProps.onCancel() + }) + + expect(latestListProps.selectedIds).toEqual([]) + + const configButton = document.querySelector('.action-btn') as HTMLButtonElement + fireEvent.click(configButton) + expect(await screen.findByTestId('config-modal')).toBeInTheDocument() + + fireEvent.click(screen.getByTestId('config-hide')) + expect(screen.queryByTestId('config-modal')).not.toBeInTheDocument() + }) }) diff --git a/web/app/components/app/annotation/list.spec.tsx b/web/app/components/app/annotation/__tests__/list.spec.tsx similarity index 97% rename from web/app/components/app/annotation/list.spec.tsx rename to web/app/components/app/annotation/__tests__/list.spec.tsx index c126092ecf..aa47a5304b 100644 --- a/web/app/components/app/annotation/list.spec.tsx +++ b/web/app/components/app/annotation/__tests__/list.spec.tsx @@ -1,7 +1,7 @@ -import type { AnnotationItem } from './type' +import type { AnnotationItem } from '../type' import { fireEvent, render, screen, within } from '@testing-library/react' import * as React from 'react' -import List from './list' +import List from '../list' const mockFormatTime = vi.fn(() => 'formatted-time') diff --git a/web/app/components/app/annotation/add-annotation-modal/index.spec.tsx b/web/app/components/app/annotation/add-annotation-modal/__tests__/index.spec.tsx similarity index 99% rename from web/app/components/app/annotation/add-annotation-modal/index.spec.tsx rename to web/app/components/app/annotation/add-annotation-modal/__tests__/index.spec.tsx index 14f94d910b..d7e46f9f92 100644 --- a/web/app/components/app/annotation/add-annotation-modal/index.spec.tsx +++ b/web/app/components/app/annotation/add-annotation-modal/__tests__/index.spec.tsx @@ -2,7 +2,7 @@ import type { Mock } from 'vitest' import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import * as React from 'react' import { useProviderContext } from '@/context/provider-context' -import AddAnnotationModal from './index' +import AddAnnotationModal from '../index' vi.mock('@/context/provider-context', () => ({ useProviderContext: vi.fn(), diff --git a/web/app/components/app/annotation/add-annotation-modal/edit-item/index.spec.tsx b/web/app/components/app/annotation/add-annotation-modal/edit-item/__tests__/index.spec.tsx similarity index 96% rename from web/app/components/app/annotation/add-annotation-modal/edit-item/index.spec.tsx rename to web/app/components/app/annotation/add-annotation-modal/edit-item/__tests__/index.spec.tsx index ce660f7880..6dd1d42246 100644 --- a/web/app/components/app/annotation/add-annotation-modal/edit-item/index.spec.tsx +++ b/web/app/components/app/annotation/add-annotation-modal/edit-item/__tests__/index.spec.tsx @@ -1,6 +1,6 @@ import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' -import EditItem, { EditItemType } from './index' +import EditItem, { EditItemType } from '../index' describe('AddAnnotationModal/EditItem', () => { it('should render query inputs with user avatar and placeholder strings', () => { diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/csv-downloader.spec.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/csv-downloader.spec.tsx similarity index 95% rename from web/app/components/app/annotation/batch-add-annotation-modal/csv-downloader.spec.tsx rename to web/app/components/app/annotation/batch-add-annotation-modal/__tests__/csv-downloader.spec.tsx index 2ab0934fe2..69574564eb 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/csv-downloader.spec.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/csv-downloader.spec.tsx @@ -1,10 +1,11 @@ +/* eslint-disable ts/no-explicit-any */ import type { Mock } from 'vitest' import type { Locale } from '@/i18n-config' import { render, screen } from '@testing-library/react' import * as React from 'react' import { useLocale } from '@/context/i18n' import { LanguagesSupported } from '@/i18n-config/language' -import CSVDownload from './csv-downloader' +import CSVDownload from '../csv-downloader' const downloaderProps: any[] = [] diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.spec.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/csv-uploader.spec.tsx similarity index 88% rename from web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.spec.tsx rename to web/app/components/app/annotation/batch-add-annotation-modal/__tests__/csv-uploader.spec.tsx index 847db74619..d26ab051ef 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.spec.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/csv-uploader.spec.tsx @@ -1,7 +1,7 @@ -import type { Props } from './csv-uploader' +import type { Props } from '../csv-uploader' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import * as React from 'react' -import CSVUploader from './csv-uploader' +import CSVUploader from '../csv-uploader' const toastMocks = vi.hoisted(() => ({ notify: vi.fn(), @@ -75,6 +75,20 @@ describe('CSVUploader', () => { expect(dropZone.className).not.toContain('border-components-dropzone-border-accent') }) + it('should handle drag over and clear dragging state when leaving through the overlay', () => { + renderComponent() + const { dropZone, dropContainer } = getDropElements() + + fireEvent.dragEnter(dropContainer) + const dragLayer = dropContainer.querySelector('.absolute') as HTMLDivElement + + fireEvent.dragOver(dropContainer) + fireEvent.dragLeave(dragLayer) + + expect(dropZone.className).not.toContain('border-components-dropzone-border-accent') + expect(dropZone.className).not.toContain('bg-components-dropzone-bg-accent') + }) + it('should ignore drop events without dataTransfer', () => { renderComponent() const { dropContainer } = getDropElements() diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/index.spec.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/index.spec.tsx similarity index 96% rename from web/app/components/app/annotation/batch-add-annotation-modal/index.spec.tsx rename to web/app/components/app/annotation/batch-add-annotation-modal/__tests__/index.spec.tsx index 8929cc292f..5104706045 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/index.spec.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/index.spec.tsx @@ -1,10 +1,10 @@ import type { Mock } from 'vitest' -import type { IBatchModalProps } from './index' +import type { IBatchModalProps } from '../index' import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import * as React from 'react' import { useProviderContext } from '@/context/provider-context' import { annotationBatchImport, checkAnnotationBatchImportProgress } from '@/service/annotation' -import BatchModal, { ProcessStatus } from './index' +import BatchModal, { ProcessStatus } from '../index' vi.mock('@/service/annotation', () => ({ annotationBatchImport: vi.fn(), @@ -15,13 +15,13 @@ vi.mock('@/context/provider-context', () => ({ useProviderContext: vi.fn(), })) -vi.mock('./csv-downloader', () => ({ +vi.mock('../csv-downloader', () => ({ default: () =>
, })) let lastUploadedFile: File | undefined -vi.mock('./csv-uploader', () => ({ +vi.mock('../csv-uploader', () => ({ default: ({ file, updateFile }: { file?: File, updateFile: (file?: File) => void }) => (
+ +
+ ) + }, +})) + +vi.mock('@/app/components/base/features/hooks', () => ({ + useFeatures: (selector: (state: { features: typeof mockFeatures }) => unknown) => selector({ features: mockFeatures }), + useFeaturesStore: () => ({ + getState: () => ({ + features: mockFeatures, + setFeatures: mockSetFeatures, + }), + }), +})) + +describe('FeaturesWrappedAppPublisher', () => { + const publishedConfig = { + modelConfig: { + more_like_this: { enabled: true }, + opening_statement: 'Hello there', + suggested_questions: ['Q1'], + sensitive_word_avoidance: { enabled: true }, + speech_to_text: { enabled: true }, + text_to_speech: { enabled: true }, + suggested_questions_after_answer: { enabled: true }, + retriever_resource: { enabled: true }, + annotation_reply: { enabled: true }, + file_upload: { + enabled: true, + image: { + enabled: true, + detail: 'low', + number_limits: 5, + transfer_methods: ['remote_url'], + }, + allowed_file_types: ['image'], + allowed_file_extensions: ['.jpg'], + allowed_file_upload_methods: ['remote_url'], + number_limits: 5, + }, + resetAppConfig: vi.fn(), + }, + } + + beforeEach(() => { + vi.clearAllMocks() + mockAppPublisherProps.current = null + }) + + it('should pass current features through to onPublish', async () => { + render( + , + ) + + fireEvent.click(screen.getByText('publish-through-wrapper')) + + await waitFor(() => { + expect(mockOnPublish).toHaveBeenCalledWith({ id: 'model-1' }, mockFeatures) + }) + }) + + it('should restore published features after confirmation', async () => { + render( + , + ) + + fireEvent.click(screen.getByText('restore-through-wrapper')) + fireEvent.click(screen.getByRole('button', { name: 'operation.confirm' })) + + await waitFor(() => { + expect(publishedConfig.modelConfig.resetAppConfig).toHaveBeenCalledTimes(1) + expect(mockSetFeatures).toHaveBeenCalledWith(expect.objectContaining({ + moreLikeThis: { enabled: true }, + opening: { + enabled: true, + opening_statement: 'Hello there', + suggested_questions: ['Q1'], + }, + moderation: { enabled: true }, + speech2text: { enabled: true }, + text2speech: { enabled: true }, + suggested: { enabled: true }, + citation: { enabled: true }, + annotationReply: { enabled: true }, + })) + }) + }) +}) diff --git a/web/app/components/app/app-publisher/__tests__/index.spec.tsx b/web/app/components/app/app-publisher/__tests__/index.spec.tsx new file mode 100644 index 0000000000..e97efaa525 --- /dev/null +++ b/web/app/components/app/app-publisher/__tests__/index.spec.tsx @@ -0,0 +1,455 @@ +/* eslint-disable ts/no-explicit-any */ +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import * as React from 'react' +import { AccessMode } from '@/models/access-control' +import { AppModeEnum } from '@/types/app' +import { basePath } from '@/utils/var' +import AppPublisher from '../index' + +const mockOnPublish = vi.fn() +const mockOnToggle = vi.fn() +const mockSetAppDetail = vi.fn() +const mockTrackEvent = vi.fn() +const mockRefetch = vi.fn() +const mockOpenAsyncWindow = vi.fn() +const mockFetchInstalledAppList = vi.fn() +const mockFetchAppDetailDirect = vi.fn() +const mockToastError = vi.fn() + +const sectionProps = vi.hoisted(() => ({ + summary: null as null | Record, + access: null as null | Record, + actions: null as null | Record, +})) +const ahooksMocks = vi.hoisted(() => ({ + keyPressHandlers: [] as Array<(event: { preventDefault: () => void }) => void>, +})) + +let mockAppDetail: Record | null = null + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('ahooks', async () => { + return { + useKeyPress: (_keys: unknown, handler: (event: { preventDefault: () => void }) => void) => { + ahooksMocks.keyPressHandlers.push(handler) + }, + } +}) + +vi.mock('@/app/components/app/store', () => ({ + useStore: (selector: (state: { appDetail: Record | null, setAppDetail: typeof mockSetAppDetail }) => unknown) => selector({ + appDetail: mockAppDetail, + setAppDetail: mockSetAppDetail, + }), +})) + +vi.mock('@/context/global-public-context', () => ({ + useGlobalPublicStore: (selector: (state: { systemFeatures: { webapp_auth: { enabled: boolean } } }) => unknown) => selector({ + systemFeatures: { + webapp_auth: { + enabled: true, + }, + }, + }), +})) + +vi.mock('@/hooks/use-format-time-from-now', () => ({ + useFormatTimeFromNow: () => ({ + formatTimeFromNow: () => 'moments ago', + }), +})) + +vi.mock('@/hooks/use-async-window-open', () => ({ + useAsyncWindowOpen: () => mockOpenAsyncWindow, +})) + +vi.mock('@/service/access-control', () => ({ + useGetUserCanAccessApp: () => ({ + data: { result: true }, + isLoading: false, + refetch: mockRefetch, + }), + useAppWhiteListSubjects: () => ({ + data: { groups: [], members: [] }, + isLoading: false, + }), +})) + +vi.mock('@/service/explore', () => ({ + fetchInstalledAppList: (...args: unknown[]) => mockFetchInstalledAppList(...args), +})) + +vi.mock('@/service/apps', () => ({ + fetchAppDetailDirect: (...args: unknown[]) => mockFetchAppDetailDirect(...args), +})) + +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + error: (...args: unknown[]) => mockToastError(...args), + }, +})) + +vi.mock('@/app/components/base/amplitude', () => ({ + trackEvent: (...args: unknown[]) => mockTrackEvent(...args), +})) + +vi.mock('@/app/components/app/overview/embedded', () => ({ + default: ({ isShow, onClose }: { isShow: boolean, onClose: () => void }) => (isShow + ? ( +
+ embedded modal + +
+ ) + : null), +})) + +vi.mock('../../app-access-control', () => ({ + default: ({ onConfirm, onClose }: { onConfirm: () => Promise, onClose: () => void }) => ( +
+ + +
+ ), +})) + +vi.mock('@/app/components/base/portal-to-follow-elem', async () => { + const ReactModule = await vi.importActual('react') + const OpenContext = ReactModule.createContext(false) + + return { + PortalToFollowElem: ({ children, open }: { children: React.ReactNode, open: boolean }) => ( + +
{children}
+
+ ), + PortalToFollowElemTrigger: ({ children, onClick }: { children: React.ReactNode, onClick?: () => void }) => ( +
{children}
+ ), + PortalToFollowElemContent: ({ children }: { children: React.ReactNode }) => { + const open = ReactModule.useContext(OpenContext) + return open ?
{children}
: null + }, + } +}) + +vi.mock('../sections', () => ({ + PublisherSummarySection: (props: Record) => { + sectionProps.summary = props + return ( +
+ + +
+ ) + }, + PublisherAccessSection: (props: Record) => { + sectionProps.access = props + return + }, + PublisherActionsSection: (props: Record) => { + sectionProps.actions = props + return ( +
+ + +
+ ) + }, +})) + +describe('AppPublisher', () => { + beforeEach(() => { + vi.clearAllMocks() + ahooksMocks.keyPressHandlers.length = 0 + sectionProps.summary = null + sectionProps.access = null + sectionProps.actions = null + mockAppDetail = { + id: 'app-1', + name: 'Demo App', + mode: AppModeEnum.CHAT, + access_mode: AccessMode.SPECIFIC_GROUPS_MEMBERS, + site: { + app_base_url: 'https://example.com', + access_token: 'token-1', + }, + } + mockFetchInstalledAppList.mockResolvedValue({ + installed_apps: [{ id: 'installed-1' }], + }) + mockFetchAppDetailDirect.mockResolvedValue({ + id: 'app-1', + access_mode: AccessMode.PUBLIC, + }) + mockOpenAsyncWindow.mockImplementation(async (resolver: () => Promise) => { + await resolver() + }) + }) + + it('should open the publish popover and refetch access permission data', async () => { + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + + expect(screen.getByText('publisher-summary-publish')).toBeInTheDocument() + expect(mockOnToggle).toHaveBeenCalledWith(true) + + await waitFor(() => { + expect(mockRefetch).toHaveBeenCalledTimes(1) + }) + }) + + it('should publish and track the publish event', async () => { + mockOnPublish.mockResolvedValue(undefined) + + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-summary-publish')) + + await waitFor(() => { + expect(mockOnPublish).toHaveBeenCalledTimes(1) + expect(mockTrackEvent).toHaveBeenCalledWith('app_published_time', expect.objectContaining({ + action_mode: 'app', + app_id: 'app-1', + app_name: 'Demo App', + })) + }) + }) + + it('should open the embedded modal from the actions section', () => { + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-embed')) + + expect(screen.getByTestId('embedded-modal')).toBeInTheDocument() + }) + + it('should close embedded and access control panels through child callbacks', async () => { + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-embed')) + fireEvent.click(screen.getByText('close-embedded-modal')) + expect(screen.queryByTestId('embedded-modal')).not.toBeInTheDocument() + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-access-control')) + expect(screen.getByTestId('access-control')).toBeInTheDocument() + fireEvent.click(screen.getByText('close-access-control')) + expect(screen.queryByTestId('access-control')).not.toBeInTheDocument() + }) + + it('should refresh app detail after access control confirmation', async () => { + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-access-control')) + + expect(screen.getByTestId('access-control')).toBeInTheDocument() + + fireEvent.click(screen.getByText('confirm-access-control')) + + await waitFor(() => { + expect(mockFetchAppDetailDirect).toHaveBeenCalledWith({ url: '/apps', id: 'app-1' }) + expect(mockSetAppDetail).toHaveBeenCalledWith({ + id: 'app-1', + access_mode: AccessMode.PUBLIC, + }) + }) + }) + + it('should open the installed explore page through the async window helper', async () => { + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-open-in-explore')) + + await waitFor(() => { + expect(mockOpenAsyncWindow).toHaveBeenCalledTimes(1) + expect(mockFetchInstalledAppList).toHaveBeenCalledWith('app-1') + expect(sectionProps.actions?.appURL).toBe(`https://example.com${basePath}/chat/token-1`) + }) + }) + + it('should ignore the trigger when the publish button is disabled', () => { + render( + , + ) + + fireEvent.click(screen.getByText('common.publish').parentElement?.parentElement as HTMLElement) + + expect(screen.queryByText('publisher-summary-publish')).not.toBeInTheDocument() + expect(mockOnToggle).not.toHaveBeenCalled() + }) + + it('should publish from the keyboard shortcut and restore the popover state', async () => { + const preventDefault = vi.fn() + const onRestore = vi.fn().mockResolvedValue(undefined) + mockOnPublish.mockResolvedValue(undefined) + + render( + , + ) + + ahooksMocks.keyPressHandlers[0]({ preventDefault }) + + await waitFor(() => { + expect(preventDefault).toHaveBeenCalled() + expect(mockOnPublish).toHaveBeenCalledTimes(1) + }) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-summary-restore')) + + await waitFor(() => { + expect(onRestore).toHaveBeenCalledTimes(1) + }) + expect(screen.queryByText('publisher-summary-publish')).not.toBeInTheDocument() + }) + + it('should keep the popover open when restore fails and reset published state after publish failures', async () => { + const preventDefault = vi.fn() + const onRestore = vi.fn().mockRejectedValue(new Error('restore failed')) + mockOnPublish.mockRejectedValueOnce(new Error('publish failed')) + + render( + , + ) + + ahooksMocks.keyPressHandlers[0]({ preventDefault }) + + await waitFor(() => { + expect(preventDefault).toHaveBeenCalled() + expect(mockOnPublish).toHaveBeenCalledTimes(1) + }) + expect(mockTrackEvent).not.toHaveBeenCalled() + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-summary-restore')) + + await waitFor(() => { + expect(onRestore).toHaveBeenCalledTimes(1) + }) + expect(screen.getByText('publisher-summary-publish')).toBeInTheDocument() + }) + + it('should report missing explore installations', async () => { + mockFetchInstalledAppList.mockResolvedValueOnce({ + installed_apps: [], + }) + mockOpenAsyncWindow.mockImplementation(async (resolver: () => Promise, options: { onError: (error: Error) => void }) => { + try { + await resolver() + } + catch (error) { + options.onError(error as Error) + } + }) + + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-open-in-explore')) + + await waitFor(() => { + expect(mockToastError).toHaveBeenCalledWith('No app found in Explore') + }) + }) + + it('should report explore errors when the app cannot be opened', async () => { + mockAppDetail = { + ...mockAppDetail, + id: undefined, + } + mockOpenAsyncWindow.mockImplementation(async (resolver: () => Promise, options: { onError: (error: Error) => void }) => { + try { + await resolver() + } + catch (error) { + options.onError(error as Error) + } + }) + + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-open-in-explore')) + + await waitFor(() => { + expect(mockToastError).toHaveBeenCalledWith('App not found') + }) + }) + + it('should keep access control open when app detail is unavailable during confirmation', async () => { + mockAppDetail = null + + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-access-control')) + fireEvent.click(screen.getByText('confirm-access-control')) + + await waitFor(() => { + expect(mockFetchAppDetailDirect).not.toHaveBeenCalled() + }) + expect(screen.getByTestId('access-control')).toBeInTheDocument() + }) +}) diff --git a/web/app/components/app/app-publisher/__tests__/publish-with-multiple-model.spec.tsx b/web/app/components/app/app-publisher/__tests__/publish-with-multiple-model.spec.tsx new file mode 100644 index 0000000000..f476d8b188 --- /dev/null +++ b/web/app/components/app/app-publisher/__tests__/publish-with-multiple-model.spec.tsx @@ -0,0 +1,110 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import * as React from 'react' +import PublishWithMultipleModel from '../publish-with-multiple-model' + +const mockUseProviderContext = vi.fn() + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('@/context/provider-context', () => ({ + useProviderContext: () => mockUseProviderContext(), +})) + +vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({ + useLanguage: () => 'en_US', +})) + +vi.mock('../../header/account-setting/model-provider-page/model-icon', () => ({ + default: ({ modelName }: { modelName: string }) => {modelName}, +})) + +vi.mock('@/app/components/base/portal-to-follow-elem', async () => { + const ReactModule = await vi.importActual('react') + const OpenContext = ReactModule.createContext(false) + + return { + PortalToFollowElem: ({ children, open }: { children: React.ReactNode, open: boolean }) => ( + +
{children}
+
+ ), + PortalToFollowElemTrigger: ({ children, onClick, className }: { children: React.ReactNode, onClick?: () => void, className?: string }) => ( +
+ {children} +
+ ), + PortalToFollowElemContent: ({ children, className }: { children: React.ReactNode, className?: string }) => { + const open = ReactModule.useContext(OpenContext) + return open ?
{children}
: null + }, + } +}) + +describe('PublishWithMultipleModel', () => { + beforeEach(() => { + vi.clearAllMocks() + mockUseProviderContext.mockReturnValue({ + textGenerationModelList: [ + { + provider: 'openai', + models: [ + { + model: 'gpt-4o', + label: { + en_US: 'GPT-4o', + }, + }, + ], + }, + ], + }) + }) + + it('should disable the trigger when no valid model configuration is available', () => { + render( + , + ) + + expect(screen.getByRole('button', { name: 'operation.applyConfig' })).toBeDisabled() + expect(screen.queryByText('publishAs')).not.toBeInTheDocument() + }) + + it('should open matching model options and call onSelect', () => { + const handleSelect = vi.fn() + const modelConfig = { + id: 'config-1', + provider: 'openai', + model: 'gpt-4o', + parameters: { temperature: 0.7 }, + } + + render( + , + ) + + fireEvent.click(screen.getByRole('button', { name: 'operation.applyConfig' })) + + expect(screen.getByText('publishAs')).toBeInTheDocument() + + fireEvent.click(screen.getByText('GPT-4o')) + + expect(handleSelect).toHaveBeenCalledWith(expect.objectContaining(modelConfig)) + }) +}) diff --git a/web/app/components/app/app-publisher/__tests__/sections.spec.tsx b/web/app/components/app/app-publisher/__tests__/sections.spec.tsx new file mode 100644 index 0000000000..57e7a55b13 --- /dev/null +++ b/web/app/components/app/app-publisher/__tests__/sections.spec.tsx @@ -0,0 +1,266 @@ +/* eslint-disable ts/no-explicit-any */ +import type { ReactNode } from 'react' +import { fireEvent, render, screen } from '@testing-library/react' +import { AccessMode } from '@/models/access-control' +import { AppModeEnum } from '@/types/app' +import { AccessModeDisplay, PublisherAccessSection, PublisherActionsSection, PublisherSummarySection } from '../sections' + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('../publish-with-multiple-model', () => ({ + default: ({ onSelect }: { onSelect: (item: Record) => void }) => ( + + ), +})) + +vi.mock('../suggested-action', () => ({ + default: ({ children, onClick, link, disabled }: { children: ReactNode, onClick?: () => void, link?: string, disabled?: boolean }) => ( + + ), +})) + +vi.mock('@/app/components/tools/workflow-tool/configure-button', () => ({ + default: (props: Record) => ( +
+ workflow-tool-configure + {String(props.disabledReason || '')} +
+ ), +})) + +describe('app-publisher sections', () => { + it('should render restore controls for published chat apps', () => { + const handleRestore = vi.fn() + + render( + '3 minutes ago'} + handlePublish={vi.fn()} + handleRestore={handleRestore} + isChatApp + multipleModelConfigs={[]} + publishDisabled={false} + published={false} + publishedAt={Date.now()} + publishShortcut={['ctrl', '⇧', 'P']} + startNodeLimitExceeded={false} + upgradeHighlightStyle={{}} + />, + ) + + fireEvent.click(screen.getByText('common.restore')) + expect(handleRestore).toHaveBeenCalled() + }) + + it('should expose the access control warning when subjects are missing', () => { + render( + , + ) + + expect(screen.getByText('publishApp.notSet')).toBeInTheDocument() + expect(screen.getByText('publishApp.notSetDesc')).toBeInTheDocument() + }) + + it('should render the publish update action when the draft has not been published yet', () => { + render( + '1 minute ago'} + handlePublish={vi.fn()} + handleRestore={vi.fn()} + isChatApp={false} + multipleModelConfigs={[]} + publishDisabled={false} + published={false} + publishedAt={undefined} + publishShortcut={['ctrl', '⇧', 'P']} + startNodeLimitExceeded={false} + upgradeHighlightStyle={{}} + />, + ) + + expect(screen.getByText('common.publishUpdate')).toBeInTheDocument() + }) + + it('should render multiple-model publishing', () => { + const handlePublish = vi.fn() + + render( + '1 minute ago'} + handlePublish={handlePublish} + handleRestore={vi.fn()} + isChatApp={false} + multipleModelConfigs={[{ id: '1' } as any]} + publishDisabled={false} + published={false} + publishedAt={undefined} + publishShortcut={['ctrl', '⇧', 'P']} + startNodeLimitExceeded={false} + upgradeHighlightStyle={{}} + />, + ) + + fireEvent.click(screen.getByText('publish-multiple-model')) + + expect(handlePublish).toHaveBeenCalledWith({ model: 'gpt-4o' }) + }) + + it('should render the upgrade hint when the start node limit is exceeded', () => { + render( + '1 minute ago'} + handlePublish={vi.fn()} + handleRestore={vi.fn()} + isChatApp={false} + multipleModelConfigs={[]} + publishDisabled={false} + published={false} + publishedAt={undefined} + publishShortcut={['ctrl', '⇧', 'P']} + startNodeLimitExceeded + upgradeHighlightStyle={{}} + />, + ) + + expect(screen.getByText('publishLimit.startNodeDesc')).toBeInTheDocument() + }) + + it('should render loading access state and access mode labels when enabled', () => { + const { rerender } = render( + , + ) + + expect(document.querySelector('.spin-animation')).toBeInTheDocument() + + rerender( + , + ) + + expect(screen.getByText('accessControlDialog.accessItems.anyone')).toBeInTheDocument() + expect(render().container).toBeEmptyDOMElement() + }) + + it('should render workflow actions, batch run links, and workflow tool configuration', () => { + const handleOpenInExplore = vi.fn() + const handleEmbed = vi.fn() + + const { rerender } = render( + , + ) + + expect(screen.getByText('common.batchRunApp')).toHaveAttribute('data-link', 'https://example.com/app?mode=batch') + fireEvent.click(screen.getByText('common.openInExplore')) + expect(handleOpenInExplore).toHaveBeenCalled() + expect(screen.getByText('workflow-tool-configure')).toBeInTheDocument() + expect(screen.getByText('workflow-disabled')).toBeInTheDocument() + + rerender( + , + ) + + fireEvent.click(screen.getByText('common.embedIntoSite')) + expect(handleEmbed).toHaveBeenCalled() + expect(screen.getByText('common.accessAPIReference')).toBeDisabled() + + rerender( + , + ) + + expect(screen.queryByText('common.runApp')).not.toBeInTheDocument() + }) +}) diff --git a/web/app/components/app/app-publisher/__tests__/suggested-action.spec.tsx b/web/app/components/app/app-publisher/__tests__/suggested-action.spec.tsx new file mode 100644 index 0000000000..ea199dfb78 --- /dev/null +++ b/web/app/components/app/app-publisher/__tests__/suggested-action.spec.tsx @@ -0,0 +1,49 @@ +import type { MouseEvent as ReactMouseEvent } from 'react' +import { fireEvent, render, screen } from '@testing-library/react' +import SuggestedAction from '../suggested-action' + +describe('SuggestedAction', () => { + it('should render an enabled external link', () => { + render( + + Open docs + , + ) + + const link = screen.getByRole('link', { name: 'Open docs' }) + expect(link).toHaveAttribute('href', 'https://example.com/docs') + expect(link).toHaveAttribute('target', '_blank') + }) + + it('should block clicks when disabled', () => { + const handleClick = vi.fn() + + render( + + Disabled action + , + ) + + const link = screen.getByText('Disabled action').closest('a') as HTMLAnchorElement + fireEvent.click(link) + + expect(link).not.toHaveAttribute('href') + expect(handleClick).not.toHaveBeenCalled() + }) + + it('should forward click events when enabled', () => { + const handleClick = vi.fn((event: ReactMouseEvent) => { + event.preventDefault() + }) + + render( + + Enabled action + , + ) + + fireEvent.click(screen.getByRole('link', { name: 'Enabled action' })) + + expect(handleClick).toHaveBeenCalledTimes(1) + }) +}) diff --git a/web/app/components/app/app-publisher/__tests__/utils.spec.ts b/web/app/components/app/app-publisher/__tests__/utils.spec.ts new file mode 100644 index 0000000000..9f191ce514 --- /dev/null +++ b/web/app/components/app/app-publisher/__tests__/utils.spec.ts @@ -0,0 +1,70 @@ +import type { TFunction } from 'i18next' +import { AccessMode } from '@/models/access-control' +import { AppModeEnum } from '@/types/app' +import { basePath } from '@/utils/var' +import { + getDisabledFunctionTooltip, + getPublisherAppMode, + getPublisherAppUrl, + isPublisherAccessConfigured, +} from '../utils' + +describe('app-publisher utils', () => { + describe('getPublisherAppMode', () => { + it('should normalize chat-like apps to chat mode', () => { + expect(getPublisherAppMode(AppModeEnum.AGENT_CHAT)).toBe(AppModeEnum.CHAT) + }) + + it('should keep completion mode unchanged', () => { + expect(getPublisherAppMode(AppModeEnum.COMPLETION)).toBe(AppModeEnum.COMPLETION) + }) + }) + + describe('getPublisherAppUrl', () => { + it('should build the published app url from site info', () => { + expect(getPublisherAppUrl({ + appBaseUrl: 'https://example.com', + accessToken: 'token-1', + mode: AppModeEnum.CHAT, + })).toBe(`https://example.com${basePath}/chat/token-1`) + }) + }) + + describe('isPublisherAccessConfigured', () => { + it('should require members or groups for specific access mode', () => { + expect(isPublisherAccessConfigured( + { access_mode: AccessMode.SPECIFIC_GROUPS_MEMBERS }, + { groups: [], members: [] }, + )).toBe(false) + }) + + it('should treat public access as configured', () => { + expect(isPublisherAccessConfigured( + { access_mode: AccessMode.PUBLIC }, + { groups: [], members: [] }, + )).toBe(true) + }) + }) + + describe('getDisabledFunctionTooltip', () => { + const t = ((key: string) => key) as unknown as TFunction + + it('should prioritize the unpublished hint', () => { + expect(getDisabledFunctionTooltip({ + t, + publishedAt: undefined, + missingStartNode: false, + noAccessPermission: false, + })).toBe('notPublishedYet') + }) + + it('should return the access error when the app is published but blocked', () => { + expect(getDisabledFunctionTooltip({ + t, + publishedAt: Date.now(), + missingStartNode: false, + noAccessPermission: true, + })).toBe('noAccessPermission') + }) + }) +}) diff --git a/web/app/components/app/app-publisher/__tests__/version-info-modal.spec.tsx b/web/app/components/app/app-publisher/__tests__/version-info-modal.spec.tsx new file mode 100644 index 0000000000..fa8b1c92fc --- /dev/null +++ b/web/app/components/app/app-publisher/__tests__/version-info-modal.spec.tsx @@ -0,0 +1,128 @@ +/* eslint-disable ts/no-explicit-any */ +import { fireEvent, render, screen } from '@testing-library/react' +import { toast } from '@/app/components/base/ui/toast' +import VersionInfoModal from '../version-info-modal' + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + error: vi.fn(), + }, +})) + +describe('VersionInfoModal', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should prefill the fields from the current version info', () => { + render( + , + ) + + expect(screen.getByDisplayValue('Release 1')).toBeInTheDocument() + expect(screen.getByDisplayValue('Initial release')).toBeInTheDocument() + }) + + it('should reject overlong titles', () => { + const handlePublish = vi.fn() + + render( + , + ) + + const [titleInput] = screen.getAllByRole('textbox') + fireEvent.change(titleInput, { target: { value: 'a'.repeat(16) } }) + fireEvent.click(screen.getByRole('button', { name: 'common.publish' })) + + expect(toast.error).toHaveBeenCalledWith('versionHistory.editField.titleLengthLimit') + expect(handlePublish).not.toHaveBeenCalled() + }) + + it('should publish valid values and close the modal', () => { + const handlePublish = vi.fn() + const handleClose = vi.fn() + + render( + , + ) + + const [titleInput, notesInput] = screen.getAllByRole('textbox') + fireEvent.change(titleInput, { target: { value: 'Release 2' } }) + fireEvent.change(notesInput, { target: { value: 'Updated notes' } }) + fireEvent.click(screen.getByRole('button', { name: 'common.publish' })) + + expect(handlePublish).toHaveBeenCalledWith({ + title: 'Release 2', + releaseNotes: 'Updated notes', + id: 'version-2', + }) + expect(handleClose).toHaveBeenCalledTimes(1) + }) + + it('should validate release note length and clear previous errors before publishing', () => { + const handlePublish = vi.fn() + const handleClose = vi.fn() + + render( + , + ) + + const [titleInput, notesInput] = screen.getAllByRole('textbox') + + fireEvent.change(titleInput, { target: { value: 'a'.repeat(16) } }) + fireEvent.click(screen.getByRole('button', { name: 'common.publish' })) + expect(toast.error).toHaveBeenCalledWith('versionHistory.editField.titleLengthLimit') + + fireEvent.change(titleInput, { target: { value: 'Release 3' } }) + fireEvent.change(notesInput, { target: { value: 'b'.repeat(101) } }) + fireEvent.click(screen.getByRole('button', { name: 'common.publish' })) + expect(toast.error).toHaveBeenCalledWith('versionHistory.editField.releaseNotesLengthLimit') + + fireEvent.change(notesInput, { target: { value: 'Stable release notes' } }) + fireEvent.click(screen.getByRole('button', { name: 'common.publish' })) + + expect(handlePublish).toHaveBeenCalledWith({ + title: 'Release 3', + releaseNotes: 'Stable release notes', + id: 'version-3', + }) + expect(handleClose).toHaveBeenCalledTimes(1) + }) +}) diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index 0b5f629829..8792a1c507 100644 --- a/web/app/components/app/app-publisher/index.tsx +++ b/web/app/components/app/app-publisher/index.tsx @@ -1,6 +1,5 @@ import type { ModelAndParameter } from '../configuration/debug/types' import type { InputVar, Variable } from '@/app/components/workflow/types' -import type { I18nKeysByPrefix } from '@/types/i18n' import type { PublishWorkflowParams } from '@/types/workflow' import { useKeyPress } from 'ahooks' import { @@ -15,15 +14,11 @@ import EmbeddedModal from '@/app/components/app/overview/embedded' import { useStore as useAppStore } from '@/app/components/app/store' import { trackEvent } from '@/app/components/base/amplitude' import Button from '@/app/components/base/button' -import { CodeBrowser } from '@/app/components/base/icons/src/vender/line/development' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' -import UpgradeBtn from '@/app/components/billing/upgrade-btn' -import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/configure-button' -import { appDefaultIconBackground } from '@/config' import { useGlobalPublicStore } from '@/context/global-public-context' import { useAsyncWindowOpen } from '@/hooks/use-async-window-open' import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' @@ -33,54 +28,19 @@ import { fetchAppDetailDirect } from '@/service/apps' import { fetchInstalledAppList } from '@/service/explore' import { AppModeEnum } from '@/types/app' import { basePath } from '@/utils/var' -import Divider from '../../base/divider' -import Loading from '../../base/loading' -import Tooltip from '../../base/tooltip' import { toast } from '../../base/ui/toast' -import ShortcutsName from '../../workflow/shortcuts-name' import { getKeyboardKeyCodeBySystem } from '../../workflow/utils' import AccessControl from '../app-access-control' -import PublishWithMultipleModel from './publish-with-multiple-model' -import SuggestedAction from './suggested-action' - -type AccessModeLabel = I18nKeysByPrefix<'app', 'accessControlDialog.accessItems.'> - -const ACCESS_MODE_MAP: Record = { - [AccessMode.ORGANIZATION]: { - label: 'organization', - icon: 'i-ri-building-line', - }, - [AccessMode.SPECIFIC_GROUPS_MEMBERS]: { - label: 'specific', - icon: 'i-ri-lock-line', - }, - [AccessMode.PUBLIC]: { - label: 'anyone', - icon: 'i-ri-global-line', - }, - [AccessMode.EXTERNAL_MEMBERS]: { - label: 'external', - icon: 'i-ri-verified-badge-line', - }, -} - -const AccessModeDisplay: React.FC<{ mode?: AccessMode }> = ({ mode }) => { - const { t } = useTranslation() - - if (!mode || !ACCESS_MODE_MAP[mode]) - return null - - const { icon, label } = ACCESS_MODE_MAP[mode] - - return ( - <> - -
- {t(`accessControlDialog.accessItems.${label}`, { ns: 'app' })} -
- - ) -} +import { + PublisherAccessSection, + PublisherActionsSection, + PublisherSummarySection, +} from './sections' +import { + getDisabledFunctionTooltip, + getPublisherAppUrl, + isPublisherAccessConfigured, +} from './utils' export type AppPublisherProps = { disabled?: boolean @@ -143,32 +103,28 @@ const AppPublisher = ({ const { formatTimeFromNow } = useFormatTimeFromNow() const { app_base_url: appBaseURL = '', access_token: accessToken = '' } = appDetail?.site ?? {} - const appMode = (appDetail?.mode !== AppModeEnum.COMPLETION && appDetail?.mode !== AppModeEnum.WORKFLOW) ? AppModeEnum.CHAT : appDetail.mode - const appURL = `${appBaseURL}${basePath}/${appMode}/${accessToken}` + const appURL = getPublisherAppUrl({ appBaseUrl: appBaseURL, accessToken, mode: appDetail?.mode }) const isChatApp = [AppModeEnum.CHAT, AppModeEnum.AGENT_CHAT, AppModeEnum.COMPLETION].includes(appDetail?.mode || AppModeEnum.CHAT) const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp, refetch } = useGetUserCanAccessApp({ appId: appDetail?.id, enabled: false }) const { data: appAccessSubjects, isLoading: isGettingAppWhiteListSubjects } = useAppWhiteListSubjects(appDetail?.id, open && systemFeatures.webapp_auth.enabled && appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS) const openAsyncWindow = useAsyncWindowOpen() - const isAppAccessSet = useMemo(() => { - if (appDetail && appAccessSubjects) { - return !(appDetail.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && appAccessSubjects.groups?.length === 0 && appAccessSubjects.members?.length === 0) - } - return true - }, [appAccessSubjects, appDetail]) + const isAppAccessSet = useMemo(() => isPublisherAccessConfigured(appDetail, appAccessSubjects), [appAccessSubjects, appDetail]) - const noAccessPermission = useMemo(() => systemFeatures.webapp_auth.enabled && appDetail && appDetail.access_mode !== AccessMode.EXTERNAL_MEMBERS && !userCanAccessApp?.result, [systemFeatures, appDetail, userCanAccessApp]) + const noAccessPermission = useMemo(() => Boolean( + systemFeatures.webapp_auth.enabled + && appDetail + && appDetail.access_mode !== AccessMode.EXTERNAL_MEMBERS + && !userCanAccessApp?.result, + ), [systemFeatures, appDetail, userCanAccessApp]) const disabledFunctionButton = useMemo(() => (!publishedAt || missingStartNode || noAccessPermission), [publishedAt, missingStartNode, noAccessPermission]) - - const disabledFunctionTooltip = useMemo(() => { - if (!publishedAt) - return t('notPublishedYet', { ns: 'app' }) - if (missingStartNode) - return t('noUserInputNode', { ns: 'app' }) - if (noAccessPermission) - return t('noAccessPermission', { ns: 'app' }) - }, [missingStartNode, noAccessPermission, publishedAt, t]) + const disabledFunctionTooltip = useMemo(() => getDisabledFunctionTooltip({ + t, + publishedAt, + missingStartNode, + noAccessPermission, + }), [missingStartNode, noAccessPermission, publishedAt, t]) useEffect(() => { if (systemFeatures.webapp_auth.enabled && open && appDetail) @@ -244,9 +200,9 @@ const AppPublisher = ({ }, { exactMatch: true, useCapture: true }) const hasPublishedVersion = !!publishedAt - const workflowToolDisabled = !hasPublishedVersion || !workflowToolAvailable - const workflowToolMessage = workflowToolDisabled ? t('common.workflowAsToolDisabledHint', { ns: 'workflow' }) : undefined - const showStartNodeLimitHint = Boolean(startNodeLimitExceeded) + const workflowToolMessage = !hasPublishedVersion || !workflowToolAvailable + ? t('common.workflowAsToolDisabledHint', { ns: 'workflow' }) + : undefined const upgradeHighlightStyle = useMemo(() => ({ background: 'linear-gradient(97deg, var(--components-input-border-active-prompt-1, rgba(11, 165, 236, 0.95)) -3.64%, var(--components-input-border-active-prompt-2, rgba(21, 90, 239, 0.95)) 45.14%)', WebkitBackgroundClip: 'text', @@ -277,199 +233,51 @@ const AppPublisher = ({
-
-
- {publishedAt ? t('common.latestPublished', { ns: 'workflow' }) : t('common.currentDraftUnpublished', { ns: 'workflow' })} -
- {publishedAt - ? ( -
-
- {t('common.publishedAt', { ns: 'workflow' })} - {' '} - {formatTimeFromNow(publishedAt)} -
- {isChatApp && ( - - )} -
- ) - : ( -
- {t('common.autoSaved', { ns: 'workflow' })} - {' '} - · - {Boolean(draftUpdatedAt) && formatTimeFromNow(draftUpdatedAt!)} -
- )} - {debugWithMultipleModel - ? ( - handlePublish(item)} - // textGenerationModelList={textGenerationModelList} - /> - ) - : ( - <> - - {showStartNodeLimitHint && ( -
-

- {t('publishLimit.startNodeTitlePrefix', { ns: 'workflow' })} - {t('publishLimit.startNodeTitleSuffix', { ns: 'workflow' })} -

-

- {t('publishLimit.startNodeDesc', { ns: 'workflow' })} -

- -
- )} - - )} -
- {(systemFeatures.webapp_auth.enabled && (isGettingUserCanAccessApp || isGettingAppWhiteListSubjects)) - ?
- : ( - <> - - {systemFeatures.webapp_auth.enabled && ( -
-
-

{t('publishApp.title', { ns: 'app' })}

-
-
{ - setShowAppAccessControl(true) - }} - > -
- -
- {!isAppAccessSet &&

{t('publishApp.notSet', { ns: 'app' })}

} -
- -
-
- {!isAppAccessSet &&

{t('publishApp.notSetDesc', { ns: 'app' })}

} -
- )} - { - // Hide run/batch run app buttons when there is a trigger node. - !hasTriggerNode && ( -
- - } - > - {t('common.runApp', { ns: 'workflow' })} - - - {appDetail?.mode === AppModeEnum.WORKFLOW || appDetail?.mode === AppModeEnum.COMPLETION - ? ( - - } - > - {t('common.batchRunApp', { ns: 'workflow' })} - - - ) - : ( - { - setEmbeddingModalOpen(true) - handleTrigger() - }} - disabled={!publishedAt} - icon={} - > - {t('common.embedIntoSite', { ns: 'workflow' })} - - )} - - { - if (publishedAt) - handleOpenInExplore() - }} - disabled={disabledFunctionButton} - icon={} - > - {t('common.openInExplore', { ns: 'workflow' })} - - - - } - > - {t('common.accessAPIReference', { ns: 'workflow' })} - - - {appDetail?.mode === AppModeEnum.WORKFLOW && !hasHumanInputNode && ( - - )} -
- ) - } - - )} + + setShowAppAccessControl(true)} + /> + { + setEmbeddingModalOpen(true) + handleTrigger() + }} + handleOpenInExplore={handleOpenInExplore} + handlePublish={handlePublish} + hasHumanInputNode={hasHumanInputNode} + hasTriggerNode={hasTriggerNode} + inputs={inputs} + missingStartNode={missingStartNode} + onRefreshData={onRefreshData} + outputs={outputs} + published={published} + publishedAt={publishedAt} + toolPublished={toolPublished} + workflowToolAvailable={workflowToolAvailable} + workflowToolMessage={workflowToolMessage} + />
& { + formatTimeFromNow: (value: number) => string + handlePublish: (params?: ModelAndParameter | PublishWorkflowParams) => Promise + handleRestore: () => Promise + isChatApp: boolean + published: boolean + publishShortcut: string[] + upgradeHighlightStyle: CSSProperties + } + +type AccessSectionProps = { + enabled: boolean + isAppAccessSet: boolean + isLoading: boolean + accessMode?: keyof typeof ACCESS_MODE_MAP + onClick: () => void +} + +type ActionsSectionProps = Pick & { + appDetail: { + id?: string + icon?: string + icon_type?: string | null + icon_background?: string | null + description?: string + mode?: AppModeEnum + name?: string + } | null | undefined + appURL: string + disabledFunctionButton: boolean + disabledFunctionTooltip?: string + handleEmbed: () => void + handleOpenInExplore: () => void + handlePublish: (params?: ModelAndParameter | PublishWorkflowParams) => Promise + published: boolean + workflowToolMessage?: string + } + +export const AccessModeDisplay = ({ mode }: { mode?: keyof typeof ACCESS_MODE_MAP }) => { + const { t } = useTranslation() + + if (!mode || !ACCESS_MODE_MAP[mode]) + return null + + const { icon, label } = ACCESS_MODE_MAP[mode] + + return ( + <> + +
+ {t(`accessControlDialog.accessItems.${label}`, { ns: 'app' })} +
+ + ) +} + +export const PublisherSummarySection = ({ + debugWithMultipleModel = false, + draftUpdatedAt, + formatTimeFromNow, + handlePublish, + handleRestore, + isChatApp, + multipleModelConfigs = [], + publishDisabled = false, + published, + publishedAt, + publishShortcut, + startNodeLimitExceeded = false, + upgradeHighlightStyle, +}: SummarySectionProps) => { + const { t } = useTranslation() + + return ( +
+
+ {publishedAt ? t('common.latestPublished', { ns: 'workflow' }) : t('common.currentDraftUnpublished', { ns: 'workflow' })} +
+ {publishedAt + ? ( +
+
+ {t('common.publishedAt', { ns: 'workflow' })} + {' '} + {formatTimeFromNow(publishedAt)} +
+ {isChatApp && ( + + )} +
+ ) + : ( +
+ {t('common.autoSaved', { ns: 'workflow' })} + {' '} + · + {Boolean(draftUpdatedAt) && formatTimeFromNow(draftUpdatedAt!)} +
+ )} + {debugWithMultipleModel + ? ( + handlePublish(item)} + /> + ) + : ( + <> + + {startNodeLimitExceeded && ( +
+

+ {t('publishLimit.startNodeTitlePrefix', { ns: 'workflow' })} + {t('publishLimit.startNodeTitleSuffix', { ns: 'workflow' })} +

+

+ {t('publishLimit.startNodeDesc', { ns: 'workflow' })} +

+ +
+ )} + + )} +
+ ) +} + +export const PublisherAccessSection = ({ + enabled, + isAppAccessSet, + isLoading, + accessMode, + onClick, +}: AccessSectionProps) => { + const { t } = useTranslation() + + if (isLoading) + return
+ + return ( + <> + + {enabled && ( +
+
+

{t('publishApp.title', { ns: 'app' })}

+
+
+
+ +
+ {!isAppAccessSet &&

{t('publishApp.notSet', { ns: 'app' })}

} +
+ +
+
+ {!isAppAccessSet &&

{t('publishApp.notSetDesc', { ns: 'app' })}

} +
+ )} + + ) +} + +const ActionTooltip = ({ + disabled, + tooltip, + children, +}: { + disabled: boolean + tooltip?: ReactNode + children: ReactNode +}) => { + if (!disabled || !tooltip) + return <>{children} + + return ( + + {children}
} /> + + {tooltip} + + + ) +} + +export const PublisherActionsSection = ({ + appDetail, + appURL, + disabledFunctionButton, + disabledFunctionTooltip, + handleEmbed, + handleOpenInExplore, + handlePublish, + hasHumanInputNode = false, + hasTriggerNode = false, + inputs, + missingStartNode = false, + onRefreshData, + outputs, + published, + publishedAt, + toolPublished, + workflowToolAvailable = true, + workflowToolMessage, +}: ActionsSectionProps) => { + const { t } = useTranslation() + + if (hasTriggerNode) + return null + + const workflowToolDisabled = !publishedAt || !workflowToolAvailable + + return ( +
+ + } + > + {t('common.runApp', { ns: 'workflow' })} + + + {appDetail?.mode === AppModeEnum.WORKFLOW || appDetail?.mode === AppModeEnum.COMPLETION + ? ( + + } + > + {t('common.batchRunApp', { ns: 'workflow' })} + + + ) + : ( + } + > + {t('common.embedIntoSite', { ns: 'workflow' })} + + )} + + { + if (publishedAt) + handleOpenInExplore() + }} + disabled={disabledFunctionButton} + icon={} + > + {t('common.openInExplore', { ns: 'workflow' })} + + + + } + > + {t('common.accessAPIReference', { ns: 'workflow' })} + + + {appDetail?.mode === AppModeEnum.WORKFLOW && !hasHumanInputNode && ( + + )} +
+ ) +} diff --git a/web/app/components/app/app-publisher/suggested-action.tsx b/web/app/components/app/app-publisher/suggested-action.tsx index 71ddceec01..56879d8fea 100644 --- a/web/app/components/app/app-publisher/suggested-action.tsx +++ b/web/app/components/app/app-publisher/suggested-action.tsx @@ -2,7 +2,7 @@ import type { HTMLProps, PropsWithChildren } from 'react' import { RiArrowRightUpLine } from '@remixicon/react' import { cn } from '@/utils/classnames' -export type SuggestedActionProps = PropsWithChildren & { +type SuggestedActionProps = PropsWithChildren & { icon?: React.ReactNode link?: string disabled?: boolean diff --git a/web/app/components/app/app-publisher/utils.ts b/web/app/components/app/app-publisher/utils.ts new file mode 100644 index 0000000000..5ce8a4425d --- /dev/null +++ b/web/app/components/app/app-publisher/utils.ts @@ -0,0 +1,84 @@ +import type { TFunction } from 'i18next' +import type { I18nKeysByPrefix } from '@/types/i18n' +import { AccessMode } from '@/models/access-control' +import { AppModeEnum } from '@/types/app' +import { basePath } from '@/utils/var' + +type AccessSubjectsLike = { + groups?: unknown[] + members?: unknown[] +} | null | undefined + +type AppDetailLike = { + access_mode?: AccessMode + mode?: AppModeEnum +} + +type AccessModeLabel = I18nKeysByPrefix<'app', 'accessControlDialog.accessItems.'> + +export const ACCESS_MODE_MAP: Record = { + [AccessMode.ORGANIZATION]: { + label: 'organization', + icon: 'i-ri-building-line', + }, + [AccessMode.SPECIFIC_GROUPS_MEMBERS]: { + label: 'specific', + icon: 'i-ri-lock-line', + }, + [AccessMode.PUBLIC]: { + label: 'anyone', + icon: 'i-ri-global-line', + }, + [AccessMode.EXTERNAL_MEMBERS]: { + label: 'external', + icon: 'i-ri-verified-badge-line', + }, +} + +export const getPublisherAppMode = (mode?: AppModeEnum) => { + if (mode !== AppModeEnum.COMPLETION && mode !== AppModeEnum.WORKFLOW) + return AppModeEnum.CHAT + + return mode +} + +export const getPublisherAppUrl = ({ + appBaseUrl, + accessToken, + mode, +}: { + appBaseUrl: string + accessToken: string + mode?: AppModeEnum +}) => `${appBaseUrl}${basePath}/${getPublisherAppMode(mode)}/${accessToken}` + +export const isPublisherAccessConfigured = (appDetail: AppDetailLike | null | undefined, appAccessSubjects: AccessSubjectsLike) => { + if (!appDetail || !appAccessSubjects) + return true + + if (appDetail.access_mode !== AccessMode.SPECIFIC_GROUPS_MEMBERS) + return true + + return Boolean(appAccessSubjects.groups?.length || appAccessSubjects.members?.length) +} + +export const getDisabledFunctionTooltip = ({ + t, + publishedAt, + missingStartNode, + noAccessPermission, +}: { + t: TFunction + publishedAt?: number + missingStartNode: boolean + noAccessPermission: boolean +}) => { + if (!publishedAt) + return t('notPublishedYet', { ns: 'app' }) + if (missingStartNode) + return t('noUserInputNode', { ns: 'app' }) + if (noAccessPermission) + return t('noAccessPermission', { ns: 'app' }) + + return undefined +} diff --git a/web/app/components/app/configuration/__tests__/configuration-view.spec.tsx b/web/app/components/app/configuration/__tests__/configuration-view.spec.tsx new file mode 100644 index 0000000000..459df5d063 --- /dev/null +++ b/web/app/components/app/configuration/__tests__/configuration-view.spec.tsx @@ -0,0 +1,283 @@ +import type { ComponentProps } from 'react' +import type { ConfigurationViewModel } from '../hooks/use-configuration' +import type AppPublisher from '@/app/components/app/app-publisher/features-wrapper' +import type ConfigContext from '@/context/debug-configuration' +import { fireEvent, render, screen } from '@testing-library/react' +import * as React from 'react' +import { AppModeEnum, ModelModeType } from '@/types/app' +import ConfigurationView from '../configuration-view' + +vi.mock('@/app/components/app/app-publisher/features-wrapper', () => ({ + default: () =>
, +})) + +vi.mock('@/app/components/app/configuration/config', () => ({ + default: () =>
, +})) + +vi.mock('@/app/components/app/configuration/debug', () => ({ + default: () =>
, +})) + +vi.mock('@/app/components/app/configuration/config/agent-setting-button', () => ({ + default: () =>
, +})) + +vi.mock('@/app/components/header/account-setting/model-provider-page/model-parameter-modal', () => ({ + default: () =>
, +})) + +vi.mock('@/app/components/app/configuration/dataset-config/select-dataset', () => ({ + default: () =>
, +})) + +vi.mock('@/app/components/app/configuration/config-prompt/conversation-history/edit-modal', () => ({ + default: () =>
, +})) + +vi.mock('@/app/components/base/features/new-feature-panel', () => ({ + default: () =>
, +})) + +vi.mock('@/app/components/workflow/plugin-dependency', () => ({ + default: () =>
, +})) + +const createContextValue = (): ComponentProps['value'] => ({ + appId: 'app-1', + isAPIKeySet: true, + isTrailFinished: false, + mode: AppModeEnum.CHAT, + modelModeType: ModelModeType.chat, + promptMode: 'simple' as never, + setPromptMode: vi.fn(), + isAdvancedMode: false, + isAgent: false, + isFunctionCall: false, + isOpenAI: false, + collectionList: [], + canReturnToSimpleMode: false, + setCanReturnToSimpleMode: vi.fn(), + chatPromptConfig: { prompt: [] } as never, + completionPromptConfig: { + prompt: { text: '' }, + conversation_histories_role: { + user_prefix: 'user', + assistant_prefix: 'assistant', + }, + } as never, + currentAdvancedPrompt: [], + setCurrentAdvancedPrompt: vi.fn(), + showHistoryModal: vi.fn(), + conversationHistoriesRole: { + user_prefix: 'user', + assistant_prefix: 'assistant', + }, + setConversationHistoriesRole: vi.fn(), + hasSetBlockStatus: { + context: false, + history: true, + query: true, + }, + conversationId: '', + setConversationId: vi.fn(), + introduction: '', + setIntroduction: vi.fn(), + suggestedQuestions: [], + setSuggestedQuestions: vi.fn(), + controlClearChatMessage: 0, + setControlClearChatMessage: vi.fn(), + prevPromptConfig: { + prompt_template: '', + prompt_variables: [], + }, + setPrevPromptConfig: vi.fn(), + moreLikeThisConfig: { enabled: false }, + setMoreLikeThisConfig: vi.fn(), + suggestedQuestionsAfterAnswerConfig: { enabled: false }, + setSuggestedQuestionsAfterAnswerConfig: vi.fn(), + speechToTextConfig: { enabled: false }, + setSpeechToTextConfig: vi.fn(), + textToSpeechConfig: { enabled: false, voice: '', language: '' }, + setTextToSpeechConfig: vi.fn(), + citationConfig: { enabled: false }, + setCitationConfig: vi.fn(), + annotationConfig: { + id: '', + enabled: false, + score_threshold: 0.5, + embedding_model: { + embedding_model_name: '', + embedding_provider_name: '', + }, + }, + setAnnotationConfig: vi.fn(), + moderationConfig: { enabled: false }, + setModerationConfig: vi.fn(), + externalDataToolsConfig: [], + setExternalDataToolsConfig: vi.fn(), + formattingChanged: false, + setFormattingChanged: vi.fn(), + inputs: {}, + setInputs: vi.fn(), + query: '', + setQuery: vi.fn(), + completionParams: {}, + setCompletionParams: vi.fn(), + modelConfig: { + provider: 'openai', + model_id: 'gpt-4o', + mode: ModelModeType.chat, + configs: { + prompt_template: '', + prompt_variables: [], + }, + chat_prompt_config: null, + completion_prompt_config: null, + opening_statement: '', + more_like_this: null, + suggested_questions: [], + suggested_questions_after_answer: null, + speech_to_text: null, + text_to_speech: null, + file_upload: null, + retriever_resource: null, + sensitive_word_avoidance: null, + annotation_reply: null, + external_data_tools: [], + system_parameters: { + audio_file_size_limit: 1, + file_size_limit: 1, + image_file_size_limit: 1, + video_file_size_limit: 1, + workflow_file_upload_limit: 1, + }, + dataSets: [], + agentConfig: { + enabled: false, + strategy: 'react', + max_iteration: 1, + tools: [], + }, + } as never, + setModelConfig: vi.fn(), + dataSets: [], + setDataSets: vi.fn(), + showSelectDataSet: vi.fn(), + datasetConfigs: { + retrieval_model: 'multiple', + reranking_model: { + reranking_provider_name: '', + reranking_model_name: '', + }, + top_k: 3, + score_threshold_enabled: false, + score_threshold: 0.5, + datasets: { datasets: [] }, + } as never, + datasetConfigsRef: { current: {} as never }, + setDatasetConfigs: vi.fn(), + hasSetContextVar: false, + isShowVisionConfig: false, + visionConfig: { + enabled: false, + number_limits: 1, + detail: 'low', + transfer_methods: ['local_file'], + } as never, + setVisionConfig: vi.fn(), + isAllowVideoUpload: false, + isShowDocumentConfig: false, + isShowAudioConfig: false, + rerankSettingModalOpen: false, + setRerankSettingModalOpen: vi.fn(), +}) + +const createViewModel = (overrides: Partial = {}): ConfigurationViewModel => ({ + appPublisherProps: { + publishDisabled: false, + publishedAt: 0, + debugWithMultipleModel: false, + multipleModelConfigs: [], + onPublish: vi.fn(), + publishedConfig: { + modelConfig: createContextValue().modelConfig, + completionParams: {}, + }, + resetAppConfig: vi.fn(), + } as ComponentProps, + contextValue: createContextValue(), + featuresData: { + moreLikeThis: { enabled: false }, + opening: { enabled: false, opening_statement: '', suggested_questions: [] }, + moderation: { enabled: false }, + speech2text: { enabled: false }, + text2speech: { enabled: false, voice: '', language: '' }, + file: { enabled: false, image: { enabled: false, detail: 'high', number_limits: 3, transfer_methods: ['local_file'] } } as never, + suggested: { enabled: false }, + citation: { enabled: false }, + annotationReply: { enabled: false }, + }, + isAgent: false, + isAdvancedMode: false, + isMobile: false, + isShowDebugPanel: false, + isShowHistoryModal: false, + isShowSelectDataSet: false, + modelConfig: createContextValue().modelConfig, + multipleModelConfigs: [], + onAutoAddPromptVariable: vi.fn(), + onAgentSettingChange: vi.fn(), + onCloseFeaturePanel: vi.fn(), + onCloseHistoryModal: vi.fn(), + onCloseSelectDataSet: vi.fn(), + onCompletionParamsChange: vi.fn(), + onConfirmUseGPT4: vi.fn(), + onEnableMultipleModelDebug: vi.fn(), + onFeaturesChange: vi.fn(), + onHideDebugPanel: vi.fn(), + onModelChange: vi.fn(), + onMultipleModelConfigsChange: vi.fn(), + onOpenAccountSettings: vi.fn(), + onOpenDebugPanel: vi.fn(), + onSaveHistory: vi.fn(), + onSelectDataSets: vi.fn(), + promptVariables: [], + selectedIds: [], + showAppConfigureFeaturesModal: false, + showLoading: false, + showUseGPT4Confirm: false, + setShowUseGPT4Confirm: vi.fn(), + ...overrides, +}) + +describe('ConfigurationView', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should render a loading state before configuration data is ready', () => { + render() + + expect(screen.getByRole('status', { name: 'appApi.loading' })).toBeInTheDocument() + expect(screen.queryByTestId('app-publisher')).not.toBeInTheDocument() + }) + + it('should open the mobile debug panel from the header button', () => { + const onOpenDebugPanel = vi.fn() + render() + + fireEvent.click(screen.getByRole('button', { name: /appDebug.operation.debugConfig/i })) + + expect(onOpenDebugPanel).toHaveBeenCalledTimes(1) + }) + + it('should close the GPT-4 confirmation dialog when cancel is clicked', () => { + const setShowUseGPT4Confirm = vi.fn() + render() + + fireEvent.click(screen.getByRole('button', { name: /operation.cancel/i })) + + expect(setShowUseGPT4Confirm).toHaveBeenCalledWith(false) + }) +}) diff --git a/web/app/components/app/configuration/__tests__/index.spec.tsx b/web/app/components/app/configuration/__tests__/index.spec.tsx new file mode 100644 index 0000000000..b855725b1d --- /dev/null +++ b/web/app/components/app/configuration/__tests__/index.spec.tsx @@ -0,0 +1,32 @@ +import { render } from '@testing-library/react' +import * as React from 'react' +import { useConfiguration } from '../hooks/use-configuration' +import Configuration from '../index' + +const mockView = vi.fn((_: unknown) =>
) + +vi.mock('../configuration-view', () => ({ + default: (props: unknown) => mockView(props), +})) + +vi.mock('../hooks/use-configuration', () => ({ + useConfiguration: vi.fn(), +})) + +describe('Configuration entry', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should pass the hook view model into ConfigurationView', () => { + const viewModel = { + showLoading: true, + } + vi.mocked(useConfiguration).mockReturnValue(viewModel as never) + + render() + + expect(useConfiguration).toHaveBeenCalledTimes(1) + expect(mockView).toHaveBeenCalledWith(viewModel) + }) +}) diff --git a/web/app/components/app/configuration/__tests__/utils.spec.ts b/web/app/components/app/configuration/__tests__/utils.spec.ts new file mode 100644 index 0000000000..65a6192177 --- /dev/null +++ b/web/app/components/app/configuration/__tests__/utils.spec.ts @@ -0,0 +1,226 @@ +import type { ModelConfig } from '@/models/debug' +import { AppModeEnum, ModelModeType, Resolution, TransferMethod } from '@/types/app' +import { buildConfigurationFeaturesData, getConfigurationPublishingState, withCollectionIconBasePath } from '../utils' + +const createModelConfig = (overrides: Partial = {}): ModelConfig => ({ + provider: 'openai', + model_id: 'gpt-4o', + mode: ModelModeType.chat, + configs: { + prompt_template: 'Hello', + prompt_variables: [], + }, + chat_prompt_config: { + prompt: [], + } as ModelConfig['chat_prompt_config'], + completion_prompt_config: { + prompt: { text: '' }, + conversation_histories_role: { + user_prefix: 'user', + assistant_prefix: 'assistant', + }, + } as ModelConfig['completion_prompt_config'], + opening_statement: '', + more_like_this: { enabled: false }, + suggested_questions: [], + suggested_questions_after_answer: { enabled: false }, + speech_to_text: { enabled: false }, + text_to_speech: { enabled: false, voice: '', language: '' }, + file_upload: null, + retriever_resource: { enabled: false }, + sensitive_word_avoidance: { enabled: false }, + annotation_reply: null, + external_data_tools: [], + system_parameters: { + audio_file_size_limit: 1, + file_size_limit: 1, + image_file_size_limit: 1, + video_file_size_limit: 1, + workflow_file_upload_limit: 1, + }, + dataSets: [], + agentConfig: { + enabled: false, + strategy: 'react', + max_iteration: 1, + tools: [], + } as ModelConfig['agentConfig'], + ...overrides, +}) + +describe('configuration utils', () => { + describe('withCollectionIconBasePath', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should prefix relative collection icons with the base path', () => { + const result = withCollectionIconBasePath([ + { id: 'tool-1', icon: '/icons/tool.svg' }, + { id: 'tool-2', icon: '/console/icons/prefixed.svg' }, + ] as never, '/console') + + expect(result[0].icon).toBe('/console/icons/tool.svg') + expect(result[1].icon).toBe('/console/icons/prefixed.svg') + }) + }) + + describe('buildConfigurationFeaturesData', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should derive feature toggles and upload fallbacks from model config', () => { + const result = buildConfigurationFeaturesData(createModelConfig({ + opening_statement: 'Welcome', + suggested_questions: ['How are you?'], + file_upload: { + enabled: true, + image: { + enabled: true, + detail: Resolution.low, + number_limits: 2, + transfer_methods: [TransferMethod.local_file], + }, + allowed_file_types: ['image'], + allowed_file_extensions: ['.png'], + allowed_file_upload_methods: [TransferMethod.local_file], + number_limits: 2, + }, + }), undefined) + + expect(result.opening).toEqual({ + enabled: true, + opening_statement: 'Welcome', + suggested_questions: ['How are you?'], + }) + expect(result.file).toBeDefined() + expect(result.file!.enabled).toBe(true) + expect(result.file!.image!.detail).toBe(Resolution.low) + expect(result.file!.allowed_file_extensions).toEqual(['.png']) + }) + }) + + describe('getConfigurationPublishingState', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should block publish when advanced completion mode is missing required blocks', () => { + const result = getConfigurationPublishingState({ + chatPromptConfig: { + prompt: [], + } as never, + completionPromptConfig: { + prompt: { text: 'Answer' }, + conversation_histories_role: { + user_prefix: 'user', + assistant_prefix: 'assistant', + }, + } as never, + hasSetBlockStatus: { + context: false, + history: false, + query: false, + }, + hasSetContextVar: false, + hasSelectedDataSets: false, + isAdvancedMode: true, + mode: AppModeEnum.CHAT, + modelModeType: ModelModeType.completion, + promptTemplate: 'ignored', + }) + + expect(result.promptEmpty).toBe(false) + expect(result.cannotPublish).toBe(true) + }) + + it('should require a context variable only for completion apps with selected datasets', () => { + const result = getConfigurationPublishingState({ + chatPromptConfig: { + prompt: [], + } as never, + completionPromptConfig: { + prompt: { text: 'Completion prompt' }, + conversation_histories_role: { + user_prefix: 'user', + assistant_prefix: 'assistant', + }, + } as never, + hasSetBlockStatus: { + context: false, + history: true, + query: true, + }, + hasSetContextVar: false, + hasSelectedDataSets: true, + isAdvancedMode: false, + mode: AppModeEnum.COMPLETION, + modelModeType: ModelModeType.completion, + promptTemplate: 'Prompt', + }) + + expect(result.promptEmpty).toBe(false) + expect(result.cannotPublish).toBe(false) + expect(result.contextVarEmpty).toBe(true) + }) + + it('should treat advanced completion chat prompts as empty when every segment is blank', () => { + const result = getConfigurationPublishingState({ + chatPromptConfig: { + prompt: [{ text: '' }, { text: '' }], + } as never, + completionPromptConfig: { + prompt: { text: 'ignored' }, + conversation_histories_role: { + user_prefix: 'user', + assistant_prefix: 'assistant', + }, + } as never, + hasSetBlockStatus: { + context: true, + history: true, + query: true, + }, + hasSetContextVar: true, + hasSelectedDataSets: false, + isAdvancedMode: true, + mode: AppModeEnum.COMPLETION, + modelModeType: ModelModeType.chat, + promptTemplate: 'ignored', + }) + + expect(result.promptEmpty).toBe(true) + expect(result.cannotPublish).toBe(true) + }) + + it('should treat advanced completion text prompts as empty when the completion prompt is missing', () => { + const result = getConfigurationPublishingState({ + chatPromptConfig: { + prompt: [{ text: 'ignored' }], + } as never, + completionPromptConfig: { + prompt: { text: '' }, + conversation_histories_role: { + user_prefix: 'user', + assistant_prefix: 'assistant', + }, + } as never, + hasSetBlockStatus: { + context: true, + history: true, + query: true, + }, + hasSetContextVar: true, + hasSelectedDataSets: false, + isAdvancedMode: true, + mode: AppModeEnum.COMPLETION, + modelModeType: ModelModeType.completion, + promptTemplate: 'ignored', + }) + + expect(result.promptEmpty).toBe(true) + expect(result.cannotPublish).toBe(true) + }) + }) +}) diff --git a/web/app/components/app/configuration/base/feature-panel/index.spec.tsx b/web/app/components/app/configuration/base/feature-panel/__tests__/index.spec.tsx similarity index 98% rename from web/app/components/app/configuration/base/feature-panel/index.spec.tsx rename to web/app/components/app/configuration/base/feature-panel/__tests__/index.spec.tsx index 7e1b661399..0daf638b49 100644 --- a/web/app/components/app/configuration/base/feature-panel/index.spec.tsx +++ b/web/app/components/app/configuration/base/feature-panel/__tests__/index.spec.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react' -import FeaturePanel from './index' +import FeaturePanel from '../index' describe('FeaturePanel', () => { // Rendering behavior for standard layout. diff --git a/web/app/components/app/configuration/base/feature-panel/index.tsx b/web/app/components/app/configuration/base/feature-panel/index.tsx index 7f337c1572..77ee7bc8dd 100644 --- a/web/app/components/app/configuration/base/feature-panel/index.tsx +++ b/web/app/components/app/configuration/base/feature-panel/index.tsx @@ -3,7 +3,7 @@ import type { FC, ReactNode } from 'react' import * as React from 'react' import { cn } from '@/utils/classnames' -export type IFeaturePanelProps = { +type IFeaturePanelProps = { className?: string headerIcon?: ReactNode title: ReactNode diff --git a/web/app/components/app/configuration/base/group-name/index.spec.tsx b/web/app/components/app/configuration/base/group-name/__tests__/index.spec.tsx similarity index 92% rename from web/app/components/app/configuration/base/group-name/index.spec.tsx rename to web/app/components/app/configuration/base/group-name/__tests__/index.spec.tsx index be698c3233..ce1ee7a18a 100644 --- a/web/app/components/app/configuration/base/group-name/index.spec.tsx +++ b/web/app/components/app/configuration/base/group-name/__tests__/index.spec.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react' -import GroupName from './index' +import GroupName from '../index' describe('GroupName', () => { beforeEach(() => { diff --git a/web/app/components/app/configuration/base/group-name/index.tsx b/web/app/components/app/configuration/base/group-name/index.tsx index b21b0c5825..210ba4ca87 100644 --- a/web/app/components/app/configuration/base/group-name/index.tsx +++ b/web/app/components/app/configuration/base/group-name/index.tsx @@ -2,7 +2,7 @@ import type { FC } from 'react' import * as React from 'react' -export type IGroupNameProps = { +type IGroupNameProps = { name: string } diff --git a/web/app/components/app/configuration/base/operation-btn/index.spec.tsx b/web/app/components/app/configuration/base/operation-btn/__tests__/index.spec.tsx similarity index 98% rename from web/app/components/app/configuration/base/operation-btn/index.spec.tsx rename to web/app/components/app/configuration/base/operation-btn/__tests__/index.spec.tsx index 8e254d261b..325aebe6c0 100644 --- a/web/app/components/app/configuration/base/operation-btn/index.spec.tsx +++ b/web/app/components/app/configuration/base/operation-btn/__tests__/index.spec.tsx @@ -1,5 +1,5 @@ import { fireEvent, render, screen } from '@testing-library/react' -import OperationBtn from './index' +import OperationBtn from '../index' vi.mock('@remixicon/react', () => ({ RiAddLine: (props: { className?: string }) => ( diff --git a/web/app/components/app/configuration/base/operation-btn/index.tsx b/web/app/components/app/configuration/base/operation-btn/index.tsx index d33b632071..e3bdfd01ba 100644 --- a/web/app/components/app/configuration/base/operation-btn/index.tsx +++ b/web/app/components/app/configuration/base/operation-btn/index.tsx @@ -9,7 +9,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { cn } from '@/utils/classnames' -export type IOperationBtnProps = { +type IOperationBtnProps = { className?: string type: 'add' | 'edit' actionName?: string diff --git a/web/app/components/app/configuration/base/var-highlight/index.spec.tsx b/web/app/components/app/configuration/base/var-highlight/__tests__/index.spec.tsx similarity index 97% rename from web/app/components/app/configuration/base/var-highlight/index.spec.tsx rename to web/app/components/app/configuration/base/var-highlight/__tests__/index.spec.tsx index 77fe1f2b28..1add8601c4 100644 --- a/web/app/components/app/configuration/base/var-highlight/index.spec.tsx +++ b/web/app/components/app/configuration/base/var-highlight/__tests__/index.spec.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react' -import VarHighlight, { varHighlightHTML } from './index' +import VarHighlight, { varHighlightHTML } from '../index' describe('VarHighlight', () => { beforeEach(() => { diff --git a/web/app/components/app/configuration/base/var-highlight/index.tsx b/web/app/components/app/configuration/base/var-highlight/index.tsx index 697007d0b0..fda58f0b65 100644 --- a/web/app/components/app/configuration/base/var-highlight/index.tsx +++ b/web/app/components/app/configuration/base/var-highlight/index.tsx @@ -4,7 +4,7 @@ import * as React from 'react' import s from './style.module.css' -export type IVarHighlightProps = { +type IVarHighlightProps = { name: string className?: string } diff --git a/web/app/components/app/configuration/base/var-highlight/style.module.css b/web/app/components/app/configuration/base/var-highlight/style.module.css index b7a66085ce..2bcef0dabb 100644 --- a/web/app/components/app/configuration/base/var-highlight/style.module.css +++ b/web/app/components/app/configuration/base/var-highlight/style.module.css @@ -1,5 +1,3 @@ -@reference "../../../../../styles/globals.css"; - .item { background-color: rgba(21, 94, 239, 0.05); } diff --git a/web/app/components/app/configuration/base/warning-mask/cannot-query-dataset.spec.tsx b/web/app/components/app/configuration/base/warning-mask/__tests__/cannot-query-dataset.spec.tsx similarity index 94% rename from web/app/components/app/configuration/base/warning-mask/cannot-query-dataset.spec.tsx rename to web/app/components/app/configuration/base/warning-mask/__tests__/cannot-query-dataset.spec.tsx index 730b251e67..161bd5073d 100644 --- a/web/app/components/app/configuration/base/warning-mask/cannot-query-dataset.spec.tsx +++ b/web/app/components/app/configuration/base/warning-mask/__tests__/cannot-query-dataset.spec.tsx @@ -1,6 +1,6 @@ import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' -import CannotQueryDataset from './cannot-query-dataset' +import CannotQueryDataset from '../cannot-query-dataset' describe('CannotQueryDataset WarningMask', () => { it('should render dataset warning copy and action button', () => { diff --git a/web/app/components/app/configuration/base/warning-mask/formatting-changed.spec.tsx b/web/app/components/app/configuration/base/warning-mask/__tests__/formatting-changed.spec.tsx similarity index 95% rename from web/app/components/app/configuration/base/warning-mask/formatting-changed.spec.tsx rename to web/app/components/app/configuration/base/warning-mask/__tests__/formatting-changed.spec.tsx index 9b5a5d93e1..81655f2d99 100644 --- a/web/app/components/app/configuration/base/warning-mask/formatting-changed.spec.tsx +++ b/web/app/components/app/configuration/base/warning-mask/__tests__/formatting-changed.spec.tsx @@ -1,6 +1,6 @@ import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' -import FormattingChanged from './formatting-changed' +import FormattingChanged from '../formatting-changed' describe('FormattingChanged WarningMask', () => { it('should display translation text and both actions', () => { diff --git a/web/app/components/app/configuration/base/warning-mask/has-not-set-api.spec.tsx b/web/app/components/app/configuration/base/warning-mask/__tests__/has-not-set-api.spec.tsx similarity index 93% rename from web/app/components/app/configuration/base/warning-mask/has-not-set-api.spec.tsx rename to web/app/components/app/configuration/base/warning-mask/__tests__/has-not-set-api.spec.tsx index abcf5795d0..5995f3472f 100644 --- a/web/app/components/app/configuration/base/warning-mask/has-not-set-api.spec.tsx +++ b/web/app/components/app/configuration/base/warning-mask/__tests__/has-not-set-api.spec.tsx @@ -1,6 +1,6 @@ import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' -import HasNotSetAPI from './has-not-set-api' +import HasNotSetAPI from '../has-not-set-api' describe('HasNotSetAPI', () => { it('should render the empty state copy', () => { diff --git a/web/app/components/app/configuration/base/warning-mask/index.spec.tsx b/web/app/components/app/configuration/base/warning-mask/__tests__/index.spec.tsx similarity index 95% rename from web/app/components/app/configuration/base/warning-mask/index.spec.tsx rename to web/app/components/app/configuration/base/warning-mask/__tests__/index.spec.tsx index cb8ef0b678..d287a790e6 100644 --- a/web/app/components/app/configuration/base/warning-mask/index.spec.tsx +++ b/web/app/components/app/configuration/base/warning-mask/__tests__/index.spec.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react' import * as React from 'react' -import WarningMask from './index' +import WarningMask from '../index' describe('WarningMask', () => { // Rendering of title, description, and footer content diff --git a/web/app/components/app/configuration/base/warning-mask/cannot-query-dataset.tsx b/web/app/components/app/configuration/base/warning-mask/cannot-query-dataset.tsx index 791230566b..d5fbd9e78f 100644 --- a/web/app/components/app/configuration/base/warning-mask/cannot-query-dataset.tsx +++ b/web/app/components/app/configuration/base/warning-mask/cannot-query-dataset.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import WarningMask from '.' -export type IFormattingChangedProps = { +type IFormattingChangedProps = { onConfirm: () => void } diff --git a/web/app/components/app/configuration/base/warning-mask/formatting-changed.tsx b/web/app/components/app/configuration/base/warning-mask/formatting-changed.tsx index 1fe7b9c182..56ccae5ade 100644 --- a/web/app/components/app/configuration/base/warning-mask/formatting-changed.tsx +++ b/web/app/components/app/configuration/base/warning-mask/formatting-changed.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import WarningMask from '.' -export type IFormattingChangedProps = { +type IFormattingChangedProps = { onConfirm: () => void onCancel: () => void } diff --git a/web/app/components/app/configuration/base/warning-mask/has-not-set-api.tsx b/web/app/components/app/configuration/base/warning-mask/has-not-set-api.tsx index 06c81a9f95..25587c5e58 100644 --- a/web/app/components/app/configuration/base/warning-mask/has-not-set-api.tsx +++ b/web/app/components/app/configuration/base/warning-mask/has-not-set-api.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react' import * as React from 'react' import { useTranslation } from 'react-i18next' -export type IHasNotSetAPIProps = { +type IHasNotSetAPIProps = { onSetting: () => void } diff --git a/web/app/components/app/configuration/base/warning-mask/index.tsx b/web/app/components/app/configuration/base/warning-mask/index.tsx index 6d6aeceb97..5275f022cb 100644 --- a/web/app/components/app/configuration/base/warning-mask/index.tsx +++ b/web/app/components/app/configuration/base/warning-mask/index.tsx @@ -4,7 +4,7 @@ import * as React from 'react' import s from './style.module.css' -export type IWarningMaskProps = { +type IWarningMaskProps = { title: string description: string footer: React.ReactNode diff --git a/web/app/components/app/configuration/base/warning-mask/style.module.css b/web/app/components/app/configuration/base/warning-mask/style.module.css index f922ec332f..a2c394de2a 100644 --- a/web/app/components/app/configuration/base/warning-mask/style.module.css +++ b/web/app/components/app/configuration/base/warning-mask/style.module.css @@ -1,5 +1,3 @@ -@reference "../../../../../styles/globals.css"; - .mask { backdrop-filter: blur(2px); } diff --git a/web/app/components/app/configuration/config-prompt/__tests__/advanced-prompt-input.spec.tsx b/web/app/components/app/configuration/config-prompt/__tests__/advanced-prompt-input.spec.tsx new file mode 100644 index 0000000000..413721ee2e --- /dev/null +++ b/web/app/components/app/configuration/config-prompt/__tests__/advanced-prompt-input.spec.tsx @@ -0,0 +1,228 @@ +/* eslint-disable ts/no-explicit-any */ +import type { ReactNode } from 'react' +import type { PromptRole } from '@/models/debug' +import { fireEvent, render, screen } from '@testing-library/react' +import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '@/app/components/base/prompt-editor/plugins/variable-block' +import ConfigContext from '@/context/debug-configuration' +import { AppModeEnum } from '@/types/app' +import AdvancedPromptInput from '../advanced-prompt-input' + +const mockEmit = vi.fn() +const mockSetShowExternalDataToolModal = vi.fn() +const mockSetModelConfig = vi.fn() +const mockOnTypeChange = vi.fn() +const mockOnChange = vi.fn() +const mockOnDelete = vi.fn() +const mockOnHideContextMissingTip = vi.fn() +const mockCopy = vi.fn() +const mockToastError = vi.fn() + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('copy-to-clipboard', () => ({ + default: (...args: unknown[]) => mockCopy(...args), +})) + +vi.mock('@remixicon/react', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + RiDeleteBinLine: ({ onClick }: { onClick: () => void }) => ( + + ), + RiErrorWarningFill: () => warning-icon, + } +}) + +vi.mock('@/app/components/base/icons/src/vender/line/files', () => ({ + Copy: ({ onClick }: { onClick: () => void }) => ( + + ), + CopyCheck: () => copy-checked, +})) + +vi.mock('@/context/event-emitter', () => ({ + useEventEmitterContextContext: () => ({ + eventEmitter: { + emit: (...args: unknown[]) => mockEmit(...args), + }, + }), +})) + +vi.mock('@/context/modal-context', () => ({ + useModalContext: () => ({ + setShowExternalDataToolModal: mockSetShowExternalDataToolModal, + }), +})) + +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + error: (...args: unknown[]) => mockToastError(...args), + }, +})) + +vi.mock('../message-type-selector', () => ({ + default: ({ onChange, value }: { onChange: (value: PromptRole) => void, value: PromptRole }) => ( + + ), +})) + +vi.mock('@/app/components/base/prompt-editor', () => ({ + default: (props: { + onBlur: () => void + onChange: (value: string) => void + externalToolBlock: { onAddExternalTool: () => void } + }) => ( +
+ + + +
+ ), +})) + +vi.mock('../prompt-editor-height-resize-wrap', () => ({ + default: ({ children, footer }: { children: ReactNode, footer: ReactNode }) => ( +
+ {children} + {footer} +
+ ), +})) + +const createContextValue = () => ({ + mode: AppModeEnum.CHAT, + hasSetBlockStatus: { + context: false, + history: false, + query: false, + }, + modelConfig: { + configs: { + prompt_variables: [ + { key: 'existing_var', name: 'Existing', type: 'string', required: true }, + ], + }, + }, + setModelConfig: mockSetModelConfig, + conversationHistoriesRole: { + user_prefix: 'user', + assistant_prefix: 'assistant', + }, + showHistoryModal: vi.fn(), + dataSets: [], + showSelectDataSet: vi.fn(), + externalDataToolsConfig: [], +}) as any + +describe('AdvancedPromptInput', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should delegate prompt text and role changes to the parent callbacks', () => { + render( + + + , + ) + + fireEvent.click(screen.getByText('change-advanced')) + fireEvent.click(screen.getByText('selector:user')) + fireEvent.click(screen.getByText('copy-prompt')) + fireEvent.click(screen.getByText('delete-prompt')) + + expect(mockOnChange).toHaveBeenCalledWith('Updated {{new_var}}') + expect(mockOnTypeChange).toHaveBeenCalledWith('assistant') + expect(mockCopy).toHaveBeenCalledWith('Hello') + expect(mockOnDelete).toHaveBeenCalled() + }) + + it('should add newly discovered variables after blur confirmation', () => { + render( + + + , + ) + + fireEvent.click(screen.getByText('blur-advanced')) + fireEvent.click(screen.getByText('operation.add')) + + expect(mockSetModelConfig).toHaveBeenCalledWith(expect.objectContaining({ + configs: expect.objectContaining({ + prompt_variables: expect.arrayContaining([ + expect.objectContaining({ + key: 'new_var', + name: 'new_var', + }), + ]), + }), + })) + }) + + it('should open the external data tool modal and validate duplicates', () => { + render( + + + , + ) + + fireEvent.click(screen.getByText('open-advanced-tool-modal')) + + const modalConfig = mockSetShowExternalDataToolModal.mock.calls[0][0] + expect(modalConfig.onValidateBeforeSaveCallback({ variable: 'existing_var' })).toBe(false) + expect(mockToastError).toHaveBeenCalledWith('varKeyError.keyAlreadyExists') + + modalConfig.onSaveCallback({ + label: 'Search', + variable: 'search_api', + }) + + expect(mockEmit).toHaveBeenCalledWith(expect.objectContaining({ + type: 'ADD_EXTERNAL_DATA_TOOL', + })) + expect(mockEmit).toHaveBeenCalledWith(expect.objectContaining({ + payload: 'search_api', + type: INSERT_VARIABLE_VALUE_BLOCK_COMMAND, + })) + }) +}) diff --git a/web/app/components/app/configuration/config-prompt/index.spec.tsx b/web/app/components/app/configuration/config-prompt/__tests__/index.spec.tsx similarity index 98% rename from web/app/components/app/configuration/config-prompt/index.spec.tsx rename to web/app/components/app/configuration/config-prompt/__tests__/index.spec.tsx index c784a09ab6..d42eedf16b 100644 --- a/web/app/components/app/configuration/config-prompt/index.spec.tsx +++ b/web/app/components/app/configuration/config-prompt/__tests__/index.spec.tsx @@ -1,4 +1,5 @@ -import type { IPromptProps } from './index' +/* eslint-disable ts/no-explicit-any */ +import type { IPromptProps } from '../index' import type { PromptItem, PromptVariable } from '@/models/debug' import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' @@ -6,7 +7,7 @@ import { MAX_PROMPT_MESSAGE_LENGTH } from '@/config' import ConfigContext from '@/context/debug-configuration' import { PromptRole } from '@/models/debug' import { AppModeEnum, ModelModeType } from '@/types/app' -import Prompt from './index' +import Prompt from '../index' type DebugConfiguration = { isAdvancedMode: boolean @@ -30,7 +31,7 @@ const defaultPromptVariables: PromptVariable[] = [ let mockSimplePromptInputProps: IPromptProps | null = null -vi.mock('./simple-prompt-input', () => ({ +vi.mock('../simple-prompt-input', () => ({ default: (props: IPromptProps) => { mockSimplePromptInputProps = props return ( @@ -65,7 +66,7 @@ type AdvancedMessageInputProps = { noResize?: boolean } -vi.mock('./advanced-prompt-input', () => ({ +vi.mock('../advanced-prompt-input', () => ({ default: (props: AdvancedMessageInputProps) => { return (
{ beforeEach(() => { diff --git a/web/app/components/app/configuration/config-prompt/prompt-editor-height-resize-wrap.spec.tsx b/web/app/components/app/configuration/config-prompt/__tests__/prompt-editor-height-resize-wrap.spec.tsx similarity index 95% rename from web/app/components/app/configuration/config-prompt/prompt-editor-height-resize-wrap.spec.tsx rename to web/app/components/app/configuration/config-prompt/__tests__/prompt-editor-height-resize-wrap.spec.tsx index abd95e7660..7e168cc7f7 100644 --- a/web/app/components/app/configuration/config-prompt/prompt-editor-height-resize-wrap.spec.tsx +++ b/web/app/components/app/configuration/config-prompt/__tests__/prompt-editor-height-resize-wrap.spec.tsx @@ -1,6 +1,6 @@ import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' -import PromptEditorHeightResizeWrap from './prompt-editor-height-resize-wrap' +import PromptEditorHeightResizeWrap from '../prompt-editor-height-resize-wrap' describe('PromptEditorHeightResizeWrap', () => { beforeEach(() => { diff --git a/web/app/components/app/configuration/config-prompt/__tests__/simple-prompt-input.spec.tsx b/web/app/components/app/configuration/config-prompt/__tests__/simple-prompt-input.spec.tsx new file mode 100644 index 0000000000..a0bc072760 --- /dev/null +++ b/web/app/components/app/configuration/config-prompt/__tests__/simple-prompt-input.spec.tsx @@ -0,0 +1,320 @@ +/* eslint-disable ts/no-explicit-any */ +import type { ReactNode } from 'react' +import { fireEvent, render, screen } from '@testing-library/react' +import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '@/app/components/base/prompt-editor/plugins/variable-block' +import ConfigContext from '@/context/debug-configuration' +import { AppModeEnum } from '@/types/app' +import Prompt from '../simple-prompt-input' + +const mockEmit = vi.fn() +const mockSetFeatures = vi.fn() +const mockSetShowExternalDataToolModal = vi.fn() +const mockSetModelConfig = vi.fn() +const mockSetPrevPromptConfig = vi.fn() +const mockSetIntroduction = vi.fn() +const mockOnChange = vi.fn() +const mockToastError = vi.fn() + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('@/hooks/use-breakpoints', () => ({ + __esModule: true, + default: () => 'desktop', + MediaType: { + mobile: 'mobile', + }, +})) + +vi.mock('@/app/components/base/features/hooks', () => ({ + useFeaturesStore: () => ({ + getState: () => ({ + features: { + opening: { + enabled: false, + opening_statement: '', + }, + }, + setFeatures: mockSetFeatures, + }), + }), +})) + +vi.mock('@/context/event-emitter', () => ({ + useEventEmitterContextContext: () => ({ + eventEmitter: { + emit: (...args: unknown[]) => mockEmit(...args), + }, + }), +})) + +vi.mock('@/context/modal-context', () => ({ + useModalContext: () => ({ + setShowExternalDataToolModal: mockSetShowExternalDataToolModal, + }), +})) + +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + error: (...args: unknown[]) => mockToastError(...args), + }, +})) + +vi.mock('@/app/components/app/configuration/config/automatic/automatic-btn', () => ({ + default: ({ onClick }: { onClick: () => void }) => , +})) + +vi.mock('@/app/components/app/configuration/config/automatic/get-automatic-res', () => ({ + default: ({ onFinished }: { onFinished: (value: Record) => void }) => ( + + ), +})) + +vi.mock('@/app/components/base/prompt-editor', () => ({ + default: (props: { + onBlur: () => void + onChange: (value: string) => void + contextBlock: { datasets: Array<{ id: string, name: string, type: string }> } + variableBlock: { variables: Array<{ name: string, value: string }> } + queryBlock: { selectable: boolean } + externalToolBlock: { + onAddExternalTool: () => void + externalTools: Array<{ name: string, variableName: string }> + } + }) => ( +
+
{`datasets:${props.contextBlock.datasets.map(item => item.name).join(',')}`}
+
{`variables:${props.variableBlock.variables.map(item => item.value).join(',')}`}
+
{`external-tools:${props.externalToolBlock.externalTools.map(item => item.variableName).join(',')}`}
+
{`query-selectable:${String(props.queryBlock.selectable)}`}
+ + + +
+ ), +})) + +vi.mock('../prompt-editor-height-resize-wrap', () => ({ + default: ({ children, footer }: { children: ReactNode, footer: ReactNode }) => ( +
+ {children} + {footer} +
+ ), +})) + +const createContextValue = (overrides: Record = {}) => ({ + appId: 'app-1', + modelConfig: { + configs: { + prompt_template: 'Hello {{new_var}}', + prompt_variables: [ + { key: 'existing_var', name: 'Existing', type: 'string', required: true }, + ], + }, + }, + dataSets: [], + setModelConfig: mockSetModelConfig, + setPrevPromptConfig: mockSetPrevPromptConfig, + setIntroduction: mockSetIntroduction, + hasSetBlockStatus: { + context: false, + history: false, + query: false, + }, + showSelectDataSet: vi.fn(), + externalDataToolsConfig: [], + ...overrides, +}) as any + +describe('SimplePromptInput', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should prompt to add new variables discovered from the prompt template', () => { + render( + + + , + ) + + fireEvent.click(screen.getByText('blur-prompt')) + + expect(screen.getByText('autoAddVar')).toBeInTheDocument() + + fireEvent.click(screen.getByText('operation.add')) + + expect(mockOnChange).toHaveBeenCalledWith('Hello {{new_var}}', [ + expect.objectContaining({ + key: 'new_var', + name: 'new_var', + }), + ]) + }) + + it('should open the external data tool modal and emit insert events after save', () => { + render( + + + , + ) + + fireEvent.click(screen.getByText('open-tool-modal')) + + expect(mockSetShowExternalDataToolModal).toHaveBeenCalledTimes(1) + const modalConfig = mockSetShowExternalDataToolModal.mock.calls[0][0] + + expect(modalConfig.onValidateBeforeSaveCallback({ variable: 'existing_var' })).toBe(false) + expect(mockToastError).toHaveBeenCalledWith('varKeyError.keyAlreadyExists') + expect(modalConfig.onValidateBeforeSaveCallback({ variable: 'fresh_var' })).toBe(true) + + modalConfig.onSaveCallback(undefined) + expect(mockEmit).not.toHaveBeenCalled() + + modalConfig.onSaveCallback({ + label: 'Search', + variable: 'search_api', + }) + + expect(mockEmit).toHaveBeenCalledWith(expect.objectContaining({ + type: 'ADD_EXTERNAL_DATA_TOOL', + })) + expect(mockEmit).toHaveBeenCalledWith(expect.objectContaining({ + payload: 'search_api', + type: INSERT_VARIABLE_VALUE_BLOCK_COMMAND, + })) + }) + + it('should apply automatic generation results to prompt and opening statement', () => { + render( + + + , + ) + + fireEvent.click(screen.getByText('automatic-btn')) + fireEvent.click(screen.getByText('finish-automatic')) + + expect(mockEmit).toHaveBeenCalledWith(expect.objectContaining({ + payload: 'auto prompt', + type: 'PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER', + })) + expect(mockSetModelConfig).toHaveBeenCalledWith(expect.objectContaining({ + configs: expect.objectContaining({ + prompt_template: 'auto prompt', + prompt_variables: [ + expect.objectContaining({ key: 'city', name: 'city' }), + ], + }), + })) + expect(mockSetPrevPromptConfig).toHaveBeenCalled() + expect(mockSetIntroduction).toHaveBeenCalledWith('hello there') + expect(mockSetFeatures).toHaveBeenCalled() + }) + + it('should expose dataset and external tool metadata to the editor', () => { + render( + + + , + ) + + expect(screen.getByText('datasets:Knowledge Base')).toBeInTheDocument() + expect(screen.getByText('variables:existing_var')).toBeInTheDocument() + expect(screen.getByText('external-tools:search_api')).toBeInTheDocument() + expect(screen.getByText('query-selectable:false')).toBeInTheDocument() + }) + + it('should skip external tool variables and incomplete prompt variables when deciding whether to auto add', () => { + render( + + + , + ) + + fireEvent.click(screen.getByText('change-prompt')) + expect(mockOnChange).toHaveBeenCalledWith('Hello {{new_var}}', []) + + fireEvent.click(screen.getByText('blur-prompt')) + expect(mockOnChange).toHaveBeenLastCalledWith('Hello {{search_api}} {{existing_var}}', []) + }) + + it('should keep invalid prompt variables in the confirmation flow', () => { + render( + + + , + ) + + fireEvent.click(screen.getByText('blur-prompt')) + expect(screen.getByText('autoAddVar')).toBeInTheDocument() + + fireEvent.click(screen.getByText('operation.cancel')) + expect(mockOnChange).toHaveBeenCalledWith('Hello {{existing_var}}', []) + }) +}) diff --git a/web/app/components/app/configuration/config-prompt/confirm-add-var/index.spec.tsx b/web/app/components/app/configuration/config-prompt/confirm-add-var/__tests__/index.spec.tsx similarity index 93% rename from web/app/components/app/configuration/config-prompt/confirm-add-var/index.spec.tsx rename to web/app/components/app/configuration/config-prompt/confirm-add-var/__tests__/index.spec.tsx index c5a1500c59..1d6aa91552 100644 --- a/web/app/components/app/configuration/config-prompt/confirm-add-var/index.spec.tsx +++ b/web/app/components/app/configuration/config-prompt/confirm-add-var/__tests__/index.spec.tsx @@ -1,8 +1,8 @@ import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' -import ConfirmAddVar from './index' +import ConfirmAddVar from '../index' -vi.mock('../../base/var-highlight', () => ({ +vi.mock('../../../base/var-highlight', () => ({ default: ({ name }: { name: string }) => {name}, })) diff --git a/web/app/components/app/configuration/config-prompt/confirm-add-var/index.tsx b/web/app/components/app/configuration/config-prompt/confirm-add-var/index.tsx index c2f0cb000a..47d9aaa4d2 100644 --- a/web/app/components/app/configuration/config-prompt/confirm-add-var/index.tsx +++ b/web/app/components/app/configuration/config-prompt/confirm-add-var/index.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import VarHighlight from '../../base/var-highlight' -export type IConfirmAddVarProps = { +type IConfirmAddVarProps = { varNameArr: string[] onConfirm: () => void onCancel: () => void diff --git a/web/app/components/app/configuration/config-prompt/conversation-history/edit-modal.spec.tsx b/web/app/components/app/configuration/config-prompt/conversation-history/__tests__/edit-modal.spec.tsx similarity index 97% rename from web/app/components/app/configuration/config-prompt/conversation-history/edit-modal.spec.tsx rename to web/app/components/app/configuration/config-prompt/conversation-history/__tests__/edit-modal.spec.tsx index 2f417fdded..236f9403c9 100644 --- a/web/app/components/app/configuration/config-prompt/conversation-history/edit-modal.spec.tsx +++ b/web/app/components/app/configuration/config-prompt/conversation-history/__tests__/edit-modal.spec.tsx @@ -1,7 +1,7 @@ import type { ConversationHistoriesRole } from '@/models/debug' import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' -import EditModal from './edit-modal' +import EditModal from '../edit-modal' vi.mock('@/app/components/base/modal', () => ({ default: ({ children }: { children: React.ReactNode }) =>
{children}
, diff --git a/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.spec.tsx b/web/app/components/app/configuration/config-prompt/conversation-history/__tests__/history-panel.spec.tsx similarity index 95% rename from web/app/components/app/configuration/config-prompt/conversation-history/history-panel.spec.tsx rename to web/app/components/app/configuration/config-prompt/conversation-history/__tests__/history-panel.spec.tsx index 827986f521..9a34dc2da1 100644 --- a/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.spec.tsx +++ b/web/app/components/app/configuration/config-prompt/conversation-history/__tests__/history-panel.spec.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react' -import HistoryPanel from './history-panel' +import HistoryPanel from '../history-panel' vi.mock('@/app/components/app/configuration/base/operation-btn', () => ({ default: ({ onClick }: { onClick: () => void }) => ( diff --git a/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx b/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx index e5f1556cc5..da1949af60 100644 --- a/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx +++ b/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx @@ -33,7 +33,7 @@ import { getNewVar, getVars } from '@/utils/var' import ConfirmAddVar from './confirm-add-var' import PromptEditorHeightResizeWrap from './prompt-editor-height-resize-wrap' -export type ISimplePromptInput = { +type ISimplePromptInput = { mode: AppModeEnum promptTemplate: string promptVariables: PromptVariable[] diff --git a/web/app/components/app/configuration/config-prompt/style.module.css b/web/app/components/app/configuration/config-prompt/style.module.css index 3086441327..224d59d9c8 100644 --- a/web/app/components/app/configuration/config-prompt/style.module.css +++ b/web/app/components/app/configuration/config-prompt/style.module.css @@ -1,5 +1,3 @@ -@reference "../../../../styles/globals.css"; - .gradientBorder { background: radial-gradient(circle at 100% 100%, #fcfcfd 0, #fcfcfd 10px, transparent 10px) 0% 0%/12px 12px no-repeat, radial-gradient(circle at 0 100%, #fcfcfd 0, #fcfcfd 10px, transparent 10px) 100% 0%/12px 12px no-repeat, diff --git a/web/app/components/app/configuration/config-var/index.spec.tsx b/web/app/components/app/configuration/config-var/__tests__/index.spec.tsx similarity index 82% rename from web/app/components/app/configuration/config-var/index.spec.tsx rename to web/app/components/app/configuration/config-var/__tests__/index.spec.tsx index a48d3233f5..fb190d844a 100644 --- a/web/app/components/app/configuration/config-var/index.spec.tsx +++ b/web/app/components/app/configuration/config-var/__tests__/index.spec.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react' -import type { IConfigVarProps } from './index' +import type { IConfigVarProps } from '../index' import type { ExternalDataTool } from '@/models/common' import type { PromptVariable } from '@/models/debug' import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react' @@ -9,7 +9,7 @@ import { toast } from '@/app/components/base/ui/toast' import DebugConfigurationContext from '@/context/debug-configuration' import { AppModeEnum } from '@/types/app' -import ConfigVar, { ADD_EXTERNAL_DATA_TOOL } from './index' +import ConfigVar, { ADD_EXTERNAL_DATA_TOOL } from '../index' const toastErrorSpy = vi.spyOn(toast, 'error').mockReturnValue('toast-error') @@ -393,5 +393,94 @@ describe('ConfigVar', () => { }), ]) }) + + it('should update an api variable with the modal save callback', () => { + const onPromptVariablesChange = vi.fn() + const apiVar = createPromptVariable({ + key: 'api_var', + name: 'API Var', + type: 'api', + }) + + renderConfigVar({ + promptVariables: [apiVar], + onPromptVariablesChange, + }) + + const item = screen.getByTitle('api_var · API Var') + const itemContainer = item.closest('div.group') + expect(itemContainer).not.toBeNull() + + const actionButtons = itemContainer!.querySelectorAll('div.h-6.w-6') + fireEvent.click(actionButtons[0]) + + const modalState = setShowExternalDataToolModal.mock.calls.at(-1)?.[0] + + act(() => { + modalState.onSaveCallback?.({ + variable: 'next_api_var', + label: 'Next API Var', + enabled: true, + type: 'api', + config: { endpoint: '/search' }, + icon: 'tool-icon', + icon_background: '#fff', + }) + }) + + expect(onPromptVariablesChange).toHaveBeenCalledWith([ + expect.objectContaining({ + key: 'next_api_var', + name: 'Next API Var', + type: 'api', + icon: 'tool-icon', + }), + ]) + }) + + it('should ignore empty external tool saves and reject duplicate variable names during validation', () => { + const onPromptVariablesChange = vi.fn() + const firstVar = createPromptVariable({ + key: 'api_var', + name: 'API Var', + type: 'api', + }) + const secondVar = createPromptVariable({ + key: 'existing_var', + name: 'Existing Var', + type: 'string', + }) + + renderConfigVar({ + promptVariables: [firstVar, secondVar], + onPromptVariablesChange, + }) + + const item = screen.getByTitle('api_var · API Var') + const itemContainer = item.closest('div.group') + expect(itemContainer).not.toBeNull() + + const actionButtons = itemContainer!.querySelectorAll('div.h-6.w-6') + fireEvent.click(actionButtons[0]) + + const modalState = setShowExternalDataToolModal.mock.calls.at(-1)?.[0] + + act(() => { + modalState.onSaveCallback?.(undefined) + }) + + expect(onPromptVariablesChange).not.toHaveBeenCalled() + + const isValid = modalState.onValidateBeforeSaveCallback?.({ + variable: 'existing_var', + label: 'Duplicated', + enabled: true, + type: 'api', + config: {}, + }) + + expect(isValid).toBe(false) + expect(toastErrorSpy).toHaveBeenCalled() + }) }) }) diff --git a/web/app/components/app/configuration/config-var/config-modal/__tests__/form-fields.spec.tsx b/web/app/components/app/configuration/config-var/config-modal/__tests__/form-fields.spec.tsx new file mode 100644 index 0000000000..0740f0cde3 --- /dev/null +++ b/web/app/components/app/configuration/config-var/config-modal/__tests__/form-fields.spec.tsx @@ -0,0 +1,207 @@ +/* eslint-disable ts/no-explicit-any */ +import type { ReactNode } from 'react' +import { fireEvent, render, screen } from '@testing-library/react' +import { InputVarType } from '@/app/components/workflow/types' +import ConfigModalFormFields from '../form-fields' + +vi.mock('@/app/components/base/file-uploader', () => ({ + FileUploaderInAttachmentWrapper: ({ onChange }: { onChange: (files: Array>) => void }) => ( + + ), +})) + +vi.mock('@/app/components/workflow/nodes/_base/components/file-upload-setting', () => ({ + default: ({ onChange, isMultiple }: { onChange: (payload: Record) => void, isMultiple: boolean }) => ( + + ), +})) + +vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({ + default: ({ onChange }: { onChange: (value: string) => void }) => ( + + ), +})) + +vi.mock('@/app/components/base/checkbox', () => ({ + default: ({ onCheck, checked }: { onCheck: () => void, checked: boolean }) => ( + + ), +})) + +vi.mock('@/app/components/base/select', () => ({ + default: ({ onSelect }: { onSelect: (item: { value: string }) => void }) => ( + + ), +})) + +vi.mock('@/app/components/base/ui/select', () => ({ + Select: ({ value, onValueChange, children }: { value: string, onValueChange: (value: string) => void, children: ReactNode }) => ( +
+ + {children} +
+ ), + SelectTrigger: ({ children }: { children: ReactNode }) =>
{children}
, + SelectValue: () => select-value, + SelectContent: ({ children }: { children: ReactNode }) =>
{children}
, + SelectItem: ({ children }: { children: ReactNode }) =>
{children}
, +})) + +vi.mock('../field', () => ({ + default: ({ children, title }: { children: ReactNode, title: string }) => ( +
+ {title} + {children} +
+ ), +})) + +vi.mock('../type-select', () => ({ + default: ({ onSelect }: { onSelect: (item: { value: InputVarType }) => void }) => ( + + ), +})) + +vi.mock('../../config-select', () => ({ + default: ({ onChange }: { onChange: (value: string[]) => void }) => ( + + ), +})) + +vi.mock('../../config-string', () => ({ + default: ({ onChange }: { onChange: (value: number) => void }) => ( + + ), +})) + +const t = (key: string) => key + +const createPayloadChangeHandler = () => vi.fn<(value: unknown) => void>() + +const createBaseProps = () => { + const payloadChangeHandlers: Record> = { + default: createPayloadChangeHandler(), + hide: createPayloadChangeHandler(), + label: createPayloadChangeHandler(), + max_length: createPayloadChangeHandler(), + options: createPayloadChangeHandler(), + required: createPayloadChangeHandler(), + } + + return { + checkboxDefaultSelectValue: 'false', + isStringInput: false, + jsonSchemaStr: '', + maxLength: 32, + modelId: 'gpt-4o', + onFilePayloadChange: vi.fn(), + onJSONSchemaChange: vi.fn(), + onPayloadChange: (key: string) => { + if (!payloadChangeHandlers[key]) + payloadChangeHandlers[key] = createPayloadChangeHandler() + return payloadChangeHandlers[key] + }, + onTypeChange: vi.fn(), + onVarKeyBlur: vi.fn(), + onVarNameChange: vi.fn(), + options: undefined as string[] | undefined, + selectOptions: [], + tempPayload: { + type: InputVarType.textInput, + label: 'Question', + variable: 'question', + required: false, + hide: false, + } as any, + t, + payloadChangeHandlers, + } +} + +describe('ConfigModalFormFields', () => { + it('should update paragraph, number, checkbox, and select defaults', () => { + const paragraphProps = createBaseProps() + paragraphProps.tempPayload = { ...paragraphProps.tempPayload, type: InputVarType.paragraph, default: 'hello' } + render() + fireEvent.change(screen.getByDisplayValue('hello'), { target: { value: 'updated paragraph' } }) + expect(paragraphProps.payloadChangeHandlers.default).toHaveBeenCalledWith('updated paragraph') + + const numberProps = createBaseProps() + numberProps.tempPayload = { ...numberProps.tempPayload, type: InputVarType.number, default: '1' } + render() + fireEvent.change(screen.getByDisplayValue('1'), { target: { value: '2' } }) + expect(numberProps.payloadChangeHandlers.default).toHaveBeenCalledWith('2') + + const checkboxProps = createBaseProps() + checkboxProps.tempPayload = { ...checkboxProps.tempPayload, type: InputVarType.checkbox, default: false } + checkboxProps.checkboxDefaultSelectValue = 'true' + render() + fireEvent.click(screen.getByText('ui-select:true')) + expect(checkboxProps.payloadChangeHandlers.default).toHaveBeenCalledWith(false) + + const selectProps = createBaseProps() + selectProps.tempPayload = { ...selectProps.tempPayload, type: InputVarType.select, default: 'alpha' } + selectProps.options = ['alpha', 'beta'] + render() + fireEvent.click(screen.getByText('config-select')) + fireEvent.click(screen.getByText('ui-select:alpha')) + expect(selectProps.payloadChangeHandlers.options).toHaveBeenCalledWith(['alpha', 'beta']) + expect(selectProps.payloadChangeHandlers.default).toHaveBeenCalledWith('beta') + }) + + it('should wire file, json schema, and visibility controls', () => { + const singleFileProps = createBaseProps() + singleFileProps.tempPayload = { + ...singleFileProps.tempPayload, + type: InputVarType.singleFile, + allowed_file_types: ['document'], + allowed_file_extensions: [], + allowed_file_upload_methods: ['remote_url'], + } + render() + fireEvent.click(screen.getByText('single-file-setting')) + fireEvent.click(screen.getByText('upload-file')) + fireEvent.click(screen.getAllByText('unchecked')[0]) + fireEvent.click(screen.getAllByText('unchecked')[1]) + + expect(singleFileProps.onFilePayloadChange).toHaveBeenCalledWith({ number_limits: 1 }) + expect(singleFileProps.payloadChangeHandlers.default).toHaveBeenCalledWith(expect.objectContaining({ + fileId: 'file-1', + })) + expect(singleFileProps.payloadChangeHandlers.required).toHaveBeenCalledWith(true) + expect(singleFileProps.payloadChangeHandlers.hide).toHaveBeenCalledWith(true) + + const multiFileProps = createBaseProps() + multiFileProps.tempPayload = { + ...multiFileProps.tempPayload, + type: InputVarType.multiFiles, + allowed_file_types: ['document'], + allowed_file_extensions: [], + allowed_file_upload_methods: ['remote_url'], + } + render() + fireEvent.click(screen.getByText('multi-file-setting')) + fireEvent.click(screen.getAllByText('upload-file')[1]) + expect(multiFileProps.onFilePayloadChange).toHaveBeenCalledWith({ number_limits: 3 }) + expect(multiFileProps.payloadChangeHandlers.default).toHaveBeenCalledWith([ + expect.objectContaining({ fileId: 'file-1' }), + expect.objectContaining({ fileId: 'file-2' }), + ]) + + const jsonProps = createBaseProps() + jsonProps.tempPayload = { ...jsonProps.tempPayload, type: InputVarType.jsonObject } + render() + fireEvent.click(screen.getByText('json-editor')) + expect(jsonProps.onJSONSchemaChange).toHaveBeenCalledWith('{\n "type": "object"\n}') + }) +}) diff --git a/web/app/components/app/configuration/config-var/config-modal/__tests__/index-logic.spec.tsx b/web/app/components/app/configuration/config-var/config-modal/__tests__/index-logic.spec.tsx new file mode 100644 index 0000000000..4888d284d2 --- /dev/null +++ b/web/app/components/app/configuration/config-var/config-modal/__tests__/index-logic.spec.tsx @@ -0,0 +1,150 @@ +/* eslint-disable ts/no-explicit-any */ +import type { InputVar } from '@/app/components/workflow/types' +import type { App, AppSSO } from '@/types/app' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import * as React from 'react' +import { useStore } from '@/app/components/app/store' +import { toast } from '@/app/components/base/ui/toast' +import { InputVarType } from '@/app/components/workflow/types' +import DebugConfigurationContext from '@/context/debug-configuration' +import { AppModeEnum } from '@/types/app' +import ConfigModal from '../index' + +const toastErrorSpy = vi.spyOn(toast, 'error').mockReturnValue('toast-error') +let latestFormProps: Record | null = null + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('../form-fields', () => ({ + default: (props: Record) => { + latestFormProps = props + return ( +
+
{String(props.tempPayload.type)}
+
{String(props.tempPayload.label ?? '')}
+
{String(props.tempPayload.json_schema ?? '')}
+
{String(props.tempPayload.default ?? '')}
+ + + + + + + + +
+ ) + }, +})) + +const createPayload = (overrides: Partial = {}): InputVar => ({ + type: InputVarType.textInput, + label: '', + variable: 'question', + required: false, + hide: false, + options: [], + default: 'hello', + max_length: 32, + ...overrides, +}) + +const renderConfigModal = (payload: InputVar = createPayload()) => render( + + + , +) + +describe('ConfigModal logic', () => { + beforeEach(() => { + vi.clearAllMocks() + latestFormProps = null + useStore.setState({ + appDetail: { + mode: AppModeEnum.CHAT, + } as App & Partial, + }) + }) + + it('should surface validation errors from invalid variable name callbacks', async () => { + renderConfigModal() + + fireEvent.click(screen.getByTestId('invalid-key-blur')) + fireEvent.click(screen.getByTestId('invalid-name-change')) + + await waitFor(() => { + expect(toastErrorSpy).toHaveBeenCalledTimes(2) + }) + }) + + it('should keep the existing label when blur runs on a payload that already has one', async () => { + renderConfigModal(createPayload({ label: 'Existing label' })) + + fireEvent.click(screen.getByTestId('valid-key-blur')) + + await waitFor(() => { + expect(screen.getByTestId('payload-label')).toHaveTextContent('Existing label') + }) + }) + + it('should derive payload fields from mocked form-field callbacks', async () => { + renderConfigModal() + + fireEvent.click(screen.getByTestId('valid-key-blur')) + await waitFor(() => { + expect(screen.getByTestId('payload-label')).toHaveTextContent('auto_label') + }) + + fireEvent.click(screen.getByTestId('valid-json-change')) + await waitFor(() => { + expect(screen.getByTestId('payload-schema')).toHaveTextContent(/"foo": "bar"/) + }) + + fireEvent.click(screen.getByTestId('invalid-json-change')) + expect(screen.getByTestId('payload-schema')).toHaveTextContent(/"foo": "bar"/) + + fireEvent.click(screen.getByTestId('empty-json-change')) + await waitFor(() => { + expect(screen.getByTestId('payload-schema')).toHaveTextContent('') + }) + + fireEvent.click(screen.getByTestId('type-change')) + await waitFor(() => { + expect(screen.getByTestId('payload-type')).toHaveTextContent(InputVarType.singleFile) + }) + + fireEvent.click(screen.getByTestId('file-payload-change')) + await waitFor(() => { + expect(screen.getByTestId('payload-default')).toHaveTextContent('file-default') + }) + + expect(latestFormProps?.modelId).toBe('model-1') + }) +}) diff --git a/web/app/components/app/configuration/config-var/config-modal/__tests__/index.spec.tsx b/web/app/components/app/configuration/config-var/config-modal/__tests__/index.spec.tsx new file mode 100644 index 0000000000..31256f0c08 --- /dev/null +++ b/web/app/components/app/configuration/config-var/config-modal/__tests__/index.spec.tsx @@ -0,0 +1,89 @@ +import type { InputVar } from '@/app/components/workflow/types' +import type { App, AppSSO } from '@/types/app' +import { fireEvent, render, screen } from '@testing-library/react' +import * as React from 'react' +import { useStore } from '@/app/components/app/store' +import { toast } from '@/app/components/base/ui/toast' +import { InputVarType } from '@/app/components/workflow/types' +import { AppModeEnum } from '@/types/app' +import ConfigModal from '../index' + +const toastErrorSpy = vi.spyOn(toast, 'error').mockReturnValue('toast-error') + +const createPayload = (overrides: Partial = {}): InputVar => ({ + type: InputVarType.textInput, + label: '', + variable: 'question', + required: false, + hide: false, + options: [], + default: 'hello', + max_length: 32, + ...overrides, +}) + +describe('ConfigModal', () => { + beforeEach(() => { + vi.clearAllMocks() + useStore.setState({ + appDetail: { + mode: AppModeEnum.CHAT, + } as App & Partial, + }) + }) + + it('should copy the variable name into the label when the label is empty', () => { + render( + , + ) + + const textboxes = screen.getAllByRole('textbox') + fireEvent.blur(textboxes[0], { target: { value: 'question' } }) + + expect(textboxes[1]).toHaveValue('question') + }) + + it('should submit the edited payload when the form is valid', () => { + const onConfirm = vi.fn() + render( + , + ) + + fireEvent.change(screen.getByDisplayValue('hello'), { target: { value: 'updated default' } }) + fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' })) + + expect(onConfirm).toHaveBeenCalledWith(expect.objectContaining({ + default: 'updated default', + label: 'Question', + variable: 'question', + }), undefined) + }) + + it('should block save when the label is missing', () => { + render( + , + ) + + fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' })) + + expect(toastErrorSpy).toHaveBeenCalledWith('appDebug.variableConfig.errorMsg.labelNameRequired') + }) +}) diff --git a/web/app/components/app/configuration/config-var/config-modal/__tests__/type-select.spec.tsx b/web/app/components/app/configuration/config-var/config-modal/__tests__/type-select.spec.tsx new file mode 100644 index 0000000000..2512aa93e8 --- /dev/null +++ b/web/app/components/app/configuration/config-var/config-modal/__tests__/type-select.spec.tsx @@ -0,0 +1,37 @@ +/* eslint-disable ts/no-explicit-any */ +import { fireEvent, render, screen } from '@testing-library/react' +import TypeSelector from '../type-select' + +vi.mock('@/app/components/base/portal-to-follow-elem', () => ({ + PortalToFollowElem: ({ children }: { children: React.ReactNode }) =>
{children}
, + PortalToFollowElemTrigger: ({ children, onClick }: { children: React.ReactNode, onClick?: () => void }) => ( + + ), + PortalToFollowElemContent: ({ children }: { children: React.ReactNode }) =>
{children}
, +})) + +vi.mock('@/app/components/workflow/nodes/_base/components/input-var-type-icon', () => ({ + default: ({ type }: { type: string }) => {type}, +})) + +describe('TypeSelector', () => { + it('should toggle open state and select a new variable type', () => { + const onSelect = vi.fn() + + render( + , + ) + + fireEvent.click(screen.getByRole('button')) + fireEvent.click(screen.getByText('Number')) + + expect(onSelect).toHaveBeenCalledWith({ value: 'number', name: 'Number' }) + }) +}) diff --git a/web/app/components/app/configuration/config-var/config-modal/__tests__/utils.spec.ts b/web/app/components/app/configuration/config-var/config-modal/__tests__/utils.spec.ts new file mode 100644 index 0000000000..1c00e1c5b2 --- /dev/null +++ b/web/app/components/app/configuration/config-var/config-modal/__tests__/utils.spec.ts @@ -0,0 +1,267 @@ +import type { InputVar } from '@/app/components/workflow/types' +import { DEFAULT_FILE_UPLOAD_SETTING } from '@/app/components/workflow/constants' +import { ChangeType, InputVarType, SupportUploadFileTypes } from '@/app/components/workflow/types' +import { + buildSelectOptions, + createPayloadForType, + getCheckboxDefaultSelectValue, + getJsonSchemaEditorValue, + isJsonSchemaEmpty, + isStringInputType, + normalizeSelectDefaultValue, + parseCheckboxSelectValue, + updatePayloadField, + validateConfigModalPayload, +} from '../utils' + +const t = (key: string) => key + +const createInputVar = (overrides: Partial = {}): InputVar => ({ + type: InputVarType.textInput, + label: 'Question', + variable: 'question', + required: false, + options: [], + hide: false, + ...overrides, +}) + +describe('config-modal utils', () => { + describe('payload helpers', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should clear the default value when options no longer include it', () => { + const payload = createInputVar({ + type: InputVarType.select, + default: 'beta', + options: ['alpha', 'beta'], + }) + + const nextPayload = updatePayloadField(payload, 'options', ['alpha']) + + expect(nextPayload.default).toBeUndefined() + expect(nextPayload.options).toEqual(['alpha']) + }) + + it('should seed upload defaults when switching to multi-file input', () => { + const payload = createInputVar({ + type: InputVarType.textInput, + default: 'hello', + }) + + const nextPayload = createPayloadForType(payload, InputVarType.multiFiles) + + expect(nextPayload.type).toBe(InputVarType.multiFiles) + expect(nextPayload.max_length).toBe(DEFAULT_FILE_UPLOAD_SETTING.max_length) + expect(nextPayload.allowed_file_types).toEqual(DEFAULT_FILE_UPLOAD_SETTING.allowed_file_types) + expect(nextPayload.default).toBe('hello') + }) + + it('should clear the default value when switching to a select input type', () => { + const payload = createInputVar({ + type: InputVarType.textInput, + default: 'hello', + }) + + const nextPayload = createPayloadForType(payload, InputVarType.select) + + expect(nextPayload.type).toBe(InputVarType.select) + expect(nextPayload.default).toBeUndefined() + }) + + it('should normalize empty select defaults to undefined', () => { + const nextPayload = normalizeSelectDefaultValue(createInputVar({ + type: InputVarType.select, + default: '', + })) + + expect(nextPayload.default).toBeUndefined() + }) + + it('should parse checkbox default values and normalize json schema editor content', () => { + expect(parseCheckboxSelectValue('true')).toBe(true) + expect(parseCheckboxSelectValue('false')).toBe(false) + expect(getJsonSchemaEditorValue(InputVarType.jsonObject, { type: 'object' } as never)).toBe(JSON.stringify({ type: 'object' }, null, 2)) + expect(getJsonSchemaEditorValue(InputVarType.textInput, '{"type":"object"}')).toBe('') + expect(getJsonSchemaEditorValue(InputVarType.jsonObject, '{"type":"object"}')).toBe('{"type":"object"}') + }) + + it('should fall back to an empty editor value when json schema serialization fails', () => { + const circular: Record = {} + circular.self = circular + + expect(getJsonSchemaEditorValue(InputVarType.jsonObject, circular as never)).toBe('') + }) + }) + + describe('derived values', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should expose upload and json options only when supported', () => { + const options = buildSelectOptions({ + isBasicApp: false, + supportFile: true, + t, + }) + + expect(options.map(option => option.value)).toEqual(expect.arrayContaining([ + InputVarType.singleFile, + InputVarType.multiFiles, + InputVarType.jsonObject, + ])) + }) + + it('should derive checkbox defaults from boolean and string values', () => { + expect(getCheckboxDefaultSelectValue(true)).toBe('true') + expect(getCheckboxDefaultSelectValue('TRUE')).toBe('true') + expect(getCheckboxDefaultSelectValue(undefined)).toBe('false') + }) + + it('should detect blank json schema values', () => { + expect(isJsonSchemaEmpty(undefined)).toBe(true) + expect(isJsonSchemaEmpty(' ')).toBe(true) + expect(isJsonSchemaEmpty('{}')).toBe(false) + expect(isJsonSchemaEmpty({ type: 'object' } as never)).toBe(false) + expect(isStringInputType(InputVarType.textInput)).toBe(true) + expect(isStringInputType(InputVarType.paragraph)).toBe(true) + expect(isStringInputType(InputVarType.number)).toBe(false) + }) + }) + + describe('validation', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should reject duplicate select options', () => { + const checkVariableName = vi.fn(() => true) + + const result = validateConfigModalPayload({ + tempPayload: createInputVar({ + type: InputVarType.select, + options: ['alpha', 'alpha'], + }), + checkVariableName, + payload: createInputVar({ + variable: 'question', + }), + t, + }) + + expect(result.errorMessage).toBe('variableConfig.errorMsg.optionRepeat') + expect(checkVariableName).toHaveBeenCalledWith('question') + }) + + it('should require custom extensions when custom file types are enabled', () => { + const result = validateConfigModalPayload({ + tempPayload: createInputVar({ + type: InputVarType.singleFile, + allowed_file_types: [SupportUploadFileTypes.custom], + allowed_file_extensions: [], + }), + checkVariableName: () => true, + payload: createInputVar(), + t, + }) + + expect(result.errorMessage).toBe('errorMsg.fieldRequired') + }) + + it('should require at least one select option and supported file types', () => { + const selectResult = validateConfigModalPayload({ + tempPayload: createInputVar({ + type: InputVarType.select, + options: [], + }), + checkVariableName: () => true, + payload: createInputVar(), + t, + }) + + const fileResult = validateConfigModalPayload({ + tempPayload: createInputVar({ + type: InputVarType.singleFile, + allowed_file_types: [], + }), + checkVariableName: () => true, + payload: createInputVar(), + t, + }) + + expect(selectResult.errorMessage).toBe('variableConfig.errorMsg.atLeastOneOption') + expect(fileResult.errorMessage).toBe('errorMsg.fieldRequired') + }) + + it('should reject invalid json schema definitions', () => { + const invalidResult = validateConfigModalPayload({ + tempPayload: createInputVar({ + type: InputVarType.jsonObject, + json_schema: '{', + }), + payload: createInputVar(), + checkVariableName: () => true, + t, + }) + + const nonObjectResult = validateConfigModalPayload({ + tempPayload: createInputVar({ + type: InputVarType.jsonObject, + json_schema: JSON.stringify({ type: 'string' }), + }), + payload: createInputVar(), + checkVariableName: () => true, + t, + }) + + expect(invalidResult.errorMessage).toBe('variableConfig.errorMsg.jsonSchemaInvalid') + expect(nonObjectResult.errorMessage).toBe('variableConfig.errorMsg.jsonSchemaMustBeObject') + }) + + it('should normalize blank json schema and return rename metadata', () => { + const result = validateConfigModalPayload({ + tempPayload: createInputVar({ + type: InputVarType.jsonObject, + variable: 'question_new', + json_schema: ' ', + }), + payload: createInputVar({ + variable: 'question_old', + }), + checkVariableName: () => true, + t, + }) + + expect(result.errorMessage).toBeUndefined() + expect(result.payloadToSave).toEqual(expect.objectContaining({ + json_schema: undefined, + variable: 'question_new', + })) + expect(result.moreInfo).toEqual({ + type: ChangeType.changeVarName, + payload: { + beforeKey: 'question_old', + afterKey: 'question_new', + }, + }) + }) + + it('should stop validation when the variable name checker rejects the payload', () => { + const result = validateConfigModalPayload({ + tempPayload: createInputVar({ + variable: 'invalid_name', + }), + payload: createInputVar({ + variable: 'question', + }), + checkVariableName: () => false, + t, + }) + + expect(result).toEqual({}) + }) + }) +}) diff --git a/web/app/components/app/configuration/config-var/config-modal/config.ts b/web/app/components/app/configuration/config-var/config-modal/config.ts index 6586c2fd54..e03464d453 100644 --- a/web/app/components/app/configuration/config-var/config-modal/config.ts +++ b/web/app/components/app/configuration/config-var/config-modal/config.ts @@ -1,10 +1,3 @@ -export const jsonObjectWrap = { - type: 'object', - properties: {}, - required: [], - additionalProperties: true, -} - export const jsonConfigPlaceHolder = JSON.stringify( { type: 'object', diff --git a/web/app/components/app/configuration/config-var/config-modal/form-fields.tsx b/web/app/components/app/configuration/config-var/config-modal/form-fields.tsx new file mode 100644 index 0000000000..c2a02f4710 --- /dev/null +++ b/web/app/components/app/configuration/config-var/config-modal/form-fields.tsx @@ -0,0 +1,228 @@ +'use client' +import type { ChangeEvent, FC } from 'react' +import type { Item as SelectOptionItem } from './type-select' +import type { FileEntity } from '@/app/components/base/file-uploader/types' +import type { InputVar, UploadFileSetting } from '@/app/components/workflow/types' +import * as React from 'react' +import Checkbox from '@/app/components/base/checkbox' +import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader' +import Input from '@/app/components/base/input' +import Textarea from '@/app/components/base/textarea' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/app/components/base/ui/select' +import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' +import FileUploadSetting from '@/app/components/workflow/nodes/_base/components/file-upload-setting' +import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' +import { InputVarType, SupportUploadFileTypes } from '@/app/components/workflow/types' +import { TransferMethod } from '@/types/app' +import ConfigSelect from '../config-select' +import ConfigString from '../config-string' +import { jsonConfigPlaceHolder } from './config' +import Field from './field' +import TypeSelector from './type-select' +import { CHECKBOX_DEFAULT_FALSE_VALUE, CHECKBOX_DEFAULT_TRUE_VALUE, TEXT_MAX_LENGTH } from './utils' + +type Translate = (key: string, options?: Record) => string +const EMPTY_SELECT_VALUE = '__empty__' + +type ConfigModalFormFieldsProps = { + checkboxDefaultSelectValue: string + isStringInput: boolean + jsonSchemaStr: string + maxLength?: number + modelId: string + onFilePayloadChange: (payload: UploadFileSetting) => void + onJSONSchemaChange: (value: string) => void + onPayloadChange: (key: string) => (value: unknown) => void + onTypeChange: (item: SelectOptionItem) => void + onVarKeyBlur: (event: ChangeEvent) => void + onVarNameChange: (event: ChangeEvent) => void + options?: string[] + selectOptions: SelectOptionItem[] + tempPayload: InputVar + t: Translate +} + +const ConfigModalFormFields: FC = ({ + checkboxDefaultSelectValue, + isStringInput, + jsonSchemaStr, + maxLength, + modelId, + onFilePayloadChange, + onJSONSchemaChange, + onPayloadChange, + onTypeChange, + onVarKeyBlur, + onVarNameChange, + options, + selectOptions, + tempPayload, + t, +}) => { + const { type, label, variable } = tempPayload + + return ( +
+ + + + + + + + + onPayloadChange('label')(e.target.value)} + placeholder={t('variableConfig.inputPlaceholder', { ns: 'appDebug' })} + /> + + + {isStringInput && ( + + + + )} + + {type === InputVarType.textInput && ( + + onPayloadChange('default')(e.target.value || undefined)} + placeholder={t('variableConfig.inputPlaceholder', { ns: 'appDebug' })} + /> + + )} + + {type === InputVarType.paragraph && ( + +