setup suite: the port wait, and the first run's accounts on a failure

Two harness faults found on the bench reference in the campaign on image
20260923084705.

setup.gate-blocks-controllers probed the Grbl port once, at the moment
GET /mode first read the controller running. The supervisor reports
running from the fork and the controller binds TCP 23 a few tens of
milliseconds later, so the probe failed whenever the 0.25 s poll landed
inside that window ("the Grbl port is closed with the controller
running"). Timed on the board with the controller restarted through
forgectrl: /mode read running at 0.066 s, the port accepted at 0.087 s.
The test now waits up to 10 s for the port (50 ms polls) and records
how long it took (port_open_after_s). PASS on the bench (0.001 s).

first_run() replayed the system accounts (forgefirm-users reload) and
removed the temporary home only after its block succeeded. When the
block failed, the records came back but /etc/passwd kept the temporary
account in place of the operator's, and image.health failed after it
(the record's account missing from /etc/passwd). The replay and
the home removal now run in a finally. Proof on the bench: the failing
case run on purpose (setup.first-run-flow against the image's own
forgectrl, which refuses a home by hand at the cloud step) leaves the
operator's account the one account from uid 1000 up and no temporary
home; the same failure before the fix left the temporary account in
/etc/passwd and its home in /data/forgefirm/home.

forgetest's unit tests: 452 OK. setup.first-run-flow and
setup.gate-blocks-controllers PASS on the bench with this file mounted.
This commit is contained in:
ScottW514
2026-09-23 16:29:55 -04:00
parent 853f862f03
commit 82c354d70e
+30 -21
View File
@@ -427,8 +427,12 @@ def gate_blocks_controllers(ctx):
ev["override_mode"] = m
ctx.check(took is not None and isinstance(m, dict) and m.get("controller") == "running",
"the controller did not come back under the override: %s", m)
ctx.check(hw.grbl_port_open(timeout=5), "the Grbl port is closed with the controller running")
ctx.log("override: the controller is back (%s)", m)
# /mode reads running from the fork; the controller binds its port
# a few tens of milliseconds later
opened = ctx.wait_for(lambda: hw.grbl_port_open(timeout=1), 10, poll=0.05)
ev["port_open_after_s"] = opened
ctx.check(opened is not None, "the Grbl port is closed with the controller running")
ctx.log("override: the controller is back (%s), the Grbl port accepts after %.3f s", m, opened)
# the real record is back under a restart; the override reads as found
after = wiz(fc)
@@ -1419,25 +1423,30 @@ def first_run(ctx):
except OSError:
homes_before = set()
state = {"temp_account": None}
with Restore(ctx, FIRST_RUN_SETTINGS):
with installed(ctx, {record_path(): seed, users_path(): None, override_path(): b""}):
w = wiz(fc)
ev["fresh"] = wiz_summary(w)
ctx.check(w.get("first_run") is True and not (w.get("users") or {}).get("exists")
and w.get("advisories_complete") is False and w.get("acceptance_done") is False,
"the machine does not read as a first run: %s", wiz_summary(w))
m = mode(fc)
ctx.check(m.get("controller") == "gated", "the controller is %s on a first run", m.get("controller"))
yield state
# the real records are back; the system accounts follow the record
rc, out = hw.initd("forgefirm-users", "reload")
ev["users_reload_rc"] = rc
ctx.log("forgefirm-users reload -> rc %s %s", rc, out.strip()[:200])
temp = state.get("temp_account")
if temp and temp not in homes_before and temp != ev["account_before"]:
import shutil
shutil.rmtree(os.path.join(homes, temp), ignore_errors=True)
ctx.log("removed the temporary home directory of %r", temp)
try:
with Restore(ctx, FIRST_RUN_SETTINGS):
with installed(ctx, {record_path(): seed, users_path(): None, override_path(): b""}):
w = wiz(fc)
ev["fresh"] = wiz_summary(w)
ctx.check(w.get("first_run") is True and not (w.get("users") or {}).get("exists")
and w.get("advisories_complete") is False and w.get("acceptance_done") is False,
"the machine does not read as a first run: %s", wiz_summary(w))
m = mode(fc)
ctx.check(m.get("controller") == "gated", "the controller is %s on a first run",
m.get("controller"))
yield state
finally:
# the real records are back, whether or not the block passed; the
# system accounts follow the record, and a temporary account the
# block created goes with its home
rc, out = hw.initd("forgefirm-users", "reload")
ev["users_reload_rc"] = rc
ctx.log("forgefirm-users reload -> rc %s %s", rc, out.strip()[:200])
temp = state.get("temp_account")
if temp and temp not in homes_before and temp != ev["account_before"]:
import shutil
shutil.rmtree(os.path.join(homes, temp), ignore_errors=True)
ctx.log("removed the temporary home directory of %r", temp)
after = wiz(fc)
ev["after"] = wiz_summary(after)
ctx.check((after.get("users") or {}).get("name") == ev["account_before"],