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'
|
||||
}
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user