153 lines
18 KiB
Markdown
153 lines
18 KiB
Markdown
---
|
|
phase: quick-260910-krx
|
|
verified: 2026-09-11T09:15:00Z
|
|
status: gaps_found
|
|
score: 10/11 must-haves verified
|
|
covered_files:
|
|
- ".planning/WINDOWS.md"
|
|
- ".planning/quick/260910-krx-mandantentrennung-etappe-2-bereich-dashb/260910-krx-PLAN.md"
|
|
- ".planning/quick/260910-krx-mandantentrennung-etappe-2-bereich-dashb/260910-krx-SUMMARY.md"
|
|
- "apps/api/scripts/rls-scratch-check.mjs"
|
|
- "apps/api/src/dashboard/dashboard.controller.ts"
|
|
- "apps/api/src/dashboard/dashboard.service.spec.ts"
|
|
- "apps/api/src/dashboard/dashboard.service.ts"
|
|
- "docs/mandantentrennung-etappe2-fehlerrichtung.md"
|
|
- "docs/mandantentrennung-zugriffsklassifikation.md"
|
|
covered_digest: "v1:sha256:76946e6dc0468eaade2fe79aaa847c342e94364af5040814f495ba1e2c460d09"
|
|
behavior_unverified: 0
|
|
overrides_applied: 0
|
|
gaps:
|
|
- truth: "Die gemessene Konfliktbehandlung in saveLayout (PrismaClientUnknownRequestError statt P2002) ist im committeten Werkzeug reproduzierbar belegt und durch einen Testfall abgesichert."
|
|
status: partial
|
|
reason: >
|
|
Die im SUMMARY und in docs/mandantentrennung-etappe2-fehlerrichtung.md
|
|
(Zeilen 1812-1831) als "zentrale Abweichung vom tenders-Muster"
|
|
dargestellte Kernaussage — dass `tenantPrisma.dashboardLayout.upsert()`
|
|
am ECHTEN, generierten Prisma Client tatsaechlich einen
|
|
`PrismaClientUnknownRequestError` wirft, nicht `PrismaClientKnownRequestError`
|
|
mit `.code === 'P2002'` — ist NICHT im committeten `rls-scratch-check.mjs`
|
|
abgebildet. Die dortige Pruefung Nr. 5
|
|
(`dashboardlayout-gebundener-konfliktschreibvorgang-auf-unsichtbare-zeile-scheitert-laut`,
|
|
Zeile 2586-2599 des Skripts) ruft ausschliesslich `tx.$executeRaw` mit
|
|
handgeschriebenem `INSERT ... ON CONFLICT ("userId") DO UPDATE` auf —
|
|
an keiner Stelle des Skripts wird `.upsert()` ueber den generierten
|
|
Prisma Client aufgerufen (grep nach `dashboardLayout.upsert` im ganzen
|
|
Skript: 0 Treffer). Die staerkere, spezifischere Behauptung (generierter
|
|
Client, andere Prisma-Fehlerklasse als P2002) stammt laut Dokument aus
|
|
einem separaten, nicht committeten Testaufbau ("eigens dafuer angelegte
|
|
Wegwerf-Datenbank... eigene Wegwerf-Rolle ohne BYPASSRLS") und ist damit
|
|
nicht unabhaengig nachvollziehbar. Zusaetzlich: kein Testfall in
|
|
`dashboard.service.spec.ts` exercised den `catch`-Zweig von `saveLayout`
|
|
(`grep -n "ConflictException\|PrismaClientUnknownRequestError"
|
|
dashboard.service.spec.ts` — 0 Treffer) — ein Refactoring, das den
|
|
`catch`-Block entfernt oder auf `.code === 'P2002'` umstellt, wuerde von
|
|
keinem Test erkannt.
|
|
artifacts:
|
|
- path: "apps/api/scripts/rls-scratch-check.mjs"
|
|
issue: "Pruefung 5 misst nur rohes SQL ($executeRaw), nicht den generierten Prisma-Client-Aufruf .upsert(), der in dashboard.service.ts tatsaechlich verwendet wird."
|
|
- path: "apps/api/src/dashboard/dashboard.service.spec.ts"
|
|
issue: "Kein Testfall ruft saveLayout mit einem Mock auf, der PrismaClientUnknownRequestError wirft, und prueft die Uebersetzung in ConflictException."
|
|
missing:
|
|
- "Entweder: Erweiterung von rls-scratch-check.mjs um eine Pruefung, die tatsaechlich prisma.dashboardLayout.upsert() (generierter Client) gegen die Konfliktzeile aufruft und die beobachtete Fehlerklasse (err.constructor.name bzw. instanceof-Pruefung) protokolliert."
|
|
- "Oder: ein Unit-Test in dashboard.service.spec.ts, der tenantPrisma.dashboardLayout.upsert im Fake mit einem Prisma.PrismaClientUnknownRequestError-Wurf versieht und erwartet, dass saveLayout eine ConflictException wirft — analog zum bestehenden Wachhund-Muster dieser Datei."
|
|
- "Reproduzierbarer Beleg (Skript oder Test), der im Repository landet, statt einer nur im Dokumentationstext behaupteten, einmaligen Ad-hoc-Messung."
|
|
---
|
|
|
|
# Quick Task 260910-krx: Mandantentrennung Etappe 2, Bereich `dashboard` — Verification Report
|
|
|
|
**Task Goal:** Bind the tenant-bound access sites in `apps/api/src/dashboard/dashboard.service.ts`, leave the platform-wide module catalogue unbound with the measured reason, create the missing spec coverage, record the evidence-destroying loop, and leave the classification document's five hand-maintained sections in sync.
|
|
|
|
**Verified:** 2026-09-11T09:15:00Z
|
|
**Status:** gaps_found (one partial gap — see below; all other checked items pass)
|
|
**Re-verification:** No — initial verification
|
|
|
|
## Goal Achievement
|
|
|
|
### Observable Truths
|
|
|
|
| # | Truth | Status | Evidence |
|
|
|---|-------|--------|----------|
|
|
| 1 | 13 Datenbankzugriffe vollstaendig entschieden: 12 gebunden, 1 begruendet ungebunden, keiner unentschieden | ✓ VERIFIED | `git show c7d93f2:.../dashboard.service.ts \| grep -oE "this\.prisma\.[a-zA-Z]+" \| wc -l` = 13 (2 dashboardLayout, 1 module, 4 searchProvider, 6 widgetInstance) at base; current file: 12 `tenantPrisma.*` call sites over 9 `forTenant()` invocations + 1 `this.prisma.module.findMany` (unbound, catalogue). Zero occurrences of `tenantPrisma.module.` (negative gate confirmed by direct grep of the committed file). |
|
|
| 2 | Lesen/Schreiben derselben Tabelle nie auf gebunden/ungebunden aufgeteilt; getLayout/saveLayout gemeinsam gebunden; drei Besitzpruefungen laufen ueber denselben Klienten | ✓ VERIFIED | Read `dashboard.service.ts`: every method creates exactly one `const tenantPrisma = forTenant(this.prisma, tenantId)` and both queries of `updateWidgetConfig`, `removeWidget`, `removeSearchProvider` use that same `tenantPrisma`. Test "Anordnung lesen und speichern sind GEMEINSAM gebunden" and "keine Methode ... erzeugt mehr als EINEN gebundenen Klienten" pin this down; both pass (27/27). |
|
|
| 3 | Umgekehrte Fehlerrichtung an den echten Regeln nach Migration 20260910120000 gemessen, mit Signal je Pfad | ✓ VERIFIED | Re-ran `rls-scratch-check.mjs` live against `tessera-ctl-db-1` (172.19.0.2): "Alle 87 Pruefungen bestanden." (74 baseline + 13 new, matching the SUMMARY's claim exactly). `docs/mandantentrennung-etappe2-fehlerrichtung.md` `## Bereich dashboard` (w1)/(w2) present with per-path signal table. |
|
|
| 4 | Beweisvernichtende Schleife ausdruecklich benannt: in Kritikschrift UND offener WINDOWS-Eintrag mit Etappe-4-Vorabpruefung | ✓ VERIFIED | `(w3)` in fehlerrichtung.md describes the loop (empty dashboard → rebuild → auto-writeback → overwritten row → duplicate widgets) at the two actual web files. `.planning/WINDOWS.md` entry #25 present in both the markdown table and the JSON block, `status: open`, names the concrete Etappe-4 preflight check and cross-references #22. Frontend files (`apps/web/**`) confirmed untouched by `git diff --name-only c7d93f2..HEAD`. |
|
|
| 5 | Widerlegte SearchProvider-Praemisse eigenstaendig nachgeprueft, mit benannter Reichweite | ✓ VERIFIED | `(w1)` names the exact search instruction and scope limits (no dynamic model-name write path, no manual DB edit covered). Database-side defense reproduced live: `searchprovider-gebundenes-einfuegen-ohne-mandant-abgelehnt: bestanden` with SQLSTATE 42501. |
|
|
| 6 | Geerbte Entlastung (Modul-Zugriffsaufloesung bereits gebunden seit 260910-exd) nachgeprueft, nicht doppelt repariert | ✓ VERIFIED | `git diff --name-only c7d93f2..HEAD -- apps/api/src/module-registry/` returns nothing — no file under that path touched. `dashboard.service.ts` calls `this.moduleAccessService.getAccessibleModuleIds(...)` unchanged, with an explicit comment that it "already binds internally". |
|
|
| 7 | Modulkatalog bleibt ungebunden, MESSUNG und BEDINGUNG getrennt, beruft sich auf bestehende Werkzeugpruefung | ✓ VERIFIED | End-of-file comment block in `dashboard.service.ts` (lines 340-354) separates MESSUNG (`pg_class.relrowsecurity` false, cites `module-tabelle-traegt-keinen-zeilenschutz`) from BEDINGUNG (becomes catastrophic once Etappe 3 adds a rule). `module-tabelle-traegt-keinen-zeilenschutz: bestanden` reproduced live. |
|
|
| 8 | Testlage deckt vorher ungetestete Pfade ab; vergessener Bindungsaufruf wird rot | ✓ VERIFIED | `dashboard.service.spec.ts`: 27 test cases (8 pre-existing, unchanged; 19 new, counted). Reverted `tenantPrisma.widgetInstance.delete` → `this.prisma.widgetInstance.delete` in `removeWidget`: exactly the claimed test failed with the exact claimed message; restored, 27/27 green again (independently reproduced, see Behavioral Spot-Checks). |
|
|
| 9 | Regeln kennen keine Benutzerdimension; Besitzpruefungen ueber Benutzerkennung bleiben einziger Schutz, unveraendert | ✓ VERIFIED | `widget.userId !== userId` present in `updateWidgetConfig` and `removeWidget`; `provider.userId !== userId` present in `removeSearchProvider` — all three intact, all comparing against the session-sourced `userId` parameter, not touched by the binding. Three `*-fremder-nutzer-desselben-mandanten-gebunden-sichtbar` checks pass (gelingen IS the expected/passing result), confirming the DB layer alone has no user dimension. |
|
|
| 10 | Baseline gehalten am Ende jeder Aufgabe (≥839 Tests, ≥56 Dateien, saubere Typpruefung, Werkzeug 0) | ✓ VERIFIED | Per orchestrator's independent measurement: 858/858 tests green across 56 files, `type-check` exit 0. Independently reproduced for the two most relevant suites (`dashboard.service.spec.ts` 27/27, `rls-access-inventory.spec.ts` 10/10) and for `rls-scratch-check.mjs` (87/87, live run). |
|
|
| 11 | Schalter bleibt AUS; kein Schema-, Migrations-, Compose- oder Umgebungs-Datei angefasst | ✓ VERIFIED | `git diff --name-only c7d93f2..HEAD` touches exactly 9 files (confirmed independently), none under `apps/api/prisma`, no compose/env file. `groups.service.ts` and `apps/api/src/module-registry/**` confirmed untouched. |
|
|
| 12 | Die gemessene Konfliktbehandlung in `saveLayout` ist im committeten Werkzeug reproduzierbar belegt und durch einen Test abgesichert | ✗ FAILED (partial) | See `gaps` in frontmatter. The committed `rls-scratch-check.mjs` only measures a raw-SQL `$executeRaw ... ON CONFLICT` path (reproduced live: SQLSTATE 42501), never the actual generated `prisma.dashboardLayout.upsert()` call that `saveLayout` uses. The specific, stronger claim in the SUMMARY/critique document — that the generated client throws `PrismaClientUnknownRequestError` rather than the `P2002`-shaped `PrismaClientKnownRequestError` — is attributed to a separate, non-committed ad-hoc measurement and is not independently reproducible from repo state. No unit test exercises `saveLayout`'s `catch` branch (0 hits for `ConflictException`/`PrismaClientUnknownRequestError` in `dashboard.service.spec.ts`). |
|
|
|
|
**Score:** 10/11 truths fully verified, 1 partial gap (0 present-but-behavior-unverified truths; the one gap is a documented, provable absence, not an uncertainty).
|
|
|
|
### Deferred Items
|
|
|
|
None identified — no later-phase success criteria in the current milestone roadmap were found to cover this gap; this quick task is not tied to a numbered ROADMAP phase, so no deferral cross-check applies.
|
|
|
|
### Required Artifacts
|
|
|
|
| Artifact | Expected | Status | Details |
|
|
|----------|----------|--------|---------|
|
|
| `apps/api/scripts/rls-scratch-check.mjs` | 9th section `runDashboardAreaChecks`, 13 named checks | ✓ VERIFIED | Present at line 2425; live re-run: "Alle 87 Pruefungen bestanden." |
|
|
| `docs/mandantentrennung-etappe2-fehlerrichtung.md` | `## Bereich dashboard` with (w1)-(w5) + Nachtrag in `## Bereich module-registry` | ✓ VERIFIED | Section at line 1745; (w4)/(w5) at 1930/1966; Nachtrag at 1614. |
|
|
| `apps/api/src/dashboard/dashboard.service.ts` | 12 bound, 1 unbound-with-reason | ✓ VERIFIED | Confirmed by direct read and grep counts above. |
|
|
| `apps/api/src/dashboard/dashboard.controller.ts` | 5 handlers pass tenant through | ✓ VERIFIED | All 8 handlers call `extractContext(req)` and pass `tenantId` positionally after `userId`, matching service signatures. |
|
|
| `apps/api/src/dashboard/dashboard.service.spec.ts` | Two-client TDD proof, 8→27 cases, all 8 original green | ✓ VERIFIED | 27/27 passing; describe block 1 (getWidgets — Modulfilter) unchanged with 8 cases. |
|
|
| `docs/mandantentrennung-zugriffsklassifikation.md` | 4 Bestandsaufnahme rows, overview row, sum row, class distribution, background-service section, "Was NICHT entscheidet" | ✓ VERIFIED | All five confirmed by direct read; `rls-access-inventory.spec.ts` 10/10 (machine-gated consistency). |
|
|
| `.planning/WINDOWS.md` | Open entry for evidence-destroying loop, table + JSON | ✓ VERIFIED | Entry #25 present in both forms, `status: open`. |
|
|
|
|
### Key Link Verification
|
|
|
|
| From | To | Via | Status | Details |
|
|
|------|----|----|--------|---------|
|
|
| `getLayout`/`saveLayout` | Same `DashboardLayout` row | Shared `userId` uniqueness key, no tenant component | ✓ WIRED | Both bound over the same `tenantPrisma` per call; co-binding test present and passing. |
|
|
| `dashboard.controller.ts` | `dashboard.service.ts` | `extractContext(req)` → positional `tenantId` argument | ✓ WIRED | All 8 handlers pass `tenantId` from session-derived context, no new trust source introduced. |
|
|
| `dashboard.service.ts` `getWidgets` | `module-access.service.ts` `getAccessibleModuleIds` | Direct call, already bound since 260910-exd | ✓ WIRED | Confirmed unchanged; `module-registry` files untouched by this diff. |
|
|
| `dashboard.service.ts` `saveLayout` catch branch | `ConflictException` | `instanceof Prisma.PrismaClientUnknownRequestError` | ⚠️ WIRED BUT UNTESTED | Code path exists and reads correctly, but no test exercises it — see gap above. |
|
|
|
|
### Behavioral Spot-Checks
|
|
|
|
| Behavior | Command | Result | Status |
|
|
|----------|---------|--------|--------|
|
|
| `rls-scratch-check.mjs` reproducibly passes against live DB | `TESSERA_SCRATCH_ADMIN_URL=... node apps/api/scripts/rls-scratch-check.mjs` | "Alle 87 Pruefungen bestanden." | ✓ PASS |
|
|
| `rls-access-inventory.spec.ts` reproducibly passes | `npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts` | 10/10 | ✓ PASS |
|
|
| Falsification proof 1 (removeWidget binding reverted) | Edited `tenantPrisma.widgetInstance.delete` → `this.prisma.widgetInstance.delete`, ran suite, reverted | Exact claimed test failed with exact claimed message; restored clean, 27/27 green | ✓ PASS |
|
|
| Falsification proof 2 (module catalogue bound) | Edited `this.prisma.module.findMany` → `tenantPrisma.module.findMany`, ran suite, reverted | 8 tests failed incl. the named watchdog, `TypeError: Cannot read properties of undefined (reading 'findMany')` at `dashboard.service.ts:175` — matches SUMMARY verbatim; restored clean, 27/27 green | ✓ PASS |
|
|
| Falsification proof 3 (classification `Stand` set wrong) | Edited row 320 `gebunden`→`ungebunden`, ran inventory spec, reverted | Failed with exact claimed mismatch message; restored clean, 10/10 green | ✓ PASS |
|
|
| `saveLayout` ConflictException translation | Searched for a test exercising it | 0 hits for `ConflictException`/`PrismaClientUnknownRequestError` in `dashboard.service.spec.ts` | ✗ FAIL (gap, see above) |
|
|
|
|
### Anti-Patterns Found
|
|
|
|
None (`TBD`/`FIXME`/`XXX`/`TODO`/`HACK`/placeholder scan of all 9 changed files: no hits). No hardcoded-empty-return stubs found; `getLayout`'s default-arrangement return and `getWidgets`'/`getSearchProviders`' empty-list behavior are explicitly documented as intentional, pre-existing "emptiness as absence" semantics, not stubs introduced by this task.
|
|
|
|
### Requirements Coverage
|
|
|
|
No `.planning/REQUIREMENTS.md` entries exist for `WINDOWS-18` or `ETAPPE-2-DASHBOARD` (this is a quick task, not tracked against the formal requirements ledger) — not orphaned, simply out of scope for that document's tracking.
|
|
|
|
### Human Verification Required
|
|
|
|
None — the one gap found (saveLayout conflict-translation test/measurement coverage) is a provable absence, not an item requiring human judgment to establish the facts. It is reported as a `gaps_found` item so a human can decide whether to accept it via an override or request a follow-up fix.
|
|
|
|
## Gaps Summary
|
|
|
|
Twelve of thirteen checked truths verify cleanly against the codebase, and three separate, independently-reproduced falsification proofs (all three claimed in the SUMMARY) matched verbatim — this is a well-evidenced piece of work overall, with the classification document, the WINDOWS ledger entry, the critique document sections, the ownership checks, and the "module catalogue stays unbound" negative gate all holding up under direct adversarial re-verification.
|
|
|
|
The one real gap: the SUMMARY's most load-bearing new *technical* claim — that a bound conflicting `saveLayout` write throws `PrismaClientUnknownRequestError` (not the `P2002`-shaped error the `tenders` area handles) — is asserted with unusual specificity ("am echten, generierten Prisma Client gemessen, nicht nur an rohem SQL") but that specific measurement was not committed anywhere reproducible: `rls-scratch-check.mjs`'s check 5 only exercises raw SQL via `$executeRaw`, and no unit test exercises `saveLayout`'s `catch (error)` branch. The production code itself (`instanceof Prisma.PrismaClientUnknownRequestError`) is plausible and well-reasoned, and the raw-SQL measurement it's grounded in did reproduce live with SQLSTATE 42501 — but the exact claim made in the document goes beyond what's in the repository, and nothing would catch a regression if the catch clause were later changed or removed.
|
|
|
|
**This looks like a real, if narrow, evidence gap rather than intentional scope creep** — the fix is small (either extend the scratch tool to call the real `.upsert()` and log the observed error class, or add one unit test asserting `saveLayout` throws `ConflictException` when the bound client's `upsert` rejects with a `Prisma.PrismaClientUnknownRequestError`). To accept the current state as-is instead of requiring that follow-up, add to this file's frontmatter:
|
|
|
|
```yaml
|
|
overrides:
|
|
- must_have: "Die gemessene Konfliktbehandlung in saveLayout ist im committeten Werkzeug reproduzierbar belegt und durch einen Testfall abgesichert."
|
|
reason: "Die Codepfad-Logik ist inhaltlich plausibel und durch eine verwandte (rohe SQL) Messung gestuetzt; die fehlende .upsert()-spezifische Messung/der fehlende Unit-Test werden als Nachtrag statt als Blocker akzeptiert."
|
|
accepted_by: "<name>"
|
|
accepted_at: "<ISO timestamp>"
|
|
```
|
|
|
|
---
|
|
|
|
*Verified: 2026-09-11T09:15:00Z*
|
|
*Verifier: Claude (gsd-verifier)*
|