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
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:
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = ["pytest>=7.0", "pyyaml>=6.0"]
|
||||
# ///
|
||||
"""Tests for diff-profiles.py"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from importlib.util import spec_from_file_location, module_from_spec
|
||||
|
||||
spec = spec_from_file_location(
|
||||
"diff_profiles",
|
||||
Path(__file__).parent.parent / "diff-profiles.py"
|
||||
)
|
||||
diff_profiles_mod = module_from_spec(spec)
|
||||
spec.loader.exec_module(diff_profiles_mod)
|
||||
diff_profiles = diff_profiles_mod.diff_profiles
|
||||
|
||||
|
||||
PROFILE_A = {
|
||||
"name": "Test Band",
|
||||
"genre": "indie rock",
|
||||
"mood": "melancholic",
|
||||
"model_preference": "v4.5-all",
|
||||
"tier": "free",
|
||||
"style_baseline": "Indie rock with warm guitars",
|
||||
"vocal": {
|
||||
"gender": "male",
|
||||
"tone": "warm",
|
||||
"delivery": "intimate",
|
||||
"energy": "restrained",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def write_yaml(tmp_path, filename, data):
|
||||
path = tmp_path / filename
|
||||
with open(path, "w") as f:
|
||||
yaml.dump(data, f)
|
||||
return path
|
||||
|
||||
|
||||
def test_identical_profiles(tmp_path):
|
||||
a = write_yaml(tmp_path, "a.yaml", PROFILE_A)
|
||||
b = write_yaml(tmp_path, "b.yaml", PROFILE_A)
|
||||
result = diff_profiles(a, b)
|
||||
assert result["status"] == "pass"
|
||||
assert result["has_changes"] is False
|
||||
assert result["summary"]["total_changes"] == 0
|
||||
|
||||
|
||||
def test_changed_fields(tmp_path):
|
||||
modified = {**PROFILE_A, "genre": "electronic", "mood": "energetic"}
|
||||
a = write_yaml(tmp_path, "a.yaml", PROFILE_A)
|
||||
b = write_yaml(tmp_path, "b.yaml", modified)
|
||||
result = diff_profiles(a, b)
|
||||
assert result["has_changes"] is True
|
||||
assert result["summary"]["fields_changed"] == 2
|
||||
changed_fields = [c["field"] for c in result["changed"]]
|
||||
assert "genre" in changed_fields
|
||||
assert "mood" in changed_fields
|
||||
|
||||
|
||||
def test_added_fields(tmp_path):
|
||||
modified = {**PROFILE_A, "language": "Spanish", "instrumental": True}
|
||||
a = write_yaml(tmp_path, "a.yaml", PROFILE_A)
|
||||
b = write_yaml(tmp_path, "b.yaml", modified)
|
||||
result = diff_profiles(a, b)
|
||||
assert result["has_changes"] is True
|
||||
assert result["summary"]["fields_added"] >= 2
|
||||
added_fields = [c["field"] for c in result["added"]]
|
||||
assert "language" in added_fields
|
||||
assert "instrumental" in added_fields
|
||||
|
||||
|
||||
def test_removed_fields(tmp_path):
|
||||
modified = {k: v for k, v in PROFILE_A.items() if k != "mood"}
|
||||
a = write_yaml(tmp_path, "a.yaml", PROFILE_A)
|
||||
b = write_yaml(tmp_path, "b.yaml", modified)
|
||||
result = diff_profiles(a, b)
|
||||
assert result["has_changes"] is True
|
||||
assert result["summary"]["fields_removed"] >= 1
|
||||
removed_fields = [c["field"] for c in result["removed"]]
|
||||
assert "mood" in removed_fields
|
||||
|
||||
|
||||
def test_nested_changes(tmp_path):
|
||||
modified = {**PROFILE_A, "vocal": {**PROFILE_A["vocal"], "tone": "bright, clear"}}
|
||||
a = write_yaml(tmp_path, "a.yaml", PROFILE_A)
|
||||
b = write_yaml(tmp_path, "b.yaml", modified)
|
||||
result = diff_profiles(a, b)
|
||||
assert result["has_changes"] is True
|
||||
changed_fields = [c["field"] for c in result["changed"]]
|
||||
assert "vocal.tone" in changed_fields
|
||||
|
||||
|
||||
def test_missing_original(tmp_path):
|
||||
b = write_yaml(tmp_path, "b.yaml", PROFILE_A)
|
||||
result = diff_profiles(tmp_path / "nope.yaml", b)
|
||||
assert result["status"] == "fail"
|
||||
assert "errors" in result
|
||||
|
||||
|
||||
def test_missing_modified(tmp_path):
|
||||
a = write_yaml(tmp_path, "a.yaml", PROFILE_A)
|
||||
result = diff_profiles(a, tmp_path / "nope.yaml")
|
||||
assert result["status"] == "fail"
|
||||
|
||||
|
||||
def test_invalid_yaml(tmp_path):
|
||||
a = write_yaml(tmp_path, "a.yaml", PROFILE_A)
|
||||
bad = tmp_path / "bad.yaml"
|
||||
bad.write_text(": {{invalid yaml")
|
||||
result = diff_profiles(a, bad)
|
||||
assert result["status"] == "fail"
|
||||
|
||||
|
||||
def test_mixed_changes(tmp_path):
|
||||
modified = {**PROFILE_A, "genre": "electronic", "language": "French"}
|
||||
del modified["mood"]
|
||||
a = write_yaml(tmp_path, "a.yaml", PROFILE_A)
|
||||
b = write_yaml(tmp_path, "b.yaml", modified)
|
||||
result = diff_profiles(a, b)
|
||||
assert result["has_changes"] is True
|
||||
assert result["summary"]["fields_changed"] >= 1
|
||||
assert result["summary"]["fields_added"] >= 1
|
||||
assert result["summary"]["fields_removed"] >= 1
|
||||
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = ["pytest>=7.0", "pyyaml>=6.0"]
|
||||
# ///
|
||||
"""Tests for list-profiles.py"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from importlib.util import spec_from_file_location, module_from_spec
|
||||
|
||||
spec = spec_from_file_location(
|
||||
"list_profiles",
|
||||
Path(__file__).parent.parent / "list-profiles.py"
|
||||
)
|
||||
list_profiles_mod = module_from_spec(spec)
|
||||
spec.loader.exec_module(list_profiles_mod)
|
||||
list_profiles = list_profiles_mod.list_profiles
|
||||
check_profile = list_profiles_mod.check_profile
|
||||
|
||||
|
||||
SAMPLE_PROFILE = {
|
||||
"name": "Test Band",
|
||||
"genre": "indie rock",
|
||||
"mood": "melancholic",
|
||||
"model_preference": "v4.5-all",
|
||||
"tier": "free",
|
||||
}
|
||||
|
||||
|
||||
def test_nonexistent_directory(tmp_path):
|
||||
result = list_profiles(tmp_path / "nope")
|
||||
assert result["status"] == "pass"
|
||||
assert result["count"] == 0
|
||||
assert "No profiles directory" in result.get("message", "")
|
||||
|
||||
|
||||
def test_empty_directory(tmp_path):
|
||||
profiles_dir = tmp_path / "profiles"
|
||||
profiles_dir.mkdir()
|
||||
result = list_profiles(profiles_dir)
|
||||
assert result["status"] == "pass"
|
||||
assert result["count"] == 0
|
||||
|
||||
|
||||
def test_single_profile(tmp_path):
|
||||
profiles_dir = tmp_path / "profiles"
|
||||
profiles_dir.mkdir()
|
||||
with open(profiles_dir / "test-band.yaml", "w") as f:
|
||||
yaml.dump(SAMPLE_PROFILE, f)
|
||||
result = list_profiles(profiles_dir)
|
||||
assert result["count"] == 1
|
||||
assert result["profiles"][0]["name"] == "Test Band"
|
||||
assert result["profiles"][0]["genre"] == "indie rock"
|
||||
|
||||
|
||||
def test_multiple_profiles(tmp_path):
|
||||
profiles_dir = tmp_path / "profiles"
|
||||
profiles_dir.mkdir()
|
||||
for i in range(3):
|
||||
data = {**SAMPLE_PROFILE, "name": f"Band {i}"}
|
||||
with open(profiles_dir / f"band-{i}.yaml", "w") as f:
|
||||
yaml.dump(data, f)
|
||||
result = list_profiles(profiles_dir)
|
||||
assert result["count"] == 3
|
||||
|
||||
|
||||
def test_writer_voice_detection(tmp_path):
|
||||
profiles_dir = tmp_path / "profiles"
|
||||
profiles_dir.mkdir()
|
||||
data_with_voice = {
|
||||
**SAMPLE_PROFILE,
|
||||
"writer_voice": {"vocabulary": "formal, archaic", "rhythm": "long flowing"}
|
||||
}
|
||||
with open(profiles_dir / "voiced.yaml", "w") as f:
|
||||
yaml.dump(data_with_voice, f)
|
||||
data_without = {**SAMPLE_PROFILE}
|
||||
with open(profiles_dir / "plain.yaml", "w") as f:
|
||||
yaml.dump(data_without, f)
|
||||
|
||||
result = list_profiles(profiles_dir)
|
||||
voiced = next(p for p in result["profiles"] if p["file"] == "voiced.yaml")
|
||||
plain = next(p for p in result["profiles"] if p["file"] == "plain.yaml")
|
||||
assert voiced["has_writer_voice"] is True
|
||||
assert plain["has_writer_voice"] is False
|
||||
|
||||
|
||||
def test_invalid_yaml_skipped(tmp_path):
|
||||
profiles_dir = tmp_path / "profiles"
|
||||
profiles_dir.mkdir()
|
||||
(profiles_dir / "bad.yaml").write_text(": {{invalid")
|
||||
with open(profiles_dir / "good.yaml", "w") as f:
|
||||
yaml.dump(SAMPLE_PROFILE, f)
|
||||
result = list_profiles(profiles_dir)
|
||||
assert result["count"] == 1
|
||||
|
||||
|
||||
def test_new_fields_in_listing(tmp_path):
|
||||
profiles_dir = tmp_path / "profiles"
|
||||
profiles_dir.mkdir()
|
||||
data = {
|
||||
**SAMPLE_PROFILE,
|
||||
"instrumental": True,
|
||||
"language": "Spanish",
|
||||
"creativity_default": "experimental",
|
||||
"generation_history": [{"date": "2026-03-19"}],
|
||||
}
|
||||
with open(profiles_dir / "test.yaml", "w") as f:
|
||||
yaml.dump(data, f)
|
||||
result = list_profiles(profiles_dir)
|
||||
p = result["profiles"][0]
|
||||
assert p["instrumental"] is True
|
||||
assert p["language"] == "Spanish"
|
||||
assert p["creativity_default"] == "experimental"
|
||||
assert p["has_generation_history"] is True
|
||||
|
||||
|
||||
# --- check_profile tests ---
|
||||
|
||||
def test_check_profile_exists(tmp_path):
|
||||
profiles_dir = tmp_path / "profiles"
|
||||
profiles_dir.mkdir()
|
||||
with open(profiles_dir / "test-band.yaml", "w") as f:
|
||||
yaml.dump(SAMPLE_PROFILE, f)
|
||||
result = check_profile(profiles_dir, "test-band")
|
||||
assert result["exists"] is True
|
||||
assert result["name"] == "Test Band"
|
||||
assert "size_bytes" in result
|
||||
assert "last_modified" in result
|
||||
|
||||
|
||||
def test_check_profile_exists_with_extension(tmp_path):
|
||||
profiles_dir = tmp_path / "profiles"
|
||||
profiles_dir.mkdir()
|
||||
with open(profiles_dir / "test-band.yaml", "w") as f:
|
||||
yaml.dump(SAMPLE_PROFILE, f)
|
||||
result = check_profile(profiles_dir, "test-band.yaml")
|
||||
assert result["exists"] is True
|
||||
|
||||
|
||||
def test_check_profile_not_exists(tmp_path):
|
||||
profiles_dir = tmp_path / "profiles"
|
||||
profiles_dir.mkdir()
|
||||
result = check_profile(profiles_dir, "nonexistent")
|
||||
assert result["exists"] is False
|
||||
assert result["query"] == "nonexistent"
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = ["pytest>=7.0"]
|
||||
# ///
|
||||
"""Tests for tier-features.py"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from importlib.util import spec_from_file_location, module_from_spec
|
||||
|
||||
spec = spec_from_file_location(
|
||||
"tier_features",
|
||||
Path(__file__).parent.parent / "tier-features.py"
|
||||
)
|
||||
tier_features_mod = module_from_spec(spec)
|
||||
spec.loader.exec_module(tier_features_mod)
|
||||
get_tier_features = tier_features_mod.get_tier_features
|
||||
|
||||
|
||||
def test_free_tier():
|
||||
result = get_tier_features("free")
|
||||
assert result["status"] == "pass"
|
||||
assert result["tier"] == "free"
|
||||
assert result["sliders_available"] is False
|
||||
assert result["personas_available"] is False
|
||||
assert result["audio_influence_available"] is False
|
||||
assert result["studio_available"] is False
|
||||
assert "v4.5-all" in result["models"]
|
||||
assert len(result["models"]) == 1
|
||||
|
||||
|
||||
def test_pro_tier():
|
||||
result = get_tier_features("pro")
|
||||
assert result["status"] == "pass"
|
||||
assert result["sliders_available"] is True
|
||||
assert result["personas_available"] is True
|
||||
assert result["audio_influence_available"] is True
|
||||
assert result["studio_available"] is False
|
||||
assert "v5 Pro" in result["models"]
|
||||
assert len(result["unavailable"]) >= 1 # Studio and related
|
||||
|
||||
|
||||
def test_premier_tier():
|
||||
result = get_tier_features("premier")
|
||||
assert result["status"] == "pass"
|
||||
assert result["sliders_available"] is True
|
||||
assert result["studio_available"] is True
|
||||
assert len(result["unavailable"]) == 0 # Everything available
|
||||
|
||||
|
||||
def test_invalid_tier():
|
||||
result = get_tier_features("ultimate")
|
||||
assert result["status"] == "fail"
|
||||
assert "error" in result
|
||||
|
||||
|
||||
def test_case_insensitive():
|
||||
result = get_tier_features("PRO")
|
||||
assert result["status"] == "pass"
|
||||
assert result["tier"] == "pro"
|
||||
|
||||
|
||||
def test_free_has_unavailable_features():
|
||||
result = get_tier_features("free")
|
||||
assert len(result["unavailable"]) > 5 # Many features gated
|
||||
|
||||
|
||||
def test_all_tiers_have_available():
|
||||
for tier in ["free", "pro", "premier"]:
|
||||
result = get_tier_features(tier)
|
||||
assert len(result["available"]) > 0
|
||||
|
||||
|
||||
def test_all_tiers_have_pricing():
|
||||
for tier in ["free", "pro", "premier"]:
|
||||
result = get_tier_features(tier)
|
||||
assert "pricing" in result
|
||||
assert "monthly" in result["pricing"]
|
||||
assert "annual_monthly" in result["pricing"]
|
||||
|
||||
|
||||
def test_all_tiers_have_song_length():
|
||||
for tier in ["free", "pro", "premier"]:
|
||||
result = get_tier_features(tier)
|
||||
assert "song_length_max" in result
|
||||
|
||||
|
||||
def test_all_tiers_have_download_quality():
|
||||
for tier in ["free", "pro", "premier"]:
|
||||
result = get_tier_features(tier)
|
||||
assert "download_quality" in result
|
||||
|
||||
|
||||
def test_all_tiers_have_credit_cost():
|
||||
for tier in ["free", "pro", "premier"]:
|
||||
result = get_tier_features(tier)
|
||||
assert "credit_cost" in result
|
||||
assert result["credit_cost"]["generation"] == 10
|
||||
assert result["credit_cost"]["extension"] == 5
|
||||
|
||||
|
||||
def test_free_pricing_is_zero():
|
||||
result = get_tier_features("free")
|
||||
assert result["pricing"]["monthly"] == 0
|
||||
assert result["pricing"]["annual_monthly"] == 0
|
||||
|
||||
|
||||
def test_pro_pricing():
|
||||
result = get_tier_features("pro")
|
||||
assert result["pricing"]["monthly"] == 10
|
||||
assert result["pricing"]["annual_monthly"] == 8
|
||||
|
||||
|
||||
def test_premier_pricing():
|
||||
result = get_tier_features("premier")
|
||||
assert result["pricing"]["monthly"] == 30
|
||||
assert result["pricing"]["annual_monthly"] == 24
|
||||
|
||||
|
||||
def test_legacy_models_flagged():
|
||||
for tier in ["pro", "premier"]:
|
||||
result = get_tier_features(tier)
|
||||
assert "legacy_models" in result
|
||||
assert "v4 Pro" in result["legacy_models"]
|
||||
@@ -0,0 +1,314 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = ["pytest>=7.0", "pyyaml>=6.0"]
|
||||
# ///
|
||||
"""Tests for validate-profile.py"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
# Add parent directory to path for import
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from importlib.util import spec_from_file_location, module_from_spec
|
||||
|
||||
# Import the module
|
||||
spec = spec_from_file_location(
|
||||
"validate_profile",
|
||||
Path(__file__).parent.parent / "validate-profile.py"
|
||||
)
|
||||
validate_profile_mod = module_from_spec(spec)
|
||||
spec.loader.exec_module(validate_profile_mod)
|
||||
validate_profile = validate_profile_mod.validate_profile
|
||||
derive_filename = validate_profile_mod.derive_filename
|
||||
|
||||
|
||||
def write_profile(tmp_path, data):
|
||||
"""Helper to write a YAML profile and return its path."""
|
||||
profile_path = tmp_path / "test-band.yaml"
|
||||
with open(profile_path, "w") as f:
|
||||
yaml.dump(data, f)
|
||||
return profile_path
|
||||
|
||||
|
||||
VALID_PROFILE = {
|
||||
"name": "Test Band",
|
||||
"genre": "indie rock",
|
||||
"mood": "melancholic",
|
||||
"model_preference": "v4.5-all",
|
||||
"tier": "free",
|
||||
"style_baseline": "Indie rock with warm guitars and atmospheric pads",
|
||||
"vocal": {
|
||||
"gender": "male",
|
||||
"tone": "warm, breathy",
|
||||
"delivery": "intimate",
|
||||
"energy": "restrained",
|
||||
},
|
||||
}
|
||||
|
||||
VALID_INSTRUMENTAL_PROFILE = {
|
||||
"name": "Ambient Waves",
|
||||
"genre": "ambient electronic",
|
||||
"mood": "contemplative, spacious",
|
||||
"model_preference": "v4.5-all",
|
||||
"tier": "free",
|
||||
"style_baseline": "Ambient electronic with lush pads and field recordings",
|
||||
"instrumental": True,
|
||||
}
|
||||
|
||||
|
||||
def test_valid_profile(tmp_path):
|
||||
path = write_profile(tmp_path, VALID_PROFILE)
|
||||
result = validate_profile(path)
|
||||
assert result["status"] == "pass"
|
||||
assert result["summary"]["total"] == 0
|
||||
|
||||
|
||||
def test_missing_file(tmp_path):
|
||||
path = tmp_path / "nonexistent.yaml"
|
||||
result = validate_profile(path)
|
||||
assert result["status"] == "fail"
|
||||
assert result["summary"]["critical"] == 1
|
||||
|
||||
|
||||
def test_invalid_yaml(tmp_path):
|
||||
path = tmp_path / "bad.yaml"
|
||||
path.write_text(": invalid: yaml: {{{{")
|
||||
result = validate_profile(path)
|
||||
assert result["status"] == "fail"
|
||||
assert result["summary"]["critical"] >= 1
|
||||
|
||||
|
||||
def test_missing_required_fields(tmp_path):
|
||||
path = write_profile(tmp_path, {"name": "Test"})
|
||||
result = validate_profile(path)
|
||||
assert result["status"] == "fail"
|
||||
assert result["summary"]["critical"] >= 1
|
||||
|
||||
|
||||
def test_invalid_model(tmp_path):
|
||||
data = {**VALID_PROFILE, "model_preference": "v99 Ultra"}
|
||||
path = write_profile(tmp_path, data)
|
||||
result = validate_profile(path)
|
||||
assert any(f.get("location", {}).get("field") == "model_preference"
|
||||
for f in result["findings"])
|
||||
|
||||
|
||||
def test_invalid_tier(tmp_path):
|
||||
data = {**VALID_PROFILE, "tier": "ultimate"}
|
||||
path = write_profile(tmp_path, data)
|
||||
result = validate_profile(path)
|
||||
assert any("tier" in str(f) for f in result["findings"])
|
||||
|
||||
|
||||
def test_style_baseline_too_long(tmp_path):
|
||||
data = {**VALID_PROFILE, "style_baseline": "x" * 1001}
|
||||
path = write_profile(tmp_path, data)
|
||||
result = validate_profile(path)
|
||||
assert any("style_baseline" in str(f) for f in result["findings"])
|
||||
|
||||
|
||||
def test_style_baseline_v4_pro_200_limit(tmp_path):
|
||||
data = {**VALID_PROFILE, "model_preference": "v4 Pro", "tier": "pro",
|
||||
"style_baseline": "x" * 201}
|
||||
path = write_profile(tmp_path, data)
|
||||
result = validate_profile(path)
|
||||
assert any("style_baseline" in str(f) and "200" in str(f)
|
||||
for f in result["findings"])
|
||||
|
||||
|
||||
def test_free_tier_wrong_model(tmp_path):
|
||||
data = {**VALID_PROFILE, "tier": "free", "model_preference": "v5 Pro"}
|
||||
path = write_profile(tmp_path, data)
|
||||
result = validate_profile(path)
|
||||
assert any("free" in f.get("issue", "").lower() or "free" in f.get("fix", "").lower()
|
||||
for f in result["findings"])
|
||||
|
||||
|
||||
def test_free_tier_slider_warning(tmp_path):
|
||||
data = {**VALID_PROFILE, "sliders": {"weirdness": 80, "style_influence": 30}}
|
||||
path = write_profile(tmp_path, data)
|
||||
result = validate_profile(path)
|
||||
assert any("slider" in f.get("issue", "").lower() for f in result["findings"])
|
||||
|
||||
|
||||
def test_slider_out_of_range(tmp_path):
|
||||
data = {**VALID_PROFILE, "tier": "pro", "model_preference": "v5 Pro",
|
||||
"sliders": {"weirdness": 150}}
|
||||
path = write_profile(tmp_path, data)
|
||||
result = validate_profile(path)
|
||||
assert any("out of range" in f.get("issue", "").lower() for f in result["findings"])
|
||||
|
||||
|
||||
def test_audio_influence_slider_validation(tmp_path):
|
||||
data = {**VALID_PROFILE, "tier": "pro", "model_preference": "v5 Pro",
|
||||
"sliders": {"audio_influence": 200}}
|
||||
path = write_profile(tmp_path, data)
|
||||
result = validate_profile(path)
|
||||
assert any("audio_influence" in str(f) and "out of range" in f.get("issue", "").lower()
|
||||
for f in result["findings"])
|
||||
|
||||
|
||||
def test_invalid_vocal_gender(tmp_path):
|
||||
data = {**VALID_PROFILE}
|
||||
data["vocal"] = {**VALID_PROFILE["vocal"], "gender": "robot"}
|
||||
path = write_profile(tmp_path, data)
|
||||
result = validate_profile(path)
|
||||
assert any("gender" in str(f) for f in result["findings"])
|
||||
|
||||
|
||||
def test_missing_vocal_fields(tmp_path):
|
||||
data = {**VALID_PROFILE, "vocal": {"gender": "male"}}
|
||||
path = write_profile(tmp_path, data)
|
||||
result = validate_profile(path)
|
||||
assert result["summary"]["high"] >= 1
|
||||
|
||||
|
||||
def test_too_many_exclusions(tmp_path):
|
||||
data = {**VALID_PROFILE, "exclusion_defaults": [f"no thing {i}" for i in range(7)]}
|
||||
path = write_profile(tmp_path, data)
|
||||
result = validate_profile(path)
|
||||
assert any("exclusion" in f.get("issue", "").lower() for f in result["findings"])
|
||||
|
||||
|
||||
def test_pro_tier_valid_with_sliders(tmp_path):
|
||||
data = {
|
||||
**VALID_PROFILE,
|
||||
"tier": "pro",
|
||||
"model_preference": "v5 Pro",
|
||||
"sliders": {"weirdness": 70, "style_influence": 40},
|
||||
}
|
||||
path = write_profile(tmp_path, data)
|
||||
result = validate_profile(path)
|
||||
assert result["status"] == "pass"
|
||||
|
||||
|
||||
# --- Instrumental profile tests ---
|
||||
|
||||
def test_instrumental_profile_valid_without_vocal(tmp_path):
|
||||
path = write_profile(tmp_path, VALID_INSTRUMENTAL_PROFILE)
|
||||
result = validate_profile(path)
|
||||
assert result["status"] == "pass"
|
||||
assert result["summary"]["total"] == 0
|
||||
|
||||
|
||||
def test_instrumental_profile_with_optional_vocal(tmp_path):
|
||||
data = {**VALID_INSTRUMENTAL_PROFILE, "vocal": {"gender": "any"}}
|
||||
path = write_profile(tmp_path, data)
|
||||
result = validate_profile(path)
|
||||
assert result["status"] == "pass"
|
||||
|
||||
|
||||
def test_non_instrumental_requires_vocal(tmp_path):
|
||||
data = {**VALID_PROFILE}
|
||||
del data["vocal"]
|
||||
path = write_profile(tmp_path, data)
|
||||
result = validate_profile(path)
|
||||
assert result["status"] == "fail"
|
||||
assert result["summary"]["high"] >= 1
|
||||
|
||||
|
||||
# --- New field tests ---
|
||||
|
||||
def test_valid_creativity_default(tmp_path):
|
||||
for mode in ["conservative", "balanced", "experimental"]:
|
||||
data = {**VALID_PROFILE, "creativity_default": mode}
|
||||
path = write_profile(tmp_path, data)
|
||||
result = validate_profile(path)
|
||||
assert not any(f.get("location", {}).get("field") == "creativity_default"
|
||||
for f in result["findings"]), f"Failed for {mode}"
|
||||
|
||||
|
||||
def test_invalid_creativity_default(tmp_path):
|
||||
data = {**VALID_PROFILE, "creativity_default": "wild"}
|
||||
path = write_profile(tmp_path, data)
|
||||
result = validate_profile(path)
|
||||
assert any("creativity_default" in str(f) for f in result["findings"])
|
||||
|
||||
|
||||
def test_valid_language(tmp_path):
|
||||
data = {**VALID_PROFILE, "language": "Spanish"}
|
||||
path = write_profile(tmp_path, data)
|
||||
result = validate_profile(path)
|
||||
assert not any(f.get("location", {}).get("field") == "language"
|
||||
for f in result["findings"])
|
||||
|
||||
|
||||
def test_empty_language(tmp_path):
|
||||
data = {**VALID_PROFILE, "language": ""}
|
||||
path = write_profile(tmp_path, data)
|
||||
result = validate_profile(path)
|
||||
assert any("language" in str(f) for f in result["findings"])
|
||||
|
||||
|
||||
def test_generation_history_valid(tmp_path):
|
||||
data = {**VALID_PROFILE, "generation_history": [
|
||||
{"date": "2026-03-19", "style_prompt": "test", "model": "v4.5-all"}
|
||||
]}
|
||||
path = write_profile(tmp_path, data)
|
||||
result = validate_profile(path)
|
||||
assert not any(f.get("location", {}).get("field") == "generation_history"
|
||||
for f in result["findings"])
|
||||
|
||||
|
||||
def test_generation_history_too_many(tmp_path):
|
||||
data = {**VALID_PROFILE, "generation_history": [
|
||||
{"date": f"2026-03-{i:02d}"} for i in range(1, 15)
|
||||
]}
|
||||
path = write_profile(tmp_path, data)
|
||||
result = validate_profile(path)
|
||||
assert any("generation_history" in str(f) for f in result["findings"])
|
||||
|
||||
|
||||
def test_generation_history_not_list(tmp_path):
|
||||
data = {**VALID_PROFILE, "generation_history": "not a list"}
|
||||
path = write_profile(tmp_path, data)
|
||||
result = validate_profile(path)
|
||||
assert any("generation_history" in str(f) for f in result["findings"])
|
||||
|
||||
|
||||
def test_studio_preferences_non_premier_warning(tmp_path):
|
||||
data = {**VALID_PROFILE, "tier": "pro", "model_preference": "v5 Pro",
|
||||
"studio_preferences": {"bpm": 120, "key": "C minor"}}
|
||||
path = write_profile(tmp_path, data)
|
||||
result = validate_profile(path)
|
||||
assert any("studio" in f.get("issue", "").lower() for f in result["findings"])
|
||||
|
||||
|
||||
def test_studio_preferences_premier_valid(tmp_path):
|
||||
data = {**VALID_PROFILE, "tier": "premier", "model_preference": "v5 Pro",
|
||||
"studio_preferences": {"bpm": 120, "key": "C minor", "time_signature": "4/4"}}
|
||||
path = write_profile(tmp_path, data)
|
||||
result = validate_profile(path)
|
||||
assert not any("studio" in f.get("issue", "").lower() for f in result["findings"])
|
||||
|
||||
|
||||
def test_studio_preferences_invalid_bpm(tmp_path):
|
||||
data = {**VALID_PROFILE, "tier": "premier", "model_preference": "v5 Pro",
|
||||
"studio_preferences": {"bpm": "fast"}}
|
||||
path = write_profile(tmp_path, data)
|
||||
result = validate_profile(path)
|
||||
assert any("bpm" in str(f).lower() for f in result["findings"])
|
||||
|
||||
|
||||
# --- derive_filename tests ---
|
||||
|
||||
def test_derive_filename_basic():
|
||||
assert derive_filename("Test Band") == "test-band.yaml"
|
||||
|
||||
|
||||
def test_derive_filename_special_chars():
|
||||
assert derive_filename("The Band's Name!") == "the-bands-name.yaml"
|
||||
|
||||
|
||||
def test_derive_filename_multiple_spaces():
|
||||
assert derive_filename(" My Cool Band ") == "my-cool-band.yaml"
|
||||
|
||||
|
||||
def test_derive_filename_already_kebab():
|
||||
assert derive_filename("already-kebab") == "already-kebab.yaml"
|
||||
Reference in New Issue
Block a user