feat(career): career plugin — stars from song_stats, venue tiers, pack downloads (career mode PR2)

Bundled plugin: per-song stars from best_accuracy (60/75/85% → 1/2/3★),
cumulative stars unlock bar → club → arena (data-driven venues.json).
Venue packs (UE-rendered crowd loops) download on demand to
CONFIG_DIR/plugin_uploads/career/ on a background thread with sha256 +
zip-slip validation, served via FileResponse. Career screen (promoted
sidebar entry) shows progress and pushes the active venue's manifest
into the crowd video layer (v3VenueCrowd, PR1) — degrades cleanly when
either side is absent. Pack URLs land in venues.json in PR3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
byrongamatos
2026-07-12 01:48:02 +02:00
co-authored by Claude Fable 5
parent f5d448af5c
commit 05a9bee38f
12 changed files with 781 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
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-only stand-in for MetadataDB (the plugin reads nothing else)."""
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
)"""
)
def add(self, filename, arrangement, best_accuracy):
self.conn.execute("INSERT INTO song_stats VALUES (?, ?, ?)",
(filename, arrangement, best_accuracy))
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)