diff --git a/lib/song.py b/lib/song.py index 5d80027..ce3b859 100644 --- a/lib/song.py +++ b/lib/song.py @@ -328,10 +328,15 @@ def chord_template_to_wire(ct: ChordTemplate) -> dict: if ct.voicing: out["voicing"] = ct.voicing # CAGED shape + guide tones (§6.6) — default-omitted, mirroring voicing. - if ct.caged: - out["caged"] = ct.caged - if ct.guide_tones: - out["guideTones"] = list(ct.guide_tones) + # Sanitize on EMIT too (not just on decode): a directly-constructed template + # must not be able to write a non-enum `caged` or an out-of-range `guideTone` + # to the wire (the spec constrains caged to C/A/G/E/D and guideTones to 0..11). + _caged = _sanitize_caged(ct.caged) + if _caged: + out["caged"] = _caged + _guide_tones = _sanitize_guide_tones(ct.guide_tones) + if _guide_tones: + out["guideTones"] = _guide_tones return out diff --git a/tests/test_song.py b/tests/test_song.py index 92af496..b98cae0 100644 --- a/tests/test_song.py +++ b/tests/test_song.py @@ -514,6 +514,20 @@ def test_template_caged_tolerates_malformed(bad): assert arr.chord_templates[0].caged == "" +def test_template_caged_guide_tones_sanitized_on_emit(): + """A directly-constructed template can't write an invalid caged / out-of-range + guideTone to the wire — the emitter sanitizes, not just the decoder.""" + ct = ChordTemplate(name="Am", fingers=[-1] * 6, frets=[-1] * 6, + caged="X", guide_tones=[3, 99, -1, "x", True]) + wire = chord_template_to_wire(ct) + assert "caged" not in wire # non-enum dropped, not emitted + assert wire["guideTones"] == [3] # only the valid in-range int survives + # A wholly-invalid guideTones list omits the key entirely. + ct2 = ChordTemplate(name="Am", fingers=[-1] * 6, frets=[-1] * 6, + guide_tones=[42, "nope"]) + assert "guideTones" not in chord_template_to_wire(ct2) + + def test_template_guide_tones_round_trip(): """A non-empty guideTones list survives the template wire + round-trip.""" ct = ChordTemplate(name="G7", display_name="G7", fingers=[3, 2, 0, 0, 0, 1],