Spaces:
Runtime error
Runtime error
| """Headless tests for the deterministic core (no Ollama required). | |
| Exercises retrieval, the Spine veto, the offline-fallback advisor, and the | |
| reflection append. Run: `make test` (= `uv run python test_core.py`). | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import tempfile | |
| from pathlib import Path | |
| # Force the deterministic fallback path so this suite is truly offline + fast | |
| # even when `ollama serve` is running (otherwise advise() would do a real, slow | |
| # model call and appear to hang). Set before importing modules that call the LLM. | |
| os.environ.setdefault("CHIEF_ENGINEER_OFFLINE", "1") | |
| from core.chief_engineer import advise | |
| from core.ledger import LedgerManager | |
| from core.models import Environment, Job, PrintSettings | |
| from core.reflect import reflect_on_job | |
| from core.seed_lessons import ensure_seeded | |
| from core.spine import SpineValidator | |
| def test_seed_and_retrieve(): | |
| led = LedgerManager(Path(tempfile.mkdtemp()) / "lessons.jsonl") | |
| n = ensure_seeded(led) | |
| assert n == 12, f"expected 12 seeds, got {n}" | |
| # PLA overhang in a warm room → should match the warm-room sag seed nearest | |
| hits = led.retrieve("PLA", "overhang", temp=28, humidity=50) | |
| assert hits, "expected precedent for PLA/overhang" | |
| assert hits[0][0].geometry_type == "overhang" and hits[0][0].material == "PLA" | |
| # a material+geometry with no seeds → empty (valid 'no precedent' case) | |
| assert led.retrieve("TPU", "vase", 22, 45) == [] | |
| print("✓ seed + retrieval (nearest:", hits[0][0].job_id, f"dist {hits[0][1]:.2f})") | |
| def test_spine_veto(): | |
| s = SpineValidator() | |
| # model proposes a PLA nozzle way too hot → must clamp to 220 and trip approval | |
| bad = PrintSettings(nozzle_temp=260, bed_temp=60, retraction_mm=5, fan_pct=100, first_layer_fan_pct=0) | |
| res = s.check(bad, "PLA") | |
| assert res.settings.nozzle_temp == 220, res.settings.nozzle_temp | |
| assert res.requires_approval and res.vetoes | |
| print("✓ spine clamps PLA 260→220 and trips HITL:", res.vetoes[0]) | |
| def test_fallback_advise(): | |
| led = LedgerManager(Path(tempfile.mkdtemp()) / "lessons.jsonl") | |
| ensure_seeded(led) | |
| job = Job(geometry_type="overhang", material="PLA", description="45° bracket") | |
| env = Environment(temp=28, humidity=50) | |
| rec = advise(job, env, led.retrieve("PLA", "overhang", 28, 50)) | |
| assert rec.used_fallback, "no Ollama here → should use fallback" | |
| assert rec.advice.settings.nozzle_temp > 0 and rec.advice.risks | |
| print("✓ fallback advise:", rec.advice.reasoning[:70], "…") | |
| def test_reflect_appends(): | |
| led = LedgerManager(Path(tempfile.mkdtemp()) / "lessons.jsonl") | |
| ensure_seeded(led) | |
| before = led.count()["earned"] | |
| job = Job(geometry_type="bridge", material="PETG") | |
| env = Environment(temp=24, humidity=44) | |
| settings = PrintSettings(nozzle_temp=235, bed_temp=80, retraction_mm=4, fan_pct=70, first_layer_fan_pct=0) | |
| entry = reflect_on_job(job, env, settings, "success", led) | |
| assert led.count()["earned"] == before + 1 and entry.source == "earned" | |
| print("✓ reflect appends earned lesson:", entry.lesson[:70], "…") | |
| def test_retrieval_orders_by_env_distance(): | |
| led = LedgerManager(Path(tempfile.mkdtemp()) / "lessons.jsonl") | |
| ensure_seeded(led) | |
| # PLA/stringing seeds sit at (22,45) and (24,70). A humid query should rank | |
| # the humid seed first; a dry query the dry one. | |
| humid = led.retrieve("PLA", "stringing", temp=24, humidity=70) | |
| dry = led.retrieve("PLA", "stringing", temp=22, humidity=45) | |
| assert humid[0][0].env_humidity >= 65, humid[0][0].env_humidity | |
| assert dry[0][0].env_humidity <= 50, dry[0][0].env_humidity | |
| print("✓ retrieval ranks by normalized env distance (humid→humid, dry→dry)") | |
| def test_gcode_readout_ties_to_settings(): | |
| from core.viewer import gcode_readout | |
| s = PrintSettings(nozzle_temp=205, bed_temp=60, retraction_mm=5, fan_pct=100, first_layer_fan_pct=0) | |
| g = gcode_readout(s, "PLA") | |
| assert "M104 S205" in g and "M140 S60" in g, g | |
| assert "layer height 0.20 mm" in g, g | |
| print("✓ g-code header is populated from proposed settings") | |
| # GIF export button removed; keep motion preview tests via UI smoke if needed. | |
| def test_virtual_printer_html_ties_to_settings(): | |
| from core.widgets import virtual_printer_html | |
| from core.viewer import generate_primitive | |
| from core.models import PrintSettings | |
| mesh, _geo = generate_primitive("box", 20) | |
| default_html = virtual_printer_html(mesh) | |
| assert "0.20 mm layers" in default_html, default_html | |
| fine = PrintSettings(nozzle_temp=200, bed_temp=60, retraction_mm=4.5, fan_pct=80, | |
| first_layer_fan_pct=0, layer_height=0.12) | |
| fine_html = virtual_printer_html(mesh, settings=fine) | |
| assert "0.12 mm layers" in fine_html, fine_html | |
| print("✓ virtual-print preview layer height follows PrintSettings") | |
| def test_ingest_distiller(): | |
| from pathlib import Path as _P | |
| from ingest.distill import parse_prusa_ini, parse_klipper_cfg, parse_marlin_config | |
| samples = _P(__file__).resolve().parent / "ingest" / "samples" | |
| prusa = parse_prusa_ini(samples / "prusa_filaments.ini") | |
| assert any(f.material == "PLA" and f.param == "bed_temp" for f in prusa), "PLA bed_temp not parsed" | |
| assert parse_klipper_cfg(samples / "klipper_extruder.cfg"), "klipper max_temp not parsed" | |
| assert parse_marlin_config(samples / "marlin_config.h"), "marlin maxtemp not parsed" | |
| print("✓ distiller parses Prusa INI + Klipper cfg + Marlin config") | |
| def test_precedent_eval_narration(): | |
| from core.viewer import precedent_eval_html | |
| from core.models import LessonEntry as LE, Environment as E | |
| e = LE(job_id="x", material="PLA", geometry_type="overhang", env_temp=28, env_humidity=50, | |
| outcome="failed_sag", lesson="sagged", source="seed", timestamp="t") | |
| html = precedent_eval_html([(e, 0.28)], E(temp=32, humidity=62)) | |
| assert "warmer" in html and "more humid" in html and "worse" in html, html | |
| assert "NO CLOSE PRECEDENT" in precedent_eval_html([], E(temp=22, humidity=45)) | |
| print("✓ precedent evaluation narrates env delta + novel case") | |
| def test_simulator_physical_and_deterministic(): | |
| from sim.outcome import simulate | |
| from core.models import Job as J, Environment as E, PrintSettings as PS | |
| bad = PS(nozzle_temp=235, bed_temp=80, retraction_mm=4, fan_pct=40, first_layer_fan_pct=0) | |
| r1 = simulate(bad, J(geometry_type="bridge", material="PETG"), E(temp=29, humidity=62)) | |
| assert r1.outcome != "success" and r1.quality < 0.7, r1 | |
| r2 = simulate(bad, J(geometry_type="bridge", material="PETG"), E(temp=29, humidity=62)) | |
| assert (r2.outcome, r2.quality) == (r1.outcome, r1.quality), "simulator must be deterministic" | |
| good = PS(nozzle_temp=205, bed_temp=60, retraction_mm=5, fan_pct=100, first_layer_fan_pct=0) | |
| rg = simulate(good, J(geometry_type="overhang", material="PLA"), E(temp=20, humidity=40)) | |
| assert rg.outcome == "success", rg | |
| # build-plate position: corner > edge > center warp for a shrink-prone material; | |
| # 'center' (default) must be unchanged. | |
| abs_s = PS(nozzle_temp=248, bed_temp=95, retraction_mm=4, fan_pct=20, first_layer_fan_pct=0) | |
| env = E(temp=22, humidity=40) | |
| qc = simulate(abs_s, J(geometry_type="adhesion", material="ABS", bed_position="center"), env).quality | |
| qe = simulate(abs_s, J(geometry_type="adhesion", material="ABS", bed_position="edge"), env).quality | |
| qk = simulate(abs_s, J(geometry_type="adhesion", material="ABS", bed_position="corner"), env).quality | |
| assert qc > qe > qk, (qc, qe, qk) | |
| assert qc == simulate(abs_s, J(geometry_type="adhesion", material="ABS"), env).quality | |
| print("✓ simulator is physical + deterministic, and bed-position warps edges/corners") | |
| def test_policy_learns_and_generalizes(): | |
| import tempfile, os | |
| from pathlib import Path | |
| from learn.policy import LearnedPolicy | |
| from learn.loop import run_session, run_iteration | |
| from core.ledger import LedgerManager | |
| from core.models import Job as J, Environment as E | |
| d = Path(tempfile.mkdtemp()) | |
| pol = LearnedPolicy(path=d / "policy.json") | |
| led = LedgerManager(path=d / "lessons.jsonl") | |
| job, env = J(geometry_type="bridge", material="PETG"), E(temp=29, humidity=62) | |
| sess = run_session(job, env, 10, pol, led) | |
| assert sess.trajectory[-1] > sess.trajectory[0], "quality must improve" | |
| assert sess.first_success is not None, "should reach a clean print" | |
| # generalization: a similar (same-bucket, different exact env) job benefits | |
| cold = sess.trajectory[0] | |
| warm_start = run_iteration(J(geometry_type="bridge", material="PETG"), | |
| E(temp=28, humidity=58), pol, led, 1, record=False) | |
| assert warm_start.result.quality > cold, "policy must transfer to similar conditions" | |
| print("✓ policy learns (quality climbs to a clean print) and generalizes to similar jobs") | |
| if __name__ == "__main__": | |
| test_seed_and_retrieve() | |
| test_spine_veto() | |
| test_fallback_advise() | |
| test_reflect_appends() | |
| test_retrieval_orders_by_env_distance() | |
| test_gcode_readout_ties_to_settings() | |
| test_virtual_printer_html_ties_to_settings() | |
| test_ingest_distiller() | |
| test_precedent_eval_narration() | |
| test_simulator_physical_and_deterministic() | |
| test_policy_learns_and_generalizes() | |
| print("\nALL CORE TESTS PASSED") | |