Files
schalli f1017fa6e9
Tessera CI/CD / Lint & Type Check (push) Successful in 47s
Tessera CI/CD / Tests (push) Successful in 53s
Tessera CI/CD / Build & Publish Images (push) Successful in 28s
docs(quick-260911-cwh): Etappe 2 Bereich calendar abgeschlossen und verifiziert
2026-09-11 10:08:01 +02:00

308 lines
17 KiB
Markdown

---
phase: quick-260911-cwh
verified: 2026-09-11T10:15:00Z
status: passed
score: 12/12 must-haves verified
covered_files:
- ".planning/WINDOWS.md"
- ".planning/quick/260911-cwh-mandantentrennung-etappe-2-bereich-calen/260911-cwh-PLAN.md"
- ".planning/quick/260911-cwh-mandantentrennung-etappe-2-bereich-calen/260911-cwh-SUMMARY.md"
- "apps/api/scripts/rls-scratch-check.mjs"
- "apps/api/src/calendar/calendar.controller.ts"
- "apps/api/src/calendar/calendar.service.spec.ts"
- "apps/api/src/calendar/calendar.service.ts"
- "docs/mandantentrennung-etappe2-fehlerrichtung.md"
- "docs/mandantentrennung-zugriffsklassifikation.md"
covered_digest: "v1:sha256:f092c2eea8be58064da21108d78ef37879ab4183bdc6c622c699cee603c0f434"
behavior_unverified: 0
overrides_applied: 0
---
# Quick Task 260911-cwh: Mandantentrennung Etappe 2, Bereich `calendar` — Verification Report
**Task Goal:** Bind all 12 `calendarSource` access sites in `calendar.service.ts`, create the area's missing spec coverage, pin the credential-path exoneration and the cache-key verdict as tests, and keep the classification document's five hand-maintained sections in sync.
**Verified:** 2026-09-11T10:15:00Z
**Status:** passed
**Commits reviewed:** bf5fc4d, 77cb124, e0e163e, 06038b9 (base 508d9e4), all present in `git log`, working tree clean before and after this review.
## Re-verification Checklist Results
### 1. Coverage — all 12 sites bound, one `tenantPrisma` per method, six methods
Independently counted (not taken from SUMMARY):
```
grep -ro "tenantPrisma\.calendarSource\." apps/api/src/calendar/calendar.service.ts | wc -l → 12
grep -ro "this\.prisma\.[a-zA-Z]*" apps/api/src/calendar/calendar.service.ts | wc -l → 0
grep -o "forTenant(this\.prisma" (non-comment source) → 6
```
Distribution matches the plan's Befund A exactly: `getSources` (1), `addSource` (1),
`updateSource` (2), `deleteSource` (2), `testConnection` (3),
`fetchAndCacheEvents` (3) = 12. `aggregateEvents`/`refreshCacheInBackground`
create no client of their own (confirmed by reading both bodies — they only
pass `tenantId` through to `fetchAndCacheEvents`).
**VERIFIED.**
### 2. Credential exoneration pinned, not asserted
Three test cases exist in `calendar.service.spec.ts` (lines 289, 303, 313):
"Feld FEHLT" (missing), "Feld LEER" (empty → `null`), "Feld GESETZT" (set →
re-encrypted). The "Feld FEHLT" case asserts `crypto.decrypt`/`crypto.encrypt`
were **not called** and that `update(...).data` does **not** have an
`encryptedPassword` key at all — this is a real pin, not a shape check. A
regression that reintroduced the `dkv` read-decrypt-re-encrypt form would
fail this test on both the decrypt-not-called assertion and the
data-shape assertion.
**VERIFIED.**
### 3. Cache-key verdict
Code comment directly above `eventCache = new Map(...)` in
`calendar.service.ts` (lines 125-142) states the full four-link chain
(`User.id @id @default(uuid())` → `auth.service.ts` `sub: user.id` →
`JwtStrategy.validate` `id: payload.sub` → `extractContext`) and concludes
the key stays without a tenant component because Etappe-3-Entscheidung (1)
only touches `username`/`email`, not `id`. Independently confirmed against
`apps/api/prisma/schema.prisma:30` (`id String @id @default(uuid())`),
`apps/api/src/auth/auth.service.ts:143,332` (`sub: user.id`), and
`apps/api/src/auth/strategies/jwt.strategy.ts:29` (`id: payload.sub`) — the
chain in the comment matches the actual source. The identical verdict is
also written in `docs/mandantentrennung-etappe2-fehlerrichtung.md` (k4)(a),
naming the same four links.
**VERIFIED.**
### 4. Both write-backs in `fetchAndCacheEvents` bound and pinned
Success path (line 429, inside the `try`) and catch path (line 440, inside
the `catch`) both call `tenantPrisma.calendarSource.update(...)`. Two named
tests cover this (`aggregateEvents, Erfolgspfad...` line 428,
`aggregateEvents, Fehlerpfad (Providerfehler)...` line 439), both asserting
`expectBoundCall(..., 'update')`.
**Falsification performed by this verifier** (not just re-reading the
SUMMARY's claim): reverted the success-path binding
(`tenantPrisma.calendarSource.update` → `this.prisma.calendarSource.update`
at line 429) and ran `npm --prefix apps/api run test -- src/calendar/calendar.service.spec.ts`.
Result: 1 of 23 tests failed —
`aggregateEvents, Erfolgspfad: ... → AssertionError: erwarteter gebundener Aufruf calendarSource.update(tenant=t1) fehlt im Protokoll: [{"tenantId":"t1","model":"calendarSource","method":"findMany"}]`
— exactly the failure mode described in the SUMMARY's own falsification
proof #1. Reverted the change; re-ran the same test file: 23/23 green.
Working tree confirmed clean afterward (`git status --short` empty).
**VERIFIED** (independently reproduced, not merely re-read).
### 5. Ownership checks survived
`updateSource` (line 221), `deleteSource` (line 273), `testConnection`
(line 289) all still compare `existing.userId !== userId` /
`source.userId !== userId` against the session-derived `userId` parameter
and throw `ForbiddenException`. Three ForbiddenException test cases and
three NotFoundException test cases exist and pass.
**VERIFIED.**
### 6. No conflict translation added
`grep` over `calendar.service.ts` shows no `P2002`/unique-constraint
handling anywhere; the only Prisma-error-sensitive code is the generic
`catch` blocks in `testConnection`/`fetchAndCacheEvents`, which existed
before this plan and are unrelated to uniqueness. Matches Befund H (no
uniqueness chain on `CalendarSource` besides the client-generated UUID PK).
**VERIFIED.**
### 7. Frontend NOT touched
`git diff --name-only 508d9e4 HEAD` and `git diff --name-only 50b3a36 HEAD`
both show zero files under `apps/web`. The silent-empty-state behaviour is
recorded, not fixed: `docs/mandantentrennung-etappe2-fehlerrichtung.md` (k3)
names `calendar-widget.tsx`/`calendar-settings-panel.tsx`/`calendar-source-form.tsx`
by file and line, and `.planning/WINDOWS.md` entry #26 (open, table row +
JSON block both present) describes the same silent-empty-state defect with
"das Frontend wird von 260911-cwh NICHT geaendert" stated explicitly.
**VERIFIED.**
### 8. Generated-client measurements — committed and honest
Re-ran `apps/api/scripts/rls-scratch-check.mjs` live against the running
`tessera-ctl-db-1` container (freshly resolved IP `172.19.0.2`, not reused
from any cached value):
```
DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}')
TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs
```
Exit code: 0. Final line: `Alle 101 Pruefungen bestanden.` — matches the
SUMMARY's claimed total (101). All 13 `calendarsource-*` named checks passed
(confirmed by name), including the 4 generated-client checks:
`calendarsource-generierter-client-gebundene-quellenliste-nur-eigener-mandant`,
`calendarsource-generierter-client-ungebundene-quellenliste-null-zeilen`,
`calendarsource-generierter-client-gebundenes-update-auf-unsichtbare-zeile-scheitert-laut`,
`calendarsource-generierter-client-gebundenes-anlegen-eigener-mandant-gelingt`.
The runtime column-set comparison (`calendarsource-wegwerftabelle-deckt-alle-spalten-des-generierten-clients`)
actually executed and printed both sets: schema.prisma yields 17 fields,
the scratch table's `information_schema.columns` yields the same 17 fields,
sorted identically — this is a real comparison, not a hardcoded pass.
Call-order gate confirmed: `runCalendarAreaChecks` is invoked after
`runDashboardAreaChecks` and before `runTransactionShapeMeasurement` in
`main()` (source inspection, line 3335).
**VERIFIED.**
### 9. Five hand-maintained sections — class distribution recomputed independently
Recomputed the class distribution directly from the Bestandsaufnahme rows
with an independent `awk` one-liner (not copy-pasted from the plan's gate):
```
muss-mandantengebunden: 31
keine-mandantengebundene-tabelle: 17
beides: 13
bewusst-uebergreifend: 2
TOTAL: 63
```
Matches the documented table exactly (31/17/13/2, Summe 63, heading "63
Paare"). Also recomputed the overview table's column sums across all 12
area rows (tenders 35/27, groups 0/31, ldap 4/26, dkv 1/22, user 8/14,
module-registry 7/10, dashboard 1/12, auth 8/5, calendar 0/12, tenant 8/0,
favorites 7/0, settings 4/0) → 83/159, matching the documented **Summe**
row. The `calendar` row itself reads `0 | 12` with the required
`**war 12/0**` marker and explicit no-remaining-unbound justification. The
"Stand 260911-cwh" unchanged-marker paragraph is present under
`## Klassen-Verteilung`. The background-service section header reads
"fünf Fälle" (matching its own bullet count) and contains a plain paragraph
naming `refreshCacheInBackground` and `calendar` without adding a sixth
bullet-point case (confirmed: no new `- **\`...\`**` entry and no "Der ...
Fall, anderer Bauart" line was added for this area). `## Was diese Etappe
NICHT entscheidet` mentions `calendar`. WINDOWS #26 present in table row,
JSON block, `open` status, and header counters (`total_count: 26` = 26
table rows, `open_count: 8` = 8 rows with `| open |`, both independently
recomputed and matching).
**VERIFIED (all five sections, independently recomputed, not re-read from
SUMMARY).**
### 10. Falsification proofs — three claimed
1. **Aggregation write-back binding revert** — independently reproduced by
this verifier (see item 4 above). Confirmed identical failure mode and
message pattern to the one quoted in the SUMMARY.
2. **Bestandsaufnahme `Stand` mismatch** — mechanism confirmed present:
`apps/api/src/prisma/rls-access-inventory.spec.ts:321` contains the exact
assertion template `Abweichender Stand (Dokument vs. Quelltext):` that
the SUMMARY's quoted failure message is built from. Not independently
re-triggered (would require editing and reverting the classification doc
under time budget), but the underlying gate genuinely exists and matches
the described mechanism precisely — not a fabricated quote.
3. **Uebersichtszeile wrong-number gate** — the awk gate quoted in the
SUMMARY (`grep -qE "^\| calendar \| ${DU} \| ${DB} \| \*\*war 12/0\*\*"`)
is present verbatim in the plan's Aufgabe 3 `<verify>` block, which
executed successfully as part of the task-3 commit's automated gate (the
task would not have committed otherwise, since these are `autonomous:
true` plans with gated commits).
**VERIFIED** (one directly reproduced by this verifier, two confirmed as
genuinely-wired mechanisms matching the quoted evidence, not fabricated).
### 11. Constraints held
- **Allow-list scope against `50b3a36`:** `git diff --name-only 50b3a36 HEAD`
shows exactly the 7 files in `files_modified` (`rls-scratch-check.mjs`,
`mandantentrennung-etappe2-fehlerrichtung.md`,
`calendar.service.spec.ts`, `calendar.service.ts`,
`calendar.controller.ts`, `mandantentrennung-zugriffsklassifikation.md`,
`.planning/WINDOWS.md`) plus `.planning/STATE.md` and the plan/summary
files themselves under `.planning/`. No `apps/api/prisma`, no
`apps/web`, no compose/env file.
- **Providers not exercised against real endpoints:** confirmed —
`icsProvider`/`caldavProvider`/`exchangeProvider` are `vi.fn()` mocks
throughout the spec file; no network call is made.
- **Switch OFF:** no compose/env file in the diff; nothing under
`apps/api/prisma` changed (`git diff --name-only 50b3a36 -- apps/api/prisma`
is empty).
**VERIFIED.**
## Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | All 12 `calendarSource` accesses bound via `tenantPrisma`, one client per method (6 methods) | ✓ VERIFIED | Independent grep count: 12 bound, 0 unbound, 6 `forTenant()` call sites |
| 2 | Ownership checks (`updateSource`/`deleteSource`/`testConnection`) read+write over the same bound client, pinned as tests | ✓ VERIFIED | Source read + 2 tests per path (ForbiddenException/NotFoundException) + `expectBoundCall` pairs |
| 3 | Reverse error direction measured against the real post-20260910120000 rule, ≥4 checks via generated client on a schema-matching scratch table | ✓ VERIFIED | Live tool run: 101/101, 13 named `calendarsource-*` checks, 4 via generated client, column-set comparison executed with real 17/17 match |
| 4 | Reverse error direction's silent-empty shape named, including frontend swallowing | ✓ VERIFIED | (k3) names both backend spots and 3 frontend files; WINDOWS #26 open entry |
| 5 | Credential-preservation question answered by measurement, pinned as 3 tests | ✓ VERIFIED | 3 tests at lines 289/303/313, one asserting decrypt/encrypt NOT called |
| 6 | Cache-key verdict, 4-link chain, written in code AND critique | ✓ VERIFIED | Comment above `eventCache`, (k4)(a) in critique doc, chain matches schema/auth source |
| 7 | Ownership checks read (not assumed), kept, pinned | ✓ VERIFIED | Same as #2 |
| 8 | Test suite created from nothing, two-client proof, providers not exercised | ✓ VERIFIED | 23 test cases, `__makeBoundClient`, providers are `vi.fn()` |
| 9 | Controller passes resolved tenant through to all 5 (of 6) previously-discarding handlers | ✓ VERIFIED | 6/6 handlers destructure `{ userId, tenantId }`, 0 handlers take `userId` alone |
| 10 | Five hand-maintained classification sections in sync, allow-list gated | ✓ VERIFIED | Independently recomputed sums/class distribution match exactly |
| 11 | Baseline held at end of every task | ✓ VERIFIED (via orchestrator measurement) | 883/883 tests, 57 files, type-check clean — independently measured by orchestrator per task instructions |
**Score:** 12/12 truths verified (0 present-but-behavior-unverified).
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `apps/api/scripts/rls-scratch-check.mjs` | 11th section `runCalendarAreaChecks`, ≥12 named checks, ≥4 via generated client | ✓ VERIFIED | 13 named checks in normal path, 4 generated-client; live-run confirmed |
| `docs/mandantentrennung-etappe2-fehlerrichtung.md` | `## Bereich calendar` with (k1)-(k5) | ✓ VERIFIED | All 5 subsections present, content spot-checked |
| `apps/api/src/calendar/calendar.service.spec.ts` | New, two-client proof, mocks for crypto+3 providers | ✓ VERIFIED | 23 tests, `vi.mock` on `prisma-tenant.extension`, `makeFakeCrypto`, `makeFakeProviders` |
| `apps/api/src/calendar/calendar.service.ts` | All 12 accesses bound, cache-key verdict as comment | ✓ VERIFIED | 12/12 bound, comment present and accurate |
| `apps/api/src/calendar/calendar.controller.ts` | 5 discarding handlers pass tenant through | ✓ VERIFIED | 6/6 handlers pass `{ userId, tenantId }` |
| `docs/mandantentrennung-zugriffsklassifikation.md` | Bestandsaufnahme, overview, sum, class distribution, background-service section, "was NICHT entscheidet" | ✓ VERIFIED | All independently recomputed and matched |
| `.planning/WINDOWS.md` | New open entry via `gsd-tools windows append` | ✓ VERIFIED | Entry #26, table+JSON+counters consistent |
### Key Link Verification
| From | To | Via | Status | Details |
|------|-----|-----|--------|---------|
| `calendar.controller.ts extractContext` | `CalendarService` methods | Destructured `{ userId, tenantId }` passed as args | ✓ WIRED | All 6 handlers |
| `fetchAndCacheEvents` success write-back | `tenantPrisma.calendarSource.update` | Same client as the `findMany` load | ✓ WIRED | Falsified and restored by this verifier |
| `fetchAndCacheEvents` catch write-back | `tenantPrisma.calendarSource.update` | Same client as the `findMany` load | ✓ WIRED | Test exists, source confirmed |
| `eventCache` key | `User.id` via `extractContext`→`JwtStrategy`→`auth.service.ts`→`schema.prisma` | Comment chain | ✓ WIRED | All 4 links independently checked against source |
### Behavioral Spot-Checks
| Behavior | Command | Result | Status |
|----------|---------|--------|--------|
| Reverting one write-back binding breaks exactly the named test | Edited line 429, ran `vitest run src/calendar/calendar.service.spec.ts`, restored | 22 passed, 1 failed with quoted message; then 23/23 after restore | ✓ PASS |
| `rls-scratch-check.mjs` passes live against running DB | `TESSERA_SCRATCH_ADMIN_URL=... node apps/api/scripts/rls-scratch-check.mjs` | exit 0, "Alle 101 Pruefungen bestanden." | ✓ PASS |
### Anti-Patterns Found
None. Grepped modified files for `TBD|FIXME|XXX|TODO|HACK|PLACEHOLDER` and empty-return stubs — no matches beyond pre-existing, unrelated code.
### Human Verification Required
None. All must-haves resolved to VERIFIED via direct source inspection, independent recomputation, and live tool execution (including one directly reproduced falsification).
### Gaps Summary
None found. Working tree is clean; all commits (bf5fc4d, 77cb124, e0e163e,
06038b9) are present in `git log` on `main`, in the correct order, on top of
base `508d9e4`. Scope is allow-listed against `50b3a36` with zero
unexpected files. The one minor documentation wrinkle — the plan's Aufgabe 2
carries `tdd="true"` while the SUMMARY states "Kein TDD-Modus fuer diesen
Plan" under "Deviations from Plan: None" — is a self-contradiction inside
the SUMMARY's own prose (claims "executed exactly as written" while also
describing a TDD-flag deviation), but it has no bearing on the delivered
artifacts: the test file exists, is substantive, and all pins are real and
falsifiable as demonstrated above. Noted here for completeness, not raised
as a gap since it does not affect goal achievement.
---
*Verified: 2026-09-11T10:15:00Z*
*Verifier: Claude (gsd-verifier)*