fix: surface Spotify quota exhaustion
Some checks are pending
ci / backend (push) Waiting to run
ci / frontend (push) Waiting to run

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Justin Visser 2026-08-11 22:07:06 +02:00
parent 358d058392
commit bfea9efb5e
6 changed files with 97 additions and 10 deletions

View file

@ -38,6 +38,8 @@ from app.ports.protocols import (
RERANK_FALLBACK_CODE = "rerank_fallback"
RERANK_FALLBACK_MESSAGE = "Ranking output was invalid, so grounded results are shown instead."
RERANK_FALLBACK_JUSTIFICATION = "Selected as a grounded match for your request."
QUOTA_ERROR_CODE = "quota_exhausted"
QUOTA_ERROR_MESSAGE = "Spotify's Development Mode quota is temporarily exhausted."
class _TrackSelection:
@ -140,10 +142,8 @@ class RecommendationPipeline:
return
except CatalogQuotaExhaustedError:
yield PipelineErrorEvent(
code="quota_exhausted",
message=(
"Spotify request quota was exhausted before recommendations could be prepared."
),
code=QUOTA_ERROR_CODE,
message=QUOTA_ERROR_MESSAGE,
)
return
except SpotifyError:
@ -158,9 +158,16 @@ class RecommendationPipeline:
candidate_count=len(intent.candidates),
)
pool = await self._grounded_pool(
session_id, catalog, intent, taste, deadline_at, seeded_pool
)
try:
pool = await self._grounded_pool(
session_id, catalog, intent, taste, deadline_at, seeded_pool
)
except CatalogQuotaExhaustedError:
yield PipelineErrorEvent(
code=QUOTA_ERROR_CODE,
message=QUOTA_ERROR_MESSAGE,
)
return
if not pool:
yield PipelineErrorEvent(
code="no_grounded_results",
@ -209,6 +216,8 @@ class RecommendationPipeline:
self.settings.rerank_count + self.settings.rerank_pool_buffer,
deadline_at,
)
if not result.tracks and result.metrics.did_exhaust_quota:
raise CatalogQuotaExhaustedError
if result.tracks:
self.last_pools[session_id] = result.tracks
return result.tracks

View file

@ -146,6 +146,30 @@ def test_intent_stage_failures_yield_one_typed_error_event() -> None:
asyncio.run(run())
def test_grounding_quota_yields_typed_error_instead_of_empty_results() -> None:
class QuotaCatalog(FakeCatalog):
"""Exhaust the Spotify quota on the first grounding search."""
async def search_tracks(self, query: str, limit: int = 10) -> list[Track]:
self.search_call_count += 1
raise SpotifyQuotaExhaustedError(0.0, "QUOTA_EXCEEDED")
async def run() -> None:
track = _track("candidate", "Candidate Song")
catalog = QuotaCatalog(())
recommender = FakeRecommender([_intent(track)])
events = await _run_pipeline(catalog, recommender)
assert [event.type for event in events] == ["metadata", "error"]
assert isinstance(events[-1], PipelineErrorEvent)
assert events[-1].code == "quota_exhausted"
assert catalog.search_call_count == 1
assert recommender.rerank_call_count == 0
asyncio.run(run())
def test_taste_failure_cancels_sibling_fetches() -> None:
class FailingTasteCatalog(FakeCatalog):
"""Fail one taste request after all sibling requests have started."""