fix(station): Signiervariante aus Produkt-ID und Version bestimmen

Eine Entwicklungs-G95 meldete sich am 2026-09-15 mit einer Produkt-ID OHNE
"-dev", aber einer Version MIT "-dev". products.py leitete die Variante
ausschliesslich aus der Produkt-ID ab, die Lampe galt als Produktionsgeraet,
bekam das produktionssigniertes Image und lehnte es ab - ihr Bootloader prueft
gegen den Entwicklungsschluessel.

Die Variante ist damit keine Eigenschaft der Produkt-ID, sondern der Anfrage.
PRODUCTS bildet nur noch auf Geraetetyp und Anzeigename ab; beide Schreibweisen
der Produkt-ID fuehren auf denselben Eintrag. classify_variant() in
parse_request() wertet Produkt-ID UND Version aus: "-dev" an einer der beiden
Stellen bedeutet Entwicklungsgeraet, in der Version nur als ganzes Wort
("-devel"/"-dev2" zaehlen nicht).

Damit eine Fehlklassifikation nicht wieder erst an der Lampe auffaellt, fuehren
status.json (last_seen und jeder recent-Eintrag) und das Panel die Quelle der
Einstufung mit: "g95_cw (dev/Ver)", "dev/ID", "dev/ID+Ver" oder "prod".

Tests: die drei am Geraet mitgeschnittenen Anfragen plus die vierte denkbare
Kombination, die Wortgrenze in der Version, zwei Ende-zu-Ende-Tests im Server
(dev-Image statt prod-Image) und die Panel-Beschriftung.
This commit is contained in:
Michael Sauer
2026-09-15 00:24:29 +02:00
parent 6f0137908c
commit 6325ce073f
6 changed files with 216 additions and 33 deletions
+34
View File
@@ -594,6 +594,40 @@ def test_migration_auto_serves_known_product(migration_server):
assert st["last_seen"]["variant"] == "dev"
def test_migration_dev_only_in_version_serves_the_dev_image(migration_server):
"""Der Hardwarebefund vom 2026-09-15: Produkt-ID OHNE "-dev", Version MIT. Frueher
ging hier das produktionssignierte Image raus (200, und die Lampe lehnte es ab) -
jetzt muss das dev-signierte gehen, und status.json muss die Quelle mitfuehren."""
srv, imgdir = migration_server
(imgdir / "migrate").mkdir(exist_ok=True)
(imgdir / "migrate" / "g95_cw_dev.img").write_bytes(b"DEV" * 1000)
(imgdir / "migrate" / "g95_cw_prod.img").write_bytes(b"PROD" * 1000)
r = srv.get("/qcx_firmware_-klyqa.lighting.cw-ww.g95_V1.1.1.2357-dev_ota_light.img")
assert r.status == 200
assert r.body == b"DEV" * 1000
st = srv.status(lambda s: s.get("last_seen") is not None)
assert st["last_seen"]["target"] == "g95_cw"
assert st["last_seen"]["variant"] == "dev"
assert st["last_seen"]["variant_source"] == "version"
assert st["recent"][-1]["variant"] == "dev"
assert st["recent"][-1]["variant_source"] == "version"
def test_migration_production_lamp_still_gets_the_prod_image(migration_server):
"""Die erfolgreich migrierte GU10: nirgends ein "-dev", also prod - und die Quelle
"none", damit am Panel/in status.json steht, worauf die Einstufung beruht."""
srv, imgdir = migration_server
(imgdir / "migrate").mkdir(exist_ok=True)
(imgdir / "migrate" / "gu10_rgb_dev.img").write_bytes(b"DEV" * 1000)
(imgdir / "migrate" / "gu10_rgb_prod.img").write_bytes(b"PROD" * 1000)
r = srv.get("/qcx_firmware_-klyqa.lighting.rgb-cw-ww.gu10_V1.1.0.1138_ota_light.img")
assert r.status == 200
assert r.body == b"PROD" * 1000
st = srv.status(lambda s: s.get("last_seen") is not None)
assert st["last_seen"]["variant"] == "prod"
assert st["last_seen"]["variant_source"] == "none"
def test_migration_listen_records_but_serves_nothing(migration_server):
srv, imgdir = migration_server
(imgdir / "migrate").mkdir(exist_ok=True)