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

View file

@ -0,0 +1,184 @@
"""Adapter wire-protocol tests (M7): JSON shapes in/out, error mapping.
HTTP adapters take an injectable httpx client; faster-whisper is an
optional dependency and is only exercised when installed.
"""
from __future__ import annotations
import importlib.util
import httpx
import pytest
from shonar.core.config import Settings
from shonar.services import ai
from shonar.services.ai import ProviderConfigError, SummaryResult
from shonar.services.ai.ollama import OllamaProvider
from shonar.services.ai.openai_compat import OpenAICompatProvider
from shonar.services.ai.whisper_http import WhisperHttpProvider
def mock_client(handler) -> httpx.AsyncClient:
return httpx.AsyncClient(transport=httpx.MockTransport(handler))
# --- factories -----------------------------------------------------------------
def test_factories_none_means_skip():
s = Settings(transcription_provider="none", llm_provider="none")
assert ai.get_transcription_provider(s) is None
assert ai.get_llm_provider(s) is None
def test_factories_unknown_names_raise_config_error():
s = Settings(transcription_provider="whisper-9k")
with pytest.raises(ProviderConfigError):
ai.get_transcription_provider(s)
s = Settings(llm_provider="clippy")
with pytest.raises(ProviderConfigError):
ai.get_llm_provider(s)
def test_factories_missing_urls_raise_config_error():
s = Settings(transcription_provider="whisper_http", transcription_base_url="")
with pytest.raises(ProviderConfigError):
ai.get_transcription_provider(s)
s = Settings(llm_provider="openai_compat", llm_base_url="http://x", llm_model="")
with pytest.raises(ProviderConfigError):
ai.get_llm_provider(s)
def test_faster_whisper_missing_dep_is_config_error():
if importlib.util.find_spec("faster_whisper") is not None:
pytest.skip("faster-whisper installed; missing-dep path N/A")
# Constructor validates eagerly so misconfiguration fails at startup,
# not on the first recording.
with pytest.raises(ProviderConfigError):
ai.get_transcription_provider(Settings(transcription_provider="faster_whisper"))
# --- whisper_http -----------------------------------------------------------------
def whisper_ok(request: httpx.Request) -> httpx.Response:
assert request.url.path == "/v1/audio/transcriptions"
assert request.method == "POST"
return httpx.Response(200, json={
"text": "hello world",
"language": "en",
"segments": [{"start": 0.0, "end": 1.2, "text": "hello world"}],
})
async def test_whisper_http_happy_path():
p = WhisperHttpProvider("http://stt:8000", model="small",
http_client=mock_client(whisper_ok))
res = await p.transcribe(b"\x00" * 16, "audio/wav")
assert res.text == "hello world"
assert res.language == "en"
assert [(s.start, s.end, s.text) for s in res.segments] == [(0.0, 1.2, "hello world")]
assert res.model == "small"
async def test_whisper_http_401_is_config_error():
async def denied(request: httpx.Request) -> httpx.Response:
return httpx.Response(401, json={"detail": "nope"})
p = WhisperHttpProvider("http://stt:8000", http_client=mock_client(denied))
with pytest.raises(ProviderConfigError):
await p.transcribe(b"\x00" * 16, "audio/wav")
async def test_whisper_http_503_is_transient():
async def busy(request: httpx.Request) -> httpx.Response:
return httpx.Response(503, text="overloaded")
from shonar.services.ai import ProviderTransientError
p = WhisperHttpProvider("http://stt:8000", http_client=mock_client(busy))
with pytest.raises(ProviderTransientError):
await p.transcribe(b"\x00" * 16, "audio/wav")
# --- openai_compat ------------------------------------------------------------------
def chat_ok(request: httpx.Request) -> httpx.Response:
assert request.url.path == "/v1/chat/completions"
body = {
"short": "Standup.",
"detailed": "The team met.",
"key_points": ["a", "b"],
"decisions": ["ship"],
"action_items": [{"not": "a string"}, "call ana"],
"questions": [],
"extra_key": "ignored",
}
import json as _json
return httpx.Response(200, json={"choices": [{"message": {"content": _json.dumps(body)}}]})
async def test_openai_compat_parses_and_sanitizes():
p = OpenAICompatProvider("http://llm:8000", model="qwen",
http_client=mock_client(chat_ok))
res = await p.summarize("a very long meeting transcript", title="Standup")
assert isinstance(res, SummaryResult)
assert res.short == "Standup."
assert res.action_items == ("call ana",) # non-strings dropped
assert res.model == "qwen"
async def test_openai_compat_partial_json_gets_defaults():
import json as _json
async def partial(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json={
"choices": [{"message": {"content": _json.dumps({"short": "Hi."})}}]
})
p = OpenAICompatProvider("http://llm:8000", model="qwen",
http_client=mock_client(partial))
res = await p.summarize("hi")
assert res.short == "Hi."
assert res.detailed == "" and res.key_points == ()
async def test_openai_compat_non_json_is_transient():
async def garbage(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json={
"choices": [{"message": {"content": "Sure! Here it is..."}}]
})
from shonar.services.ai import ProviderTransientError
p = OpenAICompatProvider("http://llm:8000", model="qwen",
http_client=mock_client(garbage))
with pytest.raises(ProviderTransientError):
await p.summarize("hi")
# --- ollama ---------------------------------------------------------------------------
async def test_ollama_happy_path():
import json as _json
async def ok(request: httpx.Request) -> httpx.Response:
assert request.url.path == "/api/chat"
payload = _json.loads(request.content.decode())
assert payload["format"] == "json" and payload["stream"] is False
return httpx.Response(200, json={
"message": {"content": _json.dumps({"short": "S.", "detailed": "D."})}
})
p = OllamaProvider("http://ollama:11434", model="llama3",
http_client=mock_client(ok))
res = await p.summarize("meeting notes")
assert res.short == "S." and res.model == "llama3"
async def test_ollama_404_is_config_error():
async def missing(request: httpx.Request) -> httpx.Response:
return httpx.Response(404, text="model not found")
p = OllamaProvider("http://ollama:11434", model="nope",
http_client=mock_client(missing))
with pytest.raises(ProviderConfigError):
await p.summarize("hi")