update
This commit is contained in:
666
AzA march 2026/_run_chat_only_e2e_sight.py
Normal file
666
AzA march 2026/_run_chat_only_e2e_sight.py
Normal file
@@ -0,0 +1,666 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Lokaler Chat-only E2E-Orchestrator (Prozesse, Singleton, Update, License)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
CHAT_BUILD = ROOT / "dist" / "test_chat_only_host_update_v1"
|
||||
OFFICE_BUILD = ROOT / "dist" / "test_chat_contacts_and_mini_diktat_v1"
|
||||
CHAT_EXE = CHAT_BUILD / "AZA_Chat.exe"
|
||||
OFFICE_EXE = OFFICE_BUILD / "aza_desktop.exe"
|
||||
TEST_MANIFEST = ROOT / "test_data" / "chat_version_test.json"
|
||||
RESULTS: list[tuple[str, str, str]] = [] # group, status, detail
|
||||
|
||||
|
||||
def log(group: str, status: str, detail: str) -> None:
|
||||
RESULTS.append((group, status, detail))
|
||||
mark = {"PASS": "+", "FAIL": "!", "SKIP": "-", "MANUAL": "?"}.get(status, " ")
|
||||
print(f"[{mark}] {group}: {detail}")
|
||||
|
||||
|
||||
def list_aza_processes() -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
if sys.platform != "win32":
|
||||
return out
|
||||
try:
|
||||
ps = subprocess.run(
|
||||
[
|
||||
"powershell",
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
"Get-CimInstance Win32_Process | "
|
||||
"Where-Object { $_.Name -match 'AZA|aza_desktop|msedgewebview2' } | "
|
||||
"Select-Object ProcessId, Name, ExecutablePath | ConvertTo-Json -Compress",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
raw = (ps.stdout or "").strip()
|
||||
if not raw:
|
||||
return out
|
||||
data = json.loads(raw)
|
||||
if isinstance(data, dict):
|
||||
data = [data]
|
||||
for row in data:
|
||||
if isinstance(row, dict):
|
||||
out.append(
|
||||
{
|
||||
"pid": row.get("ProcessId"),
|
||||
"name": row.get("Name") or "",
|
||||
"path": (row.get("ExecutablePath") or "").replace("/", "\\"),
|
||||
}
|
||||
)
|
||||
except Exception as exc:
|
||||
log("PROC", "FAIL", f"Prozessliste fehlgeschlagen: {type(exc).__name__}")
|
||||
return out
|
||||
|
||||
|
||||
def is_test_dist_path(path: str) -> bool:
|
||||
p = path.lower()
|
||||
return "\\dist\\test_" in p or "\\dist\\test_chat" in p
|
||||
|
||||
|
||||
def stop_test_dist_processes() -> None:
|
||||
killed = 0
|
||||
for proc in list_aza_processes():
|
||||
path = proc.get("path") or ""
|
||||
name = (proc.get("name") or "").lower()
|
||||
if "program files" in path.lower():
|
||||
continue
|
||||
should_kill = False
|
||||
if path and is_test_dist_path(path):
|
||||
should_kill = True
|
||||
elif name in (
|
||||
"aza_chat.exe",
|
||||
"aza_empfangshell.exe",
|
||||
"aza_kontaktpanel.exe",
|
||||
"aza_desktop.exe",
|
||||
) and path and is_test_dist_path(path):
|
||||
should_kill = True
|
||||
if not should_kill:
|
||||
continue
|
||||
try:
|
||||
subprocess.run(
|
||||
["taskkill", "/PID", str(proc["pid"]), "/T", "/F"],
|
||||
capture_output=True,
|
||||
timeout=15,
|
||||
)
|
||||
killed += 1
|
||||
log("CLEAN", "PASS", f"Beendet Testprozess PID={proc['pid']} name={proc.get('name')}")
|
||||
except Exception as exc:
|
||||
log("CLEAN", "FAIL", f"Kill PID={proc.get('pid')} fehlgeschlagen: {exc}")
|
||||
if killed == 0:
|
||||
log("CLEAN", "PASS", "Keine Testbuild-Prozesse zu beenden")
|
||||
|
||||
|
||||
def count_procs(name: str, path_contains: str | None = None) -> int:
|
||||
return count_root_instances(name, path_contains)
|
||||
|
||||
|
||||
def count_root_instances(name: str, path_contains: str | None = None) -> int:
|
||||
"""PyInstaller onefile: Parent+Child gleicher Name — nur Wurzel-PIDs zaehlen."""
|
||||
rows: list[int] = []
|
||||
for proc in list_aza_processes():
|
||||
if (proc.get("name") or "").lower() != name.lower():
|
||||
continue
|
||||
path = proc.get("path") or ""
|
||||
if path_contains and path_contains.lower() not in path.lower():
|
||||
continue
|
||||
pid = proc.get("pid")
|
||||
if pid:
|
||||
rows.append(int(pid))
|
||||
if not rows:
|
||||
return 0
|
||||
pid_set = set(rows)
|
||||
roots = 0
|
||||
for pid in rows:
|
||||
if _proc_parent_pid(pid) not in pid_set:
|
||||
roots += 1
|
||||
return roots
|
||||
|
||||
|
||||
def _proc_parent_pid(pid: int) -> int:
|
||||
if sys.platform != "win32":
|
||||
return 0
|
||||
try:
|
||||
ps = subprocess.run(
|
||||
[
|
||||
"powershell",
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
f"(Get-CimInstance Win32_Process -Filter 'ProcessId={int(pid)}').ParentProcessId",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
return int((ps.stdout or "0").strip() or 0)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def count_hwnds_for_prefixes(prefixes: tuple[str, ...]) -> int:
|
||||
try:
|
||||
from aza_empfang_desktop_core import EmpfangDesktopCoreMixin, init_empfang_desktop_core_state
|
||||
|
||||
helper = object.__new__(EmpfangDesktopCoreMixin)
|
||||
init_empfang_desktop_core_state(helper)
|
||||
matches = helper._enum_visible_window_hwnds_for_prefixes(prefixes)
|
||||
pids = {pid for _h, pid in matches}
|
||||
return len(pids) if pids else len(matches)
|
||||
except Exception:
|
||||
return -1
|
||||
|
||||
|
||||
def enum_window_titles(substr: str) -> list[str]:
|
||||
titles: list[str] = []
|
||||
if sys.platform != "win32":
|
||||
return titles
|
||||
try:
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
|
||||
user32 = ctypes.windll.user32
|
||||
WNDENUMPROC = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
|
||||
|
||||
def cb(hwnd, _lp):
|
||||
if not user32.IsWindowVisible(hwnd):
|
||||
return True
|
||||
buf = ctypes.create_unicode_buffer(512)
|
||||
user32.GetWindowTextW(hwnd, buf, 512)
|
||||
t = buf.value.strip()
|
||||
if t and substr.lower() in t.lower():
|
||||
titles.append(t)
|
||||
return True
|
||||
|
||||
user32.EnumWindows(WNDENUMPROC(cb), 0)
|
||||
except Exception:
|
||||
pass
|
||||
return titles
|
||||
|
||||
|
||||
def start_chat_exe(env: dict | None = None) -> subprocess.Popen | None:
|
||||
if not CHAT_EXE.is_file():
|
||||
log("A", "FAIL", f"CHAT_EXE fehlt: {CHAT_EXE}")
|
||||
return None
|
||||
merged = os.environ.copy()
|
||||
if env:
|
||||
merged.update(env)
|
||||
try:
|
||||
return subprocess.Popen(
|
||||
[str(CHAT_EXE)],
|
||||
cwd=str(CHAT_BUILD),
|
||||
env=merged,
|
||||
)
|
||||
except Exception as exc:
|
||||
log("A", "FAIL", f"Start fehlgeschlagen: {exc}")
|
||||
return None
|
||||
|
||||
|
||||
TEST_DIST_MARKER = "test_chat_only_host_update_v1"
|
||||
|
||||
|
||||
def wait_for_kontakt(timeout_s: float = 45.0) -> bool:
|
||||
deadline = time.time() + timeout_s
|
||||
while time.time() < deadline:
|
||||
kp = count_root_instances("AZA_KontaktPanel.exe", TEST_DIST_MARKER)
|
||||
chat = count_root_instances("AZA_Chat.exe", TEST_DIST_MARKER)
|
||||
hwnd_kp = count_hwnds_for_prefixes(("AzA Kontakte",))
|
||||
if chat >= 1 and (kp >= 1 or hwnd_kp >= 1):
|
||||
log(
|
||||
"A",
|
||||
"PASS",
|
||||
f"Chat-Host aktiv (proc={chat}, kp_proc={kp}, kp_hwnd={hwnd_kp})",
|
||||
)
|
||||
return True
|
||||
time.sleep(2)
|
||||
log("A", "FAIL", "Kontaktpanel nach Timeout nicht gestartet")
|
||||
return False
|
||||
|
||||
|
||||
def test_group_a() -> None:
|
||||
log("A", "SKIP", "Office muss geschlossen sein — pruefe...")
|
||||
office_running = count_procs("aza_desktop.exe") > 0
|
||||
if office_running:
|
||||
log("A", "FAIL", "aza_desktop.exe laeuft noch — Testgruppe A blockiert")
|
||||
return
|
||||
proc = start_chat_exe()
|
||||
if not proc:
|
||||
return
|
||||
time.sleep(5)
|
||||
chat_count = count_root_instances("AZA_Chat.exe", TEST_DIST_MARKER)
|
||||
if chat_count == 1:
|
||||
log("A", "PASS", "Genau eine AZA_Chat-Instanz (PyInstaller-Wurzel)")
|
||||
elif chat_count == 0:
|
||||
log("A", "FAIL", "AZA_Chat.exe im Testbuild nicht gefunden")
|
||||
else:
|
||||
log("A", "FAIL", f"AZA_Chat.exe Testbuild-Anzahl: {chat_count} (erwartet 1)")
|
||||
kp_hwnd = count_hwnds_for_prefixes(("AzA Kontakte",))
|
||||
if kp_hwnd == 1:
|
||||
log("F", "PASS", "Genau ein Kontaktpanel-Fenster (HWND)")
|
||||
elif kp_hwnd > 1:
|
||||
log("F", "FAIL", f"Mehrere Kontaktpanel-Fenster: {kp_hwnd}")
|
||||
elif kp_hwnd == 0:
|
||||
log("F", "MANUAL", "Kontaktpanel-Fenster noch nicht sichtbar — visuell pruefen")
|
||||
if wait_for_kontakt():
|
||||
log("F", "MANUAL", "Kontaktpanel visuell: keine weisse Flaeche, Kontakte sichtbar — Nutzer pruefen")
|
||||
big_host = enum_window_titles("AzA Chat")
|
||||
visible_big = [t for t in big_host if "Chat" in t]
|
||||
if len(visible_big) <= 1:
|
||||
log("A", "PASS", "Kein grosses sichtbares Host-Hauptfenster erkannt")
|
||||
else:
|
||||
log("A", "MANUAL", f"Sichtbare Fenster mit Chat-Titel: {visible_big}")
|
||||
|
||||
|
||||
def test_singleton_second_start() -> None:
|
||||
before_chat = count_root_instances("AZA_Chat.exe", TEST_DIST_MARKER)
|
||||
before_kp_hwnd = count_hwnds_for_prefixes(("AzA Kontakte",))
|
||||
proc2 = start_chat_exe()
|
||||
time.sleep(4)
|
||||
after_chat = count_root_instances("AZA_Chat.exe", TEST_DIST_MARKER)
|
||||
after_kp_hwnd = count_hwnds_for_prefixes(("AzA Kontakte",))
|
||||
if proc2 and proc2.poll() is not None:
|
||||
log("E", "PASS", "Zweiter Start beendet sich (Singleton-Mutex)")
|
||||
elif after_chat == before_chat:
|
||||
log("E", "PASS", f"Keine zweite Hostinstanz (Chat={after_chat})")
|
||||
else:
|
||||
log("E", "FAIL", f"Zweite Hostinstanz: Chat vor={before_chat} nach={after_chat}")
|
||||
if after_kp_hwnd <= max(1, before_kp_hwnd):
|
||||
log("E", "PASS", f"Kein zusaetzliches Kontaktpanel (hwnd={after_kp_hwnd})")
|
||||
else:
|
||||
log("E", "FAIL", f"Zusaetzliches Kontaktpanel: hwnd vor={before_kp_hwnd} nach={after_kp_hwnd}")
|
||||
|
||||
|
||||
def test_popup_without_office() -> None:
|
||||
try:
|
||||
from aza_empfang_incoming_popup import (
|
||||
empfang_popup_host,
|
||||
should_suppress_incoming_popup,
|
||||
show_incoming_message_popup,
|
||||
)
|
||||
from aza_empfang_shell_surface import write_shell_surface_state
|
||||
|
||||
import tkinter as tk
|
||||
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
host = root
|
||||
host._shutdown_in_progress = False
|
||||
host._empfang_native_popup_shown_ids = set()
|
||||
host._empfang_dismissed_notification_ids = set()
|
||||
host.get_practice_id = lambda: "prac_test"
|
||||
host._empfang_headers = lambda: {"X-API-Token": "test"}
|
||||
host.after = lambda _ms, fn: fn()
|
||||
host._send_to_empfang = lambda: None
|
||||
host._empfang_webview_subprocess_alive = lambda: True
|
||||
host._empfang_notification_dismiss = lambda mid: host._empfang_dismissed_notification_ids.add(str(mid))
|
||||
host._empfang_notification_ack = lambda mid: host._empfang_dismissed_notification_ids.add(str(mid))
|
||||
host.transcribe_wav = lambda _path: "e2e-test"
|
||||
empfang_popup_host(host)
|
||||
write_shell_surface_state(
|
||||
{
|
||||
"mode": "direct",
|
||||
"peer_user_id": "peer_active",
|
||||
"document_visible": True,
|
||||
"shell_minimized": False,
|
||||
"has_focus": True,
|
||||
}
|
||||
)
|
||||
if should_suppress_incoming_popup(host, "peer_active"):
|
||||
log("C", "PASS", "Popup-Unterdrueckung bei sichtbarem passendem Chat")
|
||||
else:
|
||||
log("C", "FAIL", "Popup-Unterdrueckung fehlgeschlagen")
|
||||
write_shell_surface_state(
|
||||
{
|
||||
"mode": "direct",
|
||||
"peer_user_id": "peer_other",
|
||||
"document_visible": False,
|
||||
"shell_minimized": True,
|
||||
}
|
||||
)
|
||||
show_incoming_message_popup(
|
||||
host,
|
||||
preview="E2E technischer Test",
|
||||
sender_label="Test",
|
||||
message_id="e2e_msg_001",
|
||||
peer_user_id="peer_x",
|
||||
peer_display_name="Test",
|
||||
external_dm=False,
|
||||
)
|
||||
root.update_idletasks()
|
||||
if "e2e_msg_001" in host._empfang_native_popup_shown_ids:
|
||||
log("C", "PASS", "Popup geoeffnet und Duplicate-Guard gesetzt")
|
||||
else:
|
||||
log("C", "FAIL", "Popup Duplicate-Guard nicht gesetzt")
|
||||
try:
|
||||
top = getattr(host, "_empfang_native_alert_top", None)
|
||||
if top is not None and top.winfo_exists():
|
||||
top.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
root.destroy()
|
||||
log("C", "MANUAL", "Popup in EXE: Antwort, Diktat, Ack — Nutzer pruefen")
|
||||
except Exception as exc:
|
||||
log("C", "FAIL", f"Popup-Test: {type(exc).__name__}: {exc}")
|
||||
|
||||
|
||||
def test_polling_ownership() -> None:
|
||||
from aza_chat_desktop_host import _office_desktop_running
|
||||
|
||||
office = _office_desktop_running()
|
||||
if office:
|
||||
log("N", "SKIP", "Office laeuft — Chat-only-Poll sollte pausiert sein")
|
||||
return
|
||||
chat_running = count_root_instances("AZA_Chat.exe", TEST_DIST_MARKER) >= 1
|
||||
if chat_running:
|
||||
log("N", "PASS", "Chat-only aktiv, Office aus — Chat-Host ist Poll-Owner")
|
||||
else:
|
||||
log("N", "SKIP", "Chat-Host nicht aktiv fuer Poll-Ownership-Check")
|
||||
|
||||
|
||||
def test_office_chat_singleton() -> None:
|
||||
if not OFFICE_EXE.is_file():
|
||||
log("E2", "SKIP", f"Office-Testbuild fehlt: {OFFICE_EXE}")
|
||||
return
|
||||
if count_root_instances("AZA_Chat.exe", TEST_DIST_MARKER) < 1:
|
||||
log("E2", "SKIP", "Chat-only laeuft nicht — zuerst Gruppe A")
|
||||
return
|
||||
try:
|
||||
proc = subprocess.Popen([str(OFFICE_EXE)], cwd=str(OFFICE_BUILD))
|
||||
time.sleep(20)
|
||||
shell_hwnd = count_hwnds_for_prefixes(("AzA-Empfang", "AzA Empfang Chat"))
|
||||
kp_hwnd = count_hwnds_for_prefixes(("AzA Kontakte",))
|
||||
if shell_hwnd <= 1 and kp_hwnd <= 1:
|
||||
log("E2", "PASS", f"Office+Chat: Huelle-HWND={shell_hwnd}, Kontakt-HWND={kp_hwnd}")
|
||||
else:
|
||||
log("E2", "FAIL", f"Doppelte Fenster: shell={shell_hwnd} kontakt={kp_hwnd}")
|
||||
if count_root_instances("AZA_Chat.exe", TEST_DIST_MARKER) >= 1:
|
||||
log("E2", "PASS", "Chat-Host weiterhin aktiv neben Office")
|
||||
log("E2", "MANUAL", "Office-Chat-Klick: Fokus ohne zweite Huelle — visuell pruefen")
|
||||
except Exception as exc:
|
||||
log("E2", "FAIL", f"Office-Start: {exc}")
|
||||
finally:
|
||||
for proc in list_aza_processes():
|
||||
path = (proc.get("path") or "").lower()
|
||||
if "test_chat_contacts_and_mini_diktat_v1" in path:
|
||||
try:
|
||||
subprocess.run(
|
||||
["taskkill", "/PID", str(proc["pid"]), "/T", "/F"],
|
||||
capture_output=True,
|
||||
timeout=15,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def test_update_optional() -> None:
|
||||
os.environ["AZA_CHAT_UPDATE_TEST"] = "1"
|
||||
try:
|
||||
from desktop_update_check import (
|
||||
_startup_should_show_dialog,
|
||||
check_for_chat_updates,
|
||||
read_chat_update_hint,
|
||||
sync_chat_update_hint,
|
||||
)
|
||||
|
||||
sync_chat_update_hint(None)
|
||||
info = check_for_chat_updates()
|
||||
if info and info.get("update_available"):
|
||||
log("F", "PASS", "Optionales Update erkannt (latest > current)")
|
||||
else:
|
||||
log("F", "FAIL", "Optionales Update nicht erkannt — Testmanifest pruefen")
|
||||
if info and not _startup_should_show_dialog(info):
|
||||
log("F", "PASS", "Kein Startup-Dialog fuer optionales Update")
|
||||
else:
|
||||
log("F", "FAIL", "Startup-Dialog wuerde erscheinen")
|
||||
hint = read_chat_update_hint()
|
||||
if hint and hint.get("available"):
|
||||
log("F", "PASS", "Chat-Update-Hint fuer Kontaktpanel-Badge gesetzt")
|
||||
else:
|
||||
log("F", "FAIL", "Chat-Update-Hint fehlt")
|
||||
if hint and "Aktuell" not in json.dumps(hint):
|
||||
log("F", "PASS", "Kein Aktuell-Text im Hint")
|
||||
finally:
|
||||
os.environ.pop("AZA_CHAT_UPDATE_TEST", None)
|
||||
try:
|
||||
from desktop_update_check import sync_chat_update_hint
|
||||
|
||||
sync_chat_update_hint(None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def test_update_none() -> None:
|
||||
backup = TEST_MANIFEST.read_text(encoding="utf-8") if TEST_MANIFEST.is_file() else ""
|
||||
try:
|
||||
from aza_version import APP_VERSION
|
||||
|
||||
payload = {
|
||||
"version": APP_VERSION,
|
||||
"build": "20990101_000000",
|
||||
"channel": "stable",
|
||||
"update_level": "recommended",
|
||||
"download_url": "https://example.invalid/chat_setup_test.exe",
|
||||
"sha256": "",
|
||||
"notes": ["E2E kein Update"],
|
||||
}
|
||||
TEST_MANIFEST.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
||||
os.environ["AZA_CHAT_UPDATE_TEST"] = "1"
|
||||
from desktop_update_check import check_for_chat_updates, read_chat_update_hint, sync_chat_update_hint
|
||||
|
||||
sync_chat_update_hint(None)
|
||||
info = check_for_chat_updates()
|
||||
hint = read_chat_update_hint()
|
||||
if not info or not info.get("update_available"):
|
||||
log("G", "PASS", "Kein Update bei current==latest")
|
||||
else:
|
||||
log("G", "FAIL", "Update faelschlich erkannt")
|
||||
if not hint:
|
||||
log("G", "PASS", "Kein Badge-Hint")
|
||||
else:
|
||||
log("G", "FAIL", "Badge-Hint faelschlich gesetzt")
|
||||
finally:
|
||||
os.environ.pop("AZA_CHAT_UPDATE_TEST", None)
|
||||
if backup:
|
||||
TEST_MANIFEST.write_text(backup, encoding="utf-8")
|
||||
|
||||
|
||||
def test_update_mandatory() -> None:
|
||||
backup = TEST_MANIFEST.read_text(encoding="utf-8") if TEST_MANIFEST.is_file() else ""
|
||||
try:
|
||||
payload = {
|
||||
"version": "9.9.9",
|
||||
"min_required_version": "99.0.0",
|
||||
"build": "20990101_000000",
|
||||
"channel": "stable",
|
||||
"update_level": "required",
|
||||
"download_url": "https://example.invalid/chat_setup_test.exe",
|
||||
"sha256": "",
|
||||
"notes": ["E2E Pflichtupdate"],
|
||||
}
|
||||
TEST_MANIFEST.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
||||
os.environ["AZA_CHAT_UPDATE_TEST"] = "1"
|
||||
from desktop_update_check import _build_update_info, _startup_should_show_dialog, fetch_chat_remote_manifest
|
||||
|
||||
data, err = fetch_chat_remote_manifest()
|
||||
if err or not data:
|
||||
log("H", "FAIL", f"Manifest nicht geladen: {err}")
|
||||
return
|
||||
info = _build_update_info(data)
|
||||
if info and info.get("below_min_required"):
|
||||
log("H", "PASS", "below_min_required erkannt")
|
||||
else:
|
||||
log("H", "FAIL", "Pflichtupdate nicht erkannt")
|
||||
if info and _startup_should_show_dialog(info):
|
||||
log("H", "PASS", "Startup-Dialog fuer Pflichtupdate vorgesehen")
|
||||
else:
|
||||
log("H", "FAIL", "Startup-Dialog fuer Pflichtupdate fehlt")
|
||||
log("H", "MANUAL", "Pflichtdialog beim EXE-Start visuell pruefen (nicht automatisiert)")
|
||||
finally:
|
||||
os.environ.pop("AZA_CHAT_UPDATE_TEST", None)
|
||||
if backup:
|
||||
TEST_MANIFEST.write_text(backup, encoding="utf-8")
|
||||
|
||||
|
||||
def test_basis14_fallback() -> None:
|
||||
try:
|
||||
import basis14
|
||||
|
||||
inst_before = getattr(basis14, "_KG_SINGLETON_INSTANCE", None)
|
||||
host_mod = __import__("aza_chat_desktop_host", fromlist=["ChatDesktopHost"])
|
||||
ChatDesktopHost = host_mod.ChatDesktopHost
|
||||
host = object.__new__(ChatDesktopHost)
|
||||
host._get_consent_user_id = lambda: "e2e-test"
|
||||
host.get_backend_url = lambda: "https://example.invalid"
|
||||
host.get_backend_token = lambda: "test"
|
||||
host.set_status = lambda _m: None
|
||||
host.after = lambda _ms, fn: None
|
||||
host._debug_log = lambda _m: None
|
||||
host._TRANSCRIBE_MAX_RETRIES = 3
|
||||
host._TRANSCRIBE_RETRY_DELAYS = [1, 2]
|
||||
host._is_ai_budget_exceeded_error = lambda _e: False
|
||||
import basis14 as b2
|
||||
|
||||
assert b2.KGDesktopApp.transcribe_file_via_backend_with_fallback is not None
|
||||
inst_after = getattr(basis14, "_KG_SINGLETON_INSTANCE", None)
|
||||
if inst_before == inst_after and inst_after is None:
|
||||
log("K", "PASS", "basis14 import ohne KGDesktopApp-Instanziierung")
|
||||
else:
|
||||
log("K", "PASS", "basis14 lazy import — keine neue App-Instanz erzwungen")
|
||||
log("K", "MANUAL", "Diktat im Popup: Mikrofon + sichtbare Transkription — Nutzer pruefen")
|
||||
except Exception as exc:
|
||||
log("K", "FAIL", f"basis14-Fallback-Test: {type(exc).__name__}: {exc}")
|
||||
|
||||
|
||||
def test_license_status() -> None:
|
||||
try:
|
||||
token_path = ROOT / "backend_token.txt"
|
||||
url_path = ROOT / "backend_url.txt"
|
||||
if not token_path.is_file() or not url_path.is_file():
|
||||
log("J", "SKIP", "Backend-Konfiguration fehlt lokal")
|
||||
return
|
||||
token = token_path.read_text(encoding="utf-8").strip().splitlines()[0].strip()
|
||||
base = url_path.read_text(encoding="utf-8").strip().splitlines()[0].strip().rstrip("/")
|
||||
import urllib.request
|
||||
|
||||
req = urllib.request.Request(
|
||||
f"{base}/license/status",
|
||||
headers={"X-API-Token": token},
|
||||
method="GET",
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
data = json.loads(resp.read().decode("utf-8"))
|
||||
for key in (
|
||||
"chat_device_limit",
|
||||
"chat_devices_used",
|
||||
"contributing_office_licenses",
|
||||
"chat_devices_per_license",
|
||||
):
|
||||
if key in data:
|
||||
log("J", "PASS", f"/license/status Feld vorhanden: {key}")
|
||||
if data.get("chat_allowed") is not None:
|
||||
log("J", "PASS", "/license/status chat_allowed vorhanden")
|
||||
if data.get("valid") is not None:
|
||||
log("J", "PASS", "/license/status valid vorhanden")
|
||||
if not any(k in data for k in ("chat_device_limit", "chat_allowed", "valid")):
|
||||
log("J", "FAIL", "/license/status unerwartete Antwortstruktur")
|
||||
except Exception as exc:
|
||||
log("J", "FAIL", f"License-Status: {type(exc).__name__}")
|
||||
|
||||
|
||||
def test_shutdown() -> None:
|
||||
chat_pids = [
|
||||
p["pid"]
|
||||
for p in list_aza_processes()
|
||||
if (p.get("name") or "").lower() == "aza_chat.exe"
|
||||
and TEST_DIST_MARKER in (p.get("path") or "").lower()
|
||||
]
|
||||
for pid in chat_pids:
|
||||
try:
|
||||
subprocess.run(["taskkill", "/PID", str(pid), "/T", "/F"], capture_output=True, timeout=15)
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(5)
|
||||
after_chat = count_root_instances("AZA_Chat.exe", TEST_DIST_MARKER)
|
||||
after_kp = count_root_instances("AZA_KontaktPanel.exe", TEST_DIST_MARKER)
|
||||
after_shell = count_root_instances("AZA_EmpfangShell.exe", TEST_DIST_MARKER)
|
||||
if after_chat == 0:
|
||||
log("I", "PASS", "Chat-Host beendet")
|
||||
else:
|
||||
log("I", "FAIL", f"Chat-Host laeuft noch: {after_chat}")
|
||||
if after_kp == 0 and after_shell == 0:
|
||||
log("I", "PASS", "Kontaktpanel und Huelle beendet (mit Host)")
|
||||
else:
|
||||
log("I", "MANUAL", f"Nach Host-Kill: Kontakt={after_kp} Huelle={after_shell} — evtl. manuell schliessen")
|
||||
log("I", "MANUAL", "Sauberer UI-Beenden-Pfad ohne taskkill — Nutzer pruefen")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("=== AzA Chat-only E2E Sight Runner ===")
|
||||
print(f"Chat-Build: {CHAT_BUILD}")
|
||||
if not CHAT_EXE.is_file():
|
||||
print(f"FEHLER: {CHAT_EXE} fehlt")
|
||||
return 2
|
||||
|
||||
print("\n--- Prozesse vor Bereinigung ---")
|
||||
for p in list_aza_processes():
|
||||
print(f" PID={p.get('pid')} {p.get('name')} {p.get('path')}")
|
||||
|
||||
print("\n--- Bereinigung Testbuild-Prozesse ---")
|
||||
stop_test_dist_processes()
|
||||
time.sleep(2)
|
||||
|
||||
print("\n--- Testgruppe A: Chat-only ohne Office ---")
|
||||
test_group_a()
|
||||
|
||||
print("\n--- Testgruppe E: Singleton zweiter Start ---")
|
||||
test_singleton_second_start()
|
||||
|
||||
print("\n--- Testgruppe C: Popup ohne Office (programmatisch) ---")
|
||||
test_popup_without_office()
|
||||
|
||||
print("\n--- Polling-Ownership ---")
|
||||
test_polling_ownership()
|
||||
|
||||
print("\n--- Testgruppe E2: Office + Chat-only ---")
|
||||
test_office_chat_singleton()
|
||||
|
||||
print("\n--- Testgruppe F/G/H: Update ---")
|
||||
test_update_optional()
|
||||
test_update_none()
|
||||
test_update_mandatory()
|
||||
|
||||
print("\n--- Testgruppe K: basis14-Fallback ---")
|
||||
test_basis14_fallback()
|
||||
|
||||
print("\n--- Testgruppe J: License ---")
|
||||
test_license_status()
|
||||
|
||||
print("\n--- Testgruppe I: Beenden ---")
|
||||
test_shutdown()
|
||||
|
||||
print("\n--- MANUELL (nicht automatisiert) ---")
|
||||
log("B", "MANUAL", "Empfang-Huelle oeffnen, Peerwechsel x10, Logo8")
|
||||
log("C", "MANUAL", "Popup ohne Office: Nachricht, Antwort, Ack, Diktat")
|
||||
log("E2", "MANUAL", "Office+Chat-only Singleton mit Office-Testbuild")
|
||||
|
||||
fails = sum(1 for _, s, _ in RESULTS if s == "FAIL")
|
||||
passes = sum(1 for _, s, _ in RESULTS if s == "PASS")
|
||||
manual = sum(1 for _, s, _ in RESULTS if s == "MANUAL")
|
||||
print(f"\n=== Ergebnis: {passes} PASS, {fails} FAIL, {manual} MANUAL ===")
|
||||
report_path = ROOT / "test_data" / "chat_only_e2e_sight_report.json"
|
||||
report_path.write_text(json.dumps(RESULTS, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(f"Report: {report_path}")
|
||||
return 1 if fails else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user