forgetest: the latch-unlock gate waits for the safety chain to release

The gate that refuses a latch unlock while the chain may hold HV_ENABLE
up (charge_pump_alive or a pulse engine not idle) ran at the start of
phases B, U and K3 of kernel.fire-line, within a second of the previous
phase's run. A run feeds the charge-pump watchdog every 200 ms and the
one-shot holds ALIVE for 0.45 s after the last feed, so the gate read
alive=1 and refused: the first bench run of the gate (forgefirm
64f552fc; the laser_pgood gate before it was vacuous) failed phase B on
image 20260902144848.

wait_hv_off() polls the chain for up to 3 s before it refuses, logs the
release when it was not immediate and records every wait in the
evidence (hv_release_s). require_hv_off and check_hv_off use it. The
bench scripts that copy the gate (fire_test.py per phase,
gate_a_kernel_drills.py K3 after K2) get the same wait.

Bench: kernel.fire-line PASS on 20260902144848 with the chain released
after 0.41 s at each of the three phase boundaries. Host:
tests/test_kernel_suite.py covers release inside the window, a chain
held past it, and a chain already off.

Catalog consequence: the kernel.* implementation hashes move (the suite
file changed); the kernel set re-ran and passed.
This commit is contained in:
ScottW514
2026-09-02 15:39:24 -04:00
parent 133b61a062
commit b3efab9c46
4 changed files with 119 additions and 4 deletions
+26 -2
View File
@@ -164,10 +164,34 @@ def hv_off():
return None
HV_RELEASE_S = 3.0
def wait_hv_off(ctx, timeout_s=HV_RELEASE_S):
"""hv_off_reason() once the chain has released, or the reason that still
stands after timeout_s. A run feeds the charge-pump watchdog every
200 ms and the one-shot holds ALIVE for 0.45 s after the last feed (the
feed's soft timer can add one more feed after the state leaves
running), so a phase that follows a run finds the chain still up for
under a second. The wait covers that release and nothing more; the
time it took is logged when it was not immediate and kept in the
evidence."""
t0 = time.time()
why = hv_off_reason()
while why is not None and time.time() - t0 < timeout_s:
ctx.sleep(0.05)
why = hv_off_reason()
dt = round(time.time() - t0, 2)
ctx.evidence.setdefault("hv_release_s", []).append(dt)
if why is not None or dt >= 0.1:
ctx.log("hv off: %s after %.2f s", "released" if why is None else "still held", dt)
return why
def require_hv_off(ctx):
"""The same rule at the start of the run (the precheck ran a moment
earlier; the machine must still agree)."""
why = hv_off_reason()
why = wait_hv_off(ctx)
ctx.evidence["charge_pump_alive"] = rd("cnc/charge_pump_alive")
ctx.evidence["kernel_state"] = rd("cnc/state")
ctx.check(why is None, "%s - refusing the latch unlock", why)
@@ -175,7 +199,7 @@ def require_hv_off(ctx):
def check_hv_off(ctx):
"""The hard check right before an unlock (no prompt: forgectrl is down)."""
why = hv_off_reason()
why = wait_hv_off(ctx)
ctx.check(why is None, "%s - refusing the latch unlock", why)