Sammelcommit fuer den ueber mehrere Sitzungen gewachsenen, bisher nicht
eingecheckten Stand. Getrennte Commits waeren ehrlicher gewesen, sind es
nachtraeglich aber nicht mehr: BugRepo, changelog.json und die Doku tragen
Aenderungen aus allen Straengen gleichzeitig, und keine Zwischenstufe waere
lauffaehig.
ROADMAP IM DEPLOYMENTCENTER (2.7.0)
Planungsstand, Reihenfolge und Zustaendigkeit lagen in handgepflegten
Markdown-Dateien neben einem Bugtracker, der genau diese drei Dinge bereits
fuehrt. Zwei Pfleger fuer dasselbe Feld - Drift war eine Frage der Zeit.
Ab jetzt: Zustand im DC, Begruendung in der Datei. Kein Feld hat zwei Pfleger.
- Migration 016: epic, roadmap_rank, target_build, defer_trigger,
doc_anchor, Status "deferred", Tabelle roadmap_epics.
KEINE Roadmap-Tabelle - ein Eintrag ist ein bugtracker_item.
- Zugehoerigkeit ist ein Feld: Ein Item steht auf der Roadmap, wenn es ein
epic hat. Ein zweites Kennzeichen daneben koennte auseinanderlaufen.
- Kennung ist die Item-ID. Handvergebene Kuerzel ("A12") erzeugen die
schlimmste Fehlerart: Beide Seiten stimmig, Verschiedenes gemeint, weil
die Kennung nach einer Loeschung neu vergeben wurde.
- Zurueckstellen verlangt defer_trigger. Ohne benannten Anlass ist ein
Vorhaben nicht zurueckgestellt, sondern vergessen. Die Regel sitzt im
Repository, damit sie auf allen drei Schreibwegen gleich greift.
- Modules/Bugtracker/Roadmap.php, /api/bugtracker/v1/roadmap.php,
WebUI-Modul mit Filtern, cli/roadmap.php --write/--check,
dc_roadmap und dc_roadmap_set im MCP-Adapter.
- cli/roadmap.php --write ERZEUGT den Zustandsteil der Markdown-Datei;
--check prueft nur die Prosa. Was erzeugt wird, kann nicht driften.
- docs/ROADMAP_MIGRATION.md: Uebernahme fuer die anderen Projekte.
TESTINSTANZ UND TESTTOR
- dc-dev.mhdf.de: zweite Installation, eigene DB, eigene Schluessel,
Basic-Auth davor, Rocket.Chat aus.
- 124 Testfaelle in tests/e2e gegen API, Rechte, Migrationen, Sperre.
- scripts/deploy.py bringt vor jedem Upload erst den anstehenden Stand auf
die Testinstanz und laesst dann den Testlauf laufen. Erst bei
Rueckgabewert 0 geht etwas auf die Produktion (--ohne-test als Notausgang).
Andersherum prueft der Lauf aelteren Code als den, der gleich hochgeht.
- tests/ ist aus dem Produktiv-Deploy ausgeschlossen; testctl.php kann
nicht versehentlich auf dc.mhdf.de landen.
- Ein Lauf mit mehr uebersprungenen als geprueften Faellen endet mit
Rueckgabewert 2 statt 0. Nichts ist kaputt, geprueft wurde es aber auch
nicht - der Unterschied darf nicht am Rueckgabewert verschwinden.
SUPERVISOR, PROJEKTLANDSCHAFT, NACHPRUEFUNG (2.6.0/2.6.1)
- Migration 013: projektuebergreifend melden (dev_agent, evidence,
triage_external, affects).
- Migration 014: supervisor_findings, Katalog, fuenf serverseitige
Detektoren im Tick.
- Migration 015: Nachpruefung als Arbeitsschlange statt Benachrichtigung.
- DEV-Lizenzschluessel zur Selbstbedienung, dc-testkit fuer die Test-VM.
- MCP-Adapter: 15 Werkzeuge, ohne Fremdabhaengigkeiten, mit Abgrenzung
von Fremddaten gegen Prompt-Injektion.
BEHOBEN
- observe und status im Supervisor legten den Zustand des Befundes als
"status" in die Antwort. Http::ok() merged ['status' => 'success']
darunter, also gewann der Befundzustand - eine angenommene Meldung kam
als status="open" an. Heisst jetzt finding_status. War Item #59.
Migrationen 013-016 sind auf dem Produktivserver noch NICHT ausgefuehrt.
Ohne sie bricht das WebUI, weil die Roadmap-Ansicht bei jedem Seitenaufruf
gegen bugtracker_items.epic abfragt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
12 KiB
Testinstanz und automatisierter Testlauf
Gilt für: https://dc-dev.mhdf.de — eine vollständige zweite Installation des
Deploymentcenters, die jederzeit gelöscht und neu aufgebaut werden darf.
Bewusst nicht unter
public/. Diese Datei nennt interne Adressen und den Weg, die Datenbank der Testinstanz zu leeren./docs/auf dem Deploymentcenter ist öffentlich erreichbar; dort gehört das nicht hin. Zugangsdaten stehen ausschließlich inServerdaten.txtundscripts/deploy_dev_config.json— beide sind per.gitignoreausgeschlossen und werden nie hochgeladen.
1. Wozu es sie gibt — und wozu nicht
Testinstanz dc-dev.mhdf.de |
Test-VM (siehe TESTUMGEBUNG.md) | |
|---|---|---|
| Prüft | den Server: API, Rechte, Migrationen, Zugangssperre | den Client: Installer, Updateagent, Oberfläche |
| Datenbestand | wird bei jedem Lauf gelöscht | bleibt |
| Zugriff | HTTPS + Basic-Auth | SSH |
Beides zusammen deckt die Kette ab. Getrennt zu halten ist Absicht: Ein Testlauf, der Tabellen löscht, darf nicht auf derselben Maschine laufen wie ein Test, der eine Installation über Stunden beobachtet.
Was die Testinstanz nicht prüft: die Produktion. Sie hat eigene Schlüssel, eine eigene
Datenbank und Rocket.Chat ist abgeschaltet (rocketchat.enabled = false). Ein Testlauf,
der echte Kanäle vollschreibt, wäre der Fehler, der am ehesten unbemerkt teuer wird.
2. Aufbau
dc-dev.mhdf.de Subdomain, Docroot = /deploymentcenter_dev/ auf dem FTP
├── .htaccess Sperre + Routen (erzeugt, siehe §5)
├── config/
│ ├── .htpasswd erzeugt aus scripts/deploy_dev_config.json
│ ├── config.php erzeugt aus scripts/dev/config.dev.php.tmpl
│ └── release-signing.pem auf der Instanz erzeugt (testctl genkey)
├── public/ src/ sql/ var/ cli/ wie in der Produktion
└── testctl.php nur hier — Steuerung des Testlaufs (§6)
Im Repository:
| Datei | Zweck |
|---|---|
scripts/deploy_dev.py |
Deployment auf die Testinstanz |
scripts/deploy_dev_config.json |
Zugangsdaten (nicht im Git, Vorlage: …example.json) |
scripts/dev/htaccess-prelude.conf |
der Sperrblock, der der .htaccess vorangestellt wird |
scripts/dev/config.dev.php.tmpl |
Vorlage der config.php der Testinstanz |
tests/server/testctl.php |
wird nach /testctl.php hochgeladen — nur von deploy_dev.py |
tests/e2e/ |
der Testlauf |
scripts/deploy.py (Produktion) schließt tests/ vollständig aus. testctl.php kann nicht
versehentlich auf dc.mhdf.de landen.
3. Deployment
python scripts/deploy_dev.py --dry-run
python scripts/deploy_dev.py
Überträgt nur geänderte Dateien (eigener Cache: scripts/.deploy_dev_cache.json).
--full ignoriert den Cache.
Drei Dinge macht das Skript anders als das Produktiv-Deployment:
- Die
.htaccesswird erzeugt — der Sperrblock ausscripts/dev/htaccess-prelude.confplus die.htaccessdes Repositories. Eine zweite gepflegte Kopie der Routen gibt es damit nicht; Änderungen an den Routen wirken auf beiden Instanzen. - Die
config.phpwird erzeugt — mit der Testdatenbank, eigenen Schlüsseln,debug = trueund abgeschaltetem Rocket.Chat. Die Produktivkonfiguration wird nie übertragen. testctl.phpkommt dazu.
Das Skript bricht ab, wenn die Zugangsdaten auf den Produktivserver zeigen oder base_url
kein dc-dev enthält.
Das Testtor vor der Produktion
scripts/deploy.py (Produktion) führt seit dem 29.08.2026 vor jedem Upload aus:
scripts/deploy_dev.py— der Stand, der gleich hochgehen soll, kommt auf die Testinstanztests/e2e/run.py— der vollständige Testlauf
Erst bei Rückgabewert 0 beginnt der Upload auf dc.mhdf.de. Schlägt etwas fehl, wird
nichts übertragen.
Die Reihenfolge ist der Punkt: Liefe der Testlauf gegen den vorhandenen Dev-Stand, prüfte er einen älteren Code als den, der gleich produktiv geht — und gäbe grünes Licht für etwas, das er nie gesehen hat.
python scripts/deploy.py --ohne-test
Der Notausgang. Er gehört in die Hand eines Menschen, der weiß, warum er ihn nimmt —
--dry-run läuft ohnehin ohne Testlauf, weil dabei nichts übertragen wird.
4. Testlauf
python tests/e2e/run.py
Der Lauf setzt zuerst die Datenbank zurück, spielt alle Migrationen ein, legt einen Administrator und ein Master-Token an — und prüft dann in dieser Reihenfolge:
| Gruppe | Was geprüft wird |
|---|---|
sperre |
Basic-Auth, gesperrte Verzeichnisse, Sicherheitskopfzeilen, testctl-Schlösser |
kern |
Health, OpenAPI, Migration (auch: wiederholbar), 404/405/400-Verhalten |
token |
Master-/Sub-Token, Rechteeinschränkung, keine Rechteausweitung |
bugtracker |
melden, Idempotenz, Arbeit holen, Reservierung, Kommentar, Abschluss |
roadmap |
Epics, Einplanung, Lesewege |
fehlerstrom |
Ausnahmen aus Clients |
lizenz |
Entwicklungsschlüssel, Prüfung, Rückgabe |
watchdog |
Lebenszeichen, Ereignisse, Kennzahlen, Auswertung |
update |
Release veröffentlichen, Update finden bzw. nicht finden, Signierschlüssel |
setup |
Erstinstallation: Anmeldung, Katalog, Projekttoken, Monitor |
supervisor |
Befunde melden, lesen, einstufen |
Nützliche Schalter:
python tests/e2e/run.py --gruppe sperre --gruppe kern
python tests/e2e/run.py --nur roadmap --log
python tests/e2e/run.py --bericht bericht.json --kein-reset
| Schalter | Wirkung |
|---|---|
--gruppe X |
nur diese Gruppe (mehrfach möglich) |
--nur TEXT |
Namensfilter |
--kein-reset |
Datenbestand behalten (dann master_token in der Konfiguration hinterlegen) |
--log |
bei Fehlschlägen das Serverlog der Instanz anhängen |
--stopp |
beim ersten Fehlschlag abbrechen |
--bericht DATEI |
Ergebnisse als JSON |
--liste |
nur auflisten, nichts ausführen |
--ipv4 |
IPv4 erzwingen (siehe §8) |
Rückgabewert: 0 alles bestanden, 1 Fehlschläge, 2 der Lauf sagt nichts aus.
Damit lässt er sich ohne Auswertung der Ausgabe in eine Pipeline hängen.
2 kommt in zwei Fällen: der Lauf konnte gar nicht starten (Zurücksetzen fehlgeschlagen,
kein Testfall passt zur Auswahl) — oder es wurde mehr übersprungen als geprüft.
Der zweite Fall trifft fast nur gefilterte Läufe. Übersprungen wird, wenn eine
Voraussetzung aus einem früheren Testfall fehlt, und bei --gruppe/--nur ist der oft
mit weggefiltert: ctx['freies_item'] etwa entsteht in der Gruppe bugtracker, wird aber
in roadmap gebraucht. Vorher endete so ein Lauf mit „2 bestanden, 0 fehlgeschlagen,
5 übersprungen" und Rückgabewert 0 — grün, obwohl fünf von sieben Fällen nie liefen.
Nichts ist dabei kaputt; geprüft wurde es aber auch nicht, und der Unterschied zwischen
„kaputt" und „nicht prüfbar" darf nicht ausgerechnet am Rückgabewert verschwinden. Der
Bericht führt dasselbe als aussagekräftig: false.
Einen Testfall ergänzen
@test('bugtracker', 'Kurze Aussage, was gelten soll')
def _(ctx):
"""Warum das wichtig ist - eine Zeile, kein Roman."""
r = ctx.client.post('/api/bugtracker/v1/report', body={...})
erwarte_erfolg(r)
ctx['bug_id'] = r.feld('item_id') # steht dem nächsten Testfall zur Verfügung
Die Reihenfolge der Registrierung ist die Reihenfolge der Ausführung; die Importe in
run.py legen die Reihenfolge der Dateien fest. Fehlt eine Voraussetzung (ctx['bug_id']
wurde nie gesetzt), wird der Testfall übersprungen statt fälschlich rot — der
Unterschied zwischen „kaputt" und „nicht prüfbar" muss im Bericht sichtbar bleiben.
Bekannte Befunde
Ein gefundener Fehler bekommt zweierlei: ein Item im Bugtracker und einen Testfall, der
ihn festhält. Damit dieser Testfall nicht jedes Deployment blockiert, bis der Fehler behoben
ist, steht er in tests/e2e/bekannte_befunde.json:
"supervisor/Antwortumschlag von observe bleibt erkennbar": {
"item": 59,
"seit": "2026-08-29",
"grund": "observe ueberschreibt das Umschlagfeld 'status' mit dem Zustand des Befundes."
}
Der Fall läuft weiter mit, erscheint als BEK und im Bericht — aber der Lauf bleibt grün.
Die beiden Alternativen wären schlechter: den Testfall löschen heißt, den Befund zu
vergessen; mit einem dauerhaft roten Lauf leben heißt, dass niemand mehr hinsieht, wenn
etwas Neues rot wird.
Ein Eintrag ohne Item-Nummer ist eine Ausrede, kein Befund. Besteht der Fall wieder,
meldet der Lauf das ausdrücklich (NEU!) — dann gehört der Eintrag heraus und das Item auf
resolved.
5. Die Zugangssperre
Die Testinstanz läuft mit debug = true, mit Beispieldaten und mit einem Datenbestand, den
jeder Testlauf löschen darf. Nichts davon gehört ins offene Netz.
- Basic-Auth vor allem, was ein Mensch im Browser aufruft — WebUI,
/docs/,install_db.php,testctl.php. - Ausnahme für
/api/, wenn der Aufruf bereits ein Token der Anwendung mitbringt (X-Agent-Token,X-Master-Token,X-License-Key,X-Watchdog-KeyoderAuthorization: Bearer …).
Ohne diese Ausnahme müsste jedes SDK zusätzlich zu seinem Token die Basic-Auth-Daten kennen.
Die Testinstanz prüfte dann einen Aufrufweg, den es auf dc.mhdf.de nicht gibt. Die
Ausnahme öffnet nichts, was in der Produktion nicht ohnehin offen wäre — geprüft wird das
Token weiterhin von ApiAuth/TokenManager. Ein Token-Header allein schaltet weder WebUI
noch Dokumentation frei; die Gruppe sperre prüft genau das bei jedem Lauf.
config/.htpasswd liegt im Webroot, ist aber durch die zweite .htaccess in config/
gesperrt (Require all denied) — die Datei muss dort liegen, weil AuthUserFile einen
absoluten Pfad braucht.
6. testctl.php
Steuert die Instanz. Erreichbar nur mit Basic-Auth und Shared Key, und nur auf einer
Instanz mit testing.enabled = true und app.env = 'test'.
curl -s -u deploy:PASSWORT -H "X-Master-Token: SHARED_KEY" \
"https://dc-dev.mhdf.de/testctl.php?action=status"
| Aktion | Wirkung |
|---|---|
status |
Tabellen, Zeilenzahlen, angewendete Migrationen, Debug-Zustand |
reset |
löschen, migrieren, Administrator und Master-Token anlegen (braucht confirm) |
wipe |
nur löschen (braucht confirm = Name der Datenbank) |
migrate |
Migrationen nachziehen |
genkey |
Signierschlüssel für den Updateservice erzeugen |
log |
die letzten Zeilen aus var/log/ |
clearlog |
Logdateien leeren |
reset und wipe verlangen confirm mit dem Namen der Datenbank. Damit lässt sich der
Aufruf nicht versehentlich gegen die falsche Instanz absetzen.
7. Wenn etwas fehlschlägt
python tests/e2e/run.py --nur "<Name>" --log— der einzelne Fall mit Serverlog.--kein-resetdazu, wenn der Datenbestand des fehlgeschlagenen Laufs erhalten bleiben soll.- Die Instanz steht im Browser offen:
https://dc-dev.mhdf.de(Basic-Auth, dann Anmeldung mit dem Administrator ausServerdaten.txt).
Ein Fehlschlag ist nicht automatisch ein Fehler der Anwendung — er kann auch heißen, dass der Testfall einen Vertrag beschreibt, den es so nicht mehr gibt. Beides ist ein Befund; die Frage ist nur, welche Seite geändert wird.
8. Bekannte Eigenheiten der Umgebung
IPv6. Alle Hoster-Adressen haben einen AAAA-Eintrag. Ist von einem Arbeitsplatz aus
keine IPv6-Route vorhanden, läuft in Python jeder Aufruf zuerst 21 Sekunden in einen
TCP-Timeout, bevor er auf IPv4 zurückfällt (curl tut das sofort, Python nicht). Der
Testlauf prüft IPv6 deshalb einmal beim Start und schaltet bei Bedarf für den ganzen Lauf
auf IPv4 um; die gewählte Familie steht in der Kopfzeile der Ausgabe. Betrifft nur den
Testlauf — für andere Python-Werkzeuge gegen dc.mhdf.de gilt dasselbe.
PHP 8.2.33, FPM, memory_limit 512 MB, max_execution_time 120 s. Die Produktion kann
davon abweichen; wo eine Laufzeitgrenze zum Befund gehört, muss sie mitgeprüft werden.
Datenbank watchdog auf lwrs.your-database.de — vorher die Datenbank des alten
Watchdogs, jetzt ausschließlich Testbestand. Sie wird bei jedem Lauf mit reset geleert.