test: cover remaining cloud fallback safeguards

This commit is contained in:
2026-07-30 18:11:27 +02:00
parent 141ff2e013
commit a0af15a037
5 changed files with 39 additions and 9 deletions
+1
View File
@@ -15,6 +15,7 @@ key-pw.xml
secrets.local.xml secrets.local.xml
*.local.xml *.local.xml
config/javis.toml config/javis.toml
/javis.toml
config/*secret*.xml config/*secret*.xml
!config/*.example.xml !config/*.example.xml
+10 -5
View File
@@ -170,10 +170,15 @@ Letzter bestätigter Projektstand:
- uv-Lock und `uv sync --dev`: bestanden - uv-Lock und `uv sync --dev`: bestanden
- Python in `.venv`: 3.12.13 - Python in `.venv`: 3.12.13
- Ruff in `.venv`: 0.16.0 - Ruff in `.venv`: 0.16.0
- Unit-Tests: 60 bestanden; Startskript-Sicherheitsregeln zusätzlich abgedeckt - Unit-Tests: 62 bestanden; Timeout und fehlendes Modell fallen nachweislich lokal zurück
- PowerShell-Syntax des Startskripts: erfolgreich geparst - PowerShell-Syntax des Startskripts: erfolgreich geparst
- Ruff Lint: bestanden - Ruff Lint: bestanden
- Ruff Formatprüfung: bestanden - Ruff Formatprüfung: bestanden
- lokaler nachrichtenfreier CLI-Smoke: bestanden; Nullkostenmodus aktiv,
Gemini nicht konfiguriert, Schlüssel nicht vorhanden, 0 Cloudanfragen
- Obsidian-Vault unverändert: 1 Markdown-, 11 Gesamtdateien, letzter
Schreibzeitpunkt weiterhin 30.07.2026 15:42:31
- Rootserver und Ubuntu-Laptop wurden nicht kontaktiert
Lokaler Modell-Smoke-Test: Lokaler Modell-Smoke-Test:
@@ -257,10 +262,10 @@ Abnahmestatus:
Auf dem bestehenden Feature-Branch als nächstes einzelnes Arbeitspaket: Auf dem bestehenden Feature-Branch als nächstes einzelnes Arbeitspaket:
1. geforderte Resttests ergänzen, insbesondere Timeout und bereinigte Fehler 1. veraltete Architektur-, Sicherheits-, Changelog- und Nächste-Sitzung-Texte
2. vollständige Qualitäts-, Gitignore-, Secret- und Außenbereichsprüfung ausführen auf den tatsächlich implementierten Stand bringen
3. Abschlussdokumentation aktualisieren, Feature-Branch pushen und vor Pascals 2. Feature-Branch pushen und vor Pascals manuellem Live-Test stoppen
manuellem Live-Test stoppen; nicht nach `main` mergen 3. nicht nach `main` mergen
Keinen API-Key in Chat, Git, Obsidian, Logs oder Dokumentation übernehmen. Keinen API-Key in Chat, Git, Obsidian, Logs oder Dokumentation übernehmen.
+4 -4
View File
@@ -60,7 +60,8 @@ Stand: lokaler Textchat auf `main`, 30.07.2026.
- `/provider`, `/privacy` und `/status` geben keine Inhalte oder Keywerte aus - `/provider`, `/privacy` und `/status` geben keine Inhalte oder Keywerte aus
- sicheres `scripts/start-javis.ps1` mit relativer Repositoryprüfung, - sicheres `scripts/start-javis.ps1` mit relativer Repositoryprüfung,
Loopback-Ollama und überschreibbaren externen Pfaden Loopback-Ollama und überschreibbaren externen Pfaden
- 60 Unit-Tests aktuell erfolgreich; PowerShell-Syntax, Ruff-Format und Lint sauber - 62 Unit-Tests aktuell erfolgreich; PowerShell-Syntax, Lockfile, Sync,
Strukturprüfung, Secret-Scan, Gitignore, Ruff-Format und Lint sauber
## Nicht implementiert ## Nicht implementiert
@@ -85,6 +86,5 @@ Living-Mind-Frontend und automatische Ollama-Prozessverwaltung.
Ollama wird bewusst manuell in einem eigenen Terminal gestartet. Antworten werden Ollama wird bewusst manuell in einem eigenen Terminal gestartet. Antworten werden
noch nicht gestreamt; Sitzungen haben noch keine Titel, Suche oder Löschfunktion. noch nicht gestreamt; Sitzungen haben noch keine Titel, Suche oder Löschfunktion.
`qwen3:8b` ist nicht automatisch für die GTX 1050 des späteren Zielhosts geeignet. `qwen3:8b` ist nicht automatisch für die GTX 1050 des späteren Zielhosts geeignet.
Abschließende Resttests, Qualitätsprüfung und Feature-Branch-Push fehlen noch. Nur Abschlussdokumentation und Feature-Branch-Push fehlen. Es wurde kein echter
Es wurde kein echter API-Key angefordert, angezeigt oder konfiguriert und keine API-Key angefordert, angezeigt oder konfiguriert und keine Cloudanfrage ausgeführt.
Cloudanfrage ausgeführt.
+8
View File
@@ -14,6 +14,7 @@ from javis.providers.base import (
InvalidProviderResponseError, InvalidProviderResponseError,
MissingApiKeyError, MissingApiKeyError,
ProviderRateLimitError, ProviderRateLimitError,
ProviderTimeoutError,
) )
from javis.providers.gemini import GeminiProvider from javis.providers.gemini import GeminiProvider
@@ -140,6 +141,13 @@ class GeminiProviderTests(unittest.TestCase):
with self.assertRaises(CloudNetworkError): with self.assertRaises(CloudNetworkError):
provider.chat([ChatMessage("user", "Was ist Python?")]) provider.chat([ChatMessage("user", "Was ist Python?")])
def test_timeout_is_a_distinct_fallback_signal(self) -> None:
request = httpx.Request("POST", "https://example.invalid")
provider, _ = self._provider(httpx.ReadTimeout("timeout", request=request))
with self.assertRaises(ProviderTimeoutError):
provider.chat([ChatMessage("user", "Was ist Python?")])
def test_empty_response_is_rejected(self) -> None: def test_empty_response_is_rejected(self) -> None:
provider, _ = self._provider(_response(" ")) provider, _ = self._provider(_response(" "))
+16
View File
@@ -8,11 +8,13 @@ from javis.core.provider_router import HybridProvider
from javis.memory.usage_store import ProviderEvent, SQLiteUsageStore from javis.memory.usage_store import ProviderEvent, SQLiteUsageStore
from javis.providers.base import ( from javis.providers.base import (
ChatMessage, ChatMessage,
CloudModelUnavailableError,
CloudNetworkError, CloudNetworkError,
InvalidApiKeyError, InvalidApiKeyError,
LocalModelProvider, LocalModelProvider,
MissingApiKeyError, MissingApiKeyError,
ProviderRateLimitError, ProviderRateLimitError,
ProviderTimeoutError,
ProviderUsage, ProviderUsage,
) )
from javis.security.privacy import CloudPolicy, PrivacyRouter from javis.security.privacy import CloudPolicy, PrivacyRouter
@@ -167,6 +169,20 @@ class HybridProviderTests(unittest.TestCase):
self.assertEqual(answer, "Lokal") self.assertEqual(answer, "Lokal")
self.assertTrue(router.last_route.fallback) self.assertTrue(router.last_route.fallback)
def test_timeout_and_missing_model_fall_back(self) -> None:
for error in (
ProviderTimeoutError("timeout"),
CloudModelUnavailableError("missing model"),
):
with self.subTest(error=type(error).__name__):
self.cloud.error = error
router = self._router()
answer = router.chat([ChatMessage("user", "Wie funktioniert Python?")])
self.assertEqual(answer, "Lokal")
self.assertTrue(router.last_route.fallback)
def test_missing_key_falls_back(self) -> None: def test_missing_key_falls_back(self) -> None:
def missing_key() -> _RecordingProvider: def missing_key() -> _RecordingProvider:
raise MissingApiKeyError("missing") raise MissingApiKeyError("missing")