From d6f648b539d971a7106fb1fd5d007453f650cd78 Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Thu, 3 Sep 2026 18:08:16 -0400 Subject: [PATCH] forgetest: presence is proved once per test, not once per ready gate The ready gate lives inside the arm-and-fire helper, and the kill drill calls that helper twice, once for the expected stop and once for the SIGKILL. So the presence gate asked the operator for a second press part way through a test they had already proved themselves present for, with the actuator standing by holding the presses. It is the only test in the catalog with two ready gates. The second gate now returns at once. Its setup line still goes up, because the second half may want the scrap moved, but there is no press to make. --- forgetest/forgetest/runner.py | 11 +++++++++++ forgetest/tests/test_fixture.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/forgetest/forgetest/runner.py b/forgetest/forgetest/runner.py index bd866d9..5ee27f8 100644 --- a/forgetest/forgetest/runner.py +++ b/forgetest/forgetest/runner.py @@ -293,6 +293,17 @@ class Context: self.log("READY (fixture performs the step): %s", text) return + # Presence is proved once per test, not once per gate. A test with + # two armed halves (the kill drill stops the controller and does it + # again) reaches this twice, and asking a second time is friction + # with nothing behind it: the operator proved presence a moment ago + # and the actuator is already making the presses. The setup line + # still goes up, because the second half may want the scrap moved. + if self.run.fixture_takeover: + self.notice(text) + self.log("READY: presence already proved this test; the bench has the presses") + return + fixture = getattr(self.runner, "fixture", None) if self.runner is not None else None if fixture is not None and fixture.covers("button"): self.notice(text + " Then press the button on the machine to start.") diff --git a/forgetest/tests/test_fixture.py b/forgetest/tests/test_fixture.py index f3a8a4a..275efe0 100644 --- a/forgetest/tests/test_fixture.py +++ b/forgetest/tests/test_fixture.py @@ -536,6 +536,36 @@ class RoutingTests(unittest.TestCase): self.assertEqual(rec[0]["by"], "operator") self.assertTrue(any("press the button on the machine" in ln.lower() for ln in run.lines)) + def test_presence_is_proved_once_per_test_not_once_per_gate(self): + """A test with two armed halves reaches the ready gate twice. The + second one must not ask for another press: the operator proved + presence a moment earlier and the actuator has the presses. The + setup line still goes up, because the second half may want the + scrap moved.""" + run = Run("test", "r.live", "r.live") + ctx = Context(run, self.runner, self.reg["r.live"]) + self.runner.probe_fixture(force=True) + asked = [] + run.ask = lambda q, o: asked.append(q) + self.press_button(False) + threading.Timer(0.3, self.press_button, args=(True,)).start() + threading.Timer(0.8, self.press_button, args=(False,)).start() + ctx.ready("First half.") + self.assertTrue(run.fixture_takeover) + presses = [r for r in run.evidence["actions"] if r["state"] == "presence"] + self.assertEqual(len(presses), 1) + + # the second gate: returns at once, no new press, notice still shown + seen = [] + run.set_notice = lambda text: seen.append(text) + t0 = time.time() + ctx.ready("Second half. Move the scrap.") + self.assertLess(time.time() - t0, 1.0) + self.assertEqual(asked, []) + self.assertEqual(len([r for r in run.evidence["actions"] if r["state"] == "presence"]), 1) + self.assertIn("Second half. Move the scrap.", seen) + self.assertTrue(any("presence already proved" in ln for ln in run.lines)) + def test_the_presence_press_hands_the_arm_press_to_the_actuator(self): """The bench's standing opt-in is not needed once the operator has proved presence: that press is what the opt-in existed to