- Manual participant entry for paper tests: name + correct answers → register entry with source 'paper' (app runs are tagged 'app') - Scan upload per register entry: marked paper sheets (image/PDF, max 1 MB) stored locally as data URL and anchored with a SHA-256 checksum of the original file; view scan in-app, checksum shown - Document verification: certificate and training register printouts carry a SHA-256 checksum footer over the document content — a presented printout can be re-verified against the tool; certificate additionally references its register entry ID (evidence chain certificate ↔ register ↔ scan) - Register printout extended with source, evidence checksum and ID columns; record deletion with confirmation warning - UI labels in all nine languages; 73 tests green Claude-Session: https://claude.ai/code/session_017YjohVNx1ZGNM4MX7tHpfv
140 lines
5.3 KiB
TypeScript
140 lines
5.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import { CONTENT_PACK } from '../content/pack';
|
||
import {
|
||
answerKeyDoc,
|
||
certificateDoc,
|
||
getQuiz,
|
||
handoutDoc,
|
||
printableTestDoc,
|
||
scoreQuiz,
|
||
trainingLogDoc,
|
||
} from './quiz';
|
||
import { QUIZ_PASS_PCT, QUIZ_QUESTIONS } from './quizContent';
|
||
import type { CompanyProfile } from './types';
|
||
|
||
const profile: CompanyProfile = {
|
||
name: 'Muster GmbH',
|
||
street: '',
|
||
zipCity: '',
|
||
contactPerson: '',
|
||
email: '',
|
||
phone: '',
|
||
website: '',
|
||
logoDataUrl: '',
|
||
countries: [],
|
||
};
|
||
|
||
describe('KI-Kompetenztest', () => {
|
||
it('hat 12 einfache Fragen mit je 3 Optionen und gültigem Lösungsindex', () => {
|
||
expect(QUIZ_QUESTIONS).toHaveLength(12);
|
||
for (const q of QUIZ_QUESTIONS) {
|
||
expect(q.options).toHaveLength(3);
|
||
expect([0, 1, 2]).toContain(q.correctIndex);
|
||
}
|
||
});
|
||
|
||
it('löst Fragen lokalisiert auf (DE und EN unterscheiden sich)', () => {
|
||
const de = getQuiz('de');
|
||
const en = getQuiz('en');
|
||
expect(de).toHaveLength(en.length);
|
||
expect(de[0]?.question).not.toBe(en[0]?.question);
|
||
});
|
||
|
||
it('wertet korrekt aus: alle richtig = bestanden, alle falsch = durchgefallen', () => {
|
||
const perfect = scoreQuiz(QUIZ_QUESTIONS.map((q) => q.correctIndex));
|
||
expect(perfect.pct).toBe(100);
|
||
expect(perfect.passed).toBe(true);
|
||
|
||
const wrong = scoreQuiz(QUIZ_QUESTIONS.map((q) => ((q.correctIndex + 1) % 3) as 0 | 1 | 2));
|
||
expect(wrong.correct).toBe(0);
|
||
expect(wrong.passed).toBe(false);
|
||
});
|
||
|
||
it('wendet die Bestehensgrenze an (knapp darunter = durchgefallen)', () => {
|
||
// 8 von 12 = 67 % < 70 % → nicht bestanden; 9 von 12 = 75 % → bestanden
|
||
const eight = scoreQuiz(
|
||
QUIZ_QUESTIONS.map((q, i) => (i < 8 ? q.correctIndex : ((q.correctIndex + 1) % 3) as 0 | 1 | 2)),
|
||
);
|
||
expect(eight.passed).toBe(false);
|
||
const nine = scoreQuiz(
|
||
QUIZ_QUESTIONS.map((q, i) => (i < 9 ? q.correctIndex : ((q.correctIndex + 1) % 3) as 0 | 1 | 2)),
|
||
);
|
||
expect(nine.passed).toBe(true);
|
||
expect(QUIZ_PASS_PCT).toBe(70);
|
||
});
|
||
|
||
it('erzeugt ein Zertifikat mit Name, Ergebnis, Art.-4-Bezug und Disclaimer', () => {
|
||
const score = scoreQuiz(QUIZ_QUESTIONS.map((q) => q.correctIndex));
|
||
const md = certificateDoc('Max Mustermann', score, CONTENT_PACK, '2026-07-04', profile);
|
||
expect(md).toContain('Nachweis KI-Kompetenz (Art. 4');
|
||
expect(md).toContain('Max Mustermann');
|
||
expect(md).toContain('BESTANDEN');
|
||
expect(md).toContain('Muster GmbH');
|
||
expect(md).toContain('keine Rechtsberatung');
|
||
});
|
||
|
||
it('Papierfassung enthält alle Fragen, Ankreuzfelder und Auswertungszeile', () => {
|
||
const md = printableTestDoc(CONTENT_PACK, '2026-07-04', profile);
|
||
expect(md).toContain('Papierfassung');
|
||
expect((md.match(/☐/g) ?? []).length).toBe(36); // 12 Fragen × 3 Optionen
|
||
expect(md).toContain('**Name:**');
|
||
expect(md).toContain('Ergebnis: ______ / 12');
|
||
expect(md).not.toContain('richtige Antwort'); // keine Lösungen im Testbogen
|
||
});
|
||
|
||
it('Lösungsbogen enthält je Frage den Fragetext, die richtige Antwort im Wortlaut und die Erklärung', () => {
|
||
const md = answerKeyDoc(CONTENT_PACK, '2026-07-04', profile);
|
||
expect(md).toContain('Lösungsbogen');
|
||
// Fragetext enthalten (selbsterklärend, ohne Testbogen daneben):
|
||
expect((md.match(/^## \d+\./gm) ?? []).length).toBe(12);
|
||
expect(md).toContain('Was macht ein KI-System');
|
||
// Richtige Antwort mit Buchstabe UND Wortlaut:
|
||
expect((md.match(/\*\*Richtige Antwort: [ABC]\)\*\*/g) ?? []).length).toBe(12);
|
||
expect(md).toContain('Nein — nur in vom Unternehmen freigegebene KI-Systeme.');
|
||
// Erklärungen enthalten:
|
||
expect((md.match(/_Erklärung:_/g) ?? []).length).toBe(12);
|
||
});
|
||
|
||
it('Schulungshandout enthält die 12 Erklärungen und ist lokalisiert', () => {
|
||
const de = handoutDoc(CONTENT_PACK, '2026-07-04', profile, 'de');
|
||
expect(de).toContain('Schulungshandout');
|
||
expect((de.match(/^## \d+\./gm) ?? []).length).toBe(12);
|
||
const en = handoutDoc(CONTENT_PACK, '2026-07-04', profile, 'en');
|
||
expect(en).toContain('Training handout');
|
||
expect(en).not.toBe(de);
|
||
});
|
||
|
||
it('erzeugt das Schulungsregister mit Quelle, Beleg-Prüfsumme und ID', () => {
|
||
const md = trainingLogDoc(
|
||
[
|
||
{
|
||
id: 'abcdef12-3456',
|
||
name: 'Max Mustermann',
|
||
date: '2026-07-04',
|
||
scorePct: 92,
|
||
passed: true,
|
||
source: 'paper',
|
||
attachment: { fileName: 'scan.pdf', sha256: 'f'.repeat(64), dataUrl: 'data:application/pdf;base64,x' },
|
||
},
|
||
{ id: '2', name: 'Erika Musterfrau', date: '2026-07-03', scorePct: 58, passed: false },
|
||
],
|
||
CONTENT_PACK,
|
||
'2026-07-04',
|
||
profile,
|
||
);
|
||
expect(md).toContain('Schulungsregister');
|
||
expect(md).toContain('| Papier |'); // Quelle Papier-Test
|
||
expect(md).toContain('| App |'); // Alt-Einträge ohne source = App
|
||
expect(md).toContain('`ffffffffffffffff…`'); // Beleg-Prüfsumme (gekürzt)
|
||
expect(md).toContain('`abcdef12`'); // Register-ID (gekürzt)
|
||
expect(md).toContain('NICHT BESTANDEN');
|
||
});
|
||
|
||
it('Zertifikat enthält die Register-ID für die Nachweiskette', () => {
|
||
const score = scoreQuiz(QUIZ_QUESTIONS.map((q) => q.correctIndex));
|
||
const md = certificateDoc('Max', score, CONTENT_PACK, '2026-07-04', profile, 'de', 'abcdef12-3456-7890');
|
||
expect(md).toContain('Register-ID');
|
||
expect(md).toContain('`abcdef12`');
|
||
});
|
||
});
|