The sheet handed the bench actuator every press after the operator's presence press, and the actuator pressed every time - into nothing. On the bench reference all six cards were pressed before the card asked: the frame by 8 s, the focus by 33, the floor by 10, the dose curve by 8, the corner by 9, and the flow-load card by 69. The operator then pressed all six himself, which is the opposite of what the ready gate promises. arm_press() waits for hw.button_lit(), which is true when any button LED is on, and burn() started that wait before run_check had even started the wizard. The button is lit through parts of a card that are not the arm - the lens reference, the program on its way to the controller - so the wait ended at once, the press landed before the job waited for it, and the thread was gone by the time the real cue came. forgectrl uses the same predicate but only inside the job's own sample callback, with the tube still dark, where a lit button does mean the arm. The machine already says when it wants the press: a live check opens a `press` wait prompt at that moment, and run_check sees every prompt. It now presses there, through a new Ctx.press_now() - no LED read, no waiting thread, no timing guess. That retires the per-card lit-timeout column of CARDS, which existed only to give the flow-load card's coolant settle enough room for a wait that was reading the wrong thing. The four grbl-driven arm_press() callers in laser.py and cooling.py are left as they are: they call it with the machine idle and its LEDs dark, so the level read is the edge they mean. The same shape would bite them if that ever stopped being true. Pin: forgectrl 0.1.15 (6e69479, the flow-load tail ending at the coolant peak and the way out of a finished setup page). Host-proven: 357 forgetest unit tests, including two new ones - the actuator presses on the prompt with button_lit stubbed false throughout, so the LED is provably not consulted, and the press falls to the operator without a takeover.
forgetest - the ForgeFIRM release acceptance tool
The daemon behind http://<machine>:8090/ on the dev image: runs the
acceptance catalog against the machine, keeps the append-only result log,
decides which results still apply to the image that is running, exports
the release artifact scripts/release.sh gates on, and serves the bench
diagnostics page. The contract - catalog, campaigns, fingerprints,
inheritance, the gate, the coverage rule - is
the Acceptance page of the documentation site.
Run the host tests
cd forgetest
python3 -m unittest discover -s tests -v
Run the daemon on a workstation (against a mock or a manifest file)
FORGETEST_DATA=/tmp/ft FORGETEST_MANIFEST=../tree-manifest.json \
FORGECTRL_URL=http://<machine> python3 -m forgetest --port 8090
scripts/manifest-from-tree.py produces tree-manifest.json from the recipe
pins; the coverage lint is python3 -m forgetest.coverage --manifest ....
Environment
| Variable | Default | Purpose |
|---|---|---|
FORGETEST_DATA |
/data/forgetest |
results.jsonl, bench.jsonl, token, export/ |
FORGETEST_MANIFEST |
/etc/forgefirm-manifest.json |
the image manifest |
FORGETEST_PORT, FORGETEST_HOST |
8090, 0.0.0.0 | listener |
FORGETEST_BENCH_DIR |
/usr/share/forgetest/bench |
the installed bench scripts |
FORGETEST_BENCH_DATA |
<FORGETEST_DATA>/bench |
passed to bench tools: where they keep their data files (with GF_HOST=127.0.0.1 and the panel token in GF_TOKEN) |
FORGETEST_MARKER |
/run/forgetest.active |
takeover marker |
FORGECTRL_URL, FORGECTRL_TOKEN_FILE |
http://127.0.0.1, /data/forgefirm/panel.token |
forgectrl client (HTTP; the token authorizes writes from the board) |
FORGECTRL_TLS_URL |
https://127.0.0.1 |
forgectrl over HTTPS (self-signed, unverified), for the login test |
GF_SYSFS_ROOT |
/sys/glowforge/ |
kernel module sysfs |
GRBL_HOST, GRBL_PORT |
127.0.0.1, 23 | Grbl TCP |
Adding a test
Register it in the subsystem module under forgetest/suite/ with
@test(...): id subsystem.name, kind, hardware, mode (the controller
mode the test needs; the runner switches to it first), covers,
requires, always, steps. The body gets a Context (log, check, fail,
prompt, confirm, instruct, sleep, evidence, forgectrl, sysfs,
grbl, takeover). Return normally for PASS, raise runner.Failed for
FAIL. Then run the unit tests and the coverage lint.