diff --git a/docs/adr/001-phpunit-integration.md b/docs/adr/001-phpunit-integration.md new file mode 100644 index 0000000..b4b8a2d --- /dev/null +++ b/docs/adr/001-phpunit-integration.md @@ -0,0 +1,184 @@ +# ADR-001: PHPUnit-Integration in CI + Pre-Commit-Hook + +**Status:** Accepted (nachträglich dokumentiert) +**Datum:** 2026-06-04 +**Issues:** #65, #67 +**PRs:** #69, #70 +**Author:** Hermes (nachträgliche Doku auf Martin-Anweisung) + +## Kontext + +Das Projekt `landingpage-haus-schleusingen` enthält 18 PHPUnit-Tests (31 Assertions, 100% Pass-Rate) im `tests/`-Verzeichnis. Vor diesem ADR gab es: + +- **Lokale Test-Verifikation** nur manuell (`vendor/bin/phpunit`) +- **Keine CI-Pipeline** — Tests liefen nicht automatisch bei Push/PR +- **Pre-Commit-Hook** deckte nur Linting (PHP-Syntax, HTML, CSS, JS, Prettier), keine Tests + +**Probleme:** + +1. **Refactoring-Risiko:** Ohne CI-Tests können Bugs bei zukünftigen Änderungen unentdeckt auf `main` landen +2. **Regressions:** Kein Schutz gegen versehentliches Brechen existierender Tests +3. **Code-Qualität:** Manuelle Test-Verifikation ist fehleranfällig (vergessen, übersprungen) +4. **Reviewer-Belastung:** Martin muss bei jedem PR manuell Tests laufen lassen + +**Anforderungen:** + +- Tests müssen **automatisch** bei jedem Push/PR laufen +- Tests müssen **lokal vor dem Commit** laufen (schneller Feedback-Loop) +- Bei Test-Fehler: **Commit/PR abbrechen** mit klarer Fehlermeldung +- **Performance:** Tests dürfen nicht bei CSS/HTML/JS-only-Änderungen laufen (false-positive-Friktion vermeiden) +- **DRY:** Eine einzige Test-Ausführungslogik für lokale + CI-Ausführung + +## Entscheidung + +**Zwei-Layer-Strategie:** CI-Pipeline (Remote-Verifikation) + Pre-Commit-Hook (Lokal vor Push). + +### Layer 1: CI-Pipeline (`.gitea/workflows/phpunit.yml`) + +- **Trigger:** `push` + `pull_request` auf `main` +- **Runtime:** `ubuntu-latest`, PHP 8.5 + Composer +- **Install:** `apt-get install -y php-cli composer php-xml php-mbstring` +- **Test:** `composer install` (Lazy: nur wenn `vendor/` fehlt) → `vendor/bin/phpunit` +- **Architektur:** Analog zu existierender `lint.yml`, eigenständige Pipeline (nicht mit Lint kombiniert, da Test-Laufzeit ~25s unabhängig von Lint) + +### Layer 2: Pre-Commit-Hook (`.husky/pre-commit` + `scripts/pre-commit-checks.sh`) + +- **Trigger:** Lokaler `git commit` (Husky 9 Standard) +- **PHP_Detection:** `git diff --cached --name-only | grep -E '\.(php)$|^phpunit\.xml$|^composer\.(json|lock)$'` +- **Bei PHP-Files:** `scripts/pre-commit-checks.sh` ausführen +- **Bei Non-PHP-Commits:** PHPUnit skippen (Performance) +- **Bei Test-Fehler:** Exit-Code != 0 → Husky bricht Commit ab +- **DRY:** Shared `scripts/pre-commit-checks.sh` wird auch von `scripts/safe-commit.sh` aufgerufen (AI-Agent-Bypass-Schutz) + +### Schichten-Logik + +``` +┌─────────────────────────────────────┐ +│ Git Commit (lokal) │ +│ ↓ │ +│ Husky Pre-Commit Hook │ +│ ↓ │ +│ scripts/pre-commit-checks.sh │ ← Eine Source-of-Truth +│ ├─ Lint (PHP, HTML, CSS, JS) │ +│ └─ PHPUnit (wenn PHP touched) │ +│ ├─ composer install (lazy) │ +│ └─ vendor/bin/phpunit │ +│ ↓ (Exit 0) │ +│ Commit erstellt │ +│ ↓ │ +│ git push → Gitea │ +│ ↓ │ +│ .gitea/workflows/phpunit.yml │ ← CI-Verifikation +│ ├─ PHP + Composer install │ +│ └─ vendor/bin/phpunit │ +│ ↓ (Exit 0) │ +│ PR mergeable │ +└─────────────────────────────────────┘ +``` + +## Konsequenzen + +### Positiv + +- **Doppelte Absicherung:** Lokal (schnell) + CI (authoritativ) +- **Frühes Feedback:** Entwickler merkt sofort bei `git commit` statt erst nach Push +- **Performance:** Non-PHP-Commits (CSS, HTML, JS, Markdown) lösen keinen PHPUnit-Run aus +- **Wartbarkeit:** Single-Source-of-Truth (`scripts/pre-commit-checks.sh`) — Hook und safe-commit.sh synchron +- **CI-Laufzeit:** ~25s für 18 Tests, akzeptabel für Standard-Pipeline +- **Audit-Kette:** Issue → PR → Merge → autom. Issue-Close bleibt sauber + +### Negativ + +- **Wartungs-Overhead:** Bei neuen Test-Dateien (z.B. `tests/Integration/`) muss `phpunit.xml` ggf. angepasst werden +- **Pre-Commit-Delay:** Bei PHP-Commits ~5-10s lokaler Test-Lauf (akzeptabel, schneller als CI-Round-Trip) +- **Composer-Install-Falle:** Bei fehlendem `vendor/` wird `composer install` ausgeführt — potenziell langsam beim ersten Commit in neuem Clone +- **Bypass-Pfad:** `git commit --no-verify` überspringt Hook (per Design, aber Risiko) +- **Schutz gegen Bypass:** `scripts/safe-commit.sh` ruft `pre-commit-checks.sh` direkt auf (auch bei `--no-verify` würde der Bypass hier nicht greifen, da `safe-commit.sh` das Script direkt invoked) + +### Risiken + +- **PHP-Versions-Drift:** CI läuft auf PHP 8.5, lokal möglicherweise älter. Mitigation: `phpunit.xml` schema-konform, keine PHP-8.5-spezifischen Features in Tests +- **Test-Datenbank:** Aktuell keine DB-Tests, aber bei zukünftigen Integration-Tests muss SQLite-in-memory oder Test-Fixture sichergestellt werden +- **Composer-Versions-Drift:** CI nutzt neueste Composer-Version, lokal ggf. älter → `composer.lock` muss gepflegt sein +- **Stale-Index-Edge-Case:** `git add file.php; rm file.php; git commit` würde PHPUnit gegen veraltete staged-Version laufen lassen. Mitigation: Stale-Index-Safety-Check in `pre-commit-checks.sh` prüft Disk-Existenz aller gestaged PHP-Files + +## Alternativen (verworfen) + +### Alternative A: PHPUnit NUR in CI, kein Pre-Commit-Hook + +- **Pro:** Einfacher, kein lokaler Overhead +- **Pro:** Bypass unmöglich (`--no-verify` irrelevant) +- **Contra:** Feedback-Loop erst nach Push (30s+) +- **Contra:** Martin muss auf CI warten statt sofort beim Commit zu sehen +- **Verworfen weil:** Schnelleres Feedback-Loop wichtiger als Einfachheit + +### Alternative B: PHPUnit NUR lokal, keine CI-Pipeline + +- **Pro:** Schnellste lokale Feedback-Loop +- **Pro:** Keine CI-Infrastruktur nötig +- **Contra:** Kein Schutz vor `git commit --no-verify`-Bypass +- **Contra:** Kein Schutz vor ungetesteten Pushes direkt auf main +- **Verworfen weil:** CI-Protection vor versehentlichen Pushes essentiell + +### Alternative C: PHPUnit mit `npm test` statt direkter `vendor/bin/phpunit` + +- **Pro:** Konsistenz mit existierendem `npm run lint`-Pattern +- **Pro:** Lint + Test in einem Schritt möglich +- **Contra:** Zusätzlicher npm-Wrapper-Layer, Overhead +- **Contra:** PHP-Files würden trotzdem in npm-Skript laufen (unidiomatisch) +- **Verworfen weil:** Direkter `vendor/bin/phpunit` ist PHP-idiomatisch, klarer + +### Alternative D: PHPUnit in bestehende `lint.yml` integrieren + +- **Pro:** Weniger Workflow-Files +- **Contra:** Lint- und Test-Stage schwerer zu trennen +- **Contra:** Lint-Pipeline bricht bei Test-Fehler, obwohl Lint sauber ist +- **Verworfen weil:** Trennung der Verantwortlichkeiten (Lint = Syntax, Test = Verhalten) + +## Auswirkungen + +### Performance + +- **CI-Pipeline:** ~25s für 18 Tests, akzeptabel +- **Pre-Commit-Local:** ~5-10s bei PHP-Commits, <1s bei Non-PHP-Commits (Skip) +- **Composer-Install:** ~3-5s beim ersten Run nach Clone, dann Cache-Hit + +### Security + +- Keine Secrets in Test-Files +- Keine externen API-Calls in Tests +- Keine Production-DB-Zugriffe (alle Tests in-memory oder mit Test-Fixtures) + +### Testbarkeit + +- Bestehende Tests bleiben unverändert (TDD-konform: 18 Tests, 31 Assertions) +- PHPUnit-Konfiguration in `phpunit.xml` +- Test-Layout: `tests/Core/RouterTest.php` (Namespaces-Pattern `App\Tests\`) + +### Migrationspfad + +- Keine Migration nötig — additive Änderung +- Bestehende Commits funktionieren unverändert +- Hook aktiviert sich automatisch bei `npm install` (Husky 9 Standard) +- CI-Pipeline triggert beim ersten Push nach Merge + +### Rollback + +- **CI:** `.gitea/workflows/phpunit.yml` löschen → keine CI-Tests mehr +- **Pre-Commit:** `.husky/pre-commit` revertieren + `scripts/pre-commit-checks.sh` löschen +- **Atomic:** Jede Schicht unabhängig deaktivierbar + +## Verwandte Entscheidungen + +- ADR-002 (offen): Stylelint-Pattern-Fix für `./public/css/**/*.css` (Folge-Bug entdeckt beim Test der Pipeline) +- ADR-003 (offen): act_runner-Docker-Orchestrierung-Workaround (v0.6.1 startet keine Container) + +## Nachträgliche Dokumentation + +Dieser ADR wird **nachträglich** erstellt (Code-Phase bereits abgeschlossen, PRs gemerged), um: + +1. Die Architektur-Entscheidung für die Nachwelt festzuhalten +2. Den bewussten 2-Layer-Ansatz (lokal + CI) zu begründen +3. Als Template für zukünftige Test-Integrationen in anderen Projekten zu dienen + +**Lesson Learned:** ADRs sollten VOR der Code-Phase erstellt werden (Forward-Engineering). Nachträgliche Doku ist besser als keine, aber ein Auditor würde die Entscheidungs-Spur zwischen Issue-Erstellung und Implementation schwer nachvollziehen können. Für die nächsten Issues: ADR in Ph0.5 verbindlich, nicht erst in Ph8.