feat: design system overhaul — sidebar, AI chats, settings, brainstorm, color cleanup
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s

- Sidebar: dynamic brand-accent colors, brainstorm section restyled
- AI chat general: popup panel with expand/collapse, hides when contextual AI open
- AI chat contextual: tabs reordered (Actions first), X close button, height fix
- Settings: all tabs restyled, 6 new color presets (sage, terracotta, iron, etc.)
- Global color cleanup: emerald/orange hardcoded → brand-accent dynamic
- Brainstorm page: orange → brand-accent throughout
- PageEntry animation component added to key pages
- Floating AI button: bg-brand-accent instead of hardcoded black
- i18n: all 15 locales updated with new AI/billing keys
- Billing: freemium quota tracking, BYOK, stripe subscription scaffolding
- Admin: integrated into new design
- AGENTS.md + CLAUDE.md project rules added
This commit is contained in:
Antigravity
2026-05-16 12:59:30 +00:00
parent 1fcea6ed7d
commit bd495be965
2284 changed files with 395285 additions and 2327 deletions

View File

@@ -0,0 +1,7 @@
"""Configure pytest to collect test files with hyphens in names."""
import pytest
def pytest_collect_file(parent, file_path):
if file_path.suffix == ".py" and file_path.name.startswith("test-"):
return pytest.Module.from_parent(parent, path=file_path)

View File

@@ -0,0 +1,113 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = ["pytest>=7.0"]
# ///
"""Tests for analyze-input.py"""
import json
import subprocess
import sys
from pathlib import Path
SCRIPT = str(Path(__file__).parent.parent / "analyze-input.py")
def run_script(*args):
"""Run the script and return parsed JSON output."""
result = subprocess.run(
[sys.executable, SCRIPT, *args],
capture_output=True, text=True
)
return json.loads(result.stdout) if result.stdout else None, result.returncode
class TestAnalyzeInput:
def test_basic_metrics(self):
text = "Hello world\nThis is a test\nThree lines here"
report, code = run_script("--text", text)
assert report is not None
m = report["metrics"]
assert m["line_count"] == 3
assert m["non_empty_line_count"] == 3
assert m["word_count"] == 9
assert m["character_count"] > 0
def test_detects_existing_structure(self):
text = "[Verse 1]\nSome lyrics here\nMore lyrics\n\n[Chorus]\nChorus line"
report, code = run_script("--text", text)
assert report is not None
m = report["metrics"]
assert m["has_existing_structure"] is True
assert "Verse 1" in m["existing_tags"]
assert "Chorus" in m["existing_tags"]
def test_no_structure_detected(self):
text = "Just raw text\nWith no brackets\nPlain poetry"
report, code = run_script("--text", text)
assert report is not None
m = report["metrics"]
assert m["has_existing_structure"] is False
assert m["existing_tags"] == []
def test_repeated_phrases(self):
text = "come back to me tonight\nwhen the stars are bright\ncome back to me tonight\nunder the pale moonlight"
report, code = run_script("--text", text)
assert report is not None
m = report["metrics"]
phrases = [p["phrase"] for p in m["repeated_phrases"]]
assert any("come back to me" in p for p in phrases)
def test_rhyme_pairs(self):
text = "Walking down the street\nFeeling the beat\nLooking for the light\nShining in the night"
report, code = run_script("--text", text)
assert report is not None
m = report["metrics"]
rhymes = m["potential_rhyme_pairs"]
rhyme_words = [set(r["words"]) for r in rhymes]
assert any({"street", "beat"} == w for w in rhyme_words) or any({"light", "night"} == w for w in rhyme_words)
def test_short_structure_estimate(self):
text = "\n".join(f"Line {i}" for i in range(1, 10))
report, code = run_script("--text", text)
assert report is not None
m = report["metrics"]
assert m["estimated_structure"] == "short"
def test_medium_structure_estimate(self):
text = "\n".join(f"Line number {i} of the song" for i in range(1, 25))
report, code = run_script("--text", text)
assert report is not None
m = report["metrics"]
assert m["estimated_structure"] == "medium"
def test_long_structure_estimate(self):
text = "\n".join(f"Line number {i} of a very long song" for i in range(1, 35))
report, code = run_script("--text", text)
assert report is not None
m = report["metrics"]
assert m["estimated_structure"] == "long"
def test_report_structure(self):
report, code = run_script("--text", "Some text")
assert report is not None
assert "script" in report
assert "version" in report
assert "timestamp" in report
assert "status" in report
assert "metrics" in report
assert "findings" in report
assert "summary" in report
def test_help_flag(self):
result = subprocess.run(
[sys.executable, SCRIPT, "--help"],
capture_output=True, text=True
)
assert result.returncode == 0
assert "analyze" in result.stdout.lower()
if __name__ == "__main__":
import pytest
pytest.main([__file__, "-v"])

View File

