mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-23 21:32:33 +00:00
* feat(career): gigs backend — propose a setlist, log the completed set
Career v3, WS3 (backend half). A gig is career's verb:
- POST /gigs/propose {instrument, genre, size}: setlist from the
passport's own stubs — qualifying songs (per the genre's badge bar,
family-aware) shuffled for a free re-roll, topped with the
highest-accuracy near-bar songs as stakes, and filled from UNPLAYED
genre songs when the passport is young (the first gig is how stubs
start). Names the highest venue the current stars can book.
- POST /gigs: logs a COMPLETED set only (abandoned sets never log — no
fail state). Per-song accuracy = MAX(last_accuracy) from song_stats,
freshly written by the set's own plays; encore = avg ≥ the data-driven
bar (passports.json gig.encore_accuracy, 0.75). Appends to the career
state file (same atomic _save_json pattern).
- Passports view: per-passport gigs (newest first, capped 20) and
per-instrument gig_count — the profile wall's gig line lights up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(career): gig backfill offsets by qualifying taken, not picks length
CodeRabbit on #954: after the stakes loop appends near-bar songs,
qualifying[len(picks):] overshoots and skips eligible qualifying songs
— a stocked passport could still get a short set.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
96 lines
3.3 KiB
Python
96 lines
3.3 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
|
|
)"""
|
|
)
|
|
|
|
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 ?, ?, ?, ?, ? 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=""):
|
|
"""A library song with no plays — feeds the genre (brochure) list."""
|
|
self.conn.execute("INSERT INTO songs VALUES (?, ?, ?, ?, ?)",
|
|
(filename, filename, "Test Artist", genre, None))
|
|
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)
|