Standalone Shonar Desktop: vendor portable sources + local engine; decouple from ~/Projects/Shonar

- shared/ = portable Android-origin sources vendored from deferred/desktop-server
  (app/build.gradle.kts srcDir repointed; PlaybackController.kt excluded as Android-only)
- backend/ = bundled-lite engine (SQLite + inline queue); .venv symlinked from the
  old checkout, PYTHONPATH pins THIS backend's code over any editable install
- repoRoot() resolves this project dir (env SHONAR_REPO still wins); desktop-dev.sh
  watches shared/ + backend/
- Verified: :app:compileKotlin + :app:test green (23 tests); engine boots on :8010,
  self-migrates, /healthz ok
This commit is contained in:
avi 2026-09-14 17:14:54 -05:00
commit 76c867fca4
136 changed files with 21099 additions and 0 deletions

113
backend/tests/conftest.py Normal file
View file

@ -0,0 +1,113 @@
"""Pytest fixtures.
Tests run against a real PostgreSQL (deploy/docker-compose.dev.yml) using a
dedicated ``shonar_test`` database, plus a temporary local storage root.
"""
from __future__ import annotations
import os
import tempfile
from collections.abc import AsyncIterator
from pathlib import Path
import pytest
import pytest_asyncio
# Configure env BEFORE importing the app so Settings picks it up.
TEST_DB = os.environ.get(
"SHONAR_TEST_DATABASE_URL",
"postgresql+asyncpg://shonar:shonar@localhost:5432/shonar_test",
)
os.environ["SHONAR_DATABASE_URL"] = TEST_DB
os.environ["SHONAR_SECRET_KEY"] = "test-secret-key-0123456789abcdef0123456789abcdef"
os.environ["SHONAR_STORAGE_BACKEND"] = "local"
# Effectively disable the auth rate limit under test (dedicated tests cover
# the limiter behaviour itself).
os.environ["SHONAR_RATE_LIMIT_AUTH"] = "10000/minute"
_tmp_storage = tempfile.mkdtemp(prefix="shonar-test-storage-")
os.environ["SHONAR_STORAGE_PATH"] = _tmp_storage
@pytest_asyncio.fixture(scope="session", loop_scope="session")
async def _setup_db() -> AsyncIterator[None]:
from shonar.db import models # noqa: F401
from shonar.db.base import Base
from shonar.db.session import dispose_engine, get_engine
engine = get_engine()
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
# The tsvector generated columns live only in migration
# fts0000000001 (not in the ORM models), so create_all misses them.
# Apply the same DDL the migration applies (M9 search needs them).
if engine.dialect.name == "postgresql":
# Reuse the real migration's DDL (not a copy) via a sync
# MigrationContext — op.execute() is synchronous there.
import importlib.util
from pathlib import Path
def _apply(sync_conn):
from alembic.migration import MigrationContext
from alembic.operations import Operations
spec = importlib.util.spec_from_file_location(
"fts_migration",
Path(__file__).resolve().parents[1]
/ "migrations/versions/fts0000000001_fts_columns.py",
)
assert spec is not None and spec.loader is not None
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
ctx = MigrationContext.configure(sync_conn)
with Operations.context(ctx):
mod.upgrade()
await conn.run_sync(_apply)
yield
await dispose_engine()
@pytest_asyncio.fixture(loop_scope="session", autouse=True)
async def clean_db(_setup_db: None) -> AsyncIterator[None]:
"""Truncate between tests for isolation."""
yield
from sqlalchemy import delete, text
from shonar.db.base import Base
from shonar.db.session import _session_factory, get_engine # type: ignore[attr-defined]
assert _session_factory is not None
async with _session_factory() as s:
if get_engine().dialect.name == "postgresql":
await s.execute(
text(
"TRUNCATE users, devices, refresh_tokens, recordings, assets, "
"upload_sessions, upload_chunks, transcripts, summaries, tags, "
"recording_tags, processing_jobs, export_jobs, app_settings "
"RESTART IDENTITY CASCADE"
)
)
else:
# SQLite: delete child-first (reverse dependency order).
for table in reversed(Base.metadata.sorted_tables):
await s.execute(delete(table))
await s.commit()
@pytest_asyncio.fixture(loop_scope="session")
async def client(_setup_db) -> AsyncIterator:
from httpx import ASGITransport, AsyncClient
from shonar.main import app
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as c:
yield c
@pytest.fixture()
def storage_root() -> Path:
return Path(_tmp_storage)