Files
replypier/backend/tests/services/test_workflow_execution.py
T
WangShen007 20fef2b9f3 chore: settle product naming across app, widget and deployments
One word everywhere: ReplyPier in UI copy, package and image names, the
replypier.site domains (app/api/docs/help), REPLYPIER_* settings and the
--rp-* design tokens.

- public widget API is window.ReplyPier with replypierId / replypierBaseUrl /
  replypierConfig / replypierToken, loader at webclient/replypier.min.js;
  both bundles rebuilt from source
- NOTICE, llms.txt, CONTRIBUTING, HELP_CENTER_INFRA and the demo runtime
  config rewritten; the demo config ships no third-party keys
- migration adds the ReplyPier managed model value to the AI model enum
- local compose project, database, role, volume and Docker network carry the
  same name
2026-10-26 00:57:22 +08:00

797 lines
31 KiB
Python

"""
Copyright 2024-2026 ReplyPier
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import pytest
from unittest.mock import Mock, patch, AsyncMock, MagicMock
from uuid import uuid4
from app.services.workflow_execution import WorkflowExecutionService
from app.models.workflow_node import WorkflowNode, NodeType, ExitCondition
from app.agents.chat_agent import ChatResponse
from app.models.workflow import Workflow, WorkflowStatus
@pytest.fixture
def mock_db():
return Mock()
@pytest.fixture
def workflow_service(mock_db):
return WorkflowExecutionService(mock_db)
@pytest.fixture
def sample_workflow():
workflow = MagicMock(spec=Workflow)
workflow.id = uuid4()
workflow.name = "Test Workflow"
workflow.nodes = [] # List to store nodes
workflow.connections = [] # List to store connections
workflow.status = WorkflowStatus.PUBLISHED
return workflow
@pytest.fixture
def sample_workflow_node():
node = Mock(spec=WorkflowNode)
node.id = uuid4()
node.node_type = NodeType.LLM
config_dict = {
"system_prompt": "You are a helpful assistant.",
"exit_condition": ExitCondition.CONTINUOUS_EXECUTION,
"model_config": {
"temperature": 0.7,
"max_tokens": 150
}
}
node.config = MagicMock()
node.config.__getitem__.side_effect = config_dict.__getitem__
node.config.get.side_effect = config_dict.get
return node
@pytest.fixture
def sample_chat_history():
"""Create sample chat history for testing"""
from datetime import datetime
history = [
Mock(),
Mock(),
Mock()
]
history[0].message = "Hello! How can I help you today?"
history[0].message_type = "assistant"
history[0].created_at = datetime.now()
history[0].attributes = {}
history[1].message = "I need help with my order"
history[1].message_type = "user"
history[1].created_at = datetime.now()
history[1].attributes = {}
history[2].message = "I'll help you with your order. What's your order number?"
history[2].message_type = "assistant"
history[2].created_at = datetime.now()
history[2].attributes = {}
return history
class TestWorkflowExecutionService:
@pytest.mark.asyncio
async def test_build_context_message_with_full_history(
self, workflow_service, sample_chat_history
):
"""Test building context message with full chat history"""
# Mock repositories
mock_chat_repo = Mock()
mock_chat_repo.get_session_history = AsyncMock(return_value=sample_chat_history)
with patch('app.services.workflow_execution.ChatRepository', return_value=mock_chat_repo), \
patch.object(workflow_service.session_repo, 'get_workflow_history', return_value=[]):
# Build context message
context_message = await workflow_service._build_context_message("test-session", {})
# Verify context message contains expected structure and chat history
assert "CONTEXT on previous workflow messages" in context_message
assert "Hello! How can I help you today?" in context_message
assert "I need help with my order" in context_message
assert "I'll help you with your order. What's your order number?" in context_message
assert "CONVERSATION HISTORY:" in context_message
# Instruction header changed in implementation; detailed analysis instructions are implicit now
@pytest.mark.asyncio
async def test_build_context_message_empty_history(self, workflow_service):
"""Test building context message with empty chat history"""
# Mock repositories with empty history
mock_chat_repo = Mock()
mock_chat_repo.get_session_history = AsyncMock(return_value=[])
with patch('app.services.workflow_execution.ChatRepository', return_value=mock_chat_repo), \
patch.object(workflow_service.session_repo, 'get_workflow_history', return_value=[]):
# Build context message
context_message = await workflow_service._build_context_message("test-session", {})
# Verify context message structure for empty history
assert "CONTEXT on previous workflow messages" in context_message
assert "No previous messages" in context_message
assert "No previous workflow interactions" in context_message
# Instruction header changed in implementation; detailed analysis instructions are implicit now
@pytest.mark.asyncio
async def test_build_context_message_error_handling(self, workflow_service):
"""Test building context message with error in repository"""
# Mock repositories to raise exception
mock_chat_repo = Mock()
mock_chat_repo.get_session_history = AsyncMock(side_effect=Exception("Test error"))
with patch('app.services.workflow_execution.ChatRepository', return_value=mock_chat_repo), \
patch.object(workflow_service.session_repo, 'get_workflow_history', return_value=[]):
# Build context message
context_message = await workflow_service._build_context_message("test-session", {})
# Verify fallback context message on error
assert "Please analyze the current conversation context" in context_message
assert "provide an appropriate response" in context_message
@pytest.mark.asyncio
async def test_execute_llm_node_with_empty_message(
self, workflow_service, sample_workflow_node, sample_workflow, sample_chat_history
):
"""Test LLM node execution with empty user message"""
# Mock chat agent and its response
mock_chat_agent = AsyncMock()
mock_response = Mock()
mock_response.message = "Based on our conversation, I can see you're asking about order ORDER123. Let me check that for you."
mock_response.transfer_to_human = False
mock_response.end_chat = False
mock_response.transfer_reason = None
mock_response.transfer_description = None
mock_response.success = True # Add success attribute
# Set up async method return value
mock_chat_agent._get_llm_response_only = AsyncMock(return_value=mock_response)
# Mock repositories
mock_chat_repo = Mock()
mock_chat_repo.get_session_history = AsyncMock(return_value=sample_chat_history)
with patch('app.services.workflow_execution.ChatAgent') as mock_chat_agent_class:
mock_chat_agent_class.create_async = AsyncMock(return_value=mock_chat_agent)
with patch('app.services.workflow_execution.ChatRepository', return_value=mock_chat_repo), \
patch.object(workflow_service.session_repo, 'get_workflow_history', return_value=[]):
# Test with empty user message
result = await workflow_service._execute_llm_node(
node=sample_workflow_node,
workflow=sample_workflow,
workflow_state={},
user_message="", # Empty message
api_key="test-key",
model_name="gpt-4",
model_type="openai",
base_url=None,
org_id="test-org",
agent_id="test-agent",
customer_id="test-customer",
session_id="test-session"
)
# Verify that context message was built and used
assert result.success is True
assert result.message == mock_response.message
assert result.next_node_id is None
assert result.should_continue is False
@pytest.mark.asyncio
async def test_execute_llm_node_with_normal_message(
self, workflow_service, sample_workflow_node, sample_workflow
):
"""Test LLM node execution with normal user message (should not use context building)"""
# Mock chat agent and its response
mock_chat_agent = AsyncMock()
mock_response = Mock()
mock_response.message = "I understand you want to check your order status."
mock_response.transfer_to_human = False
mock_response.end_chat = False
mock_response.transfer_reason = None
mock_response.transfer_description = None
mock_response.success = True # Add success attribute
# Set up async method return value
mock_chat_agent._get_llm_response_only = AsyncMock(return_value=mock_response)
with patch('app.services.workflow_execution.ChatAgent') as mock_chat_agent_class:
mock_chat_agent_class.create_async = AsyncMock(return_value=mock_chat_agent)
with patch.object(workflow_service.session_repo, 'get_workflow_history', return_value=[]):
# Test with normal user message
user_message = "I want to check my order status"
result = await workflow_service._execute_llm_node(
node=sample_workflow_node,
workflow=sample_workflow,
workflow_state={},
user_message=user_message,
api_key="test-key",
model_name="gpt-4",
model_type="openai",
base_url=None,
org_id="test-org",
agent_id="test-agent",
customer_id="test-customer",
session_id="test-session"
)
# Verify that original message was used
assert result.success is True
assert result.message == mock_response.message
assert result.next_node_id is None
assert result.should_continue is False
def test_find_start_node(self, workflow_service, sample_workflow):
"""Test finding the start node in a workflow"""
# Create start node with no incoming connections
start_node = Mock(spec=WorkflowNode)
start_node.id = uuid4()
start_node.node_type = NodeType.MESSAGE
start_node.incoming_connections = []
start_node.config = {}
# Create other nodes with incoming connections
other_node = Mock(spec=WorkflowNode)
other_node.id = uuid4()
other_node.node_type = NodeType.LLM
other_node.incoming_connections = [Mock()] # Has incoming connection
sample_workflow.nodes = [other_node, start_node]
# Find start node (should return the one with no incoming connections)
result = workflow_service._find_start_node(sample_workflow)
assert result.id == start_node.id
def test_find_node_by_id(self, workflow_service, sample_workflow):
"""Test finding a node by ID"""
node_id = uuid4()
target_node = Mock(spec=WorkflowNode)
target_node.id = node_id
other_node = Mock(spec=WorkflowNode)
other_node.id = uuid4()
sample_workflow.nodes = [other_node, target_node]
# Find existing node
result = workflow_service._find_node_by_id(sample_workflow, node_id)
assert result.id == node_id
# Try to find non-existent node
result = workflow_service._find_node_by_id(sample_workflow, uuid4())
assert result is None
def test_execute_message_node(self, workflow_service):
"""Test executing a MESSAGE node"""
next_node_id = uuid4()
connection = Mock()
connection.target_node_id = next_node_id
node = Mock(spec=WorkflowNode)
node.id = uuid4()
node.node_type = NodeType.MESSAGE
node.config = {"message_text": "Hello {{user_name}}!"}
node.outgoing_connections = [connection]
node_id_str = str(node.id)
workflow_state = {
"variables": {
node_id_str: {"user_name": "John"}
}
}
result = workflow_service._execute_message_node(node, workflow_state)
assert result.success is True
assert "Hello John!" in result.message
assert result.should_continue is True
assert result.next_node_id == next_node_id
def test_execute_condition_node_true(self, workflow_service, sample_workflow):
"""Test condition node with true condition"""
next_node_id = uuid4()
connection = Mock()
connection.target_node_id = next_node_id
connection.label = "true"
connection.condition = "true"
node = Mock(spec=WorkflowNode)
node.id = uuid4()
node.node_type = NodeType.CONDITION
node.config = {"condition_expression": "{{order_total}} > 100"}
node.outgoing_connections = [connection]
node_id_str = str(node.id)
workflow_state = {
"variables": {
node_id_str: {"order_total": "150"}
}
}
result = workflow_service._execute_condition_node(node, sample_workflow, workflow_state)
assert result.success is True
assert result.should_continue is True
def test_execute_condition_node_false(self, workflow_service, sample_workflow):
"""Test condition node with false condition"""
next_node_id = uuid4()
connection = Mock()
connection.target_node_id = next_node_id
connection.label = "false"
connection.condition = "false"
node = Mock(spec=WorkflowNode)
node.id = uuid4()
node.node_type = NodeType.CONDITION
node.config = {"condition_expression": "{{order_total}} > 200"}
node.outgoing_connections = [connection]
node_id_str = str(node.id)
workflow_state = {
"variables": {
node_id_str: {"order_total": "150"}
}
}
result = workflow_service._execute_condition_node(node, sample_workflow, workflow_state)
assert result.success is True
assert result.should_continue is True
def test_execute_end_node(self, workflow_service):
"""Test executing an END node"""
node = Mock(spec=WorkflowNode)
node.id = uuid4()
node.node_type = NodeType.END
node.config = {"message_text": "Thank you for using our service!"}
result = workflow_service._execute_end_node(node, {})
assert result.success is True
assert "Thank you" in result.message
assert result.end_chat is True
assert result.should_continue is False
def test_execute_human_transfer_node(self, workflow_service):
"""Test executing a HUMAN_TRANSFER node"""
node = Mock(spec=WorkflowNode)
node.id = uuid4()
node.node_type = NodeType.HUMAN_TRANSFER
node.config = {
"transfer_rules": {
"message": "Transferring you to a human agent. Please wait..."
},
"transfer_group_id": "support_team"
}
result = workflow_service._execute_human_transfer_node(node, {})
assert result.success is True
assert result.transfer_to_human is True
assert result.transfer_group_id == "support_team"
assert result.should_continue is False
def test_execute_wait_node(self, workflow_service):
"""Test executing a WAIT node"""
next_node_id = uuid4()
connection = Mock()
connection.target_node_id = next_node_id
node = Mock(spec=WorkflowNode)
node.id = uuid4()
node.node_type = NodeType.WAIT
node.config = {"wait_duration": 1} # unit defaults to seconds
node.outgoing_connections = [connection]
result = workflow_service._execute_wait_node(node, {})
assert result.success is True
assert result.should_continue is True
assert result.next_node_id == next_node_id
def test_process_variables(self, workflow_service):
"""Test variable replacement in text"""
node_id = str(uuid4())
text = "Hello {{user_name}}, your order {{order_id}} is ready!"
workflow_state = {
"variables": {
node_id: {
"user_name": "Alice",
"order_id": "ORDER123"
}
}
}
result = workflow_service._process_variables(text, workflow_state)
assert result == "Hello Alice, your order ORDER123 is ready!"
def test_process_variables_missing_var(self, workflow_service):
"""Test variable replacement with missing variable"""
text = "Hello {{user_name}}"
workflow_state = {"variables": {}}
result = workflow_service._process_variables(text, workflow_state)
# Should leave placeholder as is or handle gracefully
assert "{{user_name}}" in result or "user_name" in result
def test_evaluate_condition_greater_than(self, workflow_service):
"""Test evaluating > condition"""
condition = "100 > 50"
result = workflow_service._evaluate_condition(condition, {})
assert result is True
condition = "50 > 100"
result = workflow_service._evaluate_condition(condition, {})
assert result is False
def test_evaluate_condition_equals(self, workflow_service):
"""Test evaluating === condition"""
condition = "active === active"
result = workflow_service._evaluate_condition(condition, {})
assert result is True
condition = "active === inactive"
result = workflow_service._evaluate_condition(condition, {})
assert result is False
def test_evaluate_condition_contains(self, workflow_service):
"""Test evaluating contains condition"""
condition = "hello contains ell"
result = workflow_service._evaluate_condition(condition, {})
assert result is True
condition = "hello contains xyz"
result = workflow_service._evaluate_condition(condition, {})
assert result is False
def test_find_next_node(self, workflow_service):
"""Test finding the next node from connections"""
from app.models.workflow_connection import WorkflowConnection
current_node_id = uuid4()
next_node_id = uuid4()
node = Mock(spec=WorkflowNode)
node.id = current_node_id
node.workflow_id = uuid4()
# Create a connection
connection = Mock(spec=WorkflowConnection)
connection.source_node_id = current_node_id
connection.target_node_id = next_node_id
connection.condition_type = None
node.outgoing_connections = [connection]
result = workflow_service._find_next_node(node)
assert result == next_node_id
def test_find_next_node_no_connections(self, workflow_service):
"""Test finding next node when no connections exist"""
node = Mock(spec=WorkflowNode)
node.id = uuid4()
node.outgoing_connections = []
result = workflow_service._find_next_node(node)
assert result is None
def test_interpolate_variables(self, workflow_service):
"""Test interpolating variables in text"""
node_id = str(uuid4())
text = "Order {{order_id}} for {{customer_name}}"
workflow_state = {
"variables": {
node_id: {
"order_id": "12345",
"customer_name": "Bob"
}
}
}
result = workflow_service._interpolate_variables(text, workflow_state)
assert result == "Order 12345 for Bob"
def test_store_form_variables(self, workflow_service):
"""Test storing form data in workflow state"""
workflow_state = {"variables": {}}
node_id = uuid4()
form_data = {
"name": "John Doe",
"email": "john@example.com",
"phone": "123-456-7890"
}
workflow_service._store_form_variables(workflow_state, node_id, form_data)
# Variables are stored under the node_id
node_id_str = str(node_id)
assert node_id_str in workflow_state["variables"]
assert workflow_state["variables"][node_id_str]["name"] == "John Doe"
assert workflow_state["variables"][node_id_str]["email"] == "john@example.com"
assert workflow_state["variables"][node_id_str]["phone"] == "123-456-7890"
@pytest.mark.asyncio
async def test_execute_workflow_not_found(self, workflow_service):
"""Test executing non-existent workflow"""
with patch.object(workflow_service.workflow_repo, 'get_workflow_with_nodes_and_connections', return_value=None):
result = await workflow_service.execute_workflow(
session_id="test-session",
user_message="Hello",
workflow_id=uuid4()
)
assert result.success is False
# The error occurs when trying to access workflow.id on None
assert "error" in result.message.lower()
@pytest.mark.asyncio
async def test_execute_workflow_not_published(self, workflow_service, sample_workflow):
"""Test executing unpublished workflow"""
sample_workflow.status = WorkflowStatus.DRAFT
with patch.object(workflow_service.workflow_repo, 'get_workflow_with_nodes_and_connections', return_value=sample_workflow):
result = await workflow_service.execute_workflow(
session_id="test-session",
user_message="Hello",
workflow_id=sample_workflow.id
)
assert result.success is False
assert "not published" in result.message.lower()
@pytest.mark.asyncio
async def test_execute_workflow_no_start_node(self, workflow_service, sample_workflow):
"""Test executing workflow with no start node"""
sample_workflow.nodes = []
with patch.object(workflow_service.workflow_repo, 'get_workflow_with_nodes_and_connections', return_value=sample_workflow):
result = await workflow_service.execute_workflow(
session_id="test-session",
user_message="Hello",
workflow_id=sample_workflow.id
)
assert result.success is False
# The error occurs when trying to access current_node.id on None
assert "error" in result.message.lower()
def test_execute_form_node(self, workflow_service):
"""Test executing a FORM node"""
node = Mock(spec=WorkflowNode)
node.id = uuid4()
node.name = "Contact Form"
node.node_type = NodeType.FORM
node.config = {
"form_fields": [
{"name": "name", "type": "text", "label": "Name", "required": True},
{"name": "email", "type": "email", "label": "Email", "required": True}
],
"form_title": "Contact Us",
"form_description": "Please fill out the form",
"submit_button_text": "Submit"
}
result = workflow_service._execute_form_node(node, {}, None, "test-session")
assert result.success is True
assert result.form_data is not None
assert "fields" in result.form_data
assert result.should_continue is False
def test_execute_landing_page_node(self, workflow_service):
"""Test executing a LANDING_PAGE node"""
node = Mock(spec=WorkflowNode)
node.id = uuid4()
node.node_type = NodeType.LANDING_PAGE
node.config = {
"landing_page_heading": "Welcome",
"landing_page_content": "Welcome to our service"
}
result = workflow_service._execute_landing_page_node(node, {}, None)
assert result.success is True
assert result.landing_page_data is not None
assert "heading" in result.landing_page_data
assert result.should_continue is False
def test_execute_action_node(self, workflow_service):
"""Test executing an ACTION node"""
next_node_id = uuid4()
connection = Mock()
connection.target_node_id = next_node_id
node = Mock(spec=WorkflowNode)
node.id = uuid4()
node.node_type = NodeType.ACTION
node.config = {
"action_type": "set_variable",
"action_config": {
"variable_name": "status",
"variable_value": "active"
}
}
node.outgoing_connections = [connection]
workflow_state = {"variables": {}}
result = workflow_service._execute_action_node(node, workflow_state)
assert result.success is True
assert result.should_continue is True
assert result.next_node_id == next_node_id
def _linked_nodes(*nodes):
"""Chain mock nodes in order with mock connections; the first is the start."""
for node in nodes:
node.incoming_connections = []
node.outgoing_connections = []
for source, target in zip(nodes, nodes[1:]):
connection = Mock()
connection.target_node_id = target.id
source.outgoing_connections.append(connection)
target.incoming_connections.append(connection)
return list(nodes)
def _message_node(text):
node = Mock(spec=WorkflowNode)
node.id = uuid4()
node.node_type = NodeType.MESSAGE
node.config = {"message_text": text}
return node
async def _run_workflow(workflow_service, workflow, nodes, user_message="hi"):
workflow.nodes = nodes
with patch.object(workflow_service.workflow_repo, 'get_workflow_with_nodes_and_connections',
return_value=workflow), \
patch.object(workflow_service, '_update_session_workflow_state'):
return await workflow_service.execute_workflow(
session_id="test-session",
user_message=user_message,
workflow_id=workflow.id,
)
class TestTerminalMessageNotDuplicated:
"""The node execution stops on is returned as the final message, so it must
not also be reported as an intermediate one: every caller emits and stores
both, which sent the visitor the same text twice."""
@pytest.mark.asyncio
async def test_single_message_node_is_only_the_final_message(self, workflow_service, sample_workflow):
result = await _run_workflow(workflow_service, sample_workflow, _linked_nodes(_message_node("Hello")))
assert result.success is True
assert result.message == "Hello"
assert result.intermediate_messages == []
@pytest.mark.asyncio
async def test_message_chain_keeps_earlier_messages_as_intermediate(self, workflow_service, sample_workflow):
nodes = _linked_nodes(_message_node("First"), _message_node("Second"))
result = await _run_workflow(workflow_service, sample_workflow, nodes)
assert result.message == "Second"
assert result.intermediate_messages == ["First"]
@pytest.mark.asyncio
async def test_message_before_user_input_stays_intermediate(self, workflow_service, sample_workflow):
user_input = Mock(spec=WorkflowNode)
user_input.id = uuid4()
user_input.name = "Ask"
user_input.node_type = NodeType.USER_INPUT
user_input.config = {"prompt_message": "What is your name?"}
nodes = _linked_nodes(_message_node("Welcome"), user_input)
result = await _run_workflow(workflow_service, sample_workflow, nodes, user_message=None)
assert result.message == "What is your name?"
assert result.intermediate_messages == ["Welcome"]
class TestUserMessageVariable:
"""{{user_message}} sits at the top level of variables, not under a node."""
def test_user_message_is_interpolated(self, workflow_service):
workflow_state = {"variables": {"user_message": "where is my order"}}
result = workflow_service._process_variables("You said: {{user_message}}", workflow_state)
assert result == "You said: where is my order"
def test_user_message_does_not_hide_node_variables(self, workflow_service):
# The user message was searched as if it were a node's variables, so a
# message containing a field's name broke the lookup for that field.
node_id = str(uuid4())
workflow_state = {"variables": {
node_id: {"contact_email": "a@example.com"},
"user_message": "my contact_email changed",
}}
result = workflow_service._process_variables("Email: {{contact_email}}", workflow_state)
assert result == "Email: a@example.com"
@pytest.mark.asyncio
async def test_message_node_sees_the_current_user_message(self, workflow_service, sample_workflow):
nodes = _linked_nodes(_message_node("Echo: {{user_message}}"))
result = await _run_workflow(workflow_service, sample_workflow, nodes, user_message="hello there")
assert result.message == "Echo: hello there"
class TestWaitNodeConfig:
@pytest.mark.asyncio
async def test_wait_node_reads_duration_from_config(self, workflow_service, sample_workflow):
# A real node: wait settings live in config; there is no wait_duration column.
node = WorkflowNode(
id=uuid4(),
node_type=NodeType.WAIT,
name="Wait",
config={"wait_duration": 5, "wait_unit": "minutes"},
)
result = await workflow_service._execute_node(
node, sample_workflow, {}, None, None, None, None, None, None, None, None, "test-session")
assert result.success is True
assert result.message == "Please wait 5 minutes..."
assert result.next_node_id is None
class TestUnknownConditionFormat:
def test_unparseable_condition_is_false(self, workflow_service):
assert workflow_service._evaluate_condition("this is not a condition", {}) is False
def test_unparseable_condition_takes_the_false_branch(self, workflow_service, sample_workflow):
true_target, false_target = uuid4(), uuid4()
true_conn = Mock(target_node_id=true_target, label="true", condition="true")
false_conn = Mock(target_node_id=false_target, label="false", condition="false")
node = Mock(spec=WorkflowNode)
node.id = uuid4()
node.node_type = NodeType.CONDITION
node.config = {"condition_expression": "{{missing_field}}"}
node.outgoing_connections = [true_conn, false_conn]
result = workflow_service._execute_condition_node(node, sample_workflow, {"variables": {}})
assert result.next_node_id == false_target