@@ -0,0 +1,162 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = ["pytest>=7.0"]
# ///
"""Tests for assemble-summary.py"""
import json
import subprocess
import sys
from pathlib import Path
SCRIPT = str(Path(__file__).parent.parent / "assemble-summary.py")
def run_script(*args, input_data=None):
"""Run the script and return stdout and returncode."""
result = subprocess.run(
[sys.executable, SCRIPT, *args],
capture_output=True, text=True,
input=input_data
)
return result.stdout, result.returncode
def create_test_files(tmp_path):
"""Create sample JSON input files for testing."""
validation = {
"script": "validate-lyrics",
"status": "pass",
"metrics": {
"total_lines": 20,
"lyric_lines": 14,
"section_count": 4,
"sections": ["Verse 1", "Chorus", "Verse 2", "Chorus"]
},
"findings": [],
"summary": {"total": 0}
}
syllables = {
"script": "syllable-counter",
"status": "pass",
"metrics": {
"total_lyric_lines": 14,
"total_syllables": 112,
"average_syllables_per_line": 8.0,
"min_syllables": 5,
"max_syllables": 12
},
"findings": [],
"summary": {"total": 0}
}
cliches = {
"script": "cliche-detector",
"status": "pass",
"metrics": {
"total_cliches_found": 2,
"categories": {"emotional": 1, "nature": 1}
},
"findings": [],
"summary": {"total": 2}
}
val_file = tmp_path / "validation.json"
syl_file = tmp_path / "syllables.json"
cli_file = tmp_path / "cliches.json"
val_file.write_text(json.dumps(validation))
syl_file.write_text(json.dumps(syllables))
cli_file.write_text(json.dumps(cliches))
return str(val_file), str(syl_file), str(cli_file)
class TestAssembleSummary:
def test_basic_assembly(self, tmp_path):
val, syl, cli = create_test_files(tmp_path)
output, code = run_script("--validation", val, "--syllables", syl, "--cliches", cli)
assert code == 0
assert "Transformation Summary" in output
assert "Validation Status" in output
assert "Sections:" in output
def test_with_transformations(self, tmp_path):
val, syl, cli = create_test_files(tmp_path)
output, code = run_script(
"--validation", val, "--syllables", syl, "--cliches", cli,
"--transformations", "ST,CC,RA"
)
assert code == 0
assert "Transformations Applied" in output
assert "ST:" in output
assert "CC:" in output
assert "RA:" in output
def test_json_output(self, tmp_path):
val, syl, cli = create_test_files(tmp_path)
out_file = tmp_path / "output.json"
output, code = run_script(
"--validation", val, "--syllables", syl, "--cliches", cli,
"-o", str(out_file)
)
assert code == 0
report = json.loads(out_file.read_text())
assert report["script"] == "assemble-summary"
assert "metrics" in report
assert "markdown" in report
def test_markdown_output_file(self, tmp_path):
val, syl, cli = create_test_files(tmp_path)
out_file = tmp_path / "output.md"
output, code = run_script(
"--validation", val, "--syllables", syl, "--cliches", cli,
"-o", str(out_file)
)
assert code == 0
content = out_file.read_text()
assert "## Transformation Summary" in content
def test_cliche_categories_displayed(self, tmp_path):
val, syl, cli = create_test_files(tmp_path)
output, code = run_script("--validation", val, "--syllables", syl, "--cliches", cli)
assert code == 0
assert "2 found" in output
assert "emotional" in output
assert "nature" in output
def test_syllable_range_displayed(self, tmp_path):
val, syl, cli = create_test_files(tmp_path)
output, code = run_script("--validation", val, "--syllables", syl, "--cliches", cli)
assert code == 0
assert "5-12" in output
assert "avg 8.0" in output
def test_estimated_duration(self, tmp_path):
val, syl, cli = create_test_files(tmp_path)
output, code = run_script("--validation", val, "--syllables", syl, "--cliches", cli)
assert code == 0
# 4 sections * 15 sec = 60 sec = 1:00
assert "1:00" in output
def test_missing_files_handled(self, tmp_path):
missing = str(tmp_path / "nonexistent.json")
output, code = run_script(
"--validation", missing, "--syllables", missing, "--cliches", missing
)
assert code == 2
def test_help_flag(self):
result = subprocess.run(
[sys.executable, SCRIPT, "--help"],
capture_output=True, text=True
)
assert result.returncode == 0
assert "assemble" in result.stdout.lower()
if __name__ == "__main__":
import pytest
pytest.main([__file__, "-v"])

View File

