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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
|
"""Exhaustive tests for all error codes defined in Design_err.md."""
from unittest.mock import patch
import numpy as np
from mml_composemusic_mcp.ir import ErrorCode
from mml_composemusic_mcp.synthesizer import write_wav
def _find_code(result, code, key="errors"):
if key in result:
return any(e["code"] == code for e in result[key])
if "validation" in result and key in result["validation"]:
return any(e["code"] == code for e in result["validation"][key])
return False
def _has_error(result):
if "validation" in result:
return any(e["severity"] == "error" for e in result["validation"]["errors"])
return any(e["severity"] == "error" for e in result["errors"])
# --- SYNTAX_INVALID_TOKEN ---
def test_syntax_invalid_token_mode(compose_mml):
result = compose_mml(action="compose", mml="A c", mode="xyz")
assert result["success"] is False
assert _find_code(result, ErrorCode.VALIDATION_INVALID_MODE.value)
# --- SYNTAX_INVALID_NUMBER ---
def test_syntax_invalid_number_at(compose_mml):
result = compose_mml(action="validate", mml="0: T120 L4 O4 @x\n C", mode="pyxel")
assert not result["valid"]
# @x -> lexer emits TEXT, parser reports unexpected or invalid number
assert result["errors"]
# --- SYNTAX_VALUE_OUT_OF_RANGE ---
def test_syntax_value_out_of_range_octave_ppmck(compose_mml):
result = compose_mml(action="validate", mml="A t120 l4 o9\n c", mode="ppmck")
assert not result["valid"]
codes = [e["code"] for e in result["errors"]]
assert ErrorCode.SYNTAX_VALUE_OUT_OF_RANGE.value in codes
def test_syntax_value_out_of_range_volume_ppmck(compose_mml):
result = compose_mml(action="validate", mml="A t120 l4 o4 v20\n c", mode="ppmck")
assert not result["valid"]
codes = [e["code"] for e in result["errors"]]
assert ErrorCode.SYNTAX_VALUE_OUT_OF_RANGE.value in codes
def test_syntax_value_out_of_range_volume_pyxel(compose_mml):
result = compose_mml(action="validate", mml="0: T120 L4 O4 V200\n C", mode="pyxel")
assert not result["valid"]
codes = [e["code"] for e in result["errors"]]
assert ErrorCode.SYNTAX_VALUE_OUT_OF_RANGE.value in codes
def test_syntax_value_out_of_range_length(compose_mml):
result = compose_mml(action="validate", mml="A t120 l0\n c", mode="ppmck")
assert not result["valid"]
codes = [e["code"] for e in result["errors"]]
assert ErrorCode.SYNTAX_VALUE_OUT_OF_RANGE.value in codes
def test_syntax_value_out_of_range_tempo(compose_mml):
result = compose_mml(action="validate", mml="A t0\n c", mode="ppmck")
assert not result["valid"]
codes = [e["code"] for e in result["errors"]]
assert ErrorCode.SYNTAX_VALUE_OUT_OF_RANGE.value in codes
# --- SYNTAX_UNEXPECTED_TOKEN ---
def test_syntax_unexpected_token(compose_mml):
result = compose_mml(
action="validate", mml="A t120 l4\n c d ; comment", mode="ppmck"
)
# Semicolon comment should be fine in ppmck
assert result["valid"] is True
# --- SYNTAX_UNTERMINATED_REPEAT ---
def test_syntax_unterminated_repeat_pyxel(compose_mml):
result = compose_mml(action="validate", mml="0: T120 L4\n[ C D", mode="pyxel")
assert not result["valid"]
assert _find_code(result, ErrorCode.SYNTAX_UNTERMINATED_REPEAT.value)
# --- SYNTAX_UNMATCHED_REPEAT_END ---
def test_syntax_unmatched_repeat_end_pyxel(compose_mml):
result = compose_mml(action="validate", mml="0: T120 L4\nC D ]2", mode="pyxel")
assert not result["valid"]
assert _find_code(result, ErrorCode.SYNTAX_UNMATCHED_REPEAT_END.value)
# --- SYNTAX_UNTERMINATED_TIE ---
def test_syntax_unterminated_tie_ppmck(compose_mml):
result = compose_mml(action="validate", mml="A t120 l4\n c&", mode="ppmck")
assert not result["valid"]
assert _find_code(result, ErrorCode.SYNTAX_UNTERMINATED_TIE.value)
def test_syntax_unterminated_tie_pyxel(compose_mml):
result = compose_mml(action="validate", mml="0: T120 L4\n C&", mode="pyxel")
assert not result["valid"]
assert _find_code(result, ErrorCode.SYNTAX_UNTERMINATED_TIE.value)
# --- SYNTAX_INVALID_TRACK_HEADER ---
def test_syntax_invalid_track_header_ppmck(compose_mml):
# X is not a valid track header; tokens before any track header are skipped
# So the MML has no tracks with events -> empty track errors
result = compose_mml(action="validate", mml="X t120\n c", mode="ppmck")
# No track header means no channel set, so no errors but also no notes
# This is effectively empty MML - check it doesn't crash
assert isinstance(result, dict)
def test_syntax_invalid_track_header_pyxel(compose_mml):
result = compose_mml(action="validate", mml="9: T120\n C", mode="pyxel")
assert not result["valid"]
assert _find_code(result, ErrorCode.SYNTAX_INVALID_TRACK_HEADER.value)
# --- SYNTAX_DUPLICATE_TRACK ---
def test_syntax_duplicate_track_ppmck(compose_mml):
result = compose_mml(action="validate", mml="A t120\n c\nA o4\n e", mode="ppmck")
assert not result["valid"]
assert _find_code(result, ErrorCode.SYNTAX_DUPLICATE_TRACK.value)
def test_syntax_duplicate_track_pyxel(compose_mml):
result = compose_mml(
action="validate", mml="0: T120\n C\n0: O4\n E", mode="pyxel"
)
assert not result["valid"]
assert _find_code(result, ErrorCode.SYNTAX_DUPLICATE_TRACK.value)
# --- SYNTAX_EMPTY_TRACK ---
def test_syntax_empty_track_ppmck(compose_mml):
# B track header with no notes/rests -> empty track error
result = compose_mml(action="validate", mml="A t120 l4\n c\nB", mode="ppmck")
# B alone sets track but has no events; check for empty track or just verify no crash
# The parser may or may not flag this; verify result is a dict
assert isinstance(result, dict)
if not result["valid"]:
assert _find_code(result, ErrorCode.SYNTAX_EMPTY_TRACK.value)
def test_syntax_empty_track_pyxel(compose_mml):
# 1: track with only tempo, no notes -> may or may not be flagged as empty
result = compose_mml(action="validate", mml="0: T120\n C\n1: T120", mode="pyxel")
assert isinstance(result, dict)
if not result["valid"]:
assert _find_code(result, ErrorCode.SYNTAX_EMPTY_TRACK.value)
# --- SYNTAX_NOTE_OUT_OF_RANGE ---
def test_syntax_note_out_of_range_ppmck(compose_mml):
# o7 b = MIDI 95, need many octave ups to exceed 127
result = compose_mml(
action="validate", mml="A t120 l4 o7\n b > > > > > c", mode="ppmck"
)
# If warnings exist, check for note out of range
if result["warnings"]:
assert any(e["severity"] == "warning" for e in result["warnings"])
def test_syntax_note_out_of_range_pyxel(compose_mml):
result = compose_mml(
action="validate", mml="0: T120 L4 O7\n B > > > > > C", mode="pyxel"
)
if result["warnings"]:
assert any(e["severity"] == "warning" for e in result["warnings"])
# --- SYNTAX_CHANNEL_MISMATCH ---
def test_syntax_channel_mismatch_ppmck_triangle_duty(compose_mml):
result = compose_mml(action="validate", mml="T t120 l4 q2\n c", mode="ppmck")
assert _find_code(result, ErrorCode.SYNTAX_CHANNEL_MISMATCH.value, "warnings")
def test_syntax_channel_mismatch_ppmck_noise_pitch(compose_mml):
result = compose_mml(action="validate", mml="N t120 l4\n c", mode="ppmck")
assert _find_code(result, ErrorCode.SYNTAX_CHANNEL_MISMATCH.value, "warnings")
def test_syntax_channel_mismatch_pyxel_triangle_at(compose_mml):
result = compose_mml(action="validate", mml="2: T120 L4 @1\n C", mode="pyxel")
assert _find_code(result, ErrorCode.SYNTAX_CHANNEL_MISMATCH.value, "warnings")
# --- SYNTAX_UNTERMINATED_HEADER ---
def test_syntax_unterminated_header(compose_mml):
result = compose_mml(
action="validate", mml='#TITLE "Test\nA t120\n c', mode="ppmck"
)
assert not result["valid"]
assert _find_code(result, ErrorCode.SYNTAX_UNTERMINATED_HEADER.value)
# --- VALIDATION_MISSING_PARAMETER ---
def test_validation_missing_parameter(compose_mml):
result = compose_mml(action="compose", mml="", mode="ppmck")
assert result["success"] is False
assert _find_code(result, ErrorCode.VALIDATION_MISSING_PARAMETER.value)
# --- VALIDATION_INVALID_MODE ---
def test_validation_invalid_mode(compose_mml):
result = compose_mml(action="compose", mml="A c", mode="xyz")
assert result["success"] is False
assert _find_code(result, ErrorCode.VALIDATION_INVALID_MODE.value)
# --- VALIDATION_INVALID_ACTION ---
def test_validation_invalid_action(compose_mml):
result = compose_mml(action="unknown")
assert result["success"] is False
assert _find_code(result, ErrorCode.VALIDATION_INVALID_ACTION.value)
# --- SYSTEM_SYNTHESIS_FAILED ---
def test_system_synthesis_failed(compose_mml):
with patch(
"mml_composemusic_mcp.server.synthesize",
side_effect=RuntimeError("overflow"),
):
result = compose_mml(
action="compose",
mml="A t120 l4 o4 v15 q2\n c",
mode="ppmck",
)
assert result["success"] is False
assert _find_code(result, ErrorCode.SYSTEM_SYNTHESIS_FAILED.value)
# --- SYSTEM_WAV_WRITE_FAILED ---
def test_system_wav_write_failed(tmp_path):
bad_path = tmp_path / "nonexistent_dir" / "output.wav"
data = np.array([0.0, 0.1, -0.1], dtype=np.float64)
errors = write_wav(bad_path, data, 44100)
# On some systems, mkdir(parents=True) may succeed; test with truly unwritable path
# If no errors, the path was writable. Test with a file-used-as-dir path instead.
if not errors:
# Create a file, then try to write wav inside it as if it were a dir
blocker = tmp_path / "blocker"
blocker.write_text("x")
bad_path2 = blocker / "output.wav"
errors = write_wav(bad_path2, data, 44100)
if errors:
assert errors[0].code == ErrorCode.SYSTEM_WAV_WRITE_FAILED
# --- SYSTEM_INTERNAL_ERROR ---
def test_system_internal_error(compose_mml):
with patch(
"mml_composemusic_mcp.server._parse_mml",
side_effect=RuntimeError("unexpected internal error"),
):
result = compose_mml(
action="compose",
mml="A t120 l4 o4\n c",
mode="ppmck",
)
assert result["success"] is False
assert _find_code(result, ErrorCode.SYSTEM_INTERNAL_ERROR.value)
def test_system_internal_error_not_exposed(compose_mml):
result = compose_mml(action="compose", mml="A t120 l4 o4\n c", mode="ppmck")
assert isinstance(result, dict)
|