waffensachkunde

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

/ app tests verwandt.test.ts

5,1 KB Rohdatei
app/tests/verwandt.test.ts — 139 Zeilen
1 /**
2 * Querverweise zwischen verwandten Fragen.
3 *
4 * **Der Anlass.** Der amtliche Katalog kennt keine Frage-zu-Frage-Beziehung,
5 * obwohl die Bündel auf der Hand liegen: II-34 fragt, was ein Schalldämpfer
6 * bewirkt, II-36 nach der Umkehrung – die Erklärung zu II-36 sagt das selbst
7 * im Text. Wer eine davon verstanden hat, erfuhr bis 0.22.0 nie, dass es die
8 * anderen gibt.
9 *
10 * Geprüft wird die Sammelregel an gebauten Fällen und der **ausgelieferte
11 * Bestand** auf Beidseitigkeit: Ein einseitiger Verweis zeigt die Verbindung
12 * nur dem, der zufällig von der richtigen Seite kommt.
13 */
14
15 import { readFileSync } from 'node:fs';
16 import { join } from 'node:path';
17
18 import { describe, expect, it } from 'vitest';
19
20 import { verwandteSammeln, type Erklaerung, type Erklaerungen } from '../src/shared/erklaerungen';
21 import type { Katalog } from '../src/shared/katalog';
22
23 const WURZEL = join(process.cwd(), '..');
24 const ERKLAERUNGEN = JSON.parse(
25 readFileSync(join(WURZEL, 'content', 'erklaerungen.json'), 'utf8'),
26 ) as Erklaerungen;
27 const KATALOG = JSON.parse(
28 readFileSync(join(WURZEL, 'content', 'katalog', 'katalog.json'), 'utf8'),
29 ) as Katalog;
30
31 function nachschlagen(bestand: Record<string, readonly string[]>) {
32 return (frageId: string): Erklaerung | null => {
33 const verwandt = bestand[frageId];
34 return verwandt === undefined
35 ? null
36 : { kurz: 'k', text: 't', fundstellen: [], ohneFundstelleGrund: 'g', verwandt };
37 };
38 }
39
40 describe('verwandteSammeln', () => {
41 it('sammelt die Verwandten mehrerer Fragen', () => {
42 const zu = nachschlagen({ 'II-34': ['II-33', 'II-36'], 'I.5-02': ['I.5-03'] });
43
44 expect(verwandteSammeln(zu, ['II-34', 'I.5-02'])).toEqual(['II-33', 'II-36', 'I.5-03']);
45 });
46
47 it('lässt weg, was schon in der Sitzung stand', () => {
48 // Wer II-33 gerade bearbeitet hat, braucht sie nicht als „verwandt“
49 // vorgeschlagen zu bekommen.
50 const zu = nachschlagen({ 'II-34': ['II-33', 'II-36'], 'II-33': ['II-34', 'II-36'] });
51
52 expect(verwandteSammeln(zu, ['II-34', 'II-33'])).toEqual(['II-36']);
53 });
54
55 it('nennt keine Frage zweimal', () => {
56 const zu = nachschlagen({ 'II-34': ['II-36'], 'II-33': ['II-36'] });
57
58 expect(verwandteSammeln(zu, ['II-34', 'II-33'])).toEqual(['II-36']);
59 });
60
61 it('behält die Reihenfolge der Begegnung bei', () => {
62 /* Nach Kennung zu sortieren ginge schief: „I.2-99“ kommt im Katalog vor
63 „I.2-135“, lexikografisch steht es dahinter. */
64 const zu = nachschlagen({ a: ['I.2-135'], b: ['I.2-99'] });
65
66 expect(verwandteSammeln(zu, ['a', 'b'])).toEqual(['I.2-135', 'I.2-99']);
67 });
68
69 it('kommt mit Fragen ohne Erklärung zurecht', () => {
70 expect(verwandteSammeln(() => null, ['II-34'])).toEqual([]);
71 expect(verwandteSammeln(nachschlagen({}), [])).toEqual([]);
72 });
73 });
74
75 describe('Der ausgelieferte Bestand', () => {
76 const mitVerwandten = Object.entries(ERKLAERUNGEN.zuFrage).filter(
77 ([, eintrag]) => eintrag.verwandt !== undefined,
78 );
79 const bekannt = new Set(KATALOG.fragen.map((frage) => frage.id));
80
81 it('führt überhaupt Querverweise', () => {
82 expect(mitVerwandten.length).toBeGreaterThan(50);
83 });
84
85 it('verweist nur auf Fragen, die es gibt', () => {
86 const unbekannt = mitVerwandten.flatMap(([frageId, eintrag]) =>
87 (eintrag.verwandt ?? [])
88 .filter((ziel) => !bekannt.has(ziel))
89 .map((ziel) => `${frageId} -> ${ziel}`),
90 );
91
92 expect(unbekannt).toEqual([]);
93 });
94
95 it('verweist nirgends auf sich selbst', () => {
96 const selbstbezug = mitVerwandten
97 .filter(([frageId, eintrag]) => (eintrag.verwandt ?? []).includes(frageId))
98 .map(([frageId]) => frageId);
99
100 expect(selbstbezug).toEqual([]);
101 });
102
103 it('hält jeden Verweis beidseitig', () => {
104 /* Die eigentliche Zusage. Ein einseitiger Verweis ist fast immer ein
105 Versehen beim Schreiben – und er zeigt die Verbindung nur dem, der
106 zufällig von der richtigen Seite kommt. */
107 const einseitig = mitVerwandten.flatMap(([frageId, eintrag]) =>
108 (eintrag.verwandt ?? [])
109 .filter((ziel) => !(ERKLAERUNGEN.zuFrage[ziel]?.verwandt ?? []).includes(frageId))
110 .map((ziel) => `${frageId} nennt ${ziel}, aber nicht zurück`),
111 );
112
113 expect(einseitig).toEqual([]);
114 });
115
116 it('nennt keine Frage doppelt in derselben Liste', () => {
117 const doppelt = mitVerwandten
118 .filter(([, eintrag]) => {
119 const liste = eintrag.verwandt ?? [];
120 return new Set(liste).size !== liste.length;
121 })
122 .map(([frageId]) => frageId);
123
124 expect(doppelt).toEqual([]);
125 });
126
127 it('kennt das Schalldämpfer-Bündel, mit dem der Befund begann', () => {
128 // II-36 nennt sich in der eigenen Erklärung „die Umkehrung von Frage 34“.
129 expect(ERKLAERUNGEN.zuFrage['II-36']?.verwandt).toContain('II-34');
130 expect(ERKLAERUNGEN.zuFrage['II-34']?.verwandt).toContain('II-36');
131 });
132
133 it('verbindet auch über Kapitelgrenzen hinweg', () => {
134 /* I.1-07 fragt nach der waffenrechtlichen Einordnung des
135 Double-Action-Revolvers, II-47 nach der Technik der Double-Action-
136 Pistole. Genau solche Paare findet man im Katalog nicht von allein. */
137 expect(ERKLAERUNGEN.zuFrage['I.1-07']?.verwandt).toContain('II-47');
138 });
139 });