2 Commits

Author SHA1 Message Date
Dystroyer8 7a14fe87f9 fix: refine privacy routing and provider reporting 2026-07-30 18:34:01 +02:00
Dystroyer8 a6c46f34e4 docs: record feature branch push 2026-07-30 18:13:57 +02:00
10 changed files with 142 additions and 72 deletions
+5
View File
@@ -39,6 +39,9 @@ Alle wesentlichen Projektänderungen werden hier in verständlicher Form dokumen
bleiben ladbar. bleiben ladbar.
- Nicht geheime Providerkonfiguration liegt lokal außerhalb von Git; ein Key wird - Nicht geheime Providerkonfiguration liegt lokal außerhalb von Git; ein Key wird
erst unmittelbar vor einem zulässigen Cloudaufruf geladen. erst unmittelbar vor einem zulässigen Cloudaufruf geladen.
- Allgemeine unpersönliche Technikfragen werden präziser als `allowed`
klassifiziert; persönliche, interne und geheime Inhalte behalten Vorrang.
- Privacy-Ausgabe unterscheidet lokalen Modus von technischem Fallback.
### Security ### Security
@@ -48,3 +51,5 @@ Alle wesentlichen Projektänderungen werden hier in verständlicher Form dokumen
Nicht-Billing deaktiviert; Javis aktiviert niemals Billing. Nicht-Billing deaktiviert; Javis aktiviert niemals Billing.
- Cloudkontext wird lokal klassifiziert und minimiert; 429, Netzwerk-, Timeout-, - Cloudkontext wird lokal klassifiziert und minimiert; 429, Netzwerk-, Timeout-,
Key- und Modellfehler fallen auf Ollama zurück. Key- und Modellfehler fallen auf Ollama zurück.
- Gemini-/Datenschutzablauf wurde am 30.07.2026 zusätzlich von Pascal manuell
erfolgreich abgenommen; kein Keywert erschien in der Ausgabe.
+21 -11
View File
@@ -99,6 +99,11 @@ Rootserver:
- `scripts/start-javis.ps1` verifiziert das Repository, setzt nur - `scripts/start-javis.ps1` verifiziert das Repository, setzt nur
Prozessvariablen, prüft Ollama auf Loopback, startet es bei Bedarf versteckt Prozessvariablen, prüft Ollama auf Loopback, startet es bei Bedarf versteckt
und wartet begrenzt. Pfade sind per Parameter oder `JAVIS_*` überschreibbar. und wartet begrenzt. Pfade sind per Parameter oder `JAVIS_*` überschreibbar.
- Allgemeine unpersönliche Technikfragen zu RAM/SSD, SQLite, HTTP, Git und
ähnlichen Begriffen werden gezielter `allowed`; `never` und `ask` behalten
Vorrang bei Secrets, interner Infrastruktur und persönlichem Kontext.
- `/privacy` trennt Inhaltsklassifikation, tatsächlichen Provider, Unterdrückung
durch Modus `local` und technischen Provider-Fallback.
## Aktuelle Architektur ## Aktuelle Architektur
@@ -171,12 +176,11 @@ 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: 62 bestanden; Timeout und fehlendes Modell fallen nachweislich lokal zurück - Unit-Tests: 66 bestanden; neue Technikklassifikation und Privacy-Ausgabe abgedeckt
- 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, - lokaler CLI-Smoke ohne Secretzugriff: bestanden
Gemini nicht konfiguriert, Schlüssel nicht vorhanden, 0 Cloudanfragen
- Obsidian-Vault unverändert: 1 Markdown-, 11 Gesamtdateien, letzter - Obsidian-Vault unverändert: 1 Markdown-, 11 Gesamtdateien, letzter
Schreibzeitpunkt weiterhin 30.07.2026 15:42:31 Schreibzeitpunkt weiterhin 30.07.2026 15:42:31
- Rootserver und Ubuntu-Laptop wurden nicht kontaktiert - Rootserver und Ubuntu-Laptop wurden nicht kontaktiert
@@ -203,15 +207,21 @@ Abnahmestatus:
- manuell durch Pascal getestet: am 30.07.2026 erfolgreich; CLI-Start, echte - manuell durch Pascal getestet: am 30.07.2026 erfolgreich; CLI-Start, echte
deutsche Antworten, Sitzungskontext, `/sessions`, `/exit`, Neustart, deutsche Antworten, Sitzungskontext, `/sessions`, `/exit`, Neustart,
`/load <UUID>`, vollständiger Verlauf und Fortsetzung `/load <UUID>`, vollständiger Verlauf und Fortsetzung
- Gemini/Datenschutz/Fallback automatisiert durch Codex getestet: ja, nur mit - Gemini/Datenschutz/Fallback automatisiert durch Codex getestet: ja
Test-Doubles und lokalem nachrichtenfreiem CLI-Smoke - Gemini/Datenschutz/Fallback manuell durch Pascal getestet: am 30.07.2026;
- Gemini/Datenschutz/Fallback manuell durch Pascal getestet: noch nicht verdeckte Key-Speicherung ohne Wertausgabe, vollständiger `/status`, echter
- von Pascal für produktive Verwendung freigegeben: noch nicht ausdrücklich erklärt Gemini-Erfolg, Cloudzähler, `ask`-Ablehnung, `never`, `/provider local` und
Startskript erfolgreich
- für Merge freigegeben: ja, nach den dokumentierten kleinen Korrekturen
- für produktive Nutzung freigegeben: nicht separat erklärt; keine Freigabe für
spätere Tools, Obsidian oder autonome Aktionen
## Git-Stand ## Git-Stand
- aktueller Branch: `feat/gemini-privacy-router` - aktueller Branch: `feat/gemini-privacy-router`
- stabiler Branch: `main` - stabiler Branch: `main`
- Feature-Branch ist zu `origin/feat/gemini-privacy-router` gepusht und verfolgt
diesen Remote-Branch; `main` und `origin/main` blieben auf `8a678ae`
- Grundgerüstcommit: `6d04171` - Grundgerüstcommit: `6d04171`
- letzter Commit vor der Handoff-Verdichtung: `0e82748` - letzter Commit vor der Handoff-Verdichtung: `0e82748`
- Handoff-Verdichtung auf `main`: `e6eb995` - Handoff-Verdichtung auf `main`: `e6eb995`
@@ -243,7 +253,7 @@ Abnahmestatus:
- aktuell: portable Ollama-Laufzeit und `qwen3:8b` außerhalb von Git verifiziert - aktuell: portable Ollama-Laufzeit und `qwen3:8b` außerhalb von Git verifiziert
- aktuell: lokaler CLI-Chat und persistente SQLite-Sitzungen vollständig abgenommen - aktuell: lokaler CLI-Chat und persistente SQLite-Sitzungen vollständig abgenommen
- aktuell: Gemini-Free-Hybridrouting, Datenschutz und Nullkostenmodus - aktuell: Gemini-Free-Hybridrouting, Datenschutz und Nullkostenmodus
automatisiert vollständig geprüft; manueller Live-Test steht aus automatisiert und durch Pascal manuell vollständig abgenommen
## Offene Entscheidungen und Fehler ## Offene Entscheidungen und Fehler
@@ -273,9 +283,9 @@ Abnahmestatus:
## Nächster sinnvoller Auftrag ## Nächster sinnvoller Auftrag
Pascal führt anhand `docs/NEXT_SESSION.md` den echten Gemini-, Ask- und Nach dem Merge ein separates, kleines Paket für Chatkomfort und Robustheit
Never-Test mit ausschließlich künstlichen beziehungsweise unpersönlichen Daten planen, beispielsweise Sitzungstitel/Suche oder Streaming. Noch keine
durch. Bis zu seiner Bestätigung nicht nach `main` mergen. Obsidian-Integration, Tools, Sprache oder Serversteuerung beginnen.
Keinen API-Key in Chat, Git, Obsidian, Logs oder Dokumentation übernehmen. Keinen API-Key in Chat, Git, Obsidian, Logs oder Dokumentation übernehmen.
+15 -34
View File
@@ -1,45 +1,26 @@
# Nächste Sitzung # Nächste Sitzung
## Ziel ## Ausgangsstand
Pascal nimmt den Gemini-Free-Provider und die lokalen Fallbacks manuell ab. Der Core-Chat und der Gemini-/Datenschutz-Meilenstein sind automatisiert durch
Bis dahin bleibt `feat/gemini-privacy-router` ungemergt. Codex und am 30.07.2026 manuell durch Pascal abgenommen. Der Featurestand ist zum
Merge freigegeben.
## Vorbereitung durch Pascal Bestätigt sind echter Gemini-Erfolg, verdeckte Key-Speicherung, Nullkostenmodus,
bereinigter Status, Cloudzähler, `ask`- und `never`-Routing, erzwungener lokaler
Modus sowie das Startskript.
1. In Google AI Studio einen neuen, nur für Javis verwendeten Gemini-Auth-Key ## Nächstes sinnvolles Paket
erstellen.
2. Für das zugehörige Google-Projekt kein Billing verbinden beziehungsweise den
externen Billingstatus selbst prüfen.
3. Den Key niemals in Chat, Codex, Git, Obsidian oder einen sichtbaren
Kommandozeilenparameter kopieren.
4. Im Repository ausführen:
```powershell Nur ein kleines Chatkomfort-/Robustheitsthema auswählen, zum Beispiel:
& 'D:\Javis-Tools\uv\uv.exe' run javis configure gemini
& 'D:\Javis-Tools\uv\uv.exe' run javis secrets set gemini
powershell -ExecutionPolicy Bypass -File .\scripts\start-javis.ps1
```
Die erste Einrichtung verlangt zwei lokale Bestätigungen. Der zweite Befehl liest - automatische, lokal erzeugte Sitzungstitel und Sitzungssuche oder
den Key verdeckt ein und speichert ihn im Windows Credential Manager. - Streaming lokaler und Cloudantworten mit sauberem Abbruchverhalten
## Manueller Test Vorher wie immer Branch, Arbeitsbaum, aktuellen Handoff und Tests prüfen.
1. `/status`: Key und Konfiguration `ja`, Nullkostenmodus `aktiv`.
2. Unpersönlich: `Erkläre in zwei Sätzen, was eine SQLite-Transaktion ist.`
3. `/privacy`: `allowed`, Provider `gemini`, kein Fallback.
4. Ask ohne private Details: `Ich habe eine private Alltagssituation. Nenne nur
allgemeine Möglichkeiten.` Bei der Freigabe Enter drücken; Antwort muss lokal
über Ollama kommen.
5. Never mit Kunstbeispiel: `Nur lokal: Der Beispiel-API-Key ist absichtlich
erfunden.` Es darf keine Cloudfreigabe angeboten werden; Antwort bleibt lokal.
6. `/status`: bereinigte Zähler prüfen, danach `/exit`.
Erst wenn Pascal diese Punkte bestätigt, darf über den Merge nach `main`
entschieden werden. Google-Quoten sind projektabhängig und in AI Studio zu prüfen.
## Weiterhin nicht beginnen ## Weiterhin nicht beginnen
Kein Obsidian-Zugriff, SSH-/Serverzugriff, Sprache, Mobile Client, Tools oder Noch keine Obsidian-Integration, Tools, SSH-/Serversteuerung, Sprache, Mobile
Living Mind. Keine echten persönlichen Daten für Cloudtests verwenden. Client oder Living Mind. Der vorhandene Gemini-Key bleibt im Betriebssystem-
Keyring und darf nicht erneut in Chat, Logs oder Dokumentation erscheinen.
+14 -8
View File
@@ -33,8 +33,10 @@ Stand: Gemini-Free-Feature automatisiert geprüft, 30.07.2026.
- Core-Chat automatisiert durch Codex getestet: ja - Core-Chat automatisiert durch Codex getestet: ja
- Core-Chat manuell durch Pascal getestet: ja, am 30.07.2026 - Core-Chat manuell durch Pascal getestet: ja, am 30.07.2026
- Gemini/Datenschutz/Fallback automatisiert durch Codex getestet: ja - Gemini/Datenschutz/Fallback automatisiert durch Codex getestet: ja
- Gemini/Datenschutz/Fallback manuell durch Pascal getestet: noch nicht - Gemini/Datenschutz/Fallback manuell durch Pascal getestet: ja, am 30.07.2026
- von Pascal für produktive Verwendung freigegeben: noch nicht ausdrücklich erklärt - für Merge freigegeben: ja, nach den zwei kleinen Korrekturen
- für produktive Nutzung freigegeben: nicht separat erklärt; keine spätere Tool-,
Obsidian- oder Autonomiefunktion
## Aktuelles Featurepaket ## Aktuelles Featurepaket
@@ -60,9 +62,12 @@ Stand: Gemini-Free-Feature automatisiert geprüft, 30.07.2026.
- `javis configure gemini` mit doppelter ausdrücklicher Bestätigung - `javis configure gemini` mit doppelter ausdrücklicher Bestätigung
- echter Chat auf Hybridprovider verdrahtet; Key wird verzögert geladen - echter Chat auf Hybridprovider verdrahtet; Key wird verzögert geladen
- `/provider`, `/privacy` und `/status` geben keine Inhalte oder Keywerte aus - `/provider`, `/privacy` und `/status` geben keine Inhalte oder Keywerte aus
- allgemeine unpersönliche Technikbegriffe werden gezielter `allowed`, während
persönliche, interne oder geheime technische Inhalte `ask`/`never` bleiben
- `/privacy` trennt lokalen Modus von einem technischen Provider-Fallback
- 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
- 62 Unit-Tests aktuell erfolgreich; PowerShell-Syntax, Lockfile, Sync, - 66 Unit-Tests aktuell erfolgreich; PowerShell-Syntax, Lockfile, Sync,
Strukturprüfung, Secret-Scan, Gitignore, Ruff-Format und Lint sauber Strukturprüfung, Secret-Scan, Gitignore, Ruff-Format und Lint sauber
## Nicht implementiert ## Nicht implementiert
@@ -74,6 +79,7 @@ Living-Mind-Frontend.
- stabiler Branch: `main` - stabiler Branch: `main`
- aktueller Branch: `feat/gemini-privacy-router` - aktueller Branch: `feat/gemini-privacy-router`
- Feature-Branch: zu `origin/feat/gemini-privacy-router` gepusht
- Grundgerüstcommit: `6d04171` - Grundgerüstcommit: `6d04171`
- Basis des Feature-Branches: `e6eb995` - Basis des Feature-Branches: `e6eb995`
- Toolchain: `fa90a5c` - Toolchain: `fa90a5c`
@@ -85,9 +91,9 @@ Living-Mind-Frontend.
## Bekannte Einschränkung ## Bekannte Einschränkung
Ollama wird bewusst manuell in einem eigenen Terminal gestartet. Antworten werden Das Startskript startet Ollama bei Bedarf lokal. Antworten werden noch nicht
noch nicht gestreamt; Sitzungen haben noch keine Titel, Suche oder Löschfunktion. 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.
Der echte Gemini-/Fallback-Test durch Pascal und danach die Mergeentscheidung Der Gemini-Key liegt ausschließlich im Betriebssystem-Keyring; sein Wert wurde
fehlen. Es wurde kein echter API-Key angefordert, angezeigt oder konfiguriert und von Codex weder gelesen noch ausgegeben. Pascal hat den echten Cloud- und
keine Cloudanfrage ausgeführt. Fallbackablauf erfolgreich geprüft.
+30 -10
View File
@@ -26,6 +26,8 @@ class RouteStatus:
provider: str provider: str
privacy_policy: CloudPolicy privacy_policy: CloudPolicy
fallback: bool fallback: bool
cloud_suppressed_by_local_mode: bool
technical_fallback: bool
reason: str reason: str
@@ -94,7 +96,13 @@ class HybridProvider:
) )
if self.mode == "local": if self.mode == "local":
return self._local(messages, decision, "lokaler Modus", fallback=False) return self._local(
messages,
decision,
"lokaler Modus",
fallback=False,
cloud_suppressed_by_local_mode=True,
)
if decision.policy is CloudPolicy.NEVER: if decision.policy is CloudPolicy.NEVER:
return self._local(messages, decision, decision.reason, fallback=True) return self._local(messages, decision, decision.reason, fallback=True)
@@ -148,10 +156,12 @@ class HybridProvider:
) )
) )
self.last_route = RouteStatus( self.last_route = RouteStatus(
cloud_provider.name, provider=cloud_provider.name,
decision.policy, privacy_policy=decision.policy,
False, fallback=False,
"Cloudaufruf erfolgreich", cloud_suppressed_by_local_mode=False,
technical_fallback=False,
reason="Cloudaufruf erfolgreich",
) )
return response return response
@@ -192,7 +202,13 @@ class HybridProvider:
reason = "Gemini nicht verfügbar" reason = "Gemini nicht verfügbar"
if disable_notice: if disable_notice:
reason = "Gemini-Schlüssel abgelehnt; Cloud für diese Sitzung deaktiviert" reason = "Gemini-Schlüssel abgelehnt; Cloud für diese Sitzung deaktiviert"
return self._local(messages, decision, reason, fallback=True) return self._local(
messages,
decision,
reason,
fallback=True,
technical_fallback=True,
)
def _local( def _local(
self, self,
@@ -201,6 +217,8 @@ class HybridProvider:
reason: str, reason: str,
*, *,
fallback: bool, fallback: bool,
cloud_suppressed_by_local_mode: bool = False,
technical_fallback: bool = False,
) -> str: ) -> str:
if fallback: if fallback:
self._notice_callback(f"{reason} lokale Antwort mit Ollama.") self._notice_callback(f"{reason} lokale Antwort mit Ollama.")
@@ -216,10 +234,12 @@ class HybridProvider:
) )
) )
self.last_route = RouteStatus( self.last_route = RouteStatus(
self.local_provider.name, provider=self.local_provider.name,
decision.policy, privacy_policy=decision.policy,
fallback, fallback=fallback,
reason, cloud_suppressed_by_local_mode=cloud_suppressed_by_local_mode,
technical_fallback=technical_fallback,
reason=reason,
) )
return response return response
+4 -2
View File
@@ -227,9 +227,11 @@ def run_chat(
output("Noch keine Datenschutzentscheidung in dieser Sitzung.") output("Noch keine Datenschutzentscheidung in dieser Sitzung.")
else: else:
output( output(
"Letzte Datenschutzentscheidung: " "Datenschutzentscheidung: "
f"{route.privacy_policy.value}; Provider: {route.provider}; " f"{route.privacy_policy.value}; Provider: {route.provider}; "
f"Fallback: {'ja' if route.fallback else 'nein'}" "Cloud durch Modus local unterdrückt: "
f"{'ja' if route.cloud_suppressed_by_local_mode else 'nein'}; "
f"Technischer Fallback: {'ja' if route.technical_fallback else 'nein'}"
) )
continue continue
if entered == "/status": if entered == "/status":
+5 -2
View File
@@ -75,7 +75,8 @@ _ASK_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
), ),
( (
re.compile( re.compile(
r"(?i)\b(intern(?:e[rsn]?|er)? hostname|interne? domain|rootserver|" r"(?i)\b(intern(?:e|er|es|en)?\s+"
r"(?:hostname|server|domain|infrastruktur)|rootserver|"
r"privates? projekt|nicht öffentlich|systeminformation|" r"privates? projekt|nicht öffentlich|systeminformation|"
r"private[rs]? termin)\b" r"private[rs]? termin)\b"
), ),
@@ -90,7 +91,9 @@ _ASK_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
_ALLOWED_PATTERNS: tuple[re.Pattern[str], ...] = ( _ALLOWED_PATTERNS: tuple[re.Pattern[str], ...] = (
re.compile( re.compile(
r"(?i)\b(python|programmier|quellcode|algorithmus|sqlite|linux|windows|" r"(?i)\b(python|programmier|quellcode|algorithmus|sqlite|linux|windows|"
r"öffentliche dokumentation|allgemeine wissensfrage|was ist|wie funktioniert)\b" r"git|ram|arbeitsspeicher|ssd|solid state drive|http|statuscode|"
r"transaktion|öffentliche dokumentation|allgemeine wissensfrage|"
r"was ist|wie funktioniert)\b"
), ),
) )
+10 -2
View File
@@ -70,7 +70,8 @@ class CliTests(unittest.TestCase):
provider.last_route = SimpleNamespace( provider.last_route = SimpleNamespace(
privacy_policy=CloudPolicy.ASK, privacy_policy=CloudPolicy.ASK,
provider="ollama", provider="ollama",
fallback=True, cloud_suppressed_by_local_mode=True,
technical_fallback=False,
) )
service = ChatService(store, provider) service = ChatService(store, provider)
inputs = iter( inputs = iter(
@@ -97,7 +98,14 @@ class CliTests(unittest.TestCase):
self.assertEqual(result, 0) self.assertEqual(result, 0)
self.assertIn("Aktiver Providermodus: auto", output) self.assertIn("Aktiver Providermodus: auto", output)
self.assertIn("Providermodus: local", output) self.assertIn("Providermodus: local", output)
self.assertTrue(any("ask; Provider: ollama; Fallback: ja" in line for line in output)) self.assertTrue(
any(
"Datenschutzentscheidung: ask; Provider: ollama; "
"Cloud durch Modus local unterdrückt: ja; "
"Technischer Fallback: nein" in line
for line in output
)
)
self.assertIn("Gemini-Schlüssel vorhanden: ja", output) self.assertIn("Gemini-Schlüssel vorhanden: ja", output)
def test_gemini_configuration_defaults_to_no(self) -> None: def test_gemini_configuration_defaults_to_no(self) -> None:
+24 -1
View File
@@ -9,11 +9,34 @@ class PrivacyRouterTests(unittest.TestCase):
self.router = PrivacyRouter() self.router = PrivacyRouter()
def test_general_technical_question_is_allowed(self) -> None: def test_general_technical_question_is_allowed(self) -> None:
for question in (
"Erkläre in zwei Sätzen den Unterschied zwischen RAM und SSD.",
"Wie funktioniert eine SQLite-Transaktion?",
"Was bedeutet HTTP 404?",
"Wie funktioniert Git?",
):
with self.subTest(question=question):
self.assertEqual( self.assertEqual(
self.router.classify("Wie funktioniert SQLite in Python?").policy, self.router.classify(question).policy,
CloudPolicy.ALLOWED, CloudPolicy.ALLOWED,
) )
def test_internal_server_question_requires_approval(self) -> None:
decision = self.router.classify(
"Wie konfiguriere ich den internen Server mit Hostname srv-intern-01?"
)
self.assertEqual(decision.policy, CloudPolicy.ASK)
def test_technical_question_with_api_key_is_never(self) -> None:
decision = self.router.classify("Wie nutze ich diesen API-Key in Python: AIza" + "x" * 25)
self.assertEqual(decision.policy, CloudPolicy.NEVER)
def test_personal_technical_situation_requires_approval(self) -> None:
decision = self.router.classify(
"Meine SSD ist voll. Wie verschiebe ich meine persönlichen Dateien?"
)
self.assertEqual(decision.policy, CloudPolicy.ASK)
def test_personal_information_requires_approval(self) -> None: def test_personal_information_requires_approval(self) -> None:
self.assertEqual( self.assertEqual(
self.router.classify("Ich möchte meinen Alltag besser planen.").policy, self.router.classify("Ich möchte meinen Alltag besser planen.").policy,
+12
View File
@@ -253,6 +253,18 @@ class HybridProviderTests(unittest.TestCase):
self.assertEqual(answer, "Lokal") self.assertEqual(answer, "Lokal")
self.assertFalse(self.approvals) self.assertFalse(self.approvals)
self.assertFalse(self.cloud.calls) self.assertFalse(self.cloud.calls)
self.assertFalse(router.last_route.fallback)
self.assertTrue(router.last_route.cloud_suppressed_by_local_mode)
self.assertFalse(router.last_route.technical_fallback)
def test_network_failure_is_reported_as_technical_fallback(self) -> None:
self.cloud.error = CloudNetworkError("offline")
router = self._router()
router.chat([ChatMessage("user", "Wie funktioniert Python?")])
self.assertFalse(router.last_route.cloud_suppressed_by_local_mode)
self.assertTrue(router.last_route.technical_fallback)
if __name__ == "__main__": if __name__ == "__main__":