feat: SDD workflow — test sync, song generation + validation, ReaScript hybrid pipeline

- compose-test-sync: fix 3 failing tests (NOTE_TO_MIDI, DrumLoopAnalyzer mock, section name)
- generate-song: CLI wrapper + RPP validator (6 structural checks) + 4 e2e tests
- reascript-hybrid: ReaScriptGenerator + command protocol + CLI + 16 unit tests
- 110/110 tests passing
- Full SDD cycle (propose→spec→design→tasks→apply→verify) for all 3 changes
This commit is contained in:
renato97
2026-05-03 22:00:26 -03:00
parent 7729d5f12f
commit 48bc271afc
25 changed files with 2842 additions and 343 deletions

View File

@@ -0,0 +1,97 @@
"""Tests for scripts/generate.py — E2E song generation and RPP validator."""
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).parents[1]))
class TestGenerateCLI:
"""Smoke and integration tests for the generate.py CLI."""
def test_generate_cli_smoke(self, tmp_path):
"""CLI produces a non-empty .rpp file at the expected path."""
output = tmp_path / "song.rpp"
result = subprocess.run(
[
sys.executable,
"scripts/generate.py",
"--bpm", "95",
"--key", "Am",
"--output", str(output),
"--seed", "42",
],
capture_output=True,
text=True,
timeout=60,
)
assert result.returncode == 0, f"stderr: {result.stderr}"
assert output.exists(), f"File not created: {output}"
assert output.stat().st_size > 0, "File is empty"
def test_validate_passes_for_valid_output(self, tmp_path):
"""With --validate, CLI returns 0 when validator sees no errors."""
output = tmp_path / "song.rpp"
result = subprocess.run(
[
sys.executable,
"scripts/generate.py",
"--bpm", "95",
"--key", "Am",
"--output", str(output),
"--seed", "42",
"--validate",
],
capture_output=True,
text=True,
timeout=60,
)
assert result.returncode == 0, f"stderr: {result.stderr}\nstdout: {result.stdout}"
def test_validate_detects_track_count_violation(self, tmp_path):
"""Validator flags a project with fewer than 9 tracks."""
from src.validator.rpp_validator import validate_rpp_output
rpp_path = tmp_path / "bad.rpp"
# Write a minimal .rpp with only 5 <TRACK> blocks
content = (
"<REAPER_PROJECT 0.1 \"7.65/win64\" 0 0\n"
+ " <TRACK\n NAME \"t1\"\n>\n"
+ " <TRACK\n NAME \"t2\"\n>\n"
+ " <TRACK\n NAME \"t3\"\n>\n"
+ " <TRACK\n NAME \"t4\"\n>\n"
+ " <TRACK\n NAME \"t5\"\n>\n"
+ ">"
)
rpp_path.write_text(content, encoding="utf-8")
errors = validate_rpp_output(str(rpp_path))
assert any("Expected 9" in e for e in errors), f"Got: {errors}"
def test_reproducibility_same_seed(self, tmp_path):
"""Two runs with the same seed produce byte-identical output."""
output_a = tmp_path / "song_a.rpp"
output_b = tmp_path / "song_b.rpp"
for out_path in (output_a, output_b):
result = subprocess.run(
[
sys.executable,
"scripts/generate.py",
"--bpm", "95",
"--key", "Am",
"--output", str(out_path),
"--seed", "42",
],
capture_output=True,
text=True,
timeout=60,
)
assert result.returncode == 0, f"stderr: {result.stderr}"
assert output_a.read_bytes() == output_b.read_bytes(), (
"Outputs differ — seed does not guarantee reproducibility"
)