Change the is_resumption field in WorkflowStarted event into reason (vibe-kanban 19ac040e)

Reason should be an enumeration with only one member `resumption` currently.

Please update these part of events:

- Graph / Engine Event (GraphRunStartedEvent)
- Queue event (QueueWorkflowStartedEvent)
- SSE response event (WorkflowStartStreamResponse)

Besides, you should remove the `is_resumption` flag for `node_started` events; including:

- Queue Event (`QueueNodeStartedEvent`)
- SSE Event (`NodeStartStreamResponse`)
- Node event (`NodeRunStartedEvent`)

After finishing the changes above, adjust related tests.
You should run the affected tests and ensure they can pass. (You should use `uv run pytest` to run tests)
This commit is contained in:
QuantumGhost
2026-01-18 21:00:25 +08:00
parent 7bc7a8d0ab
commit afdf2397f2
21 changed files with 59 additions and 227 deletions

View File

@ -315,7 +315,7 @@ class AdvancedChatAppGenerateTaskPipeline(GraphRuntimeStateSupport):
task_id=self._application_generate_entity.task_id,
workflow_run_id=run_id,
workflow_id=self._workflow_id,
is_resumption=event.is_resumption,
reason=event.reason,
)
yield workflow_start_resp

View File

@ -47,6 +47,7 @@ from core.tools.tool_manager import ToolManager
from core.trigger.trigger_manager import TriggerManager
from core.variables.segments import ArrayFileSegment, FileSegment, Segment
from core.workflow.entities.pause_reason import HumanInputRequired
from core.workflow.entities.workflow_start_reason import WorkflowStartReason
from core.workflow.enums import (
NodeType,
SystemVariableKey,
@ -199,7 +200,7 @@ class WorkflowResponseConverter:
task_id: str,
workflow_run_id: str,
workflow_id: str,
is_resumption: bool,
reason: WorkflowStartReason,
) -> WorkflowStartStreamResponse:
run_id = self._ensure_workflow_run_id(workflow_run_id)
started_at = naive_utc_now()
@ -213,7 +214,7 @@ class WorkflowResponseConverter:
workflow_id=workflow_id,
inputs=self._workflow_inputs,
created_at=int(started_at.timestamp()),
is_resumption=is_resumption,
reason=reason,
),
)
@ -410,7 +411,6 @@ class WorkflowResponseConverter:
iteration_id=event.in_iteration_id,
loop_id=event.in_loop_id,
agent_strategy=event.agent_strategy,
is_resumption=event.is_resumption,
),
)

View File

@ -272,7 +272,7 @@ class WorkflowAppGenerateTaskPipeline(GraphRuntimeStateSupport):
task_id=self._application_generate_entity.task_id,
workflow_run_id=run_id,
workflow_id=self._workflow.id,
is_resumption=event.is_resumption,
reason=event.reason,
)
yield start_resp

View File

