From a2468b2b21e8cc521a15527401b3931e4e0a2485 Mon Sep 17 00:00:00 2001 From: filzmann Date: Wed, 22 Jul 2026 19:15:02 +0200 Subject: [PATCH 1/5] docs: plan personal monthly duty export --- ROADMAP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index d75b279..ee24f56 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -9,7 +9,7 @@ Diese Datei bündelt geplante Erweiterungen und offene Produktentscheidungen. Ve ## Geplante Erweiterungen -- Dienstplanexport. +- Persönliche Monatsansicht „Alle meine Einsätze“ mit PDF-Export und optionaler Verbindung zu gängigen Kalendern. - Benachrichtigungen für relevante Planungs- und Statusänderungen. - Fachlich eindeutige Festschreibung eines Dienstplans. - Teambezogene Konfigurierbarkeit nur dort erweitern, wo konkrete Teams unterschiedliche Regeln benötigen. From 13388d0d89ea44b3eacdb1c1815df59c92b6fe03 Mon Sep 17 00:00:00 2001 From: filzmann Date: Sun, 26 Jul 2026 20:12:25 +0200 Subject: [PATCH 2/5] docs: sync local TDD workflow --- .agents/skills/test-driven-change/SKILL.md | 109 ++++++++++++++++++ .agents/skills/work-in-nextcloud-app/SKILL.md | 12 +- AGENTS.md | 42 +++---- 3 files changed, 135 insertions(+), 28 deletions(-) create mode 100644 .agents/skills/test-driven-change/SKILL.md diff --git a/.agents/skills/test-driven-change/SKILL.md b/.agents/skills/test-driven-change/SKILL.md new file mode 100644 index 0000000..6c148dd --- /dev/null +++ b/.agents/skills/test-driven-change/SKILL.md @@ -0,0 +1,109 @@ +--- +name: test-driven-change +description: Deliver observable behavior changes through a verified Red–Green–Refactor cycle. Use for new features, bug fixes, domain logic, permission changes, API behavior, data changes, and contract changes; do not use automatically for documentation-only, formatting, generated-file, or mechanical changes without testable behavior. +--- + +# Deliver a test-driven change + +## Establish the proof + +Before writing the test, record: + +- the domain invariant; +- the observable target behavior; +- the defect or regression the test must detect; +- the chosen test level; +- what the test proves and what it does not prove; +- relevant negative and boundary cases. + +Choose the lowest level that proves the real contract without replacing +observable behavior with implementation detail. Use a higher integration, +contract, smoke, or end-to-end level when the lower level cannot exercise the +relevant boundary truthfully. For a bug fix, make the smallest proof a +regression test. + +## Red + +1. Write the smallest meaningful test before changing production code. +2. Run that test and record the command, exit code, and concrete failure or + assertion difference. +3. Confirm that it fails for the expected domain reason: the target behavior + is missing or wrong. +4. Do not proceed to Green when: + - the test is immediately green and is not an explicitly justified + characterization test; + - it fails only because of infrastructure, syntax, fixture, or configuration + defects; + - it does not reach the target behavior; + - it checks only a mock call although observable behavior can be checked + meaningfully. + +Repair a defective test environment without adding the target production +behavior, then repeat Red. An immediately green test can preserve known +behavior as a characterization test, but is not evidence of a TDD Red step. + +## Green + +1. Make only the smallest production-code change needed to satisfy the test. +2. Add no speculative abstraction or unrelated refactoring. +3. Run the new test again. +4. Run the relevant existing regression suite. + +## Cover risk-specific evidence + +For permission changes, verify at least: + +- unauthorized access is rejected; +- authorized access succeeds; +- a foreign or manipulated object ID causes no data change. + +For database or migration changes, verify at least: + +- fresh installation on an empty schema; +- upgrade from at least the immediately relevant prior version; +- preservation or correct migration of existing data; +- no incomplete state after failure. + +For shared libraries or contracts, verify: + +- relevant provider and consumer contract tests; +- every affected dependent app; +- backward compatibility or an explicitly documented break. + +For write operations, verify: + +- the return value or HTTP result; +- the persisted state; +- absence of unwanted side effects; +- repeated execution when idempotency is relevant. + +These checks supplement applicable repository stop gates. They do not authorize +database, permission, cross-repository, or production changes. + +## Refactor + +Refactor only after the new test and relevant regression suite are green. +Change no domain behavior. Run the new test and relevant existing tests again +after refactoring. + +## Handle non-behavioral changes and deviations + +For documentation-only, formatting, generated-file, or mechanical changes +without meaningfully testable behavior, use the suitable deterministic syntax, +contract, generation, layout, link, or structure check instead of an artificial +TDD cycle. + +State and justify every deviation from the test-driven workflow. Do not treat a +test that was never observed red as TDD evidence. + +## Report + +Include: + +- the protected invariant and chosen test level; +- the initially red test and expected failure reason; +- the minimal implementation; +- executed focused, regression, and contract tests as applicable; +- concrete commands, results, and exit codes; +- remaining untested risks; +- justified deviations. diff --git a/.agents/skills/work-in-nextcloud-app/SKILL.md b/.agents/skills/work-in-nextcloud-app/SKILL.md index c06b2d3..35e0ff6 100644 --- a/.agents/skills/work-in-nextcloud-app/SKILL.md +++ b/.agents/skills/work-in-nextcloud-app/SKILL.md @@ -52,15 +52,23 @@ Stop immediately if production systems, Git history rewriting, new production de ## Test-first and coverage -- New development and bug fixes normally follow Red – Green – Refactor. Start a bug fix with a regression test and a refactoring with characterization tests. Develop domain logic, permissions, hierarchies, conflicts, and validation test-first. +- Use the locally available sibling skill `test-driven-change` for every new feature, bug fix, domain rule, permission change, API behavior, data change, or contract change. It is the sole detailed Red–Green–Refactor workflow; this section adds only Nextcloud-app test selection and coverage requirements. - API changes cover success, validation failure, and typical Allow/Deny cases. Cross-app contracts have provider and consumer contract tests. Use integration/DDEV tests for migrations and repository behavior when unit tests cannot represent the real contract. - Develop executable UI logic test-first; additionally cover layout, accessibility, and Nextcloud integration with suitable smoke or browser checks. -- Permitted test-first entry exceptions are time-boxed exploratory spikes, purely declarative text/metadata or trivial presentation changes, and hard-to-isolate Nextcloud integration where a broader integration test is more truthful. Discard spike code or characterize it before adoption; give declarative changes appropriate syntax, contract, layout, or visibility checks. +- Treat a time-boxed exploratory spike or hard-to-isolate Nextcloud integration as an explicitly justified deviation. Discard spike code or characterize it before adoption; choose the truthful broader integration level when isolation would hide the real contract. - Use the local fast entries named by `AGENTS.md`, normally `php tests/run.php` and `node tests/run-js.mjs`; dependency-light PHP smokes run in isolated processes. Run LocalBase and every affected consumer contract/smoke suite after a LocalBase contract change. - In local pre-production, app tests may use shared LocalBase test helpers through relative repository paths. Add heavier packaging/autoload structure or a larger test framework only when path handling, runners, assertions, mocks, or fixtures are materially duplicated or impair readability. - Known overall and app coverage must not decline unnoticed. Aim for at least 85 percent line coverage for new or materially changed executable code, report PHP and JavaScript separately, and fully cover security invariants regardless of percentages. Coverage is a warning and delivery indicator, not a substitute for meaningful assertions. - Do not prepare a commit or release with red relevant fast tests, contract tests, security checks, coverage gates, or delivery gates. +## Persistent state and migrations + +- Before implementing a feature that changes persistent domain objects, determine the complete state model: allowed and forbidden starting states, preconditions, target state, side effects, error states, retry or repetition behavior, and relevant concurrency conflicts. +- Do not expose unrestricted generic setters for status changes governed by domain transition rules. Encapsulate allowed transitions in the domain model or one clearly responsible application service and cover positive, negative, and failure cases. +- Before a database change that can encounter existing data, document the old and new schema, transformation rules, known existing-data variants, integrity conditions, transaction boundary, resumability, and rollback limits. +- Such a database change requires at least a fresh-install test, an upgrade test from the relevant previous version with synthetic existing data, domain data- and relationship-integrity checks, handling of invalid or contradictory legacy data, and an application test on the migrated schema. +- Never modify a published migration after the fact. Correct it with a new migration. + ## DDEV, Nextcloud, and hosting safety - Prefer local PHP/Node checks and batch DDEV checks. Control the shared DDEV project only from its documented `nextcloud-dev` root when that separate Parent workspace is actually available. diff --git a/AGENTS.md b/AGENTS.md index 5528407..abbd532 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,9 +42,10 @@ Offene Zielbereiche: ## DDEV -Die gemeinsame lokale Nextcloud-DDEV-Umgebung liegt ausserhalb dieses Repos: - - ~/projects/br-nextcloud-apps/nextcloud-dev +Die gemeinsame Nextcloud-DDEV-Umgebung wird aus dem dokumentierten +Parent-Unterverzeichnis `nextcloud-dev` gesteuert. Bei einem eigenständigen +Checkout ist der lokale DDEV-Pfad zuerst anhand der realen Umgebung zu +ermitteln. AdPlaner nutzt gemeinsame Basisbausteine aus der Hilfsapp `localbase`. In der lokalen Nextcloud muss `localbase` aktiviert sein, bevor AdPlaner vollstaendig lauffaehig ist. @@ -73,28 +74,18 @@ Die folgenden IDs sind initiale Standardwerte. Assistenzteam-Präfix, sichtbarer ## Architekturregeln -- Controller bleiben duenn. -- Fachlogik, Datenzugriff, Darstellung und Dateiablage werden getrennt. -- Wiederkehrende Logik wird nicht mehrfach in Controllern oder `main.js` dupliziert. -- Persistente Kernobjekte bekommen Modelle/DTOs oder Value Objects. -- Modelle/DTOs werden bei Neu- und Weiterentwicklungen in PHP und JavaScript einheitlich angefasst: `get(...)` fuer ein einzelnes Payload/Row/Objekt, `get_all([...])` fuer Listen, `toArray()` fuer Serialisierung und `save()` nur fuer wirklich persistierbare, store-gebundene Modelle. Nicht persistierbare DTOs duerfen `save()` bewusst mit klarer Fehlermeldung blockieren. -- Modell-Hydration wird von aussen ueber `get(...)` und `get_all([...])` aufgerufen. Hilfsmethoden wie `fromArray` oder `fromRow` bleiben, falls noetig, interne/protected Implementierungsdetails und sind keine oeffentliche Modell-API. -- Neue Modellarbeit fuehrt keine neuen `fromApi`-/`toApi`-Kompatibilitaetsaliase ein. Bestehende PHP-`toApiArray()`-Call-sites duerfen schrittweise auf `toArray()` migriert werden, wenn die betroffene Schicht ohnehin angefasst wird. -- Datenzugriffe laufen ueber Repository-, Store- oder Service-Klassen. -- Services arbeiten bevorzugt mit Modellen/DTOs statt rohen Arrays. -- Groessere HTML-Bloecke werden aus `templates/index.php` in Partials ausgelagert. -- Wiederkehrende Frontend-Logik wird in `js/components/`, `js/modules/` oder `js/repositories/` ausgelagert. -- JavaScript wird gut gekapselt, wiederverwendbar und weitgehend objektorientiert strukturiert. API-Zugriffe gehoeren in Repositories/API-Adapter, Daten in Modelle/ViewModels, Workflows in kleine Services/Controller und Rendering/Eventbindung in Komponenten. -- DRY und KISS gelten gemeinsam: echte Duplizierung wird entfernt, aber einfache Lesbarkeit und klare AdPlaner-Fachgrenzen bleiben wichtiger als fruehe generische Abstraktionen. -- Gemeinsame UI-Helfer oder Komponenten werden erst nach `localbase` verschoben, wenn sie in mindestens zwei Apps dieselbe Semantik, dieselben Zustaende, Events und Accessibility-Regeln haben. -- Fehler werden zentral protokolliert; Nutzer*innen erhalten sichere, knappe Meldungen ohne interne Details. -- Keine Architekturabstraktion wird vorsorglich gebaut. Auslagerung erfolgt, wenn sie konkrete Duplizierung, Testbarkeit oder Wartbarkeit verbessert. - -Diese Regeln gelten sinngemaess auch fuer andere eigene Nextcloud-Apps; die fachlichen Anwendungsfaelle bleiben aber getrennt. - -## Learnings pflegen - -### Gemeinsame Suite-Navigation +- Der lokale Skill `work-in-nextcloud-app` ist die kanonische Quelle für + gemeinsame Schichtungs-, Modell-, Sicherheits-, UI- und Testregeln. +- Teambezogene Schichtkonfiguration bleibt ein AdPlaner-Fachvertrag und wird + durch die zuständige EB gepflegt; sie wird nicht in eine allgemeine + Suite-Einstellung verschoben. +- AdPlaner-spezifische API-Pfade, Modelle, Workflows und Darstellung bleiben + in diesem Repository. +- Gemeinsame Bausteine werden erst nach LocalBase verschoben, wenn mindestens + zwei Apps denselben semantischen und testbaren Vertrag benötigen. +- WordPress-Kompatibilität und parallele Urlaubspersistenz sind unzulässig. + +## Verbindliche Navigation und optionale Integration - Ohne aktive OrgSuite registriert AdPlaner einen eigenen Nextcloud-Hauptnavigationseintrag. Ab zwei AD-Produkten ersetzt `orgsuite` diesen durch den gemeinsamen Einstieg `AD`. - Das Template stellt den optionalen Menühost mit `data-suite="ad"` und `data-current-app="adplaner"` bereit, lädt aber keine OrgSuite-Assets direkt. @@ -102,7 +93,6 @@ Diese Regeln gelten sinngemaess auch fuer andere eigene Nextcloud-Apps; die fach - Team- und Planungsrechte bleiben ausschliesslich serverseitig im AdPlaner; Menuesichtbarkeit ist keine Berechtigung. - Der deckende Hintergrund und das vertikale Scrolling liegen am App-Root `#adplaner-app`; globale Nextcloud-Container wie `#content` werden nicht ueberschrieben. -- App-spezifische Kandidaten zielen auf diese Datei; app-uebergreifende Kandidaten werden dem Parent nur als unverbindlicher Vorschlag berichtet. Bewertung und Freigabe folgen dem lokalen Skill `work-in-nextcloud-app`. ## Tests From 7693e7671c93650b8ab5ce50e33c58759942c592 Mon Sep 17 00:00:00 2001 From: filzmann Date: Sun, 26 Jul 2026 21:30:08 +0200 Subject: [PATCH 3/5] docs: align local TDD risk checks --- .agents/skills/test-driven-change/SKILL.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.agents/skills/test-driven-change/SKILL.md b/.agents/skills/test-driven-change/SKILL.md index 6c148dd..fff4b71 100644 --- a/.agents/skills/test-driven-change/SKILL.md +++ b/.agents/skills/test-driven-change/SKILL.md @@ -55,13 +55,17 @@ For permission changes, verify at least: - unauthorized access is rejected; - authorized access succeeds; -- a foreign or manipulated object ID causes no data change. +- a foreign or manipulated object ID grants no access and causes no data + change; +- a rejected request changes no data; +- UI visibility is never used as a substitute for server-side access control. For database or migration changes, verify at least: - fresh installation on an empty schema; - upgrade from at least the immediately relevant prior version; - preservation or correct migration of existing data; +- required constraints and indexes exist and preserve integrity; - no incomplete state after failure. For shared libraries or contracts, verify: From 6f5c2854435ed59ebaa93a71359375d7c9f1fab9 Mon Sep 17 00:00:00 2001 From: filzmann Date: Mon, 27 Jul 2026 13:44:10 +0200 Subject: [PATCH 4/5] test: characterize access and configuration boundaries --- tests/Service/ShiftConfigServiceSmokeTest.php | 26 ++++++++++++++++ tests/Service/TeamAccessServiceSmokeTest.php | 31 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/tests/Service/ShiftConfigServiceSmokeTest.php b/tests/Service/ShiftConfigServiceSmokeTest.php index 5b5c7d6..b0047f0 100644 --- a/tests/Service/ShiftConfigServiceSmokeTest.php +++ b/tests/Service/ShiftConfigServiceSmokeTest.php @@ -61,4 +61,30 @@ assertSameValue(28, count($days), 'February 2026 should have 28 days.'); assertSameValue('2026-02-01', $days[0]['date'], 'First month day should be correct.'); +$assertInvalidArgument = static function (callable $operation, string $message): void { + try { + $operation(); + } catch (\InvalidArgumentException) { + return; + } + + throw new \RuntimeException($message); +}; +$assertInvalidArgument( + static fn() => $service->normalize(['shifts' => 'invalid']), + 'Non-list shift settings should be rejected.' +); +$assertInvalidArgument( + static fn() => $service->monthDays('2026-13'), + 'Out-of-range months should be rejected.' +); +$assertInvalidArgument( + static fn() => $service->normalizeDate('2026-02-30'), + 'Impossible calendar dates should be rejected.' +); +$assertInvalidArgument( + static fn() => $service->normalizeDate('30.02.2026'), + 'Non-ISO calendar dates should be rejected.' +); + echo 'AdPlaner shift config smoke tests passed' . PHP_EOL; diff --git a/tests/Service/TeamAccessServiceSmokeTest.php b/tests/Service/TeamAccessServiceSmokeTest.php index 6ee7519..a89dc8d 100644 --- a/tests/Service/TeamAccessServiceSmokeTest.php +++ b/tests/Service/TeamAccessServiceSmokeTest.php @@ -146,9 +146,29 @@ public function settingsForTeam(string $teamCode): TeamSettings { assertSameValue(['Alice Assistenz', 'Bob EB'], array_map(static fn($assistant): string => $assistant->displayName, $team->assistants()), 'Assistants should be sorted by display name.'); assertSameValue(false, $team->assistantByUid('bob')->canReceiveShifts, 'EB users should not receive shifts.'); assertSameValue(['alice' => 'Alice Assistenz', 'bob' => 'Bob EB'], $service->assistantLabelMap($team->assistants()), 'Assistant label maps should expose display names by uid.'); + assertSameValue( + ['carla' => 'Carla Assistenz'], + $service->assistantLabelMap([['uid' => 'carla', 'displayName' => 'Carla Assistenz']]), + 'Assistant label maps should normalize array input.' + ); + assertSameValue( + [ + 'teamGroupPrefix' => 'ad-ASN-', + 'teamLabelPrefix' => 'Assistenzteam', + 'teamCodeMaxLength' => 16, + 'coordinatorGroupId' => 'ad-EB', + 'coordinatorLabel' => 'Einsatzbegleitung', + ], + $service->organizationContract(), + 'The public organization contract should expose the shared defaults.' + ); $service->assertCanCoordinate('TeamB'); $service->assertAssistantInTeam('TeamB', 'alice'); + assertDomainException( + static fn() => $service->assertAssistantInTeam('TeamB', 'missing'), + 'Assistants outside the selected team should be rejected.' + ); assertDomainException( static fn() => $service->assertTeamAccess('Missing'), 'Missing teams should not be accessible.' @@ -167,6 +187,17 @@ public function settingsForTeam(string $teamCode): TeamSettings { $session->setUser(null); assertSameValue([], $service->teamsForCurrentUser(), 'Anonymous sessions should not expose teams.'); + assertSameValue(false, $service->currentUserIsEbForTeam('TeamB'), 'Anonymous sessions should not receive EB rights.'); + assertDomainException( + static fn() => $service->assertTeamAccess('TeamB'), + 'Anonymous sessions should not access an existing team.' + ); + try { + $service->currentUserId(); + throw new RuntimeException('Anonymous sessions received a user id.'); + } catch (RuntimeException $error) { + assertSameValue('Nicht angemeldet.', $error->getMessage(), 'Anonymous sessions should fail closed.'); + } echo 'TeamAccessService smoke tests passed' . PHP_EOL; } From 8469959e916f1905e28c17a838e11027da406fbb Mon Sep 17 00:00:00 2001 From: filzmann Date: Mon, 27 Jul 2026 13:44:14 +0200 Subject: [PATCH 5/5] ci: enforce PHP and JavaScript coverage baselines --- .github/workflows/tests.yml | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2170c07..76644ec 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -46,11 +46,24 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - coverage: none + coverage: ${{ matrix.php-version == '8.3' && 'xdebug' || 'none' }} tools: none - name: PHP-Tests + if: matrix.php-version == '8.5' working-directory: app run: php tests/run.php + - name: PHP-Coverage-Tooling installieren + if: matrix.php-version == '8.3' + run: composer install --working-dir=localbase/tests/coverage --no-interaction --no-progress --prefer-dist + - name: PHP-Coverage + if: matrix.php-version == '8.3' + working-directory: app + run: | + mkdir -p "$RUNNER_TEMP/php-coverage" + PHP_COVERAGE_COMMAND="$GITHUB_WORKSPACE/localbase/tests/coverage/vendor/bin/phpcov" \ + PHP_COVERAGE_OUTPUT_DIR="$RUNNER_TEMP/php-coverage" \ + php tests/run.php + php "$GITHUB_WORKSPACE/localbase/tests/coverage/merge-clover.php" adplaner "$RUNNER_TEMP/php-coverage" 44.47 javascript: name: JavaScript @@ -82,6 +95,14 @@ jobs: with: node-version: 24 package-manager-cache: false - - name: JavaScript-Tests + - name: JavaScript-Coverage-Tooling installieren + run: npm ci --prefix localbase/tests/coverage --ignore-scripts + - name: JavaScript-Tests mit Coverage working-directory: app - run: node tests/run-js.mjs + run: | + ../localbase/tests/coverage/node_modules/.bin/c8 \ + --all \ + '--include=js/**/*.js' \ + --check-coverage \ + --lines=86.47 \ + node tests/run-js.mjs