Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion workflows/tests/test_workflow_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
Tests the fixes for go_back/go_forward (empty action models) and deterministic execution.
"""

from workflow_use.schema.views import NavigationStep
import pytest

from workflow_use.schema.views import InputStep, NavigationStep
from workflow_use.workflow.service import Workflow


class TestWorkflowExecution:
Expand Down Expand Up @@ -179,6 +182,61 @@ def test_actions_requiring_wait(self):
assert 'input' not in actions_requiring_wait
assert 'extract' not in actions_requiring_wait

def test_resolve_placeholders_preserves_positional_input(self):
"""Positional placeholders in input steps remain literal without positional context."""
workflow = Workflow.__new__(Workflow)
workflow.context = {'name': 'Ada'}
value = 'Query param: {0}'
step = InputStep(type='input', target_text='Query', value=value)

resolved = workflow._resolve_placeholders(step)

assert resolved.value == value

def test_resolve_placeholders_keeps_named_context_behavior(self):
"""Known named placeholders resolve and unknown names remain literal."""
workflow = Workflow.__new__(Workflow)
workflow.context = {'name': 'Ada', 'items': ['first']}
data = {'known': 'Hello {name}', 'unknown': 'Hello {missing}', 'nested': '{items[0]}'}

assert workflow._resolve_placeholders(data) == {'known': 'Hello Ada', 'unknown': 'Hello {missing}', 'nested': 'first'}

def test_resolve_placeholders_positional_input_uses_default(self):
"""Unresolved positional input still follows the existing default-value path."""
workflow = Workflow.__new__(Workflow)
workflow.context = {}
step = InputStep(type='input', target_text='Query', value='{0}', default_value='fallback')

resolved = workflow._resolve_placeholders(step)

assert resolved.value == 'fallback'
assert step.value == '{0}'

def test_resolve_placeholders_preserves_automatic_input(self):
"""Automatic positional placeholders remain literal without positional context."""
workflow = Workflow.__new__(Workflow)
workflow.context = {}
step = InputStep(type='input', target_text='Query', value='literal {}')

assert workflow._resolve_placeholders(step).value == 'literal {}'

def test_resolve_placeholders_named_index_error_propagates(self):
"""An invalid index on a known context value is not a missing positional field."""
workflow = Workflow.__new__(Workflow)
workflow.context = {'items': []}

with pytest.raises(IndexError):
workflow._resolve_placeholders('{items[0]}')

def test_resolve_placeholders_named_index_error_does_not_use_default(self):
"""Input defaults must not hide indexing errors on a known context value."""
workflow = Workflow.__new__(Workflow)
workflow.context = {'items': []}
step = InputStep(type='input', target_text='Query', value='{items[0]}', default_value='fallback')

with pytest.raises(IndexError):
workflow._resolve_placeholders(step)


# Helper to run async tests
import asyncio
Expand Down
13 changes: 11 additions & 2 deletions workflows/workflow_use/workflow/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import json
import logging
from pathlib import Path
from string import Formatter
from typing import Any, Dict, List, TypeVar
from typing import cast as _cast

Expand Down Expand Up @@ -518,9 +519,17 @@ def _resolve_placeholders(self, data: Any) -> Any:
return formatted_data
return data # No placeholders, return as is
except KeyError:
# A key in the placeholder was not found in the context.
# Return the original string as per previous behavior.
# A named placeholder could not be resolved from the context.
return data
except IndexError:
# Only bare positional fields have no context to resolve. Preserve
# indexing/formatting errors from named context values.
if all(
field_name is None or field_name == '' or field_name.isdecimal()
for _, field_name, _, _ in Formatter().parse(data)
):
return data
raise

# TODO: This next things are not really supported atm, we'll need to to do it in the future.
elif isinstance(data, list):
Expand Down