@@ -0,0 +1,105 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = ["pytest>=7.0"]
# ///
"""Tests for cliche-detector.py"""
import json
import subprocess
import sys
from pathlib import Path
SCRIPT = str(Path(__file__).parent.parent / "cliche-detector.py")
def run_script(*args):
"""Run the script and return parsed JSON output."""
result = subprocess.run(
[sys.executable, SCRIPT, *args],
capture_output=True, text=True
)
return json.loads(result.stdout) if result.stdout else None, result.returncode
class TestClicheDetector:
def test_detects_fire_in_soul(self):
report, code = run_script("--text", "There's a fire in my soul tonight")
assert report is not None
assert report["metrics"]["total_cliches_found"] >= 1
assert any("fire in my soul" in f["data"]["matched_text"] for f in report["findings"])
def test_detects_dance_in_rain(self):
report, code = run_script("--text", "We'll dance in the rain together")
assert report is not None
assert report["metrics"]["total_cliches_found"] >= 1
def test_detects_broken_heart(self):
report, code = run_script("--text", "My broken heart won't heal")
assert report is not None
assert report["metrics"]["total_cliches_found"] >= 1
def test_detects_stand_tall(self):
report, code = run_script("--text", "I'm standing tall against the wind")
assert report is not None
assert report["metrics"]["total_cliches_found"] >= 1
def test_no_cliches_in_clean_text(self):
report, code = run_script("--text", "The kitchen table holds three plates\nSteam rising from the coffee cup")
assert report is not None
assert report["metrics"]["total_cliches_found"] == 0
assert code == 0
def test_skips_metatags(self):
text = "[Verse 1]\nFire in my soul\n[Chorus]\nClean lyrics here"
report, code = run_script("--text", text)
assert report is not None
# Should find the cliche in the lyric line, not in metatags
assert report["metrics"]["total_cliches_found"] >= 1
def test_provides_alternatives(self):
report, code = run_script("--text", "Rise from the ashes of what we were")
assert report is not None
assert len(report["findings"]) > 0
finding = report["findings"][0]
assert "alternatives" in finding["data"]
assert len(finding["data"]["alternatives"]) > 0
def test_multiple_cliches_in_one_text(self):
text = (
"Fire in my soul keeps burning bright\n"
"Standing tall through broken dreams\n"
"Dance in the rain with a heart of gold\n"
)
report, code = run_script("--text", text)
assert report is not None
assert report["metrics"]["total_cliches_found"] >= 3
def test_case_insensitive(self):
report, code = run_script("--text", "FIRE IN MY SOUL")
assert report is not None
assert report["metrics"]["total_cliches_found"] >= 1
def test_report_structure(self):
report, code = run_script("--text", "Just a normal line")
assert report is not None
assert "script" in report
assert "version" in report
assert "timestamp" in report
assert "status" in report
assert "metrics" in report
assert "findings" in report
assert "summary" in report
def test_help_flag(self):
result = subprocess.run(
[sys.executable, SCRIPT, "--help"],
capture_output=True, text=True
)
assert result.returncode == 0
assert "cliche" in result.stdout.lower()
if __name__ == "__main__":
import pytest
pytest.main([__file__, "-v"])

View File

@@ -0,0 +1,110 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = ["pytest>=7.0"]
# ///
"""Tests for lyrics-diff.py"""
import json
import subprocess
import sys
from pathlib import Path
SCRIPT = str(Path(__file__).parent.parent / "lyrics-diff.py")
def run_script(*args):
"""Run the script and return parsed JSON output."""
result = subprocess.run(
[sys.executable, SCRIPT, *args],
capture_output=True, text=True
)
return json.loads(result.stdout) if result.stdout else None, result.returncode
class TestLyricsDiff:
def test_identical_lyrics(self):
text = "[Verse 1]\nHello world\nGoodbye moon"
report, code = run_script("--original-text", text, "--transformed-text", text)
assert report is not None
assert report["status"] == "pass"
assert len(report["changes"]) == 0
assert report["summary"]["lines_added"] == 0
assert report["summary"]["lines_removed"] == 0
assert report["summary"]["lines_modified"] == 0
def test_modified_line(self):
original = "[Verse 1]\nWalking through the rain"
transformed = "[Verse 1]\nRunning through the storm"
report, code = run_script("--original-text", original, "--transformed-text", transformed)
assert report is not None
assert report["status"] == "info"
modified = [c for c in report["changes"] if c["type"] == "modified"]
assert len(modified) >= 1
assert report["summary"]["lines_modified"] >= 1
def test_added_lines(self):
original = "[Verse 1]\nLine one"
transformed = "[Verse 1]\nLine one\nLine two\nLine three"
report, code = run_script("--original-text", original, "--transformed-text", transformed)
assert report is not None
added = [c for c in report["changes"] if c["type"] == "added"]
assert len(added) >= 1
assert report["summary"]["lines_added"] >= 1
def test_removed_lines(self):
original = "[Verse 1]\nLine one\nLine two\nLine three"
transformed = "[Verse 1]\nLine one"
report, code = run_script("--original-text", original, "--transformed-text", transformed)
assert report is not None
removed = [c for c in report["changes"] if c["type"] == "removed"]
assert len(removed) >= 1
assert report["summary"]["lines_removed"] >= 1
def test_section_tracking(self):
original = "[Verse 1]\nOld verse line\n\n[Chorus]\nOld chorus line"
transformed = "[Verse 1]\nNew verse line\n\n[Chorus]\nNew chorus line"
report, code = run_script("--original-text", original, "--transformed-text", transformed)
assert report is not None
assert len(report["summary"]["sections_affected"]) >= 1
def test_unified_diff_output(self):
original = "[Verse 1]\nHello"
transformed = "[Verse 1]\nGoodbye"
report, code = run_script("--original-text", original, "--transformed-text", transformed)
assert report is not None
assert "unified_diff" in report
assert len(report["unified_diff"]) > 0
def test_file_input(self, tmp_path):
orig_file = tmp_path / "orig.txt"
trans_file = tmp_path / "trans.txt"
orig_file.write_text("[Verse 1]\nOriginal line")
trans_file.write_text("[Verse 1]\nTransformed line")
report, code = run_script("--original", str(orig_file), "--transformed", str(trans_file))
assert report is not None
assert len(report["changes"]) >= 1
def test_report_structure(self):
report, code = run_script("--original-text", "a", "--transformed-text", "b")
assert report is not None
assert "script" in report
assert "version" in report
assert "timestamp" in report
assert "status" in report
assert "changes" in report
assert "summary" in report
assert "unified_diff" in report
def test_help_flag(self):
result = subprocess.run(
[sys.executable, SCRIPT, "--help"],
capture_output=True, text=True
)
assert result.returncode == 0
assert "diff" in result.stdout.lower()
if __name__ == "__main__":
import pytest
pytest.main([__file__, "-v"])

