File size: 9,386 Bytes
6b09b49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0b338fe
6b09b49
 
 
0b338fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6b09b49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0b338fe
6b09b49
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
"""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")