simulation: derive the stepper latch from moving bodies on add/remove

One helper sets stepper.blocked from _moving_bodies() after add_body,
pre-init remove_body, initialize and stepper recreation, so the latch never
disagrees with motion ownership. Tests: latch follows add/remove; the
after-close assertion checks the actual refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-10-30 06:53:12 +00:00
co-authored by Claude Opus 5.5
parent b448bc3f71
commit e682d16c3a
2 changed files with 21 additions and 5 deletions
+11 -1
View File
@@ -232,6 +232,16 @@ def test_add_body_and_set_body_route_targets(monkeypatch):
**kwargs)
def test_stepper_latch_follows_moving_bodies(monkeypatch):
sim = routed_simulation(monkeypatch)
still = sim.add_body("circle", (100., 40.), 5.)
assert sim.stepper.blocked is None
moving = sim.add_body("circle", (50., 40.), 10., translation=Translation((0., 0., -1., 1.), .1))
assert sim.stepper.blocked is not None
sim.remove_body(moving) # removing the last translating circle clears the latch
assert sim.stepper.blocked is None and sim.bodies.get(still).motion is None
def validate(monkeypatch, objects, flags=None, **cfg):
sim = routed_simulation(monkeypatch)
for obj in objects:
@@ -306,5 +316,5 @@ def test_failed_moving_step_marks_simulation_failed(monkeypatch):
with pytest.raises(RuntimeError, match="unknown"): # no retry on the failed circle
motion.advance(lambda *_: None, lambda: None, lambda: None)
sim.close() # close still works
with pytest.raises(Exception): # no re-entry after close
with pytest.raises(RuntimeError, match="Simulation failed"): # no re-entry after close
sim.run(1)