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
+46
View File
@@ -148,6 +148,33 @@ class TestMail:
{"query": "xyzzy_nonexistent_99", "limit": 1, "account": "mcp-test-empty"})
assert "No results" in text
def test_search_finds_old_mail(self):
# Uralte Mail (2009) muss gefunden werden -> vollstaendiger Scan, kein Crash davor
text = tool_call(self.PORT, get_token(self.PORT), "search_mail",
{"query": "URALTMAIL2009", "limit": 5, "account": "mcp-test-mail"})
assert "No results" not in text
assert "2009" in text
def test_search_survives_bad_charset(self):
# Mail mit charset x-unknown darf die Suche NICHT killen und muss findbar sein
text = tool_call(self.PORT, get_token(self.PORT), "search_mail",
{"query": "XUNKNOWNTOKEN", "limit": 5, "account": "mcp-test-mail"})
assert "No results" not in text
assert "Subject:" in text
def test_search_survives_bad_header(self):
# Kaputter encoded-word-Header darf decode_hdr nicht crashen
text = tool_call(self.PORT, get_token(self.PORT), "search_mail",
{"query": "BADHEADERTOKEN", "limit": 5, "account": "mcp-test-mail"})
assert "No results" not in text
def test_search_broad_no_crash(self):
# Breite Suche scannt ueber die Edge-Mails hinweg ohne Fehler
text = tool_call(self.PORT, get_token(self.PORT), "search_mail",
{"query": "mcptest", "limit": 50, "account": "mcp-test-mail"})
assert "Error" not in text
assert text.count("Subject:") >= 2
def test_read_mail(self):
# First search to get a key
text = tool_call(self.PORT, get_token(self.PORT), "search_mail",
@@ -298,6 +325,25 @@ class TestContacts:
assert "Mustermann" in text
assert "UID:" in text
def test_set_and_read_photo(self):
# Foto setzen + zuruecklesen (get_contact liefert dann ImageContent)
token = get_token(self.PORT)
tag = f"PhotoTest{int(time.time())}"
tool_call(self.PORT, token, "create_contact", {"name": f"{tag} Bildmann", "email": f"{tag.lower()}@example.com"})
search = tool_call(self.PORT, token, "search_contacts", {"query": tag, "limit": 1})
uid = [l for l in search.split("\n") if "UID:" in l][0].split("UID:")[1].strip()
# 1x1 PNG
png_b64 = ("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwC"
"AAAAC0lEQVR42mNk+M8AAAMCAYAAAS+ZkAAAAABJRU5ErkJggg==")
res = tool_call(self.PORT, token, "set_contact_photo",
{"uid": uid, "image_base64": png_b64, "image_type": "png"})
assert "esetzt" in res.lower() or "set" in res.lower()
content = mcp_call(self.PORT, token, "tools/call",
{"name": "get_contact", "arguments": {"uid": uid}})["result"]["content"]
imgs = [c for c in content if c.get("type") == "image"]
assert imgs, f"Kein Bild in get_contact: {[c.get('type') for c in content]}"
assert imgs[0].get("mimeType", "").startswith("image/")
# ============================================================
# Files Tests (CRUD on /mcp-tests/)