feat(2.7.0): Roadmap im DC, Testinstanz mit Testtor, Supervisor und MCP

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>
This commit is contained in:
Deploymentcenter Bot
2026-08-29 21:59:20 +02:00
co-authored by Claude Opus 5
parent 9d0261306c
commit 1d5ac92051
52 changed files with 15261 additions and 103 deletions
+833
View File
@@ -0,0 +1,833 @@
# Linux-Testumgebung — Anleitung für Agenten
**Stand: 28.08.2026** · Gilt projektübergreifend für alle Agenten und Entwickler, die auf der
geteilten Test-VM arbeiten.
> **Diese Datei ist die maßgebliche Fassung.** Sie liegt im Deploymentcenter-Repo unter
> `docs/TESTUMGEBUNG.md`. Die Kopie auf der Maschine (`~/TESTUMGEBUNG.md`) und die Verweise in
> den Projektrepos zeigen hierher. Wer etwas an der Umgebung ändert, ändert es **hier** und
> kopiert danach auf die VM — nicht umgekehrt.
>
> **Bewusst nicht unter `public/`:** Diese Datei nennt eine interne Adresse, einen Benutzernamen
> und den Weg, `sudo` zeitweise freizuschalten. `/docs/` auf dem Deploymentcenter ist öffentlich
> erreichbar; dort gehört das nicht hin. Das Testkit-Skript dagegen schon — es enthält nichts
> davon.
---
## 1. Was diese Maschine ist
Eine Ubuntu-Desktop-VM zum **Testen** — nicht für Produktivbetrieb, nicht für echte Zugangsdaten,
nicht für Geld bewegende Läufe.
| | |
|---|---|
| System | Ubuntu 26.04 LTS, Kernel 7.0, x86_64 |
| Ausstattung | 31 GB RAM, 5 CPUs, ~33 GB freier Plattenplatz |
| Erreichbar | `192.168.40.48` über einen WireGuard-Tunnel |
| Benutzer | `richard` |
| Besonderheit | **Desktop-Umgebung vorhanden** — grafische Anwendungen sind testbar, nicht nur Dienste |
**Warum eine Desktop-VM?** Weil sich damit beides prüfen lässt: der kopflose Serverbetrieb
(systemd) *und* die grafische Oberfläche auf der Zielplattform.
---
## 2. Zugang einrichten
Der Zugang läuft über **SSH-Schlüssel**. Passwort-Anmeldung wird nicht verwendet — Agenten geben
grundsätzlich keine Passwörter ein. Diese Regel gilt auch dort, wo es bequemer wäre: Der
`update-agent` fragt bei `--action install` nach Zugangsdaten; genau deshalb nimmt das Testkit
diesen Weg nicht (siehe §4.7).
### 2.1 Schlüssel erzeugen (auf dem eigenen Rechner)
```bash
ssh-keygen -t ed25519 -f ~/.ssh/testvm_<projekt> -N "" -C "<projekt>@testvm"
```
Ein **eigener Schlüssel je Projekt** — dann lässt sich einzelnen Projekten der Zugang entziehen,
ohne alle anderen auszusperren.
### 2.2 Öffentlichen Schlüssel hinterlegen
Der Inhalt von `~/.ssh/testvm_<projekt>.pub` wird auf der VM in `~/.ssh/authorized_keys`
eingetragen. **Das muss ein Mensch tun** (einmalig, mit Passwort). Direkt auf der VM:
```bash
mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo '<INHALT DER .pub-DATEI>' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys
```
### 2.3 SSH-Konfiguration (auf dem eigenen Rechner)
```
Host testvm
HostName 192.168.40.48
User richard
IdentityFile ~/.ssh/testvm_<projekt>
IdentitiesOnly yes
ServerAliveInterval 30
ServerAliveCountMax 4
```
⚠️ **Kein `ControlMaster`/`ControlPath` eintragen, wenn der Client unter Windows läuft** —
Windows-OpenSSH unterstützt Verbindungs-Multiplexing nicht und scheitert mit
„Failed to connect to new control master". Stattdessen mehrere Prüfschritte pro Aufruf bündeln.
Test: `ssh testvm "hostname"`
---
## 3. Was installiert ist
| Werkzeug | Version | Anmerkung |
|---|---|---|
| .NET SDK | 10.0.111 (Runtime 10.0.11) | SDK, nicht nur Runtime — auf der VM kann gebaut werden |
| MariaDB | 11.8.6 | Dienst `mariadb`, lauscht auf `localhost:3306` |
| Git | 2.53.0 | |
| curl, python3 | vorhanden | Voraussetzung für das Testkit (§4) |
| Xvfb | vorhanden | Virtueller Framebuffer für GUI-Tests ohne Bildschirm (siehe 5.7) |
| xdotool | vorhanden | Klicks und Tastatureingaben skripten |
| ImageMagick | vorhanden | `import` für Bildschirmaufnahmen, `convert` zum Zuschneiden |
| JetBrains Rider | vorhanden | |
| Docker | **nicht** installiert | |
| Node.js | **nicht** installiert | |
| jq | **nicht** installiert | Wird auch nicht gebraucht — das Testkit liest JSON mit `python3` |
**Nicht vorhandene Werkzeuge dürfen installiert werden** — aber bitte in der Belegungsliste
(Abschnitt 7) vermerken, damit andere wissen, was sich geändert hat.
---
## 4. Das Testkit — vorhandene Infrastruktur statt Eigenbau
Alles, was ein Testlauf braucht, gibt es im Deploymentcenter schon: Lizenzschlüssel,
Release-Ablage, Update-Agent, Bugtracker. Was fehlte, war der Weg dorthin ohne einen Menschen,
der im WebUI etwas anklickt.
**`dc-testkit` ist dieser Weg.** Ein Skript, ein Aufruf je Schritt, für alle Projekte dasselbe.
> **Warum überhaupt ein Skript und nicht „der Agent ruft die API direkt auf"?** Weil sonst jeder
> Agent seine eigene Fassung der immer gleichen fünf `curl`-Aufrufe baut — jede mit anderen
> Vorgaben, anderer Fehlerbehandlung und anderem Umgang mit dem Token. Die Unterschiede fallen
> niemandem auf, solange nichts schiefgeht. Und wenn etwas schiefgeht, ist unklar, ob die
> Anwendung oder der Aufruf schuld war. Ein Weg, den alle nehmen, ist der einzige, dessen Fehler
> man findet.
### 4.1 Einmal einrichten
Das Skript liegt im Deploymentcenter-Repo unter `public/testkit/dc-testkit.sh` und wird mit
`deploy.py` ins Webroot ausgeliefert. Auf der VM:
```bash
mkdir -p ~/bin
curl -fsSL https://dc.mhdf.de/testkit/dc-testkit.sh -o ~/bin/dc-testkit
chmod +x ~/bin/dc-testkit
```
> ⚠️ **Beides — dieser Download und `dc-testkit license` — setzt Deploymentcenter 2.6.1 voraus.**
> Solange die Fassung auf dem Server älter ist, gibt es die Datei unter `/testkit/` nicht, und der
> Endpunkt `/api/license/v1/dev-key` fehlt. Der fehlende Endpunkt fällt dabei **nicht** mit 404
> auf: Die Lizenz-API antwortet bei unbekannten Pfadsegmenten mit ihrer Verfügbarkeitsprüfung, also
> mit HTTP 200. Das Testkit erkennt das und sagt es; wer von Hand `curl`t, sucht sonst lange.
>
> Bis dahin geht es auch per `scp` aus dem Repo:
> `scp public/testkit/dc-testkit.sh testvm:~/bin/dc-testkit`
Dann je Projekt eine Konfiguration — **das Token gehört in die Datei, nicht in die Befehlszeile**,
sonst steht es in der Shell-History:
```bash
mkdir -p ~/.config/dc-testkit
cat > ~/.config/dc-testkit/config <<'EOF'
DC_URL=https://dc.mhdf.de
DC_TOKEN=<Werkbank-Token des Projekts>
DC_PROJECT=<slug>
DC_CHANNEL=prod
EOF
chmod 600 ~/.config/dc-testkit/config
```
Arbeitet ein Agent an mehreren Projekten, hält er je Projekt eine eigene Datei und wählt sie über
`DC_TESTKIT_CONFIG=~/.config/dc-testkit/polytrader dc-testkit …`.
Prüfen, ob alles steht:
```bash
dc-testkit doctor
```
### 4.2 Das Werkbank-Token
Ein Mensch legt es **einmal je Projekt** an — im WebUI unter *System → Tokens*:
| Feld | Wert |
|---|---|
| Typ | Master |
| Besitzerart | `dev_agent` |
| Projekt | der Slug des Projekts |
| Rechte | `bugtracker:manage`, `license:dev`, `updateservice:read` |
Mehr braucht es nicht, und mehr sollte es nicht sein. `bugtracker:manage` schließt Lesen und
Melden ein, `license:dev` schließt `updateservice:read` ein — zwei Häkchen genügen also.
**Nicht `*` vergeben.** Ein Token mit allen Rechten auf einer geteilten Testmaschine darf auch
Releases veröffentlichen und Lizenzen fremder Projekte anfassen.
### 4.3 DEV-Lizenzschlüssel selbst holen
```bash
dc-testkit license
```
Das ist der Aufruf, der bisher fehlte. Er stellt für **diese Maschine und dieses Projekt** einen
befristeten Lizenzschlüssel aus und legt ihn mit Rechten 600 ab. Ausgegeben wird er nicht — wer
ihn wirklich sehen muss, hängt `--print` an.
Was dabei passiert und warum:
| | |
|---|---|
| **Befristet** | 30 Tage (einstellbar über `license.dev_ttl_days`, höchstens 90). Ein Schlüssel, den beim Anlegen niemand befristet hat, läuft nie ab und liegt in drei Monaten noch in einem Chatverlauf. |
| **Erkennbar** | Trägt `dev-agent@testumgebung.invalid` als Kundenadresse und `DEV <maschine>` als Namen. In der Aktivierungsliste ist Testhardware damit von echten Installationen zu unterscheiden. |
| **Wiederholbar** | Derselbe Aufruf liefert denselben Schlüssel zurück und verlängert die Frist. Er darf deshalb am Anfang **jedes** Laufs stehen. |
| **Öffnet die Release-Ablage** | `/releases/<slug>/` liegt seit 2.4 hinter Basic-Auth, die am Lizenzschlüssel hängt. Der neue Schlüssel wird beim Ausstellen sofort in die `.htpasswd` des Produkts nachgezogen. |
| **Begrenzt** | Höchstens 12 gleichzeitig je Projekt. Eine Schleife in einem fehlerhaften Testskript legt sonst über Nacht tausende an. |
Zurückgeben, wenn die Maschine das Projekt nicht mehr testet:
```bash
dc-testkit license --revoke
```
Nötig ist das nicht — abgelaufene Schlüssel verschwinden von selbst. Es macht nur sofort Platz.
> **Ein DEV-Schlüssel ist kein Sonderfall im Lizenzsystem.** Er durchläuft dieselbe Prüfung,
> dieselbe Aktivierungsgrenze, denselben Release-Zugang wie ein Kundenschlüssel. Das ist Absicht:
> Wäre er ein Sonderweg, prüfte der Test hinterher etwas anderes als den Betrieb.
### 4.4 Installations- und Updateroutinen prüfen
```bash
dc-testkit agent # update-agent holen, Prüfsumme gegen installer.json abgleichen
dc-testkit install # Erstauslieferung in einen leeren Sandkasten
dc-testkit verify # Was liegt jetzt da, und passt es zu dem, was der Server führt?
dc-testkit update # Update auf latest
dc-testkit verify
```
Oder alles auf einmal, mit automatischer Meldung dessen, was scheitert:
```bash
dc-testkit run
```
Der Sandkasten liegt unter `~/.local/state/dc-testkit/<projekt>/sandbox` — bewusst **nicht** in
`/opt`. Ein Testlauf, der die echte Installation überschreibt, ist kein Test, sondern ein
Deployment.
**Was geprüft wird:** Download aus der Release-Ablage mit dem eigenen Lizenzschlüssel,
SHA256-Abgleich, Signaturprüfung, Staging und Rollback, das geschriebene `manifest.json`, und der
Abgleich der installierten Fassung gegen die, die der Server als neueste führt.
Ein Rückfall auf eine ältere Fassung — der Weg, der im Ernstfall gebraucht wird und deshalb
geprüft gehört:
```bash
dc-testkit update --version 1.3.2
dc-testkit verify
```
Der Lizenzschlüssel geht dabei über die Umgebungsvariable `DC_LICENSE_KEY` an den Agenten, nicht
über `--license-key`. **Auf einer geteilten Maschine sieht jeder andere Benutzer die
Befehlszeilen fremder Prozesse** in `ps aux`; die Umgebung eines Prozesses liest nur, wem er
gehört.
### 4.5 Gefundenes melden
Ein Befund, von dem nur das Terminal weiß, ist kein Befund gewesen.
```bash
dc-testkit report --type bug --severity high \
--title "Update bricht bei fehlender manifest.json ab" \
--desc "Nach der Erstauslieferung fehlt manifest.json; der naechste Lauf haelt jede Version fuer neuer." \
--file "client-dotnet/Deploymentcenter.UpdateAgent/Program.cs" --line 412
```
Auch ein Wunsch statt eines Fehlers:
```bash
dc-testkit report --type feature --severity wishlist \
--title "update-agent: kopfloser Installationsmodus" --desc-file /tmp/begruendung.txt
```
Die Beschreibung darf über die Standardeingabe kommen — Fehlertexte entstehen meist als Ausgabe
eines anderen Befehls:
```bash
./meinprogramm 2>&1 | dc-testkit report --title "Absturz beim Start" --severity critical
```
**`client_ref` wird abgeleitet, nicht gewürfelt** — aus Projekt, Titel und Fundstelle. Derselbe
Befund aus zehn Testläufen legt damit ein Item an und nicht zehn. Wer bewusst ein zweites will,
gibt `--ref` selbst an.
### Der Tracker antwortet — melde nicht blind
Bevor du etwas meldest, ist die Frage nicht „ist das ein Fehler?", sondern **„ist das hier schon
bekannt, und in welcher Fassung wurde es behoben?"** Diese Antwort steht bereits in der Datenbank:
Jedes erledigte Item trägt seine Zielfassung in `resolved_in_build`.
Das Testkit schickt deshalb bei jeder Meldung die geprüfte Fassung mit — automatisch, gelesen aus
der `manifest.json` im Sandkasten. **Ohne diese Angabe kann der Server nicht unterscheiden, worum
es sich handelt**, und legt im Zweifel ein Regressionsitem an. Sie zu vergessen ist der häufigste
Weg, den Tracker unbrauchbar zu machen.
Daraus fallen zwei verschiedene Antworten:
**Behoben in einer späteren Fassung als deiner** — der Regelfall auf einem Prüfstand, der nicht
immer die neueste Fassung fährt:
```
ok Nicht gemeldet - schon behoben.
Item #128, behoben in 1.4.3; geprüft wurde 1.4.2.
Es wurde nichts angelegt und nichts hochgezählt.
Aktualisiere und prüfe erneut. Tritt es dann immer noch auf,
ist es eine Regression und gehört gemeldet.
```
Es entsteht **kein** Item, kein Kommentar, kein hochgezählter Zähler. Der Befund sagt nichts über
die Software aus, sondern nur über die Fassung, die du geprüft hast.
**Behoben in einer Fassung, die du bereits hast** — dann ist es ernst:
```
warn REGRESSION: Item #144 ist ein Wiederauftreten von #128.
warn Der Fehler galt als behoben und ist in 1.4.7 wieder da.
```
Hier entsteht ein neues Item mit Verweis auf das alte, und am alten wird vermerkt, in welcher
Fassung es wieder auftrat. Das ist der Fall, für den die Kennzeichnung da ist.
> **Warum das vorher nicht ging.** Der Server verglich die Fassungen nicht. Jede Meldung gegen ein
> bereits erledigtes Item wurde zur Regression erklärt — auch die von einem Prüfstand, der den Fix
> schlicht noch nicht hatte. Bei einer Testumgebung, die absichtlich auch ältere Fassungen prüft,
> wäre bald jede zweite Meldung eine „Regression" gewesen, und die echten wären darin
> untergegangen.
### Der Prüfauftrag — Arbeit, die von selbst zu dir kommt
Ein Fix ist eine Behauptung, bis jemand ihn gegen ein ausgeliefertes Paket geprüft hat. Genau
dafür gibt es seit 2.6.1 einen Platz in der Arbeitsschlange — und deshalb braucht der Rückkanal
keine Zustellung: **`dc-testkit next` gibt dir Prüfaufträge wie jede andere Arbeit.**
```
==> Naechstes Item
NACHPRUEFUNG, keine neue Fehlersuche:
Item #128 Update bricht bei fehlender manifest.json ab
Behoben laut Tracker in: 1.4.3
Projekt: deploymentcenter
Gegen diesen Build testen. Dann eines von beidem:
traegt -> dc-testkit confirm 128 "kurz was geprueft wurde"
traegt nicht-> den Fehler erneut melden (dc-testkit report ...);
das Item wechselt dann selbst auf fix_insufficient.
```
**Lies den Hinweis.** Ein Prüfauftrag sieht im JSON aus wie jede andere Arbeit, stellt aber eine
andere Frage: nicht „behebe das", sondern „trägt der Fix?". Wer das übersieht, fängt an, einen
bereits behobenen Fehler ein zweites Mal zu beheben.
### Der Lebenslauf
| Status | Bedeutung |
|---|---|
| `open` → `in_progress` | Gewöhnliche Arbeit. Übernehmen befördert |
| `resolved` | Ein Agent sagt: behoben in Build X. Noch nichts ausgeliefert |
| `awaiting_verification` | Build X **ist veröffentlicht** — jetzt lässt sich nachsehen |
| `in_verification` | Jemand sieht gerade nach. Entsteht durch denselben Claim |
| `fix_insufficient` | Gegen den Build geprüft, Fehler immer noch da |
| `closed` | Bestätigt weg |
Der Übergang nach `awaiting_verification` passiert **beim Veröffentlichen**, nicht beim Beheben.
Vorher gibt es kein Paket, gegen das man prüfen könnte — eine Prüfaufgabe zu diesem Zeitpunkt
wäre eine, die niemand erledigen kann.
**`confirm` ist nicht `resolve`.** `resolve` behauptet „ich habe es behoben, in Build X" und setzt
`resolved_in_build`. `confirm` behauptet nichts, sondern bestätigt den Fix eines anderen. Beides in
einen Befehl zu legen hieße, den Unterschied zwischen Fixen und Prüfen einzuebnen.
### Was passiert, wenn du nicht fertig wirst
Nichts Schlimmes — aber es bleibt nicht liegen:
- **Lease läuft ab** → das Item geht zurück auf `awaiting_verification` und liegt wieder in der
Schlange. (Dasselbe gilt seit 2.6.1 für gewöhnliche Arbeit: `in_progress` → `open`. Vorher blieb
ein liegengelassenes Item auf `in_progress` stehen und wurde nie wieder ausgegeben — es war weg.)
- **Dreimal begonnen, nie beendet** → ein Vermerk am Item holt einen Menschen. Still weiter
einzureihen hieße, eine Schlange zu führen, in die niemand sieht.
- **30 Tage niemand angenommen** → wird ungeprüft geschlossen, mit einem Kommentar, der genau das
sagt. Ein ehrliches Ende ist besser als eine Aufgabe, die niemand annimmt. Die Zahl
„geschlossen" bedeutet dann eben nicht „bestätigt", und das steht am Item.
### Auch für Befunde in fremden Projekten
Hast du einen Fehler im Deploymentcenter gemeldet (§4.6), bekommst **du** den Prüfauftrag, wenn
er behoben ist — obwohl das Item dem Deploymentcenter gehört. Du bist als betroffen vermerkt, und
oft bist du der Einzige, der bestätigen kann, dass der Fix bei dir wirkt.
Das ist die eine, eng gefasste Ausnahme von der Regel „ein Agent arbeitet nur im eigenen Projekt":
nur Items, die auf Nachprüfung warten, nur wenn dein Projekt in `affects` steht. Du darfst sie
kommentieren, im Status setzen und zurückgeben — aber nicht `resolve`. Was in einem fremden Projekt
als „behoben in Build X" gilt, entscheidet dieses Projekt.
Den Kreis schließen, wenn daran gearbeitet wird:
```bash
dc-testkit next # naechstes Item uebernehmen (exklusiv)
dc-testkit comment 42 "Ursache gefunden: Pfadtrenner unter Linux." fix_proposed
dc-testkit resolve 42 v1.4.3 "Behoben in Installer.cs." # ich habe es behoben
dc-testkit confirm 42 "Gegen 1.4.3 geprueft, Paket sauber." # der Fix eines anderen traegt
dc-testkit unclaim 42 "Komme nicht weiter, brauche Zugriff auf die Kundendaten."
```
**Übernehmen ist keine Formsache.** Wer es überspringt, arbeitet irgendwann parallel zu jemandem
am selben Problem. Antwortet der Server mit `409 already_claimed`, nimm das nächste Item.
### 4.6 Befunde, die ein anderes Projekt betreffen
**Das ist der wichtigste Abschnitt dieser Datei.** Ein Agent auf der Test-VM stolpert
zwangsläufig über Dinge, die nicht seinem Projekt gehören: Der PolyTrader-Agent findet einen
Fehler im `update-agent`. Ihm fällt auf, dass diese Anleitung an einer Stelle lügt. Er hätte
einen Vorschlag, wie das Testkit besser wäre.
Landet das im eigenen Projekt, ist es verloren — dort liest es niemand, der es beheben könnte.
Landet es gar nirgends, wiederholt sich derselbe Stolperstein bei jedem, der nach ihm kommt.
```bash
dc-testkit projects # welche Slugs gibt es überhaupt?
dc-testkit report --project deploymentcenter --type bug --severity medium --title "update-agent schreibt bei --action update kein manifest.json" --desc-file /tmp/befund.txt --evidence "Nach dc-testkit install liegt kein manifest.json im Zielverzeichnis. Der naechste Lauf meldet deshalb 'Unbekannt' als installierte Fassung und haelt jede Version fuer neuer. Reproduziert mit polytrader und deploymentcenter, also nicht projektspezifisch."
```
### Wohin gehört was
| Befund | Slug |
|---|---|
| Fehler im `update-agent`, im SDK, in der Lizenzprüfung, im Bugtracker | `deploymentcenter` |
| Fehler oder Lücke in **dieser Anleitung** oder in `dc-testkit` | `deploymentcenter` — beide liegen in dessen Repo |
| Die Test-VM selbst: fehlendes Werkzeug, belegtes Display, kaputte Konvention | `deploymentcenter`, `--type feature` |
| Alles am eigenen Programm | das eigene Projekt, ohne `--project` |
**Die Testumgebung ist kein eigenes Projekt.** Sie hat keinen Slug. Wer sie verbessern will,
meldet an `deploymentcenter` — dort liegen Skript und Anleitung, und dort wird beides gepflegt.
### Drei Dinge, die dabei anders sind
**1. Eine Begründung ist Pflicht** — `--evidence`, mindestens 40 Zeichen. Nicht als Schikane:
Ein Agent in Projekt X kann selten sicher unterscheiden, ob das fremde Projekt kaputt ist oder ob
X es falsch benutzt. Die Voreinstellung eines hilfsbereiten Modells ist im Zweifel „melde es".
Etwas ausformulieren zu müssen kostet diesen Reflex fast immer den Kopf. Beantworte darin: *Was
wurde beobachtet, und warum liegt die Ursache dort und nicht bei mir?*
**2. Der Schweregrad wird auf `medium` gedeckelt.** `high` und `critical` vergibt die empfangende
Seite — nur sie weiß, wer sonst noch betroffen ist. Das Testkit warnt, wenn du höher einstufst.
Was dringlich ist, gehört in die Begründung, nicht in das Feld.
**3. Die Meldung landet in `triage_external`, nicht in `open`.** Das ist ein Eingang, kein
Ablagefach: Fremdmeldungen zählen erst nach Bestätigung in die Statistik des Zielprojekts —
sonst verdürben Fehlmeldungen die Zahlen, auf denen die Supervisor-Detektoren kalibrieren.
**Such dein Item also nicht in der offenen Liste des fremden Projekts.**
### Die Herkunft ist bezeugt, nicht behauptet
`reported_from` wird aus der Bindung deines Tokens abgeleitet und **nie** aus dem Body übernommen.
Du kannst dich nicht als jemand anderes ausgeben. Genau das macht das Signal erst wertvoll: Melden
drei Projekte dasselbe, ist das eine Aussage über die Sache und nicht über die Meldefreudigkeit
eines Agenten. Meldet ein zweites Projekt einen bereits erfassten Befund, entsteht kein neues Item
— es wird als weiterer Betroffener geführt (`affects`).
**Voraussetzung ist ein Werkbank-Token** (Besitzerart `dev_agent`, §4.2). Nur solche Token dürfen
projektübergreifend melden. Ein Laufzeit-Token darf es nicht: Es steckt in der ausgelieferten
Anwendung und ist extrahierbar.
### Wenn ein Fremdbefund dich betrifft
**Du musst nicht benachrichtigt werden — du kannst fragen.** Meldest du denselben Befund erneut,
antwortet der Tracker mit `known_fixed_in` (siehe §4.5): behoben in Fassung X, du hast Y. Das
beantwortet die Frage „ist mein Workaround noch nötig?" ohne Rückkanal, und zwar für Fremdmeldungen
genauso wie für eigene.
Was **nicht** von selbst passiert: Niemand stößt dich an. Wird ein Item behoben, das du gemeldet
hast, erfährst du es erst, wenn du das nächste Mal fragst — beim Melden oder über den Changelog:
```bash
curl -s "https://dc.mhdf.de/api/updateservice/v1/changelog?since=<deine Fassung>"
```
Für einen Testlauf, der ohnehin regelmäßig läuft, genügt das: Er fragt bei jedem Durchgang. Wer
einen Workaround gebaut hat und ihn zurückbauen möchte, ohne darauf zu warten, braucht den aktiven
Rückkanal (Roadmap C3).
### 4.7 Was das Testkit nicht kann — und warum
**Den interaktiven Installationsdialog.** `update-agent --action install` ist ein Dialog mit
Auswahllisten und Passwortabfrage. Er lässt sich nicht kopflos treiben, und ein Agent darf ohnehin
kein Passwort eingeben (§2).
Was `dc-testkit install` stattdessen tut, ist die Erstauslieferung in ein **leeres** Verzeichnis
über den Update-Weg. Das ist keine Notlösung: Der Installer holt die Dateien laut
[SETUP_INTEGRATION_GUIDE](./SETUP_INTEGRATION_GUIDE.md) über genau denselben Pfad — Download,
SHA256, Signaturprüfung, Staging, Rollback. Was der Dialog zusätzlich tut, ist fragen und eine
Konfiguration schreiben.
**Geprüft wird also die Auslieferung, nicht die Gesprächsführung.** Die Auflösung von `setup.json`
und das Schreiben der Konfiguration bleiben ungeprüft. Ein kopfloser Installationsmodus
(`--action install --answers antworten.json`) würde diese Lücke schließen; er steht in der
[Roadmap](./ROADMAP.md) unter G1.
---
## 5. Konventionen für mehrere Projekte
Die Maschine wird von mehreren Projekten geteilt. Diese Regeln verhindern, dass sich Tests
gegenseitig zerstören.
### 5.1 Datenbanken — je Projekt eine eigene
**Niemals** eine fremde Datenbank mitbenutzen und **niemals** eine Produktivdatenbank verbinden.
```bash
# Passwort erzeugen und ablegen (nur für den Besitzer lesbar)
PW=$(openssl rand -base64 18 | tr -d "/+=" | head -c 20)
echo "$PW" > ~/.<projekt>_test_dbpw && chmod 600 ~/.<projekt>_test_dbpw
sudo mariadb -e "CREATE DATABASE IF NOT EXISTS <projekt>_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS '<projekt>_test'@'localhost' IDENTIFIED BY '$PW';
GRANT ALL PRIVILEGES ON <projekt>_test.* TO '<projekt>_test'@'localhost';
FLUSH PRIVILEGES;"
```
| Was | Schema | Beispiel |
|---|---|---|
| Datenbank | `<projekt>_test` | `polytrader_test` |
| Benutzer | `<projekt>_test`@`localhost` | `polytrader_test` |
| Passwortdatei | `~/.<projekt>_test_dbpw` (Rechte 600) | |
Die Rechte enden **an der eigenen Datenbank** — `GRANT ALL ON <projekt>_test.*`, nie global.
### 5.2 Verzeichnisse
| Zweck | Ort |
|---|---|
| Quellcode | `~/RiderProjects/<projekt>` |
| Sandkasten des Testkits | `~/.local/state/dc-testkit/<projekt>/sandbox` |
| Publish/Testlauf von Hand | `~/<projekt>-test` (voller Projektname, keine Kürzel) |
| Installation als Dienst | `/opt/<projekt>` |
### 5.3 systemd-Dienste
Unit-Name = Projektname: `<projekt>.service`. Eigenes Dienstkonto je Projekt:
```bash
sudo useradd --system --home-dir /opt/<projekt> --shell /usr/sbin/nologin <projekt>
```
Nach dem Test **abschalten** (Abschnitt 8) — sonst laufen irgendwann fünf Dienste, die niemand
mehr zuordnen kann, und der nächste Test misst gegen fremde Last.
> **Im Dienst ohne Heimatverzeichnis** (systemd `User=` ohne `$HOME`) fällt der Lizenz-Cache auf
> `/var/lib/<slug>/license` zurück. Ist auch das nicht beschreibbar, gibt es keinen Offline-Cache
> — dann muss `DEPLOYMENTCENTER_STORAGE_DIR` auf ein beschreibbares Verzeichnis zeigen. Das ist
> ein Fehlerbild, das nur im Dienstbetrieb auftritt und deshalb hier gehört.
### 5.4 Ports
Vor dem Belegen prüfen: `ss -tlnp`. Belegte Ports stehen in Abschnitt 7. Wer einen Port dauerhaft
belegt, trägt ihn dort ein.
### 5.5 X-Displays — feste Nummer je Projekt
Damit **mehrere Agenten gleichzeitig** Oberflächen testen können, bekommt jedes Projekt ein
eigenes virtuelles Display. Zwei Prozesse auf demselben Display sehen die Fenster des jeweils
anderen — Klicks landen im falschen Programm, Aufnahmen zeigen fremde Anwendungen.
| Display | Projekt |
|---|---|
| `:99` | **PolyTrader** |
| `:98` | **Predictalytics** |
| `:97` | **ClawdDotNet** |
| `:96` | **IBKRTrader** |
| `:95` und **absteigend** | künftige Projekte (nächstes freies von oben nach unten) |
Wer ein neues Projekt aufnimmt, trägt seine Nummer in die Belegungsliste (Abschnitt 7) ein.
**Belegte Displays prüfen:**
```bash
ls /tmp/.X11-unix/ # X0 = der echte Desktop, X99 = PolyTrader usw.
pgrep -a Xvfb # zeigt, welcher Xvfb auf welchem Display läuft
```
**Verwaiste Sockets** — beide Befehle gemeinsam lesen: Steht in `/tmp/.X11-unix/` ein `X98`, zu
dem `pgrep` keinen Prozess zeigt, ist das ein Überbleibsel eines abgestürzten oder hart beendeten
Xvfb. Die Nummer wirkt dann belegt, obwohl sie frei ist. Aufräumen:
```bash
sudo rm -f /tmp/.X11-unix/X98
```
*(Genau so entstanden: ein `xvfb-run -a`, das sich :98 nahm, weil :99 gerade lief — siehe die
Warnung in 5.7.)*
⚠️ **Drei Regeln, damit der Parallelbetrieb wirklich trägt:**
1. **`DISPLAY` in jedem Skript setzen** (`export DISPLAY=:98`). Fehlt die Variable, landet man auf
`:0` — dem echten Desktop — und stört den Menschen, der dort gerade arbeitet.
2. **Nie `pkill Xvfb` ohne Displaynummer.** Das erschlägt die Sitzungen aller anderen Projekte.
Immer `pkill -f "Xvfb :98"`. Dasselbe gilt für die eigene Anwendung: `pkill -f` mit einem zu
allgemeinen Muster trifft fremde Prozesse — besser die gemerkte PID beenden.
3. **Dateien in `/tmp` mit Projektpräfix.** `/tmp/v_1.png` überschreibt sich gegenseitig; nimm
`/tmp/<projekt>_v_1.png`. Gleiches gilt für Logdateien und Skripte.
### 5.6 Erhöhte Rechte
`sudo` verlangt normalerweise ein Passwort. Für Testläufe kann es befristet freigeschaltet werden
(**ein Mensch, einmalig**):
```bash
echo "richard ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/99-testing && sudo chmod 440 /etc/sudoers.d/99-testing
```
**Nach Abschluss wieder entfernen:**
```bash
sudo rm /etc/sudoers.d/99-testing
```
Das hebt den Schutz für das Konto vollständig auf — vertretbar auf einer Test-VM hinter VPN, aber
nicht dauerhaft und niemals auf einer Produktivmaschine.
**Für das Testkit wird das nicht gebraucht.** Es arbeitet vollständig im Benutzerkontext.
### 5.7 GUI-Anwendungen ohne Bildschirm testen
Braucht eine Anwendung X11, scheitert sie über SSH mit `XOpenDisplay failed`. Für einen einzelnen
Durchlauf genügt:
```bash
xvfb-run -n <deine-nummer> ./MeineAnwendung --irgendein-schalter
```
⚠️ **Nicht `xvfb-run -a` verwenden.** Das `-a` sucht sich selbst ein freies Display und **beginnt
dabei bei :99** — also ausgerechnet auf PolyTraders Nummer. Läuft dort gerade nichts, belegt es
diese Nummer; startet PolyTrader kurz darauf, kollidieren beide. Mit `-n 98` (bzw. der eigenen
Nummer aus 5.5) bleibt die Zuordnung eindeutig.
### 5.8 Oberflächen selbst begutachten — ohne Bildschirm, ohne fremde Hilfe
**Ein Agent kann die Oberfläche eigenständig prüfen.** Kein Mensch muss am Bildschirm sitzen und
Bildschirmfotos schicken. Das Verfahren hat sich am 26.08.2026 bewährt und **sieben Befunde
zutage gefördert**, darunter einen, der sämtliche Tabellen der Anwendung unsichtbar machte.
Drei Werkzeuge, alle installiert: **Xvfb** (virtuelles Display), **xdotool** (Klicken und Tippen),
**ImageMagick** (`import` für Aufnahmen, `convert` zum Zuschneiden).
Anders als `xvfb-run` wird das Display hier **dauerhaft** gestartet, damit sich zwischen den
Aufnahmen navigieren lässt:
```bash
#!/bin/bash
# Kopf anpassen - das ist alles, was sich je Projekt unterscheidet:
PROJEKT="polytrader"
DISP=":99" # eigene Nummer aus Abschnitt 5.5!
FENSTER="^PolyTrader$"
cd ~/$PROJEKT-test
export DISPLAY=$DISP
pkill -f "Xvfb $DISP" 2>/dev/null; sleep 1
Xvfb $DISP -screen 0 1600x1000x24 >/dev/null 2>&1 &
sleep 3
nohup ./MeineAnwendung > /tmp/${PROJEKT}_gui.log 2>&1 &
APP=$!
sleep 25 # Startzeit großzügig bemessen
# Fenster-ID ermitteln (bestaetigt, dass die Oberflaeche ueberhaupt erscheint)
xdotool search --name "$FENSTER" | head -1
# navigieren und aufnehmen - Dateinamen mit Projektpraefix, sonst
# ueberschreiben sich parallel laufende Projekte gegenseitig
shoot() { sleep 5; import -window root /tmp/${PROJEKT}_$1.png; echo " $1"; }
shoot 1_start
xdotool mousemove 127 205 click 1; shoot 2_zweite_ansicht
xdotool mousemove 127 237 click 1; shoot 3_dritte_ansicht
kill $APP 2>/dev/null; sleep 2; pkill -f "Xvfb $DISP" 2>/dev/null
```
Danach die Bilder abholen und ansehen:
```bash
scp testvm:'/tmp/polytrader_*.png' .
```
**Was sich dabei bewährt hat:**
- **Großzügige Wartezeiten.** 25 Sekunden nach dem Start, 5 Sekunden nach jedem Klick. Wird zu
früh aufgenommen, entsteht ein halb gezeichnetes Fenster, und man sucht Fehler, die keine sind.
- **Klickpunkte aus dem ersten Bild ablesen.** Erst eine Aufnahme machen, dort die Koordinaten der
Menüpunkte bestimmen, dann das Skript damit bestücken.
- **Details vergrößern statt raten.** Bei einem Verdacht den Ausschnitt herausschneiden:
```bash
convert v_1_start.png -crop 560x130+265+540 +repage -resize 200% /tmp/detail.png
```
- **Kontraste nachmessen statt schätzen.** Ob Text lesbar ist, lässt sich ausrechnen — Pixelfarben
auslesen und das WCAG-Verhältnis bestimmen (4,5:1 für normalen Text). So wird aus „wirkt blass"
ein belegter Befund.
- **Nur eine Instanz des eigenen Projekts gleichzeitig.** Läuft der systemd-Dienst noch, schreiben
zwei Prozesse in dieselbe Datenbank und es entstehen Fehler, die nichts mit der Oberfläche zu tun
haben. Vorher `sudo systemctl stop <projekt>`. (Ein **anderes** Projekt auf seinem eigenen
Display stört dagegen nicht — dafür ist die Trennung aus 5.5 da.)
- **Auf Zeichen achten, die falsch aussehen.** Fehlt eine Icon-Schriftart, ersetzt Fontconfig sie
stillschweigend durch etwas anderes — ein Kalendersymbol wird dann zu einer sinnlosen Zahl. Das
fällt nur im Bild auf, nie im Log.
Was dabei auffällt, gehört in den Bugtracker — nicht in eine Notizdatei. Siehe §4.5.
Für einen Test **mit echtem Bildschirm** kann trotzdem jemand am Desktop der VM sitzen (oder RDP
nutzen, Port 3389 ist offen) — nötig ist es für die Sichtprüfung nicht.
---
## 6. Was hier nicht passieren darf
1. **Keine Produktivdatenbank verbinden.** Hintergrunddienste schreiben oft ungefragt — bei
PolyTrader etwa synchronisiert der `MarketSyncService` selbständig Marktdaten.
2. **Keine echten Handels- oder Zugangsdaten hinterlegen.** Keine privaten Schlüssel, keine
Live-API-Zugänge. **Lizenzschlüssel nur als DEV-Schlüssel aus `dc-testkit license`** — ein
Kundenschlüssel auf einer geteilten Maschine verbraucht dessen Aktivierungsplätze und ist
hinterher nicht mehr sauber freizubekommen.
3. **Kein Token mit `*`.** Für Testläufe genügen zwei Rechte, siehe §4.2.
4. **Keine fremden Projektverzeichnisse oder Datenbanken anfassen.**
5. **Nichts veröffentlichen.** `updateservice:publish` gehört in die CI, nicht auf die Test-VM.
Ein Release aus einem Testlauf heraus wäre für Kunden sofort sichtbar.
6. **Nichts Dauerhaftes hinterlassen**, was nicht in Abschnitt 7 steht.
---
## 7. Belegung (bei Änderungen fortschreiben)
**Datenbanken**
| Datenbank | Benutzer | Projekt | seit |
|---|---|---|---|
| `polytrader_test` | `polytrader_test`@localhost | PolyTrader | 26.08.2026 |
**Dienste**
| Unit | Konto | Verzeichnis | Projekt | seit |
|---|---|---|---|---|
| `polytrader.service` | `polytrader` | `/opt/polytrader` | PolyTrader | 26.08.2026 |
**Verzeichnisse**
| Projekt | Publish-Verzeichnis | Dienst |
|---|---|---|
| PolyTrader | `~/polytrader-test` | `/opt/polytrader` |
**Werkbank-Token** (Vergabe siehe 4.2)
| Projekt | Token angelegt | Rechte |
|---|---|---|
| *noch keins* | | |
**X-Displays** (Vergabe siehe 5.5)
| Display | Projekt | Stand |
|---|---|---|
| `:0` | echter Desktop — **nie belegen** | |
| `:99` | PolyTrader | 26.08.2026 |
| `:98` | Predictalytics | reserviert |
| `:97` | ClawdDotNet | reserviert |
| `:96` | IBKRTrader | reserviert |
| `:95` abwärts | frei für künftige Projekte | |
**Ports** (Stand 26.08.2026)
| Port | Belegt durch |
|---|---|
| 22 | SSH |
| 3306 | MariaDB (nur localhost) |
| 3389 | RDP |
| 5000 | vorhandener Dienst (nur IPv6-localhost) |
| 631 | CUPS |
**Nachträglich installiert:** MariaDB 11.8.6, Xvfb, xdotool, ImageMagick, x11-utils
(alle am 26.08.2026 für PolyTrader).
---
## 8. Aufräumen nach dem Test
Das Testkit räumt sein eigenes auf:
```bash
dc-testkit cleanup # Sandkasten weg, DEV-Schlüssel zurückgegeben, Agent gelöscht
dc-testkit cleanup --sandbox # nur der Sandkasten, Schlüssel behalten
```
Alles Übrige von Hand:
```bash
# Virtuelles Display beenden (falls noch offen) - NUR das eigene!
pkill -f "Xvfb :<deine-nummer>"
rm -f /tmp/<projekt>_*.png /tmp/<projekt>_gui.log
# Dienst abschalten und entfernen
sudo systemctl disable --now <projekt>
sudo rm -f /etc/systemd/system/<projekt>.service /etc/logrotate.d/<projekt>
sudo systemctl daemon-reload
# Dateien
sudo rm -rf /opt/<projekt> ~/<projekt>-test
sudo userdel <projekt>
# Datenbank (nur wenn nicht mehr gebraucht)
sudo mariadb -e "DROP DATABASE <projekt>_test; DROP USER '<projekt>_test'@'localhost';"
rm -f ~/.<projekt>_test_dbpw
# sudo-Freigabe zurücknehmen
sudo rm -f /etc/sudoers.d/99-testing
```
Danach Abschnitt 7 nachziehen.
---
## 9. Bekannte Einschränkungen
- **Claude Code lässt sich auf dieser VM nicht betreiben.** Es blockiert die Maschine bis zum
Neustart; mehrere Reparaturversuche waren erfolglos, es wird auf ein Update gewartet. Agenten
arbeiten deshalb **über SSH von außen**. (Speichermangel ist **nicht** die Ursache — die
Maschine hat 31 GB, davon 25 GB frei.)
- **Verbindungs-Multiplexing funktioniert nicht mit Windows-OpenSSH** (siehe 2.3).
- **Der Installationsdialog ist nicht kopflos prüfbar** (siehe 4.7). Damit bleiben die Auflösung
von `setup.json` und das Schreiben der Konfiguration ungeprüft.
- **Die Systemzeitzone ist Testgegenstand.** Für realistische Servertests wird sie zeitweise auf
UTC gestellt (`sudo timedatectl set-timezone UTC`). Wer sie ändert, stellt sie danach zurück —
Standard hier ist `Europe/Berlin`.
---
## Weiterführend
| | |
|---|---|
| [LICENSE_INTEGRATION_GUIDE.md](./LICENSE_INTEGRATION_GUIDE.md) | Wie die Lizenzprüfung in der Anwendung selbst funktioniert |
| [UPDATESERVICE_INTEGRATION_GUIDE.md](./UPDATESERVICE_INTEGRATION_GUIDE.md) | Release-Verteilung, Kanäle, Signatur, Zugangsschutz der Ablage |
| [SETUP_INTEGRATION_GUIDE.md](./SETUP_INTEGRATION_GUIDE.md) | Erstinstallation und `setup.json` |
| [Agenten-Handbuch](../public/docs/bugtracker.md) | Vollständige Beschreibung des Bugtracker-Workflows |
| `GET /api/openapi.json` | Die Schnittstelle maschinenlesbar |