chore: drop dead context compat, retire plan boxes, fix spec wording
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user