Commit Graph
762 Commits
Author SHA1 Message Date
ScottW514 56aa164708 Pin forgeext 0.1.4 on the pushed head
- forgeext e09f7b8 -> bb75b23 (0.1.3 -> 0.1.4): sandbox_test opens the
  machine's own files from inside the sandbox - /dev/glowforge,
  /data/forgefirm/forgefirm.conf, /data/forgefirm/setup.json - and
  requires EACCES of each (ENOENT is no pass; a file the host lacks is
  made for the run, readable by every account, and removed after).
  Test-only; nothing the image installs changes.

Proof: forgeext's CI run on bb75b23 (38780750106) is green on a kernel
with Landlock ABI 7; its log shows the three files made for the run and
each open refused with EACCES (sandbox_test, 0 failures). bitbake -c
fetch forgeext from this pin succeeds (2 tasks).
2026-10-15 13:57:52 -04:00
ScottW514 df5ae151ab Pin forgeext 0.1.3 on the pushed head
- forgeext 31cdb40 -> e09f7b8 (0.1.2 -> 0.1.3): run_test's last phase
  runs the host under init/forgeext.init's own wrapper; a SIGKILL of the
  host takes its service with it at once, the wrapper starts the host
  again, and the service runs again under a new pid. Test-only; nothing
  the image installs changes.

Proof: forgeext's CI run on e09f7b8 (38777839870) is green on a kernel
with Landlock ABI 7, and the phase's six checks passed in it (run_test,
0 failures). bitbake -c fetch forgeext from this pin succeeds (2 tasks).
2026-10-15 13:37:42 -04:00
ScottW514 10ae7bf39c Pin forgectrl 0.1.33 on the pushed head
- forgectrl 22a2cfc -> 6c2bb38 (0.1.32 -> 0.1.33): cam.c resolves the
  lid and head lamps under GF_SYSFS_ROOT (unset: /sys/glowforge/, the
  paths as they were), and host_routes.sh proves the lid lamp follows
  lid_lamp_idle: a level is written, a refused value writes nothing, a
  cleared key writes the default 236.

Proof: forgectrl's CI run on 6c2bb38 (38769524512) is green, and
host_routes.sh ran in it. bitbake -c fetch forgectrl from this pin
succeeds (2 tasks). The tree manifest from the pins holds forgectrl at
6c2bb38 (229 files); the coverage lint finds no uncovered path and no
row problem (47 tests); check-ui-vendor finds the shared UI files
identical; test_tree_manifest passes; the gate self-check refuses the
empty artifact (rc 1).
2026-10-15 12:50:22 -04:00
ScottW514 c40dc6c24c Catalog: setup.machine-name leaves for the rootfs lint and test_image.py
Its checks, and what holds each now:
- the name is forgefirm-<the last four hex digits of the MAC>, wlan0
  first and eth0 on a machine with no WiFi, and the console banner
  offers the addresses and nothing else: test_image.py's
  MachineNameTests run forgefirm-hostname and forgefirm-banner as the
  image runs them, on a scratch /sys/class/net;
- /etc/hostname is the bind-mounted copy: image.health;
- the DHCP request sends the name, and no mDNS responder is on the
  image: the rootfs lint's dhcp-name and mdns rules (08c7df9), which
  image 20261015142442 ran in do_rootfs and which passed campaign
  c-20261015143634-68f0, 48 of 48.
It covered no component path, so no HOST_TESTED row. Its helpers
mac_suffix and cmdlines had no other caller and go with it, with
test_setup_suite's cases of them and of its registration; setup.py's re
and lan_ip imports went with them.

Fingerprints: every test in setup.py moves with the module text.

Proof: the coverage lint over the tree manifest from the pins finds 0
uncovered paths and 0 host-tested row problems (47 tests); pyflakes on
the touched files is clean; forgetest's unit suite under Linux passes
(329).
2026-10-15 11:28:25 -04:00
ScottW514 08c7df9cbb rootfs-lint.sh: the image content fixed at build time, checked as the last rootfs step
A new script beside the image recipe, run by both images as the last
rootfs step (ROOTFS_POSTPROCESS_COMMAND:append, after every +=, the dev
recipe's and debug-tweaks' included); a refusal is bbfatal. It checks
every rule and prints every failure before it exits. FORGEFIRM_IMAGE_KIND
(release by default, dev in forgefirm-image-dev.bb) picks the sshd policy.
The rules:

- syslog: /usr/sbin/rsyslogd is on the image, /etc/init.d/syslog starts
  it and runlevel 5 starts that script; no syslogd or klogd is linked in
  a bin directory and no init script runs one. This is the image half of
  what logs.routing checked on the board; that test left in 01f5991 with
  nothing else holding it.
- sshd: sshd_config and sshd_config_readonly take passwords
  (PasswordAuthentication yes) and send no banner; on a release image
  PermitRootLogin no and PermitEmptyPasswords no; sshd_config.d sets none
  of the four, since an included value comes first. setup.ssh-until-
  reboot checked the first two on the board and left in 01f5991;
  release.sh gates the release pair at release time only.
- dhcp-name: the wlan0 dhcp stanza's udhcpc_opts carries
  -x hostname:$(hostname).
- mdns: no /etc/avahi, avahi-daemon or libnss_mdns, and no mdns source
  on nsswitch.conf's hosts line.
dhcp-name and mdns are the image half of setup.machine-name, whose
scripts test_image.py's MachineNameTests already run.

test_image.py's RootfsLintTests run it on a fixture tree: the tree that
holds every rule passes as either image, the BSP layer's own interfaces
file passes, and one drift per rule fails naming the rule (a busybox
syslogd link; a Banner, on either image; root and an empty password,
refused on a release image and passed on a dev one; a dhcp stanza with
no name; /etc/avahi).

Proof: test_image.py under Linux, 21 OK; pyflakes clean. On image
20261015134521's two rootfs trees from the build, the release tree
passes as release, the dev tree passes as dev, and the dev tree linted
as release is refused on PermitRootLogin and PermitEmptyPasswords in
both sshd files. bitbake -e places rootfs_lint last in both images'
ROOTFS_POSTPROCESS_COMMAND, with FORGEFIRM_IMAGE_KIND release and dev.
The layer content hash moves: the next image cannot inherit a campaign.
2026-10-15 10:19:54 -04:00
ScottW514 61652e237d Catalog: exthost.motion-job leaves, after it passed on forgeext 31cdb40
exthost.motion-job stayed in 01f5991 as the bench proof of
machine_host_token() on the default token path. forgeext 31cdb40, the
revision c177aab pins, makes the token file overridable
(FORGEEXT_HOST_TOKEN_FILE) and keeps /run/forgefirm/ext-host.token when
the variable is unset or empty. The recipe's binary from that revision
(sha256 15ca6003b59f69fb9eadf686094430fbbd25e7cc177230a0417738484fe3427e)
ran on the bench reference with the variable unset, and
exthost.platform, exthost.service, motion.jog-roundtrip and
exthost.motion-job passed on it (campaign c-20261015103714-0c91): the
package's job reached the machine as job:org.forgetest.reference through
the credential read from the default file.

Its paths, now:
- forgectrl src/jobpost.*: tests/jobpost_test.c, POST /job's multipart
  reader (row added).
- forgectrl src/tokens.*: tests/tokens_test.c, the scoped tokens and
  the extension host's credential file (row added).
- forgectrl src/jobrun.*: still covered by the burn tests.
- forgeext src/api.*, src/machine.*, src/run.*: still covered by
  exthost.service's forgeext/**.
Both rows name tests forgectrl's build.yml runs at the pinned 22a2cfc.
No other catalog test required exthost.motion-job. exthost.py's hw
import went with it.

Fingerprints: every exthost test moves with the module text. The next
image owes exthost.service and exthost.armed-freeze for forgeext's
source change.

Proof: the coverage lint over the tree manifest from the pins finds 0
uncovered paths and 0 host-tested row problems (48 tests); pyflakes on
the touched files is clean; forgetest's unit suite under Linux passes
(327).
2026-10-15 09:38:20 -04:00
ScottW514 c177aabefa Pin forgeext 0.1.2 on the pushed head
- forgeext 2d0391e -> 31cdb40 (0.1.1 -> 0.1.2): machine_host_token()
  opens FORGEEXT_HOST_TOKEN_FILE when it is set and not empty, and
  /run/forgefirm/ext-host.token otherwise (ebdbf59); the rest is host
  tests (run_test's sender claim, event stream, storage quota and
  memory.max; install_test's served page; the trimmed kit, ffx and
  allowlist cases).

Proof: the recipe's forgeext, built from 31cdb40 (bitbake forgeext
through an untracked externalsrc overlay; packaged /usr/bin/forgeext
sha256 15ca6003b59f69fb9eadf686094430fbbd25e7cc177230a0417738484fe3427e),
replaced /usr/bin/forgeext on the bench reference's image
20261015101725 and ran with no FORGEEXT_HOST_TOKEN_FILE in its
environment. exthost.platform, exthost.service, motion.jog-roundtrip
and exthost.motion-job passed on it, in campaign c-20261015103714-0c91:
the package's job reached the machine as job:org.forgetest.reference
through the host's credential read from the default token file, 6 lines
sent and acknowledged, the head moved, the latch locked and no emission
throughout. The flashed binary was put back after the run. bitbake -c
fetch forgeext resolves this pin.
2026-10-15 09:31:54 -04:00
ScottW514 01f59914e8 Catalog: forgectrl.panel-serves, setup.ssh-until-reboot, setup.first-run-flow and logs.routing leave
Each leaves for a host proof that forgectrl's build.yml runs at the
pinned 22a2cfc, and that run passed (38728407038):

- forgectrl.panel-serves: tests/host_routes.sh serves the panel page
  bundled, with the theme and the save bar and the policy frame-src
  'none', and answers /status, /cam/status and the dose-curve reads.
  Rows: src/ui.h.
- setup.ssh-until-reboot: tests/host_routes.sh, GET and POST
  /system/ssh with the flag in the run directory and sshd's init a
  stand-in told start and stop; enable=2 refused. Rows: src/paths.h.
- setup.first-run-flow: tests/host_setup.sh, the first run in the
  page's order: the four documents accepted at their hashes, the press
  step breathing teal on a scratch LED tree, the press taken as the
  acceptance, the account and its cookie, the gate's reason after each
  form step, the run completed. Rows: src/advisories.*, src/button.*,
  src/led.*, src/hooks.h and the four first-run documents, one row
  each (docs/advisories/extensions.md stays with exthost.service);
  src/sheetid.* to tests/sheetid_test.c, the golden ids, because
  host_setup.sh does not read the sheet id.
- logs.routing: tests/fflog_e2e.sh, the image's rsyslog.conf and the
  rendered rules, each logger in its own file at its level, a stray
  tag in system/, the kern facility in kernel/. Rows: src/fflog.*.

logs.py held logs.routing alone and goes with its import. The five
camera tests no longer require forgectrl.panel-serves.
test_setup_suite.py keeps setup.account-login and setup.machine-name in
its ids; the cases that described setup.first-run-flow (takeover,
fixture, covers) go, with the cases of the helpers that left with the
two tests (breathes_teal, first_run_record, sshd_policy).
test_manifest's behavioral-path case runs the report with no
host-tested rows: the real table now names the first-run documents,
and the case is about the allowlist.

exthost.motion-job stays. It is the only catalog test that grants
motion.job and posts /v0/motion/job, the path that reads
machine_host_token() on the default token file, and it stays the bench
proof of that path until an image carries forgeext's token-file change.
src/jobpost.* stays in its covers, with no row.

Fingerprints: every test in forgectrl.py, setup.py and camera.py moves
with the module text. The next image cannot inherit a campaign.

