waffensachkunde

Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.

/ app tests lernplan.test.ts

20,5 KB Rohdatei
app/tests/lernplan.test.ts — 618 Zeilen
1 /**
2 * Prüft die Prüfungstermin-Schicht über FSRS.
3 *
4 * Alles hier ist eigene Entscheidung und nicht durch eine Referenz gedeckt –
5 * deshalb wird geprüft, was die Regeln *zusagen*, nicht welche Zahlen gerade
6 * herauskommen. Ein Test, der nur die aktuelle Ausgabe festschreibt, würde bei
7 * jeder Feinjustierung rot, ohne dass etwas kaputt wäre.
8 */
9
10 import { describe, expect, it } from 'vitest';
11
12 import {
13 ANHEBUNG_AB_TAGEN,
14 einfuehrungstage,
15 lernplanBerechnen,
16 machbarkeitFuer,
17 MAX_INTERVALL_TAGE,
18 SEKUNDEN_PRO_FRAGE_VORGABE,
19 SITZUNGSUMFANG,
20 VORSCHAU_TAGE,
21 wiedervorlageBerechnen,
22 zielquoteFuer,
23 ZIELQUOTE_GRUND,
24 ZIELQUOTE_TERMIN,
25 type PlanFrage,
26 } from '../src/shared/lernplan';
27 import { abrufwahrscheinlichkeit } from '../src/shared/fsrs';
28
29 function frage(teil: Partial<PlanFrage> = {}): PlanFrage {
30 /* `bestaetigt: true` als Vorgabe: Diese Datei prüft die Planung, nicht die
31 Belegregel. Ohne den Beleg wäre jede Prognose hier null, und die Tests
32 prüften nichts mehr. Die Regel selbst steht in `reife.test.ts`. */
33 return {
34 bereich: 'I.1',
35 stabilitaet: null,
36 tageSeitAntwort: 0,
37 bestaetigt: true,
38 faellig: false,
39 ...teil,
40 };
41 }
42
43 /** `anzahl` gleichartige Fragen – spart Wiederholung in den Fällen unten. */
44 function fragen(anzahl: number, teil: Partial<PlanFrage> = {}): PlanFrage[] {
45 return Array.from({ length: anzahl }, () => frage(teil));
46 }
47
48 describe('Zielquote', () => {
49 it('bleibt ohne Termin beim Vorgabewert', () => {
50 expect(zielquoteFuer(null)).toBe(ZIELQUOTE_GRUND);
51 });
52
53 it('bleibt weit vor dem Termin beim Vorgabewert', () => {
54 expect(zielquoteFuer(ANHEBUNG_AB_TAGEN)).toBe(ZIELQUOTE_GRUND);
55 expect(zielquoteFuer(ANHEBUNG_AB_TAGEN + 100)).toBe(ZIELQUOTE_GRUND);
56 });
57
58 it('erreicht am Prüfungstag die höchste Stufe', () => {
59 expect(zielquoteFuer(0)).toBeCloseTo(ZIELQUOTE_TERMIN, 10);
60 });
61
62 it('steigt dazwischen streng monoton', () => {
63 let vorher = zielquoteFuer(ANHEBUNG_AB_TAGEN);
64 for (let tage = ANHEBUNG_AB_TAGEN - 1; tage >= 0; tage -= 1) {
65 const jetzt = zielquoteFuer(tage);
66 expect(jetzt).toBeGreaterThan(vorher);
67 vorher = jetzt;
68 }
69 });
70
71 it('bleibt nach einem verstrichenen Termin oben', () => {
72 // Wer den Termin verpasst hat, lernt vermutlich für einen Nachholtermin.
73 // Die Quote darf dann nicht auf den Ruhewert zurückfallen.
74 expect(zielquoteFuer(-1)).toBeCloseTo(ZIELQUOTE_TERMIN, 10);
75 expect(zielquoteFuer(-90)).toBeCloseTo(ZIELQUOTE_TERMIN, 10);
76 });
77 });
78
79 describe('Wiedervorlage', () => {
80 it('macht eine Frage bei „nochmal“ sofort wieder fällig', () => {
81 expect(wiedervorlageBerechnen(null, 1, 0, null).intervallTage).toBe(0);
82 expect(
83 wiedervorlageBerechnen({ stabilitaet: 200, schwierigkeit: 2 }, 1, 30, null).intervallTage,
84 ).toBe(0);
85 });
86
87 it('schreibt den Gedächtnisstand auch bei „nochmal“ fort', () => {
88 // Intervall 0 ist eine Entscheidung der Oberfläche, kein Zurücksetzen des
89 // Modells. Sonst ginge die Historie der Frage verloren.
90 const vorher = { stabilitaet: 40, schwierigkeit: 3 };
91 const nachher = wiedervorlageBerechnen(vorher, 1, 30, null);
92
93 expect(nachher.stabilitaet).toBeGreaterThan(0);
94 expect(nachher.stabilitaet).toBeLessThan(vorher.stabilitaet);
95 expect(nachher.schwierigkeit).toBeGreaterThan(vorher.schwierigkeit);
96 });
97
98 it('vergibt sonst nie weniger als einen Tag', () => {
99 for (const grad of [2, 3, 4] as const) {
100 expect(wiedervorlageBerechnen(null, grad, 0, null).intervallTage).toBeGreaterThanOrEqual(1);
101 }
102 });
103
104 it('überschreitet die Obergrenze nicht', () => {
105 const fest = { stabilitaet: 100_000, schwierigkeit: 1 };
106 expect(wiedervorlageBerechnen(fest, 4, 10, null).intervallTage).toBe(MAX_INTERVALL_TAGE);
107 });
108
109 it('verkürzt sich, je näher der Termin rückt', () => {
110 const stand = { stabilitaet: 60, schwierigkeit: 5 };
111 const weit = wiedervorlageBerechnen(stand, 3, 60, 200).intervallTage;
112 const mittel = wiedervorlageBerechnen(stand, 3, 60, 14).intervallTage;
113 const nah = wiedervorlageBerechnen(stand, 3, 60, 2).intervallTage;
114
115 expect(mittel).toBeLessThan(weit);
116 expect(nah).toBeLessThan(mittel);
117 });
118
119 it('lässt den Gedächtnisstand vom Termin unberührt', () => {
120 // Der Termin darf nur den Vorlagerhythmus ändern, nicht das Modell –
121 // sonst würde eine Terminänderung die gemessene Gedächtnisleistung
122 // rückwirkend verfälschen.
123 const stand = { stabilitaet: 20, schwierigkeit: 4 };
124 const ohne = wiedervorlageBerechnen(stand, 3, 20, null);
125 const mit = wiedervorlageBerechnen(stand, 3, 20, 3);
126
127 expect(mit.stabilitaet).toBe(ohne.stabilitaet);
128 expect(mit.schwierigkeit).toBe(ohne.schwierigkeit);
129 });
130 });
131
132 describe('Einführungstage', () => {
133 it('lässt bei langer Vorbereitung fast alle Tage übrig', () => {
134 expect(einfuehrungstage(100)).toBe(97);
135 });
136
137 it('behält bei sehr kurzer Vorbereitung mindestens einen Tag', () => {
138 expect(einfuehrungstage(0)).toBe(1);
139 expect(einfuehrungstage(1)).toBe(1);
140 expect(einfuehrungstage(2)).toBe(2);
141 });
142
143 it('nimmt nie mehr als den Puffer weg', () => {
144 for (let tage = 0; tage <= 400; tage += 1) {
145 const uebrig = einfuehrungstage(tage);
146 expect(uebrig).toBeGreaterThanOrEqual(1);
147 expect(uebrig).toBeLessThanOrEqual(Math.max(1, tage));
148 expect(tage - uebrig).toBeLessThanOrEqual(3);
149 }
150 });
151 });
152
153 describe('Machbarkeit', () => {
154 it('meldet fehlenden Termin gesondert', () => {
155 expect(machbarkeitFuer(0, null)).toBe('kein_termin');
156 expect(machbarkeitFuer(500, null)).toBe('kein_termin');
157 });
158
159 it('meldet einen verstrichenen Termin gesondert', () => {
160 expect(machbarkeitFuer(10, -1)).toBe('termin_vorbei');
161 });
162
163 it('stuft nach Tagesaufwand ab', () => {
164 expect(machbarkeitFuer(10, 30)).toBe('entspannt');
165 expect(machbarkeitFuer(30, 30)).toBe('machbar');
166 expect(machbarkeitFuer(60, 30)).toBe('knapp');
167 expect(machbarkeitFuer(200, 30)).toBe('zu_wenig_zeit');
168 });
169
170 it('wird mit steigendem Aufwand nie günstiger', () => {
171 const rang = ['entspannt', 'machbar', 'knapp', 'zu_wenig_zeit'];
172 let bisher = -1;
173 for (let minuten = 0; minuten <= 300; minuten += 1) {
174 const stufe = rang.indexOf(machbarkeitFuer(minuten, 30));
175 expect(stufe).toBeGreaterThanOrEqual(bisher);
176 bisher = stufe;
177 }
178 });
179 });
180
181 describe('Lernplan', () => {
182 it('zählt nie beantwortete Fragen und Fällige getrennt', () => {
183 const plan = lernplanBerechnen({
184 termin: '2026-09-01',
185 tageBisTermin: 30,
186 fragen: [
187 ...fragen(10),
188 ...fragen(5, { stabilitaet: 10, tageSeitAntwort: 12, faellig: true }),
189 ...fragen(3, { stabilitaet: 10, tageSeitAntwort: 1, faellig: false }),
190 ],
191 sekundenProFrage: null,
192 });
193
194 expect(plan.gesamtFragen).toBe(18);
195 expect(plan.nieBeantwortet).toBe(10);
196 expect(plan.faellig).toBe(5);
197 });
198
199 it('wertet eine nie beantwortete Frage als nicht gekonnt', () => {
200 const plan = lernplanBerechnen({
201 termin: null,
202 tageBisTermin: null,
203 fragen: fragen(100),
204 sekundenProFrage: null,
205 });
206
207 expect(plan.prognoseHeute).toBe(0);
208 });
209
210 it('meldet auch bei eben beantwortetem Katalog nie volle Prognose', () => {
211 /*
212 Dieser Test hat bis Fassung 0.11.0 das Gegenteil verlangt – er prüfte
213 auf „fast genau 1“ und schrieb damit eine Unwahrheit fest:
214 `abrufwahrscheinlichkeit(S, 0)` ist für JEDES S exakt 1, auch für eine
215 Frage, die eben erst zum ersten Mal aufgetaucht ist. Während einer
216 Sitzung wanderte die Prognose deshalb gegen 100 Prozent, und zwar
217 unabhängig davon, wie gut jemand geantwortet hatte.
218
219 Seit `reife.ts` wird nie mit weniger als einem Tag Abstand gerechnet.
220 Der Wert bleibt hoch – 30 Tage Stabilität sind viel –, aber er ist
221 keine Zusage mehr.
222 */
223 const plan = lernplanBerechnen({
224 termin: null,
225 tageBisTermin: null,
226 fragen: fragen(50, { stabilitaet: 30, tageSeitAntwort: 0 }),
227 sekundenProFrage: null,
228 });
229
230 expect(plan.prognoseHeute).toBeLessThan(1);
231 expect(plan.prognoseHeute).toBeCloseTo(abrufwahrscheinlichkeit(30, 1), 10);
232 });
233
234 it('zeigt in der Prognose zum Termin, was Nichtstun kostet', () => {
235 const plan = lernplanBerechnen({
236 termin: '2026-04-01',
237 tageBisTermin: 40,
238 fragen: fragen(50, { stabilitaet: 20, tageSeitAntwort: 0 }),
239 sekundenProFrage: null,
240 });
241
242 expect(plan.prognoseAmTermin).not.toBeNull();
243 expect(plan.prognoseAmTermin!).toBeLessThan(plan.prognoseHeute);
244 });
245
246 it('lässt die Prognose zum Termin ohne Termin offen', () => {
247 const plan = lernplanBerechnen({
248 termin: null,
249 tageBisTermin: null,
250 fragen: fragen(10, { stabilitaet: 5, tageSeitAntwort: 1 }),
251 sekundenProFrage: null,
252 });
253
254 expect(plan.prognoseAmTermin).toBeNull();
255 });
256
257 it('verteilt neue Fragen über die verbleibende Zeit', () => {
258 const plan = lernplanBerechnen({
259 termin: '2026-04-01',
260 tageBisTermin: 40,
261 fragen: fragen(370),
262 sekundenProFrage: null,
263 });
264
265 // 370 Fragen auf 37 Einführungstage.
266 expect(plan.pensum.neu).toBe(10);
267 expect(plan.pensum.wiederholung).toBe(0);
268 expect(plan.pensum.gesamt).toBe(10);
269 });
270
271 it('verlangt bei knapper Zeit mehr pro Tag', () => {
272 const wenige = lernplanBerechnen({
273 termin: '2026-04-01',
274 tageBisTermin: 5,
275 fragen: fragen(575),
276 sekundenProFrage: null,
277 });
278 const viele = lernplanBerechnen({
279 termin: '2026-09-01',
280 tageBisTermin: 180,
281 fragen: fragen(575),
282 sekundenProFrage: null,
283 });
284
285 // 575 Fragen auf vier Einführungstage sind rund eine Stunde am Tag –
286 // anstrengend, aber machbar. Erst darunter wird es unrealistisch.
287 expect(wenige.pensum.neu).toBeGreaterThan(viele.pensum.neu);
288 expect(wenige.machbarkeit).toBe('knapp');
289 expect(viele.machbarkeit).toBe('entspannt');
290 });
291
292 it('sagt deutlich, wenn die Zeit nicht mehr reicht', () => {
293 const plan = lernplanBerechnen({
294 termin: '2026-03-03',
295 tageBisTermin: 2,
296 fragen: fragen(575),
297 sekundenProFrage: null,
298 });
299
300 expect(plan.pensum.minuten).toBeGreaterThan(90);
301 expect(plan.machbarkeit).toBe('zu_wenig_zeit');
302 });
303
304 it('verlangt nie mehr neue Fragen, als es überhaupt gibt', () => {
305 const plan = lernplanBerechnen({
306 termin: '2026-03-02',
307 tageBisTermin: 1,
308 fragen: fragen(5),
309 sekundenProFrage: null,
310 });
311
312 expect(plan.pensum.neu).toBe(5);
313 });
314
315 it('schlägt ohne Termin eine Sitzung vor', () => {
316 /* Ohne Stichtag gibt es nichts zu verteilen. „Nichts zu tun“ wäre aber
317 falsch, solange der halbe Katalog ungesehen ist – deshalb ein
318 Vorschlag in der Größe einer Lernsitzung. */
319 const plan = lernplanBerechnen({
320 termin: null,
321 tageBisTermin: null,
322 fragen: fragen(575),
323 sekundenProFrage: null,
324 });
325
326 expect(plan.pensum.neu).toBe(SITZUNGSUMFANG);
327 expect(plan.machbarkeit).toBe('kein_termin');
328 });
329
330 it('meldet ohne Termin nichts zu tun, wenn alles beantwortet ist', () => {
331 const plan = lernplanBerechnen({
332 termin: null,
333 tageBisTermin: null,
334 fragen: fragen(575, { stabilitaet: 30, tageSeitAntwort: 1 }),
335 sekundenProFrage: null,
336 });
337
338 expect(plan.pensum.gesamt).toBe(0);
339 });
340
341 it('rechnet die Dauer mit dem eigenen Tempo, wenn eines vorliegt', () => {
342 const daten = {
343 termin: '2026-04-01',
344 tageBisTermin: 10,
345 fragen: fragen(60, { stabilitaet: 1, tageSeitAntwort: 5, faellig: true }),
346 };
347
348 const schnell = lernplanBerechnen({ ...daten, sekundenProFrage: 10 });
349 const langsam = lernplanBerechnen({ ...daten, sekundenProFrage: 40 });
350
351 expect(schnell.pensum.minuten).toBe(10);
352 expect(langsam.pensum.minuten).toBe(40);
353 expect(schnell.sekundenProFrage).toBe(10);
354 });
355
356 it('greift ohne eigenes Tempo auf den Vorgabewert zurück', () => {
357 const plan = lernplanBerechnen({
358 termin: null,
359 tageBisTermin: null,
360 fragen: fragen(1),
361 sekundenProFrage: null,
362 });
363
364 expect(plan.sekundenProFrage).toBe(SEKUNDEN_PRO_FRAGE_VORGABE);
365 });
366
367 it('weist ein unsinniges Tempo ab, statt damit zu rechnen', () => {
368 for (const tempo of [0, -5]) {
369 const plan = lernplanBerechnen({
370 termin: null,
371 tageBisTermin: null,
372 fragen: fragen(1),
373 sekundenProFrage: tempo,
374 });
375 expect(plan.sekundenProFrage).toBe(SEKUNDEN_PRO_FRAGE_VORGABE);
376 }
377 });
378
379 it('kommt mit einem leeren Katalog zurecht', () => {
380 const plan = lernplanBerechnen({
381 termin: null,
382 tageBisTermin: null,
383 fragen: [],
384 sekundenProFrage: null,
385 });
386
387 expect(plan.gesamtFragen).toBe(0);
388 expect(plan.prognoseHeute).toBe(0);
389 expect(plan.pensum.gesamt).toBe(0);
390 });
391
392 it('führt nach dem Termin keine neuen Fragen mehr ein', () => {
393 const plan = lernplanBerechnen({
394 termin: '2026-01-01',
395 tageBisTermin: -30,
396 fragen: fragen(100),
397 sekundenProFrage: null,
398 });
399
400 expect(plan.pensum.neu).toBe(0);
401 expect(plan.prognoseAmTermin).toBeNull();
402 expect(plan.machbarkeit).toBe('termin_vorbei');
403 });
404 });
405
406 /*
407 Die Arbeitslast-Vorschau.
408
409 Der Plan kannte bis 0.22.0 nur „heute“. Weil die Zielquote ab
410 ANHEBUNG_AB_TAGEN Tagen vor dem Termin steigt und die Intervalle dadurch
411 zusammenrücken, türmt sich ein Wiederholungsberg, den der Lernende erst am
412 jeweiligen Morgen erfuhr.
413
414 Was hier geprüft wird, ist vor allem die **Ehrlichkeit** der Zahl: Sie ist
415 eine Untergrenze und darf nichts enthalten, was noch gar keinen Termin hat.
416 */
417 describe('Arbeitslast-Vorschau', () => {
418 it('zählt die Wiederholungen je Tag', () => {
419 const plan = lernplanBerechnen({
420 termin: null,
421 tageBisTermin: null,
422 sekundenProFrage: null,
423 fragen: [
424 ...fragen(3, { stabilitaet: 5, faelligInTagen: 0, faellig: true }),
425 ...fragen(2, { stabilitaet: 5, faelligInTagen: 1 }),
426 ...fragen(7, { stabilitaet: 5, faelligInTagen: 4 }),
427 ],
428 });
429
430 expect(plan.vorschau?.[0]).toBe(3);
431 expect(plan.vorschau?.[1]).toBe(2);
432 expect(plan.vorschau?.[4]).toBe(7);
433 expect(plan.vorschau?.[2]).toBe(0);
434 });
435
436 it('lässt nie beantwortete Fragen draußen', () => {
437 /* Sie haben keinen Termin, sondern kommen mit der Einführungsrate — eine
438 eigene, planbare Größe. Sie hier mitzuzählen machte aus der
439 Bestandsauskunft eine Vermutung. */
440 const plan = lernplanBerechnen({
441 termin: null,
442 tageBisTermin: null,
443 sekundenProFrage: null,
444 fragen: fragen(20, { stabilitaet: null, faelligInTagen: null }),
445 });
446
447 expect(plan.vorschau?.every((anzahl) => anzahl === 0)).toBe(true);
448 });
449
450 it('reicht nie über den Prüfungstermin hinaus', () => {
451 /* Eine Vorschau auf Tage nach der Prüfung wäre eine Auskunft ohne
452 Adressaten. */
453 const plan = lernplanBerechnen({
454 termin: '2026-03-05',
455 tageBisTermin: 3,
456 sekundenProFrage: null,
457 fragen: fragen(5, { stabilitaet: 5, faelligInTagen: 9 }),
458 });
459
460 expect(plan.vorschau).toHaveLength(4);
461 expect(plan.vorschau?.every((anzahl) => anzahl === 0)).toBe(true);
462 });
463
464 it('reicht ohne Termin bis zur festen Weite', () => {
465 const plan = lernplanBerechnen({
466 termin: null,
467 tageBisTermin: null,
468 sekundenProFrage: null,
469 fragen: fragen(1, { stabilitaet: 5, faelligInTagen: 0, faellig: true }),
470 });
471
472 expect(plan.vorschau).toHaveLength(VORSCHAU_TAGE + 1);
473 });
474
475 it('kommt ohne die neue Angabe zurecht', () => {
476 /* Ein Lernstand einer älteren Fassung liefert `faelligInTagen` nicht.
477 Das darf die Planung nicht zu Fall bringen. */
478 const plan = lernplanBerechnen({
479 termin: null,
480 tageBisTermin: null,
481 sekundenProFrage: null,
482 fragen: fragen(4, { stabilitaet: 5, faellig: true }),
483 });
484
485 expect(plan.vorschau?.every((anzahl) => anzahl === 0)).toBe(true);
486 });
487 });
488
489 describe('Der Prüfungstag', () => {
490 it('verlangt keine neuen Fragen mehr', () => {
491 /*
492 Der Grund ist keine Schonung, sondern eine Ableitung aus der Belegregel:
493 Eine Frage, die heute zum ersten Mal auftaucht, kann nicht mehr belegt
494 werden – dafür bräuchte es eine richtige Antwort nach mindestens einem
495 Tag Abstand, und diesen Tag gibt es nicht mehr.
496
497 Vorher stand hier `< 0`. Da `einfuehrungstage(0)` auf 1 zurückfällt,
498 ergab das den ganzen Rest: 300 ungesehene Fragen und 20 fällige wurden
499 am Morgen der Prüfung zu „Heute 320 Fragen, etwa 133 Minuten“.
500 */
501 const plan = lernplanBerechnen({
502 termin: '2026-06-15',
503 tageBisTermin: 0,
504 fragen: [
505 ...fragen(300),
506 ...fragen(20, { stabilitaet: 5, tageSeitAntwort: 9, faellig: true }),
507 ],
508 sekundenProFrage: null,
509 });
510
511 expect(plan.pensum.neu).toBe(0);
512 expect(plan.pensum.wiederholung).toBe(20);
513 expect(plan.pensum.gesamt).toBe(20);
514 });
515
516 it('lässt den Vortag unberührt', () => {
517 /* Dort ist der Beleg noch erreichbar, und mehr Tage gibt es nicht. Der
518 Randfall gehört benannt, nicht behoben – ein harter Einführungsstopp
519 über mehrere Tage verstecke ungesehene Fragen ganz. */
520 const plan = lernplanBerechnen({
521 termin: '2026-06-15',
522 tageBisTermin: 1,
523 fragen: fragen(300),
524 sekundenProFrage: null,
525 });
526
527 expect(plan.pensum.neu).toBeGreaterThan(0);
528 });
529 });
530
531 describe('Was bewusst nicht gebaut ist', () => {
532 /*
533 Diese Prüfungen halten Ablehnungen fest, die in
534 `docs/entscheidung-lernphasen.md` begründet sind. Ohne sie wäre jede
535 Ablehnung nur ein Kommentar – und Kommentare überschreibt irgendwann
536 jemand, der PLAN.md liest und die Zusage darin findet.
537 */
538 it('deckelt Wiedervorlagen nicht am Prüfungstermin', () => {
539 /*
540 Jede geprüfte Kappung schiebt alles, was über den Termin hinausginge,
541 auf den Prüfungstag selbst – gemessen 432 von 486 Fragen an einem Tag,
542 rund drei Stunden. Und sie senkt den Reifegrad, weil in dieser Anwendung
543 eine überflüssige Wiederholung nicht nur Zeit kostet: Eine falsche
544 Antwort nimmt den Beleg weg.
545 */
546 const stand = { stabilitaet: 60, schwierigkeit: 5 };
547 const vorlage = wiedervorlageBerechnen(stand, 3, 1, 10);
548
549 expect(vorlage.intervallTage).toBeGreaterThan(10);
550 });
551
552 it('kennt keinen Phasenbegriff im Lernplan', () => {
553 /*
554 Phase 1 wäre die Belegregel aus `reife.ts`, Phase 2 die Definition des
555 FSRS-Intervalls, Phase 3 rechnet der Plan von selbst nach, und Phase 4
556 wäre ein Etikett über `ANHEBUNG_AB_TAGEN`. Vier Namen für Dinge, die es
557 gibt oder die nichts ändern.
558 */
559 const plan = lernplanBerechnen({
560 termin: '2026-06-15',
561 tageBisTermin: 30,
562 fragen: fragen(100),
563 sekundenProFrage: null,
564 });
565
566 expect(Object.keys(plan)).not.toContain('phase');
567 expect(Object.keys(plan)).not.toContain('rueckstand');
568 });
569
570 it('holt ausgelassene Tage von selbst auf', () => {
571 /*
572 Die Zusage „Rückstand erhöht das Tagespensum statt still zu
573 verschieben“ ist durch die Bauart bereits erfüllt: Die Rate ergibt sich
574 täglich neu aus den ungesehenen Fragen und den Resttagen. Wer Tage
575 auslässt, bekommt danach mehr – ohne dass irgendwo „Rückstand“ stünde.
576
577 Gemessen wird mit zehn ausgelassenen Tagen und nicht mit dreien: Bei 40
578 Tagen Rest ändern drei Tage die Rate gar nicht (9 bleibt 9). Das ist
579 keine Schwäche, sondern der eigentliche Grund gegen einen
580 Rückstandsbegriff – der Plan schluckt kleine Lücken, und eine Anzeige,
581 die sie benennte, machte aus Nichts eine Mahnung.
582 */
583 const ungesehen = 300;
584 const vorher = lernplanBerechnen({
585 termin: '2026-06-15',
586 tageBisTermin: 20,
587 fragen: fragen(ungesehen),
588 sekundenProFrage: null,
589 });
590 const nachZehnTagenNichtstun = lernplanBerechnen({
591 termin: '2026-06-15',
592 tageBisTermin: 10,
593 fragen: fragen(ungesehen),
594 sekundenProFrage: null,
595 });
596
597 expect(nachZehnTagenNichtstun.pensum.neu).toBeGreaterThan(vorher.pensum.neu);
598 });
599
600 it('lässt drei ausgelassene Tage bei viel Vorlauf gar nicht auffallen', () => {
601 /* Genau deshalb gibt es keinen Rückstandsbegriff: Es gibt nichts zu
602 melden. */
603 const bei40 = lernplanBerechnen({
604 termin: '2026-06-15',
605 tageBisTermin: 40,
606 fragen: fragen(300),
607 sekundenProFrage: null,
608 });
609 const bei37 = lernplanBerechnen({
610 termin: '2026-06-15',
611 tageBisTermin: 37,
612 fragen: fragen(300),
613 sekundenProFrage: null,
614 });
615
616 expect(bei37.pensum.neu).toBe(bei40.pensum.neu);
617 });
618 });