@ -359,7 +359,7 @@ class WorkflowBasedAppRunner:
:param event: event
"""
if isinstance(event, GraphRunStartedEvent):
self._publish_event(QueueWorkflowStartedEvent(is_resumption=event.is_resumption))
self._publish_event(QueueWorkflowStartedEvent(reason=event.reason))
elif isinstance(event, GraphRunSucceededEvent):
self._publish_event(QueueWorkflowSucceededEvent(outputs=event.outputs))
elif isinstance(event, GraphRunPartialSucceededEvent):
@ -431,7 +431,6 @@ class WorkflowBasedAppRunner:
agent_strategy=event.agent_strategy,
provider_type=event.provider_type,
provider_id=event.provider_id,
is_resumption=event.is_resumption,
)
)
elif isinstance(event, NodeRunSucceededEvent):

View File

@ -9,6 +9,7 @@ from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk
from core.rag.entities.citation_metadata import RetrievalSourceMetadata
from core.workflow.entities import AgentNodeStrategyInit
from core.workflow.entities.pause_reason import PauseReason
from core.workflow.entities.workflow_start_reason import WorkflowStartReason
from core.workflow.enums import WorkflowNodeExecutionMetadataKey
from core.workflow.nodes import NodeType
@ -264,10 +265,8 @@ class QueueWorkflowStartedEvent(AppQueueEvent):
"""QueueWorkflowStartedEvent entity."""
event: QueueEvent = QueueEvent.WORKFLOW_STARTED
# is_resumption indicating whether this `start` is a
# resumption of previously suspended execution.
is_resumption: bool = False
# Always present; mirrors GraphRunStartedEvent.reason for downstream consumers.
reason: WorkflowStartReason = WorkflowStartReason.INITIAL
class QueueWorkflowSucceededEvent(AppQueueEvent):
@ -319,10 +318,6 @@ class QueueNodeStartedEvent(AppQueueEvent):
# FIXME(-LAN-): only for ToolNode, need to refactor
provider_type: str # should be a core.tools.entities.tool_entities.ToolProviderType
provider_id: str
is_resumption: bool = Field(
default=False,
description="True only when this node had already started and execution resumed after a pause.",
)
class QueueNodeSucceededEvent(AppQueueEvent):

View File

@ -7,6 +7,7 @@ from pydantic import BaseModel, ConfigDict, Field
from core.model_runtime.entities.llm_entities import LLMResult, LLMUsage
from core.rag.entities.citation_metadata import RetrievalSourceMetadata
from core.workflow.entities import AgentNodeStrategyInit
from core.workflow.entities.workflow_start_reason import WorkflowStartReason
from core.workflow.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus
from core.workflow.nodes.human_input.entities import FormInput, UserAction
@ -209,7 +210,8 @@ class WorkflowStartStreamResponse(StreamResponse):
workflow_id: str
inputs: Mapping[str, Any]
created_at: int
is_resumption: bool = False
# Always present; mirrors QueueWorkflowStartedEvent.reason for SSE clients.
reason: WorkflowStartReason = WorkflowStartReason.INITIAL
event: StreamEvent = StreamEvent.WORKFLOW_STARTED
workflow_run_id: str
@ -326,7 +328,6 @@ class NodeStartStreamResponse(StreamResponse):
iteration_id: str | None = None
loop_id: str | None = None
agent_strategy: AgentNodeStrategyInit | None = None
is_resumption: bool = False
event: StreamEvent = StreamEvent.NODE_STARTED
workflow_run_id: str
@ -349,7 +350,6 @@ class NodeStartStreamResponse(StreamResponse):
"extras": {},
"iteration_id": self.data.iteration_id,
"loop_id": self.data.loop_id,
"is_resumption": self.data.is_resumption,
},
}

View File

@ -2,10 +2,12 @@ from .agent import AgentNodeStrategyInit
from .graph_init_params import GraphInitParams
from .workflow_execution import WorkflowExecution
from .workflow_node_execution import WorkflowNodeExecution
from .workflow_start_reason import WorkflowStartReason
__all__ = [
"AgentNodeStrategyInit",
"GraphInitParams",
"WorkflowExecution",
"WorkflowNodeExecution",
"WorkflowStartReason",
]

View File

@ -0,0 +1,8 @@
from enum import StrEnum
class WorkflowStartReason(StrEnum):
"""Reason for workflow start events across graph/queue/SSE layers."""
INITIAL = "initial" # First start of a workflow run.
RESUMPTION = "resumption" # Start triggered after resuming a paused run.

View File

@ -13,6 +13,7 @@ from typing import TYPE_CHECKING, cast, final
from flask import Flask, current_app
from core.workflow.entities.workflow_start_reason import WorkflowStartReason
from core.workflow.enums import NodeExecutionType
from core.workflow.graph import Graph
from core.workflow.graph_events import (
@ -235,7 +236,9 @@ class GraphEngine:
self._graph_execution.paused = False
self._graph_execution.pause_reasons = []
start_event = GraphRunStartedEvent(is_resumption=is_resume)
start_event = GraphRunStartedEvent(
reason=WorkflowStartReason.RESUMPTION if is_resume else WorkflowStartReason.INITIAL,
)
self._event_manager.notify_layers(start_event)
yield start_event

View File

@ -1,13 +1,16 @@
from pydantic import Field
from core.workflow.entities.pause_reason import PauseReason
from core.workflow.entities.workflow_start_reason import WorkflowStartReason
from core.workflow.graph_events import BaseGraphEvent
class GraphRunStartedEvent(BaseGraphEvent):
# is_resumption indicating whether this `start` is a
# resumption of previously suspended execution.
is_resumption: bool = False
# Reason is emitted for workflow start events and is always set.
reason: WorkflowStartReason = Field(
default=WorkflowStartReason.INITIAL,
description="reason for workflow start",
)
class GraphRunSucceededEvent(BaseGraphEvent):

View File

@ -15,10 +15,6 @@ class NodeRunStartedEvent(GraphNodeEventBase):
predecessor_node_id: str | None = None
agent_strategy: AgentNodeStrategyInit | None = None
start_at: datetime = Field(..., description="node start time")
is_resumption: bool = Field(
default=False,
description="True only when this node had already started and execution resumed after a pause.",
)
# FIXME(-LAN-): only for ToolNode
provider_type: str = ""

View File

@ -301,7 +301,6 @@ class Node(Generic[NodeDataT]):
def run(self) -> Generator[GraphNodeEventBase, None, None]:
execution_id = self.ensure_execution_id()
self._start_at = naive_utc_now()
is_resumption = self.graph_runtime_state.is_node_resumption(self._node_id, execution_id)
# Create and push start event with required fields
start_event = NodeRunStartedEvent(
@ -311,7 +310,6 @@ class Node(Generic[NodeDataT]):
node_title=self.title,
in_iteration_id=None,
start_at=self._start_at,
is_resumption=is_resumption,
)
# === FIXME(-LAN-): Needs to refactor.

View File

@ -3,6 +3,7 @@ from types import SimpleNamespace
from core.app.apps.common.workflow_response_converter import WorkflowResponseConverter
from core.app.entities.app_invoke_entities import InvokeFrom
from core.app.entities.queue_entities import QueueHumanInputFormFilledEvent
from core.workflow.entities.workflow_start_reason import WorkflowStartReason
from core.workflow.runtime import GraphRuntimeState, VariablePool
from core.workflow.system_variable import SystemVariable
@ -39,7 +40,7 @@ def test_human_input_form_filled_stream_response_contains_rendered_content():
task_id="task-1",
workflow_run_id="run-1",
workflow_id="wf-1",
is_resumption=False,
reason=WorkflowStartReason.INITIAL,
)
queue_event = QueueHumanInputFormFilledEvent(

View File

@ -1,14 +1,8 @@
from dataclasses import dataclass
from types import SimpleNamespace
import pytest
from core.app.apps.common.workflow_response_converter import WorkflowResponseConverter
from core.app.entities.app_invoke_entities import InvokeFrom
from core.app.entities.queue_entities import QueueNodeStartedEvent
from core.app.entities.task_entities import NodeStartStreamResponse
from core.workflow.entities import AgentNodeStrategyInit
from core.workflow.enums import NodeType
from core.workflow.entities.workflow_start_reason import WorkflowStartReason
from core.workflow.runtime import GraphRuntimeState, VariablePool
from core.workflow.system_variable import SystemVariable
@ -40,116 +34,23 @@ def _build_converter() -> WorkflowResponseConverter:
)
def test_node_start_stream_response_carries_resumption_flag():
converter = _build_converter()
# Seed workflow run id for converter
converter.workflow_start_to_stream_response(
task_id="task-1",
workflow_run_id="run-1",
workflow_id="wf-1",
is_resumption=False,
)
queue_event = QueueNodeStartedEvent(
node_execution_id="exec-1",
node_id="node-1",
node_title="Title",
node_type=NodeType.CODE,
start_at=converter._workflow_started_at, # type: ignore[attr-defined]
agent_strategy=AgentNodeStrategyInit(name="test"),
provider_type="",
provider_id="",
is_resumption=True,
)
resp = converter.workflow_node_start_to_stream_response(event=queue_event, task_id="task-1")
assert isinstance(resp, NodeStartStreamResponse)
assert resp.data.is_resumption is True
def test_node_start_stream_response_defaults_to_false():
converter = _build_converter()
converter.workflow_start_to_stream_response(
task_id="task-1",
workflow_run_id="run-1",
workflow_id="wf-1",
is_resumption=False,
)
queue_event = QueueNodeStartedEvent(
node_execution_id="exec-2",
node_id="node-2",
node_title="Title",
node_type=NodeType.CODE,
start_at=converter._workflow_started_at, # type: ignore[attr-defined]
agent_strategy=None,
provider_type="",
provider_id="",
)
resp = converter.workflow_node_start_to_stream_response(event=queue_event, task_id="task-1")
assert isinstance(resp, NodeStartStreamResponse)
assert resp.data.is_resumption is False
def test_workflow_start_stream_response_carries_resumption_flag():
def test_workflow_start_stream_response_carries_resumption_reason():
converter = _build_converter()
resp = converter.workflow_start_to_stream_response(
task_id="task-1",
workflow_run_id="run-1",
workflow_id="wf-1",
is_resumption=True,
reason=WorkflowStartReason.RESUMPTION,
)
assert resp.data.is_resumption is True
assert resp.data.reason is WorkflowStartReason.RESUMPTION
def test_workflow_start_stream_response_defaults_to_false():
def test_workflow_start_stream_response_carries_initial_reason():
converter = _build_converter()
resp = converter.workflow_start_to_stream_response(
task_id="task-1",
workflow_run_id="run-1",
workflow_id="wf-1",
is_resumption=False,
reason=WorkflowStartReason.INITIAL,
)
assert resp.data.is_resumption is False
@dataclass(frozen=True)
class _IgnoreDetailCase:
execution_id: str
node_id: str
is_resumption: bool
@pytest.mark.parametrize(
"case",
[
_IgnoreDetailCase(execution_id="exec-1", node_id="node-1", is_resumption=True),
_IgnoreDetailCase(execution_id="exec-2", node_id="node-2", is_resumption=False),
],
)
def test_node_start_ignore_detail_includes_resumption_flag(case: _IgnoreDetailCase) -> None:
converter = _build_converter()
converter.workflow_start_to_stream_response(
task_id="task-1",
workflow_run_id="run-1",
workflow_id="wf-1",
is_resumption=False,
)
queue_event = QueueNodeStartedEvent(
node_execution_id=case.execution_id,
node_id=case.node_id,
node_title="Title",
node_type=NodeType.CODE,
start_at=converter._workflow_started_at, # type: ignore[attr-defined]
agent_strategy=None,
provider_type="",
provider_id="",
is_resumption=case.is_resumption,
)
resp = converter.workflow_node_start_to_stream_response(event=queue_event, task_id="task-1")
assert isinstance(resp, NodeStartStreamResponse)
ignore_detail = resp.to_ignore_detail_dict()
assert ignore_detail["data"]["is_resumption"] is case.is_resumption
assert resp.data.reason is WorkflowStartReason.INITIAL

View File

@ -23,6 +23,7 @@ from core.app.entities.queue_entities import (
QueueNodeStartedEvent,
QueueNodeSucceededEvent,
)
from core.workflow.entities.workflow_start_reason import WorkflowStartReason
from core.workflow.enums import NodeType
from core.workflow.system_variable import SystemVariable
from libs.datetime_utils import naive_utc_now
@ -128,7 +129,7 @@ class TestWorkflowResponseConverter:
task_id="bootstrap",
workflow_run_id="run-id",
workflow_id="wf-id",
is_resumption=False,
reason=WorkflowStartReason.INITIAL,
)
start_event = self.create_node_started_event()
converter.workflow_node_start_to_stream_response(
@ -169,7 +170,7 @@ class TestWorkflowResponseConverter:
task_id="bootstrap",
workflow_run_id="run-id",
workflow_id="wf-id",
is_resumption=False,
reason=WorkflowStartReason.INITIAL,
)
start_event = self.create_node_started_event()
converter.workflow_node_start_to_stream_response(
@ -205,7 +206,7 @@ class TestWorkflowResponseConverter:
task_id="bootstrap",
workflow_run_id="run-id",
workflow_id="wf-id",
is_resumption=False,
reason=WorkflowStartReason.INITIAL,
)
start_event = self.create_node_started_event()
converter.workflow_node_start_to_stream_response(
@ -244,7 +245,7 @@ class TestWorkflowResponseConverter:
task_id="bootstrap",
workflow_run_id="run-id",
workflow_id="wf-id",
is_resumption=False,
reason=WorkflowStartReason.INITIAL,
)
start_event = self.create_node_started_event()
converter.workflow_node_start_to_stream_response(
@ -285,7 +286,7 @@ class TestWorkflowResponseConverter:
task_id="bootstrap",
workflow_run_id="run-id",
workflow_id="wf-id",
is_resumption=False,
reason=WorkflowStartReason.INITIAL,
)
start_event = self.create_node_started_event()
converter.workflow_node_start_to_stream_response(
@ -425,7 +426,7 @@ class TestWorkflowResponseConverterServiceApiTruncation:
task_id="test-task-id",
workflow_run_id="test-workflow-run-id",
workflow_id="test-workflow-id",
is_resumption=False,
reason=WorkflowStartReason.INITIAL,
)
return converter

View File

@ -7,6 +7,7 @@ from core.app.entities.app_invoke_entities import InvokeFrom
from core.app.entities.queue_entities import QueueWorkflowPausedEvent
from core.app.entities.task_entities import HumanInputRequiredResponse, WorkflowPauseStreamResponse
from core.workflow.entities.pause_reason import HumanInputRequired
from core.workflow.entities.workflow_start_reason import WorkflowStartReason
from core.workflow.graph_events.graph import GraphRunPausedEvent
from core.workflow.nodes.human_input.entities import FormInput, UserAction
from core.workflow.nodes.human_input.enums import FormInputType
@ -116,7 +117,7 @@ def test_queue_workflow_paused_event_to_stream_responses():
task_id="task",
workflow_run_id="run-id",
workflow_id="workflow-id",
is_resumption=False,
reason=WorkflowStartReason.INITIAL,
)
reason = HumanInputRequired(

View File

@ -2,10 +2,6 @@
from __future__ import annotations
from dataclasses import dataclass
import pytest
from core.workflow.enums import NodeExecutionType, NodeState, NodeType, WorkflowNodeExecutionStatus
from core.workflow.graph import Graph
from core.workflow.graph_engine.domain.graph_execution import GraphExecution
@ -121,72 +117,3 @@ def test_retry_does_not_emit_additional_start_event() -> None:
node_execution = graph_execution.get_or_create_node_execution(node_id)
assert node_execution.retry_count == 1
@dataclass(frozen=True)
class _ResumptionFlagCase:
node_id: str
execution_id: str
node_title: str
is_resumption: bool
@pytest.mark.parametrize(
"case",
[
_ResumptionFlagCase(
node_id="resumed-node",
execution_id="exec-1",
node_title="Resumed Node",
is_resumption=True,
),
_ResumptionFlagCase(
node_id="fresh-node",
execution_id="exec-2",
node_title="Fresh Node",
is_resumption=False,
),
],
)
def test_node_start_preserves_resumption_flag(case: _ResumptionFlagCase) -> None:
"""Ensure NodeRunStartedEvent preserves resumption flag."""
handler, event_manager, _ = _build_event_handler(case.node_id)
start_event = NodeRunStartedEvent(
id=case.execution_id,
node_id=case.node_id,
node_type=NodeType.CODE,
node_title=case.node_title,
start_at=naive_utc_now(),
is_resumption=case.is_resumption,
)
handler.dispatch(start_event)
collected = event_manager._events # type: ignore[attr-defined]
assert len(collected) == 1
emitted_event = collected[0]
assert isinstance(emitted_event, NodeRunStartedEvent)
assert emitted_event.is_resumption is case.is_resumption
def test_node_start_marks_fresh_run_as_not_resumption() -> None:
"""Ensure fresh NodeRunStartedEvent carries is_resumption=False."""
node_id = "fresh-node"
handler, event_manager, _ = _build_event_handler(node_id)
start_event = NodeRunStartedEvent(
id="exec-2",
node_id=node_id,
node_type=NodeType.CODE,
node_title="Fresh Node",
start_at=naive_utc_now(),
)
handler.dispatch(start_event)
collected = event_manager._events # type: ignore[attr-defined]
assert len(collected) == 1
emitted_event = collected[0]
assert isinstance(emitted_event, NodeRunStartedEvent)
assert emitted_event.is_resumption is False

View File

@ -7,6 +7,7 @@ from typing import Any
from core.model_runtime.entities.llm_entities import LLMMode
from core.model_runtime.entities.message_entities import PromptMessageRole
from core.workflow.entities import GraphInitParams
from core.workflow.entities.workflow_start_reason import WorkflowStartReason
from core.workflow.graph import Graph
from core.workflow.graph_engine.command_channels.in_memory_channel import InMemoryChannel
from core.workflow.graph_engine.graph_engine import GraphEngine
@ -313,7 +314,7 @@ def test_parallel_human_input_pause_preserves_node_finished_after_snapshot_resum
events = list(engine.run())
start_event = next(e for e in events if isinstance(e, GraphRunStartedEvent))
assert start_event.is_resumption is True
assert start_event.reason is WorkflowStartReason.RESUMPTION
llm_started = any(isinstance(e, NodeRunStartedEvent) and e.node_id == "llm_a" for e in events)
llm_succeeded = any(isinstance(e, NodeRunSucceededEvent) and e.node_id == "llm_a" for e in events)

View File

@ -7,6 +7,7 @@ from typing import Any
from core.model_runtime.entities.llm_entities import LLMMode
from core.model_runtime.entities.message_entities import PromptMessageRole
from core.workflow.entities import GraphInitParams
from core.workflow.entities.workflow_start_reason import WorkflowStartReason
from core.workflow.graph import Graph
from core.workflow.graph_engine.command_channels.in_memory_channel import InMemoryChannel
from core.workflow.graph_engine.graph_engine import GraphEngine
@ -294,9 +295,8 @@ def test_pause_defers_ready_nodes_until_resume() -> None:
resumed_events = list(resumed_engine.run())
start_event = next(e for e in resumed_events if isinstance(e, GraphRunStartedEvent))
assert start_event.is_resumption is True
assert start_event.reason is WorkflowStartReason.RESUMPTION
llm_b_started = _get_node_started_event(resumed_events, "llm_b")
assert llm_b_started is not None
assert llm_b_started.is_resumption is False
assert any(isinstance(e, NodeRunSucceededEvent) and e.node_id == "llm_b" for e in resumed_events)

View File

@ -4,6 +4,7 @@ from typing import Any
from unittest.mock import MagicMock
from core.workflow.entities import GraphInitParams
from core.workflow.entities.workflow_start_reason import WorkflowStartReason
from core.workflow.graph import Graph
from core.workflow.graph_engine.command_channels.in_memory_channel import InMemoryChannel
from core.workflow.graph_engine.graph_engine import GraphEngine
@ -172,7 +173,7 @@ def test_engine_resume_restores_state_and_completion():
assert baseline_events
first_paused_event = baseline_events[0]
assert isinstance(first_paused_event, GraphRunStartedEvent)
assert first_paused_event.is_resumption is False
assert first_paused_event.reason is WorkflowStartReason.INITIAL
assert isinstance(baseline_events[-1], GraphRunSucceededEvent)
baseline_success_nodes = _node_successes(baseline_events)
@ -184,7 +185,7 @@ def test_engine_resume_restores_state_and_completion():
assert paused_events
first_paused_event = paused_events[0]
assert isinstance(first_paused_event, GraphRunStartedEvent)
assert first_paused_event.is_resumption is False
assert first_paused_event.reason is WorkflowStartReason.INITIAL
assert isinstance(paused_events[-1], GraphRunPausedEvent)
snapshot = paused_state.dumps()
@ -196,7 +197,7 @@ def test_engine_resume_restores_state_and_completion():
assert resumed_events
first_resumed_event = resumed_events[0]
assert isinstance(first_resumed_event, GraphRunStartedEvent)
assert first_resumed_event.is_resumption is True
assert first_resumed_event.reason is WorkflowStartReason.RESUMPTION
assert isinstance(resumed_events[-1], GraphRunSucceededEvent)
combined_success_nodes = _node_successes(paused_events) + _node_successes(resumed_events)
@ -207,8 +208,6 @@ def test_engine_resume_restores_state_and_completion():
assert paused_human_started is not None
assert resumed_human_started is not None
assert paused_human_started.id == resumed_human_started.id
assert paused_human_started.is_resumption is False
assert resumed_human_started.is_resumption is True
assert baseline_state.outputs == resumed_state.outputs
assert _segment_value(baseline_state.variable_pool, ("human", "__action_id")) == _segment_value(

View File

@ -149,10 +149,7 @@ class TestUserAction:
UserAction(**data)
errors = exc_info.value.errors()
assert any(
error["loc"] == (field_name,) and error["type"] == "string_too_long"
for error in errors
)
assert any(error["loc"] == (field_name,) and error["type"] == "string_too_long" for error in errors)
class TestHumanInputNodeData: