667 lines
24 KiB
Python
667 lines
24 KiB
Python
# -*- 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())
|