Baustein 8: Lernzielkontrollen mit Freigabe, Abgabe und Korrektur

- Ausbilder gibt frei; vorher sieht der Azubi die Fragen nicht
- Freigabe laesst sich zuruecknehmen, solange nicht abgegeben wurde
- Antworten werden beim Tippen je Frage zwischengespeichert
- Musterloesungen erst nach dem Absenden: sichtbareKontrolle() entfernt sie
  serverseitig, bevor die Daten in die Ausgabe gelangen
- Korrektur mit Punkten und Anmerkung je Frage, Summe und Note laufen mit
- Notenschluessel der IHK; solange eine Frage offen ist, bleibt das Ergebnis
  vorlaeufig und die Kontrolle gilt nicht als korrigiert
- Ergebnis bei beiden sichtbar, Schnitt ueber alle Kontrollen
- Abgegebene Kontrollen unter "Was auf dich wartet" und im Wochenfortschritt
- scripts/pruefungen/musterloesung-dichtheit.mjs prueft die Sperre gegen den
  echten Wortlaut aus der Inhaltsdatei im rohen Seitenquelltext

36 neue Tests, davon 21 fuer die Sichtbarkeit der Musterloesung.
Insgesamt 277.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-11 08:57:00 +02:00
co-authored by Claude Opus 5
parent 7c4a051347
commit d226a6bae8
42 changed files with 2132 additions and 9 deletions
+178
View File
@@ -0,0 +1,178 @@
import { describe, expect, it } from 'vitest';
import {
darfBearbeiten,
darfFragenSehen,
gesamtpunkte,
sichtbareKontrolle,
zustandVon,
type Bearbeitungsstand,
} from './sicht';
import type { Kontrolle } from '../inhalte/schema';
import type { Betrachter } from '../auth/regeln';
const kontrolle: Kontrolle = {
id: 'woche-03',
woche: 3,
titel: 'Lernzielkontrolle Woche 3',
dauer_minuten: 45,
hinweis: 'Offene Fragen.',
fragen: [
{ id: 'f1', frage: 'Was ist die Blockgröße bei /27?', punkte: 8, musterloesung: 'GEHEIM-EINS: 32.' },
{ id: 'f2', frage: 'Wozu dient DHCP?', punkte: 5, musterloesung: 'GEHEIM-ZWEI: Adressvergabe.' },
],
};
const azubi: Betrachter = { id: 'azu-1', rolle: 'azubi', aktiv: 1 };
const andererAzubi: Betrachter = { id: 'azu-2', rolle: 'azubi', aktiv: 1 };
const ausbilder: Betrachter = { id: 'aus-1', rolle: 'ausbilder', aktiv: 1 };
const fremderAusbilder: Betrachter = { id: 'aus-9', rolle: 'ausbilder', aktiv: 1 };
const bezug = { azubiId: 'azu-1', ausbilderId: 'aus-1' };
function stand(teil: Partial<Bearbeitungsstand> = {}): Bearbeitungsstand {
return { azubiId: 'azu-1', freigegebenAm: null, abgegebenAm: null, ...teil };
}
/** Findet heraus, ob irgendwo im Ergebnis noch eine Musterlösung steckt. */
function enthaeltGeheimnis(wert: unknown): boolean {
return JSON.stringify(wert).includes('GEHEIM');
}
describe('Musterlösung vor dem Absenden', () => {
it('kommt beim gesperrten Zustand nicht mit', () => {
const sicht = sichtbareKontrolle(kontrolle, azubi, stand(), bezug);
expect(sicht.musterloesungSichtbar).toBe(false);
expect(enthaeltGeheimnis(sicht)).toBe(false);
});
it('kommt auch nach der Freigabe nicht mit, solange nicht abgegeben wurde', () => {
const sicht = sichtbareKontrolle(kontrolle, azubi, stand({ freigegebenAm: '2026-09-21T07:00:00.000Z' }), bezug);
expect(sicht.musterloesungSichtbar).toBe(false);
expect(sicht.fragen.every((f) => f.musterloesung === null)).toBe(true);
expect(enthaeltGeheimnis(sicht)).toBe(false);
});
it('kommt nach dem Absenden mit', () => {
const sicht = sichtbareKontrolle(
kontrolle,
azubi,
stand({ freigegebenAm: '2026-09-21T07:00:00.000Z', abgegebenAm: '2026-09-21T08:00:00.000Z' }),
bezug,
);
expect(sicht.musterloesungSichtbar).toBe(true);
expect(sicht.fragen[0]!.musterloesung).toContain('GEHEIM-EINS');
});
it('kommt für den zuständigen Ausbilder jederzeit mit', () => {
const sicht = sichtbareKontrolle(kontrolle, ausbilder, stand(), bezug);
expect(sicht.musterloesungSichtbar).toBe(true);
expect(sicht.fragen[1]!.musterloesung).toContain('GEHEIM-ZWEI');
});
it('kommt für einen fremden Ausbilder nie mit', () => {
const sicht = sichtbareKontrolle(
kontrolle,
fremderAusbilder,
stand({ abgegebenAm: '2026-09-21T08:00:00.000Z' }),
bezug,
);
expect(sicht.musterloesungSichtbar).toBe(false);
expect(enthaeltGeheimnis(sicht)).toBe(false);
});
it('kommt für einen anderen Azubi nie mit', () => {
const sicht = sichtbareKontrolle(
kontrolle,
andererAzubi,
stand({ abgegebenAm: '2026-09-21T08:00:00.000Z' }),
bezug,
);
expect(sicht.musterloesungSichtbar).toBe(false);
expect(enthaeltGeheimnis(sicht)).toBe(false);
});
it('kommt bei gesperrtem Konto nie mit', () => {
const sicht = sichtbareKontrolle(
kontrolle,
{ ...azubi, aktiv: 0 },
stand({ abgegebenAm: '2026-09-21T08:00:00.000Z' }),
bezug,
);
expect(enthaeltGeheimnis(sicht)).toBe(false);
});
it('kommt ohne Ausbildungsverhältnis für den Ausbilder nicht mit', () => {
const sicht = sichtbareKontrolle(kontrolle, ausbilder, stand(), null);
expect(enthaeltGeheimnis(sicht)).toBe(false);
});
it('lässt Frage, Punkte und Titel immer durch – nur die Lösung fehlt', () => {
const sicht = sichtbareKontrolle(kontrolle, azubi, stand(), bezug);
expect(sicht.titel).toBe('Lernzielkontrolle Woche 3');
expect(sicht.fragen[0]!.frage).toContain('Blockgröße');
expect(sicht.fragen[0]!.punkte).toBe(8);
expect(sicht.gesamtpunkte).toBe(13);
});
});
describe('Fragen sehen', () => {
it('zeigt dem Azubi vor der Freigabe nichts', () => {
expect(darfFragenSehen(azubi, stand(), bezug)).toBe(false);
});
it('zeigt sie ihm ab der Freigabe', () => {
expect(darfFragenSehen(azubi, stand({ freigegebenAm: '2026-09-21T07:00:00.000Z' }), bezug)).toBe(true);
});
it('zeigt sie dem zuständigen Ausbilder jederzeit – er muss sie vorher durchsehen', () => {
expect(darfFragenSehen(ausbilder, stand(), bezug)).toBe(true);
});
it('zeigt sie einem fremden Ausbilder nie', () => {
expect(darfFragenSehen(fremderAusbilder, stand({ freigegebenAm: '2026-09-21T07:00:00.000Z' }), bezug)).toBe(false);
});
it('zeigt sie einem anderen Azubi nie', () => {
expect(darfFragenSehen(andererAzubi, stand({ freigegebenAm: '2026-09-21T07:00:00.000Z' }), bezug)).toBe(false);
});
});
describe('Bearbeiten', () => {
it('geht nur zwischen Freigabe und Abgabe', () => {
expect(darfBearbeiten(azubi, stand())).toBe(false);
expect(darfBearbeiten(azubi, stand({ freigegebenAm: '2026-09-21T07:00:00.000Z' }))).toBe(true);
expect(
darfBearbeiten(azubi, stand({ freigegebenAm: '2026-09-21T07:00:00.000Z', abgegebenAm: '2026-09-21T08:00:00.000Z' })),
).toBe(false);
});
it('geht für den Ausbilder nie – er korrigiert, er schreibt nicht mit', () => {
expect(darfBearbeiten(ausbilder, stand({ freigegebenAm: '2026-09-21T07:00:00.000Z' }))).toBe(false);
});
it('geht für einen anderen Azubi nie', () => {
expect(darfBearbeiten(andererAzubi, stand({ freigegebenAm: '2026-09-21T07:00:00.000Z' }))).toBe(false);
});
it('geht bei gesperrtem Konto nicht', () => {
expect(darfBearbeiten({ ...azubi, aktiv: 0 }, stand({ freigegebenAm: '2026-09-21T07:00:00.000Z' }))).toBe(false);
});
});
describe('Zustand', () => {
it('benennt die vier Stufen', () => {
expect(zustandVon(stand())).toBe('gesperrt');
expect(zustandVon(stand({ freigegebenAm: 'x' }))).toBe('freigegeben');
expect(zustandVon(stand({ freigegebenAm: 'x', abgegebenAm: 'y' }))).toBe('abgegeben');
expect(zustandVon({ ...stand({ freigegebenAm: 'x', abgegebenAm: 'y' }), korrigiertAm: 'z' })).toBe('korrigiert');
});
});
describe('Gesamtpunkte', () => {
it('summiert die Punkte aller Fragen', () => {
expect(gesamtpunkte(kontrolle)).toBe(13);
});
it('kommt mit einer Kontrolle ohne Fragen zurecht', () => {
expect(gesamtpunkte({ fragen: [] })).toBe(0);
});
});