forked from viewit/KX-Bridge-Release
server/files/metadata returned broken placeholders (size: 1, estimated_time: null) for any file that wasn't uploaded through the bridge's own GCode store - e.g. prints started directly from Anycubic Slicer Next. Verified live against a real Kobra X (see memory reference_buried_report_trigger.md) that the printer sends a previously-unused MQTT topic, buried/report, exactly once per print start - fires identically whether the print was started via Anycubic Slicer Next or via OrcaSlicer/the bridge itself. It carries gcode_size, estimate_duration, and total_layers: precisely the fields the metadata endpoint was missing. Add _on_buried() (registered alongside the existing file/report callback) that caches the single most recent buried/report payload. _build_file_metadata() now tries this cache - matched by task_name - as a third fallback, between the existing GCodeStore lookup and the final size:1 hardcoded placeholder. Ordering is deliberate: live tracked-job state and the file's own GCodeStore row (if the file was uploaded through the bridge) still take priority; buried/report only fills the gap for files the bridge has no other record of. Also surfaces the printer's own storage usage (storage_total_mb/ storage_used_mb from the same payload) in /api/state, previously not exposed anywhere in the bridge. Verified end-to-end against the real printer: after a print start, server/files/metadata for that file returned real size (9573908), estimated_time (3203s), and layer_count (497) instead of the placeholders, and /api/state reported real storage_total_mb/ storage_used_mb.
112 lines
4.0 KiB
Python
112 lines
4.0 KiB
Python
"""
|
|
Tests für buried/report — das druckerseitige Analytics-Event, das einmal pro
|
|
Druckstart feuert (verifiziert live gegen einen echten Kobra X, sowohl für
|
|
Anycubic Slicer Next als auch für OrcaSlicer/die Bridge selbst). Liefert
|
|
gcode_size/estimate_duration/total_layers, die server/files/metadata für
|
|
Dateien außerhalb des eigenen GCodeStore sonst nicht hat (Issue #102).
|
|
"""
|
|
import pytest
|
|
|
|
|
|
BURIED_PAYLOAD = {
|
|
"type": "buried",
|
|
"action": "PrintStart",
|
|
"code": 200,
|
|
"state": "done",
|
|
"data": {
|
|
"task_name": "Smileys_simple_plate(01)_PLA_0.2_16m39s.gcode",
|
|
"gcode_size": 644437,
|
|
"estimate_duration": 1264,
|
|
"total_layers": 8,
|
|
"storage_total": 6481,
|
|
"storage_used": 898,
|
|
"slicer": "OrcaSlicer",
|
|
},
|
|
}
|
|
|
|
|
|
def test_on_buried_populates_cache(client):
|
|
_, bridge = client
|
|
bridge._on_buried(BURIED_PAYLOAD)
|
|
assert bridge._buried_cache == {
|
|
"task_name": "Smileys_simple_plate(01)_PLA_0.2_16m39s.gcode",
|
|
"gcode_size": 644437,
|
|
"estimate_duration": 1264,
|
|
"total_layers": 8,
|
|
}
|
|
|
|
|
|
def test_on_buried_populates_storage_state(client):
|
|
_, bridge = client
|
|
bridge._on_buried(BURIED_PAYLOAD)
|
|
assert bridge._state["storage_total_mb"] == 6481
|
|
assert bridge._state["storage_used_mb"] == 898
|
|
|
|
|
|
def test_on_buried_ignores_payload_without_task_name(client):
|
|
_, bridge = client
|
|
bridge._buried_cache = None
|
|
bridge._on_buried({"type": "buried", "data": {"gcode_size": 123}})
|
|
assert bridge._buried_cache is None
|
|
|
|
|
|
def test_build_file_metadata_uses_buried_fallback_for_unknown_file(client):
|
|
"""A file not in the GCodeStore and not the currently-tracked job should
|
|
still get real size/estimated_time/layer_count from the buried cache."""
|
|
_, bridge = client
|
|
bridge._on_buried(BURIED_PAYLOAD)
|
|
meta = bridge._build_file_metadata("Smileys_simple_plate(01)_PLA_0.2_16m39s.gcode")
|
|
assert meta["size"] == 644437
|
|
assert meta["estimated_time"] == 1264
|
|
assert meta["layer_count"] == 8
|
|
|
|
|
|
def test_build_file_metadata_ignores_buried_cache_for_different_file(client):
|
|
"""The buried cache must only apply when task_name matches the queried
|
|
filename - otherwise it would leak the last print's data into an
|
|
unrelated query, the exact bug Issue #102 already fixed for live state."""
|
|
_, bridge = client
|
|
bridge._on_buried(BURIED_PAYLOAD)
|
|
meta = bridge._build_file_metadata("some_other_file.gcode")
|
|
assert meta["size"] == 1 # unchanged fallback, not leaked from buried cache
|
|
assert meta["estimated_time"] is None
|
|
assert meta["layer_count"] is None
|
|
|
|
|
|
def test_build_file_metadata_prefers_gcodestore_over_buried(client):
|
|
"""GCodeStore data (from the bridge's own upload) must win over the
|
|
buried cache when both are available for the same filename."""
|
|
_, bridge = client
|
|
bridge._on_buried(BURIED_PAYLOAD)
|
|
with bridge._store._lock:
|
|
bridge._store._conn.execute(
|
|
"INSERT INTO gcode_files (id, filename, path, size_bytes, uploaded_at, layer_count, est_print_time_sec) "
|
|
"VALUES (?,?,?,?,?,?,?)",
|
|
("f1", "Smileys_simple_plate(01)_PLA_0.2_16m39s.gcode", "/tmp/f1", 999999, "2026-01-01T00:00:00Z", 42, 5000),
|
|
)
|
|
bridge._store._conn.commit()
|
|
meta = bridge._build_file_metadata("Smileys_simple_plate(01)_PLA_0.2_16m39s.gcode")
|
|
assert meta["size"] == 999999
|
|
assert meta["estimated_time"] == 5000
|
|
assert meta["layer_count"] == 42
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_api_state_reports_storage_after_buried_report(client):
|
|
c, bridge = client
|
|
bridge._on_buried(BURIED_PAYLOAD)
|
|
resp = await c.get("/api/state")
|
|
assert resp.status == 200
|
|
data = await resp.json()
|
|
assert data["storage_total_mb"] == 6481
|
|
assert data["storage_used_mb"] == 898
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_api_state_storage_defaults_to_zero(client):
|
|
c, _ = client
|
|
resp = await c.get("/api/state")
|
|
data = await resp.json()
|
|
assert data["storage_total_mb"] == 0
|
|
assert data["storage_used_mb"] == 0
|