test: verify and document desktop ui
This commit is contained in:
@@ -28,6 +28,10 @@ Alle wesentlichen Projektänderungen werden hier in verständlicher Form dokumen
|
||||
Gedankenfilter, lesbarer Terminalausgabe und sicherem Strg+C-Abbruch.
|
||||
- Automatische lokale Sitzungstitel, `/rename`, nummerierte `/sessions`,
|
||||
`/load <NR|ID>` und begrenzte `/search`-Suche.
|
||||
- PySide6-Desktop-MVP mit Sitzungsleiste, Markdownchat, Qt-Streamingworker,
|
||||
sicherem Abbruch, Datenschutzdialog und bereinigter Statusspalte.
|
||||
- Sicherer Startweg `scripts/start-javis-gui.ps1`, der die vorhandene
|
||||
Loopback-/Prozesslogik des CLI-Starters wiederverwendet.
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ Javis ist ein langfristiges, lokal-first ausgerichtetes Projekt für einen pers
|
||||
## Aktueller Stand
|
||||
|
||||
Der lokale Textchat funktioniert auf Windows mit Ollama und `qwen3:8b`.
|
||||
Ein optionaler Gemini-Free-Provider ist auf einem Feature-Branch vorbereitet;
|
||||
Datenschutzrouter, Nullkostenmodus und lokales Fallback sind automatisiert
|
||||
getestet, aber noch nicht von Pascal mit einem echten Key abgenommen.
|
||||
Gemini-Free-Hybridrouting, Datenschutz, Streaming und Sitzungsnavigation sind
|
||||
implementiert. Zusätzlich steht ein erster PySide6-Desktopclient zur manuellen
|
||||
Abnahme bereit.
|
||||
|
||||
Noch nicht implementiert sind Obsidian-Zugriff, Serverzugriff, Sprache, Mobile
|
||||
Client und Living Mind.
|
||||
@@ -33,6 +33,15 @@ entsprechenden `JAVIS_*`-Umgebungsvariablen möglich.
|
||||
Im Chat zeigt `/help` alle Befehle. Ohne `JAVIS_DATA_DIR` nutzt Javis einen
|
||||
sicheren benutzerspezifischen Plattformstandard.
|
||||
|
||||
Desktopoberfläche:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\start-javis-gui.ps1
|
||||
```
|
||||
|
||||
Alternativ: `uv run javis gui`. Die [Desktop-Anleitung](docs/DESKTOP_UI.md)
|
||||
beschreibt Tastenkürzel, Datenschutzdialog und den manuellen Abnahmetest.
|
||||
|
||||
Gemini bleibt standardmäßig aus. Die spätere lokale Einrichtung erfolgt ohne
|
||||
Key in der Konfigurationsdatei:
|
||||
|
||||
|
||||
+13
-13
@@ -115,6 +115,8 @@ Rootserver:
|
||||
Abbruch, Modus und bereinigte Statusanzeige sind im Desktopclient verdrahtet.
|
||||
- `ask` unterstützt `Einmal erlauben`, `Lokal beantworten` und `Abbrechen`;
|
||||
`never` erreicht weiterhin keinen Cloudfreigabedialog.
|
||||
- Startwege `uv run javis gui` und `scripts/start-javis-gui.ps1` funktionieren;
|
||||
der GUI-Starter verwendet die sichere Loopback-/Prozesslogik des CLI-Starters.
|
||||
|
||||
## Aktuelle Architektur
|
||||
|
||||
@@ -195,8 +197,12 @@ Letzter bestätigter Projektstand:
|
||||
- uv-Lock und `uv sync --dev`: bestanden
|
||||
- Python in `.venv`: 3.12.13
|
||||
- Ruff in `.venv`: 0.16.0
|
||||
- Unit-Tests: 97 bestanden; zusätzlich GUI-Streamingereignisse, atomarer Abbruch
|
||||
und alle drei Ergebnisse des Datenschutzdialogs abgedeckt
|
||||
- Tests: 101 bestanden; zusätzlich Qt-Offscreen-Fenster, Dialogzuordnung und
|
||||
sichere Wiederverwendung des Startskripts abgedeckt
|
||||
- `uv run javis gui` im Offscreen-Modus: erfolgreich, Exitcode 0
|
||||
- echter lokaler GUI-Smoke: erfolgreich; 1 Sitzung, 2 Nachrichten, Provider Ollama
|
||||
- GUI-Startskript mit prozesslokalem ExecutionPolicy-Bypass: erfolgreich
|
||||
- manuelle Desktop-Abnahme durch Pascal: noch offen
|
||||
- PowerShell-Syntax des Startskripts: erfolgreich geparst
|
||||
- Ruff Lint: bestanden
|
||||
- Ruff Formatprüfung: bestanden
|
||||
@@ -243,21 +249,14 @@ Abnahmestatus:
|
||||
|
||||
- aktueller Arbeitsbranch: `feat/desktop-ui`
|
||||
- stabiler Ausgangsstand: `main` bei `d3e85f2`
|
||||
- stabiler Ausgangsstand: `main` bei `77b510b`
|
||||
- medizinischer Datenschutz-Fix: `9148193`
|
||||
- Provider- und CLI-Streaming: `00a5a6c`
|
||||
- SQLite-Titel und Migration: `112ae6d`
|
||||
- GUI-Grundgerüst und Kernadapter: `1658e04`
|
||||
- Sitzungen, Streaming, Abbruch und Datenschutz: `cf35148`
|
||||
- Sitzungsnavigation: `43c1f76`
|
||||
- Dokumentation: `328dcd9`
|
||||
- Chatkomfort-Merge nach `main`: `cbba102`
|
||||
- 91 Unit-Tests, Ruff und Strukturprüfung auf dem gemergten `main` bestanden
|
||||
- Feature-Branch erfolgreich zu `origin/feat/chat-comfort` gepusht
|
||||
- `main` erfolgreich bis `3361eae` zu `origin/main` gepusht und synchron verifiziert
|
||||
- CLI-Verdrahtung: `af9426e`
|
||||
- Startskript: `141ff2e`
|
||||
- Resttests: `a0af15a`
|
||||
- Routing-/Reporting-Korrektur: `7a14fe8`
|
||||
- Gemini-/Datenschutz-Merge nach `main`: `564a3fd`
|
||||
- `origin` verwendet HTTPS
|
||||
- kein Force-Push und keine umgeschriebene Historie
|
||||
|
||||
@@ -289,8 +288,9 @@ Abnahmestatus:
|
||||
|
||||
## Nächster sinnvoller Auftrag
|
||||
|
||||
Auf `feat/desktop-ui` als Nächstes Offscreen-GUI-Tests, sicheres Startskript,
|
||||
Desktopanleitung und anschließend echten lokalen GUI-Smoke ergänzen.
|
||||
Auf `feat/desktop-ui` als Nächstes vollständige Abschlussprüfungen ausführen,
|
||||
Dokumentation committen und Feature-Branch pushen. Danach für Pascals manuelle
|
||||
Desktop-Abnahme stoppen; noch nicht nach `main` mergen.
|
||||
Medizinische Antwortqualität bleibt ein späteres Sicherheits-/Systemprompt-Thema.
|
||||
Noch keine Obsidian-Integration oder Tools beginnen.
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
| ADR-0005 | Laufenden Handoff kompakt halten und Vollstände nur zu Meilensteinen archivieren | angenommen |
|
||||
| ADR-0006 | Ollama und qwen3:8b für den ersten Windows-Chat verwenden | angenommen |
|
||||
| ADR-0007 | Gemini kostenlos bevorzugen, sensible Inhalte lokal halten und niemals Billing aktivieren | angenommen |
|
||||
| UI-0001 | Desktop-MVP verwendet PySide6 und denselben Javis-Kern wie die CLI | angenommen |
|
||||
|
||||
Der normale Runtime-Secret-Speicher ist mit dem Betriebssystem-Keyring festgelegt;
|
||||
keine XML-Secret-Datei. Offen bleiben insbesondere der genaue
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# Javis Desktop
|
||||
|
||||
## Installation und Start
|
||||
|
||||
Die Desktopoberfläche verwendet PySide6 und denselben Python-Kern wie der CLI-Chat.
|
||||
|
||||
```powershell
|
||||
& 'D:\Javis-Tools\uv\uv.exe' sync --dev
|
||||
& 'D:\Javis-Tools\uv\uv.exe' run javis gui
|
||||
```
|
||||
|
||||
Der empfohlene Windows-Start prüft zusätzlich Repository, lokale Werkzeuge und
|
||||
Ollama und setzt ausschließlich Prozessvariablen:
|
||||
|
||||
```powershell
|
||||
.\scripts\start-javis-gui.ps1
|
||||
```
|
||||
|
||||
Falls Windows den direkten Skriptaufruf per Richtlinie blockiert, funktioniert
|
||||
ohne systemweite Änderung der prozesslokale Start:
|
||||
|
||||
```powershell
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\start-javis-gui.ps1
|
||||
```
|
||||
|
||||
Der bisherige Start über `.\scripts\start-javis.ps1` beziehungsweise
|
||||
`uv run javis chat` bleibt unverändert verfügbar.
|
||||
|
||||
## Bedienung
|
||||
|
||||
- `Enter`: Nachricht senden
|
||||
- `Shift+Enter`: neue Zeile
|
||||
- `Ctrl+N`: neue Sitzung
|
||||
- `Ctrl+K`: Sitzungssuche fokussieren
|
||||
- `Esc`: laufende Antwort abbrechen
|
||||
- Doppelklick: Sitzung laden
|
||||
- Kontextmenü einer Sitzung: umbenennen
|
||||
|
||||
Die linke Spalte verwaltet Sitzungen. In der Mitte erscheinen Verlauf,
|
||||
Markdownantwort und Eingabe. Rechts stehen nur bereinigte technische Metadaten:
|
||||
Zustand, Modus, Provider, Ollama-Erreichbarkeit, Modell, Datenschutzentscheidung,
|
||||
Cloudzähler, Fallback und Datenverzeichnis.
|
||||
|
||||
## Provider und Datenschutz
|
||||
|
||||
`Hybrid/Auto` verwendet weiterhin den vorhandenen lokalen Datenschutzrouter.
|
||||
`Lokal` verhindert Cloudnutzung. Bei `ask` stehen drei Entscheidungen bereit:
|
||||
|
||||
- `Einmal erlauben`: minimierten notwendigen Kontext einmalig an Gemini senden
|
||||
- `Lokal beantworten`: ohne Cloud mit Ollama antworten
|
||||
- `Abbrechen`: Anfrage ohne Speicherung beenden
|
||||
|
||||
`never` wird ausschließlich lokal verarbeitet und zeigt keinen Freigabedialog.
|
||||
Der Gemini-Key bleibt im Betriebssystem-Keyring und wird in der Oberfläche weder
|
||||
ausgegeben noch dokumentiert.
|
||||
|
||||
## Streaming und Abbruch
|
||||
|
||||
Die Modellgenerierung läuft in einem Qt-Worker. Es kann nur eine Antwort
|
||||
gleichzeitig laufen. `Stoppen`, `Esc` oder kontrolliertes Schließen fordern den
|
||||
Abbruch an. Ein unvollständiges Benutzer-/Assistentenpaar wird nicht gespeichert.
|
||||
|
||||
## Bekannte Grenzen
|
||||
|
||||
- keine Sprache, kein Mikrofon und kein Wakeword
|
||||
- keine Betriebssystem-, App-, Obsidian-, Server- oder Laptopsteuerung
|
||||
- keine autonomen Aktionen und keine neuen Cloudanbieter
|
||||
- Schließen kann kurz warten, bis der lokale Netzwerkstream kontrolliert reagiert
|
||||
- medizinische Antwortqualität ist ein späteres Sicherheits-/Systemprompt-Thema
|
||||
|
||||
## Manueller Abnahmetest für Pascal
|
||||
|
||||
1. `.\scripts\start-javis-gui.ps1`
|
||||
2. Prüfen, dass sich das Fenster öffnet.
|
||||
3. Eine lokale Frage stellen.
|
||||
4. Die gestreamte Antwort prüfen.
|
||||
5. Eine neue Sitzung anlegen.
|
||||
6. Die alte Sitzung laden.
|
||||
7. Eine Antwort mit `Esc` oder `Stoppen` abbrechen.
|
||||
8. Den Modus `Lokal` testen.
|
||||
9. Eine öffentliche Technikfrage im Hybridmodus testen.
|
||||
10. Eine `ask`-Anfrage lokal beantworten lassen.
|
||||
11. Anwendung schließen und neu starten.
|
||||
12. Die gespeicherte Sitzung erneut laden.
|
||||
|
||||
Abnahmestatus: automatisierte Tests und echter lokaler GUI-Smoke durch Codex
|
||||
bestanden; manuelle Abnahme durch Pascal steht noch aus.
|
||||
@@ -10,14 +10,14 @@ Chatkomfort umfasst sicheres Ollama-/Gemini-Streaming, Strg+C ohne Teilpersisten
|
||||
medizinisches `never`, automatische und manuelle Sitzungstitel, nummerierte
|
||||
Listen, Laden per Nummer/ID-Präfix und begrenzte Suche in Titeln/Benutzertexten.
|
||||
|
||||
91 Unit-Tests, Ruff, Struktur-, Secret-, Gitignore- und Auditprüfungen sowie
|
||||
echte Ollama- und Gemini-Streaming-Smokes sind erfolgreich.
|
||||
Das Desktop-MVP auf `feat/desktop-ui` verwendet denselben Kern wie die CLI.
|
||||
101 Tests, Qt-Offscreen-Start und echter lokaler Ollama-GUI-Smoke sind erfolgreich.
|
||||
Die manuelle Abnahme durch Pascal steht noch aus.
|
||||
|
||||
## Nächster sinnvoller Schritt
|
||||
|
||||
Der Chatkomfort-Branch ist nach `main` gemergt und dort erneut geprüft. Den
|
||||
finalen Stand aus `docs/CHATGPT_HANDOFF.md` übernehmen und mit Pascal ein
|
||||
einzelnes nächstes Paket festlegen; noch keine Folgephase automatisch beginnen.
|
||||
Nur den Desktop-MVP nach `docs/DESKTOP_UI.md` manuell abnehmen. Erst danach mit
|
||||
Pascal ein einzelnes nächstes Paket festlegen; keine Folgephase automatisch beginnen.
|
||||
|
||||
## Weiterhin nicht beginnen
|
||||
|
||||
|
||||
+18
-3
@@ -1,6 +1,6 @@
|
||||
# Projektstatus
|
||||
|
||||
Stand: Chatkomfort-Feature vollständig automatisiert geprüft, 30.07.2026.
|
||||
Stand: Desktop-MVP automatisiert und mit lokalem Modell geprüft, 30.07.2026.
|
||||
|
||||
## Erreicht
|
||||
|
||||
@@ -28,7 +28,7 @@ Stand: Chatkomfort-Feature vollständig automatisiert geprüft, 30.07.2026.
|
||||
Inhalte werden nicht wörtlich in automatische Titel übernommen
|
||||
- echte CLI-Modellantwort sowie Laden und Fortsetzen derselben Sitzung in einem
|
||||
neuen Prozess bestätigt
|
||||
- 91 Unit-Tests, Ruff, Struktur-, Gitignore-, Secret- und Auditprüfung erfolgreich
|
||||
- 101 Tests, Ruff, Struktur-, Gitignore-, Secret- und Auditprüfung erfolgreich
|
||||
- lokaler Core-Chat am 30.07.2026 zusätzlich von Pascal manuell erfolgreich
|
||||
geprüft: deutsche Antworten, Sitzungskontext, `/sessions`, `/exit`, Neustart,
|
||||
`/load <UUID>`, vollständiger Verlauf und Fortsetzung
|
||||
@@ -96,9 +96,24 @@ Stand: Chatkomfort-Feature vollständig automatisiert geprüft, 30.07.2026.
|
||||
Obsidian-Adapter, Tools, Serverzugriff, Sprache, Mobile Client,
|
||||
Living-Mind-Frontend.
|
||||
|
||||
## Desktop-MVP
|
||||
|
||||
- PySide6-Client verwendet Runtime, Chatservice, Hybridprovider, Datenschutz,
|
||||
SQLite, Audit und Secret-Provider des bestehenden Kerns
|
||||
- Sitzungen, Suche, Umbenennen, Markdown, Streaming, Abbruch, Modus,
|
||||
Dreifach-Freigabedialog und bereinigter Status implementiert
|
||||
- `uv run javis gui` und `scripts/start-javis-gui.ps1` vorhanden
|
||||
- Qt-Offscreen-Start und echter lokaler Ollama-GUI-Smoke bestanden
|
||||
- automatisiert durch Codex getestet: ja
|
||||
- echter lokaler GUI-Smoke durch Codex: ja; 1 Sitzung, 2 Nachrichten, Ollama
|
||||
- manuell durch Pascal abgenommen: noch nicht
|
||||
|
||||
## Git
|
||||
|
||||
- aktueller Branch: `main`
|
||||
- aktueller Branch: `feat/desktop-ui`
|
||||
- Desktop-Basis: `main` bei `d3e85f2`
|
||||
- GUI-Grundgerüst: `1658e04`
|
||||
- Sitzungen/Streaming/Datenschutz: `cf35148`
|
||||
- Basis: `main` bei `77b510b`
|
||||
- medizinisches Routing: `9148193`
|
||||
- Streaming: `00a5a6c`
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$UvPath,
|
||||
[string]$OllamaPath,
|
||||
[string]$OllamaModels,
|
||||
[string]$DataDir,
|
||||
[string]$UvCacheDir,
|
||||
[string]$PythonInstallDir
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$env:JAVIS_START_INTERFACE = 'gui'
|
||||
|
||||
$sharedStarter = Join-Path $PSScriptRoot 'start-javis.ps1'
|
||||
if (-not (Test-Path -LiteralPath $sharedStarter -PathType Leaf)) {
|
||||
throw "Das gemeinsame sichere Javis-Startskript wurde nicht gefunden."
|
||||
}
|
||||
|
||||
& $sharedStarter @PSBoundParameters
|
||||
exit $LASTEXITCODE
|
||||
@@ -113,7 +113,12 @@ if (-not (Test-OllamaReady)) {
|
||||
|
||||
Push-Location $repositoryRoot
|
||||
try {
|
||||
& $resolvedUv run javis chat
|
||||
if ($env:JAVIS_START_INTERFACE -eq 'gui') {
|
||||
& $resolvedUv run javis gui
|
||||
}
|
||||
else {
|
||||
& $resolvedUv run javis chat
|
||||
}
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
finally {
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from collections.abc import Sequence
|
||||
|
||||
from PySide6.QtCore import QTimer
|
||||
from PySide6.QtWidgets import QApplication, QMessageBox
|
||||
|
||||
from javis.config.settings import ConfigurationError, Settings
|
||||
@@ -32,6 +34,13 @@ def run_gui(argv: Sequence[str] | None = None) -> int:
|
||||
return 2
|
||||
window = MainWindow(DesktopController(runtime), approval_bridge)
|
||||
window.show()
|
||||
smoke_exit = os.environ.get("JAVIS_GUI_SMOKE_EXIT_MS")
|
||||
if smoke_exit:
|
||||
try:
|
||||
delay = max(1, min(int(smoke_exit), 10_000))
|
||||
except ValueError:
|
||||
delay = 500
|
||||
QTimer.singleShot(delay, application.quit)
|
||||
return application.exec()
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from collections.abc import Iterator
|
||||
|
||||
from javis.core.runtime import JavisRuntime
|
||||
from javis.memory.sqlite_store import ChatSession
|
||||
from javis.providers.base import ProviderError
|
||||
from javis.providers.base import ProviderError, ResponseAbortedError
|
||||
from javis.ui.models import (
|
||||
DesktopStatus,
|
||||
GenerationState,
|
||||
@@ -87,6 +87,10 @@ class DesktopController:
|
||||
self.state = GenerationState.ANSWERING
|
||||
yield StreamEvent(StreamEventKind.STATE, state=self.state)
|
||||
yield StreamEvent(StreamEventKind.CHUNK, text=chunk)
|
||||
except ResponseAbortedError:
|
||||
self.state = GenerationState.ABORTED
|
||||
yield StreamEvent(StreamEventKind.STATE, state=self.state)
|
||||
return
|
||||
except ProviderError as exc:
|
||||
if cancel_event.is_set():
|
||||
self.state = GenerationState.ABORTED
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from html import escape
|
||||
|
||||
from PySide6.QtCore import Qt, QThread, QTimer, Signal
|
||||
from PySide6.QtCore import QSignalBlocker, Qt, QThread, QTimer, Signal
|
||||
from PySide6.QtGui import QAction, QCloseEvent, QKeySequence, QShortcut
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox,
|
||||
@@ -375,6 +375,12 @@ class MainWindow(QMainWindow):
|
||||
self._show_error(str(exc))
|
||||
return
|
||||
self._set_state(status.state)
|
||||
target_mode = "local" if status.mode == "Lokal" else "auto"
|
||||
mode_index = self.mode_combo.findData(target_mode)
|
||||
if mode_index >= 0 and mode_index != self.mode_combo.currentIndex():
|
||||
blocker = QSignalBlocker(self.mode_combo)
|
||||
self.mode_combo.setCurrentIndex(mode_index)
|
||||
del blocker
|
||||
self.status_labels["provider"].setText(status.provider)
|
||||
self.status_labels["ollama"].setText("Ja" if status.ollama_reachable else "Nein")
|
||||
self.status_labels["model"].setText(status.local_model)
|
||||
|
||||
@@ -8,7 +8,7 @@ from types import SimpleNamespace
|
||||
|
||||
from javis.core.chat_service import ChatService
|
||||
from javis.memory.sqlite_store import SQLiteSessionStore
|
||||
from javis.providers.base import ChatMessage
|
||||
from javis.providers.base import ChatMessage, ResponseAbortedError
|
||||
from javis.ui.chat_controller import DesktopController, sanitized_error
|
||||
from javis.ui.models import GenerationState, StreamEventKind
|
||||
|
||||
@@ -64,6 +64,12 @@ class _StreamingProvider:
|
||||
self.mode = mode
|
||||
|
||||
|
||||
class _AbortingProvider(_StreamingProvider):
|
||||
def stream_chat(self, _messages):
|
||||
raise ResponseAbortedError("Vom Dialog abgebrochen")
|
||||
yield
|
||||
|
||||
|
||||
class DesktopControllerTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.runtime = SimpleNamespace(
|
||||
@@ -131,3 +137,19 @@ class DesktopControllerTests(unittest.TestCase):
|
||||
self.assertEqual(events[-1].state, GenerationState.ABORTED)
|
||||
session = service.list_sessions()[0]
|
||||
self.assertEqual(service.load_session(session.id).messages, [])
|
||||
|
||||
def test_provider_abort_is_reported_as_aborted_not_error(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
provider = _AbortingProvider()
|
||||
service = ChatService(
|
||||
SQLiteSessionStore(Path(directory) / "sessions.sqlite3"),
|
||||
provider,
|
||||
)
|
||||
controller = DesktopController(
|
||||
SimpleNamespace(service=service, hybrid_provider=provider)
|
||||
)
|
||||
|
||||
events = list(controller.stream_message("Hallo", threading.Event()))
|
||||
|
||||
self.assertEqual(events[-1].state, GenerationState.ABORTED)
|
||||
self.assertNotIn(StreamEventKind.ERROR, [event.kind for event in events])
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from javis.core.provider_router import ApprovalChoice
|
||||
from javis.providers.base import ChatMessage
|
||||
from javis.security.privacy import CloudPolicy, PrivacyDecision
|
||||
from javis.ui.main_window import MainWindow
|
||||
from javis.ui.models import DesktopStatus, GenerationState
|
||||
from javis.ui.workers import ApprovalBridge
|
||||
|
||||
|
||||
class _Controller:
|
||||
def __init__(self) -> None:
|
||||
self.active_session_id = None
|
||||
self.state = GenerationState.READY
|
||||
self.session = SimpleNamespace(
|
||||
id="session-one",
|
||||
title="Desktop-Test",
|
||||
last_provider="ollama",
|
||||
message_count=2,
|
||||
updated_at="2026-07-30T20:00:00+00:00",
|
||||
)
|
||||
|
||||
def list_sessions(self, search: str = ""):
|
||||
return [self.session] if not search or search in self.session.title else []
|
||||
|
||||
def load_session(self, session_id: str):
|
||||
self.active_session_id = session_id
|
||||
return SimpleNamespace(
|
||||
session=self.session,
|
||||
messages=[
|
||||
ChatMessage("user", "Hallo"),
|
||||
ChatMessage("assistant", "Guten Tag"),
|
||||
],
|
||||
)
|
||||
|
||||
def new_session(self):
|
||||
self.active_session_id = self.session.id
|
||||
return self.session
|
||||
|
||||
def rename_active_session(self, title: str):
|
||||
self.session.title = title
|
||||
return self.session
|
||||
|
||||
def set_mode(self, _mode: str) -> None:
|
||||
return
|
||||
|
||||
def status(self) -> DesktopStatus:
|
||||
return DesktopStatus(
|
||||
state=self.state,
|
||||
mode="Lokal",
|
||||
provider="ollama",
|
||||
ollama_reachable=True,
|
||||
local_model="test-model",
|
||||
privacy_policy="never",
|
||||
cloud_requests_today=0,
|
||||
local_fallback=False,
|
||||
data_dir="D:\\Testdaten",
|
||||
)
|
||||
|
||||
|
||||
class _MessageBox:
|
||||
Question = object()
|
||||
AcceptRole = object()
|
||||
RejectRole = object()
|
||||
DestructiveRole = object()
|
||||
selected_label = ""
|
||||
|
||||
def __init__(self, _parent) -> None:
|
||||
self.buttons: dict[str, object] = {}
|
||||
|
||||
def setWindowTitle(self, _text: str) -> None:
|
||||
return
|
||||
|
||||
def setIcon(self, _icon: object) -> None:
|
||||
return
|
||||
|
||||
def setText(self, _text: str) -> None:
|
||||
return
|
||||
|
||||
def setInformativeText(self, _text: str) -> None:
|
||||
return
|
||||
|
||||
def addButton(self, label: str, _role: object) -> object:
|
||||
button = object()
|
||||
self.buttons[label] = button
|
||||
return button
|
||||
|
||||
def exec(self) -> None:
|
||||
return
|
||||
|
||||
def clickedButton(self) -> object | None:
|
||||
return self.buttons.get(type(self).selected_label)
|
||||
|
||||
|
||||
class DesktopGuiTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.application = QApplication.instance() or QApplication([])
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.bridge = ApprovalBridge()
|
||||
self.window = MainWindow(_Controller(), self.bridge)
|
||||
self.window.show()
|
||||
self.application.processEvents()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.window.close()
|
||||
self.application.processEvents()
|
||||
|
||||
def test_offscreen_window_loads_session_and_core_widgets(self) -> None:
|
||||
self.assertEqual(self.window.windowTitle(), "Javis Desktop")
|
||||
self.assertEqual(self.window.session_list.count(), 1)
|
||||
self.assertIn("Hallo", self.window.chat_view.toPlainText())
|
||||
self.assertTrue(self.window.send_button.isEnabled())
|
||||
self.assertFalse(self.window.stop_button.isEnabled())
|
||||
self.assertEqual(self.window.status_labels["provider"].text(), "ollama")
|
||||
|
||||
def test_approval_dialog_maps_all_three_choices(self) -> None:
|
||||
decision = PrivacyDecision(CloudPolicy.ASK, "persönliche Information")
|
||||
self.bridge.resolve = Mock()
|
||||
expected = {
|
||||
"Einmal erlauben": ApprovalChoice.ALLOW,
|
||||
"Lokal beantworten": ApprovalChoice.LOCAL,
|
||||
"Abbrechen": ApprovalChoice.CANCEL,
|
||||
}
|
||||
|
||||
with patch("javis.ui.main_window.QMessageBox", _MessageBox):
|
||||
for label, choice in expected.items():
|
||||
with self.subTest(label=label):
|
||||
_MessageBox.selected_label = label
|
||||
self.window._show_approval(decision)
|
||||
self.bridge.resolve.assert_called_with(choice)
|
||||
@@ -25,6 +25,24 @@ class StartScriptTests(unittest.TestCase):
|
||||
):
|
||||
self.assertNotIn(forbidden, normalized)
|
||||
|
||||
def test_gui_script_reuses_safe_starter_without_system_changes(self) -> None:
|
||||
repository_root = Path(__file__).resolve().parents[2]
|
||||
script = (repository_root / "scripts" / "start-javis-gui.ps1").read_text(encoding="utf-8")
|
||||
normalized = script.lower()
|
||||
|
||||
self.assertIn("start-javis.ps1", normalized)
|
||||
self.assertIn("javis_start_interface", normalized)
|
||||
self.assertIn("'gui'", normalized)
|
||||
self.assertNotIn("c:\\users\\", normalized)
|
||||
for forbidden in (
|
||||
"set-executionpolicy",
|
||||
"setx ",
|
||||
"new-service",
|
||||
"schtasks",
|
||||
"hkey_",
|
||||
):
|
||||
self.assertNotIn(forbidden, normalized)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user