diff --git a/workflows/tests/test_workflow_execution.py b/workflows/tests/test_workflow_execution.py index 472c6456..efd3d07c 100644 --- a/workflows/tests/test_workflow_execution.py +++ b/workflows/tests/test_workflow_execution.py @@ -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: @@ -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 diff --git a/workflows/workflow_use/workflow/service.py b/workflows/workflow_use/workflow/service.py index 1a6c325b..d9539e97 100644 --- a/workflows/workflow_use/workflow/service.py +++ b/workflows/workflow_use/workflow/service.py @@ -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 @@ -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):