feat(quick-260910-exd): ModuleRegistryService binden, Klassifikation abschliessen

- module-registry.service.ts: findActiveForTenant, activateForTenant,
  isModuleActive binden je einen Aktivierungszugriff, deactivateForTenant
  bindet beide (Lesen+Schreiben) ueber EINEN Klienten unter tenantPrisma;
  alle sechs Katalogzugriffe (findAll/findBySlug/beide
  Existenzpruefungen/isModuleActive-Katalogsuche/seedModule) bleiben
  bewusst ungebunden, mit Kommentar der Messung von Bedingung trennt
- isModuleActive-Kopfkommentar richtiggestellt: der Waechter ruft sie
  nicht auf (0 Aufrufer, TEIL 3 von Aufgabe 1) — Waechter nimmt findBySlug
  + ModuleAccessService.getAccessibleModuleIds
- module-registry.service.spec.ts: NEU, Zwei-Klienten-Nachweis, deckt die
  bislang ungetestete Datei mit elf der siebzehn Zugriffe des Bereichs ab,
  inkl. der lauten (deactivate ohne Aktivierung) und stillen (isModuleActive
  ohne Aktivierung) Richtung und dem Katalog-Wachhund
- tender-scheduler.service.spec.ts: forTenant() auf Identitaet gemockt
  (dieselbe Konvention wie ldap.service.spec.ts) — cross-area Bruch durch
  die Umstellung von activateForTenant behoben (Rule 1/3)
- docs/mandantentrennung-zugriffsklassifikation.md: alle fuenf
  handgepflegten Stellen nachgezogen (Bestandsaufnahme, Uebersichtszeile
  7/10, Summenzeile 108/134, Klassen-Verteilung unveraendert bei 63 Paaren,
  Hintergrunddienst-Abschnitt haelt die Abwesenheit eines sechsten Falls
  fest) — alle gemessen, nicht abgeschrieben, Befund K haelt exakt
- docs/mandantentrennung-etappe2-fehlerrichtung.md: Nachtrag mit
  tatsaechlich umgesetzten Pfaden, beiden Falsifizierungsnachweisen
  (Testname+Meldung), und der Feststellung zum unveraenderten
  Controller-Kommentar
- .planning/WINDOWS.md: neuer offener Eintrag #23 (deviation) — kein Signal
  unterscheidet "keine Freigabe" von "Abfrage fand nichts", mit
  Vorabpruefung fuer Etappe 4 und begruendeter Verwerfung einer
  Laufzeitwarnung
