harden: per-item robustness + deadlines/caps across connectors; layered tests

Mail: _safe_decode + _iter_messages (per-mail try/except) -> eine kaputte Mail
(charset x-unknown) bricht die Suche nicht mehr ab, findet so auch uralte Mails.
Vollscan + Datum-Sort + MAX_SCAN/DEADLINE_S (Selbstheilung, kein 24h-Haenger mehr).

Files: search_files mit SEARCH_DEADLINE_S + SEARCH_MAX_DIRS begrenzt (war
unbegrenzte rekursive PROPFIND -> real 35s/449 Calls).

Calendar/Contacts: PARSE_DEADLINE_S fuer client-seitiges vobject-Parsen;
MAX_RESULTS-Output-Cap in get_events/get_tasks (war 10939 Zeilen bei 6000 Events);
search Top-N + Hinweise.

Tests: in Schichten getrennt -> Smoke (nightly), Edge + Stress (on-demand).
Neu: test_smoke/test_edge/test_stress + conftest-Marker + run_full_tests.sh,
nightly laeuft nur Smoke. Kontakt-Foto-Roundtrip-Test, Mail-Edge-Mails
(x-unknown, kaputter Header, 2009er Alt-Mail).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Lohmaier
2026-06-24 21:17:26 +02:00
co-authored by Claude Opus 4.8
parent 936ebc2f56
commit c9ccb4392b
26 changed files with 719 additions and 65 deletions
+60
View File
@@ -105,3 +105,63 @@ Gemeinsames Modul `pdfutil.py` wird von Files-MCP (`read_file`) UND Mail-MCP
(`read_attachment`) genutzt -> Scan-PDF-Mailanhaenge werden genauso gerendert.
Produktiv-Feature (alle User). Test: `TestFileTypes` scanned.pdf -> `image`.
Runtime-Deps: `pymupdf`, `pytesseract` + System `tesseract-ocr`/`-deu` (s. `requirements-extra.txt`).
## Test-Schichten (2026-06-24)
Drei Schichten, NUR Smoke laeuft naechtlich:
- **Smoke** (`test_smoke.py`, Marker `smoke`): NIGHTLY via `mcp-tests.timer` (05:00) ->
`run_tests.sh` -> `pytest test_smoke.py`. Pro Connector: Server up + OAuth + tools/list +
EIN read-only Call. Schnell (~0.5s), schreibt keine Testdaten. Faengt Totalausfaelle ab.
- **Edge** (`test_edge.py`, Marker `edge`): ON-DEMAND. Boese/ungewoehnliche Eingaben pro
Connector: Unicode/Emoji-Roundtrips, Sonderzeichen, riesige Limits/Bodies, kaputte
Datumsangaben, **Path-Traversal-Block** (Files). Muss graceful sein, nichts leaken/crashen.
- **Stress** (`test_stress.py`, Marker `stress`): ON-DEMAND. 40-60 gleichzeitige Requests
pro Connector + Mischlast ueber alle 5; danach Responsiveness-Check. Faengt den Klassiker
"single-threaded Server haengt unter Last" (genau der Mail-Haenger 2026-06-23).
- **Funktional** (`test_all.py`): CRUD + OAuth + Datei-Typen/-Edge. ON-DEMAND.
Helper (`SERVERS`, `get_token`, `tool_call`, `mcp_call`) liegen in `test_all.py`; smoke/edge/
stress importieren sie. Marker in `conftest.py`.
**Runner:**
- Nightly (Smoke): automatisch via Timer, ODER `run_tests.sh`.
- On-demand (Funktional+Edge+Stress): `run_full_tests.sh` (Log `/var/log/mcp-tests-full.log`),
oder eine Schicht: `run_full_tests.sh -m stress` (bzw. `-m edge`).
- Log `/var/log/mcp-tests.log` muss `stefan:stefan` gehoeren (Service laeuft als User=stefan).
## Mail-Server-Haertung (2026-06-23/24)
`mcp-mail` hing 24h an einem Such-`CallToolRequest` (single-threaded -> Connector tot).
Ursache: eine kaputte Mail (charset `x-unknown` -> `LookupError`) plus fehlende per-Mail-
Fehlerbehandlung. Fix in BEIDEN Servern (`/opt/mcp-servers/mail/server.py` remote,
`/opt/mcp-mail/server.py` lokal fuer Claude Code):
- `_safe_decode` (unbekannte Charsets), `_iter_messages` (per-Mail try/except statt
`md.items()`), per-Treffer try/except -> eine kaputte Mail killt die Suche NICHT mehr
(findet so auch uralte Mails nach der kaputten).
- Vollscan + Datum-Sortierung (neueste zuerst) statt frueher Abbruch bei `limit`.
- `MAX_SCAN=2000` + `DEADLINE_S=120` Wall-Clock -> Server heilt sich selbst, nie wieder
Endlos-Haenger. Volltext-Scan ueber die echten 3.3GB dauert ~60-70s fuer seltene Begriffe;
per `account`/`folder` eingrenzen ist deutlich schneller.
## Connector-Skalierung (Stand 2026-06-24, gegen ECHTE Daten gemessen)
Frage: verkraften die Connector "viel durchwuehlen" wie der Mail-Vollscan?
- **Notes:** unkritisch. Joplin server-seitige FTS (`/search`), paginiert mit Cap (max 50
Seiten), Timeouts. Alle Ops <0.1s auch bei vielen Notizen.
- **Files:** `list_files`/`read_file` ok (1 PROPFIND bzw. 25MB-Cap, Timeouts). ABER
`search_files` war riskant: client-seitige Rekursion = **1 PROPFIND pro Ordner**, kein
Deadline. Real gemessen: Miss-Suche 35s/449 PROPFINDs, wachsend mit dem Baum -> haette
den single-threaded Server blockiert. **Fix:** `SEARCH_DEADLINE_S=20` + `SEARCH_MAX_DIRS=400`
+ Truncation-Hinweis ("Begriff praezisieren / 'path' enger"). Worst-Case jetzt ~20s gekappt.
Langfristig besser: oCIS server-seitige Suche (Graph/REPORT) statt Client-Rekursion.
- **Calendar (Radicale):** `get_events` ist server-seitig zeitgefiltert (gut). ABER bei
~6000 Test-Events gemessen: `search_events` 6-8s (vobject parst ALLE Events im ~15-Mon-
Fenster client-seitig), `get_events` lieferte **10939 Zeilen** (KEIN Result-Limit -> Token-
Explosion). Real schon 5s bei 29 Kalendern. **Fix:** `_parse` mit `PARSE_DEADLINE_S=12` +
`get_events`/`get_tasks` Output-Cap `MAX_RESULTS=300` + Hinweise; `search_events` Top-30 +
Deadline. Danach get_events 1501 statt 10939 Zeilen.
- **Contacts (Radicale):** `search_contacts` parst ALLE vCards client-seitig (real 0.2s, wenige
Kontakte; skaliert linear). **Fix:** `PARSE_DEADLINE_S=12` in `_get_contacts` + Hinweis.
- Rest-Kosten = CalDAV/CardDAV-REPORT-Transfer selbst (kein Server-Result-Limit im Protokoll);
in der Praxis durch Datumsbereich/Begriff eingrenzbar. Langfristig: server-seitige Suche.