View File

@@ -0,0 +1,170 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = ["pytest>=7.0"]
# ///
"""Tests for section-length-checker.py"""
import json
import subprocess
import sys
from pathlib import Path
SCRIPT = str(Path(__file__).parent.parent / "section-length-checker.py")
def run_script(*args):
"""Run the script and return parsed JSON output."""
result = subprocess.run(
[sys.executable, SCRIPT, *args],
capture_output=True, text=True
)
return json.loads(result.stdout) if result.stdout else None, result.returncode
class TestSectionLengthChecker:
def test_sections_within_range(self):
lyrics = (
"[Verse 1]\n"
"Line one of the verse\n"
"Line two of the verse\n"
"Line three of the verse\n"
"Line four of the verse\n"
"\n"
"[Chorus]\n"
"Chorus line one\n"
"Chorus line two\n"
"Chorus line three\n"
)
report, code = run_script("--text", lyrics)
assert report is not None
assert report["status"] == "pass"
assert report["metrics"]["sections_pass"] == 2
assert report["metrics"]["sections_fail"] == 0
def test_verse_too_short(self):
lyrics = (
"[Verse 1]\n"
"Only one line\n"
"\n"
"[Chorus]\n"
"Chorus one\n"
"Chorus two\n"
)
report, code = run_script("--text", lyrics)
assert report is not None
assert report["status"] == "warning"
short_sections = [s for s in report["sections"] if s["status"] == "short"]
assert len(short_sections) >= 1
assert short_sections[0]["base_name"] == "verse"
def test_verse_too_long(self):
lyrics = "[Verse 1]\n" + "\n".join(f"Line {i}" for i in range(1, 12)) + "\n"
report, code = run_script("--text", lyrics)
assert report is not None
long_sections = [s for s in report["sections"] if s["status"] == "long"]
assert len(long_sections) >= 1
def test_intro_can_be_empty(self):
lyrics = (
"[Intro]\n"
"\n"
"[Verse 1]\n"
"Line one\nLine two\nLine three\nLine four\n"
)
report, code = run_script("--text", lyrics)
assert report is not None
intro = [s for s in report["sections"] if s["base_name"] == "intro"]
assert len(intro) == 1
assert intro[0]["status"] == "pass"
def test_numbered_sections_normalized(self):
lyrics = (
"[Verse 2]\n"
"Line one\nLine two\nLine three\nLine four\n"
"\n"
"[Chorus]\n"
"Chorus one\nChorus two\n"
)
report, code = run_script("--text", lyrics)
assert report is not None
verse = [s for s in report["sections"] if s["tag"] == "Verse 2"]
assert len(verse) == 1
assert verse[0]["base_name"] == "verse"
def test_unknown_section_type(self):
lyrics = "[Spoken Word]\nSome content\nMore content\n"
report, code = run_script("--text", lyrics)
assert report is not None
unknown = [s for s in report["sections"] if s["status"] == "unknown"]
assert len(unknown) >= 1
def test_report_structure(self):
lyrics = "[Verse 1]\nLine one\nLine two\nLine three\nLine four\n"
report, code = run_script("--text", lyrics)
assert report is not None
assert "script" in report
assert "version" in report
assert "timestamp" in report
assert "status" in report
assert "sections" in report
assert "findings" in report
assert "summary" in report
def test_help_flag(self):
result = subprocess.run(
[sys.executable, SCRIPT, "--help"],
capture_output=True, text=True
)
assert result.returncode == 0
assert "section" in result.stdout.lower()
def test_descriptor_metatags_not_counted_as_content(self):
"""Descriptor metatags like [Energy: slow] should not inflate line counts."""
lyrics = (
"[Verse 1]\n"
"[Energy: slow]\n"
"[Vocal Style: clean]\n"
"[Mood: dark]\n"
"Line one of the verse\n"
"Line two of the verse\n"
"Line three of the verse\n"
"Line four of the verse\n"
)
report, code = run_script("--text", lyrics)
assert report is not None
verse = [s for s in report["sections"] if s["base_name"] == "verse"]
assert len(verse) == 1
# Should count only 4 lyric lines, not 7
assert verse[0]["line_count"] == 4
assert verse[0]["status"] == "pass"
def test_prog_genre_relaxes_verse_limit(self):
"""With --genre prog, verses can have up to 16 lines without warning."""
lines = "\n".join(f"Line {i}" for i in range(1, 13))
lyrics = f"[Verse 1]\n{lines}\n"
# Without genre flag, 12 lines should be too long (max 8)
report_normal, _ = run_script("--text", lyrics)
assert report_normal is not None
verse_normal = [s for s in report_normal["sections"] if s["base_name"] == "verse"]
assert verse_normal[0]["status"] == "long"
# With prog genre, 12 lines should pass (max becomes 16)
report_prog, _ = run_script("--text", lyrics, "--genre", "prog")
assert report_prog is not None
verse_prog = [s for s in report_prog["sections"] if s["base_name"] == "verse"]
assert verse_prog[0]["status"] == "pass"
def test_interlude_is_known_section(self):
"""Interlude should now be a known section type with defined range."""
lyrics = "[Interlude]\nSome content\nMore content\n"
report, code = run_script("--text", lyrics)
assert report is not None
interlude = [s for s in report["sections"] if s["base_name"] == "interlude"]
assert len(interlude) == 1
assert interlude[0]["status"] == "pass"
if __name__ == "__main__":
import pytest
pytest.main([__file__, "-v"])

