mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: [Breaking] Remove Python-only declarative actions and rename alias kinds to C# canonical names (#6126)
* Remove Python-only declarative actions and rename alias kinds to C# canonical names * Address PR comments. * Address PR comments. * Reduce verbose and duplicate output from sample workflow.
This commit is contained in:
committed by
GitHub
Unverified
parent
55dc3ce734
commit
ded17b178c
@@ -515,27 +515,6 @@ class TestBasicExecutorsCoverage:
|
||||
assert state.get("Local.b") == 2
|
||||
assert state.get("Local.c") == 3
|
||||
|
||||
async def test_append_value_executor(self, mock_context, mock_state):
|
||||
"""Test AppendValueExecutor."""
|
||||
from agent_framework_declarative._workflows._executors_basic import (
|
||||
AppendValueExecutor,
|
||||
)
|
||||
|
||||
state = DeclarativeWorkflowState(mock_state)
|
||||
state.initialize()
|
||||
state.set("Local.items", ["a"])
|
||||
|
||||
action_def = {
|
||||
"kind": "AppendValue",
|
||||
"path": "Local.items",
|
||||
"value": "b",
|
||||
}
|
||||
executor = AppendValueExecutor(action_def)
|
||||
await executor.handle_action(ActionTrigger(), mock_context)
|
||||
|
||||
result = state.get("Local.items")
|
||||
assert result == ["a", "b"]
|
||||
|
||||
async def test_reset_variable_executor(self, mock_context, mock_state):
|
||||
"""Test ResetVariableExecutor."""
|
||||
from agent_framework_declarative._workflows._executors_basic import (
|
||||
@@ -632,52 +611,6 @@ class TestBasicExecutorsCoverage:
|
||||
|
||||
mock_context.yield_output.assert_called_once_with("Dynamic message")
|
||||
|
||||
async def test_emit_event_executor_graph_mode(self, mock_context, mock_state):
|
||||
"""Test EmitEventExecutor with graph-mode schema (eventName/eventValue)."""
|
||||
from agent_framework_declarative._workflows._executors_basic import (
|
||||
EmitEventExecutor,
|
||||
)
|
||||
|
||||
state = DeclarativeWorkflowState(mock_state)
|
||||
state.initialize()
|
||||
|
||||
action_def = {
|
||||
"kind": "EmitEvent",
|
||||
"eventName": "myEvent",
|
||||
"eventValue": {"key": "value"},
|
||||
}
|
||||
executor = EmitEventExecutor(action_def)
|
||||
await executor.handle_action(ActionTrigger(), mock_context)
|
||||
|
||||
mock_context.yield_output.assert_called_once()
|
||||
event_data = mock_context.yield_output.call_args[0][0]
|
||||
assert event_data["eventName"] == "myEvent"
|
||||
assert event_data["eventValue"] == {"key": "value"}
|
||||
|
||||
async def test_emit_event_executor_interpreter_mode(self, mock_context, mock_state):
|
||||
"""Test EmitEventExecutor with interpreter-mode schema (event.name/event.data)."""
|
||||
from agent_framework_declarative._workflows._executors_basic import (
|
||||
EmitEventExecutor,
|
||||
)
|
||||
|
||||
state = DeclarativeWorkflowState(mock_state)
|
||||
state.initialize()
|
||||
|
||||
action_def = {
|
||||
"kind": "EmitEvent",
|
||||
"event": {
|
||||
"name": "interpreterEvent",
|
||||
"data": {"payload": "test"},
|
||||
},
|
||||
}
|
||||
executor = EmitEventExecutor(action_def)
|
||||
await executor.handle_action(ActionTrigger(), mock_context)
|
||||
|
||||
mock_context.yield_output.assert_called_once()
|
||||
event_data = mock_context.yield_output.call_args[0][0]
|
||||
assert event_data["eventName"] == "interpreterEvent"
|
||||
assert event_data["eventValue"] == {"payload": "test"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Agent Executors Tests - Covering _executors_agents.py gaps
|
||||
@@ -1155,8 +1088,8 @@ class TestControlFlowCoverage:
|
||||
"""Tests for control flow executors covering uncovered code paths."""
|
||||
|
||||
@_requires_powerfx
|
||||
async def test_foreach_with_source_alias(self, mock_context, mock_state):
|
||||
"""Test ForeachInitExecutor with 'source' alias (interpreter mode)."""
|
||||
async def test_foreach_with_source(self, mock_context, mock_state):
|
||||
"""Test ForeachInitExecutor with the 'source' field."""
|
||||
from agent_framework_declarative._workflows._executors_control_flow import (
|
||||
ForeachInitExecutor,
|
||||
)
|
||||
@@ -1205,8 +1138,8 @@ class TestControlFlowCoverage:
|
||||
|
||||
action_def = {
|
||||
"kind": "Foreach",
|
||||
"itemsSource": "=Local.data",
|
||||
"iteratorVariable": "Local.item",
|
||||
"source": "=Local.data",
|
||||
"itemName": "item",
|
||||
}
|
||||
executor = ForeachNextExecutor(action_def, init_executor_id="foreach_init")
|
||||
|
||||
@@ -1217,81 +1150,6 @@ class TestControlFlowCoverage:
|
||||
assert msg.current_index == 1
|
||||
assert msg.current_item == "b"
|
||||
|
||||
@_requires_powerfx
|
||||
async def test_switch_evaluator_with_value_cases(self, mock_context, mock_state):
|
||||
"""Test SwitchEvaluatorExecutor with value/cases schema."""
|
||||
from agent_framework_declarative._workflows._executors_control_flow import (
|
||||
SwitchEvaluatorExecutor,
|
||||
)
|
||||
|
||||
state = DeclarativeWorkflowState(mock_state)
|
||||
state.initialize()
|
||||
state.set("Local.status", "pending")
|
||||
|
||||
action_def = {
|
||||
"kind": "Switch",
|
||||
"value": "=Local.status",
|
||||
}
|
||||
cases = [
|
||||
{"match": "active"},
|
||||
{"match": "pending"},
|
||||
]
|
||||
executor = SwitchEvaluatorExecutor(action_def, cases=cases)
|
||||
|
||||
await executor.handle_action(ActionTrigger(), mock_context)
|
||||
|
||||
msg = mock_context.send_message.call_args[0][0]
|
||||
assert isinstance(msg, ConditionResult)
|
||||
assert msg.matched is True
|
||||
assert msg.branch_index == 1 # Second case matched
|
||||
|
||||
@_requires_powerfx
|
||||
async def test_switch_evaluator_default_case(self, mock_context, mock_state):
|
||||
"""Test SwitchEvaluatorExecutor falls through to default."""
|
||||
from agent_framework_declarative._workflows._executors_control_flow import (
|
||||
SwitchEvaluatorExecutor,
|
||||
)
|
||||
|
||||
state = DeclarativeWorkflowState(mock_state)
|
||||
state.initialize()
|
||||
state.set("Local.status", "unknown")
|
||||
|
||||
action_def = {
|
||||
"kind": "Switch",
|
||||
"value": "=Local.status",
|
||||
}
|
||||
cases = [
|
||||
{"match": "active"},
|
||||
{"match": "pending"},
|
||||
]
|
||||
executor = SwitchEvaluatorExecutor(action_def, cases=cases)
|
||||
|
||||
await executor.handle_action(ActionTrigger(), mock_context)
|
||||
|
||||
msg = mock_context.send_message.call_args[0][0]
|
||||
assert isinstance(msg, ConditionResult)
|
||||
assert msg.matched is False
|
||||
assert msg.branch_index == -1 # Default case
|
||||
|
||||
async def test_switch_evaluator_no_value(self, mock_context, mock_state):
|
||||
"""Test SwitchEvaluatorExecutor with no value defaults to else."""
|
||||
from agent_framework_declarative._workflows._executors_control_flow import (
|
||||
SwitchEvaluatorExecutor,
|
||||
)
|
||||
|
||||
state = DeclarativeWorkflowState(mock_state)
|
||||
state.initialize()
|
||||
|
||||
action_def = {"kind": "Switch"} # No value
|
||||
cases = [{"match": "x"}]
|
||||
executor = SwitchEvaluatorExecutor(action_def, cases=cases)
|
||||
|
||||
await executor.handle_action(ActionTrigger(), mock_context)
|
||||
|
||||
msg = mock_context.send_message.call_args[0][0]
|
||||
assert isinstance(msg, ConditionResult)
|
||||
assert msg.branch_index == -1
|
||||
|
||||
async def test_join_executor_accepts_condition_result(self, mock_context, mock_state):
|
||||
"""Test JoinExecutor accepts ConditionResult as trigger."""
|
||||
from agent_framework_declarative._workflows._executors_control_flow import (
|
||||
@@ -1357,8 +1215,8 @@ class TestControlFlowCoverage:
|
||||
|
||||
action_def = {
|
||||
"kind": "Foreach",
|
||||
"itemsSource": "=Local.data",
|
||||
"iteratorVariable": "Local.item",
|
||||
"source": "=Local.data",
|
||||
"itemName": "item",
|
||||
}
|
||||
executor = ForeachNextExecutor(action_def, init_executor_id="missing_loop")
|
||||
|
||||
@@ -1391,8 +1249,8 @@ class TestControlFlowCoverage:
|
||||
|
||||
action_def = {
|
||||
"kind": "Foreach",
|
||||
"itemsSource": "=Local.data",
|
||||
"iteratorVariable": "Local.item",
|
||||
"source": "=Local.data",
|
||||
"itemName": "item",
|
||||
}
|
||||
executor = ForeachNextExecutor(action_def, init_executor_id="loop_id")
|
||||
|
||||
@@ -1425,8 +1283,8 @@ class TestControlFlowCoverage:
|
||||
|
||||
action_def = {
|
||||
"kind": "Foreach",
|
||||
"itemsSource": "=Local.data",
|
||||
"iteratorVariable": "Local.item",
|
||||
"source": "=Local.data",
|
||||
"itemName": "item",
|
||||
}
|
||||
executor = ForeachNextExecutor(action_def, init_executor_id="loop_id")
|
||||
|
||||
@@ -1459,8 +1317,8 @@ class TestControlFlowCoverage:
|
||||
|
||||
action_def = {
|
||||
"kind": "Foreach",
|
||||
"itemsSource": "=Local.data",
|
||||
"iteratorVariable": "Local.item",
|
||||
"source": "=Local.data",
|
||||
"itemName": "item",
|
||||
}
|
||||
executor = ForeachNextExecutor(action_def, init_executor_id="loop_id")
|
||||
|
||||
@@ -1719,60 +1577,6 @@ class TestDeclarativeActionExecutorBase:
|
||||
class TestHumanInputExecutorsCoverage:
|
||||
"""Tests for human input executors covering uncovered code paths."""
|
||||
|
||||
async def test_wait_for_input_executor_with_prompt(self, mock_context, mock_state):
|
||||
"""Test WaitForInputExecutor with prompt."""
|
||||
from agent_framework_declarative._workflows._executors_external_input import (
|
||||
ExternalInputRequest,
|
||||
WaitForInputExecutor,
|
||||
)
|
||||
|
||||
state = DeclarativeWorkflowState(mock_state)
|
||||
state.initialize()
|
||||
|
||||
action_def = {
|
||||
"kind": "WaitForInput",
|
||||
"prompt": "Please enter your name:",
|
||||
"property": "Local.userName",
|
||||
"timeout": 30,
|
||||
}
|
||||
executor = WaitForInputExecutor(action_def)
|
||||
|
||||
await executor.handle_action(ActionTrigger(), mock_context)
|
||||
|
||||
# Should yield prompt first, then call request_info
|
||||
assert mock_context.yield_output.call_count == 1
|
||||
assert mock_context.yield_output.call_args_list[0][0][0] == "Please enter your name:"
|
||||
# request_info call for ExternalInputRequest
|
||||
mock_context.request_info.assert_called_once()
|
||||
request = mock_context.request_info.call_args[0][0]
|
||||
assert isinstance(request, ExternalInputRequest)
|
||||
assert request.request_type == "user_input"
|
||||
|
||||
async def test_wait_for_input_executor_no_prompt(self, mock_context, mock_state):
|
||||
"""Test WaitForInputExecutor without prompt."""
|
||||
from agent_framework_declarative._workflows._executors_external_input import (
|
||||
ExternalInputRequest,
|
||||
WaitForInputExecutor,
|
||||
)
|
||||
|
||||
state = DeclarativeWorkflowState(mock_state)
|
||||
state.initialize()
|
||||
|
||||
action_def = {
|
||||
"kind": "WaitForInput",
|
||||
"property": "Local.input",
|
||||
}
|
||||
executor = WaitForInputExecutor(action_def)
|
||||
|
||||
await executor.handle_action(ActionTrigger(), mock_context)
|
||||
|
||||
# Should not yield output (no prompt), just call request_info
|
||||
assert mock_context.yield_output.call_count == 0
|
||||
mock_context.request_info.assert_called_once()
|
||||
request = mock_context.request_info.call_args[0][0]
|
||||
assert isinstance(request, ExternalInputRequest)
|
||||
assert request.request_type == "user_input"
|
||||
|
||||
async def test_request_external_input_executor(self, mock_context, mock_state):
|
||||
"""Test RequestExternalInputExecutor."""
|
||||
from agent_framework_declarative._workflows._executors_external_input import (
|
||||
@@ -1786,8 +1590,8 @@ class TestHumanInputExecutorsCoverage:
|
||||
action_def = {
|
||||
"kind": "RequestExternalInput",
|
||||
"requestType": "approval",
|
||||
"message": "Please approve this request",
|
||||
"property": "Local.approvalResult",
|
||||
"prompt": {"text": "Please approve this request"},
|
||||
"variable": "Local.approvalResult",
|
||||
"timeout": 3600,
|
||||
"requiredFields": ["approver", "notes"],
|
||||
"metadata": {"priority": "high"},
|
||||
@@ -1817,8 +1621,8 @@ class TestHumanInputExecutorsCoverage:
|
||||
|
||||
action_def = {
|
||||
"kind": "Question",
|
||||
"question": "Select an option:",
|
||||
"property": "Local.selection",
|
||||
"question": {"text": "Select an option:"},
|
||||
"variable": "Local.selection",
|
||||
"choices": [
|
||||
{"value": "a", "label": "Option A"},
|
||||
{"value": "b"}, # No label, should use value
|
||||
@@ -1841,6 +1645,111 @@ class TestHumanInputExecutorsCoverage:
|
||||
assert choices[2] == {"value": "c", "label": "c"}
|
||||
assert request.metadata["allow_free_text"] is False
|
||||
|
||||
async def test_question_executor_reads_nested_question_text(self, mock_context, mock_state):
|
||||
"""QuestionExecutor reads ``question.text``/``variable``/``default`` into the request."""
|
||||
from agent_framework_declarative._workflows._executors_external_input import (
|
||||
ExternalInputRequest,
|
||||
QuestionExecutor,
|
||||
)
|
||||
|
||||
state = DeclarativeWorkflowState(mock_state)
|
||||
state.initialize()
|
||||
|
||||
action_def = {
|
||||
"kind": "Question",
|
||||
"question": {"text": "What is your name?"},
|
||||
"variable": "Local.userName",
|
||||
"default": "Guest",
|
||||
}
|
||||
executor = QuestionExecutor(action_def)
|
||||
|
||||
await executor.handle_action(ActionTrigger(), mock_context)
|
||||
|
||||
mock_context.request_info.assert_called_once()
|
||||
request = mock_context.request_info.call_args[0][0]
|
||||
assert isinstance(request, ExternalInputRequest)
|
||||
# Canonical text comes through as a plain string, not the stringified dict.
|
||||
assert request.message == "What is your name?"
|
||||
# Canonical `variable` overrides the legacy default of Local.answer.
|
||||
assert request.metadata["output_property"] == "Local.userName"
|
||||
assert request.metadata["default_value"] == "Guest"
|
||||
|
||||
async def test_question_executor_reads_top_level_alternates(self, mock_context, mock_state):
|
||||
"""Top-level ``text``/``property``/``defaultValue`` are accepted as alternates."""
|
||||
from agent_framework_declarative._workflows._executors_external_input import (
|
||||
ExternalInputRequest,
|
||||
QuestionExecutor,
|
||||
)
|
||||
|
||||
state = DeclarativeWorkflowState(mock_state)
|
||||
state.initialize()
|
||||
|
||||
action_def = {
|
||||
"kind": "Question",
|
||||
"text": "Legacy question",
|
||||
"property": "Local.legacyAnswer",
|
||||
"defaultValue": "legacy-default",
|
||||
}
|
||||
executor = QuestionExecutor(action_def)
|
||||
|
||||
await executor.handle_action(ActionTrigger(), mock_context)
|
||||
|
||||
request = mock_context.request_info.call_args[0][0]
|
||||
assert isinstance(request, ExternalInputRequest)
|
||||
assert request.message == "Legacy question"
|
||||
assert request.metadata["output_property"] == "Local.legacyAnswer"
|
||||
assert request.metadata["default_value"] == "legacy-default"
|
||||
|
||||
async def test_request_external_input_reads_nested_prompt_text(self, mock_context, mock_state):
|
||||
"""RequestExternalInputExecutor reads ``prompt.text``/``variable``/``default``."""
|
||||
from agent_framework_declarative._workflows._executors_external_input import (
|
||||
ExternalInputRequest,
|
||||
RequestExternalInputExecutor,
|
||||
)
|
||||
|
||||
state = DeclarativeWorkflowState(mock_state)
|
||||
state.initialize()
|
||||
|
||||
action_def = {
|
||||
"kind": "RequestExternalInput",
|
||||
"prompt": {"text": "Please approve"},
|
||||
"variable": "Local.approved",
|
||||
"default": "pending",
|
||||
}
|
||||
executor = RequestExternalInputExecutor(action_def)
|
||||
|
||||
await executor.handle_action(ActionTrigger(), mock_context)
|
||||
|
||||
request = mock_context.request_info.call_args[0][0]
|
||||
assert isinstance(request, ExternalInputRequest)
|
||||
assert request.message == "Please approve"
|
||||
assert request.metadata["output_property"] == "Local.approved"
|
||||
assert request.metadata["default_value"] == "pending"
|
||||
|
||||
async def test_request_external_input_reads_top_level_alternates(self, mock_context, mock_state):
|
||||
"""Top-level ``message``/``property`` are accepted as alternates."""
|
||||
from agent_framework_declarative._workflows._executors_external_input import (
|
||||
ExternalInputRequest,
|
||||
RequestExternalInputExecutor,
|
||||
)
|
||||
|
||||
state = DeclarativeWorkflowState(mock_state)
|
||||
state.initialize()
|
||||
|
||||
action_def = {
|
||||
"kind": "RequestExternalInput",
|
||||
"message": "Legacy message",
|
||||
"property": "Local.legacyApproval",
|
||||
}
|
||||
executor = RequestExternalInputExecutor(action_def)
|
||||
|
||||
await executor.handle_action(ActionTrigger(), mock_context)
|
||||
|
||||
request = mock_context.request_info.call_args[0][0]
|
||||
assert isinstance(request, ExternalInputRequest)
|
||||
assert request.message == "Legacy message"
|
||||
assert request.metadata["output_property"] == "Local.legacyApproval"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Additional Agent Executor Tests - External Loop Coverage
|
||||
@@ -2122,7 +2031,7 @@ class TestBuilderControlFlowCreation:
|
||||
|
||||
# Create a mock loop_next executor
|
||||
loop_next = ForeachNextExecutor(
|
||||
{"kind": "Foreach", "itemsProperty": "items"},
|
||||
{"kind": "Foreach", "source": "=Local.items"},
|
||||
init_executor_id="foreach_init",
|
||||
id="foreach_next",
|
||||
)
|
||||
@@ -2181,7 +2090,7 @@ class TestBuilderControlFlowCreation:
|
||||
|
||||
# Create a mock loop_next executor
|
||||
loop_next = ForeachNextExecutor(
|
||||
{"kind": "Foreach", "itemsProperty": "items"},
|
||||
{"kind": "Foreach", "source": "=Local.items"},
|
||||
init_executor_id="foreach_init",
|
||||
id="foreach_next",
|
||||
)
|
||||
@@ -2235,8 +2144,8 @@ class TestBuilderEdgeWiring:
|
||||
{
|
||||
"kind": "Foreach",
|
||||
"id": "loop",
|
||||
"itemsSource": "=Local.items",
|
||||
"iteratorVariable": "Local.item",
|
||||
"source": "=Local.items",
|
||||
"itemName": "item",
|
||||
"actions": [
|
||||
{"kind": "SendActivity", "id": "step_1", "activity": {"text": "one"}},
|
||||
{"kind": "SendActivity", "id": "step_2", "activity": {"text": "two"}},
|
||||
@@ -2266,8 +2175,8 @@ class TestBuilderEdgeWiring:
|
||||
{
|
||||
"kind": "Foreach",
|
||||
"id": "loop",
|
||||
"itemsSource": "=Local.items",
|
||||
"iteratorVariable": "Local.item",
|
||||
"source": "=Local.items",
|
||||
"itemName": "item",
|
||||
"actions": [
|
||||
{"kind": "SendActivity", "id": "step_1", "activity": {"text": "one"}},
|
||||
{"kind": "BreakLoop", "id": "stop"},
|
||||
@@ -2292,8 +2201,8 @@ class TestBuilderEdgeWiring:
|
||||
{
|
||||
"kind": "Foreach",
|
||||
"id": "loop",
|
||||
"itemsSource": "=Local.items",
|
||||
"iteratorVariable": "Local.item",
|
||||
"source": "=Local.items",
|
||||
"itemName": "item",
|
||||
"actions": [
|
||||
{"kind": "SendActivity", "id": "step_1", "activity": {"text": "one"}},
|
||||
{
|
||||
@@ -2704,7 +2613,7 @@ class TestBuilderValidation:
|
||||
assert workflow is not None
|
||||
|
||||
def test_missing_required_field_foreach(self):
|
||||
"""Test Foreach without items raises error."""
|
||||
"""Test Foreach without source raises error."""
|
||||
from agent_framework_declarative._workflows._declarative_builder import DeclarativeWorkflowBuilder
|
||||
|
||||
yaml_def = {
|
||||
@@ -2717,7 +2626,7 @@ class TestBuilderValidation:
|
||||
builder.build()
|
||||
|
||||
assert "Foreach" in str(exc_info.value)
|
||||
assert "items" in str(exc_info.value)
|
||||
assert "source" in str(exc_info.value)
|
||||
|
||||
def test_self_referencing_goto_raises_error(self):
|
||||
"""Test that a goto referencing itself is detected."""
|
||||
@@ -2725,7 +2634,7 @@ class TestBuilderValidation:
|
||||
|
||||
yaml_def = {
|
||||
"name": "test_workflow",
|
||||
"actions": [{"id": "loop", "kind": "Goto", "target": "loop"}],
|
||||
"actions": [{"id": "loop", "kind": "GotoAction", "actionId": "loop"}],
|
||||
}
|
||||
|
||||
builder = DeclarativeWorkflowBuilder(yaml_def)
|
||||
@@ -2757,23 +2666,22 @@ class TestBuilderValidation:
|
||||
workflow = builder.build()
|
||||
assert workflow is not None
|
||||
|
||||
def test_validation_in_switch_branches(self):
|
||||
"""Test validation catches issues in Switch branches."""
|
||||
def test_validation_in_condition_group_branches(self):
|
||||
"""Test validation catches issues in ConditionGroup branches."""
|
||||
from agent_framework_declarative._workflows._declarative_builder import DeclarativeWorkflowBuilder
|
||||
|
||||
yaml_def = {
|
||||
"name": "test_workflow",
|
||||
"actions": [
|
||||
{
|
||||
"kind": "Switch",
|
||||
"value": "=Local.choice",
|
||||
"cases": [
|
||||
"kind": "ConditionGroup",
|
||||
"conditions": [
|
||||
{
|
||||
"match": "a",
|
||||
"condition": '=Local.choice = "a"',
|
||||
"actions": [{"id": "dup", "kind": "SendActivity", "activity": {"text": "A"}}],
|
||||
},
|
||||
{
|
||||
"match": "b",
|
||||
"condition": '=Local.choice = "b"',
|
||||
"actions": [{"id": "dup", "kind": "SendActivity", "activity": {"text": "B"}}],
|
||||
},
|
||||
],
|
||||
@@ -2796,7 +2704,7 @@ class TestBuilderValidation:
|
||||
"actions": [
|
||||
{
|
||||
"kind": "Foreach",
|
||||
"items": "=Local.items",
|
||||
"source": "=Local.items",
|
||||
"actions": [{"kind": "SendActivity"}], # Missing 'activity'
|
||||
}
|
||||
],
|
||||
|
||||
@@ -207,16 +207,16 @@ class TestDeclarativeActionExecutor:
|
||||
# Note: ConditionEvaluatorExecutor tests removed - conditions are now evaluated on edges
|
||||
|
||||
@_requires_powerfx
|
||||
async def test_foreach_init_with_items(self, mock_context, mock_state):
|
||||
"""Test ForeachInitExecutor with items."""
|
||||
async def test_foreach_init_with_source(self, mock_context, mock_state):
|
||||
"""Test ForeachInitExecutor with the 'source' field."""
|
||||
state = DeclarativeWorkflowState(mock_state)
|
||||
state.initialize()
|
||||
state.set("Local.items", ["a", "b", "c"])
|
||||
|
||||
action_def = {
|
||||
"kind": "Foreach",
|
||||
"itemsSource": "=Local.items",
|
||||
"iteratorVariable": "Local.item",
|
||||
"source": "=Local.items",
|
||||
"itemName": "item",
|
||||
}
|
||||
executor = ForeachInitExecutor(action_def)
|
||||
|
||||
@@ -240,8 +240,8 @@ class TestDeclarativeActionExecutor:
|
||||
# Use a literal empty list - no expression evaluation needed
|
||||
action_def = {
|
||||
"kind": "Foreach",
|
||||
"itemsSource": [], # Direct empty list, not an expression
|
||||
"iteratorVariable": "Local.item",
|
||||
"source": [], # Direct empty list, not an expression
|
||||
"itemName": "item",
|
||||
}
|
||||
executor = ForeachInitExecutor(action_def)
|
||||
|
||||
@@ -264,7 +264,6 @@ class TestDeclarativeWorkflowBuilder:
|
||||
"SetValue",
|
||||
"SetVariable",
|
||||
"SendActivity",
|
||||
"EmitEvent",
|
||||
"EndWorkflow",
|
||||
"InvokeAzureAgent",
|
||||
"Question",
|
||||
@@ -335,8 +334,8 @@ class TestDeclarativeWorkflowBuilder:
|
||||
{
|
||||
"kind": "Foreach",
|
||||
"id": "process_items",
|
||||
"itemsSource": "=Local.items",
|
||||
"iteratorVariable": "Local.item",
|
||||
"source": "=Local.items",
|
||||
"itemName": "item",
|
||||
"actions": [
|
||||
{"kind": "SendActivity", "id": "show_item", "activity": {"text": "=Local.item"}},
|
||||
],
|
||||
@@ -353,13 +352,13 @@ class TestDeclarativeWorkflowBuilder:
|
||||
assert "process_items_exit" in builder._executors
|
||||
assert "show_item" in builder._executors
|
||||
|
||||
def test_build_workflow_with_switch(self):
|
||||
"""Test building a workflow with Switch control flow."""
|
||||
def test_build_workflow_with_condition_group(self):
|
||||
"""Test building a workflow with ConditionGroup control flow."""
|
||||
yaml_def = {
|
||||
"name": "switch_workflow",
|
||||
"name": "condition_group_workflow",
|
||||
"actions": [
|
||||
{
|
||||
"kind": "Switch",
|
||||
"kind": "ConditionGroup",
|
||||
"id": "check_status",
|
||||
"conditions": [
|
||||
{
|
||||
@@ -375,7 +374,7 @@ class TestDeclarativeWorkflowBuilder:
|
||||
],
|
||||
},
|
||||
],
|
||||
"else": [
|
||||
"elseActions": [
|
||||
{"kind": "SendActivity", "id": "say_unknown", "activity": {"text": "Unknown"}},
|
||||
],
|
||||
},
|
||||
@@ -385,12 +384,12 @@ class TestDeclarativeWorkflowBuilder:
|
||||
workflow = builder.build()
|
||||
|
||||
assert workflow is not None
|
||||
# Verify switch executors were created
|
||||
# Verify ConditionGroup branch executors were created
|
||||
# Note: No join executors - branches wire directly to successor
|
||||
assert "say_active" in builder._executors
|
||||
assert "say_pending" in builder._executors
|
||||
assert "say_unknown" in builder._executors
|
||||
# Entry node is created when Switch is first action
|
||||
# Entry node is created when ConditionGroup is first action
|
||||
assert "_workflow_entry" in builder._executors
|
||||
|
||||
|
||||
@@ -493,9 +492,9 @@ class TestHumanInputExecutors:
|
||||
|
||||
action_def = {
|
||||
"kind": "Question",
|
||||
"text": "What is your name?",
|
||||
"property": "Local.name",
|
||||
"defaultValue": "Anonymous",
|
||||
"question": {"text": "What is your name?"},
|
||||
"variable": "Local.name",
|
||||
"default": "Anonymous",
|
||||
}
|
||||
executor = QuestionExecutor(action_def)
|
||||
|
||||
@@ -509,36 +508,6 @@ class TestHumanInputExecutors:
|
||||
assert request.request_type == "question"
|
||||
assert "What is your name?" in request.message
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_confirmation_executor(self, mock_context, mock_state):
|
||||
"""Test ConfirmationExecutor."""
|
||||
from agent_framework_declarative._workflows import (
|
||||
ConfirmationExecutor,
|
||||
ExternalInputRequest,
|
||||
)
|
||||
|
||||
state = DeclarativeWorkflowState(mock_state)
|
||||
state.initialize()
|
||||
|
||||
action_def = {
|
||||
"kind": "Confirmation",
|
||||
"text": "Do you want to continue?",
|
||||
"property": "Local.confirmed",
|
||||
"yesLabel": "Yes, continue",
|
||||
"noLabel": "No, stop",
|
||||
}
|
||||
executor = ConfirmationExecutor(action_def)
|
||||
|
||||
# Execute
|
||||
await executor.handle_action(ActionTrigger(), mock_context)
|
||||
|
||||
# Verify request_info was called with ExternalInputRequest
|
||||
mock_context.request_info.assert_called_once()
|
||||
request = mock_context.request_info.call_args[0][0]
|
||||
assert isinstance(request, ExternalInputRequest)
|
||||
assert request.request_type == "confirmation"
|
||||
assert "continue" in request.message.lower()
|
||||
|
||||
|
||||
@_requires_powerfx
|
||||
class TestParseValueExecutor:
|
||||
|
||||
@@ -100,8 +100,8 @@ class TestGraphBasedWorkflowExecution:
|
||||
{
|
||||
"kind": "Foreach",
|
||||
"id": "process_items",
|
||||
"itemsSource": "=Local.items",
|
||||
"iteratorVariable": "Local.item",
|
||||
"source": "=Local.items",
|
||||
"itemName": "item",
|
||||
"actions": [
|
||||
{"kind": "SendActivity", "id": "show_item", "activity": {"text": "=Local.item"}},
|
||||
],
|
||||
@@ -131,8 +131,8 @@ class TestGraphBasedWorkflowExecution:
|
||||
{
|
||||
"kind": "Foreach",
|
||||
"id": "loop",
|
||||
"itemsSource": "=Local.items",
|
||||
"iteratorVariable": "Local.item",
|
||||
"source": "=Local.items",
|
||||
"itemName": "item",
|
||||
"actions": [
|
||||
{"kind": "SendActivity", "id": "step_1", "activity": {"text": '="1-" & Local.item'}},
|
||||
{"kind": "SendActivity", "id": "step_2", "activity": {"text": '="2-" & Local.item'}},
|
||||
@@ -151,14 +151,14 @@ class TestGraphBasedWorkflowExecution:
|
||||
assert outputs == ["1-A", "2-A", "3-A", "1-B", "2-B", "3-B"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_workflow_with_switch(self):
|
||||
"""Test workflow with Switch/ConditionGroup."""
|
||||
async def test_workflow_with_condition_group(self):
|
||||
"""Test workflow with ConditionGroup."""
|
||||
yaml_def = {
|
||||
"name": "switch_workflow",
|
||||
"name": "condition_group_workflow",
|
||||
"actions": [
|
||||
{"kind": "SetValue", "id": "set_level", "path": "Local.level", "value": 2},
|
||||
{
|
||||
"kind": "Switch",
|
||||
"kind": "ConditionGroup",
|
||||
"id": "check_level",
|
||||
"conditions": [
|
||||
{
|
||||
@@ -174,7 +174,7 @@ class TestGraphBasedWorkflowExecution:
|
||||
],
|
||||
},
|
||||
],
|
||||
"else": [
|
||||
"elseActions": [
|
||||
{"kind": "SendActivity", "id": "default", "activity": {"text": "Other level"}},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -122,14 +122,16 @@ actions:
|
||||
- cherry
|
||||
itemName: fruit
|
||||
actions:
|
||||
- kind: AppendValue
|
||||
path: Local.fruits
|
||||
value: processed
|
||||
- kind: SendActivity
|
||||
activity:
|
||||
text: processed
|
||||
""")
|
||||
|
||||
_result = await workflow.run({}) # noqa: F841
|
||||
# The foreach should have processed 3 items
|
||||
# We can check this by examining the workflow outputs
|
||||
result = await workflow.run({})
|
||||
outputs = result.get_outputs()
|
||||
# The foreach should have processed 3 items, emitting "processed" each time.
|
||||
processed_outputs = [o for o in outputs if "processed" in str(o)]
|
||||
assert len(processed_outputs) == 3
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_if_workflow(self):
|
||||
@@ -556,28 +558,27 @@ actions:
|
||||
|
||||
|
||||
@_requires_powerfx
|
||||
class TestWorkflowFactorySwitch:
|
||||
"""Tests for Switch/Case action."""
|
||||
class TestWorkflowFactoryConditionGroup:
|
||||
"""Tests for ConditionGroup action."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_switch_with_matching_case(self):
|
||||
"""Test Switch with a matching case."""
|
||||
async def test_condition_group_with_matching_condition(self):
|
||||
"""Test ConditionGroup with a matching condition."""
|
||||
factory = WorkflowFactory()
|
||||
workflow = factory.create_workflow_from_yaml("""
|
||||
name: switch-test
|
||||
name: condition-group-test
|
||||
actions:
|
||||
- kind: SetValue
|
||||
path: Local.color
|
||||
value: red
|
||||
- kind: Switch
|
||||
value: =Local.color
|
||||
cases:
|
||||
- match: red
|
||||
- kind: ConditionGroup
|
||||
conditions:
|
||||
- condition: =Local.color = "red"
|
||||
actions:
|
||||
- kind: SendActivity
|
||||
activity:
|
||||
text: Color is red
|
||||
- match: blue
|
||||
- condition: =Local.color = "blue"
|
||||
actions:
|
||||
- kind: SendActivity
|
||||
activity:
|
||||
@@ -590,29 +591,28 @@ actions:
|
||||
assert any("Color is red" in str(o) for o in outputs)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_switch_with_default(self):
|
||||
"""Test Switch falling through to default."""
|
||||
async def test_condition_group_with_else_actions(self):
|
||||
"""Test ConditionGroup falling through to elseActions when no condition matches."""
|
||||
factory = WorkflowFactory()
|
||||
workflow = factory.create_workflow_from_yaml("""
|
||||
name: switch-default-test
|
||||
name: condition-group-else-test
|
||||
actions:
|
||||
- kind: SetValue
|
||||
path: Local.color
|
||||
value: green
|
||||
- kind: Switch
|
||||
value: =Local.color
|
||||
cases:
|
||||
- match: red
|
||||
- kind: ConditionGroup
|
||||
conditions:
|
||||
- condition: =Local.color = "red"
|
||||
actions:
|
||||
- kind: SendActivity
|
||||
activity:
|
||||
text: Red
|
||||
- match: blue
|
||||
- condition: =Local.color = "blue"
|
||||
actions:
|
||||
- kind: SendActivity
|
||||
activity:
|
||||
text: Blue
|
||||
default:
|
||||
elseActions:
|
||||
- kind: SendActivity
|
||||
activity:
|
||||
text: Unknown color
|
||||
@@ -653,54 +653,273 @@ actions:
|
||||
|
||||
assert any("Done" in str(o) for o in outputs)
|
||||
|
||||
|
||||
class TestRenamedAliasKindsAreUnknown:
|
||||
"""Tests that the previously-accepted ``Switch``/``Goto`` kind names are now unknown.
|
||||
|
||||
YAML that still names one of these kinds falls through the existing
|
||||
unknown-kind warning path (the action is silently skipped) instead
|
||||
of being routed to ``ConditionGroup``/``GotoAction``.
|
||||
"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_append_value(self):
|
||||
"""Test AppendValue action."""
|
||||
async def test_switch_kind_is_unknown(self, caplog):
|
||||
"""A workflow whose YAML uses kind: Switch logs an unknown-kind warning."""
|
||||
factory = WorkflowFactory()
|
||||
workflow = factory.create_workflow_from_yaml("""
|
||||
name: append-test
|
||||
with caplog.at_level(
|
||||
"WARNING",
|
||||
logger="agent_framework_declarative._workflows._declarative_builder",
|
||||
):
|
||||
workflow = factory.create_workflow_from_yaml("""
|
||||
name: switch-alias-removed
|
||||
actions:
|
||||
- kind: SetValue
|
||||
path: Local.list
|
||||
value: []
|
||||
- kind: AppendValue
|
||||
path: Local.list
|
||||
value: first
|
||||
- kind: AppendValue
|
||||
path: Local.list
|
||||
value: second
|
||||
- kind: Switch
|
||||
value: =Local.color
|
||||
cases:
|
||||
- match: red
|
||||
actions:
|
||||
- kind: SendActivity
|
||||
activity:
|
||||
text: Color is red
|
||||
- kind: SendActivity
|
||||
activity:
|
||||
text: Done
|
||||
""")
|
||||
result = await workflow.run({})
|
||||
|
||||
result = await workflow.run({})
|
||||
# Switch is no longer a recognised kind -> warning emitted + action skipped.
|
||||
assert any("Unknown action kind 'Switch'" in record.getMessage() for record in caplog.records)
|
||||
# The trailing SendActivity still runs so the workflow completes successfully.
|
||||
outputs = result.get_outputs()
|
||||
|
||||
assert any("Done" in str(o) for o in outputs)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_emit_event(self):
|
||||
"""Test EmitEvent action."""
|
||||
async def test_goto_kind_is_unknown(self, caplog):
|
||||
"""A workflow whose YAML uses kind: Goto logs an unknown-kind warning."""
|
||||
factory = WorkflowFactory()
|
||||
with caplog.at_level(
|
||||
"WARNING",
|
||||
logger="agent_framework_declarative._workflows._declarative_builder",
|
||||
):
|
||||
workflow = factory.create_workflow_from_yaml("""
|
||||
name: goto-alias-removed
|
||||
actions:
|
||||
- id: target
|
||||
kind: SendActivity
|
||||
activity:
|
||||
text: Arrived
|
||||
- kind: Goto
|
||||
target: target
|
||||
""")
|
||||
result = await workflow.run({})
|
||||
|
||||
# Goto is no longer a recognised kind -> warning emitted + action skipped.
|
||||
assert any("Unknown action kind 'Goto'" in record.getMessage() for record in caplog.records)
|
||||
# The first SendActivity still emits its output.
|
||||
outputs = result.get_outputs()
|
||||
assert any("Arrived" in str(o) for o in outputs)
|
||||
|
||||
|
||||
class TestDroppedShapesAreRejected:
|
||||
"""Tests that previously-accepted alternate YAML shapes are now rejected at validation.
|
||||
|
||||
``ConditionGroup`` no longer accepts the ``value``/``cases`` shape and
|
||||
``Foreach`` no longer accepts the ``items`` field. Both kinds raise a
|
||||
``ValueError`` from the builder when the required field is missing.
|
||||
"""
|
||||
|
||||
def test_condition_group_with_cases_raises(self):
|
||||
"""ConditionGroup using value/cases (no conditions) must fail validation."""
|
||||
from agent_framework_declarative._workflows._declarative_builder import DeclarativeWorkflowBuilder
|
||||
|
||||
yaml_def = {
|
||||
"name": "cg-cases-rejected",
|
||||
"actions": [
|
||||
{
|
||||
"kind": "ConditionGroup",
|
||||
"value": "=Local.color",
|
||||
"cases": [
|
||||
{"match": "red", "actions": [{"kind": "SendActivity", "activity": {"text": "Red"}}]},
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
builder = DeclarativeWorkflowBuilder(yaml_def)
|
||||
with pytest.raises(ValueError, match="conditions"):
|
||||
builder.build()
|
||||
|
||||
def test_foreach_with_items_raises(self):
|
||||
"""Foreach using items (no source) must fail validation."""
|
||||
from agent_framework_declarative._workflows._declarative_builder import DeclarativeWorkflowBuilder
|
||||
|
||||
yaml_def = {
|
||||
"name": "fe-items-rejected",
|
||||
"actions": [
|
||||
{
|
||||
"kind": "Foreach",
|
||||
"items": "=Local.list",
|
||||
"actions": [{"kind": "SendActivity", "activity": {"text": "hi"}}],
|
||||
}
|
||||
],
|
||||
}
|
||||
builder = DeclarativeWorkflowBuilder(yaml_def)
|
||||
with pytest.raises(ValueError, match="source"):
|
||||
builder.build()
|
||||
|
||||
def test_condition_group_with_else_field_raises(self):
|
||||
"""ConditionGroup with an ``else`` field must fail fast and point at ``elseActions``."""
|
||||
from agent_framework_declarative._workflows._declarative_builder import DeclarativeWorkflowBuilder
|
||||
|
||||
yaml_def = {
|
||||
"name": "cg-else-rejected",
|
||||
"actions": [
|
||||
{
|
||||
"kind": "ConditionGroup",
|
||||
"conditions": [
|
||||
{
|
||||
"condition": "=Local.x = 1",
|
||||
"actions": [{"kind": "SendActivity", "activity": {"text": "one"}}],
|
||||
},
|
||||
],
|
||||
"else": [{"kind": "SendActivity", "activity": {"text": "other"}}],
|
||||
}
|
||||
],
|
||||
}
|
||||
builder = DeclarativeWorkflowBuilder(yaml_def)
|
||||
with pytest.raises(ValueError, match="elseActions"):
|
||||
builder.build()
|
||||
|
||||
def test_condition_group_with_default_field_raises(self):
|
||||
"""ConditionGroup with a ``default`` field must fail fast and point at ``elseActions``."""
|
||||
from agent_framework_declarative._workflows._declarative_builder import DeclarativeWorkflowBuilder
|
||||
|
||||
yaml_def = {
|
||||
"name": "cg-default-rejected",
|
||||
"actions": [
|
||||
{
|
||||
"kind": "ConditionGroup",
|
||||
"conditions": [
|
||||
{
|
||||
"condition": "=Local.x = 1",
|
||||
"actions": [{"kind": "SendActivity", "activity": {"text": "one"}}],
|
||||
},
|
||||
],
|
||||
"default": [{"kind": "SendActivity", "activity": {"text": "other"}}],
|
||||
}
|
||||
],
|
||||
}
|
||||
builder = DeclarativeWorkflowBuilder(yaml_def)
|
||||
with pytest.raises(ValueError, match="elseActions"):
|
||||
builder.build()
|
||||
|
||||
|
||||
class TestQuestionAndRequestExternalInputShapes:
|
||||
"""Tests for accepted YAML shapes of ``Question`` and ``RequestExternalInput``.
|
||||
|
||||
Both kinds accept either a nested ``{question|prompt: {text: ...}}`` form
|
||||
or a top-level alternate (``text``/``message``) for the prompt content,
|
||||
and either ``variable`` or top-level ``property`` for the destination path.
|
||||
Missing both spellings of a required field raises during validation.
|
||||
"""
|
||||
|
||||
def test_question_nested_question_text_builds(self):
|
||||
"""A workflow whose Question uses nested ``question.text`` builds without error."""
|
||||
factory = WorkflowFactory()
|
||||
workflow = factory.create_workflow_from_yaml("""
|
||||
name: emit-event-test
|
||||
name: question-nested
|
||||
actions:
|
||||
- kind: EmitEvent
|
||||
event:
|
||||
name: test_event
|
||||
data:
|
||||
message: Hello
|
||||
- kind: SendActivity
|
||||
activity:
|
||||
text: Event emitted
|
||||
- kind: Question
|
||||
question:
|
||||
text: "What is your name?"
|
||||
variable: Local.userName
|
||||
default: "Guest"
|
||||
""")
|
||||
assert workflow is not None
|
||||
|
||||
def test_request_external_input_nested_prompt_text_builds(self):
|
||||
"""A workflow whose RequestExternalInput uses nested ``prompt.text`` builds without error."""
|
||||
factory = WorkflowFactory()
|
||||
workflow = factory.create_workflow_from_yaml("""
|
||||
name: rei-nested
|
||||
actions:
|
||||
- kind: RequestExternalInput
|
||||
prompt:
|
||||
text: "Please approve"
|
||||
variable: Local.approved
|
||||
default: pending
|
||||
""")
|
||||
assert workflow is not None
|
||||
|
||||
def test_question_missing_question_raises(self):
|
||||
"""A Question action missing both `question` and the `text` alternate must fail validation."""
|
||||
factory = WorkflowFactory()
|
||||
with pytest.raises((ValueError, DeclarativeWorkflowError), match="question"):
|
||||
factory.create_workflow_from_yaml("""
|
||||
name: question-missing-question
|
||||
actions:
|
||||
- kind: Question
|
||||
variable: Local.x
|
||||
""")
|
||||
|
||||
result = await workflow.run({})
|
||||
outputs = result.get_outputs()
|
||||
def test_question_missing_variable_raises(self):
|
||||
"""A Question action missing both `variable` and the `property` alternate must fail validation."""
|
||||
factory = WorkflowFactory()
|
||||
with pytest.raises((ValueError, DeclarativeWorkflowError), match="variable"):
|
||||
factory.create_workflow_from_yaml("""
|
||||
name: question-missing-variable
|
||||
actions:
|
||||
- kind: Question
|
||||
question:
|
||||
text: "Hi"
|
||||
""")
|
||||
|
||||
# Workflow should complete
|
||||
assert any("Event emitted" in str(o) for o in outputs)
|
||||
def test_request_external_input_missing_prompt_raises(self):
|
||||
"""RequestExternalInput missing both `prompt` and the `message` alternate must fail validation."""
|
||||
factory = WorkflowFactory()
|
||||
with pytest.raises((ValueError, DeclarativeWorkflowError), match="prompt"):
|
||||
factory.create_workflow_from_yaml("""
|
||||
name: rei-missing-prompt
|
||||
actions:
|
||||
- kind: RequestExternalInput
|
||||
variable: Local.x
|
||||
""")
|
||||
|
||||
def test_request_external_input_missing_variable_raises(self):
|
||||
"""RequestExternalInput missing both `variable` and the `property` alternate must fail validation."""
|
||||
factory = WorkflowFactory()
|
||||
with pytest.raises((ValueError, DeclarativeWorkflowError), match="variable"):
|
||||
factory.create_workflow_from_yaml("""
|
||||
name: rei-missing-variable
|
||||
actions:
|
||||
- kind: RequestExternalInput
|
||||
prompt:
|
||||
text: "Hi"
|
||||
""")
|
||||
|
||||
def test_question_top_level_field_names_accepted(self):
|
||||
"""Top-level ``text`` + ``property`` + ``defaultValue`` are accepted on Question."""
|
||||
factory = WorkflowFactory()
|
||||
workflow = factory.create_workflow_from_yaml("""
|
||||
name: question-legacy
|
||||
actions:
|
||||
- kind: Question
|
||||
text: "What is your name?"
|
||||
property: Local.userName
|
||||
defaultValue: "Guest"
|
||||
""")
|
||||
assert workflow is not None
|
||||
|
||||
def test_request_external_input_top_level_field_names_accepted(self):
|
||||
"""Top-level ``message`` + ``property`` are accepted on RequestExternalInput."""
|
||||
factory = WorkflowFactory()
|
||||
workflow = factory.create_workflow_from_yaml("""
|
||||
name: rei-legacy
|
||||
actions:
|
||||
- kind: RequestExternalInput
|
||||
message: "Please approve"
|
||||
property: Local.approved
|
||||
""")
|
||||
assert workflow is not None
|
||||
|
||||
|
||||
class TestWorkflowFactoryYamlErrors:
|
||||
|
||||
@@ -227,7 +227,6 @@ class TestHandlerCoverage:
|
||||
"OnConversationStart", # Trigger kind, not an action
|
||||
"ConditionGroup", # Decomposed into evaluator/join nodes
|
||||
"GotoAction", # Resolved as graph edges, not executor nodes
|
||||
"Goto", # Alias for GotoAction
|
||||
}
|
||||
|
||||
missing_executors = all_action_kinds - registered_executors - structural_kinds
|
||||
|
||||
Reference in New Issue
Block a user