mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 19:29:33 +00:00
v3 library: first-hour polish — zero-states, match progress, provenance, alias search (#730)
* v3 library: first-hour polish — zero-states, match progress, provenance, alias search
Six launch-eve fixes for a brand-new user's first hour with a fresh,
being-matched library. Each is small and reuses shipped idioms.
- Invitational repertoire meter: with no practice data yet, the home meter
no longer reads "0 of N mastered" (debt framing) — it shows an empty bar
with "grows as you master songs". A count of 0 read as failure on day one.
- "Start here" starter shelf: growth_edge_suggestions() distinguishes two
empties — attempts exist but all mastered (honest empty shelf) vs nothing
attempted yet (day one) → new starter_suggestions() returns up to 8
approachable songs (90–480s, shortest first) flagged starter:true, and the
client renders a "Start here" shelf instead of a blank home.
- Library-visible match progress: while the background pass runs, a quiet
"Matching your library — X of Y" line sits by the review chip (5s poll,
single guarded interval, cleared the moment the pass stops — no leak,
no toast, silent completion).
- One-time transparency toast: the first time an install is seen matching a
real library, one fbNotify names what's contacted (MusicBrainz / Cover Art
Archive), that results are stored locally, that files aren't changed
without you, and where the switch is. localStorage-gated, wrapped so a
blocked notifier can't break the chip.
- Empty-library dead-end card: a genuinely empty local library (no songs, no
query/filter) shows "Your library is empty" + drop-files hint + Open
Settings, instead of a bare grid under dead dropdowns.
- Alias-aware search: searching a canonical name ("AC/DC") now also finds
songs whose raw tag is a merged variant ("ACDC"), via the artist_alias
table. Probe-guarded so a no-aliases library keeps the exact original
3-term query; pure predicate, keyset-safe.
- Details-drawer provenance line: matched/manual rows show "Matched:
<artist — title> (source) · Fix match" under the Identity fields — the
wrong-match escape hatch at the point of the data, wired to the same
fix-match flow the card menu uses. New read-only GET
/api/enrichment/song/{filename} backs it.
Tests: tests/test_starter_suggestions.py (starter vs normal-shelf behaviour,
length window, attempts-exist path unchanged) + alias-search cases added to
tests/test_artist_alias.py. 34 targeted pass; node --check clean; no new
Tailwind classes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nm7tHs1Yvjjtnnu4nzJgdN
* fix(v3 library): stop match-progress poll when leaving the library screen
The 5s enrichment poll (_pollTimer) was cleared on pass completion and on
fetch error, but not when the user navigated away from the library. Leaving
v3-songs mid-pass left the interval pinging /api/enrichment/status in the
background until the pass ended. Subscribe to the existing feedBack
'screen:changed' event: clear the poll when any non-v3-songs screen shows,
and refresh (re-arming if a pass is still running) on returning to v3-songs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: byrongamatos <xasiklas@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
byrongamatos
parent
df2d660d1e
commit
8c7cde5d5c
@@ -0,0 +1,119 @@
|
||||
"""Tests for the 'Start here' starter shelf (launch polish) —
|
||||
GET /api/library/practice-suggestions when NO practice attempts exist.
|
||||
|
||||
growth_edge_suggestions returns starter picks (sensible-length songs,
|
||||
shortest first, flagged starter:true) only on a never-practiced library;
|
||||
the moment any scored attempt exists the normal growth-edge behaviour is
|
||||
unchanged — including the honest empty shelf when everything attempted is
|
||||
mastered. Read-only, like the recommender it falls back from."""
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def server(tmp_path, monkeypatch, isolate_logging):
|
||||
monkeypatch.setenv("CONFIG_DIR", str(tmp_path))
|
||||
monkeypatch.setenv("FEEDBACK_SKIP_STARTUP_TASKS", "1")
|
||||
sys.modules.pop("server", None)
|
||||
srv = importlib.import_module("server")
|
||||
try:
|
||||
yield srv
|
||||
finally:
|
||||
conn = getattr(getattr(srv, "meta_db", None), "conn", None)
|
||||
if conn is not None:
|
||||
conn.close()
|
||||
sys.modules.pop("server", None)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(server):
|
||||
return TestClient(server.app)
|
||||
|
||||
|
||||
def _seed(server, fn, duration, title=None):
|
||||
server.meta_db.put(fn, 0, 0, {
|
||||
"title": title or fn.split(".")[0], "artist": "A", "duration": duration})
|
||||
|
||||
|
||||
def _play(server, fn, acc, arr=0):
|
||||
"""Record a scored attempt so the song has a best_accuracy."""
|
||||
server.meta_db.record_session(fn, arr, score=int(acc * 1000), accuracy=acc)
|
||||
|
||||
|
||||
def _suggest(client, limit=8):
|
||||
return client.get(f"/api/library/practice-suggestions?limit={limit}").json()
|
||||
|
||||
|
||||
# ── No attempts → starter picks, shortest sensible first ─────────────────────
|
||||
|
||||
def test_no_attempts_returns_starter_rows(client, server):
|
||||
_seed(server, "long.archive", 600) # > 480s → not a starter
|
||||
_seed(server, "jingle.archive", 30) # < 90s → not a starter
|
||||
_seed(server, "mid.archive", 200)
|
||||
_seed(server, "short.archive", 120)
|
||||
rows = _suggest(client)
|
||||
assert [r["filename"] for r in rows] == ["short.archive", "mid.archive"]
|
||||
assert all(r["starter"] is True for r in rows)
|
||||
|
||||
|
||||
def test_starter_duration_bounds_inclusive(client, server):
|
||||
_seed(server, "at90.archive", 90)
|
||||
_seed(server, "at480.archive", 480)
|
||||
_seed(server, "under.archive", 89)
|
||||
_seed(server, "over.archive", 481)
|
||||
_seed(server, "nodur.archive", 0) # unknown length → never a starter
|
||||
got = {r["filename"] for r in _suggest(client)}
|
||||
assert got == {"at90.archive", "at480.archive"}
|
||||
|
||||
|
||||
def test_starter_caps_at_eight(client, server):
|
||||
for i in range(10):
|
||||
_seed(server, f"s{i:02d}.archive", 100 + i)
|
||||
assert len(_suggest(client)) == 8
|
||||
# Even an explicit larger limit never exceeds the starter cap of 8.
|
||||
assert len(_suggest(client, limit=20)) == 8
|
||||
|
||||
|
||||
def test_starter_rows_are_enriched_and_growth_shaped(client, server):
|
||||
"""Same row shape as the growth-edge rows (the client reuses the card
|
||||
markup verbatim) plus the starter marker; enriched by the route."""
|
||||
_seed(server, "song.archive", 150, title="My Song")
|
||||
r = _suggest(client)[0]
|
||||
assert r["starter"] is True
|
||||
assert r["title"] == "My Song" and r["artist"] == "A"
|
||||
assert r["art_url"].endswith("/art")
|
||||
for key in ("filename", "best_accuracy", "arrangement", "last_played_at",
|
||||
"user_difficulty", "growth_score"):
|
||||
assert key in r
|
||||
# No attempt yet → no accuracy/arrangement; the client passes an
|
||||
# undefined arrangement so playSong picks the default.
|
||||
assert r["best_accuracy"] is None
|
||||
assert r["arrangement"] is None
|
||||
|
||||
|
||||
# ── Attempts exist → normal growth-edge behaviour, unchanged ─────────────────
|
||||
|
||||
def test_attempts_exist_normal_behaviour_unchanged(client, server):
|
||||
_seed(server, "inprog.archive", 150)
|
||||
_seed(server, "fresh.archive", 150)
|
||||
_play(server, "inprog.archive", 0.6)
|
||||
rows = _suggest(client)
|
||||
assert [r["filename"] for r in rows] == ["inprog.archive"]
|
||||
assert not any(r.get("starter") for r in rows)
|
||||
|
||||
|
||||
def test_all_mastered_returns_empty_not_starter(client, server):
|
||||
"""Attempts exist and everything attempted is mastered → the shelf is
|
||||
honestly empty; the starter fallback must NOT kick in."""
|
||||
_seed(server, "done.archive", 150)
|
||||
_seed(server, "fresh.archive", 150)
|
||||
_play(server, "done.archive", 0.95)
|
||||
assert _suggest(client) == []
|
||||
|
||||
|
||||
def test_empty_library_returns_empty(client, server):
|
||||
assert _suggest(client) == []
|
||||
Reference in New Issue
Block a user