From 4fb3210f9a1658edc9bafc97afb103f504c0f8a4 Mon Sep 17 00:00:00 2001 From: Novice Date: Wed, 10 Jun 2026 15:28:12 +0800 Subject: [PATCH] fix: validate conversation variable description length to prevent varchar(255) truncation error (#33038) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: 非法操作 Co-authored-by: -LAN- --- api/controllers/common/errors.py | 10 ++++ api/controllers/console/app/workflow.py | 10 +++- .../console/app/workflow_draft_variable.py | 2 +- api/controllers/console/app/workflow_run.py | 2 +- .../rag_pipeline_draft_variable.py | 2 +- api/controllers/console/human_input_form.py | 2 +- .../snippet_workflow_draft_variable.py | 2 +- .../console/workspace/trigger_providers.py | 2 +- api/controllers/web/error.py | 10 ---- api/controllers/web/human_input_form.py | 3 +- api/controllers/web/workflow_events.py | 2 +- api/factories/variable_factory.py | 9 ++++ api/models/workflow.py | 2 + .../workflow_draft_variable_service.py | 3 ++ .../workspace/test_trigger_providers.py | 2 +- .../controllers/console/app/test_workflow.py | 24 +++++++++ .../app/test_workflow_pause_details_api.py | 2 +- .../test_rag_pipeline_draft_variable.py | 2 +- .../console/test_human_input_form.py | 2 +- .../unit_tests/controllers/web/test_error.py | 3 +- .../controllers/web/test_workflow_events.py | 2 +- .../factories/test_variable_factory.py | 13 +++++ .../test_workflow_draft_variable_service.py | 51 +++++++++++++++++++ .../use-variable-modal-state.spec.ts | 28 ++++++++++ .../variable-modal.sections.spec.tsx | 15 ++++++ .../components/use-variable-modal-state.ts | 12 +++++ .../components/variable-modal.helpers.ts | 2 + .../components/variable-modal.sections.tsx | 8 +++ .../components/variable-modal.tsx | 3 ++ web/i18n/en-US/workflow.json | 1 + web/i18n/zh-Hans/workflow.json | 1 + 31 files changed, 206 insertions(+), 26 deletions(-) diff --git a/api/controllers/common/errors.py b/api/controllers/common/errors.py index 252cf3549a..31dd2fcd74 100644 --- a/api/controllers/common/errors.py +++ b/api/controllers/common/errors.py @@ -41,3 +41,13 @@ class NoFileUploadedError(BaseHTTPException): error_code = "no_file_uploaded" description = "Please upload your file." code = 400 + + +class NotFoundError(BaseHTTPException): + error_code = "not_found" + code = 404 + + +class InvalidArgumentError(BaseHTTPException): + error_code = "invalid_param" + code = 400 diff --git a/api/controllers/console/app/workflow.py b/api/controllers/console/app/workflow.py index 5d24506f21..6a14937ffa 100644 --- a/api/controllers/console/app/workflow.py +++ b/api/controllers/console/app/workflow.py @@ -12,6 +12,7 @@ from werkzeug.exceptions import BadRequest, Forbidden, InternalServerError, NotF import services from controllers.common.controller_schemas import DefaultBlockConfigQuery, WorkflowListQuery, WorkflowUpdatePayload +from controllers.common.errors import InvalidArgumentError from controllers.common.fields import NewAppResponse, SimpleResultResponse from controllers.common.schema import ( register_response_schema_model, @@ -19,7 +20,11 @@ from controllers.common.schema import ( register_schema_models, ) from controllers.console import console_ns -from controllers.console.app.error import ConversationCompletedError, DraftWorkflowNotExist, DraftWorkflowNotSync +from controllers.console.app.error import ( + ConversationCompletedError, + DraftWorkflowNotExist, + DraftWorkflowNotSync, +) from controllers.console.app.wraps import get_app_model from controllers.console.wraps import ( account_initialization_required, @@ -56,6 +61,7 @@ from graphon.file import helpers as file_helpers from graphon.graph_engine.manager import GraphEngineManager from graphon.model_runtime.utils.encoders import jsonable_encoder from graphon.variables import SecretVariable, SegmentType, VariableBase +from graphon.variables.exc import VariableError from libs import helper from libs.datetime_utils import naive_utc_now from libs.helper import TimestampField, dump_response, to_timestamp, uuid_value @@ -452,6 +458,8 @@ class DraftWorkflowApi(Resource): ) except WorkflowHashNotEqualError: raise DraftWorkflowNotSync() + except VariableError as e: + raise InvalidArgumentError(description=str(e)) return { "result": "success", diff --git a/api/controllers/console/app/workflow_draft_variable.py b/api/controllers/console/app/workflow_draft_variable.py index f6bb2aa008..8ebd65eccf 100644 --- a/api/controllers/console/app/workflow_draft_variable.py +++ b/api/controllers/console/app/workflow_draft_variable.py @@ -9,6 +9,7 @@ from flask_restx import Resource, fields, marshal, marshal_with from pydantic import BaseModel, Field from sqlalchemy.orm import sessionmaker +from controllers.common.errors import InvalidArgumentError, NotFoundError from controllers.common.schema import register_schema_models from controllers.console import console_ns from controllers.console.app.error import ( @@ -21,7 +22,6 @@ from controllers.console.wraps import ( setup_required, with_current_user, ) -from controllers.web.error import InvalidArgumentError, NotFoundError from core.app.file_access import DatabaseFileAccessController from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID from extensions.ext_database import db diff --git a/api/controllers/console/app/workflow_run.py b/api/controllers/console/app/workflow_run.py index 0ae9fb6309..359daa12c2 100644 --- a/api/controllers/console/app/workflow_run.py +++ b/api/controllers/console/app/workflow_run.py @@ -9,6 +9,7 @@ from sqlalchemy import select from sqlalchemy.orm import sessionmaker from configs import dify_config +from controllers.common.errors import NotFoundError from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns from controllers.console.app.wraps import get_app_model @@ -18,7 +19,6 @@ from controllers.console.wraps import ( with_current_tenant_id, with_current_user, ) -from controllers.web.error import NotFoundError from core.workflow.human_input_forms import load_form_tokens_by_form_id as _load_form_tokens_by_form_id from extensions.ext_database import db from fields.base import ResponseModel 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 4643cfa15c..cd326dffe9 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 @@ -10,6 +10,7 @@ from pydantic import BaseModel, Field from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import Forbidden +from controllers.common.errors import InvalidArgumentError, NotFoundError from controllers.common.schema import register_schema_models from controllers.console import console_ns from controllers.console.app.error import ( @@ -23,7 +24,6 @@ from controllers.console.app.workflow_draft_variable import ( ) from controllers.console.datasets.wraps import get_rag_pipeline from controllers.console.wraps import account_initialization_required, setup_required, with_current_user -from controllers.web.error import InvalidArgumentError, NotFoundError from core.app.file_access import DatabaseFileAccessController from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID from extensions.ext_database import db diff --git a/api/controllers/console/human_input_form.py b/api/controllers/console/human_input_form.py index 4b34cb6d9c..0ca9d18168 100644 --- a/api/controllers/console/human_input_form.py +++ b/api/controllers/console/human_input_form.py @@ -11,6 +11,7 @@ from flask_restx import Resource from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker +from controllers.common.errors import InvalidArgumentError, NotFoundError from controllers.common.human_input import HumanInputFormSubmitPayload from controllers.common.schema import register_schema_models from controllers.console import console_ns @@ -21,7 +22,6 @@ from controllers.console.wraps import ( with_current_tenant_id, with_current_user, ) -from controllers.web.error import InvalidArgumentError, NotFoundError from core.app.apps.advanced_chat.app_generator import AdvancedChatAppGenerator from core.app.apps.base_app_generator import BaseAppGenerator from core.app.apps.common.workflow_response_converter import WorkflowResponseConverter diff --git a/api/controllers/console/snippets/snippet_workflow_draft_variable.py b/api/controllers/console/snippets/snippet_workflow_draft_variable.py index 323fb0b333..491a78a9b9 100644 --- a/api/controllers/console/snippets/snippet_workflow_draft_variable.py +++ b/api/controllers/console/snippets/snippet_workflow_draft_variable.py @@ -18,6 +18,7 @@ from flask import Response, request from flask_restx import Resource, marshal, marshal_with from sqlalchemy.orm import Session, sessionmaker +from controllers.common.errors import InvalidArgumentError, NotFoundError from controllers.console import console_ns from controllers.console.app.error import DraftWorkflowNotExist from controllers.console.app.workflow_draft_variable import ( @@ -36,7 +37,6 @@ from controllers.console.wraps import ( setup_required, with_current_user, ) -from controllers.web.error import InvalidArgumentError, NotFoundError from core.app.file_access import DatabaseFileAccessController from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID from extensions.ext_database import db diff --git a/api/controllers/console/workspace/trigger_providers.py b/api/controllers/console/workspace/trigger_providers.py index a87633e5d0..3805d0ff37 100644 --- a/api/controllers/console/workspace/trigger_providers.py +++ b/api/controllers/console/workspace/trigger_providers.py @@ -8,9 +8,9 @@ from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, Forbidden from configs import dify_config +from controllers.common.errors import NotFoundError from controllers.common.fields import SimpleResultResponse from controllers.common.schema import register_response_schema_models, register_schema_models -from controllers.web.error import NotFoundError from core.plugin.entities.plugin_daemon import CredentialType from core.plugin.impl.oauth import OAuthHandler from core.trigger.entities.entities import SubscriptionBuilderUpdater diff --git a/api/controllers/web/error.py b/api/controllers/web/error.py index d1f936768e..789c0fabcc 100644 --- a/api/controllers/web/error.py +++ b/api/controllers/web/error.py @@ -121,13 +121,3 @@ class WebFormRateLimitExceededError(BaseHTTPException): error_code = "web_form_rate_limit_exceeded" description = "Too many form requests. Please try again later." code = 429 - - -class NotFoundError(BaseHTTPException): - error_code = "not_found" - code = 404 - - -class InvalidArgumentError(BaseHTTPException): - error_code = "invalid_param" - code = 400 diff --git a/api/controllers/web/human_input_form.py b/api/controllers/web/human_input_form.py index 113c39d3dd..3065d57b5d 100644 --- a/api/controllers/web/human_input_form.py +++ b/api/controllers/web/human_input_form.py @@ -15,10 +15,11 @@ from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import Forbidden from configs import dify_config +from controllers.common.errors import NotFoundError from controllers.common.human_input import HumanInputFormSubmitPayload, stringify_form_default_values from controllers.common.schema import register_schema_models from controllers.web import web_ns -from controllers.web.error import NotFoundError, WebFormRateLimitExceededError +from controllers.web.error import WebFormRateLimitExceededError from controllers.web.site import serialize_app_site_payload from extensions.ext_database import db from graphon.nodes.human_input.entities import FormInputConfig diff --git a/api/controllers/web/workflow_events.py b/api/controllers/web/workflow_events.py index 474f9c0957..b513ade58a 100644 --- a/api/controllers/web/workflow_events.py +++ b/api/controllers/web/workflow_events.py @@ -8,8 +8,8 @@ from collections.abc import Generator from flask import Response, request from sqlalchemy.orm import sessionmaker +from controllers.common.errors import InvalidArgumentError, NotFoundError from controllers.web import api -from controllers.web.error import InvalidArgumentError, NotFoundError from controllers.web.wraps import WebApiResource from core.app.apps.advanced_chat.app_generator import AdvancedChatAppGenerator from core.app.apps.base_app_generator import BaseAppGenerator diff --git a/api/factories/variable_factory.py b/api/factories/variable_factory.py index fd7acb14d3..586430c54b 100644 --- a/api/factories/variable_factory.py +++ b/api/factories/variable_factory.py @@ -48,9 +48,18 @@ __all__ = [ ] +_MAX_VARIABLE_DESCRIPTION_LENGTH = 255 + + def build_conversation_variable_from_mapping(mapping: Mapping[str, Any], /) -> VariableBase: if not mapping.get("name"): raise VariableError("missing name") + description = mapping.get("description", "") + if len(description) > _MAX_VARIABLE_DESCRIPTION_LENGTH: + raise VariableError( + f"description of variable '{mapping['name']}' is too long" + f" (max {_MAX_VARIABLE_DESCRIPTION_LENGTH} characters)" + ) return _build_variable_from_mapping(mapping=mapping, selector=[CONVERSATION_VARIABLE_NODE_ID, mapping["name"]]) diff --git a/api/models/workflow.py b/api/models/workflow.py index 16daafe785..bf494ad1f4 100644 --- a/api/models/workflow.py +++ b/api/models/workflow.py @@ -1861,6 +1861,8 @@ class WorkflowDraftVariable(Base): variable.file_id = file_id variable._set_selector(list(variable_utils.to_selector(node_id, name))) variable.node_execution_id = node_execution_id + variable.visible = True + variable.is_default_value = False return variable @classmethod diff --git a/api/services/workflow_draft_variable_service.py b/api/services/workflow_draft_variable_service.py index d7351c5fa5..7c9bd489bd 100644 --- a/api/services/workflow_draft_variable_service.py +++ b/api/services/workflow_draft_variable_service.py @@ -749,6 +749,7 @@ class _InsertionDict(TypedDict): file_id: str | None visible: NotRequired[bool] editable: NotRequired[bool] + is_default_value: NotRequired[bool] created_at: NotRequired[datetime] updated_at: NotRequired[datetime] description: NotRequired[str] @@ -778,6 +779,8 @@ def _model_to_insertion_dict(model: WorkflowDraftVariable) -> _InsertionDict: d["updated_at"] = model.updated_at if model.description is not None: d["description"] = model.description + if model.is_default_value is not None: + d["is_default_value"] = model.is_default_value return d diff --git a/api/tests/test_containers_integration_tests/controllers/console/workspace/test_trigger_providers.py b/api/tests/test_containers_integration_tests/controllers/console/workspace/test_trigger_providers.py index e155b711a7..6c74b3193b 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/workspace/test_trigger_providers.py +++ b/api/tests/test_containers_integration_tests/controllers/console/workspace/test_trigger_providers.py @@ -8,6 +8,7 @@ import pytest from flask import Flask from werkzeug.exceptions import BadRequest, Forbidden +from controllers.common.errors import NotFoundError from controllers.console.workspace.trigger_providers import ( TriggerOAuthAuthorizeApi, TriggerOAuthCallbackApi, @@ -26,7 +27,6 @@ from controllers.console.workspace.trigger_providers import ( TriggerSubscriptionUpdateApi, TriggerSubscriptionVerifyApi, ) -from controllers.web.error import NotFoundError from core.plugin.entities.plugin_daemon import CredentialType from models.account import Account diff --git a/api/tests/unit_tests/controllers/console/app/test_workflow.py b/api/tests/unit_tests/controllers/console/app/test_workflow.py index 272337d0c5..d2f7770b2f 100644 --- a/api/tests/unit_tests/controllers/console/app/test_workflow.py +++ b/api/tests/unit_tests/controllers/console/app/test_workflow.py @@ -12,6 +12,7 @@ from flask import Flask from pydantic import ValidationError from werkzeug.exceptions import HTTPException, NotFound +from controllers.common.errors import InvalidArgumentError from controllers.console.app import workflow as workflow_module from controllers.console.app.error import DraftWorkflowNotExist, DraftWorkflowNotSync from graphon.file import File, FileTransferMethod, FileType @@ -180,6 +181,29 @@ def test_sync_draft_workflow_hash_mismatch(app: Flask, monkeypatch: pytest.Monke handler(api, "t1", app_model=SimpleNamespace(id="app")) +def test_sync_draft_workflow_variable_validation_error(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: + def _raise(*_args, **_kwargs): + raise workflow_module.VariableError("description too long") + + monkeypatch.setattr(workflow_module.variable_factory, "build_conversation_variable_from_mapping", _raise) + monkeypatch.setattr( + workflow_module, "WorkflowService", lambda: SimpleNamespace(sync_draft_workflow=lambda **_kwargs: None) + ) + + api = workflow_module.DraftWorkflowApi() + handler = inspect.unwrap(api.post) + + with app.test_request_context( + "/apps/app/workflows/draft", + method="POST", + json={"graph": {}, "features": {}, "hash": "h", "conversation_variables": [{"name": "topic"}]}, + ): + with pytest.raises(InvalidArgumentError) as exc: + handler(api, "t1", app_model=SimpleNamespace(id="app")) + + assert exc.value.description == "description too long" + + def test_restore_published_workflow_to_draft_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: workflow = SimpleNamespace( unique_hash="restored-hash", diff --git a/api/tests/unit_tests/controllers/console/app/test_workflow_pause_details_api.py b/api/tests/unit_tests/controllers/console/app/test_workflow_pause_details_api.py index aa06aeabc8..4981971bc1 100644 --- a/api/tests/unit_tests/controllers/console/app/test_workflow_pause_details_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_workflow_pause_details_api.py @@ -8,8 +8,8 @@ from unittest.mock import Mock import pytest from flask import Flask +from controllers.common.errors import NotFoundError from controllers.console.app import workflow_run as workflow_run_module -from controllers.web.error import NotFoundError from graphon.entities.pause_reason import HumanInputRequired from graphon.enums import WorkflowExecutionStatus from graphon.nodes.human_input.entities import ParagraphInputConfig, UserActionConfig diff --git a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_draft_variable.py b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_draft_variable.py index 9b491d63aa..f51b1ae1da 100644 --- a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_draft_variable.py +++ b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_draft_variable.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock, patch import pytest from flask import Flask, Response +from controllers.common.errors import InvalidArgumentError, NotFoundError from controllers.console import console_ns from controllers.console.app.error import DraftWorkflowNotExist from controllers.console.datasets.rag_pipeline.rag_pipeline_draft_variable import ( @@ -13,7 +14,6 @@ from controllers.console.datasets.rag_pipeline.rag_pipeline_draft_variable impor RagPipelineVariableCollectionApi, RagPipelineVariableResetApi, ) -from controllers.web.error import InvalidArgumentError, NotFoundError from core.workflow.variable_prefixes import SYSTEM_VARIABLE_NODE_ID from graphon.variables.types import SegmentType from models.account import Account, TenantAccountRole diff --git a/api/tests/unit_tests/controllers/console/test_human_input_form.py b/api/tests/unit_tests/controllers/console/test_human_input_form.py index 956b034673..a9e847d7d7 100644 --- a/api/tests/unit_tests/controllers/console/test_human_input_form.py +++ b/api/tests/unit_tests/controllers/console/test_human_input_form.py @@ -9,6 +9,7 @@ from unittest.mock import Mock import pytest from flask import Flask, Response +from controllers.common.errors import NotFoundError from controllers.common.human_input import HumanInputFormSubmitPayload from controllers.console.human_input_form import ( ConsoleHumanInputFormApi, @@ -17,7 +18,6 @@ from controllers.console.human_input_form import ( WorkflowResponseConverter, _jsonify_form_definition, ) -from controllers.web.error import NotFoundError from models.account import AccountStatus from models.enums import CreatorUserRole from models.human_input import RecipientType diff --git a/api/tests/unit_tests/controllers/web/test_error.py b/api/tests/unit_tests/controllers/web/test_error.py index 0387d002ba..2852c4806c 100644 --- a/api/tests/unit_tests/controllers/web/test_error.py +++ b/api/tests/unit_tests/controllers/web/test_error.py @@ -4,6 +4,7 @@ from __future__ import annotations import pytest +from controllers.common.errors import InvalidArgumentError, NotFoundError from controllers.web.error import ( AppMoreLikeThisDisabledError, AppSuggestedQuestionsAfterAnswerDisabledError, @@ -11,12 +12,10 @@ from controllers.web.error import ( AudioTooLargeError, CompletionRequestError, ConversationCompletedError, - InvalidArgumentError, InvokeRateLimitError, NoAudioUploadedError, NotChatAppError, NotCompletionAppError, - NotFoundError, NotWorkflowAppError, ProviderModelCurrentlyNotSupportError, ProviderNotInitializeError, diff --git a/api/tests/unit_tests/controllers/web/test_workflow_events.py b/api/tests/unit_tests/controllers/web/test_workflow_events.py index 64c09b5e22..ab2ca7dde6 100644 --- a/api/tests/unit_tests/controllers/web/test_workflow_events.py +++ b/api/tests/unit_tests/controllers/web/test_workflow_events.py @@ -8,7 +8,7 @@ from unittest.mock import MagicMock, patch import pytest from flask import Flask -from controllers.web.error import NotFoundError +from controllers.common.errors import NotFoundError from controllers.web.workflow_events import WorkflowEventsApi from models.enums import CreatorUserRole diff --git a/api/tests/unit_tests/factories/test_variable_factory.py b/api/tests/unit_tests/factories/test_variable_factory.py index 2439409e80..fdaf98c86a 100644 --- a/api/tests/unit_tests/factories/test_variable_factory.py +++ b/api/tests/unit_tests/factories/test_variable_factory.py @@ -200,6 +200,19 @@ def test_variable_cannot_large_than_200_kb(): ) +def test_conversation_variable_description_cannot_exceed_255_chars(): + with pytest.raises(VariableError, match="description of variable 'test_text' is too long"): + variable_factory.build_conversation_variable_from_mapping( + { + "id": str(uuid4()), + "value_type": "string", + "name": "test_text", + "value": "value", + "description": "a" * 256, + } + ) + + def test_array_none_variable(): var = variable_factory.build_segment([None, None, None, None]) assert isinstance(var, ArrayAnySegment) diff --git a/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py b/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py index 4e10dddd2b..5fb83a1cf8 100644 --- a/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py +++ b/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py @@ -31,6 +31,7 @@ from services.workflow_draft_variable_service import ( DraftVariableSaver, VariableResetError, WorkflowDraftVariableService, + _model_to_insertion_dict, ) @@ -643,3 +644,53 @@ class TestWorkflowDraftVariableService: assert node_var.visible == True assert node_var.editable == True assert node_var.node_execution_id == "exec-id" + + +class TestModelToInsertionDict: + """Reproduce two production errors in _model_to_insertion_dict / _new().""" + + def test_visible_and_is_default_value_always_present(self): + """Problem 1: _new() did not set visible/is_default_value, causing + inconsistent dict keys across rows in multi-row INSERT and missing + is_default_value in the insertion dict entirely. + """ + conv_var = WorkflowDraftVariable.new_conversation_variable( + app_id="app-1", + name="counter", + value=StringSegment(value="0"), + ) + # _new() should explicitly set these fields so they are not None + assert conv_var.visible is not None + assert conv_var.is_default_value is not None + + d = _model_to_insertion_dict(conv_var) + # visible must appear in every row's dict + assert "visible" in d + # is_default_value must always be present + assert "is_default_value" in d + + def test_description_passthrough(self): + """_model_to_insertion_dict passes description as-is; + length validation is enforced earlier in build_conversation_variable_from_mapping. + """ + desc = "a" * 200 + conv_var = WorkflowDraftVariable.new_conversation_variable( + app_id="app-1", + name="counter", + value=StringSegment(value="0"), + description=desc, + ) + d = _model_to_insertion_dict(conv_var) + assert d["description"] == desc + + def test_is_default_value_omitted_when_none(self): + conv_var = WorkflowDraftVariable.new_conversation_variable( + app_id="app-1", + name="counter", + value=StringSegment(value="0"), + ) + conv_var.is_default_value = None + + d = _model_to_insertion_dict(conv_var) + + assert "is_default_value" not in d diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/use-variable-modal-state.spec.ts b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/use-variable-modal-state.spec.ts index 176650f0ed..b3e713e488 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/use-variable-modal-state.spec.ts +++ b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/use-variable-modal-state.spec.ts @@ -195,6 +195,34 @@ describe('useVariableModalState', () => { expect(onClose).not.toHaveBeenCalled() }) + it('should notify and stop saving when description exceeds the limit', () => { + const notify = vi.fn() + const onSave = vi.fn() + const onClose = vi.fn() + const { result } = renderHook(() => useVariableModalState(createOptions({ + notify, + onClose, + onSave, + }))) + + act(() => { + result.current.handleVarNameChange({ target: { value: 'greeting' } } as ChangeEvent) + result.current.handleStringOrNumberChange(['hello']) + result.current.setDescription('a'.repeat(256)) + }) + + act(() => { + result.current.handleSave() + }) + + expect(notify).toHaveBeenCalledWith({ + type: 'error', + message: 'chatVariable.modal.descriptionTooLong', + }) + expect(onSave).not.toHaveBeenCalled() + expect(onClose).not.toHaveBeenCalled() + }) + it('should save a new variable and close when state is valid', () => { const onSave = vi.fn() const onClose = vi.fn() diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.sections.spec.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.sections.spec.tsx index 5f64407764..80df7de00f 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.sections.spec.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.sections.spec.tsx @@ -24,6 +24,7 @@ describe('variable-modal sections', () => { /> { expect(onDescriptionChange).toHaveBeenCalledWith('updated-description') }) + it('should show description length against the configured limit', () => { + render( + , + ) + + expect(screen.getByText('3/255')).toBeInTheDocument() + }) + it('renders type and value sections and forwards toggle and value changes', async () => { const onSelect = vi.fn() const onArrayChange = vi.fn() diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/use-variable-modal-state.ts b/web/app/components/workflow/panel/chat-variable-panel/components/use-variable-modal-state.ts index 07619029a3..343d27e1c8 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/use-variable-modal-state.ts +++ b/web/app/components/workflow/panel/chat-variable-panel/components/use-variable-modal-state.ts @@ -11,6 +11,7 @@ import { getEditorMinHeight, getPlaceholderByType, getTypeChangeState, + MAX_DESCRIPTION_LENGTH, parseEditorContent, validateVariableName, } from './variable-modal.helpers' @@ -196,6 +197,17 @@ export const useVariableModalState = ({ return } + if (state.description.length > MAX_DESCRIPTION_LENGTH) { + notify({ + type: 'error', + message: t('chatVariable.modal.descriptionTooLong', { + maxLength: MAX_DESCRIPTION_LENGTH, + ns: 'workflow', + }), + }) + return + } + onSave({ description: state.description, id: chatVar ? chatVar.id : uuid4(), diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.helpers.ts b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.helpers.ts index 8307cbe80b..cde72a508f 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.helpers.ts +++ b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.helpers.ts @@ -29,6 +29,8 @@ export type ToastPayload = { customComponent?: ReactNode } +export const MAX_DESCRIPTION_LENGTH = 255 + export const typeList = [ ChatVarTypeEnum.String, ChatVarTypeEnum.Number, diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.sections.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.sections.tsx index f208b6a283..49e9eb879f 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.sections.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.sections.tsx @@ -1,6 +1,7 @@ import type { ReactNode } from 'react' import type { ObjectValueItem } from './variable-modal.helpers' import { Button } from '@langgenius/dify-ui/button' +import { cn } from '@langgenius/dify-ui/cn' import { RiDraftLine, RiInputField } from '@remixicon/react' import Input from '@/app/components/base/input' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' @@ -195,6 +196,7 @@ export const ValueSection = ({ type DescriptionSectionProps = { description: string + maxLength: number onChange: (value: string) => void placeholder: string title: string @@ -202,6 +204,7 @@ type DescriptionSectionProps = { export const DescriptionSection = ({ description, + maxLength, onChange, placeholder, title, @@ -216,5 +219,10 @@ export const DescriptionSection = ({ onChange={e => onChange(e.target.value)} /> +
maxLength ? 'text-text-destructive' : 'text-text-quaternary')}> + {description.length} + / + {maxLength} +
) diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx index 6c2e5fe449..ecb81b6507 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx @@ -12,6 +12,7 @@ import { replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var' import { useVariableModalState } from './use-variable-modal-state' import { getEditorToggleLabelKey, + MAX_DESCRIPTION_LENGTH, typeList, validateVariableName, } from './variable-modal.helpers' @@ -72,6 +73,7 @@ const ChatVariableModal = ({ return handleVarNameChange(e) } + return (