diff --git a/docs/BRINGUP.md b/docs/BRINGUP.md index 08dbff3..28a402b 100644 --- a/docs/BRINGUP.md +++ b/docs/BRINGUP.md @@ -1443,6 +1443,25 @@ The coverage currency rule is in `CLAUDE.md` "Working rules". Bench validation and the bench-tab ports are Next work item 15. +**Bench campaign opened 2026-08-15 on the flashed dev image +`20260815194415` (manifest identity `2d69a61e…`, equal to the release +build's).** The tool came up on `:8090` with all 24 tests required. +Passed so far, driven through the API with the operator present: +`image.health` (kernel options, module + 16 MiB ring, forgectrl holding +`/dev/glowforge`, K80 controllers before K90 forgectrl, 0600 token and +settings, 2.6 GiB free on /data), `kernel.latch-locked-idle` (interlock +`0x2d`, FIRE 0, LASER_ON 0/0, faults 0), `forgectrl.auth`, +`forgectrl.settings-bounds`, `forgectrl.panel-serves`, +`logs.tree-tail-export` (sanitized bundle carries no panel token), +`update.slots-and-signature` - 7 of 24. One finding, on the tool side: +`forgectrl.auth` first failed because it expected `/fuse-identity` to +answer 200 to the token alone; the endpoint is two-factor (token AND the +physical button held) by design, so the test now asserts both refusals +and never fetches the identity (a 200 would have put the fuse password +in the result log). That FAIL closed the first campaign, as the rules +say; the second campaign holds the passes. Next: the takeover drills, +motion, cooling, camera, cloud, then the live tests from the page. + ## Hardware facts bank (measured) - **DRV8825 stepper drivers wedge on 40 V rail glitches** (factory board; diff --git a/forgetest/forgetest/suite/forgectrl.py b/forgetest/forgetest/suite/forgectrl.py index 5299038..14ab202 100644 --- a/forgetest/forgetest/suite/forgectrl.py +++ b/forgetest/forgetest/suite/forgectrl.py @@ -28,7 +28,8 @@ def lan_ip(): covers=_COVERS_AUTH, description="Every state-changing endpoint refuses an unauthenticated write; a non-literal " "Host, a non-literal Origin and a cross-site Sec-Fetch-Site are refused; the " - "cooling report channel refuses a non-loopback peer; the fuse view is token-gated; " + "cooling report channel refuses a non-loopback peer; the fuse view is two-factor " + "(token and the physical button) and refused without either; " "the flash and factory-restore chain is refused unauthenticated.") def auth(ctx): fc = ctx.forgectrl @@ -74,14 +75,22 @@ def auth(ctx): st, body = fc.get("/status", headers={"Sec-Fetch-Site": "same-origin", "Origin": "http://127.0.0.1:8080"}) ctx.check(st == 200, "same-origin literal Origin refused (%s)", st) - # the token opens the fuse view; without it, refused + # the fuse view is two-factor: the token AND the physical button held. + # Without the token: authentication refused; with the token and nobody + # at the button: refused with the button message. The identity itself is + # never fetched (it would land in this log). st, body = fc.get("/fuse-identity", auth=False) ev["fuse_noauth"] = st - ctx.check(st == 403, "GET /fuse-identity without token -> %s", st) + ctx.log("GET /fuse-identity (no token) -> %s %s", st, body if isinstance(body, dict) else "") + ctx.check(st == 403 and isinstance(body, dict) and body.get("error") == "authentication required", + "GET /fuse-identity without token -> %s %r", st, body) st, body = fc.get("/fuse-identity") - ev["fuse_auth"] = st - ctx.log("GET /fuse-identity with token -> %s", st) - ctx.check(st == 200, "GET /fuse-identity with the token -> %s", st) + ev["fuse_token_no_button"] = st + ctx.log("GET /fuse-identity (token, button not held) -> %s %s", st, body if isinstance(body, dict) else "") + msg = body.get("error", "") if isinstance(body, dict) else str(body) + ctx.check(st == 403 and "button" in msg, + "GET /fuse-identity with the token but no button -> %s %r (expected the two-factor refusal)", + st, body) # the cooling report channel: loopback only, even with a token ip = lan_ip()