CoolFace
Apppublic

build-small-hackathon/microfactory-lab

sourceHugging Facemitupdated 3mo agoView on Hugging Face
2likes
test_core.py190 linesDownload Raw Back to root
1"""Headless tests for the deterministic core (no Ollama required).2 3Exercises retrieval, the Spine veto, the offline-fallback advisor, and the4reflection append. Run: `make test` (= `uv run python test_core.py`).5"""6 7from __future__ import annotations8 9import os10import tempfile11from pathlib import Path12 13# Force the deterministic fallback path so this suite is truly offline + fast14# even when `ollama serve` is running (otherwise advise() would do a real, slow15# model call and appear to hang). Set before importing modules that call the LLM.16os.environ.setdefault("CHIEF_ENGINEER_OFFLINE", "1")17 18from core.chief_engineer import advise19from core.ledger import LedgerManager20from core.models import Environment, Job, PrintSettings21from core.reflect import reflect_on_job22from core.seed_lessons import ensure_seeded23from core.spine import SpineValidator24 25 26def test_seed_and_retrieve():27    led = LedgerManager(Path(tempfile.mkdtemp()) / "lessons.jsonl")28    n = ensure_seeded(led)29    assert n == 12, f"expected 12 seeds, got {n}"30    # PLA overhang in a warm room → should match the warm-room sag seed nearest31    hits = led.retrieve("PLA", "overhang", temp=28, humidity=50)32    assert hits, "expected precedent for PLA/overhang"33    assert hits[0][0].geometry_type == "overhang" and hits[0][0].material == "PLA"34    # a material+geometry with no seeds → empty (valid 'no precedent' case)35    assert led.retrieve("TPU", "vase", 22, 45) == []36    print("✓ seed + retrieval (nearest:", hits[0][0].job_id, f"dist {hits[0][1]:.2f})")37 38 39def test_spine_veto():40    s = SpineValidator()41    # model proposes a PLA nozzle way too hot → must clamp to 220 and trip approval42    bad = PrintSettings(nozzle_temp=260, bed_temp=60, retraction_mm=5, fan_pct=100, first_layer_fan_pct=0)43    res = s.check(bad, "PLA")44    assert res.settings.nozzle_temp == 220, res.settings.nozzle_temp45    assert res.requires_approval and res.vetoes46    print("✓ spine clamps PLA 260→220 and trips HITL:", res.vetoes[0])47 48 49def test_fallback_advise():50    led = LedgerManager(Path(tempfile.mkdtemp()) / "lessons.jsonl")51    ensure_seeded(led)52    job = Job(geometry_type="overhang", material="PLA", description="45° bracket")53    env = Environment(temp=28, humidity=50)54    rec = advise(job, env, led.retrieve("PLA", "overhang", 28, 50))55    assert rec.used_fallback, "no Ollama here → should use fallback"56    assert rec.advice.settings.nozzle_temp > 0 and rec.advice.risks57    print("✓ fallback advise:", rec.advice.reasoning[:70], "…")58 59 60def test_reflect_appends():61    led = LedgerManager(Path(tempfile.mkdtemp()) / "lessons.jsonl")62    ensure_seeded(led)63    before = led.count()["earned"]64    job = Job(geometry_type="bridge", material="PETG")65    env = Environment(temp=24, humidity=44)66    settings = PrintSettings(nozzle_temp=235, bed_temp=80, retraction_mm=4, fan_pct=70, first_layer_fan_pct=0)67    entry = reflect_on_job(job, env, settings, "success", led)68    assert led.count()["earned"] == before + 1 and entry.source == "earned"69    print("✓ reflect appends earned lesson:", entry.lesson[:70], "…")70 71 72def test_retrieval_orders_by_env_distance():73    led = LedgerManager(Path(tempfile.mkdtemp()) / "lessons.jsonl")74    ensure_seeded(led)75    # PLA/stringing seeds sit at (22,45) and (24,70). A humid query should rank76    # the humid seed first; a dry query the dry one.77    humid = led.retrieve("PLA", "stringing", temp=24, humidity=70)78    dry = led.retrieve("PLA", "stringing", temp=22, humidity=45)79    assert humid[0][0].env_humidity >= 65, humid[0][0].env_humidity80    assert dry[0][0].env_humidity <= 50, dry[0][0].env_humidity81    print("✓ retrieval ranks by normalized env distance (humid→humid, dry→dry)")82 83 84def test_gcode_readout_ties_to_settings():85    from core.viewer import gcode_readout86    s = PrintSettings(nozzle_temp=205, bed_temp=60, retraction_mm=5, fan_pct=100, first_layer_fan_pct=0)87    g = gcode_readout(s, "PLA")88    assert "M104 S205" in g and "M140 S60" in g, g89    assert "layer height 0.20 mm" in g, g90    print("✓ g-code header is populated from proposed settings")91 92 93# GIF export button removed; keep motion preview tests via UI smoke if needed.94 95 96def test_virtual_printer_html_ties_to_settings():97    from core.widgets import virtual_printer_html98    from core.viewer import generate_primitive99    from core.models import PrintSettings100    mesh, _geo = generate_primitive("box", 20)101    default_html = virtual_printer_html(mesh)102    assert "0.20 mm layers" in default_html, default_html103    fine = PrintSettings(nozzle_temp=200, bed_temp=60, retraction_mm=4.5, fan_pct=80,104                         first_layer_fan_pct=0, layer_height=0.12)105    fine_html = virtual_printer_html(mesh, settings=fine)106    assert "0.12 mm layers" in fine_html, fine_html107    print("✓ virtual-print preview layer height follows PrintSettings")108 109 110def test_ingest_distiller():111    from pathlib import Path as _P112    from ingest.distill import parse_prusa_ini, parse_klipper_cfg, parse_marlin_config113    samples = _P(__file__).resolve().parent / "ingest" / "samples"114    prusa = parse_prusa_ini(samples / "prusa_filaments.ini")115    assert any(f.material == "PLA" and f.param == "bed_temp" for f in prusa), "PLA bed_temp not parsed"116    assert parse_klipper_cfg(samples / "klipper_extruder.cfg"), "klipper max_temp not parsed"117    assert parse_marlin_config(samples / "marlin_config.h"), "marlin maxtemp not parsed"118    print("✓ distiller parses Prusa INI + Klipper cfg + Marlin config")119 120 121def test_precedent_eval_narration():122    from core.viewer import precedent_eval_html123    from core.models import LessonEntry as LE, Environment as E124    e = LE(job_id="x", material="PLA", geometry_type="overhang", env_temp=28, env_humidity=50,125           outcome="failed_sag", lesson="sagged", source="seed", timestamp="t")126    html = precedent_eval_html([(e, 0.28)], E(temp=32, humidity=62))127    assert "warmer" in html and "more humid" in html and "worse" in html, html128    assert "NO CLOSE PRECEDENT" in precedent_eval_html([], E(temp=22, humidity=45))129    print("✓ precedent evaluation narrates env delta + novel case")130 131 132def test_simulator_physical_and_deterministic():133    from sim.outcome import simulate134    from core.models import Job as J, Environment as E, PrintSettings as PS135    bad = PS(nozzle_temp=235, bed_temp=80, retraction_mm=4, fan_pct=40, first_layer_fan_pct=0)136    r1 = simulate(bad, J(geometry_type="bridge", material="PETG"), E(temp=29, humidity=62))137    assert r1.outcome != "success" and r1.quality < 0.7, r1138    r2 = simulate(bad, J(geometry_type="bridge", material="PETG"), E(temp=29, humidity=62))139    assert (r2.outcome, r2.quality) == (r1.outcome, r1.quality), "simulator must be deterministic"140    good = PS(nozzle_temp=205, bed_temp=60, retraction_mm=5, fan_pct=100, first_layer_fan_pct=0)141    rg = simulate(good, J(geometry_type="overhang", material="PLA"), E(temp=20, humidity=40))142    assert rg.outcome == "success", rg143    # build-plate position: corner > edge > center warp for a shrink-prone material;144    # 'center' (default) must be unchanged.145    abs_s = PS(nozzle_temp=248, bed_temp=95, retraction_mm=4, fan_pct=20, first_layer_fan_pct=0)146    env = E(temp=22, humidity=40)147    qc = simulate(abs_s, J(geometry_type="adhesion", material="ABS", bed_position="center"), env).quality148    qe = simulate(abs_s, J(geometry_type="adhesion", material="ABS", bed_position="edge"), env).quality149    qk = simulate(abs_s, J(geometry_type="adhesion", material="ABS", bed_position="corner"), env).quality150    assert qc > qe > qk, (qc, qe, qk)151    assert qc == simulate(abs_s, J(geometry_type="adhesion", material="ABS"), env).quality152    print("✓ simulator is physical + deterministic, and bed-position warps edges/corners")153 154 155def test_policy_learns_and_generalizes():156    import tempfile, os157    from pathlib import Path158    from learn.policy import LearnedPolicy159    from learn.loop import run_session, run_iteration160    from core.ledger import LedgerManager161    from core.models import Job as J, Environment as E162    d = Path(tempfile.mkdtemp())163    pol = LearnedPolicy(path=d / "policy.json")164    led = LedgerManager(path=d / "lessons.jsonl")165    job, env = J(geometry_type="bridge", material="PETG"), E(temp=29, humidity=62)166    sess = run_session(job, env, 10, pol, led)167    assert sess.trajectory[-1] > sess.trajectory[0], "quality must improve"168    assert sess.first_success is not None, "should reach a clean print"169    # generalization: a similar (same-bucket, different exact env) job benefits170    cold = sess.trajectory[0]171    warm_start = run_iteration(J(geometry_type="bridge", material="PETG"),172                               E(temp=28, humidity=58), pol, led, 1, record=False)173    assert warm_start.result.quality > cold, "policy must transfer to similar conditions"174    print("✓ policy learns (quality climbs to a clean print) and generalizes to similar jobs")175 176 177if __name__ == "__main__":178    test_seed_and_retrieve()179    test_spine_veto()180    test_fallback_advise()181    test_reflect_appends()182    test_retrieval_orders_by_env_distance()183    test_gcode_readout_ties_to_settings()184    test_virtual_printer_html_ties_to_settings()185    test_ingest_distiller()186    test_precedent_eval_narration()187    test_simulator_physical_and_deterministic()188    test_policy_learns_and_generalizes()189    print("\nALL CORE TESTS PASSED")190