- 833 Tests gruen (56 Dateien), Typpruefung sauber, Wegwerf-Werkzeug 66/66

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
2026-09-10 11:30:37 +02:00
parent 3df72687c1
commit 9c0eefee90
6 changed files with 515 additions and 17 deletions
@@ -0,0 +1,343 @@
import { NotFoundException } from '@nestjs/common';
import { describe, expect, it, vi } from 'vitest';
import { ModuleRegistryService } from './module-registry.service';
/**
* ModuleRegistryService — bisher OHNE Testdatei (260910-exd, Aufgabe 3,
* Befund C, zweite Form). Diese Datei haelt elf der siebzehn Zugriffe des
* Bereichs `module-registry`, darunter JEDEN Schreibpfad.
*
* Bindung an forTenant() (dasselbe Muster wie
* `module-grants.service.spec.ts` und `module-access.service.spec.ts`): der
* gebundene Klient ist ein ZWEITES, von `prisma` unterscheidbares Objekt
* ueber DEMSELBEN Speicher, das protokolliert, welche Aufrufe ueber ihn
* liefen. `module` wird NICHT gewrappt — der Katalogzugriff bleibt bewusst
* ungebunden (Aufgabe 1, Befund E).
*/
vi.mock('../prisma/prisma-tenant.extension', () => ({
forTenant: vi.fn((prisma: any, tenantId: string) => prisma.__makeBoundClient(tenantId)),
}));
const BOUND_MODEL_NAMES = ['tenantModuleActivation'];
function makeFakePrisma() {
const modules = new Map<string, any>();
const activations = new Map<string, any>(); // key: tenantId::moduleId
const boundCallLog: { tenantId: string; model: string; method: string }[] = [];
const fake: any = {
__seedModule(m: { id: string; slug: string; name: string; version: string; category: string; description: any; icon?: string | null; isSystem?: boolean }) {
modules.set(m.id, { icon: null, isSystem: false, ...m });
},
__seedActivation(a: { tenantId: string; moduleId: string; isActive: boolean }) {
activations.set(`${a.tenantId}::${a.moduleId}`, { ...a, activatedAt: new Date() });
},
module: {
findMany: vi.fn(async () => Array.from(modules.values()).sort((a, b) => a.name.localeCompare(b.name))),
findUnique: vi.fn(async ({ where }: any) => {
if (where.id) return modules.get(where.id) ?? null;
if (where.slug) return Array.from(modules.values()).find((m) => m.slug === where.slug) ?? null;
return null;
}),
upsert: vi.fn(async ({ where, update, create }: any) => {
const existing = Array.from(modules.values()).find((m) => m.slug === where.slug);
if (existing) {
const updated = { ...existing, ...update };
modules.set(existing.id, updated);
return updated;
}
const record = { id: `mod-${modules.size + 1}`, ...create };
modules.set(record.id, record);
return record;
}),
},
tenantModuleActivation: {
findMany: vi.fn(async ({ where }: any) => {
return Array.from(activations.values())
.filter((a) => a.tenantId === where.tenantId && (where.isActive === undefined || a.isActive === where.isActive))
.map((a) => ({ ...a, module: modules.get(a.moduleId) ?? null }));
}),
findUnique: vi.fn(async ({ where }: any) => {
const { tenantId, moduleId } = where.tenantId_moduleId;
return activations.get(`${tenantId}::${moduleId}`) ?? null;
}),
upsert: vi.fn(async ({ where, update, create }: any) => {
const key = `${where.tenantId_moduleId.tenantId}::${where.tenantId_moduleId.moduleId}`;
const existing = activations.get(key);
const record = existing
? { ...existing, ...update }
: { ...create, activatedAt: new Date() };
activations.set(key, record);
return { ...record, module: modules.get(record.moduleId) ?? null };
}),
update: vi.fn(async ({ where, data }: any) => {
const { tenantId, moduleId } = where.tenantId_moduleId;
const key = `${tenantId}::${moduleId}`;
const existing = activations.get(key);
const record = { ...existing, ...data };
activations.set(key, record);
return { ...record, module: modules.get(record.moduleId) ?? null };
}),
},
// --- Bindungsnachweis (260910-exd, Befund C uebertragen) ---------------
__boundCallLog: boundCallLog,
__makeBoundClient(tenantId: string) {
const bound: any = { __isBoundClient: true, __tenantId: tenantId };
for (const modelName of BOUND_MODEL_NAMES) {
const model = fake[modelName];
const wrapped: any = {};
for (const method of Object.keys(model)) {
wrapped[method] = async (...args: any[]) => {
boundCallLog.push({ tenantId, model: modelName, method });
return model[method](...args);
};
}
bound[modelName] = wrapped;
}
return bound;
},
};
return fake;
}
function expectBoundCall(prisma: any, tenantId: string, model: string, method: string) {
const found = prisma.__boundCallLog.some(
(c: any) => c.tenantId === tenantId && c.model === model && c.method === method,
);
expect(
found,
`erwarteter gebundener Aufruf ${model}.${method}(tenant=${tenantId}) fehlt im Protokoll: ${JSON.stringify(prisma.__boundCallLog)}`,
).toBe(true);
}
function expectNeverBound(prisma: any, model: string) {
const found = prisma.__boundCallLog.some((c: any) => c.model === model);
expect(
found,
`Modell "${model}" darf nie im Bindungsprotokoll auftauchen (Katalog bleibt ungebunden): ${JSON.stringify(prisma.__boundCallLog)}`,
).toBe(false);
}
describe('ModuleRegistryService.findAll/findBySlug — Katalog (bewusst UNGEBUNDEN)', () => {
it('findAll liefert alle registrierten Module, sortiert nach Namen', async () => {
const prisma = makeFakePrisma();
prisma.__seedModule({ id: 'mod-b', slug: 'b', name: 'B-Modul', version: '1', category: 'ops', description: {} });
prisma.__seedModule({ id: 'mod-a', slug: 'a', name: 'A-Modul', version: '1', category: 'ops', description: {} });
const service = new ModuleRegistryService(prisma as any);
const result = await service.findAll();
expect(result.map((m: any) => m.id)).toEqual(['mod-a', 'mod-b']);
expectNeverBound(prisma, 'module');
});
it('findBySlug findet ein Modul über seinen Slug', async () => {
const prisma = makeFakePrisma();
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
const service = new ModuleRegistryService(prisma as any);
const result = await service.findBySlug('domaincheck');
expect(result?.id).toBe('mod-1');
expectNeverBound(prisma, 'module');
});
it('findBySlug liefert null für einen unbekannten Slug, ohne zu werfen', async () => {
const prisma = makeFakePrisma();
const service = new ModuleRegistryService(prisma as any);
const result = await service.findBySlug('unknown');
expect(result).toBeNull();
});
});
describe('ModuleRegistryService.findActiveForTenant', () => {
it('bindet den Lesezugriff an den Mandanten UND liefert die Katalogseite mit (die verbundene Modellform überlebt die Bindung)', async () => {
const prisma = makeFakePrisma();
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
prisma.__seedActivation({ tenantId: 't1', moduleId: 'mod-1', isActive: true });
const service = new ModuleRegistryService(prisma as any);
const result = await service.findActiveForTenant('t1');
expect(result).toEqual([expect.objectContaining({ id: 'mod-1', name: 'Domaincheck' })]);
expectBoundCall(prisma, 't1', 'tenantModuleActivation', 'findMany');
});
it('die Aktivierungsliste eines zweiten Mandanten liefert keine Zeile des ersten', async () => {
const prisma = makeFakePrisma();
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
prisma.__seedActivation({ tenantId: 't1', moduleId: 'mod-1', isActive: true });
const service = new ModuleRegistryService(prisma as any);
const result = await service.findActiveForTenant('t2');
expect(result).toEqual([]);
expectBoundCall(prisma, 't2', 'tenantModuleActivation', 'findMany');
});
});
describe('ModuleRegistryService.activateForTenant', () => {
it('erreicht die Katalog-Existenzprüfung UNGEBUNDEN und schreibt die Aktivierung GEBUNDEN, an den übergebenen Mandanten', async () => {
const prisma = makeFakePrisma();
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
const service = new ModuleRegistryService(prisma as any);
const result = await service.activateForTenant('t1', 'mod-1');
expect(result.isActive).toBe(true);
expect(prisma.module.findUnique).toHaveBeenCalled();
expectNeverBound(prisma, 'module');
expectBoundCall(prisma, 't1', 'tenantModuleActivation', 'upsert');
});
it('wirft die Nicht-gefunden-Ausnahme für eine unbekannte Modulkennung, BEVOR irgendein gebundener Schreibzugriff im Protokoll steht', async () => {
const prisma = makeFakePrisma();
const service = new ModuleRegistryService(prisma as any);
await expect(service.activateForTenant('t1', 'mod-unknown')).rejects.toBeInstanceOf(NotFoundException);
expect(
prisma.__boundCallLog,
`kein gebundener Aufruf erwartet, wenn die Katalog-Existenzpruefung bereits scheitert: ${JSON.stringify(prisma.__boundCallLog)}`,
).toEqual([]);
});
});
describe('ModuleRegistryService.deactivateForTenant', () => {
it('bindet beide Aktivierungszugriffe (Lesen, Schreiben) an denselben Mandanten, über einen Klienten', async () => {
const prisma = makeFakePrisma();
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
prisma.__seedActivation({ tenantId: 't1', moduleId: 'mod-1', isActive: true });
const service = new ModuleRegistryService(prisma as any);
const result = await service.deactivateForTenant('t1', 'mod-1');
expect(result.isActive).toBe(false);
expectBoundCall(prisma, 't1', 'tenantModuleActivation', 'findUnique');
expectBoundCall(prisma, 't1', 'tenantModuleActivation', 'update');
expectNeverBound(prisma, 'module');
});
/**
* Die LAUTE, harmlose Richtung dieses Bereichs (260910-exd, m3) — hier
* ausdruecklich festgenagelt, damit sie bei einem spaeteren Umbau nicht
* versehentlich in ein stilles `false` verwandelt wird.
*/
it('wirft die Nicht-gefunden-Ausnahme, wenn keine Aktivierung vorliegt (LAUT, nicht still)', async () => {
const prisma = makeFakePrisma();
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
const service = new ModuleRegistryService(prisma as any);
await expect(service.deactivateForTenant('t1', 'mod-1')).rejects.toBeInstanceOf(NotFoundException);
});
it('wirft die Nicht-gefunden-Ausnahme für eine unbekannte Modulkennung', async () => {
const prisma = makeFakePrisma();
const service = new ModuleRegistryService(prisma as any);
await expect(service.deactivateForTenant('t1', 'mod-unknown')).rejects.toBeInstanceOf(NotFoundException);
});
});
describe('ModuleRegistryService.isModuleActive', () => {
it('bindet ihren Aktivierungs-Lesezugriff und erreicht den Katalog ungebunden', async () => {
const prisma = makeFakePrisma();
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
prisma.__seedActivation({ tenantId: 't1', moduleId: 'mod-1', isActive: true });
const service = new ModuleRegistryService(prisma as any);
const result = await service.isModuleActive('t1', 'domaincheck');
expect(result).toBe(true);
expectBoundCall(prisma, 't1', 'tenantModuleActivation', 'findUnique');
expectNeverBound(prisma, 'module');
});
/**
* Die STILLE Richtung dieses Bereichs (260910-exd, m3) — ebenfalls
* festgenagelt, mit diesem Kommentar als Markierung: heute ohne Aufrufer
* (Aufgabe 1, TEIL 3), aber die Falle für morgen, sollte diese Methode
* verdrahtet werden.
*/
it('liefert `false`, ohne Aktivierung (STILL, keine Ausnahme)', async () => {
const prisma = makeFakePrisma();
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
const service = new ModuleRegistryService(prisma as any);
const result = await service.isModuleActive('t1', 'domaincheck');
expect(result).toBe(false);
});
it('liefert `false` für einen unbekannten Slug, ohne die Aktivierungstabelle überhaupt zu befragen', async () => {
const prisma = makeFakePrisma();
const service = new ModuleRegistryService(prisma as any);
const result = await service.isModuleActive('t1', 'unknown-slug');
expect(result).toBe(false);
expect(prisma.__boundCallLog).toEqual([]);
});
});
describe('ModuleRegistryService.seedModule — Katalogpflege beim Start (bewusst UNGEBUNDEN)', () => {
it('legt ein neues Modul an, wenn der Slug noch nicht existiert', async () => {
const prisma = makeFakePrisma();
const service = new ModuleRegistryService(prisma as any);
const result = await service.seedModule({
slug: 'domaincheck',
name: 'Domaincheck',
version: '1.0.0',
category: 'ops',
description: { de: 'Test' },
});
expect(result.slug).toBe('domaincheck');
expectNeverBound(prisma, 'module');
});
it('aktualisiert ein bestehendes Modul über denselben Slug (Upsert)', async () => {
const prisma = makeFakePrisma();
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Alt', version: '1', category: 'ops', description: {} });
const service = new ModuleRegistryService(prisma as any);
const result = await service.seedModule({
slug: 'domaincheck',
name: 'Neu',
version: '2.0.0',
category: 'ops',
description: { de: 'Test' },
});
expect(result.name).toBe('Neu');
expect(result.version).toBe('2.0.0');
});
});
describe('ModuleRegistryService — Katalogzugriffe tauchen nie im Bindungsprotokoll auf (Wachhund, 260910-exd)', () => {
it('kein Katalogzugriff des Dienstes — weder Gesamtliste, noch Kennzeichen-Suche, noch Existenzprüfungen, noch Katalogpflege — taucht im Bindungsprotokoll auf', async () => {
const prisma = makeFakePrisma();
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
prisma.__seedActivation({ tenantId: 't1', moduleId: 'mod-1', isActive: true });
const service = new ModuleRegistryService(prisma as any);
await service.findAll();
await service.findBySlug('domaincheck');
await service.activateForTenant('t1', 'mod-1');
await service.deactivateForTenant('t1', 'mod-1');
await service.isModuleActive('t1', 'domaincheck');
await service.seedModule({
slug: 'domaincheck',
name: 'Domaincheck',
version: '1.0.0',
category: 'ops',
description: {},
});
expectNeverBound(prisma, 'module');
});
});
@@ -1,5 +1,6 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { forTenant } from '../prisma/prisma-tenant.extension';
/**
* Service managing the module registry and per-tenant activations.
@@ -13,6 +14,13 @@ export class ModuleRegistryService {
/**
* Returns all registered modules.
*
* Bewusst UNGEBUNDEN (260910-exd, Aufgabe 1, Befund E): "Module" traegt
* heute keinen Zeilenschutz, eine Bindung waere heute wirkungslos, nicht
* katastrophal. Katastrophal wuerde sie erst, WENN Etappe 3 dieser
* Tabelle eine Regel gibt — dann verschwaende der gesamte Katalog fuer
* jeden Mandanten. Diese Bedingung steht hier als Bedingung, nicht als
* heute beobachtbare Tatsache.
*/
async findAll() {
return this.prisma.module.findMany({
@@ -22,6 +30,12 @@ export class ModuleRegistryService {
/**
* Finds a module by its unique slug.
*
* Bewusst UNGEBUNDEN, dieselbe Begruendung wie `findAll` oben. Diese
* Methode ist zusaetzlich die Stelle, die `ModuleGuard` bei JEDER
* Modulanfrage aufruft — eine Bindung wuerde jede Modulanfrage mit einer
* Meldung abweisen, die faelschlich von einer fehlenden Aktivierung
* spricht.
*/
async findBySlug(slug: string) {
return this.prisma.module.findUnique({
@@ -33,7 +47,8 @@ export class ModuleRegistryService {
* Returns all active modules for a given tenant.
*/
async findActiveForTenant(tenantId: string) {
const activations = await this.prisma.tenantModuleActivation.findMany({
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
const activations = await tenantPrisma.tenantModuleActivation.findMany({
where: {
tenantId,
isActive: true,
@@ -43,7 +58,7 @@ export class ModuleRegistryService {
},
});
return activations.map((a) => a.module);
return activations.map((a: { module: unknown }) => a.module);
}
/**
@@ -51,7 +66,10 @@ export class ModuleRegistryService {
* Per D-08: dynamic activation without restart.
*/
async activateForTenant(tenantId: string, moduleId: string) {
// Verify module exists
// Verify module exists — bewusst UNGEBUNDEN, dieselbe Begruendung wie
// `findAll` oben (Aufgabe 1, Befund E). Laeuft VOR jedem gebundenen
// Schreibzugriff: eine unbekannte moduleId wirft, bevor der gebundene
// Klient ueberhaupt erzeugt wird.
const moduleExists = await this.prisma.module.findUnique({
where: { id: moduleId },
});
@@ -59,7 +77,8 @@ export class ModuleRegistryService {
throw new NotFoundException(`Module with id '${moduleId}' not found`);
}
return this.prisma.tenantModuleActivation.upsert({
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
return tenantPrisma.tenantModuleActivation.upsert({
where: {
tenantId_moduleId: {
tenantId,
@@ -86,7 +105,8 @@ export class ModuleRegistryService {
* Does not remove the activation record, preserving audit trail.
*/
async deactivateForTenant(tenantId: string, moduleId: string) {
// Verify module exists
// Verify module exists — bewusst UNGEBUNDEN, dieselbe Begruendung wie
// `findAll` oben.
const moduleExists = await this.prisma.module.findUnique({
where: { id: moduleId },
});
@@ -94,8 +114,13 @@ export class ModuleRegistryService {
throw new NotFoundException(`Module with id '${moduleId}' not found`);
}
// EIN gebundener Klient fuer beide Aktivierungszugriffe dieser Methode
// (Lesen, Schreiben) — nicht ein Klient je Zugriff (260910-exd,
// Aufgabe 3, dieselbe Konvention wie `module-access.service.ts`).
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
// Check if activation record exists
const activation = await this.prisma.tenantModuleActivation.findUnique({
const activation = await tenantPrisma.tenantModuleActivation.findUnique({
where: {
tenantId_moduleId: {
tenantId,
@@ -110,7 +135,7 @@ export class ModuleRegistryService {
);
}
return this.prisma.tenantModuleActivation.update({
return tenantPrisma.tenantModuleActivation.update({
where: {
tenantId_moduleId: {
tenantId,
@@ -128,7 +153,15 @@ export class ModuleRegistryService {
/**
* Checks whether a module (by slug) is active for a given tenant.
* Used by ModuleGuard to gate access to module-specific endpoints.
*
* Richtiggestellt (260910-exd, Aufgabe 1, Befund G): der vorherige
* Kommentar behauptete, `ModuleGuard` benutze diese Methode — er tut es
* NICHT. Gemessen (Aufgabe 1, TEIL 3, `grep -rn "isModuleActive"
* apps/api/src apps/web/src packages`): genau EIN Treffer, die Definition
* selbst, kein Aufrufer. Der Waechter nimmt stattdessen `findBySlug` plus
* `ModuleAccessService.getAccessibleModuleIds`. Diese Methode bleibt
* TROTZDEM umgestellt: heute toter, ungebunden gelassener Code ist die
* Falle fuer den, der ihn morgen verdrahtet.
*/
async isModuleActive(tenantId: string, moduleSlug: string): Promise<boolean> {
const module = await this.prisma.module.findUnique({
@@ -139,7 +172,8 @@ export class ModuleRegistryService {
return false;
}
const activation = await this.prisma.tenantModuleActivation.findUnique({
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
const activation = await tenantPrisma.tenantModuleActivation.findUnique({
where: {
tenantId_moduleId: {
tenantId,
@@ -154,6 +188,11 @@ export class ModuleRegistryService {
/**
* Registers or updates a module in the registry by slug (upsert).
* Used during application startup to seed built-in modules.
*
* Bewusst UNGEBUNDEN, dieselbe Begruendung wie `findAll` oben — mit einem
* zusaetzlichen Grund, den nur diese Methode hat: sie laeuft beim
* Anwendungsstart aus vier Seed-Dateien, ohne Anfrage und ohne Mandanten
* — ein gebundener Aufruf haette dort strukturell keinen Kontext.
*/
async seedModule(manifest: {
slug: string;
@@ -15,7 +15,17 @@ import { TenderSchedulerService } from './tender-scheduler.service';
* tender-ingestion.service.spec.ts / ldap.service.spec.ts, driving the REAL
* ModuleRegistryService (unmocked) so the activation call path is genuine,
* not a stand-in.
*
* forTenant() just returns the same client in these tests (identical
* convention to ldap.service.spec.ts) — tenant scoping/RLS binding is not
* what this file tests, only ModuleRegistryService.activateForTenant's
* poll-once-fan-out-many behavior. Needed since 260910-exd (Aufgabe 3)
* converted ModuleRegistryService.activateForTenant to forTenant(), and the
* hand-rolled fake below does not implement `$extends`.
*/
vi.mock('../prisma/prisma-tenant.extension', () => ({
forTenant: vi.fn((p: unknown) => p),
}));
function makeFakePrisma() {
const modules = new Map<string, any>();