fix(backend): break the app<->routes import cycle that aborts the test suite

Collecting backend/tests/ fails outright on a clean checkout:

    backend/tests/test_profile_duplicate_names.py:19: in <module>
        from database import Base, VoiceProfile as DBVoiceProfile
    E   ImportError: attempted relative import beyond top-level package

pytest stops at the collection error, so the whole backend suite runs zero
tests rather than the ~142 it otherwise would.

There are two causes stacked on top of each other.

First, a real cycle in production code. routes/profiles.py, routes/history.py
and routes/stories.py each import safe_content_disposition from ..app, while
app.py builds the FastAPI instance at module scope (app = create_app() on
import), which registers those same routers. Importing any of those three
route modules first therefore re-enters a partially initialised app and dies
with "cannot import name 'router' from partially initialized module". It only
works today because app.py always happens to be imported first.

safe_content_disposition is a pure helper over urllib.parse.quote with no
application state, so it moves to backend/utils/http.py. app.py re-exports it
so any external caller importing it from the old location keeps working.

Second, the test reached for modules through a sys.path hack
(sys.path.insert(parent) + "from database import ...") rather than the
"from backend.X import ..." style the rest of the suite uses. That flat import
makes database/models.py's "from ..utils.capture_chords import ..." point
outside the package. It also aimed at the wrong module: it wants the service
layer, which raises ValueError, not the route handler, which converts that
into an HTTPException.

Result: the full suite goes from 0 collected to 148 passed. The one remaining
failure, test_progress.py::test_hf_progress_tracker, is pre-existing and
unrelated (tqdm patching) - it reproduces identically on an unpatched tree.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Serge Simono
2026-10-04 00:01:11 +00:00
committed by capy-ai-staging[bot]
co-authored by Claude Opus 5
parent db2e7797b9
commit 3e240a9ef1
6 changed files with 27 additions and 22 deletions
+19
View File
@@ -0,0 +1,19 @@
"""HTTP header helpers shared by the download routes.
Lives here rather than in ``app`` so route modules can use it without
importing the application module — ``app`` builds the FastAPI instance at
import time, which registers those same routers and closes an import cycle.
"""
from urllib.parse import quote
def safe_content_disposition(disposition_type: str, filename: str) -> str:
"""Build a Content-Disposition header safe for non-ASCII filenames.
Uses RFC 5987 ``filename*`` parameter so browsers can decode UTF-8
filenames while the ``filename`` fallback stays ASCII-only.
"""
ascii_name = "".join(c for c in filename if c.isascii() and (c.isalnum() or c in " -_.")).strip() or "download"
utf8_name = quote(filename, safe="")
return f"{disposition_type}; filename=\"{ascii_name}\"; filename*=UTF-8''{utf8_name}"