diff --git a/src/wf_core/analysis/context_scopes.py b/src/wf_core/analysis/context_scopes.py index b8f4e86a..7518bd51 100644 --- a/src/wf_core/analysis/context_scopes.py +++ b/src/wf_core/analysis/context_scopes.py @@ -14,6 +14,7 @@ from wf_core.context_contracts import ( FOREACH_CONTEXT_KEY, LOOP_INDEX_CONTEXT_KEY, LOOP_ITEM_CONTEXT_KEY, + RESERVED_CONTEXT_KEYS, STANDARD_CONTEXT_FIELDS, ContextFieldContract, ContextSchema, @@ -319,7 +320,11 @@ def _context_schema_for_stack( required.extend([LOOP_ITEM_CONTEXT_KEY, LOOP_INDEX_CONTEXT_KEY]) for owner_id in stack: foreach = foreach_nodes.get(owner_id) - if foreach is not None and foreach.as_: + if ( + foreach is not None + and foreach.as_ + and foreach.as_ not in RESERVED_CONTEXT_KEYS + ): properties[foreach.as_] = deepcopy( _foreach_item_schema( workflow, foreach, foreach_nodes, owner_stack_by_node diff --git a/tests/core/test_context_scopes.py b/tests/core/test_context_scopes.py index 2a1c02d9..4fb850d8 100644 --- a/tests/core/test_context_scopes.py +++ b/tests/core/test_context_scopes.py @@ -156,6 +156,31 @@ def test_all_standard_context_names_are_reserved_from_foreach_aliases() -> None: frame_context_view(_run_with(frame), frame) +@pytest.mark.parametrize("alias", ["prior_outcome", "foreach"]) +def test_reserved_alias_does_not_overwrite_generated_context_schema(alias: str) -> None: + """Invalid aliases still leave secondary context diagnostics coherent.""" + from wf_core.analysis.context_scopes import context_schema_for_node + + workflow = _workflow( + start="each", + nodes=[_foreach("each", alias=alias), _node("body")], + edges=[ + {"from": "each", "outcome": "loop", "to": "body"}, + {"from": "body", "outcome": "ok", "to": "each"}, + {"from": "each", "outcome": "done", "to": END}, + ], + ) + generated = context_schema_for_node(workflow, "body")["properties"][alias] + analyzed = _field_map(workflow, "body")[alias].schema + + assert generated == analyzed + if alias == "prior_outcome": + assert generated == {"type": ["string", "null"]} + else: + assert generated["type"] == "object" + assert set(generated["properties"]) == {"each"} + + def test_serial_and_concurrent_foreach_expose_the_same_scoped_context() -> None: for mode in ("serial", "concurrent"): workflow = _workflow(