From 8aef6fa94624f732b122fb15683f6abec7a67bca Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Sat, 23 May 2026 01:37:09 +0800 Subject: [PATCH 1/2] fix: reject empty polling plugin state --- .../core/entities/plugin/request.py | 2 +- src/dify_plugin/entities/model/llm.py | 2 +- tests/test_model_polling.py | 20 +++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/dify_plugin/core/entities/plugin/request.py b/src/dify_plugin/core/entities/plugin/request.py index 01ff65d2..da4ae0b3 100644 --- a/src/dify_plugin/core/entities/plugin/request.py +++ b/src/dify_plugin/core/entities/plugin/request.py @@ -214,7 +214,7 @@ class ModelCheckPollingRequest(PluginAccessModelRequest): workflow_run_id: str node_id: str - plugin_state: dict[str, JsonValue] + plugin_state: dict[str, JsonValue] = Field(min_length=1) class ModelGetLLMNumTokens(PluginAccessModelRequest, PromptMessageMixin): diff --git a/src/dify_plugin/entities/model/llm.py b/src/dify_plugin/entities/model/llm.py index e84a8247..8fca97bd 100644 --- a/src/dify_plugin/entities/model/llm.py +++ b/src/dify_plugin/entities/model/llm.py @@ -201,7 +201,7 @@ class LLMPollingResult(BaseModel): @model_validator(mode="after") def validate_status_payload(self) -> "LLMPollingResult": - if self.status == LLMPollingStatus.RUNNING and self.plugin_state is None: + if self.status == LLMPollingStatus.RUNNING and not self.plugin_state: msg = "plugin_state is required when polling status is running." raise ValueError(msg) diff --git a/tests/test_model_polling.py b/tests/test_model_polling.py index 3e1cfc5e..49ef75e6 100644 --- a/tests/test_model_polling.py +++ b/tests/test_model_polling.py @@ -291,6 +291,23 @@ def test_start_polling_request_rejects_streaming() -> None: ) +def test_check_polling_request_rejects_empty_plugin_state() -> None: + scenario = PollingScenario() + data: dict[str, object] = { + "user_id": scenario.user_id, + "provider": scenario.provider, + "model_type": ModelType.LLM, + "model": scenario.model, + "credentials": scenario.credentials, + "workflow_run_id": scenario.workflow_run_id, + "node_id": scenario.node_id, + "plugin_state": {}, + } + + with pytest.raises(ValueError, match="at least 1 item"): + ModelCheckPollingRequest(**data) + + def test_executor_starts_llm_polling() -> None: scenario = PollingScenario() model = PollingLLM(scenario) @@ -354,6 +371,9 @@ def test_polling_result_validates_state_payloads() -> None: with pytest.raises(ValueError, match="plugin_state is required"): LLMPollingResult(status=LLMPollingStatus.RUNNING) + with pytest.raises(ValueError, match="plugin_state is required"): + LLMPollingResult(status=LLMPollingStatus.RUNNING, plugin_state={}) + with pytest.raises(ValueError, match="result is required"): LLMPollingResult(status=LLMPollingStatus.SUCCEEDED) From d9c5e47a9e5595db15d51a7b03f1f4bf46f939e5 Mon Sep 17 00:00:00 2001 From: WH-2099 Date: Sat, 23 May 2026 15:54:12 +0800 Subject: [PATCH 2/2] test: align polling validation errors --- tests/test_model_polling.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_model_polling.py b/tests/test_model_polling.py index 49ef75e6..67abc58c 100644 --- a/tests/test_model_polling.py +++ b/tests/test_model_polling.py @@ -3,7 +3,7 @@ from typing import Any, Literal import pytest -from pydantic import JsonValue +from pydantic import JsonValue, ValidationError from dify_plugin.config.config import DifyPluginEnv from dify_plugin.core.entities.plugin.request import ( @@ -284,7 +284,7 @@ def test_polling_requests_parse_daemon_payloads() -> None: def test_start_polling_request_rejects_streaming() -> None: scenario = PollingScenario() - with pytest.raises(ValueError, match="Input should be False"): + with pytest.raises(ValidationError, match="Input should be False"): scenario.start_request( prompt_messages=scenario.daemon_prompt_messages, stream=True, @@ -304,7 +304,7 @@ def test_check_polling_request_rejects_empty_plugin_state() -> None: "plugin_state": {}, } - with pytest.raises(ValueError, match="at least 1 item"): + with pytest.raises(ValidationError, match="at least 1 item"): ModelCheckPollingRequest(**data) @@ -368,16 +368,16 @@ def test_executor_rejects_llm_without_polling_feature() -> None: def test_polling_result_validates_state_payloads() -> None: - with pytest.raises(ValueError, match="plugin_state is required"): + with pytest.raises(ValidationError, match="plugin_state is required"): LLMPollingResult(status=LLMPollingStatus.RUNNING) - with pytest.raises(ValueError, match="plugin_state is required"): + with pytest.raises(ValidationError, match="plugin_state is required"): LLMPollingResult(status=LLMPollingStatus.RUNNING, plugin_state={}) - with pytest.raises(ValueError, match="result is required"): + with pytest.raises(ValidationError, match="result is required"): LLMPollingResult(status=LLMPollingStatus.SUCCEEDED) - with pytest.raises(ValueError, match="error is required"): + with pytest.raises(ValidationError, match="error is required"): LLMPollingResult(status=LLMPollingStatus.FAILED) @@ -388,7 +388,7 @@ def test_polling_result_validates_state_payloads() -> None: def test_polling_result_rejects_non_positive_limits(field_name: str) -> None: scenario = PollingScenario() - with pytest.raises(ValueError, match="Input should be greater than 0"): + with pytest.raises(ValidationError, match="Input should be greater than 0"): LLMPollingResult( status=LLMPollingStatus.RUNNING, plugin_state=scenario.plugin_state,