forgetest: cloud.mode-switch opens the lid behind the controller's start, ahead of the hunt

The supervisor holds every controller spawn until the enclosure is
closed (forgectrl 0.1.25), and the test opened the lid before it asked
for the cloud controller: POST /mode answered "waiting, the lid is
open" and the controller never came up. The round trip now switches
with the lid closed, polls /mode five times a second, and opens the
lid the moment the controller is running. The client requests its
connect-time hunt a few seconds after its start, right behind its
session, so the hunt still finds the lid open. The order is recorded
and judged: the hunt's request line must not be in the client's log
when the lid reads open (hunt_before_lid_open), and the test refuses
to start with the lid open. The catalog text tells the operator to
open the lid at once, with a hand ready on it.

Proof. Host: test_cloud_suite drives the round trip with the hunt
landing only once the lid reads open, as on the bench, plus the lost
race (the hunt requested before the lid opened fails the test with
"before the lid was open") and the start with the lid open refused;
8 mode-switch cases green. Bench reference (dev image 20260915001814,
forgectrl 0.1.25): cloud.mode-switch PASS in 118 s, the lid open 4 s
before the client requested its hunt, no refusal before the hunt's
end, the lens homed, the exhaust row unjudged, 5 service motions
after the lid closed, $H under gfhome homed in 48.4 s with 9 motion
windows. No catalog consequence beyond the test itself: its covers
map is unchanged.
This commit is contained in:
ScottW514
2026-09-15 15:41:35 -04:00
parent 0c87927388
commit db6015dc81
2 changed files with 84 additions and 21 deletions
+48 -8
View File
@@ -558,12 +558,16 @@ class CloudSuiteTests(unittest.TestCase):
HUNT_RUN_SAMPLE = {"phase": "run", "verdict": "ok", "armed": False,
"fan_gates": {"exhaust": {"state": "unjudged", "reading": 0, "floor": 500}}}
def mode_switch_setup(self, hunt_lines=None, home_complete=True):
def mode_switch_setup(self, hunt_lines=None, home_complete=True, lid_late=False):
"""The fakes a mode-switch run needs: grbl to answer $H, the lid
lamp attr, homing_mode = gfcloud, the service lines landing on
the switch to cloud (the hunt reads as a run to the cooling
engine while it lasts), the re-hunt on the lid close, and gfhome
finishing the homing after $H."""
the switch to cloud - the client's start at once, its session and
hunt once the lid reads open, as on the bench where the lid opens
behind the controller's start and ahead of the hunt (the hunt
reads as a run to the cooling engine while it lasts) - the re-hunt
on the lid close, and gfhome finishing the homing after $H. With
lid_late the hunt is requested before the lid opens: the race the
test must call."""
self.grbl = helpers.FakeGrbl().start()
os.makedirs(self.sysfs + "pic", exist_ok=True)
self._attr("pic/lid_led", "236")
@@ -575,22 +579,35 @@ class CloudSuiteTests(unittest.TestCase):
hunt_part = hunt_lines(hunt_part)
fc = self.fc
def hunt():
self.append(hunt_part, delay=0.0)
time.sleep(4.0) # the hunt outlasts the session wait on the bench
fc.state["cool"] = {"phase": "idle", "armed": False, "hold": False}
def on_post(path, form):
if path == "/mode" and form.get("controller") == "cloud":
fc.state["cool"] = dict(self.HUNT_RUN_SAMPLE)
def land():
self.append(pre, delay=0.0)
if lid_late:
return
t0 = time.time()
while fc.state["status"]["switches"]["lid"] and time.time() - t0 < 5:
time.sleep(0.02)
time.sleep(0.3)
self.append(hunt_part, delay=0.0)
time.sleep(4.0) # the hunt outlasts the session wait on the bench
fc.state["cool"] = {"phase": "idle", "armed": False, "hold": False}
hunt()
threading.Thread(target=land, daemon=True).start()
elif path == "/mode" and form.get("controller") == "grbl":
self.grbl.state = "Idle"
return None
self.fc.on_post = on_post
def lid_opened():
if lid_late:
self.append(hunt_part, delay=0.0)
self.lid(False)
def lid_closed():
self.lid(True)
self.append(close_part, delay=0.2)
@@ -605,7 +622,7 @@ class CloudSuiteTests(unittest.TestCase):
f.write(b"2026-08-17T09:46:00.000000+00:00 gfhome[2300] INFO homing complete "
b"(service quiet 8s, 3 motion windows)\n")
self.home_hook = homing
return {"Open the lid.": lambda: self.lid(False), "Close the lid.": lid_closed}
return {"Open the lid.": lid_opened, "Close the lid.": lid_closed}
def wait_home_command(self):
while "$H" not in self.grbl.sent:
@@ -633,6 +650,29 @@ class CloudSuiteTests(unittest.TestCase):
self.assertEqual(self.script.asked, [])
self.assertTrue(any("PASS:" in l for l in run.lines), run.lines[-5:])
def test_mode_switch_opens_the_lid_behind_the_controller_and_ahead_of_the_hunt(self):
# No controller starts with the enclosure open: the switch is made
# lid closed, the lid opens once the controller is up, and the
# hunt's request lands after that - the order the test records.
hooks = self.mode_switch_setup()
threading.Thread(target=self.wait_home_command, daemon=True).start()
run = self.run_test(cloud.mode_switch, hooks=hooks, test_id="cloud.mode-switch")
self.assertFalse(run.evidence["hunt_before_lid_open"])
acts = [(a["channel"], a["state"]) for a in run.evidence["actions"]]
self.assertEqual(acts, [("lid", "open"), ("lid", "close")])
i_post = next(i for i, l in enumerate(run.lines) if "POST /mode controller=cloud" in l)
i_open = next(i for i, l in enumerate(run.lines) if "ACT lid open" in l)
self.assertLess(i_post, i_open)
def test_mode_switch_refuses_to_start_with_the_lid_open(self):
hooks = self.mode_switch_setup()
self.lid(False)
self.assertFails(cloud.mode_switch, "close the lid first", hooks=hooks)
def test_mode_switch_fails_when_the_hunt_was_requested_before_the_lid_opened(self):
hooks = self.mode_switch_setup(lid_late=True)
self.assertFails(cloud.mode_switch, "before the lid was open", hooks=hooks)
def test_mode_switch_fails_when_the_hunt_is_refused_for_the_lid(self):
def refused(hunt_part):
i = next(i for i, l in enumerate(hunt_part) if "z_axis:home starting z homing cycle" in l)