The test's cleanup jogged the head back to the camera home through the panel's relative jog, the increment taken from the port's position. The core refused it every time (error:15, "the move would leave the work envelope"), eight tries each run, and the test logged "the head is not back at the home" and passed. The takeover that puts the setup record back restarts the controller, which zeroes the kernel counters where the head stands, so the baseline read (0, 0, 0) and called the machine clean. On the bench reference the head was left 10 mm from the home in X and in Y by every run of 2026-09-23 that got that far (three PASSes and one FAIL, images 20260922225653, 20260923084705, 20260923220034). Why the jog was refused: the port reports the step counters, while the core adds a relative jog to its own parser position. The bed check's last jogs, sized from the counters, leave the two apart by up to half a step. The arithmetic reproduces every logged value: the parser held X 60.1 after the answers, the port read 60.099, the check jogged -50.099 toward X 10, so the parser took 10.001, which the step grid (213.33 steps/mm) turned into 2134 steps, read back as 10.003. The test's return of -10.003 then targeted X -0.002, and a camera home's envelope begins at exactly 0, so the core refused the whole jog. No product path returns the head by a relative jog sized from the counters; the panel's Jog card sends fixed steps. Now the head goes back in one jog from a Grbl client to the home in machine coordinates ($J=G90 G53, 0.5 um inside the envelope's start, far under half a step), and the run fails unless the port reads the head on the home's step. That reading is taken before the restart, because nothing after it can see a head left out. Bench reference, image 20260923220034 with the suite file bind-mounted: a negative control that aims the return 1 mm off the home FAILs with "the head is not back at the home (0.0, 0.0, 3.08): [0.998, 0.998, 3.08]" while the baseline still says clean; the fix PASSes with "$J=G90 G53 X0.0005 Y0.0005 F1200 -> ok" and the head at [0.0, 0.0, 3.08]. forgetest's unit tests 454 OK; the coverage lint passes with --enforce on the image's manifest.
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.