forgetest: the coolant flow calibration is a required step; the gate's missing list

forgectrl requires cooling.flow after preferences, machine, and cloud,
and GET /wiz lists the required steps the gate waits for as missing.

- REQUIRED_STEPS is the form steps plus cooling.flow, in the gate's
  order; REQUIRED_FORM_STEPS are the form steps. cooling.flow leaves
  RECOMMENDED_STEPS.
- setup.gate-blocks-controllers expects "setup required: preferences,
  machine, cloud, cooling.flow" on a record with no step done, with all
  four named in missing, and an empty missing list once they are done
  and the gate is open.
- setup.first-run-flow walks the form steps alone: its seed keeps the
  checks done, the calibration among them, so the gate's reason
  narrows from the three form steps to open.

Proof: the setup host tests (74) and the manifest tests pass; the host
test of record_with_required_steps keeps the four.

Acceptance: these are the tests.
This commit is contained in:
ScottW514
2026-10-12 16:58:58 -04:00
parent 748d28686d
commit 005ff8008b
2 changed files with 26 additions and 11 deletions
+21 -9
View File
@@ -47,15 +47,18 @@ TLS_BASE = "https://127.0.0.1" # FORGECTRL_TLS_URL overrides (host test
# The first run's form wizards, all at version 1 (wiz.c). # The first run's form wizards, all at version 1 (wiz.c).
FORM_WIZARDS = ("advisories", "account", "preferences", "machine", "cloud") FORM_WIZARDS = ("advisories", "account", "preferences", "machine", "cloud")
# The steps the gate requires after the account, all at version 1, in # The steps the gate requires after the account, all at version 1, in
# the order the gate's reason names them (setup.c). # the order the gate's reason names them (setup.c): the form steps, then
REQUIRED_STEPS = ("preferences", "machine", "cloud") # the coolant flow calibration. The first-run tests seed the checks done,
# the calibration among them, so the form steps are what they walk.
REQUIRED_FORM_STEPS = ("preferences", "machine", "cloud")
REQUIRED_STEPS = REQUIRED_FORM_STEPS + ("cooling.flow",)
# What the image's step table recommends on a record with the required # What the image's step table recommends on a record with the required
# steps done and nothing else (setup.c): every step after them. The # steps done and nothing else (setup.c): every step after them. The
# cloud header joins on a machine with cloud mode on. The gate's own # cloud header joins on a machine with cloud mode on. The gate's own
# parts (the advisories, the account, the required steps, the intake fan # parts (the advisories, the account, the required steps, the intake fan
# count) are never recommended as not run yet, and the laser floor and # count) are never recommended as not run yet, and the laser floor and
# the bed size are offered, never recommended. # the bed size are offered, never recommended.
RECOMMENDED_STEPS = ("switches", "sensors", "airflow", "cooling.aa-offset", "cooling.flow", "motion", RECOMMENDED_STEPS = ("switches", "sensors", "airflow", "cooling.aa-offset", "motion",
"cameras", "lens.travel", "laser.focus") "cameras", "lens.travel", "laser.focus")
NEVER_RECOMMENDED = ("advisories", "account", "airflow.intakes", "laser.floor", "motion.envelope") NEVER_RECOMMENDED = ("advisories", "account", "airflow.intakes", "laser.floor", "motion.envelope")
# The setting that counts the intake fans: the gate holds without it. # The setting that counts the intake fans: the gate holds without it.
@@ -388,10 +391,11 @@ _GATE_COVERS = [("forgectrl", "src/setup.*"), ("forgectrl", "src/super.c"),
"process runs and the Grbl port is closed; with the consent and without the " "process runs and the Grbl port is closed; with the consent and without the "
"account, the same with the reason naming the account; with the consent and " "account, the same with the reason naming the account; with the consent and "
"the account and no step done, the same with the reason 'setup required: " "the account and no step done, the same with the reason 'setup required: "
"preferences, machine, cloud'. With those three done as well and no flag, the " "preferences, machine, cloud, cooling.flow'. With those four done as well and "
"no flag, the "
"gate is open with no reason: the supervisor spawns the controller, the Grbl " "gate is open with no reason: the supervisor spawns the controller, the Grbl "
"port accepts, and GET /wiz recommends every other step as not run yet (never " "port accepts, and GET /wiz recommends every other step as not run yet (never "
"the advisories, the account, the three required steps, or the intake fan " "the advisories, the account, the four required steps, or the intake fan "
"count; not the optional laser floor or bed size). On that record the intake " "count; not the optional laser floor or bed size). On that record the intake "
"fan count is cleared: the gate closes live with the reason naming the intake " "fan count is cleared: the gate closes live with the reason naming the intake "
"fans, GET /wiz reports them not counted, and the supervisor stops the " "fans, GET /wiz reports them not counted, and the supervisor stops the "
@@ -459,6 +463,11 @@ def gate_blocks_controllers(ctx):
with ctx.takeover(): with ctx.takeover():
write_file(record_path(), encoded(record_without_wizards(record))) write_file(record_path(), encoded(record_without_wizards(record)))
gated("required steps", "setup required: " + ", ".join(REQUIRED_STEPS)) gated("required steps", "setup required: " + ", ".join(REQUIRED_STEPS))
# The page opens the steps the gate waits for from this list.
w = wiz(fc)
ev["missing_none_done"] = w.get("missing")
ctx.check(w.get("missing") == list(REQUIRED_STEPS),
"GET /wiz does not name the required steps as missing: %s", w.get("missing"))
with ctx.takeover(): with ctx.takeover():
write_file(record_path(), encoded(record_with_required_steps(record))) write_file(record_path(), encoded(record_with_required_steps(record)))
@@ -468,6 +477,8 @@ def gate_blocks_controllers(ctx):
ev["open_wiz"] = wiz_summary(w) ev["open_wiz"] = wiz_summary(w)
ctx.check(w.get("gate") == "open" and not w.get("why") and w.get("fans_counted") is True, ctx.check(w.get("gate") == "open" and not w.get("why") and w.get("fans_counted") is True,
"the consent, the account, and the required steps do not open the gate: %s", wiz_summary(w)) "the consent, the account, and the required steps do not open the gate: %s", wiz_summary(w))
ctx.check(w.get("missing") == [], "GET /wiz names steps as missing with the required ones done: %s",
w.get("missing"))
running("with the required steps done") running("with the required steps done")
recm = recommended(w) recm = recommended(w)
missing = [s for s in RECOMMENDED_STEPS if s not in recm] missing = [s for s in RECOMMENDED_STEPS if s not in recm]
@@ -1500,7 +1511,8 @@ _FIRST_RUN_REQUIRES = ["setup.gate-blocks-controllers", "setup.advisories-rehash
"acceptance_done; POST /wiz/account with a temporary name and password, the " "acceptance_done; POST /wiz/account with a temporary name and password, the "
"account exists and the response sets the session cookie, and the gate holds " "account exists and the response sets the session cookie, and the gate holds "
"with the reason 'setup required: preferences, machine, cloud' and no " "with the reason 'setup required: preferences, machine, cloud' and no "
"controller. Then the required steps, the gate's reason naming what is left " "controller (the seed keeps the coolant flow calibration done). Then the "
"required form steps, the gate's reason naming what is left "
"after each: POST /wiz/preferences with the units as found; POST /wiz/machine " "after each: POST /wiz/preferences with the units as found; POST /wiz/machine "
"with the model as found (basic when unknown) and no TEC; POST /wiz/cloud as " "with the model as found (basic when unknown) and no TEC; POST /wiz/cloud as "
"found (enabled with the phrase and the homing as found, or off). With the " "found (enabled with the phrase and the homing as found, or off). With the "
@@ -1570,12 +1582,12 @@ def first_run_flow(ctx):
ctx.check(m.get("controller") == "gated", "the controller is %s with %s left", m.get("controller"), ctx.check(m.get("controller") == "gated", "the controller is %s with %s left", m.get("controller"),
", ".join(left)) ", ".join(left))
still_gated(REQUIRED_STEPS) still_gated(REQUIRED_FORM_STEPS)
ctx.log("the gate holds after the account: %s", wiz(fc).get("why")) ctx.log("the gate holds after the account: %s", wiz(fc).get("why"))
st, body = fc.post("/wiz/preferences", data={"ui_units": settings.get("ui_units") or "metric"}) st, body = fc.post("/wiz/preferences", data={"ui_units": settings.get("ui_units") or "metric"})
ctx.check(st == 200, "POST /wiz/preferences -> %s %s", st, body if isinstance(body, dict) else "") ctx.check(st == 200, "POST /wiz/preferences -> %s %s", st, body if isinstance(body, dict) else "")
still_gated(REQUIRED_STEPS[1:]) still_gated(REQUIRED_FORM_STEPS[1:])
machine = (w.get("machine") or {}) machine = (w.get("machine") or {})
model = machine.get("model") if machine.get("model") in ("basic", "plus", "pro") else "basic" model = machine.get("model") if machine.get("model") in ("basic", "plus", "pro") else "basic"
st, body = fc.post("/wiz/machine", data={"model": model, "tec": "0"}) st, body = fc.post("/wiz/machine", data={"model": model, "tec": "0"})
@@ -1586,7 +1598,7 @@ def first_run_flow(ctx):
ev["head"] = head ev["head"] = head
ctx.check(head.get("present") is True and bool(head.get("hw_id")), ctx.check(head.get("present") is True and bool(head.get("hw_id")),
"the machine facts do not see the head: %s", head) "the machine facts do not see the head: %s", head)
still_gated(REQUIRED_STEPS[2:]) still_gated(REQUIRED_FORM_STEPS[2:])
if settings.get("cloud_enabled") == "1": if settings.get("cloud_enabled") == "1":
data = {"enabled": "1", "phrase": SAFETY_PHRASE, data = {"enabled": "1", "phrase": SAFETY_PHRASE,
"homing_mode": settings.get("homing_mode") or "gfcloud"} "homing_mode": settings.get("homing_mode") or "gfcloud"}
+5 -2
View File
@@ -85,7 +85,9 @@ class RegistrationTests(unittest.TestCase):
self.assertIn(wid, setup.RECOMMENDED_STEPS) self.assertIn(wid, setup.RECOMMENDED_STEPS)
self.assertEqual(setup.FORM_WIZARDS, ("advisories", "account", "preferences", "machine", "cloud")) self.assertEqual(setup.FORM_WIZARDS, ("advisories", "account", "preferences", "machine", "cloud"))
# in the order the gate's reason names them # in the order the gate's reason names them
self.assertEqual(setup.REQUIRED_STEPS, ("preferences", "machine", "cloud")) self.assertEqual(setup.REQUIRED_FORM_STEPS, ("preferences", "machine", "cloud"))
self.assertEqual(setup.REQUIRED_STEPS, ("preferences", "machine", "cloud", "cooling.flow"))
self.assertIn("cooling.flow", setup.REQUIRED_STEPS)
def test_the_gate_test_starts_from_an_open_gate_and_the_intake_tests_from_a_count(self): def test_the_gate_test_starts_from_an_open_gate_and_the_intake_tests_from_a_count(self):
reg = self.reg reg = self.reg
@@ -191,9 +193,10 @@ class RecordTests(unittest.TestCase):
rec["wizards"]["switches"] = {"version": 1} rec["wizards"]["switches"] = {"version": 1}
rec["flags"] = {"machine": {"reason": "the head was replaced"}} rec["flags"] = {"machine": {"reason": "the head was replaced"}}
out = setup.record_with_required_steps(rec) out = setup.record_with_required_steps(rec)
self.assertEqual(sorted(out["wizards"]), ["cloud", "machine", "preferences"]) self.assertEqual(sorted(out["wizards"]), ["cloud", "cooling.flow", "machine", "preferences"])
self.assertEqual(out["wizards"]["machine"]["result"], {"model": "basic"}) # kept from the record self.assertEqual(out["wizards"]["machine"]["result"], {"model": "basic"}) # kept from the record
self.assertEqual(out["wizards"]["preferences"]["version"], 1) # written at version 1 self.assertEqual(out["wizards"]["preferences"]["version"], 1) # written at version 1
self.assertEqual(out["wizards"]["cooling.flow"]["version"], 1)
self.assertEqual(out["flags"], {}) self.assertEqual(out["flags"], {})
self.assertEqual(out["account"], rec["account"]) self.assertEqual(out["account"], rec["account"])
self.assertIn("switches", rec["wizards"]) # a copy self.assertIn("switches", rec["wizards"]) # a copy