The dark stretch after the M5 judged emission from the samples whose /status carried a laser block. forgectrl leaves that block out of a served /status when cnc/laser_on_sampled or cnc/laser_pgood_sampled does not read, and sample() records the count as None, which the relit judge (any sample with emission) and the HV judge (the maximum over the readings) both pass: a stretch whose count went unread after the dark point passed as dark. The kernel latches the count once per ~1 s window and holds it through the next, so a window with no read in it is a window in which a fire is not witnessed. The stretch also ended at the first disarmed sample, before the count for the window in flight at the close had latched, so the rapid's last second was never read. read_gaps(samples, key) reports, over a trail, the samples that carry a reading, the ones that do not, and the longest stretch of seconds between two readings, with the trail's ends as edges: an unread lead or tail is a gap, and a trail with no reading is one gap, its whole span. The judge requires RAPID_MIN_READS (4) readings of the count with no gap over EMISSION_READ_GAP_S (0.9 s), inside the EMISSION_WINDOW_S (1.0 s) latch period, and the same floor of readings for the HV current. Both checks come before the relit and HV-dark checks, so those never judge an unread stretch. sample_to_disarm keeps sampling for two windows after the first disarmed sample: the count for the window in flight at the close latches up to one window later and is held through the next, and the gap rule asks for a read of it in that tail, measured to the last sample served. The timeout bounds the wait for the close alone. The close is the first disarmed sample, which the disarm timing and the span are measured to; the evidence's rapid block carries emission_reads and hv_reads, the log line says how many samples carried the count and the longest unread stretch, and the description names the rule. Proof: tests/test_laser_dwell.py RapidReadTests, the Unit tests step of forgetest-ci.yml (python -B -m unittest discover -s tests), 10 tests OK on Windows and on Linux: every sample read (gap 0.2 s), one miss (0.4 s) and a 0.8 s hole are inside the rule; a 1.2 s hole, the block gone after the dark point (1 of 25 read, gap 4.8 s), the block gone for the last 1.6 s, and a one-sample stretch fail it. pyflakes reports no undefined name. Negative control on a scratch copy: with the trail's tail dropped from the edges, the last-window case and the block-gone case report no gap and their two tests catch it; with the gap bound raised to 1.3 s, the 1.2 s hole passes the rule and its test catches it. Acceptance: laser.emission-witness's fingerprint moves (its own text and laser.py's shared text), and laser.armed-kill and laser.pause-resume-lid-cancel move through the shared text; no other suite module imports .laser at module level. laser.emission-witness is always run, and the next campaign judges the rapid stretch's read cadence on the bench (about 5 Hz in the last campaign's record).
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.