From 688fccecc8f4ea94586e1390bad00f702ec9a3f0 Mon Sep 17 00:00:00 2001 From: lda Date: Sat, 5 Sep 2026 00:10:23 +0700 Subject: [PATCH] chore: drop dead context compat, retire plan boxes, fix spec wording --- .../2026-09-04-structured-runtime-context.md | 74 ++++++++-------- ...09-04-structured-runtime-context-design.md | 3 +- src/wf_core/runtime/ops/frames.py | 23 ----- src/wf_core/validation/context_paths.py | 45 +--------- tests/core/test_context_scopes.py | 87 ++++++++++++------- tests/core/test_scheduler.py | 29 ++++--- 6 files changed, 112 insertions(+), 149 deletions(-) diff --git a/docs/historical/superpowers/plans/2026-09-04-structured-runtime-context.md b/docs/historical/superpowers/plans/2026-09-04-structured-runtime-context.md index a1953815..52beede6 100644 --- a/docs/historical/superpowers/plans/2026-09-04-structured-runtime-context.md +++ b/docs/historical/superpowers/plans/2026-09-04-structured-runtime-context.md @@ -20,7 +20,7 @@ sugar. JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. **Spec:** -[`docs/superpowers/specs/2026-09-04-structured-runtime-context-design.md`](../specs/2026-09-04-structured-runtime-context-design.md) +[`docs/superpowers/specs/2026-09-04-structured-runtime-context-design.md`](../../superpowers/specs/2026-09-04-structured-runtime-context-design.md) ## Global Constraints @@ -100,7 +100,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. - `FrameContextView.graph` exposes a JSON-compatible `foreach` mapping, all unique active aliases, and innermost `loop_item`/`loop_index` values. -- [ ] **Step 1: Write failing model and ancestry tests** +- [x] **Step 1: Write failing model and ancestry tests** Add focused helpers that construct a `RunState` with explicit frames, then add tests named: @@ -133,7 +133,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. For the scope-boundary case, give the child root a scheduling parent in the caller's foreach frame and assert that the child context remains `{}`. -- [ ] **Step 2: Run the focused tests and confirm the missing API fails** +- [x] **Step 2: Run the focused tests and confirm the missing API fails** Run: @@ -144,7 +144,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. Expected: collection or assertions fail because `ForeachContext` and the ancestry-aware helper do not exist. -- [ ] **Step 3: Add the typed context value and validate frame metadata once** +- [x] **Step 3: Add the typed context value and validate frame metadata once** Add `ForeachContext` beside `RuntimeContext` and export it through `wf_core.__init__`. Keep `ForeachIterationMetadata` as the typed decoder for @@ -164,7 +164,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. ) ``` -- [ ] **Step 4: Implement fail-closed same-scope ancestry traversal** +- [x] **Step 4: Implement fail-closed same-scope ancestry traversal** Walk from the selected frame through `parent_frame_id` while scope ids match. Validate the full chain before returning materialized values: @@ -226,7 +226,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. graph[LOOP_INDEX_CONTEXT_KEY] = innermost.index ``` -- [ ] **Step 5: Add corruption and collision regressions** +- [x] **Step 5: Add corruption and collision regressions** Add: @@ -241,7 +241,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. `run.to_dict()` before the read-only test and assert it remains equal after deriving context. -- [ ] **Step 6: Run and commit the focused model slice** +- [x] **Step 6: Run and commit the focused model slice** Run: @@ -285,7 +285,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. - `RuntimeContext.metadata` remains a defensive copy of the selected frame's metadata; `RuntimeContext.foreach` is the canonical typed view. -- [ ] **Step 1: Write failing end-to-end runtime tests** +- [x] **Step 1: Write failing end-to-end runtime tests** Extend `test_structured_runtime_context.py` with: @@ -317,7 +317,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. Build the binding test with canonical `InputPathBinding` values for `context.foreach.customers.item` and `context.foreach.orders.item`. -- [ ] **Step 2: Run the end-to-end tests and observe innermost-only behavior** +- [x] **Step 2: Run the end-to-end tests and observe innermost-only behavior** Run: @@ -329,7 +329,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. Expected: the new end-to-end tests fail because runtime consumers still use innermost frame-only context and handlers do not receive `.foreach`. -- [ ] **Step 3: Update all graph-visible context call sites together** +- [x] **Step 3: Update all graph-visible context call sites together** Replace every old call and verify with search: @@ -349,7 +349,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. Root workflow output receives `frame_context_view(run, root_frame).graph` rather than an omitted context so standard root facts remain consistent. -- [ ] **Step 4: Give Python handlers the same typed projection** +- [x] **Step 4: Give Python handlers the same typed projection** In `_resolve_node_execution`, materialize the view once and pass its two projections to their consumers: @@ -374,14 +374,14 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. Resolve graph bindings against `context_view.graph`. Do not reconstruct `ForeachContext` from the graph-visible dictionary. -- [ ] **Step 5: Update old focused tests to pass `RunState` explicitly** +- [x] **Step 5: Update old focused tests to pass `RunState` explicitly** Replace direct frame-only context calls in scheduler/context tests with a run containing that frame. Keep assertions for existing standard and compatibility keys; add structured assertions rather than deleting old coverage. -- [ ] **Step 6: Run and commit the runtime integration slice** +- [x] **Step 6: Run and commit the runtime integration slice** Run: @@ -433,7 +433,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. `GraphSourcePath.context(self.id)` because that helper parses dots as path separators. -- [ ] **Step 1: Write failing path and serialization tests** +- [x] **Step 1: Write failing path and serialization tests** Add: @@ -455,7 +455,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. assert "index" not in each.model_dump(mode="json") ``` -- [ ] **Step 2: Write failing node and subgraph binding tests** +- [x] **Step 2: Write failing node and subgraph binding tests** Use the computed ref directly in both authoring boundaries: @@ -473,12 +473,12 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. Assert both compiled bindings serialize their path as `context.foreach.orders.item`. -- [ ] **Step 3: Implement only the two computed properties** +- [x] **Step 3: Implement only the two computed properties** Add the properties directly to `ForeachNode`. Do not create `ForeachRef`, a node-address type, or field-selection sugar beneath `.item`. -- [ ] **Step 4: Run and commit the authoring slice** +- [x] **Step 4: Run and commit the authoring slice** Run: @@ -596,7 +596,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. Keep entry schemas inline unless a measured schema-size problem requires `$defs`; the observable field types and required paths are contractual. -- [ ] **Step 1: Replace innermost-only tests with full-stack expectations** +- [x] **Step 1: Replace innermost-only tests with full-stack expectations** Update the existing nested test instead of adding contradictory coverage: @@ -617,7 +617,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. Add `test_inner_completion_schema_restores_outer_structured_entry` and assert `after_inner` contains only the outer structured entry. -- [ ] **Step 2: Run static analysis tests and confirm the old projection fails** +- [x] **Step 2: Run static analysis tests and confirm the old projection fails** Run: @@ -628,7 +628,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. Expected: nested assertions fail because `_available_fields` uses only `stack[-1]`. -- [ ] **Step 3: Build contracts from the whole owner stack** +- [x] **Step 3: Build contracts from the whole owner stack** Change `_available_fields` to accept the complete `ForeachOwnerStack`. For every owner id, infer its item schema in the controller's own outer region, @@ -643,7 +643,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. resolve `over=context.foreach.outer.item.children` without claiming its own not-yet-active entry. -- [ ] **Step 4: Keep context schema construction reusable and bounded** +- [x] **Step 4: Keep context schema construction reusable and bounded** Implement `context_schemas_by_node` by composing the returned contracts, not by running a second graph traversal. `context_schema_for_node` is the @@ -652,7 +652,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. generated per-node schema; callers treat that absence as invalid, not as root context. -- [ ] **Step 5: Expose structured paths through authoring inventory** +- [x] **Step 5: Expose structured paths through authoring inventory** Add tests showing `context_path_options_for_node(workflow, "inner_body")` includes at least: @@ -671,7 +671,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. quoting. Do not hand-concatenate a dotted foreach id; format literal segments through `GraphSourcePath`. -- [ ] **Step 6: Run and commit the static projection slice** +- [x] **Step 6: Run and commit the static projection slice** Run: @@ -730,7 +730,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. - Ordinary input/state validation remains where it is. The new pass owns the stronger, program-location-aware meaning of `context.*`. -- [ ] **Step 1: Write failing context-path validation tests** +- [x] **Step 1: Write failing context-path validation tests** Add: @@ -755,7 +755,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. assert "work" in issue.message ``` -- [ ] **Step 2: Cover every model surface that can contain a graph path** +- [x] **Step 2: Cover every model surface that can contain a graph path** Parameterize invalid `context.foreach.missing.item` references through: @@ -770,7 +770,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. prevents a future path-bearing model from accidentally retaining the current permissive `allow_context=True` behavior. -- [ ] **Step 3: Add one bounded structural path walker** +- [x] **Step 3: Add one bounded structural path walker** In `validation/context_paths.py`, use small typed walkers for conditions and input expressions: @@ -797,7 +797,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. for every step kind; do not duplicate context validation in each existing step validator. -- [ ] **Step 4: Validate context paths against the consuming location** +- [x] **Step 4: Validate context paths against the consuming location** For paths whose root is `context`, walk their literal `parts` through the consuming node's generated schema. A path is valid only if every segment is @@ -811,7 +811,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. Change `validate_foreach_node` so context-rooted `over` paths reach this pass instead of being rejected by the old input/state-only check. -- [ ] **Step 5: Write failing alias-collision tests** +- [x] **Step 5: Write failing alias-collision tests** Add: @@ -823,7 +823,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. and `loop_index`. The nested failure points to the inner foreach's `as` field. Siblings in separate control regions may reuse an alias. -- [ ] **Step 6: Share control-region analysis during validation** +- [x] **Step 6: Share control-region analysis during validation** In `validate_workflow`, run `analyze_control_regions(workflow)` once. Feed the result to context schema construction, translate its issues as today, then @@ -831,7 +831,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. `context_fields_by_node`; add an optional internal `control_regions=` input if necessary while keeping the existing public call form valid. -- [ ] **Step 7: Run and commit the validation slice** +- [x] **Step 7: Run and commit the validation slice** Run: @@ -879,7 +879,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. `foreach_ref.item`, while documenting `RuntimeContext.foreach` as the advanced handler escape hatch. -- [ ] **Step 1: Write the interrupt-resume identity regression** +- [x] **Step 1: Write the interrupt-resume identity regression** Build `outer.loop -> inner.loop -> ask -> inner -> outer`, interrupt one inner item, serialize it with `dump_run_state`, restore it with @@ -899,7 +899,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. Do not reuse the original in-memory `RunState`; the loaded value is the resume input so reconstruction is genuinely tested. -- [ ] **Step 2: Write the complete subgraph scope-boundary test** +- [x] **Step 2: Write the complete subgraph scope-boundary test** Build a parent foreach and map `each.item` into a saved child subgraph's declared input. Give both parent and child a foreach node with id `orders`. @@ -916,7 +916,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. `child_ctx.foreach == {}`. This proves the caller entry was not inherited and the reused static id does not collide. -- [ ] **Step 3: Run the persistence and scope pressure tests** +- [x] **Step 3: Run the persistence and scope pressure tests** Run: @@ -927,7 +927,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. Expected: all commands pass. -- [ ] **Step 4: Document the preferred authoring and advanced Python forms** +- [x] **Step 4: Document the preferred authoring and advanced Python forms** Add this shape to `docs/wf_authoring_control_flow.md` and the Python skill: @@ -953,7 +953,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. - child workflows do not inherit caller context and must receive input; - `loop_item`, `loop_index`, and aliases are migration conveniences. -- [ ] **Step 5: Mark the implementation current and retire the live plan** +- [x] **Step 5: Mark the implementation current and retire the live plan** Change the spec status from approved to implemented, replace the roadmap's proposed wording with a completed current-runtime statement, and move this @@ -965,7 +965,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. docs skills ``` -- [ ] **Step 6: Run full verification** +- [x] **Step 6: Run full verification** Run: @@ -988,7 +988,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2. remains the only failure, record its exact failing test and verify it also fails at the plan's starting commit before treating it as baseline. -- [ ] **Step 7: Commit the integration and documentation slice** +- [x] **Step 7: Commit the integration and documentation slice** Stage exact paths so the user's `docs/AGENTS.md` edit remains untouched: diff --git a/docs/superpowers/specs/2026-09-04-structured-runtime-context-design.md b/docs/superpowers/specs/2026-09-04-structured-runtime-context-design.md index 6ad72420..dbd36907 100644 --- a/docs/superpowers/specs/2026-09-04-structured-runtime-context-design.md +++ b/docs/superpowers/specs/2026-09-04-structured-runtime-context-design.md @@ -414,7 +414,8 @@ compatibility promise. - `loop_item` and `loop_index` select the innermost entry. - Unique outer and inner aliases remain available together. - Inner completion restores the outer entry and removes the inner entry. -- Concurrent item frames receive distinct activation/frame/lineage values. +- Concurrent item frames share one visit activation but receive distinct + frame/lineage values. - Interrupt resume recreates the same structured entries from persisted state. - Malformed persisted foreach metadata fails closed. diff --git a/src/wf_core/runtime/ops/frames.py b/src/wf_core/runtime/ops/frames.py index 6a658cbf..e247556f 100644 --- a/src/wf_core/runtime/ops/frames.py +++ b/src/wf_core/runtime/ops/frames.py @@ -18,29 +18,6 @@ from wf_core.errors import WorkflowExecutionError from wf_core.run_state import ExecutionFrame, ForeachContext, RunState -def frame_context_values(frame: ExecutionFrame) -> dict[str, object | None]: - context: dict[str, object | None] = { - PRIOR_OUTCOME_CONTEXT_KEY: frame.prior_outcome, - ACTIVATED_INCOMING_EDGE_CONTEXT_KEY: frame.activated_incoming_edge, - SCOPE_ID_CONTEXT_KEY: frame.scope_id, - LINEAGE_ID_CONTEXT_KEY: frame.lineage_id, - PARENT_LINEAGE_ID_CONTEXT_KEY: frame.parent_lineage_id, - } - if frame.kind == "foreach_iteration": - loop_item = frame.metadata.get("loop_item") - loop_index = frame.metadata.get("loop_index") - loop_alias = frame.metadata.get("loop_alias") - context[LOOP_ITEM_CONTEXT_KEY] = loop_item - context[LOOP_INDEX_CONTEXT_KEY] = loop_index - if ( - isinstance(loop_alias, str) - and loop_alias - and loop_alias not in RESERVED_CONTEXT_KEYS - ): - context[loop_alias] = loop_item - return context - - @dataclass(frozen=True, slots=True) class FrameContextView: """Typed handler context and graph values from one ancestry walk.""" diff --git a/src/wf_core/validation/context_paths.py b/src/wf_core/validation/context_paths.py index e20b127d..9b328aa0 100644 --- a/src/wf_core/validation/context_paths.py +++ b/src/wf_core/validation/context_paths.py @@ -199,8 +199,9 @@ def _failing_segment( """Return the first unknown segment plus the keys available there. Returns ``(None, "")`` when the path walks declared properties (or - permissive unconstrained schemas). Diagnostics only; validity follows - the same walk as :func:`_path_in_schema`. + permissive unconstrained schemas). A bare ``$ref`` fails closed: generated + per-node schemas are inline except for cyclic shapes, which cannot be + proven valid statically. """ if not parts: return None, "" @@ -226,46 +227,6 @@ def _failing_segment( return None, "" -def _path_in_schema(schema: Mapping[str, Any], parts: tuple[str, ...]) -> bool: - """Return whether literal parts walk declared object properties. - - The whole ``context`` object (no parts) and the ``foreach`` map itself - are readable. Unknown segments fail closed, except beneath an - unconstrained item schema (``{}`` or an open object with no declared - properties): a generic ``array`` collection infers ``{}``, and runtime - values may carry fields the static schema cannot see, so such subpaths - stay permissive. Closed maps with ``additionalProperties: False`` (like - the ``foreach`` map itself) still reject unknown keys. - """ - if not parts: - return True - current: Any = schema - for part in parts: - if not isinstance(current, Mapping): - return False - # Resolve local $ref if present (defensive; generated schemas are inline). - while isinstance(current.get("$ref"), str): - # Generated context schemas keep entry schemas inline, so an - # unresolvable ref here means the path cannot be proven valid. - return False - properties = current.get("properties") - if not isinstance(properties, Mapping): - # No declared properties: unconstrained `{}` or an open object - # allows subpaths; scalar or closed schemas do not. - if current == {}: - return True - if ( - current.get("type") == "object" - and current.get("additionalProperties", True) is not False - ): - return True - return False - if part not in properties: - return False - current = properties[part] - return True - - def _validate_workflow_output(workflow: Workflow, report: ValidationReport) -> None: schema = root_context_schema() for output_index, binding in enumerate(workflow.output): diff --git a/tests/core/test_context_scopes.py b/tests/core/test_context_scopes.py index 9df17090..9ac8979b 100644 --- a/tests/core/test_context_scopes.py +++ b/tests/core/test_context_scopes.py @@ -1,5 +1,7 @@ from __future__ import annotations +import pytest + from wf_core import END, Edge, ForeachNode, NodeUse, SchemaRef, StateSchema, Workflow from wf_core.analysis.context_scopes import ( ContextFieldAvailability, @@ -7,9 +9,10 @@ from wf_core.analysis.context_scopes import ( context_fields_by_node, ) from wf_core.context_contracts import STANDARD_CONTEXT_FIELDS, foreach_context_fields +from wf_core.errors import WorkflowExecutionError from wf_core.models.steps import Step -from wf_core.run_state import ExecutionFrame -from wf_core.runtime.ops.frames import frame_context_values +from wf_core.run_state import ExecutionFrame, RunState, RunStatus +from wf_core.runtime.ops.frames import frame_context_view def _node(node_id: str) -> NodeUse: @@ -66,30 +69,48 @@ def _field_map(workflow: Workflow, node_id: str) -> dict[str, ContextFieldAvaila } -def test_frame_context_values_uses_standard_and_foreach_contract_keys() -> None: - ordinary = frame_context_values( - ExecutionFrame( - id="root", - kind="root", - node_id="plain", - prior_outcome="ok", - activated_incoming_edge="start", - ) +def _run_with(*frames: ExecutionFrame) -> RunState: + run = RunState( + workflow_name="demo", + status=RunStatus.RUNNING, + workflow_input={}, + state={}, ) + for frame in frames: + run.frames[frame.id] = frame + return run + + +def test_frame_context_view_uses_standard_and_foreach_contract_keys() -> None: + root = ExecutionFrame( + id="root", + kind="root", + node_id="plain", + prior_outcome="ok", + activated_incoming_edge="start", + ) + ordinary = frame_context_view(_run_with(root), root).graph assert ordinary["prior_outcome"] == "ok" assert ordinary["activated_incoming_edge"] == "start" assert ordinary["scope_id"] == "root" assert ordinary["lineage_id"] == "root" assert ordinary["parent_lineage_id"] is None + assert ordinary["foreach"] == {} assert "loop_item" not in ordinary iteration = ExecutionFrame( id="root:each:0", kind="foreach_iteration", node_id="body", - metadata={"loop_item": "a", "loop_index": 0, "loop_alias": "item"}, + metadata={ + "foreach_node_id": "each", + "activation_id": "act-1", + "loop_item": "a", + "loop_index": 0, + "loop_alias": "item", + }, ) - context = frame_context_values(iteration) + context = frame_context_view(_run_with(iteration), iteration).graph assert context["loop_item"] == "a" assert context["loop_index"] == 0 assert context["item"] == "a" @@ -104,13 +125,6 @@ def test_context_contracts_deduplicate_aliases_that_are_standard_loop_keys() -> def test_all_standard_context_names_are_reserved_from_foreach_aliases() -> None: - expected_values = { - "prior_outcome": "ok", - "activated_incoming_edge": "start", - "scope_id": "scope", - "lineage_id": "lineage", - "parent_lineage_id": "parent", - } standard_names = {field.name for field in STANDARD_CONTEXT_FIELDS} for name in standard_names: @@ -118,20 +132,27 @@ def test_all_standard_context_names_are_reserved_from_foreach_aliases() -> None: field.name for field in foreach_context_fields(name, {"type": "string"}) } assert name not in foreach_names - context = frame_context_values( - ExecutionFrame( - id="child", - kind="foreach_iteration", - node_id="body", - scope_id="scope", - lineage_id="lineage", - parent_lineage_id="parent", - prior_outcome="ok", - activated_incoming_edge="start", - metadata={"loop_item": "item", "loop_index": 0, "loop_alias": name}, - ) + # A reserved alias can never back a runtime entry: the derived view + # fails closed instead of silently shadowing the standard key. + frame = ExecutionFrame( + id="child", + kind="foreach_iteration", + node_id="body", + scope_id="scope", + lineage_id="lineage", + parent_lineage_id="parent", + prior_outcome="ok", + activated_incoming_edge="start", + metadata={ + "foreach_node_id": "each", + "activation_id": "act-1", + "loop_item": "item", + "loop_index": 0, + "loop_alias": name, + }, ) - assert context[name] == expected_values[name] + with pytest.raises(WorkflowExecutionError, match="reserved context keys"): + frame_context_view(_run_with(frame), frame) def test_serial_and_concurrent_foreach_expose_the_same_scoped_context() -> None: diff --git a/tests/core/test_scheduler.py b/tests/core/test_scheduler.py index b88689aa..93234ff6 100644 --- a/tests/core/test_scheduler.py +++ b/tests/core/test_scheduler.py @@ -7,7 +7,7 @@ from wf_core.models.schemas import SchemaRef, StateSchema from wf_core.models.workflow import Workflow from wf_core.run_state import ExecutionFrame, FrameStatus, RunState, RunStatus from wf_core.runtime.ops.flow import advance_frame -from wf_core.runtime.ops.frames import frame_context_values +from wf_core.runtime.ops.frames import frame_context_view from wf_core.runtime.ops.runs import create_run_state from wf_core.runtime.scheduler import ( add_frame, @@ -232,19 +232,22 @@ def test_deadlock_error_includes_ready_queue_and_frame_summary() -> None: assert "parent:blocked@foreach" in message -def test_frame_context_values_exposes_configured_foreach_alias() -> None: - context = frame_context_values( - ExecutionFrame( - id="child", - kind="foreach_iteration", - node_id="body", - metadata={ - "loop_item": {"id": "a"}, - "loop_index": 2, - "loop_alias": "record", - }, - ) +def test_frame_context_view_exposes_configured_foreach_alias() -> None: + run = _run() + frame = ExecutionFrame( + id="child", + kind="foreach_iteration", + node_id="body", + metadata={ + "foreach_node_id": "each", + "activation_id": "act-1", + "loop_item": {"id": "a"}, + "loop_index": 2, + "loop_alias": "record", + }, ) + run.frames[frame.id] = frame + context = frame_context_view(run, frame).graph assert context["loop_item"] == {"id": "a"} assert context["loop_index"] == 2