#038: Die E2E-Ablaeufe laufen ab jetzt vor jedem Merge nach main, nicht vor jedem Commit. Gemessen: Bau 14 s, Start 7 s, Ablaeufe 256 s — zu den vier Minuten des bestehenden Tores. Neun Minuten vor jedem Commit ist ein Tor, das umgangen wird. Vor jedem Merge waeren es in Phase 2 acht Laeufe gewesen, und jeder der vier Befunde aus #027 waere am Tag seiner Entstehung aufgefallen. Geprueft wurde jeder der 16 Ablaeufe gegen die Frage, ob es ein Vitest-Gegenstueck gibt. Neun Zusagen haengen ausschliesslich dort, und keine laesst sich holen: alle brauchen einen echten Browser. Die Liste steht in CLAUDE.md. Der eigentliche Fund dabei war keine Testluecke, sondern eine Konvention ohne Pruefer: "jede Seite prueft selbst" steht seit #007 in CLAUDE.md, und geprueft hat es bis hierher nur ein Mensch, der nachsieht. Der Review von Phase 2 hat alle 27 Seiten durchgesehen — ein Ergebnis, das fuer genau einen Tag gilt. tests/zugang.test.ts liest sie jetzt, samt jedem Route Handler; die Ausnahmen (Auth, Healthcheck) stehen namentlich mit Begruendung, und ein Test prueft auch die Gegenrichtung. #037: Das Lesemodell der Anhaenge liegt in queries/ und engt selbst ein — vorher hing das daran, dass beide Aufrufer den Lauf vorher pruefen. /meine-geraete wird bei 390 px gemessen, mit einem echten MITARBEITER-Konto aus dem E2E-Vorlauf, dazu ein Test, dass dort kein Link in die Verwaltung angeboten wird. Dabei selbst einen Fehler gemacht, der die neue Regel begruendet: fuer das zweite Konto brauchte `anmelden` eine Variante, die nicht auf / wartet, und ich habe das Warten fuer alle gelockert. Das machte zwei unverwandte Ablaeufe unzuverlaessig — sichtbar nur im vollen Durchgang, einzeln waren beide gruen. Ein gemeinsamer Helfer in einer Testsuite ist geteilter Zustand. Ohne vollen Durchgang haette ich zwei rote Ablaeufe gemergt. 1457 Vitest-Tests (vorher 1454), 115 E2E-Ablaeufe (vorher 113). Offen bleibt nur noch #024, und das verlangt einen echten Proxmox-Host. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
80d97467da
commit
75c335df23
@@ -20,7 +20,7 @@ import { SchrittWert } from "@/components/laeufe/schritt-wert";
|
||||
import { SchrittZustandWaehlen, ZustandKennzeichen } from "@/components/laeufe/schritt-zustand";
|
||||
import { darf } from "@/server/auth/rollen";
|
||||
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { anhaengeDesLaufs } from "@/server/services/anhaenge";
|
||||
import { anhaengeDesLaufs } from "@/server/queries/anhaenge";
|
||||
import { protokollDesLaufs } from "@/server/queries/protokoll";
|
||||
import {
|
||||
kommentareDesLaufs,
|
||||
@@ -322,7 +322,7 @@ export default async function LaufSeite({
|
||||
const laufProtokoll = await protokollDesLaufs(id);
|
||||
// Die Sicht hat `laufHolen` oben schon angewandt; hier nur die Metadaten der
|
||||
// Anhaenge dieses Laufs (#032).
|
||||
const anhaenge = await anhaengeDesLaufs(id);
|
||||
const anhaenge = await anhaengeDesLaufs(id, benutzer);
|
||||
/*
|
||||
* Die Personenliste nur fuer den Onboarding-Fall holen — sonst stuende in
|
||||
* jedem Seitenaufbau eine Abfrage, die niemand braucht.
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { sichtbareKundenIds, type HandelnderBenutzer } from "@/server/mandant";
|
||||
|
||||
/**
|
||||
* Lesemodell der Anhänge (#037).
|
||||
*
|
||||
* **Warum das hier liegt und nicht beim Dienst.** `anhaengeDesLaufs` stand in
|
||||
* `src/server/services/anhaenge.ts` und nahm nur eine Lauf-Kennung — ohne Rollen-
|
||||
* und ohne Mandantensicht. Beide Aufrufer prüften den Lauf vorher, es war also
|
||||
* kein Schaden; es stand aber gegen die Regel aus `CLAUDE.md` und
|
||||
* `docs/architektur.md` §2: Lesemodelle liegen in `queries/*` und **wenden ihre
|
||||
* Sicht selbst an, damit kein Aufrufer sie vergessen kann**.
|
||||
*
|
||||
* Zwei Aufrufer in zwei Schichten gab es schon (die Abarbeitungsansicht und die
|
||||
* Druckansicht). Der dritte ist der, der es vergisst — und er fällt nicht auf,
|
||||
* weil die bestehenden korrekt sind. Gefunden bei der Durchsicht von Phase 2.
|
||||
*/
|
||||
|
||||
export type AnhangZeile = {
|
||||
readonly id: string;
|
||||
readonly schrittergebnisId: string;
|
||||
readonly dateiname: string;
|
||||
readonly medientyp: string;
|
||||
readonly groesse: number;
|
||||
readonly erstelltAm: Date;
|
||||
readonly erstelltVon: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Die Anhänge eines Laufs, je Schrittergebnis. **Nur Metadaten** — der Inhalt
|
||||
* geht über `GET /api/anhaenge/[id]`, und der prüft sein Recht selbst.
|
||||
*
|
||||
* Gibt eine leere Liste zurück, wenn der Lauf dem Benutzer nicht sichtbar ist.
|
||||
* Leer und nicht geworfen, weil dieselbe Antwort für „keine Anhänge“ und „nicht
|
||||
* sichtbar“ richtig ist: ob es den Lauf gibt, ist für jemanden, der ihn nicht
|
||||
* sehen darf, keine Auskunft.
|
||||
*/
|
||||
export async function anhaengeDesLaufs(
|
||||
laufId: string,
|
||||
benutzer: HandelnderBenutzer,
|
||||
): Promise<readonly AnhangZeile[]> {
|
||||
/*
|
||||
* `undefined` heißt „alle Kunden“ — im Modus INTERN, für ADMIN und REDAKTEUR,
|
||||
* und ohne eingerichtete Kundengruppen. Die Einschränkung sitzt deshalb in
|
||||
* einem `...(sichtbar ? … : {})` und nicht in einem `in: sichtbar ?? []`: das
|
||||
* zweite ergäbe für jeden Administrator eine leere Liste.
|
||||
*/
|
||||
const sichtbar = await sichtbareKundenIds(benutzer);
|
||||
|
||||
const zeilen = await prisma.anhang.findMany({
|
||||
where: {
|
||||
laufId,
|
||||
...(sichtbar ? { kundeId: { in: [...sichtbar] } } : {}),
|
||||
},
|
||||
orderBy: { erstelltAm: "asc" },
|
||||
select: {
|
||||
id: true,
|
||||
schrittergebnisId: true,
|
||||
dateiname: true,
|
||||
medientyp: true,
|
||||
groesse: true,
|
||||
erstelltAm: true,
|
||||
erstelltVon: { select: { name: true, email: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return zeilen.map((zeile) => ({
|
||||
id: zeile.id,
|
||||
schrittergebnisId: zeile.schrittergebnisId,
|
||||
dateiname: zeile.dateiname,
|
||||
medientyp: zeile.medientyp,
|
||||
groesse: zeile.groesse,
|
||||
erstelltAm: zeile.erstelltAm,
|
||||
// Der Name, sonst die Mailadresse: ein Systemlauf hat keinen von beidem, und
|
||||
// `null` heißt dort „von der Anwendung“ (#029 schreibt so, der Zeitplan auch).
|
||||
erstelltVon: zeile.erstelltVon?.name ?? zeile.erstelltVon?.email ?? null,
|
||||
}));
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LaufStatus, SchrittTyp, SchrittZustand } from "@/generated/prisma/enums";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { nurGeltende } from "@/lib/geltende-abschnitte";
|
||||
import { anhaengeDesLaufs } from "@/server/services/anhaenge";
|
||||
import { anhaengeDesLaufs } from "@/server/queries/anhaenge";
|
||||
import { platzhalterErsetzen } from "@/lib/platzhalter-werte";
|
||||
import { kundenEbeneSichtbar, sichtbareKundenIds, type HandelnderBenutzer } from "@/server/mandant";
|
||||
import { kommentareDesLaufs, type KommentarZeile } from "@/server/queries/laeufe";
|
||||
@@ -226,7 +226,7 @@ export async function laufZumDrucken(
|
||||
const alleKommentare = await kommentareDesLaufs(laufId, benutzer);
|
||||
// Nur Metadaten: der Ausdruck verweist auf die Adresse, er traegt die Datei
|
||||
// nicht in sich (#032).
|
||||
const alleAnhaenge = await anhaengeDesLaufs(laufId);
|
||||
const alleAnhaenge = await anhaengeDesLaufs(laufId, benutzer);
|
||||
const ergebnisse = new Map(lauf.ergebnisse.map((ergebnis) => [ergebnis.vorlagenSchrittId, ergebnis]));
|
||||
|
||||
let nummer = 0;
|
||||
|
||||
@@ -213,42 +213,6 @@ export async function anhangEntfernen(anhangId: string, benutzer: HandelnderBenu
|
||||
await rm(dateiPfad(anhang), { force: true });
|
||||
}
|
||||
|
||||
/** Die Anhaenge eines Laufs, je Schrittergebnis. Nur Metadaten. */
|
||||
export async function anhaengeDesLaufs(laufId: string): Promise<
|
||||
readonly {
|
||||
readonly id: string;
|
||||
readonly schrittergebnisId: string;
|
||||
readonly dateiname: string;
|
||||
readonly medientyp: string;
|
||||
readonly groesse: number;
|
||||
readonly erstelltAm: Date;
|
||||
readonly erstelltVon: string | null;
|
||||
}[]
|
||||
> {
|
||||
const zeilen = await prisma.anhang.findMany({
|
||||
where: { laufId },
|
||||
orderBy: { erstelltAm: "asc" },
|
||||
select: {
|
||||
id: true,
|
||||
schrittergebnisId: true,
|
||||
dateiname: true,
|
||||
medientyp: true,
|
||||
groesse: true,
|
||||
erstelltAm: true,
|
||||
erstelltVon: { select: { name: true, email: true } },
|
||||
},
|
||||
});
|
||||
return zeilen.map((zeile) => ({
|
||||
id: zeile.id,
|
||||
schrittergebnisId: zeile.schrittergebnisId,
|
||||
dateiname: zeile.dateiname,
|
||||
medientyp: zeile.medientyp,
|
||||
groesse: zeile.groesse,
|
||||
erstelltAm: zeile.erstelltAm,
|
||||
erstelltVon: zeile.erstelltVon?.name ?? zeile.erstelltVon?.email ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
/** Nur fuer Tests und Werkzeuge: der Pfad einer Zeile. */
|
||||
export function anhangPfad(anhang: {
|
||||
readonly id: string;
|
||||
|
||||
Reference in New Issue
Block a user