mirror of
https://github.com/github/spec-kit.git
synced 2026-07-05 21:49:47 +08:00
address review: namespaced execution with per-step copy-back
Revert to namespaced step IDs for execution (preserving unique log entries and state keys per iteration) but copy each step's result back to the unprefixed key immediately after it completes. This preserves backward compatibility (same namespaced key format, same log IDs) while fixing both the condition evaluation bug and inter-step references within multi-step loop bodies.
This commit is contained in:
@@ -672,30 +672,30 @@ class WorkflowEngine:
|
|||||||
for _loop_iter in range(max_iters - 1):
|
for _loop_iter in range(max_iters - 1):
|
||||||
if not evaluate_condition(condition, context):
|
if not evaluate_condition(condition, context):
|
||||||
break
|
break
|
||||||
# Snapshot current results under namespaced
|
# Namespace nested step IDs per iteration
|
||||||
# keys for per-iteration history before they
|
# so logs and state keys are unique.
|
||||||
# are overwritten by the next iteration.
|
# Execute one step at a time and alias each
|
||||||
|
# result back to the unprefixed key so that
|
||||||
|
# later steps in the same body and the loop
|
||||||
|
# condition see the latest values.
|
||||||
for ns in result.next_steps:
|
for ns in result.next_steps:
|
||||||
orig = ns.get("id")
|
ns_copy = dict(ns)
|
||||||
if orig and orig in context.steps:
|
orig = ns_copy.get("id")
|
||||||
ns_key = f"{step_id}:{orig}:{_loop_iter}"
|
if orig:
|
||||||
context.steps[ns_key] = context.steps[orig]
|
ns_copy["id"] = f"{step_id}:{orig}:{_loop_iter + 1}"
|
||||||
state.step_results[ns_key] = context.steps[orig]
|
self._execute_steps(
|
||||||
# Execute body with original step IDs so
|
[ns_copy], context, state, registry,
|
||||||
# results land at the unprefixed keys. Both
|
step_offset=-1,
|
||||||
# inter-step references within the body and
|
)
|
||||||
# the loop condition naturally see the latest
|
if orig and ns_copy["id"] in context.steps:
|
||||||
# values without a copy-back.
|
context.steps[orig] = context.steps[ns_copy["id"]]
|
||||||
self._execute_steps(
|
state.step_results[orig] = context.steps[ns_copy["id"]]
|
||||||
result.next_steps, context, state, registry,
|
if state.status in (
|
||||||
step_offset=-1,
|
RunStatus.PAUSED,
|
||||||
)
|
RunStatus.FAILED,
|
||||||
if state.status in (
|
RunStatus.ABORTED,
|
||||||
RunStatus.PAUSED,
|
):
|
||||||
RunStatus.FAILED,
|
return
|
||||||
RunStatus.ABORTED,
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
# Fan-out: execute nested step template per item with unique IDs
|
# Fan-out: execute nested step template per item with unique IDs
|
||||||
if step_type == "fan-out":
|
if step_type == "fan-out":
|
||||||
|
|||||||
@@ -1943,9 +1943,8 @@ steps:
|
|||||||
assert state.status == RunStatus.COMPLETED
|
assert state.status == RunStatus.COMPLETED
|
||||||
# The unprefixed key should reflect the latest iteration's result.
|
# The unprefixed key should reflect the latest iteration's result.
|
||||||
assert state.step_results["attempt"]["output"]["stdout"] == "done"
|
assert state.step_results["attempt"]["output"]["stdout"] == "done"
|
||||||
# Iteration-0 history preserved under namespaced key.
|
# Namespaced iteration-1 result should also exist.
|
||||||
assert "retry-loop:attempt:0" in state.step_results
|
assert "retry-loop:attempt:1" in state.step_results
|
||||||
assert state.step_results["retry-loop:attempt:0"]["output"]["stdout"] != "done"
|
|
||||||
# Counter should be 2 (iteration 0 + iteration 1), not 5.
|
# Counter should be 2 (iteration 0 + iteration 1), not 5.
|
||||||
assert counter_file.read_text(encoding="utf-8").strip() == "2"
|
assert counter_file.read_text(encoding="utf-8").strip() == "2"
|
||||||
|
|
||||||
@@ -2041,9 +2040,9 @@ steps:
|
|||||||
assert counter_file.read_text(encoding="utf-8").strip() == "3"
|
assert counter_file.read_text(encoding="utf-8").strip() == "3"
|
||||||
# Unprefixed key holds the last iteration's result.
|
# Unprefixed key holds the last iteration's result.
|
||||||
assert state.step_results["tick"]["output"]["stdout"] == "pending"
|
assert state.step_results["tick"]["output"]["stdout"] == "pending"
|
||||||
# Namespaced history keys for previous iterations exist.
|
# Namespaced keys for loop iterations exist.
|
||||||
assert "retry-loop:tick:0" in state.step_results
|
|
||||||
assert "retry-loop:tick:1" in state.step_results
|
assert "retry-loop:tick:1" in state.step_results
|
||||||
|
assert "retry-loop:tick:2" in state.step_results
|
||||||
|
|
||||||
def test_do_while_loop_runs_to_max_when_condition_stays_true(self, project_dir):
|
def test_do_while_loop_runs_to_max_when_condition_stays_true(self, project_dir):
|
||||||
"""Do-while loop must still run to max_iterations when the condition
|
"""Do-while loop must still run to max_iterations when the condition
|
||||||
|
|||||||
Reference in New Issue
Block a user