View File

@@ -0,0 +1,225 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = ["pytest>=7.0"]
# ///
"""Tests for syllable-counter.py"""
import json
import subprocess
import sys
from pathlib import Path
SCRIPT = str(Path(__file__).parent.parent / "syllable-counter.py")
def run_script(*args):
"""Run the script and return parsed JSON output."""
result = subprocess.run(
[sys.executable, SCRIPT, *args],
capture_output=True, text=True
)
return json.loads(result.stdout) if result.stdout else None, result.returncode
# Also test the count_syllables function directly
import importlib.util
_spec = importlib.util.spec_from_file_location("syllable_counter", Path(__file__).parent.parent / "syllable-counter.py")
_mod = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_mod)
count_syllables = _mod.count_syllables
estimate_duration = _mod.estimate_duration
format_duration_range = _mod.format_duration_range
class TestSyllableCounting:
"""Test individual word syllable counting."""
def test_one_syllable_words(self):
for word in ["cat", "dog", "the", "run", "light", "dream"]:
assert count_syllables(word) == 1, f"Expected 1 syllable for '{word}', got {count_syllables(word)}"
def test_two_syllable_words(self):
for word in ["hello", "window", "walking", "morning", "shadow"]:
assert count_syllables(word) == 2, f"Expected 2 syllables for '{word}', got {count_syllables(word)}"
def test_three_syllable_words(self):
for word in ["beautiful", "another", "everyone", "different"]:
result = count_syllables(word)
assert result == 3, f"Expected 3 syllables for '{word}', got {result}"
def test_contractions(self):
assert count_syllables("I'm") == 1
assert count_syllables("don't") == 1
assert count_syllables("couldn't") == 2
def test_empty_string(self):
assert count_syllables("") == 0
class TestLyricsAnalysis:
"""Test full lyrics analysis via the script."""
def test_basic_analysis(self):
lyrics = (
"[Verse 1]\n"
"Walking through the morning light\n"
"Counting shadows on the wall\n"
)
report, code = run_script("--text", lyrics)
assert report is not None
assert report["script"] == "syllable-counter"
assert report["metrics"]["total_lyric_lines"] == 2
assert report["metrics"]["total_syllables"] > 0
def test_section_grouping(self):
lyrics = (
"[Verse 1]\n"
"Short line here\n"
"Another short one\n"
"\n"
"[Chorus]\n"
"The chorus comes in strong and bold\n"
"With longer lines that carry more weight\n"
)
report, code = run_script("--text", lyrics)
assert report is not None
assert len(report["section_analysis"]) == 2
section_names = [s["section"] for s in report["section_analysis"]]
assert "[Verse 1]" in section_names
assert "[Chorus]" in section_names
def test_line_data_includes_syllables(self):
lyrics = "[Verse 1]\nHello world\n"
report, code = run_script("--text", lyrics)
assert report is not None
assert len(report["line_data"]) == 1
assert "syllables" in report["line_data"][0]
assert report["line_data"][0]["syllables"] > 0
def test_skips_metatags(self):
lyrics = "[Mood: haunting]\n[Verse 1]\nWalking through fog\n"
report, code = run_script("--text", lyrics)
assert report is not None
# Only the lyric line should be counted, not metatags
assert report["metrics"]["total_lyric_lines"] == 1
def test_high_variance_warning(self):
lyrics = (
"[Verse 1]\n"
"Hi\n"
"This is a much longer line with many more syllables than the first\n"
"Short\n"
"Another really long line that goes on and on and on\n"
)
report, code = run_script("--text", lyrics)
assert report is not None
# Should flag high syllable variance
issues = [f["issue"] for f in report["findings"]]
assert any("variance" in i.lower() or "syllable" in i.lower() for i in issues)
def test_report_structure(self):
lyrics = "[Verse 1]\nA simple test line\n"
report, code = run_script("--text", lyrics)
assert report is not None
assert "script" in report
assert "version" in report
assert "timestamp" in report
assert "status" in report
assert "metrics" in report
assert "line_data" in report
assert "section_analysis" in report
assert "findings" in report
assert "summary" in report
def test_help_flag(self):
result = subprocess.run(
[sys.executable, SCRIPT, "--help"],
capture_output=True, text=True
)
assert result.returncode == 0
assert "syllable" in result.stdout.lower()
class TestDurationEstimation:
"""Test duration estimation function."""
def test_zero_lines(self):
min_s, max_s = estimate_duration(0, 0)
assert min_s == 0
assert max_s == 0
def test_one_line(self):
# 7.0 avg syllables = mid range (3.0-4.5 secs/line)
min_s, max_s = estimate_duration(1, 7.0)
assert min_s == round(1 * 3.5) # low-density range
assert max_s == round(1 * 5.5)
def test_typical_song(self):
# 20 lines at 7.0 avg syllables (mid range)
min_s, max_s = estimate_duration(20, 7.0)
assert min_s == round(20 * 3.5) # 70
assert max_s == round(20 * 5.5) # 110
def test_high_density_faster(self):
# High syllable density = faster delivery = less time per line
min_s, max_s = estimate_duration(20, 12.0)
assert min_s == round(20 * 2.5) # 50
assert max_s == round(20 * 4.0) # 80
def test_instrumental_sections_add_time(self):
# Sections with instrumental tags add time
sections = [
{"name": "[Intro]", "lines": []},
{"name": "[Verse]", "lines": [{"syllables": 7}] * 4},
{"name": "[Guitar Solo]", "lines": []},
{"name": "[Outro]", "lines": []},
]
min_s, max_s = estimate_duration(4, 7.0, sections)
# 4 lines at mid range + intro (5-15) + guitar solo (10-25) + outro (8-20)
assert min_s > round(4 * 3.5) # More than just lyrics
assert max_s > round(4 * 5.5)
def test_formatted_range(self):
formatted = format_duration_range(50, 90)
assert formatted == "0:50-1:30"
def test_formatted_range_zero(self):
formatted = format_duration_range(0, 0)
assert formatted == "0:00-0:00"
def test_formatted_range_large(self):
formatted = format_duration_range(120, 240)
assert formatted == "2:00-4:00"
def test_duration_in_report(self):
lyrics = (
"[Verse 1]\n"
"Walking through the morning light\n"
"Counting shadows on the wall\n"
"\n"
"[Chorus]\n"
"Come undone come undone\n"
"Let the weight fall where it may\n"
)
report, code = run_script("--text", lyrics)
assert report is not None
duration = report["metrics"]["estimated_duration"]
assert "min_seconds" in duration
assert "max_seconds" in duration
assert "formatted" in duration
assert duration["min_seconds"] > 0
assert duration["max_seconds"] > duration["min_seconds"]
# Check formatted string pattern M:SS-M:SS
assert "-" in duration["formatted"]
def test_estimate_duration_flag(self):
lyrics = "[Verse 1]\nHello world\n"
report, code = run_script("--text", lyrics, "--estimate-duration")
assert report is not None
assert "estimated_duration" in report["metrics"]
if __name__ == "__main__":
import pytest
pytest.main([__file__, "-v"])

