Files
feedBack/tests/plugins/career/conftest.py
T
byrongamatosandClaude Sonnet 4.6 2a455702b8 feat(career): tuning preference filter + interstitial for gigs
Users can now pick a tuning preference before booking a gig:
- Any (default), Standard only, Drop only, or a specific tuning
- Backend filters the song pool (stubs + filler) by that preference
- Empty-filter case returns a 404 with a descriptive message; frontend
  reverts the pref to 'any' and shows a notification
- Interstitial pause before first song and on tuning changes (all prefs
  except 'specific') via window.feedBack.holdAutoplay(); opens the tuner
  panel in auto mode while the user retunes
- 'Specific' gigs skip interstitials (every song already shares one tuning)
- Graceful degradation: no holdAutoplay → interstitial silently skipped

New backend:
- _tuning_ok_fn helper for standard/drop/specific classification
- _fill_genre_songs accepts optional tuning_ok filter
- propose_gig batch-fetches tuning_name for played stubs, applies filter
- GET /gigs/tunings endpoint for the specific-tuning picker

Tests:
- tests/test_career_gig_tuning.py — 17 Python tests (classification, filter)
- tests/js/career_gig_tuning.test.js — 9 JS tests (interstitial logic)
- tests/plugins/career/conftest.py — songs table schema gets tuning_name

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H2bM5jSbMskpdxm2CmuQVj
2026-09-03 17:52:30 +02:00

97 lines
3.5 KiB
Python

import json
import sqlite3
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / 'plugins' / 'career'))
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
# Drop a sibling 'routes' cached by another plugin's tests (bare-name collision).
sys.modules.pop('routes', None)
import routes as career_routes
class FakeMetaDb:
"""song_stats/songs stand-in for MetadataDB (the plugin reads nothing else).
The real song_stats.arrangement is an INTEGER index into the song's
arrangements JSON; the legacy star tests pass strings ("guitar"), which
the passport code treats as index-less → instrument defaults to guitar."""
def __init__(self):
self.conn = sqlite3.connect(":memory:", check_same_thread=False)
self.conn.execute(
"""CREATE TABLE song_stats (
filename TEXT, arrangement TEXT, best_accuracy REAL,
last_accuracy REAL, last_played_at TEXT,
seconds_total REAL NOT NULL DEFAULT 0
)"""
)
self.conn.execute(
"""CREATE TABLE songs (
filename TEXT, title TEXT, artist TEXT,
genre TEXT DEFAULT '', arrangements TEXT,
tuning_name TEXT DEFAULT '', tuning_sort_key INTEGER DEFAULT 0
)"""
)
def add(self, filename, arrangement, best_accuracy, in_library=True,
genre="", arrangements=None, last_played_at=None, seconds_total=0,
last_accuracy=None):
self.conn.execute("INSERT INTO song_stats VALUES (?, ?, ?, ?, ?, ?)",
(filename, arrangement, best_accuracy,
last_accuracy if last_accuracy is not None else best_accuracy,
last_played_at, seconds_total))
if in_library:
self.conn.execute(
"INSERT INTO songs SELECT ?, ?, ?, ?, ?, '', 0 WHERE NOT EXISTS "
"(SELECT 1 FROM songs WHERE filename = ?)",
(filename, filename.replace(".feedpak", "").title(), "Test Artist",
genre,
json.dumps(arrangements) if arrangements is not None else None,
filename))
self.conn.commit()
def add_song_only(self, filename, genre="", tuning_name=""):
"""A library song with no plays — feeds the genre (brochure) list."""
self.conn.execute("INSERT INTO songs VALUES (?, ?, ?, ?, ?, ?, 0)",
(filename, filename, "Test Artist", genre, None, tuning_name))
self.conn.commit()
@pytest.fixture(autouse=True)
def _bind_career_routes():
"""Keep sys.modules['routes'] pointing at THIS plugin's routes for these tests."""
prev = sys.modules.get('routes')
sys.modules['routes'] = career_routes
try:
yield
finally:
if prev is not None:
sys.modules['routes'] = prev
else:
sys.modules.pop('routes', None)
@pytest.fixture(autouse=True)
def _reset_state():
# Module state outlives tests when the module stays imported — reset the
# mutable bits so ordering can't leak downloads/content between tests.
career_routes._state["downloads"] = {}
yield
@pytest.fixture
def meta_db():
return FakeMetaDb()
@pytest.fixture
def client(tmp_path, meta_db):
app = FastAPI()
career_routes.setup(app, {"config_dir": str(tmp_path), "meta_db": meta_db})
return TestClient(app)