From d40ebb090ece92b1ff10a5c8935e53db28a04069 Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Fri, 25 Sep 2026 14:22:50 -0400 Subject: [PATCH] cool_report_test: the homing runner is handed the report secret The runner case asserted that the homing runner the controller starts does not inherit GF_REPORT_SECRET. The runner is the reporter while a gfcloud $H holds the machine (the controller sends no report then), so the assertion described the defect: every camera home ran with nobody reporting, and the cooling engine's hung-controller dead-man stopped the session's first motion. The case now requires the runner to be handed exactly the controller's secret, and two new cases require that it be handed none when the controller was started with no secret (runner-none) or with one that is not 32 hex digits (runner-malformed). The harness's descriptions in scripts/bench/README.md and in the bench page's catalog say so. Proof: all 11 cases pass on the driver that hands the secret over (grblHAL-glowforge, "gfhome: the homing runner is handed the report secret"); the driver before it fails the runner case: "the homing runner was handed [], not '0123456789abcdef0123456789abcdef'". Acceptance: a host harness in the driver's CI; no catalog test runs it. forgetest/forgetest/bench.py changes one description string, which no test judges. --- forgetest/forgetest/bench.py | 3 ++- scripts/bench/README.md | 2 +- scripts/bench/cool_report_test.py | 20 ++++++++++++++------ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/forgetest/forgetest/bench.py b/forgetest/forgetest/bench.py index c57f84d..598413f 100644 --- a/forgetest/forgetest/bench.py +++ b/forgetest/forgetest/bench.py @@ -271,7 +271,8 @@ TOOLS = [ "forgectrl's listener: every report carries the secret the supervisor hands the controller at " "its spawn (GF_REPORT_SECRET), none does when there is none, a value that is not 32 hex digits " "(a CR LF with a header behind it included) never reaches the wire, and the homing runner the " - "controller starts does not inherit it. A CI harness (the grblHAL repo): needs the host-built " + "controller starts is handed it (it reports in the controller's place during a gfcloud $H), " + "and none when the controller has none. A CI harness (the grblHAL repo): needs the host-built " "null-sink controller, not the machine, so it is not a bench-page tool."}, {"id": "mcode-test", "title": "Package M-code barrier harness", "script": "mcode_test.py", "safety": "dry", "where": "host", "ported": False, "args": [], diff --git a/scripts/bench/README.md b/scripts/bench/README.md index d06ba78..5595a6f 100644 --- a/scripts/bench/README.md +++ b/scripts/bench/README.md @@ -34,7 +34,7 @@ page's takeover does that; from a host, stop them first. | `laser_lifecycle_test.py` | Host-side operator-armed-window lifecycle harness (null-sink controller): arm once per job with M5/M3 persistence, the M2 close, sender-change re-consent, the disarm grace counting down in Hold, and arm refusal under a blocking cooling verdict. Runs in the grblHAL repo's CI. | | `z_envelope_test.py` | Host-side Z envelope harness (null-sink controller): the Z soft limit belongs to the driver, not to `$20`, so the driver re-applies `sys.work_envelope`, `sys.homed` and `sys.soft_limits` for Z from the settings-changed chain. Checks that an unreferenced Z is collapsed to where the lens stands and blocks a move each way, that X and Y stay free, and that neither a `$20` write (the core clears the soft-limit mask in the setter) nor a `$132` write (which un-homes the axis as well) frees Z. Runs in the grblHAL repo's CI. | | `ctlport_test.py` | Host-side controller port and line-multiplexer harness (null-sink controller): a scripted sender that counts every `ok` and `error` runs beside the port's one client on `grbl.ctl`. A port jog's status goes to the port and never to the sender, a port error does not reach the sender's parser, a sender line cancels a port jog and gets its own `ok` (a line queued right behind the port's included), plus the refusals, the single client, a CR LF sender, a soft reset, and the client as the dead-man. `python3 ctlport_test.py `. | -| `cool_report_test.py` | Host-side cooling report harness (null-sink controller, a stand-in for forgectrl's listener on `FORGECTRL_PORT`): every `POST /cool/state` carries the secret the supervisor hands the controller at its spawn (`GF_REPORT_SECRET`) as `X-ForgeFIRM-Report`; with no secret the header is absent; a value that is not 32 hex digits, a CR LF with a header behind it included, never reaches the wire; and the homing runner the controller starts does not inherit the secret. `python3 cool_report_test.py `. | +| `cool_report_test.py` | Host-side cooling report harness (null-sink controller, a stand-in for forgectrl's listener on `FORGECTRL_PORT`): every `POST /cool/state` carries the secret the supervisor hands the controller at its spawn (`GF_REPORT_SECRET`) as `X-ForgeFIRM-Report`; with no secret the header is absent; a value that is not 32 hex digits, a CR LF with a header behind it included, never reaches the wire; and the homing runner the controller starts is handed the secret (it reports in the controller's place during a gfcloud `$H`), and none when the controller has none or a malformed one. `python3 cool_report_test.py `. | | `manual_home_test.py` | Host-side manual homing and motor release harness (null-sink controller, `GFSINK_DUMP` and `GFSINK_ATTR_LOG`): `$H` under `homing_mode = manual` ships nothing and declares X0 Y0; while the motors are released (`$MD`) every motion source is refused and ships nothing, `$X` and a soft reset do not unlock it, and only `$ME` and a manual `$H` write the energize. Imports its sender and port client from `ctlport_test.py`. `python3 manual_home_test.py `. | | `live_fire_drills.py` | **LIVE LASER** drills, on the board (the bench page) or from a LAN host (`GF_HOST`): `live_fire_drills.py [S] [F]` - `witness` (emission witness, lid-IR peaks vs the ambient baseline, HV current, job-based disarm on M2), `hold` (disarm grace in Hold), `faultpos` (armed job refuses a stale origin after an underrun), `ircut` (lid-IR characterization cut at S/F), `pthresh` (laser power-threshold ladder: 13 constant-power rungs from 2 % to 30 % of full on scrap; the lowest rung that marks is the tube's striking threshold and reads directly as the `$35` value - requires `$35` = 0 for the run), `dladder` (density ladder at a chosen base period), `pcurve` (laser performance-curve ladder: one 100 mm line per level at 10 mm/s under M3, the laser off between rungs and a mid-ladder rung repeated at the end; reads `pic/hv_current` and the head thermopile `head/beam_detect_analog` (a scatter detector in the beam path upstream of the final mirror, so it sees the beam, not the material) from sysfs at ~25 Hz on the board, brackets each rung on the controller's Run/Idle states, and reports per rung the current with a clipped-at-1023 flag, the thermopile delta over its laser-off baseline and in-line drift, then the normalized curve, monotonicity, a line fit with its threshold intercept and the repeat-rung drift; JSON record with the raw trace in the bench data directory; rungs follow `laser_power_model`, a comma list overrides; a curve measurement wants `$35` = 0), `dpatch [F] [pitch] [length]` (depth witness for the density dose curve: two rows of small serpentine-filled patches, row A CW at feeds giving relative doses 1.0 to 0.25 of the reference feed, row B at the reference feed at 100/80/60/45/30 % density; the operator matches each row-B patch to the row-A patch of equal depth, which reads the density's light fraction off the material beside the thermopile's prediction; JSON record), `m4feeds [S] [F1] [F2]` (the density time base across feeds: one out-and-back line pair per feed at the same S under M4 density, one armed run; the operator reads within-line evenness and reversal darkness at both feeds - M4's velocity scaling is what holds dose per mm through the accel), `m4corner [S] [F]` (M4 velocity-scaled power into corners: a corner-heavy vector pattern at 30 % under M4 density, one armed run; the operator confirms every commanded segment marks - the floor makes a dropout unreachable - and the drill asserts the arm report, one discharge window and dark after), `m5dark` (the rapids after an M5 ship dark: one 20 mm line at M3 S400, M5, dwell, rapid back, dwell, rapid forward; PASS when the 25 Hz current trace shows one discharge segment and reads dark after the M5 and `laser_on_sampled` never re-lights; the catalog's `laser.m5-rapid-dark` is its port), `flowload` (cooling under laser load, one armed run per invocation, the conf keys it writes put back at the end, the pump never commanded off: `t1` reproduces the flow-check trip with the check on at its defaults and two 30 x 4 mm CW fills at F1500 starting on the press with no dark dwell, and reports the engine's rise/dT verdict beside the 25 Hz trace of both coolant sensors, the current, the digital witness and the heater output in 5 s bins across the window, with the shape at fire start; `t2 [pct]` runs with the check off and one fill of about `secs` lit seconds at CW or at `pct` density, and reports the lag to each sensor, the rise per raw-second of `hv_current` and what a full 50 s window would add against the 1.6 C margin; `fit` fits rise against dose over every t2 record; JSON records), `expstop` (armed kill on the expected-stop path; needs the panel token - `GF_TOKEN`, or the board's token file) and `ctrlstart` (the separate controller restart after it). Every drill waits for the operator's physical arm press; eye protection, fire watch, extinguisher, and exhaust are mandatory. | | `pacing_test.py` | Protocol-loop pacing check (runs on the board, dry motion): idle and parked-in-Hold states are coarse-paced, active motion is tight-paced, and a feed-hold/resume mid-move preserves position with no feeder starve. | diff --git a/scripts/bench/cool_report_test.py b/scripts/bench/cool_report_test.py index 9850a0b..ca920e5 100644 --- a/scripts/bench/cool_report_test.py +++ b/scripts/bench/cool_report_test.py @@ -16,8 +16,11 @@ the null-sink controller against it, and reads every report as it arrives. malformed a value that is not 32 hex digits (short, a letter out of range, a CR LF and a header of its own behind it) is never sent, and nothing of it reaches the wire - runner the homing runner the controller starts does not inherit the - secret, and the reports still carry it afterwards + runner the homing runner the controller starts is handed the secret (it + reports in the controller's place while a gfcloud $H holds the + machine, and the controller is quiet then), and the reports + still carry it afterwards; a controller with no secret, or with + one that is not 32 hex digits, hands the runner none Usage: cool_report_test.py """ @@ -125,7 +128,7 @@ class Controller: shutil.rmtree(self.workdir, ignore_errors=True) -def run(name, secret, check, runner=False): +def run(name, secret, check, runner=False, handed=None): lis = Listener() ctl = Controller(lis, secret, runner_cmd=("env > %s" % "RUNNER_ENV") if runner else None) try: @@ -142,8 +145,11 @@ def run(name, secret, check, runner=False): time.sleep(0.3) with open(env_file) as f: runner_env = f.read() - if "GF_REPORT_SECRET" in runner_env or SECRET in runner_env: - fail("[%s] the homing runner inherited the report secret" % name) + got = [ln.split("=", 1)[1] for ln in runner_env.splitlines() if ln.startswith("GF_REPORT_SECRET=")] + if got != ([handed] if handed else []): + fail("[%s] the homing runner was handed %r, not %r" % (name, got, handed)) + if not handed and SECRET in runner_env: + fail("[%s] the secret reached the homing runner another way" % name) if "GF_STATE_DIR" not in runner_env: fail("[%s] the runner's environment was not read: %r" % (name, runner_env[:80])) s.close() @@ -184,7 +190,9 @@ def main(): SECRET[:16] + "\r\nX-Evil: 1\r\nX-Pad: 12", SECRET[:16] + "\r\nX-Evil: 1\r\nX:1"), 1): # the last is 32 long run("malformed-%d" % i, bad, without) - run("runner", SECRET, with_secret, runner=True) + run("runner", SECRET, with_secret, runner=True, handed=SECRET) + run("runner-none", None, without, runner=True) + run("runner-malformed", SECRET[:16] + "\r\nX-Evil: 1\r\nX:1", without, runner=True) print("cool_report_test: all passed")