waffensachkunde
Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.
| 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 | }); |