Proof: the coverage lint over the tree manifest from the pins finds 0
uncovered paths and 0 host-tested row problems (49 tests); pyflakes on
the touched files is clean; forgetest's unit suite under Linux passes.
2026-10-15 08:48:40 -04:00
ScottW514 87155be433 Pin forgectrl 0.1.32 on the pushed head
- forgectrl c9e3347 -> 22a2cfc (0.1.31 -> 0.1.32): fflog_e2e runs its
  private rsyslogd from a copy of the binary, out of reach of Ubuntu's
  AppArmor profile for /usr/sbin/rsyslogd. The diff is
  tests/fflog_e2e.sh alone; nothing the image installs changes.

At c9e3347 the CI run stopped at fflog_e2e, so host_routes.sh and
host_setup.sh, later in the same job, never ran there. The rows the
next catalog commit adds name those two scripts and fflog_e2e.sh, and
the coverage lint reads build.yml at this pin, so the pin is the
revision whose run executed them.

Proof: forgectrl's CI run on 22a2cfc (38728407038) is green;
fflog_e2e, host_routes, host_setup and jobpost_test passed in it.
2026-10-15 08:13:41 -04:00
ScottW514 893602e44c Pin grblHAL-glowforge 0.1.21 on the pushed head
- grblHAL-glowforge b34636f -> badc832 (0.1.20 -> 0.1.21): the host
  tests trimmed to one proof per rule, with the cases the cut dropped
  back (the tray, session-budget and envelope clamps, the switch bits
  as numbers, an M5 then an M3 inside the armed window); a host build
  may shorten the M-code answer wait (-DGFMCODE_WAIT_S, CI's 5 s, for
  mcode_test.py's no-answer case) while the image keeps 30 s; the
  homing comment covers a setup step's hand-back. The driver the image
  builds is unchanged in behavior.

It follows 205f9f2 on its own because the driver's CI runs this
repository's harnesses from master, and its mcode_test expects the
5 s host build.

Proof: image 20261015101725 carried the driver built from badc832 (its
manifest's srcrev), and campaign c-20261015103714-0c91 passed all 53
catalog tests on it with none inherited. bitbake -c fetch
grblhal-glowforge from this pin succeeds (2 tasks). The tree manifest
from the pins holds grblhal-glowforge at badc832 (176 files); the
coverage lint over it finds no uncovered path, no empty entry and no
host-tested row problem (53 tests); test_tree_manifest passes. The
driver's CI on badc832, with this repository's harnesses at 205f9f2,
passes.
2026-10-15 07:41:32 -04:00
ScottW514 205f9f22f2 Pin forgectrl 0.1.31 and forgefirm-app 0.1.32 on the pushed heads, and meta-openglow in the kas lock
- forgectrl c274f19 -> c9e3347 (0.1.30 -> 0.1.31): setup is required
  up to the intake fans and recommended after, the coolant flow
  calibration is a required step, the TEC check gives way to the
  operator's answer, the head's id is head/info's "id" line, the
  intake fan count is decided in wizcalc, and the burns hand the lens
  back referenced; every start logs its firmware and slot, and every
  update job is logged; the beam sensor is recorded and logged; the
  panel's emission help parses again. The daemon starts on a host with
  its machine paths, release URLs and controller binaries from the
  environment, and the host cases for the routes, the tokens, the
  setup, the logs, the extension host and the cooling engine run
  through it in CI.
- forgefirm-app 021323d -> 0f59c54 (0.1.31+git -> 0.1.32+git): the
  python3-gfhardware head, host tests only; it moves with the BSP's
  python3-gfhardware pin.
- kas lock: meta-openglow f012d8e -> b7626be, the head that pins the
  kernel module f924cd9, python3-gfhardware 0f59c54 and gfutilities
  993f9e1.

grblHAL-glowforge keeps b34636f here: its CI runs this repository's
harnesses from master, and its head's mcode_test expects the 5 s host
build, so the driver is pushed after this and pinned on its own.

Proof: image 20261015101725 carried forgectrl c9e3347, the module
f924cd9 and this tree's forgetest, and campaign c-20261015103714-0c91
passed all 53 catalog tests on it with none inherited. bitbake -c fetch
of forgectrl, gfcloud, gfhome, python3-ffmachine, python3-gfhardware,
python3-gfutilities and kernel-module-glowforge from these pins
succeeds (14 tasks). The tree manifest from the pins holds forgectrl at
c9e3347 (229 files); the coverage lint over it finds no uncovered path,
no empty entry and no host-tested row problem (53 tests);
check-ui-vendor finds the shared UI files identical at c9e3347;
test_tree_manifest passes, and the gate self-check refuses the empty
artifact (rc 1).
2026-10-15 07:24:51 -04:00
ScottW514 7f2da68e34 Catalog: image.license-bundle and image.network-boot leave; the network boot's board checks fold into image.health
Each left after its host proofs passed, with the coverage lint clean at
the pins, its only failure the known setup.check-lens entry on
src/lensfind.*:

- image.license-bundle: the pack half is test_image.py's
  PackLicensesTests on pack-licenses.sh (forgefirm 0d63f87); the serve
  half is host_routes.sh's license cases, the manifest, the bundle and
  the Licenses page read with no token (forgectrl c9e3347). Its
  src/main.c stays covered by image.health, and its three pages by
  forgectrl.panel-serves (src/ui/**). The 40-package and 5-text counts
  leave with it.
- image.network-boot: the interfaces file's DHCPv6 check is
  NetworkBootTests' case on the BSP layer's file (forgefirm 0d63f87).
  The board checks move into image.health, unchanged: wlan0 in ifup's
  state file and no ifup or ifdown running; a getty on ttymxc0; at least
  one udhcpc, each with -b as its own word and parent pid 1; no udhcpc6,
  dhcp6c or odhcp6c running or on the PATH, and no
  /usr/share/udhcpc/default6.script; on wlan0 disable_ipv6 0, accept_ra
  neither empty nor 0, and a link-local address, the global address
  logged. image.health's description says so. The interfaces file
  carries no -b, so -b is checked here, on the client that runs.

No HOST_TESTED row: network-boot covered nothing, and license-bundle's
paths stay covered by the two tests that keep them. image.health's
covers are unchanged. The helpers ifstate_names, dhcp6_in_interfaces,
parse_stat, link_addrs, _procs and _read, and the module's constants,
stay as they were; only the two functions and the inside of
image_health changed.

Fingerprints: image.health's moved and no other id's (the module's
shared text, which every exthost test reads through its import of
kernel_config, is unchanged). image.health is always-required, so the
next campaign reruns it.

Proof: the coverage lint, 53 tests (forgectrl, src/lensfind.* its one
entry); pyflakes; forgetest's unit suite on Linux (python3 -B -m
unittest discover -s tests), 335 OK.
2026-10-14 21:46:58 -04:00
ScottW514 0d63f8740e pack-licenses.sh: the image's license step refuses a manifest without the project's recipes; the layer's interfaces file starts no DHCPv6 client
The host proofs of image.license-bundle's pack half and of
image.network-boot's interfaces check. No catalog test is removed here;
suite/image.py is unchanged.

pack-licenses.sh, next to forgefirm-image.bb, is the license step.
pack_licenses calls it with the common-licenses directory and the
output tar path (forgefirm-image-dev.bb requires the release recipe, so
both images run the one call), and do_rootfs's file-checksums name it.
Before it writes the tar or removes the tree, it exits non-zero with a
message on stderr when the directory is missing, when the manifest is
missing, or when the manifest lacks any of the newline-terminated lines
"RECIPE NAME: forgectrl", "RECIPE NAME: grblhal-glowforge" and "RECIPE
NAME: kernel-module-glowforge". On success it packs as before: tar
--sort=name --mtime=@0 --owner=0 --group=0 --numeric-owner, gzip -9 -n,
the member common-licenses/license.manifest, and the loose tree removed
once the bundle holds the manifest.

test_image.py's PackLicensesTests drives that script on a fixture: a
missing directory, a missing manifest, and a manifest missing each one
of the three lines are refused with a message on stderr and no tar, the
tree left; a manifest naming all three packs, the tar holds
common-licenses/license.manifest, and the tree is gone.

NetworkBootTests reads the BSP layer's interfaces file from the
meta-openglow checkout beside forgefirm's (forgetest-ci checks out its
scarthgap branch there) and finds no line that starts a DHCPv6 client.
A missing file fails the case.

meta-forgefirm's content hash moved (forgefirm-image.bb and the new
script are layer content, and the platform identity of
forgefirm-image-manifest.bbclass leaves out only *.md, *-pin.inc and
forgefirm-release.inc): the next image cannot inherit a campaign.

Proof: python3 -B -m unittest discover -s tests -p test_image.py on
Linux (Ubuntu 24.04, GNU tar 1.35, meta-openglow at f012d8e): 14 OK, the
five new cases among them; pyflakes names no undefined name.
2026-10-14 21:42:12 -04:00
ScottW514 558889659c setup_burns: the module docstring describes setup.focus alone
setup.floor left the catalog in f9c298a, and the docstring still named it:
the second burn, the floor needing Focus on the record, the faintest
rung, and each wizard. It now describes the focus burn alone; the focus
burn's own description is unchanged.

The docstring is the module's shared text, so setup.focus's fingerprint
moves again; f9c298a had already moved it. No other test's fingerprint
moves (Test.source_sha compared before and after: setup.focus alone).

Proof: test_setup_burns OK; pyflakes; the coverage lint, 55 tests.
2026-10-14 21:19:46 -04:00
ScottW514 4c6ca8d701 test_setup_suite: only registered tests named
The file named setup.mirror (gone at 70e13bd) and setup.first-run-page,
setup.check-sensors, setup.check-intakes and setup.floor (gone at
f9c298a), so it failed in forgetest-ci's discover run. Every id, cover,
requires edge and attended list it asserts is now a registered test:

- IDS and OPERATOR: account-login, ssh-until-reboot, machine-name and
  first-run-flow; the four assertions on ids long folded into others go
  with them.
- the takeover check names first-run-flow; the mirror's covers check goes.
- the intake precheck: setup.check-airflow.
- first-run-flow: its fixture run and its backend covers, without the
  page's half.
- the attended setup tests: setup.cloud-header-capture and setup.focus.

It also puts tests/ on sys.path before importing helpers, as the other
suite tests do, so python -B -m unittest tests.test_setup_suite runs from
forgetest/.

Proof: python -B -m unittest tests.test_setup_suite and discover -p
test_setup_suite.py, 30 OK under Linux and on Windows; pyflakes.
tests/** is in no fingerprint.
2026-10-14 21:13:56 -04:00
ScottW514 f9c298a347 Catalog: eleven tests leave for their host proofs
Each left after its host cases passed (forgectrl 1dc85f6) and with
the coverage lint clean at the pins, its only failure the known
setup.check-lens entry on src/lensfind.*:

- forgectrl.tokens: host_routes.sh's scoped tokens (a route refused to a
  token not given it, neither token in the list or the store), with
  tokens_test and auth_scoped_test.
- update.product-gate: host_update.sh's product gate on the upload, with
  fwproduct_test.
- setup.first-run-page: tests/wizard_walk.py.
- setup.check-sensors, setup.check-intakes, setup.check-cameras,
  setup.check-flow-verify, setup.check-envelope, setup.floor:
  host_setup.sh's checks and the floor burn, with wizcalc_test.
  envelope_test.py's cases are unchanged. setup.floor's emission stays
  with setup.focus and laser.emission-witness.
- motion.job: jobpost_test, with jobrun_test and lease_test; jobpost.* is
  covered by exthost.motion-job, which stays.
- exthost.motion-jog: host_routes.sh's credential case.

Two HOST_TESTED rows, each a binary forgectrl's build.yml runs at the pin
c274f19 that proves the path the lint named: src/fwproduct.* to
fwproduct_test, src/grblport.* to grblport_test.

exthost.motion-job now requires motion.jog-roundtrip in place of
motion.job. bedsize.py held setup.check-envelope alone and goes;
forgectrl.py loses _words_of and its hw import, motion.py the job
runner's helpers, setup_burns.py FLOOR_KEY and FLOOR_KEYS.

forgetest's own tests: test_setup_dark.py loses the sensors and intakes
judges and the departed ids; test_setup_burns.py loses the floor's cases,
and the guards burn() and preview_ok() hold for the focus burn (the
program's tail and reach, the thickness in inches and the one refused, a
prompt not answered, the witnesses) run on setup.focus.

Stay: forgectrl.auth (the fuse view's button refusal on the real
button); forgectrl.settings-bounds (the lid lamp: cam.c writes
/sys/glowforge/pic/lid_led, not under GF_SYSFS_ROOT); forgectrl.panel-serves
(src/ui.h, which no binary at the pin proves; Z inside the lens reach is
setup.check-lens's); logs.routing; setup.account-login (the account's
render and its home, image content); setup.ssh-until-reboot
(src/paths.h); setup.machine-name (udhcpc -x hostname and no mDNS
responder, image content); setup.first-run-flow (src/button.*, led.*,
advisories.*, hooks.h and the four advisory documents); exthost.motion-job
(jobpost.*, which no binary at the pin proves).

Proof: the coverage lint, 55 tests; pyflakes; test_setup_burns,
test_setup_dark, test_takeover_position and test_image (54 OK).

Shared text moved the fingerprints of exthost.armed-freeze,
exthost.platform, exthost.service, forgectrl.auth,
forgectrl.panel-serves, forgectrl.settings-bounds, laser.armed-kill,
laser.emission-witness, laser.pause-resume-lid-cancel,
motion.cancel-abort, motion.deadman, motion.jog-roundtrip,
motion.liveness-probe, motion.microstep-modes, motion.pacing,
motion.release, motion.step-timing-under-load, setup.account-login,
setup.check-airflow, setup.check-lens, setup.check-motion,
setup.check-switches, setup.cloud-header-capture, setup.first-run-flow,
setup.focus, setup.machine-name, setup.ssh-until-reboot and
update.slots-and-signature; exthost.motion-job's own text moved with its
requires.
2026-10-14 21:03:58 -04:00
ScottW514 754c239e08 test_image: the hostname and the banner scripts, run over a scratch tree
MachineNameTests runs the layer's own forgefirm-hostname and
forgefirm-banner in private mount, UTS and network namespaces: /run a
fresh tmpfs, a scratch directory over /sys/class/net, /etc a read-only
overlay with the scratch hostname and issue over the host's (so each
script makes its copy under /run/forgefirm and writes through its own
bind mount, as at boot). unshare -Urmun where user namespaces are open,
sudo -n unshare -mun otherwise; skipped only where neither makes a
namespace.

- the name: wlan0's last four hex digits (forgefirm-12ab with eth0 beside
  it), eth0's with no wlan0, eth0's with a zero wlan0 MAC; the live name
  and /etc/hostname agree.
- the banner: the image's text, then the heading and one URL per global
  address of wlan0 (a veth carrying 192.0.2.5, 2001:db8::5 and fe80::5):
  https://192.0.2.5/ and https://[2001:db8::5]/, the link-local address
  left out, no marker line, no .local name.

Proof: test_image.py under Linux by both namespace routes (9 tests OK),
and on Windows with the two skipped. Mutants of scratch copies of the
scripts, each failing: eth0 read before wlan0, a zero MAC taken, the file
written without the script's bind mount, a marker line in the banner, a
.local URL, the link-local address kept.

setup.machine-name stays: the DHCP client's -x hostname and no mDNS
responder on the image are image content. Nothing under meta-forgefirm
changed.
2026-10-14 21:03:33 -04:00
ScottW514 b3a40b4aea Catalog: twelve tests leave for their host proofs; image.health reads the board temperatures
Each test below leaves for the host cases that now hold it, every one
run and passing before this commit:

- cooling.fans-quiet-after-motion, cooling.gate-off (its engine half),
  cooling.floor-and-warm-up, cooling.tec-drive, cooling.fire-watch-tiers,
  cooling.crash-watch-plumbing, cooling.critical-tier and
  cooling.fan-gate-trips: forgectrl's cool_flow_test and cool_gate_test
  (forgectrl 8e9cc0a). The jog and M8/M9 report stays in grblHAL's
  cooling_test.c and cool_report_test.py; real heater climb, a lit tube,
  tach spin-down and fans meeting their floors stay with
  cooling.flow-verify, cooling.flow-under-load, cooling.fail-tier-stop,
  cooling.aa-offset-calibrate and setup.check-airflow.
- motion.gate-waits-for-lid: super_test D2 (forgectrl 8e9cc0a). The
  physical lid read stays with setup.check-switches.
- motion.lid-cancel-home: laser_lifecycle_test's hold-lid-cancel (forgefirm
  107676b). check_kernel_returned stays in laser.pause-resume-lid-cancel's
  live lid phase.
- homing.manual: manual_home_test.py cases 1 and 7, and status_test's
  anchor readback (forgectrl 8e9cc0a).
- laser.recorder-dark: curverec_test (forgectrl 8e9cc0a). The dark arm
  wait stays test_button_wait_arms.

cooling.gate-off's board half moves into image.health: /status
temps.chassis_c from 5 through 80, temps.soc_c from 10 through 100,
temps.supply_raw an integer from 0 through 1023, and temps.soc_throttle
0. image.health's covers gain src/status.*, which serves them. The next
campaign reruns image.health (it is always run).

Stays: motion.cancel-abort (its host case fails on the abort's cnc/stop:
the null-sink controller writes cnc/stop only with a device open),
exthost.service (its init-script case has not passed on unmodified code
here), exthost.armed-freeze (its armed-capture decision has no host
proof), exthost.motion-jog (the package's jog through the host credential
has no host proof), exthost.motion-job (its POST /job reader is step 4's
jobpost_test.c).

Requires re-pointed off the tests that left: setup.check-airflow and
setup.check-intakes no longer require cooling.fan-gate-trips,
laser.pause-resume-lid-cancel no longer requires motion.lid-cancel-home,
cooling.fail-tier-stop no longer requires cooling.crash-watch-plumbing,
and exthost.armed-freeze no longer requires cooling.floor-and-warm-up.
cooling.py's docstring now describes the four cooling tests that stay.
The helpers only the departed tests used go with them, and laser.py
imports only the motion helpers it still calls.

Proof: the coverage lint on the tree manifest reports 0 uncovered paths
and 0 host-tested row problems across 66 tests; its one failure is
setup.check-lens's src/lensfind.* entry, which clears with the forgectrl
pin. No HOST_TESTED row was needed. The catalog validates; pyflakes
reports only the two unused locals that were there before
(cooling.fail-tier-stop's t0, update.py's plain);
test_cooling_order, test_cooling_suite, test_image, test_laser_dwell,
test_camera_home_return, test_setup_dark, test_setup_burns,
test_kernel_suite and test_cloud_suite pass.

Acceptance cost: the catalog goes from 78 tests to 66. Implementation
hashes move for cooling.aa-offset-calibrate, cooling.flow-verify,
cooling.flow-under-load and cooling.fail-tier-stop (cooling.py's shared
text); motion.pacing, motion.jog-roundtrip, motion.microstep-modes,
motion.liveness-probe, motion.cancel-abort, motion.deadman,
motion.step-timing-under-load, motion.release, motion.job,
laser.armed-kill, laser.emission-witness, laser.pause-resume-lid-cancel
and setup.check-envelope (motion.py's and laser.py's shared text, and the
modules that import motion.py); image.health, setup.check-airflow,
setup.check-intakes and exthost.armed-freeze (their own text).
2026-10-14 19:33:43 -04:00
ScottW514 107676b77d laser_lifecycle_test: the lid cancels a job held on the button
Rule 16, hold-lid-cancel: a travel job held by a press, then the lid
opened. The controller reports "lid opened - job canceled", prints the
reset banner, raises no alarm, and returns the head to the job start
(MPos X within 0.05 mm of where the job began); a press once the lid is
closed again resumes nothing (Idle, the position unchanged).

This is the part of motion.lid-cancel-home no host test held: the lid
during Run is test_lid_cancels_and_returns, and the kernel playing the
return stays with laser.pause-resume-lid-cancel's live lid phase
(check_kernel_returned).

Proof: the whole harness passes on the null-sink build as grblHAL's CI
builds it (-DGFMCODE_WAIT_S=5.0): every earlier case, then
hold-lid-cancel, lid-hold and sigterm-mid-job.

Acceptance cost: none. scripts/bench/ is outside every fingerprint.
2026-10-14 19:09:00 -04:00
ScottW514 6594f57362 Catalog: motion.port-jog and motion.tray leave; two leftovers of the audit
Two catalog tests leave for the host proofs that carry them; the catalog
goes from 80 to 78 (78 @test in forgetest/forgetest/suite/).

- motion.port-jog. forgectrl's tests/host_routes.sh (7b2f629 on e69d967)
  runs the controller port's routes against the port's stand-in: a jog
  refused without the token, a jog that is no number 400 with nothing on
  the port, a numeric jog on the port as G91 G21, cancel, release and
  energize each one line, GET /motion/state passing the port's state on
  with state and sender, GET /motion/sender 200. The port beside a
  sender, its polls, the cancel, the client first and the bound are
  grblHAL's ctlport_test.py and forgectrl's grblport_test; motion.release
  keeps the physical currents. The helpers before it (_port_state,
  _port_idle, _words) stay: bedsize, laser and motion.job read them.
- motion.tray. host_routes.sh holds POST /motion/tray (tray=in on the
  port, tray=up 400) and the panel page's switch (id="traybtn",
  trayToggle, /motion/tray); grblHAL's tray_test.py the mode, its offset,
  its persistence and its frame; forgectrl's tray_test the daemon's side.
  suite/tray.py held only it: the module and its import go.

The coverage lint then named forgectrl src/tray.c and src/tray.h, and
nothing else; grblhal-glowforge's src/glowforge_tray.* stays covered by
another catalog test, so it gets no row. One HOST_TESTED row takes
forgectrl's, to forgectrl:tests/tray_test.c on
forgectrl:.github/workflows/build.yml, which the pin c274f19 runs
(./build/tray_test).

Two of the audit's leftovers, in the same change:
- test_manifest.py's ImplementationHashTests sat after the __main__
  guard, so running the file directly skipped it. It is above the guard
  now, byte for byte: python3 -m unittest test_manifest.py and python3
  test_manifest.py (from tests/, where its helpers import resolves) each
  run its 7 cases, and 34 in all; before, the direct run ran none of
  them.
- laser_lifecycle_test.py's test_lid_open_in_wait said an alarm is
  raised; the docstring now says what the test asserts: the lid-open
  cancel is reported and no alarm is raised. The assertions are
  unchanged and the harness was not run.

The coverage lint on the tree manifest finds no uncovered path and no
host-tested row problem across the 78 tests; its one finding is
setup.check-lens's empty src/lensfind.* entry. pyflakes reports nothing
on motion.py, manifest.py, test_manifest.py or laser_lifecycle_test.py.
The full forgetest suite was not run.

Fingerprints, measured against the parent: 17 tests move with
motion.py's shared text: the 11 left in motion.py, and homing.manual,
laser's four and setup.check-envelope, whose modules read it. The
pending pin bumps from forgectrl c274f19 and forgeext 2d0391e already
owe all 17; this deletion adds none to the owed list. The two leftovers
move no fingerprint. releases/ is unchanged.
2026-10-14 18:21:59 -04:00
ScottW514 40c478d56c Catalog: forgectrl.lease leaves
forgectrl.lease leaves for forgectrl's tests/host_lease.sh (0086e65),
which passed as build.yml runs it: with the gate open and cnc/state idle,
the switches check held at its first wait names wizard:switches, kind
hardware, as the holder; POST /diag/flow-verify, /curve/record,
/logs/export, /mode with the mode in force, /controller/start and
/settings are each 409 in the holder's name, none for an idle machine or
a closed gate; the abort frees the lease and the same settings write is
200; lease.changed carries the take before the free. lease_test and
super_test case K hold the lease's own rules. The catalog goes from 81 to
80 (80 @test in forgetest/forgetest/suite/).

_EventStream, _lease and _text, which only it read, go with it, and so
does urllib.parse, which only _EventStream used; socket stays for lan_ip.
No test requires the id, and no unit test names it.

The coverage lint on the tree manifest then named forgectrl src/events.c
and src/events.h, and nothing else beside setup.check-lens's empty
src/lensfind.* entry. One HOST_TESTED row takes them, to
forgectrl:tests/events_test.c on forgectrl:.github/workflows/build.yml,
which the pin c274f19 runs (./build/events_test). The lint then finds
no uncovered path and no host-tested row problem across the 80 tests;
its one finding is setup.check-lens's. test_manifest.py (34) passes, and
pyflakes reports nothing on forgectrl.py or manifest.py. The full
forgetest suite was not run.

Fingerprints, measured against the parent: 26 tests move with
forgectrl.py's shared text: the 4 left in it (auth, panel-serves,
settings-bounds, tokens), the 5 left in exthost.py and the 17 of the
setup family, whose modules import lan_ip from it (exthost.py and
setup.py, and through setup.py the rest). The pending pin bumps from
forgectrl c274f19 and forgeext 2d0391e already owe 24 of them;
exthost.platform has been owed since 30c0a05 and setup.machine-name
since 70e13bd. This deletion adds none to the owed list. releases/ and
TEST-AUDIT.md are unchanged.
2026-10-14 17:33:16 -04:00
ScottW514 70e13bdd88 Catalog: setup.mirror and logs.tree-tail-export leave; the catalog's address in forgetest-ci
Two catalog tests leave for the host proofs that carry them; the catalog
goes from 83 to 81 (81 @test in forgetest/forgetest/suite/).

- setup.mirror, for forgectrl's tests/host_ext.sh (0086e65): with a
  temporary account and two logins, a package's check that session A
  starts is owned and A's, owned and not B's, and the token's with no
  session; B's abort is 409 and the run goes on; B's takeover makes it
  B's; B's abort ends it aborted. Its section header, "the lifecycle",
  held only it. setup.check-sensors and setup.account-login stay.
- logs.tree-tail-export, for forgectrl's tests/host_guard.sh (0086e65):
  GET /logs names forgectrl, the tail carries a line just written, an
  unknown logger is 404, and the export carries README.txt, the
  installer's directory (a probe without its MAC or IP address, and
  install.log) and system/extensions.json, with neither the panel token
  nor the camera key in any member. _LOG_COVERS, the _PROBE_ constants,
  and the gzip, io, and tarfile imports, which only it read, go, and so
  does logs.routing's requires on it. logs.routing stays.

forgectrl.lease stays. Its proof passed (forgectrl's tests/host_lease.sh,
0086e65), but its deletion left forgectrl src/events.c and src/events.h
uncovered, a path outside the two this chunk may row, so it does not
land.

The coverage lint then named forgectrl src/sanitize.c, src/sanitize.h,
src/extpkg.c and src/extpkg.h, and nothing else. Two HOST_TESTED rows
take them, to forgectrl:tests/sanitize_test.c and
forgectrl:tests/extpkg_test.c on forgectrl:.github/workflows/build.yml,
which the pin c274f19 runs (./build/sanitize_test, ./build/extpkg_test).

forgetest-ci, after the tree-manifest step: the key comparison moves
there unchanged, and a step beside it checks the catalog's address.
From forgectrl-pin.inc's SRCREV it reads src/extpkg.h out of the
manifest cache (git show) and takes EXTPKG_INDEX_URL; from the
checked-out catalog's tools/publish.py it takes the basename of the file
passed to gh release create; the define must be exactly
https://github.com/openglow-org/forgefirm-extensions-catalog/releases/latest/download/
and that basename. A missing pin, define, or basename fails. Nothing
requests the address or fetches the asset. The step's run text, taken
from the workflow, passes from the tree root (c274f19's address and
index-1.ffi) and on a /tmp copy of the tree; controls on that copy,
publish.py passing index-2.ffi and extpkg.h naming another repository:
each fails the comparison. The key step's text is unchanged and was not
run again.

test_manifest.py (34) passes. The coverage lint on the tree manifest
finds no uncovered path and no host-tested row problem across the 81
tests; its one finding is setup.check-lens's empty src/lensfind.*
entry. pyflakes reports nothing on setup.py, logs.py or manifest.py.
The full forgetest suite was not run.

Fingerprints, measured against the parent: 23 tests move with
setup.py's and logs.py's shared text: the 5 left in setup.py, the 12 in
the modules that read setup.py (setup_dark.py's 9, setup_burns.py's 2,
bedsize.py's setup.check-envelope), the 5 left in exthost.py, which
reads it too, and logs.routing. The pending pin bumps from forgectrl
c274f19 and forgeext 2d0391e already owe 21 of them, and
exthost.platform has been owed since 30c0a05. This adds one:
setup.machine-name, which no pinned change selects. releases/ and
TEST-AUDIT.md are unchanged.
2026-10-14 17:09:32 -04:00
ScottW514 b90299acdc Catalog: the package routes, the catalog, a package's check and the camera key leave
Four catalog tests leave for the host proofs that carry them; the
catalog goes from 87 to 83 (83 @test in forgetest/forgetest/suite/).
Each proof passed; none skipped.

- exthost.package-routes, for forgectrl's tests/host_ext.sh (05848ac):
  against a compiled stand-in that is a live host, GET /ext/status and
  POST /ext/package are 403 without the token with the host not run,
  the status passes the host's list through with host.running true and
  the stand-in's pid, enable runs as the host's command, a bad action
  and a bad id are 400 with nothing run, and a package that is not
  installed is 409 in the host's words. extpkg_test holds the relay's
  argv; the state file, the required-holds directory and removal are
  forgeext's install_test.py and run_test.py; the page card is
  check_ui_js.py. REQUIRED_HOLDS, which only it read, and the "the hold"
  section header over it go.
- exthost.wizard, for host_ext.sh (05848ac): the check listed, run to its
  prompt with no lease holder, answered yes, ended with its summary and
  listed done; the setup record's wizards and the gate as they were; a
  second run aborted; a check of a package that is not installed 404.
  wizpkg_test holds each step to its form. suite/extwizard.py held only
  it: the module and its import go.
- exthost.catalog: install_test.py and extpkg_test already assert its
  behavioral claims, and the one fact the bench test did not prove, that
  the image's extension key is the catalog's published key, is now a
  forgetest-ci step: the catalog is checked out at main as a sibling and
  meta-forgefirm/recipes-forgefirm/forgefirm-keys/files/ext/forgefirm-ext.pub
  is cmp -s'd against its keys/forgefirm-ext.pub, either one missing a
  failure. suite/extcat.py held only it: the module and its import go.
- camera.key-read, for forgectrl's tests/host_lan.sh (05848ac): with
  panel_open_reads=0, GET /cam/status from the LAN client over HTTP and
  HTTPS is 403 with no key or a wrong one and 200 with the key as ?key=
  or the header, the key alone writes no setting, and a rotation stops
  the old key and the new one reads. camera.py's json import, which only
  it used, goes.

The coverage lint then named forgectrl src/wizpkg.c, src/wizpkg.h,
src/camkey.c and src/camkey.h, and nothing else. Two HOST_TESTED rows
take them, to forgectrl:tests/wizpkg_test.c and
forgectrl:tests/camkey_test.c on forgectrl:.github/workflows/build.yml,
which the pin c274f19 runs (./build/wizpkg_test, ./build/camkey_test).
src/extpkg.* stays covered (logs.tree-tail-export, logs.routing), and
forgeext/** by exthost.service.

The key step's run text, taken from the workflow, passes from the tree
root with both files present; controls on a /tmp copy, a published key
that differs and one that is missing: each fails. test_manifest.py (34)
passes. The coverage lint on the tree manifest finds no uncovered path
and no host-tested row problem across the 83 tests; its one finding is
setup.check-lens's empty src/lensfind.* entry. pyflakes reports nothing
on exthost.py, camera.py or manifest.py. The full forgetest suite was
not run.

Fingerprints, measured: 10 tests move, the 5 left in exthost.py
(armed-freeze, motion-jog, motion-job, platform, service) and the 5 left
in camera.py (frame-health, h264-stream, lid-privacy, sensor-profile,
snapshot). The pending pin bumps from forgectrl c274f19 and forgeext
2d0391e already owe 9 of them; exthost.platform is owed already since
30c0a05. This adds none. releases/ and TEST-AUDIT.md are unchanged.
2026-10-14 16:24:45 -04:00
ScottW514 30c0a05b86 Catalog: exthost.panel-install leaves
exthost.panel-install leaves for the host proofs that carry it; the
catalog goes from 88 to 87.

- The upload sink is forgectrl's tests/host_ext.sh (005b159): 403
  without the token, 409 and not idle while cnc/state reads running with
  the host never run, the host's refusal at 400 in its words with the
  staging file gone, an archive the host takes staged byte for byte as
  tier unverified with consent button, the discard, an install with
  nothing staged at 409, and a key without the button at 409 with no key
  written. The consent each tier takes, the phrase, the grants' form,
  the button the install and a key are passed, and the key's name and
  the host's refusal of it are forgectrl's extpkg_test; the tiers
  themselves are forgeext's install_test.py.
- Its section header, "installing through the panel", held only it, and
  EXT_STAGE and the contextlib import, which only it read, go with it. Its
  requires line goes with the function.

Three stay. exthost.package-routes: extpkg_test has the status
document, the holds, disable and enable, remove, a bad action and a bad
id at 400 with nothing run, and the host's refusal at 409, but not the
403 without the login on GET /ext/status and POST /ext/package, which is
main.c's guard and no case of extpkg_test's. exthost.wizard: a package's
check takes no lease (wizdark.c), and the lease is held in memory, so
there is no lease file to assert absent at the prompt. exthost.catalog:
install_test.py and extpkg_test assert every listed claim, and what is
left is the image's own key.

The coverage lint names no uncovered path, so no HOST_TESTED row is
added: forgectrl's src/auth.* stays covered by other catalog tests, and
src/extpkg.* by the three that stay.

test_setup_suite.py (31) and test_manifest.py (34) pass. The coverage
lint on the tree manifest finds no uncovered path and no host-tested row
problem across the 87 tests; its one finding is setup.check-lens's
empty src/lensfind.* entry. pyflakes reports nothing on exthost.py. The
full forgetest suite was not run.

Fingerprints: exthost.py's shared text moves, so 8 tests move: the 6
left in exthost.py, and exthost.catalog and exthost.wizard in the
modules that read it. The pending pin bumps from forgectrl c274f19 and
forgeext 2d0391e already owe 7 of them; exthost.platform is the one this
adds. releases/ and TEST-AUDIT.md are unchanged.
2026-10-14 15:42:34 -04:00
ScottW514 cfa6557847 Catalog: the sender keep-out, the event stream and the page's call leave
Three catalog tests leave for the host proofs that carry them; the
catalog goes from 91 to 88. Each proof passed; none skipped.

- exthost.sender-keep-out, for forgeext's run_test.py (ebdbf59): a
  package granted sender.keep_out posts /v0/sender {"out": true}, the
  stand-in takes it only with the host's token, and the claim is kept
  fresh beside the holds, raised in the host's words, until the package
  is disabled. The machine's side is forgectrl's senderout_test.
  suite/extsender.py held only it: the module and its import go.
- exthost.events, for forgeext's run_test.py (ebdbf59) and forgectrl's
  host_lan.sh (b272b3a): the host holds one stream while a package wants
  events and lets it go when none does, a poll at the head comes back
  empty at its one-second deadline, a waiting poll is answered 0.10 s
  after an event; and the host's slot is a loopback peer's alone, the
  same header from a LAN address leaving the host's stream open.
  forgeext's evfeed_test and forgectrl's events_test hold the two
  daemons' own sides. Its section header, the operator's door, held
  only it, and EVENTS_MAX_STREAMS, EVENTS_HOST_HEADER and _stream, which
  only it read, go with it.
- exthost.page-call, for forgeext's sdk_test.py (ebdbf59): disabled, the
  page package's socket name is gone and its call is refused in the
  host's words; enabled, the same call answers 200. sdk_test.py already
  held the calls, their form and refusals, and the frozen service;
  forgectrl's extpkg_test holds the relay. suite/extcall.py held only
  it: the module and its import go.

The coverage lint then named forgectrl src/senderout.c and
src/senderout.h, and nothing else; one HOST_TESTED row takes them, to
forgectrl:tests/senderout_test.c on forgectrl:.github/workflows/build.yml,
which the pin c274f19 runs. grblhal-glowforge's src/serial.* stays
covered by a catalog test and gets no row; holdkeep, evfeed, call and
sandbox stay under exthost.service's forgeext/**, and forgectrl's
src/events.* under forgectrl.lease.

test_setup_suite.py (31) and test_manifest.py (34) pass. The coverage
lint on the tree manifest finds no uncovered path and no host-tested row
problem across the 88 tests; its one finding is setup.check-lens's
empty src/lensfind.* entry. pyflakes reports nothing on exthost.py or
manifest.py. The full forgetest suite was not run.

Fingerprints: exthost.py's shared text moves, so 9 tests move: the 7
left in exthost.py, and exthost.catalog and exthost.wizard in the
modules that read it. The pending pin bump from c274f19 already owes 8
of them; exthost.platform is the one it adds, as before. releases/ and
TEST-AUDIT.md are unchanged.
2026-10-14 14:07:21 -04:00
ScottW514 b5f355cb61 Catalog: exthost.ui-delivery leaves
exthost.ui-delivery leaves for the host proofs that carry it; the
catalog goes from 92 to 91.

- forgeext ui, in forgeext's tests/install_test.py (23156a4): an
  installed package's page comes back byte for byte as the html string,
  with bytes its length; an id that is not installed and a package the
  operator disabled are each refused in words.
- forgectrl's relay of the page and the settings, in tests/extpkg_test.c
  on forgectrl's build.yml: the page served, a disabled package's page
  404, the settings read, a value over its bound 400.
The install-time mismatch between the ui capability and a page stays
with forgeext's own tests.

It has no glob of its own, so no HOST_TESTED row. exthost.sender-keep-out
stays: forgeext carries a package's claim on the Grbl sender to the
machine with the token it reads only from /run/forgefirm/ext-host.token,
so run_test.py does not prove it, and suite/extsender.py is unchanged.

test_setup_suite.py passes (31 tests). The coverage lint on the tree
manifest finds no uncovered path and no host-tested row problem across
the 91 tests; its one finding is setup.check-lens's empty src/lensfind.*
entry. pyflakes reports nothing new in exthost.py. The full forgetest
suite was not run.

Fingerprints: exthost.py's shared text moves, so 12 tests move: the 8
left in exthost.py, and exthost.page-call, exthost.catalog,
exthost.wizard and exthost.sender-keep-out in the modules that read it. The pending pin bump
from c274f19 already owes 11 of them; exthost.platform is the one it
adds, as before. releases/ and TEST-AUDIT.md are unchanged.
2026-10-14 12:41:15 -04:00
ScottW514 b447ae77f1 Catalog: the LAN redirect, the release check, and the update job's lease leave
Three catalog tests leave for the host proofs that carry them; the
catalog goes from 95 to 92.

- setup.https-only-writes, for forgectrl's tests/host_lan.sh (13273a0):
  from a LAN peer in a network namespace, reads served over HTTPS and
  HTTP, three writes over HTTP each a 302 to the same path on HTTPS that
  acts on nothing, the same write served from loopback, and the token
  writing over HTTPS with no account.
- update.release-check and update.job-locks, together, for forgectrl's
  tests/host_update.sh (acc3dcf), against a release server of its own:
  GET /update/release's keys, POST /update/check with a v<semver> tag, a
  size above 0, notes and new; the dismissal refused for a version that
  is not a tag, taken for the tag, cleared by an empty one; and a probe
  download that holds the machine (a settings write and a G21 job each
  409 in the job's name, update:download of kind system the holder),
  ends refused by the signature check leaving no file, and lets the same
  settings write through after.

update.release-check was the last test in suite/update.py; _VERSION_RX
and the re import, which only it used, go with it. update.job-locks was
the only test in suite/updlock.py: the module and its import in
suite/__init__.py go. setup.https-only-writes was the only test of
setup.py's "HTTPS boundary" section. tests/test_setup_suite.py drops its
id.

update.product-gate stays: fwup is on neither this host's PATH nor CI's,
so host_update.sh does not prove the five uploads, and no HOST_TESTED
row is added for src/fwproduct.*. No other HOST_TESTED row either.

test_setup_suite.py passes (31 tests). The coverage lint on the tree
manifest finds no uncovered path and no host-tested row problem across
the 92 tests; its one finding is setup.check-lens's empty src/lensfind.*
entry. pyflakes reports only the unused local plain in
update.slots-and-signature, which HEAD already has. The full forgetest
suite was not run.

Fingerprints: setup.py's and update.py's shared text move, so 33 tests
move: the 6 left in setup.py, the 25 in the modules that read it, and
update.slots-and-signature and update.product-gate. The pending pin bump
from c274f19 already owes 31 of them; setup.machine-name and
exthost.platform are the two it adds, as before. releases/ and
TEST-AUDIT.md are unchanged.
2026-10-14 12:06:11 -04:00
ScottW514 21046cd6ff Catalog: setup.cert-page leaves
setup.cert-page leaves for forgectrl's tests/host_guard.sh (4d2d109):
GET /settings reports a 95-character tls_fingerprint, and over HTTP and
over HTTPS, with no token, GET /cert is 200 with no redirect, that
fingerprint and the host's name, and GET /cert.pem is a PEM certificate.
The catalog test reached the board by its LAN address; the page is what
it proved. The catalog goes from 96 to 95.

It called no helper of its own. src/tls.* stays on the login, HTTPS,
first-run and TLS-record tests, src/ui/wizard.html on
image.license-bundle and setup.mirror, and src/ui/wizard.js on
forgectrl.auth, exthost.wizard and setup.mirror. No unit test names it.
No HOST_TESTED row: the pinned workflow does not run host_guard.sh.

test_setup_suite.py passes (31 tests). The coverage lint on the tree
manifest finds no uncovered path and no host-tested row problem across
the 95 tests; its one finding is setup.check-lens's empty src/lensfind.*
entry. pyflakes is clean on setup.py. The full forgetest suite was not
run.

Fingerprints: the deletion moves suite/setup.py's shared text, so 32
tests move: the 7 left in setup.py and the 25 in the modules that read
it. The pending pin bump from c274f19 already owes 30 of them;
setup.machine-name and exthost.platform are the two it adds, as before.
releases/ is unchanged.
2026-10-14 11:25:28 -04:00
ScottW514 098cfdf193 Catalog: setup.record-export leaves
setup.record-export leaves for forgectrl's tests/host_guard.sh (84779c8
on d21e200): the record as JSON with the token (schema 1, the sheet id
GET /wiz reports, no password hash, a plain read not an attachment), the
download an attachment of the record named with that id, the printable
page text/html with no script, the sheet id, "setup record" and an <h3>
per completed wizard, both routes 403 without a token or a login, and
the sanitized export carrying system/setup.json with the same id and
wizards, no $6$, and no panel token anywhere in the archive. The catalog
goes from 97 to 96.

One HOST_TESTED row: forgectrl src/recordhtml.*, the one path no other
catalog test covers, to tests/recordhtml_test.c (no script, the sheet
id, the step headings, the escaping), which forgectrl's build.yml runs
at the pinned revision. src/setup.* and src/wiz.* stay on the setup
family, src/logs.* on exthost.service, forgectrl.lease and the two log
tests, and src/main.c, src/ui/index.html, src/ui/panel.js and
src/ui/wizard.html on the tests that already name them.
tests/host_guard.sh is not named: the pinned workflow does not run it.

tests/test_setup_suite.py drops the id, the record-export hardware
assertion, and the export lines of the lifecycle covers test (the
covers set and its two assertions); the mirror's assertions stay.

test_setup_suite.py passes (31 tests). The coverage lint on the tree
manifest finds no uncovered path and no host-tested row problem across
the 96 tests; its one finding is setup.check-lens's empty src/lensfind.*
entry. pyflakes is clean on setup.py and test_setup_suite.py. The full
forgetest suite was not run.

Fingerprints: the deletion moves suite/setup.py's shared text, so 33
tests move: the 8 left in setup.py and the 25 in the modules that read
it (setup_dark, setup_burns, bedsize, exthost, extcall, extcat,
extsender, extwizard). The pending pin bump from c274f19 already owes 31
of them; setup.machine-name and exthost.platform are the two it adds, as
before. releases/ is unchanged.
2026-10-14 11:10:16 -04:00
ScottW514 1e28eb92cd Catalog: setup.factory-return leaves
setup.factory-return leaves for forgectrl's tests/host_guard.sh
(d21e200): POST /restore/factory-return with the token and no confirm
is 400 and names confirm, confirm=0 is 400, GET /update/status shows no
job running, GET /slots lists the archives (run only where
/usr/sbin/ffboot is not on the host), and GET /setup carries the
confirmed call; confirm=1 is never sent. The catalog goes from 98 to 97.

It was the only test in the "the operator" section of suite/setup.py,
and called no helper of its own. tests/test_setup_suite.py drops its id
and test_the_factory_return_never_runs_the_return. src/update.* stays
covered by the update tests, src/ui/wizard.js by the wizard and mirror
tests, and src/main.c by the tests that already name it.

test_setup_suite.py passes (31 tests). The coverage lint on the tree
manifest finds no uncovered path and no host-tested row problem across
the 97 tests; its one finding is setup.check-lens's empty src/lensfind.*
entry. pyflakes is clean on both files. The full forgetest suite was not
run. No HOST_TESTED row: the pinned workflow does not run host_guard.sh.

Fingerprints: the deletion changes suite/setup.py's shared text, so the
34 tests that read it move again: the 9 left in setup.py, setup_dark 9,
setup_burns 2, bedsize 1, exthost 9, extcall, extcat, extsender and
extwizard 1 each. The pending pin bump from c274f19 already owes 32 of
them; setup.machine-name and exthost.platform are the two it adds, as
before. releases/ is unchanged.
2026-10-14 10:48:52 -04:00
ScottW514 33e8c6e43f Catalog: logs.level-settings leaves
logs.level-settings leaves for forgectrl's tests/logs_test.c (9317b54):
the six levels taken and verbose and 7 refused, port 70000 refused,
udp and tcp taken and tls refused, a server name with spaces refused,
and a remote level changed since boot listed as configured against
effective with pending_reboot raised, and lowered once they match. The
settings route's refusal of a value its validator rejects, with the file
left byte for byte, is tests/host_routes.sh's bounds case. The catalog
goes from 99 to 98.

It was the last test in suite/logs.py and called no helper of its own,
so nothing else goes. logs.tree-tail-export and logs.routing stay: they
are the only catalog covers of forgectrl src/fflog.*, and the pinned
workflow runs neither fflog_e2e.sh nor the new log tests.
setup.record-export keeps its requirement on logs.tree-tail-export.

The coverage lint on the tree manifest finds no uncovered path and no
host-tested row problem across the 98 tests; its one finding is
setup.check-lens's empty src/lensfind.* entry, which the forgectrl pin
bump clears. pyflakes is clean on logs.py. The full forgetest suite was
not run.

Fingerprints: the deletion changes suite/logs.py's shared text (the two
separator lines), so logs.tree-tail-export and logs.routing move; no
other module imports it. The pending forgectrl pin bump from c274f19
already owes both, so this commit adds none to it. releases/ is
unchanged.
2026-10-14 10:20:32 -04:00
ScottW514 71556b2480 Catalog: the setup gate, the advisories, the extensions' consent and cloud-off leave
Four catalog tests leave for the host proofs that carry them; the
catalog goes from 103 to 99.

- setup.gate-blocks-controllers, setup.advisories-rehash,
  setup.extensions-consent and setup.cloud-disabled-surface. forgectrl's
  tests/host_setup.sh (91d30f1) runs the four HTTP proofs against the
  built daemon with the controller up: each reason the gate holds on and
  the live fan-count close and reopen; the first-run advisories, their
  hashes, and an acceptance that takes the press back; the extensions'
  advisory, its five refusals, and the consent; and every cloud selection
  refused while cloud mode is off, POST /mode included. builtin_test.c
  holds the cloud sweep, sha256_test.c the digest, and
  settings_dir_test.c the search bit the consent adds to the data
  directory.
- gate_is_open and _GATE_COVERS go with the gate test, ADVISORY_DOCS and
  the hashlib import with the rehash; nothing else calls them. The record
  builders, REQUIRED_STEPS, SAFETY_PHRASE, Restore and request stay.
- The requires that named them: _FIRST_RUN_REQUIRES held only the two
  deleted ids and goes, so setup.first-run-flow requires nothing from it
  and setup.first-run-page requires setup.first-run-flow alone;
  exthost.service keeps exthost.platform.
- Coverage: HOST_TESTED rows for forgectrl src/sha256.* (tests/sha256_test.c)
  and src/builtin.* (tests/builtin_test.c), both run by forgectrl's
  build.yml at the pinned revision. docs/advisories/** stays behavioral:
  setup.first-run-flow, which accepts them, covers safety-and-risk.md,
  licenses.md, privacy.md and cloud-service.md, and exthost.service,
  which turns extensions on over it, covers extensions.md.
- tests/test_setup_suite.py drops the four ids from the id list and the
  takeover list, the gate test's precheck assertion (the method now names
  the intake prechecks only), and CloudDisabledSurfaceTests, with the
  imports only that class used; the docstring no longer describes it.

test_setup_suite.py passes (32 tests). The coverage lint on the tree
manifest finds no uncovered path and no host-tested row problem across
the 99 tests; its one finding is setup.check-lens's empty src/lensfind.*
entry, which the forgectrl pin bump clears. pyflakes is clean on the
four files. The full forgetest suite was not run.

Fingerprints: deleting the four functions changes suite/setup.py's
shared text, so 35 of the 99 tests' implementation hashes move: every
remaining test in setup.py (10), and the modules that import it at
module level, setup_dark (9), setup_burns (2), bedsize (1), exthost (9),
extcall (1), extsender (1), extwizard (1), and extcat (1, through its
import of exthost). The next forgectrl pin bump already owes 33 of them;
exthost.platform and setup.machine-name are the two it adds. releases/
and TEST-AUDIT.md are unchanged.
2026-10-14 09:26:41 -04:00
ScottW514 7172bba92b Catalog: the report channel, the event stream and what-changed leave
Three catalog tests leave for the host proofs that now carry them; the
catalog goes from 106 to 103.

- cooling.report-channel. forgectrl's tests/host_routes.sh (e69d967)
  runs the channel against a live daemon: a run report with the
  controller's secret writes exhaust 65535; no secret, a wrong secret
  and a short secret are 403 and the duty file is unchanged; an accepted
  idle while the ring still reads running writes 32768; the secret dies
  with the controller. cool_report_test.py and super_test case L own the
  secret, and cooling.fans-quiet-after-motion owns the live M8/M9 return
  to the idle duty. The forged Host: localhost was the same secret
  refusal.
- events.stream. events_test.c owns the hello, the replacement, and
  motors.released; host_routes.sh owns the live cap (three peers held,
  a fourth 503, GET /settings 200 beside them, closing one admitting the
  next). forgectrl.lease and exthost.events still cover src/events.*,
  and _EventStream stays for forgectrl.lease.
- setup.what-changed. setup_test holds tray -> laser.focus and fan ->
  airflow under the image's table beside tube -> laser.floor;
  host_routes.sh holds /wiz/changed (403, 400) and that every step the
  menu names is one GET /wiz lists. src/setup.* and src/wiz.* stay
  covered by the other setup tests, and help.js, panel.js and index.html
  by camera.key-read. preserved() and flag_of() go with it; recommended()
  stays.

tests/test_setup_suite.py drops setup.what-changed from the id list, the
takeover list and the covers lookup, and its flag_of case keeps only
the recommended() assertion.

test_setup_suite.py passes (36 tests). The coverage lint on the tree
manifest finds no uncovered path across the 103 tests; its one finding
is setup.check-lens's empty src/lensfind.* entry, which the forgectrl
pin bump clears. pyflakes on the four files reports only the unused t0
in cooling.py that HEAD already has. The full forgetest suite was not
run.

Fingerprints: the cuts change the shared text of suite/cooling.py,
suite/forgectrl.py and suite/setup.py, so 56 of the 103 tests'
implementation hashes move (cooling 12, setup 26, forgectrl 5, and
exthost 13 through its import of suite/forgectrl.py). The next forgectrl
pin bump already owes 54 of them; exthost.platform and
setup.machine-name are the two it adds. releases/ is unchanged.
2026-10-14 07:56:36 -04:00
ScottW514 8e1fe5090d Harnesses: the null-sink cases other tests prove leave
The grblHAL harnesses keep what only a running controller shows; the
cases a host unit test in grblHAL-glowforge proves, and the log and
reply wording, leave.

- laser_lifecycle_test: test_arm_waits_for_the_fans goes. laser_arm_test
  case A4 (grblHAL-glowforge) proves the fan grace stretching the arm's
  acknowledgment budget, on the same arm path. The docstring names it.
- xy_mode_test: case_x32, case_invalid and the log checks go; the
  default, x16 and the low tick stay. xy_scale_test holds each mode's
  tick and the not-a-mode parse.
- planner_blocks_test: one restart, at $398 1000; the 400 pass and the
  fresh-store probe go.
- ctlport_test: test_poll_lf and test_crlf go (the CR LF poll stays); in
  test_sender_out the network turn-away goes (serial_test holds it), and
  the busy gating, the grant and the outlives-client part stay.
  test_port_dark is whole.
- mcode_test: the timeout case runs on the host build's 5 s
  GFMCODE_WAIT_S (grblHAL-glowforge's CI configures it; the image keeps
  30 s) and still shows the hold, at 5.1 s; 5 s outlasts the 3 s
  test_dark watches the wait; test_table refuses one form per
  parser branch. test_timeout_holds and test_dark stay.
- envelope_test: test_unset and test_bounds go (lens_home_test holds the
  clamp); test_armed and the open, apply and close cases stay.
- manual_home_test: the out-of-range offset block goes, a repeat of
  gfhome_clamp_home_mm (lens_home_test); the modal-M3 FIRE check and
  every release and energize case stay.
- cool_report_test: one non-hex secret and one CR LF injection stay;
  runner-malformed and the exact header-set checks go. A bad secret is
  never sent and a good one is.
- tray_test: test_offset keeps 20 mm; test_errors keeps a bad P and a
  bad axis; the frame shift, envelope, barrier, persistence, port op,
  unreferenced lens and camera home stay.

Proof: with grblHAL-glowforge's matching commit, the eleven harnesses
its CI runs pass under Linux against the null-sink build: nine on a
build with a 1 s M-code wait, and mcode_test and tray_test, the two
that wait at an M-code, on the 5 s build CI configures
(-DGFMCODE_WAIT_S=5.0). pyflakes reports nothing in the nine changed
files.

Push this before grblHAL-glowforge's commit.

Acceptance: none; host harnesses only.
2026-10-13 21:20:31 -04:00
ScottW514 520d62ac05 forgetest tests: the confirmed trims of the tool's own unit tests
The cases that tested a mock, a timer, a wording or a table, or that
replayed a judgment a host test already holds, leave forgetest/tests.
What catches a false PASS, keeps the bench safe, or guards the gate
stays.

- test_baseline: the fake sysfs tree is a mixin with no test_ methods,
  so ExtensionFreeTests and BaselineModeTests no longer re-run
  BaselineTests' 25 cases (133 runs, now 45). CoolPublishTests, the
  lamp-fade cases and TransientNotLeftoverTests go; PositionDeadbandTests
  keeps a reading past the band and an unreadable reading, both never
  excused. Both latch-relock cases stay.
- test_fixture: MdnsTests, the three timing cases, the probe's log
  wording, and nine prompt-routing variants go. A live test never runs
  unattended, the arm-press opt-in, the short-press edge, the presence
  press handing over the arm press, and the arm notice dropping at the
  press stay (35 cases, now 21).
- test_cloud_suite: three false-PASS guards stay: the latch open at a
  dark print's button, a lid stop that is not edge-driven, the kernel
  refusing the resume. The guards for the latch unlocking in the hold
  and a paused print resuming left with their catalog tests in 6a434bb.
  The rest go, with the harness pieces and the hunt-lid fixture only
  they read (41 cases, now 3).
- test_cooling_suite: GateOffTests, CriticalTierTests, FanGateTests and
  FansQuietTests go with their harness; the flow verdict's log reads
  stay: only the text after the offset, never an older verdict before it
  (the flow-under-load false-PASS guard), a rotated log read whole.
- test_queue: RunClockTests and three ordering cases go, and refusals
  are judged by a reason given, not its words; both live
  acknowledgment cases stay.
- test_takeover_position: the six cases that judge the head and move
  nothing stay.
- test_camera_home_return: the five cases where the head must not move
  stay, and so do the three that check the hand-back jog's direction and
  distance (homed, unhomed with the counters not counted twice, after
  cloud mode): a wrong sign or a double count drives the head into the
  gantry stops (15 cases, now 8).
- test_server: the keep-alive and ETag cases, the takeover and scope
  half of the bench case, and the page's self-contained asserts
  (test_page.py's) go.
- test_operator: the wording cases, the fixture case (test_fixture.py's)
  and JournalTests go.
- test_mode: the supervisor timeout table goes.
- test_takeover_client: the listener, the offline service back, the
  online client without the hunt, and GRBL under no marker stay.
- test_responsiveness: PageTests and ImplementationCostTests go; the
  log cache keeps appends, a partial line, a replaced file and a fresh
  list, and the fingerprint cache a changed manifest and one with no
  content hash.
- test_bench_registry: GfbenchTests and two description and argument
  pins go.
- test_installer: the log-format and TLS-wording cases and
  InstallerShapeTests go; the die case checks the reason is in the log,
  not its format.
- test_image: KernelIdentTests and the registration pin go.

Each file's __main__ guard now ends the file, and the docstrings say what
each still proves.

Proof: each of the fifteen files passes under Linux as forgetest-ci
discovers it, and pyflakes reports nothing new in them.

Acceptance: none; test-only.
2026-10-13 20:29:25 -04:00
ScottW514 8536ae7076 laser_stream_test: the analog copies, the in-run levels and the curve run leave
The analog dose model runs on no machine; the density sessions beside
these prove the same rules on the shipped model.

- Sessions J and K (M5 at idle, the next job's first cut) run under
  density only.
- Session C (churn, analog) goes; its pad budget (no runaway pad across
  cycle churn) moves onto session G, density-churn, which already held
  FIRE, termination and the gaps on the same job.
- Session H (a level change inside a run costs no byte) goes: every
  density power byte is already held at full duty by check_density;
  the byte count was a cost, not an energy rule.
- Rule 19's end-to-end curve run goes: laser_arm_test.c case P pins the
  curve's mapping. The rolloff sessions keep the curve in force.
- The constants only those sessions read go (CURVE_DEFAULT,
  curve_density_for, CURVE_S, JOB_CURVE, JOB_LEVELS); the header's rules
  11, 16 and 19 say what the harness now holds.

Proof: laser_stream_test passes whole on a null-sink build of the
grblHAL-glowforge working tree under Linux (23 sessions). pyflakes is
clean.

Acceptance: none. The harness runs in grblHAL-glowforge's CI.
2026-10-13 19:10:00 -04:00
ScottW514 09cf9242a9 laser_lifecycle_test: the sessions the unit tests and the stream prove leave
Each case below repeated a rule another test in CI already proves, read
assertion by assertion next to it in the 2026-10-13 test audit:

- test_job_window: laser_arm_test.c case I (S changes, and now M5 then
  M3, inside the window never re-prompt; df7087b) and laser_stream_test
  session K (M2 closes the window, the next job re-arms).
- test_hold_grace: test_resume_after_grace makes the same assertion.
- test_verdict_blocks_arm, test_arm_waits_for_engine_ack,
  test_arm_proceeds_on_a_late_ack: laser_arm_test.c cases C, A2, A3 and
  cooling_test.c's arm-time gate. test_arm_waits_for_the_fans stays.
- test_sender_change: laser_arm_test.c case H; the disarm on a client
  change is unconditional.
- test_jog_does_not_extend_window: laser_arm_test.c case S10.
- test_verdict_fail_tier_ends_job: laser_stream_test rule 25 and
  laser_arm_test.c case V2.
- test_verdict_pause_resumes_without_press: laser_stream_test rule 24,
  which now counts one arm across the pause, and case V1.
- test_sender_change_mid_job: merged into
  test_sender_change_holds_and_rearms, which takes its "held inside the
  move" position check.
- test_sender_change_then_reset: the window is closed and the latch
  locked by the sender change itself (case H); a reset from Hold is not
  energy.

The header's rules 1, 2, 5, 11, 12 and 14 keep their numbers and name
the tests that now prove them.

Proof: laser_lifecycle_test passes whole on a null-sink build of the
grblHAL-glowforge working tree under Linux (17 sessions), and
laser_stream_test with it. pyflakes is clean; no helper was left
without a caller.

Acceptance: none. The harness runs in grblHAL-glowforge's CI.
2026-10-13 19:10:00 -04:00
ScottW514 280ceee849 laser_stream_test: rule 24's pause holds the window open: one arm
Rule 24 proved the pause tier holds lit to the stop, masks FIRE while
held, never writes the latch and resumes lit on the clean verdict. What
only laser_lifecycle_test's verdict-pause session held was that the
window stayed open through it: the resume armed nothing new. Rule 24 now
counts the arms, so that session can leave.

- verdict-rehold: exactly one "laser armed" across the pause, the
  re-hold and the resume.

Proof: laser_stream_test passes whole on a null-sink build of the
grblHAL-glowforge working tree under Linux. pyflakes is clean.

Acceptance: none. The harness runs in grblHAL-glowforge's CI.
2026-10-13 19:10:00 -04:00
ScottW514 8149a44f93 laser_stream_test: session B's FIRE-gap check moves to its density twin
Session B ran JOB_M3_TERM under the analog model; session F runs the
same job under density and checked its span, density and termination,
but not check_fire_gaps (no stepless run carrying FIRE), which B alone
held. F now runs it, and B goes: analog never runs on a machine, and
the analog ladder keeps its own termination and gap checks.

- check_fire_gaps on density-full, its worst gap in the PASS line.
- Session B's run and checks go; the JOB_M3_TERM comment names F.

Proof: the harness passes whole on a null-sink build of the
grblHAL-glowforge working tree under Linux, density-full with a worst
fire gap of 270 ticks. pyflakes is clean.

Acceptance: none. The harness runs in grblHAL-glowforge's CI.
2026-10-13 19:01:30 -04:00
ScottW514 c91f6daa9e laser_stream_test: rule 14 runs under the density model
Rule 14 (a level set while the stream is idle still cuts at that level)
ran only under the analog model, which no machine runs: its check
counted fire ticks per analog duty. Under the shipped density model the
level is the FIRE density at full duty, and nothing proved a stale
level there.

- Session I runs JOB_IDLE_S under DENSITY_CONF_FLOORED and judges it
  with rule 18's check_density: every power byte at full duty, and each
  move's span at its own S's density (23/127, 46/127, 81/127), not the
  one before it. The stream still ends dark.
- The header's rule 14 says the level is judged as the FIRE density.

Proof: the harness passes whole on a null-sink build of the
grblHAL-glowforge working tree under Linux (densities 0.1812, 0.3624,
0.638 against 0.1811, 0.3622, 0.6378). A control judging the same
stream against the levels in reverse order fails. pyflakes is clean.

Acceptance: none. The harness runs in grblHAL-glowforge's CI.
2026-10-13 19:01:30 -04:00
ScottW514 f8dc21b309 forgetest: motion.button-hold-resume leaves the catalog
motion.button-hold-resume held on the machine that the button holds a
travel job and the next press resumes it to its target. The host proves
it now: laser_lifecycle_test rule 7 runs the same job (M5, G91, G1 X40
F300) through the toggle and lands it within 0.05 mm of +40 (a0a4edb),
beside the armed half that owns the pause and resume messages. Real
stepping stays with the kept motion tests; the physical button with
setup.check-switches.

- The test goes; no helper was its alone (start_move and Watch serve
  motion.lid-cancel-home, which keeps _LID_COVERS).
- laser.pause-resume-lid-cancel no longer requires it. The removed test
  required only motion.pacing, which motion.lid-cancel-home still
  brings.

Proof: the coverage lint reports 0 uncovered paths and 0 host-tested
row problems over 106 tests (its one failure is setup.check-lens's
src/lensfind.* entry, which the forgectrl pin bump clears); the gate
self-check refuses an empty artifact cleanly; pyflakes adds no finding.
The unit modules that load the whole catalog pass under Linux:
test_artifact, test_campaign, test_cooling_order, test_fixture,
test_manifest, test_operator.

Acceptance: motion.py's shared text and laser.pause-resume-lid-cancel's
own text move. Against the image on the bench that adds no run:
6a434bb already changed both laser.py's and motion.py's shared text.
2026-10-13 18:48:38 -04:00
ScottW514 a0a4edb322 laser_lifecycle_test: a travel job takes the button toggle and lands on its target
Rule 7 proved the button's pause/resume toggle on an armed job only, and
nothing checked that the move resumed from the hold reaches its target.
The catalog's motion.button-hold-resume held both on the machine with a
travel job; it can leave the catalog once the host does.

- test_button_pause_resume, after its armed half, runs the catalog's
  job: M5, G91, G1 X40 F300 (8 s). It must reach Run without asking for
  the arm press; a press holds it part-way; a second press resumes it to
  Idle within 0.05 mm of +40.
- Rule 7's line in the header and the case's docstring say so.

Proof: the case passes on a null-sink build of the grblHAL-glowforge
working tree under Linux (the armed half as before; the travel job held
at 8.747 mm and resumed to 39.999 mm). A control, the same job held and
soft-reset instead of resumed, ends at 8.747 mm, which the landing check
fails. pyflakes is clean.

Acceptance: none. The harness runs in grblHAL-glowforge's CI.
2026-10-13 18:42:23 -04:00
ScottW514 67ac8f4064 forgetest: events.button-telemetry leaves the catalog
events.button-telemetry held on the machine that one physical press
reaches the event stream as pressed then released through its 25 Hz
switch poll, and the telemetry event's fields. The host proves the
rules now:

- the press between two 5 Hz gathers, idle snapshot, scripted switch
  word: forgectrl events_test test_short_press (d8d07fc);
- the telemetry field string, and that a press the machine is waiting
  for (arming, armed, leased, a button wait) is no event: events_test
  test_button.

The physical button reaching the switch word stays in the catalog:
setup.check-switches.

- evmore.py, the test's own module, goes with its import in
  suite/__init__.py; nothing else imported it.
- Its covers (forgectrl src/events.*, main.c, status.*) stay covered:
  events.stream holds all three, and other catalog tests hold main.c
  and status.*. No HOST_TESTED row is needed.

Proof: the coverage lint reports 0 uncovered paths and 0 host-tested
row problems over 107 tests (its one failure is setup.check-lens's
src/lensfind.* entry, which the forgectrl pin bump clears); the gate
self-check refuses an empty artifact cleanly. The unit modules that load
the whole catalog pass under Linux: test_artifact, test_campaign,
test_cooling_order, test_fixture, test_manifest, test_operator.

Acceptance: the catalog hash moves; no remaining test's fingerprint
moves (the module goes whole).
2026-10-13 18:34:04 -04:00
ScottW514 0350b943d7 forgetest: laser.py's unused motion imports, and the two helpers no test calls
laser.py imported wait_state, wait_state_text, wait_left_state and
drain_text from motion.py and used none of them. wait_state and
drain_text stay in motion.py, which uses them; wait_state_text and
wait_left_state had no caller anywhere, so they go.

Proof: pyflakes no longer reports laser.py's four unused imports and
adds no finding. The coverage lint reports 0 uncovered paths and 0
host-tested row problems over 108 tests (its one failure is
setup.check-lens's src/lensfind.* entry, which the forgectrl pin bump
clears); the gate self-check refuses an empty artifact cleanly; the
forgetest host suite passes under Linux (539 tests).

Acceptance: the shared text of laser.py and motion.py moves, and with
motion.py's the tests of every module that imports it (motion, laser,
tray, updlock, and bedsize through homeoff). Against the image on the
bench that adds no run: 6a434bb already changed both files' shared
text.
2026-10-13 18:14:26 -04:00
ScottW514 35add7cad0 forgetest: laser.power-floor leaves the catalog; laser.py's dead names go
laser.power-floor read $$ off the controller and checked $35 is the
floor, that it lands at PWMSAR 12 or above, and that $31 is 0. The host
proves all of it now: laser_stream_test rule 18 reads $$ on a fresh
controller and after the arm (d7379d9), and laser_arm_test.c case J
holds a set floor key (honored, an out-of-range key falling back to the
default, 0 honored). forgectrl keeps no default for laser_floor_density:
an absent key is the controller's own floor.

- Its own lines go, and the names only it or the removed tests read:
  _floor, _grbl_settings, FLOOR_DEFAULT with the comment above it,
  PWM_PERIOD, PWMSAR_FLOOR_MIN, DISARMED_MSG, and the extra blank lines
  the earlier own-lines removals left.
- The module docstring no longer carves laser.power-floor out as the one
  test that does not emit; it says the live tests here emit, since
  laser.recorder-dark never fires.
- HV_DARK_MAX stays: it is the emission witness's limit.

Proof: the fingerprint parts of laser.py before and after: the shared
text changed, and every remaining test's own text is unchanged. The
coverage lint reports 0 uncovered paths and 0 host-tested row problems
over 108 tests (its one failure is setup.check-lens's src/lensfind.*
entry, which the forgectrl pin bump clears); the gate self-check refuses
an empty artifact cleanly; pyflakes adds no finding; the forgetest host
suite passes under Linux (539 tests).

Acceptance: laser.py's shared text moves the fingerprints of
laser.emission-witness, laser.armed-kill, laser.pause-resume-lid-cancel
and laser.recorder-dark. Against the image on the bench that adds no
run: 6a434bb already changed laser.py's shared text, so all four re-run
in the next campaign either way.
2026-10-13 18:03:51 -04:00
ScottW514 d7379d9fc0 laser_stream_test: rule 18 reads $31 and the floor off the controller
Rule 18 proved a typed $35 is overwritten at the arm and the ladder
renders through the floor key, but took the shipped settings on faith:
the harness mirrors $31 = 0 and the board's 10 % floor as constants. The
catalog's laser.power-floor read them off the machine; it can leave the
catalog once the host does.

- The floor-derived session reads $$ on the fresh controller (no floor
  key: the shipped default) and again after the arm overwrote the typed
  $35. Both reads must show $31 = 0 (the floor, not $31, sets the bottom
  of the S range) and $35 = the board's floor, and that floor must land
  at PWMSAR_FLOOR_MIN (12) or above.
- A set floor key is laser_arm_test.c case J's (12.5 honored, an
  out-of-range key falling back to 10, 0 honored). forgectrl keeps no
  default for laser_floor_density: an absent key is the controller's
  own floor, so there is no second default to compare.

Proof: laser_stream_test passes whole on a null-sink build of the
grblHAL-glowforge working tree under Linux ("$31=0 and $35=10 (PWMSAR
12) fresh and after the arm"). A control session with a floor key of 5
reads back $35 = 5 both times through the same parse, which rule 18
would fail. pyflakes is clean.

Acceptance: none. The harness runs in grblHAL-glowforge's CI.
2026-10-13 17:49:33 -04:00
ScottW514 21f53e4e83 forgetest: laser.verdict-cut leaves the catalog; the stream rules hold its claim
laser.verdict-cut held, on the machine, that a stale cooling verdict
mid-cut holds the job dark under the open window with the latch
untouched, and that the clean verdict resumes it lit with no press.
Every part of that is a rule the host now proves:

- dark while held: laser_stream_test rules 24 and 27, check_hold_dark on
  every verdict hold's stationary stretch, and check_fire_gaps (ef15c55);
- the hold, the message, the window kept, the latch never written, the
  resume lit with no press: rules 24 and 27 (latch sideband [unlock,
  lock]), laser_lifecycle_test rule 11, cooling_test.c;
- the physical dwell gap (a kernel-run gap inside the unlocked window,
  the button latch clear, HV_ENABLE back lit): laser.emission-witness,
  unchanged.

- Its own lines go, decorator through body, and nothing else:
  DISARMED_MSG, the module constant only its check read, stays with the
  blank lines around it, so laser.py's shared text is unchanged.
- test_laser_verdict.py goes: every case loaded dark_span out of
  verdict_cut's body.

Proof: the fingerprint parts of laser.py before and after: the shared
text unchanged, the other five tests' own text unchanged. The coverage
lint reports 0 uncovered paths and 0 host-tested row problems over 109
tests (its one failure is setup.check-lens's src/lensfind.* entry, which
the forgectrl pin bump clears); the gate self-check refuses an empty
artifact cleanly; pyflakes adds no finding; the forgetest host suite
passes under Linux (539 tests).

Acceptance: the catalog hash moves; no remaining test's fingerprint
moves.
2026-10-13 17:14:13 -04:00
ScottW514 ef15c55577 laser_stream_test: the verdict's holds ship no FIRE while held
Rules 24 (the pause tier, held and held again) and 27 (a stale verdict)
asserted the deceleration into each hold ran lit to the stop, the
stretch between the holds dark, and the cut lit after the clear, but not
that the stream is dark inside a hold: the central claim of the
catalog's laser.verdict-cut, which can leave the catalog once the host
proves it. Rule 21 proved it only for a plain feed hold.

- check_hold_dark: no FIRE tick inside a hold's stationary stretch, its
  edges aside (one step pulse straddles each edge: rule 21's margin,
  HOLD_EDGE_TICKS, 120 ticks at x8 scaled to the mode: 480 at the x32
  default).
- Rule 24 checks both of its holds; rule 27 checks its one hold.
- check_fire_gaps runs on both sessions, as on rule 21's.
- The header's rules 24 and 27 say so; their PASS lines report the held
  stretches.

Proof: laser_stream_test passes whole on a null-sink build of the
grblHAL-glowforge working tree under Linux: verdict-rehold dark while
held for 22620 and 22622 ticks, verdict-stale for 22621, check_fire_gaps
clean on both. A negative control on check_hold_dark: a dark 5000-tick
hold passes, and one FIRE tick inside it fails. pyflakes is clean.

Acceptance: none. The harness runs in grblHAL-glowforge's CI.
2026-10-13 16:55:50 -04:00
ScottW514 571f702113 forgetest: the emission witness samples a rapid after its M5; m5-rapid-dark leaves
The rule that a rapid after an M5 ships no FIRE is the host's (laser
stream rule 16). What only the machine shows is LASER_ON staying off
with FIRE clear while the chain can fire; kernel.fire-line's dark
sample runs with the chain unarmed. laser.m5-rapid-dark held that
sample in a test of its own, with its own arm and press. The emission
witness, which every campaign runs, now holds it.

- The square ends at M5 with no program end, so the window stays open.
- The dark point: the LASER_ON count reads 0 after the M5, the window
  still armed, and the SoC lock line released (laser_locked false: the
  engine's armed flag can read true after the latch has locked, and a
  dark reading with the latch locked is the unarmed case). All three
  are checked before anything else is sent.
- Only then is the rapid sent: G0 X20, a 2.5 s dwell, G0 X-20, a 0.5 s
  dwell, the program end. Sending it after the dark point keeps the
  mid-square dwell's own zero from being taken for the dark point.
- The test samples until the window closes (a dwell reports Idle, so
  Idle cannot end it), with the controller's machine X beside each
  sample.
- Every sample from the dark point until the window closes reads no
  emission, and the HV current stays at its idle reading (HV_DARK_MAX);
  the reports show the head out at +20 mm and back at the start inside
  that stretch, and the latch reads released on the sample at +20 mm.
- The square still has to show emission and an HV rise, judged on the
  samples before the M5; the beam detector is judged there too.
- The job-based disarm is timed from the head being back: under 10 s,
  where the idle grace is 60 s.
- The rapid's list and its sampler live inside the test, and the
  deletion takes only m5-rapid-dark's own lines (decorator through
  body), so laser.py's shared text is byte-for-byte unchanged.
- live_fire_drills.py's m5dark note and the bench README say the
  emission witness now carries the drill's check.

Proof: the fingerprint parts of laser.py before and after: the shared
text unchanged, every other test's own text unchanged. In the
controller, glowforge_laser.c keeps the latch unlocked from the job's
first laser-on through M5/M3 toggles and relocks it at program end or
after the laser_disarm_s spindle-off grace (60 s by default), so the
dark point and the rapid fall inside the window. The coverage lint
reports 0 uncovered paths over 110 tests (its one failure is
setup.check-lens's src/lensfind.* entry, which the forgectrl pin bump
clears); pyflakes adds no finding; the forgetest host suite passes
under Linux.

Acceptance: only laser.emission-witness moves, and as an always-required
test it must pass on the bench in the next campaign before a release
can inherit its PASS.
2026-10-13 16:30:45 -04:00
ScottW514 6a434bbf77 forgetest: nineteen tests a host test already proves leave the catalog
The catalog is for what needs the machine or an operator. Each test
below asserted software rules that a host test in CI already proves,
read assertion by assertion next to that host test before it left; the
record of that reading is kept with the audit.

Safety rules (each approved for deletion by the owner on that reading):
- laser.disarm-in-hold: laser_lifecycle_test test_resume_after_grace.
- laser.arm-wait-lid: laser_arm_test E-G, laser_lifecycle_test
  test_lid_open_in_wait.
- laser.port-dark: ctlport_test test_port_dark (port jogs, a long jog
  and its cancel, four injected words, the panel ops: no FIRE tick
  under an open window with M3 modal).
- motion.respawn-gate: super_test A and D.
- motion.interlock-cancel-home: the lifecycle loop cases, laser_arm_test G.
- motion.lid-policy-hold: laser_lifecycle_test test_lid_policy_hold.
- cloud.lid-during-button-wait, cloud.dark-print, cloud.verdict-refuse,
  cloud.paused-lid-cancel: test_machine_lid_button.py.
- cooling.fan-duty-readback: cool_fanwrite_test.
- cooling.intake-one-fan: cool_intake_test A-E.
- exthost.hold-pause-tier: holdkeep_test, run_test.py, cool_flow_test P,
  holds_test.

Other rules:
- homing.cloud-offsets: manual_home_test [own-offsets],
  test_gfhome_motion; the real session stays in cloud.mode-switch.
- exthost.core-range: version_test, install_test.
- exthost.operator-destinations: run_test.py, netrules_test,
  install_test, extpkg_test.
- exthost.lifecycle: run_test.py's ext.shutdown read before the stop.
- exthost.mcode: the null-sink mcode_test, forgectrl mcode_test,
  sdk_test, install_test.
- motion.soft-limits: z_envelope_test's homed cases, which now assert
  no step before the alarm, and envelope_test.

- cooling_intake.py, extcore.py, extdest.py, extlife.py and extmcode.py
  go whole; homeoff.py keeps only the camera-home helpers that
  cloud.mode-switch, setup.check-envelope and their meta-tests use.
- The helpers only the removed tests used go with them.
- exthost.armed-freeze still runs the cloud suite's dark-print helper,
  whose latch checks still run; its description says so. It now
  requires cooling.floor-and-warm-up, the dark print's own prerequisite,
  in place of the removed cloud.dark-print.
- HOST_TESTED: forgectrl src/holds.* (holds_test) and src/mcode.*
  (mcode_test), whose only catalog home left with these tests.
- The meta-tests that replayed the removed tests go:
  test_cooling_intake_suite.py, the cloud replay cases and their
  fixture. The one that catches a false PASS stays and runs the dark
  print as armed-freeze does: a latch open at the button fails.
- Text that named a removed test (exthost.motion-job's description and
  comment, test_baseline's docstring) no longer does.

Proof: the coverage lint on a manifest from the recipe pins reports 0
uncovered paths and 0 host-tested row problems over 111 tests (its one
failure is setup.check-lens's src/lensfind.* entry, which the forgectrl
pin bump clears); pyflakes adds no finding; the gate self-check refuses
an empty artifact cleanly; the forgetest host suite passes under Linux
(547 tests); the catalog loads with 111 tests after armed-freeze's
description edit.

Acceptance: the catalog hash moves, so the next image opens a new
campaign. Every remaining test in laser.py, motion.py, cloud.py,
cooling.py, exthost.py, homeoff.py and bedsize.py moves its
fingerprint (their modules' shared text changed); exthost.armed-freeze
moves by its requires.
2026-10-13 16:05:14 -04:00