View File

@@ -0,0 +1,226 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = ["pytest>=7.0"]
# ///
"""Tests for validate-lyrics.py"""
import json
import subprocess
import sys
from pathlib import Path
SCRIPT = str(Path(__file__).parent.parent / "validate-lyrics.py")
def run_script(*args):
"""Run the script and return parsed JSON output."""
result = subprocess.run(
[sys.executable, SCRIPT, *args],
capture_output=True, text=True
)
return json.loads(result.stdout) if result.stdout else None, result.returncode
class TestValidateLyrics:
def test_valid_structured_lyrics(self):
lyrics = (
"[Verse 1]\n"
"Walking through the morning light\n"
"Counting shadows on the wall\n"
"\n"
"[Chorus]\n"
"Come undone, come undone\n"
"Let the weight fall where it may\n"
"\n"
"[Verse 2]\n"
"Fingerprints on frosted glass\n"
"Letters folded into cranes\n"
"\n"
"[Chorus]\n"
"Come undone, come undone\n"
"Let the weight fall where it may\n"
)
report, code = run_script("--text", lyrics)
assert report is not None
assert report["script"] == "validate-lyrics"
assert report["metrics"]["section_count"] == 4
assert "Verse 1" in report["metrics"]["sections"]
assert "Chorus" in report["metrics"]["sections"]
def test_no_section_tags(self):
lyrics = "Just some raw text\nWith no structure at all\nThree lines of poetry"
report, code = run_script("--text", lyrics)
assert report is not None
issues = [f["issue"] for f in report["findings"]]
assert any("No section metatags" in i for i in issues)
def test_style_cue_contamination(self):
lyrics = "[Verse 1]\nThe punchy drums echo through my mind\n"
report, code = run_script("--text", lyrics)
assert report is not None
issues = [f["issue"] for f in report["findings"]]
assert any("style cue" in i.lower() for i in issues)
def test_asterisk_detection(self):
lyrics = "[Verse 1]\n*This line has asterisks*\n"
report, code = run_script("--text", lyrics)
assert report is not None
issues = [f["issue"] for f in report["findings"]]
assert any("Asterisk" in i for i in issues)
def test_empty_lyrics(self):
report, code = run_script("--text", "")
assert report is not None
assert report["status"] == "fail"
assert any(f["severity"] == "critical" for f in report["findings"])
def test_unrecognized_metatag(self):
lyrics = "[Verse 1]\nSome line\n\n[Banana]\nAnother line\n"
report, code = run_script("--text", lyrics)
assert report is not None
issues = [f["issue"] for f in report["findings"]]
assert any("Unrecognized metatag" in i for i in issues)
def test_valid_descriptor_metatags(self):
lyrics = (
"[Mood: haunting]\n\n"
"[Verse 1]\n"
"Walking through the fog\n"
"Counting all the windows\n"
)
report, code = run_script("--text", lyrics)
assert report is not None
# Descriptor metatags should not be flagged as unrecognized
issues = [f["issue"] for f in report["findings"]]
assert not any("Mood" in i and "Unrecognized" in i for i in issues)
def test_empty_section(self):
lyrics = "[Verse 1]\n\n[Chorus]\nSome chorus line\n"
report, code = run_script("--text", lyrics)
assert report is not None
issues = [f["issue"] for f in report["findings"]]
assert any("Empty section" in i for i in issues)
def test_report_structure(self):
lyrics = "[Verse 1]\nA simple test line here\n"
report, code = run_script("--text", lyrics)
assert report is not None
assert "script" in report
assert "version" in report
assert "timestamp" in report
assert "status" in report
assert "metrics" in report
assert "findings" in report
assert "summary" in report
def test_help_flag(self):
result = subprocess.run(
[sys.executable, SCRIPT, "--help"],
capture_output=True, text=True
)
assert result.returncode == 0
assert "validate" in result.stdout.lower()
def test_character_count_error(self):
"""Lyrics exceeding 5000 chars (hard limit) should produce high severity finding."""
# Build lyrics over 5000 characters (hard limit for v4.5+/v5/v5.5)
line = "This is a long line of lyrics for testing character count limits yeah\n"
lyrics = "[Verse 1]\n" + line * 80 # well over 5000 chars
assert len(lyrics) > 5000
report, code = run_script("--text", lyrics)
assert report is not None
issues = [f for f in report["findings"] if "character count" in f["issue"].lower()]
assert len(issues) >= 1
assert any(f["severity"] == "high" for f in issues)
def test_character_count_warning(self):
"""Lyrics between 3000 and 5000 chars should produce medium severity finding (quality degrades)."""
# Build lyrics between 3000 and 5000 characters (quality budget exceeded)
line = "This is a medium line of lyrics for testing\n"
base = "[Verse 1]\n"
# Each line is 45 chars. Need total between 3000 and 5000.
count = 72 # 10 + 72*45 = 3250
lyrics = base + line * count
total = len(lyrics)
assert 3000 < total < 5000, f"Got {total} chars"
report, code = run_script("--text", lyrics)
assert report is not None
issues = [f for f in report["findings"] if "character count" in f["issue"].lower()]
assert len(issues) >= 1
assert any(f["severity"] == "medium" for f in issues)
def test_character_count_in_metrics(self):
"""Report metrics should include character_count."""
lyrics = "[Verse 1]\nHello world\n"
report, code = run_script("--text", lyrics)
assert report is not None
assert "character_count" in report["metrics"]
assert report["metrics"]["character_count"] == len(lyrics)
def test_punctuation_density_detection(self):
"""Lines with heavy punctuation should trigger a rhythm finding."""
lyrics = "[Verse 1]\nwell, - ; : ... yes\n"
report, code = run_script("--text", lyrics)
assert report is not None
issues = [f for f in report["findings"] if "punctuation" in f["issue"].lower()]
assert len(issues) >= 1
assert issues[0]["severity"] == "low"
assert issues[0]["category"] == "rhythm"
def test_clean_lyrics_normal_punctuation_passes(self):
"""Clean lyrics with normal punctuation should pass without punctuation findings."""
lyrics = (
"[Verse 1]\n"
"Walking through the morning light\n"
"Counting shadows on the wall\n"
"\n"
"[Chorus]\n"
"Come undone, come undone\n"
"Let the weight fall where it may\n"
"\n"
"[Verse 2]\n"
"Fingerprints on frosted glass\n"
"Letters folded into cranes\n"
"\n"
"[Chorus]\n"
"Come undone, come undone\n"
"Let the weight fall where it may\n"
)
report, code = run_script("--text", lyrics)
assert report is not None
punct_issues = [f for f in report["findings"] if "punctuation" in f["issue"].lower()]
assert len(punct_issues) == 0
def test_new_section_tags_recognized(self):
"""New section tags like Guitar Solo, Instrumental, etc. should not be flagged."""
new_tags = [
"Guitar Solo", "Piano Solo", "Sax Solo", "Saxophone Solo",
"Drum Solo", "Bass Solo", "Solo", "Instrumental", "Interlude",
"Build", "Build-Up", "Buildup", "Drop", "Hook", "Refrain",
"Post-Chorus", "End", "Fade Out", "Fade In", "Break",
]
for tag in new_tags:
lyrics = f"[Verse 1]\nSome line one\nSome line two\n\n[{tag}]\nContent here\n"
report, code = run_script("--text", lyrics)
assert report is not None, f"No report for [{tag}]"
unrecognized = [f for f in report["findings"]
if "Unrecognized" in f.get("issue", "") and tag in f.get("issue", "")]
assert len(unrecognized) == 0, f"[{tag}] was flagged as unrecognized"
def test_vocal_cues_recognized(self):
"""Vocal delivery cues like [Harmonized] should not be flagged as unrecognized."""
cues = ["Harmonized", "Hummed", "Humming", "Whistled", "Whistling",
"Crooning", "Scat", "Call and Response"]
for cue in cues:
lyrics = f"[Verse 1]\nSome line here\n[{cue}]\nMore content\n"
report, code = run_script("--text", lyrics)
assert report is not None, f"No report for [{cue}]"
unrecognized = [f for f in report["findings"]
if "Unrecognized" in f.get("issue", "") and cue in f.get("issue", "")]
assert len(unrecognized) == 0, f"[{cue}] was flagged as unrecognized"
if __name__ == "__main__":
import pytest
pytest.main([__file__, "-v"])

