mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 03:09:57 +00:00
* 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> * fix(career): keep manifest cleanup path alive on delete; badge only for installed venues Codex preflight: nulling _appliedManifestVenue on delete skipped pushCrowdManifest's setManifest(null) cleanup, leaving the crowd layer on a deleted pack; and the 'playing here' badge showed for an override venue whose pack was removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(career): generation-guard in-flight manifest fetches Codex preflight: a manifest fetch resolving after a newer refresh (pack deleted, venue switched) could re-apply a stale pack over the user's newer selection — fetches now carry a generation token and bail when superseded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(career): exclude orphaned song_stats from star totals Codex preflight: scans hide rather than delete stats of removed songs, so stars now apply the same existing-song filter other stats surfaces use (filename IN (SELECT filename FROM songs)). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(career): 50/150 star thresholds + star collection overview Byron's progression tuning: club at 50★, arena at 150★. /state now returns star_detail rows (title/artist joined from the library, stars, best accuracy, next-star threshold) sorted closest-to-next-star first, and the career screen renders a collection panel: tier summary plus a per-song list with a 'N% to next star' practice hint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(career): venue select/unselect UX, intro manifest support, fullmatch guards - 'Play here' now also defaults the visualization to Venue (remembering the prior viz); active venues show 'Leave venue' which restores it and sets the '__none__' override so no installed venue silently reapplies. - Pack manifests may ship an intro block (flyover video + ambience mp3); files validate like loops/stingers, .mp3 added to the serving whitelist. - Codex preflight: whitelist regexes use fullmatch (trailing-newline names could validate but 500 on serving). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(career): let pushCrowdManifest clear the manifest on Leave venue Codex preflight: nulling _appliedManifestVenue before refresh skipped the setManifest(null) cleanup branch, leaving the crowd playing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(career): refresh tailwind output --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
71 lines
2.2 KiB
Python
71 lines
2.2 KiB
Python
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
|
|
)"""
|
|
)
|
|
self.conn.execute("CREATE TABLE songs (filename TEXT, title TEXT, artist TEXT)")
|
|
|
|
def add(self, filename, arrangement, best_accuracy, in_library=True):
|
|
self.conn.execute("INSERT INTO song_stats VALUES (?, ?, ?)",
|
|
(filename, arrangement, best_accuracy))
|
|
if in_library:
|
|
self.conn.execute(
|
|
"INSERT INTO songs SELECT ?, ?, ? WHERE NOT EXISTS "
|
|
"(SELECT 1 FROM songs WHERE filename = ?)",
|
|
(filename, filename.replace(".feedpak", "").title(), "Test Artist", filename))
|
|
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)
|