View File

@@ -0,0 +1,106 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = ["pytest>=7.0"]
# ///
"""Tests for validate-options.py"""
import json
import subprocess
import sys
from pathlib import Path
SCRIPT = str(Path(__file__).parent.parent / "validate-options.py")
def run_script(*args):
"""Run the script and return parsed JSON output."""
result = subprocess.run(
[sys.executable, SCRIPT, *args],
capture_output=True, text=True
)
return json.loads(result.stdout) if result.stdout else None, result.returncode
class TestValidateOptions:
def test_all_valid_codes(self):
report, code = run_script("ST,CC,RA,CD")
assert report is not None
assert report["script"] == "validate-options"
assert report["status"] == "pass"
assert set(report["validated_codes"]) == {"ST", "CC", "RA", "CD"}
assert report["removed_codes"] == []
def test_invalid_code(self):
report, code = run_script("ST,ZZ,RA")
assert report is not None
assert report["status"] == "error"
issues = [f["issue"] for f in report["findings"]]
assert any("ZZ" in i for i in issues)
def test_fr_wf_mutual_exclusion(self):
report, code = run_script("FR,WF")
assert report is not None
assert report["status"] == "error"
issues = [f["issue"] for f in report["findings"]]
assert any("mutually exclusive" in i.lower() for i in issues)
def test_fr_auto_removes_ce(self):
report, code = run_script("FR,CE,RA")
assert report is not None
assert "CE" in report["removed_codes"]
assert "CE" not in report["validated_codes"]
assert "FR" in report["validated_codes"]
assert "RA" in report["validated_codes"]
def test_ce_cc_info_note(self):
report, code = run_script("CE,CC")
assert report is not None
issues = [f["issue"] for f in report["findings"]]
assert any("CC" in i and "redundant" in i.lower() for i in issues)
# CC should still be in validated codes (info only, not removed)
assert "CC" in report["validated_codes"]
def test_empty_codes(self):
report, code = run_script("--codes", "")
assert report is not None
assert report["status"] == "error"
assert any(f["severity"] == "critical" for f in report["findings"])
def test_codes_flag(self):
report, code = run_script("--codes", "ST,RA")
assert report is not None
assert report["status"] == "pass"
assert set(report["validated_codes"]) == {"ST", "RA"}
def test_duplicate_codes(self):
report, code = run_script("ST,ST,RA")
assert report is not None
issues = [f["issue"] for f in report["findings"]]
assert any("Duplicate" in i for i in issues)
assert report["validated_codes"].count("ST") == 1
def test_help_flag(self):
result = subprocess.run(
[sys.executable, SCRIPT, "--help"],
capture_output=True, text=True
)
assert result.returncode == 0
assert "validate" in result.stdout.lower()
def test_report_structure(self):
report, code = run_script("ST")
assert report is not None
assert "script" in report
assert "version" in report
assert "timestamp" in report
assert "status" in report
assert "validated_codes" in report
assert "removed_codes" in report
assert "findings" in report
assert "summary" in report
if __name__ == "__main__":
import pytest
pytest.main([__file__, "-v"])