// Akzeptanztest der Regel-Engine (Story F3/B2). Deterministische Assertions gegen // C2 §5 (Q-FEAT-01…10) und §8 (Aufgaben-Trigger). Kein DB-Zugriff, reine Logik. // Lauf: npx tsx scripts/test-rules.ts (Exit 1 bei Fehler). import { evaluateRules, makeContext } from "../src/lib/rules/engine"; import { ALL_RULES, FEATURE_RULES, Q } from "../src/lib/rules/feature-rules"; import { triggerById } from "../src/lib/task-triggers"; import type { Answer } from "../src/lib/rules/dsl"; let failures = 0; const ok = (cond: boolean, msg: string) => { console.log(`${cond ? "✓" : "✗ FEHLER"} ${msg}`); if (!cond) failures++; }; const evalAnswers = (answers: Record, pruefziele?: string[]) => evaluateRules(ALL_RULES, makeContext({ answers, ...(pruefziele ? { pruefziele } : {}) })); // — Q-FEAT-02 Cloud — { const on = evalAnswers({ [Q.CLOUD]: true }); ok(on.flags.FLAG_CLOUD_USED === true, "Q-FEAT-02=ja → FLAG_CLOUD_USED"); ok(["5.3.2", "5.3.4", "6.1.3"].every((c) => on.controls.includes(c)), "Q-FEAT-02=ja → Controls 5.3.2/5.3.4/6.1.3"); ok(on.risks.includes("R-CLOUD"), "Q-FEAT-02=ja → Risiko R-CLOUD"); ok(on.matched.includes("feat02-cloud"), "Q-FEAT-02=ja → Regel feat02-cloud ausgelöst"); const off = evalAnswers({ [Q.CLOUD]: false }); ok(off.flags.FLAG_CLOUD_USED !== true, "Q-FEAT-02=nein → kein FLAG_CLOUD_USED"); ok(!off.matched.includes("feat02-cloud"), "Q-FEAT-02=nein → feat02-cloud NICHT ausgelöst"); ok(!off.controls.includes("5.3.2"), "Q-FEAT-02=nein → Control 5.3.2 nicht im Scope"); } // — A2-1: Schutzbedarf kommt zentral aus dem Assessment-Level (Seed), NICHT aus dem Fragebogen — { // Ohne AL-Seed setzt keine Fragebogen-Antwort HIGH/VERY_HIGH. const noSeed = evalAnswers({ [Q.CLOUD]: true, [Q.EXTERNAL_IT]: true }); ok(noSeed.flags.FLAG_HIGH_PROTECTION !== true && noSeed.flags.FLAG_VERY_HIGH_PROTECTION !== true, "Fragebogen setzt HIGH/VERY_HIGH nicht"); ok(noSeed.flags.FLAG_ELEVATED_PROTECTION === false, "ohne AL-Seed → ELEVATED aus"); // AL2-Seed → HIGH an, VERY_HIGH aus; AL3-Seed → beide an; ELEVATED jeweils abgeleitet. const al2 = evaluateRules(ALL_RULES, makeContext({ flags: { FLAG_HIGH_PROTECTION: true, FLAG_VERY_HIGH_PROTECTION: false } })); ok(al2.flags.FLAG_HIGH_PROTECTION === true && al2.flags.FLAG_VERY_HIGH_PROTECTION !== true, "AL2-Seed → HIGH an, VERY_HIGH aus"); ok(al2.flags.FLAG_ELEVATED_PROTECTION === true, "AL2-Seed → ELEVATED abgeleitet an"); const al3 = evaluateRules(ALL_RULES, makeContext({ flags: { FLAG_HIGH_PROTECTION: true, FLAG_VERY_HIGH_PROTECTION: true } })); ok(al3.flags.FLAG_VERY_HIGH_PROTECTION === true && al3.flags.FLAG_ELEVATED_PROTECTION === true, "AL3-Seed → VERY_HIGH + ELEVATED an"); } // — Q-FEAT-09 externe IT (Control + Risiko + Aufgabe) — { const r = evalAnswers({ [Q.EXTERNAL_IT]: true }); ok(r.flags.FLAG_EXTERNAL_IT === true, "Q-FEAT-09=ja → FLAG_EXTERNAL_IT"); ok(r.controls.includes("6.1.1") && r.controls.includes("6.1.3"), "Q-FEAT-09=ja → Controls 6.1.1/6.1.3"); ok(r.risks.includes("R-SUP"), "Q-FEAT-09=ja → Risiko R-SUP"); ok(r.tasks.includes("external_it_yes"), "Q-FEAT-09=ja → Aufgabe external_it_yes"); } // — C2 §8 Aufgaben-Trigger — { const cloud = evalAnswers({ [Q.CLOUD]: true }); ok(cloud.tasks.includes("cloud_ai_without_approval"), "Cloud=ja → Aufgabe cloud_ai_without_approval"); const ai = evalAnswers({ [Q.AI]: true }); ok(ai.tasks.includes("cloud_ai_without_approval"), "KI=ja → Aufgabe cloud_ai_without_approval"); const neither = evalAnswers({ [Q.CLOUD]: false, [Q.AI]: false }); ok(!neither.tasks.includes("cloud_ai_without_approval"), "Cloud/KI=nein → keine Freigabeverfahren-Aufgabe"); const isb = evalAnswers({ [Q.ISB_BENANNT]: false }); ok(isb.tasks.includes("isb_not_named"), "ISB nicht benannt → Aufgabe isb_not_named"); const restore = evalAnswers({ [Q.RESTORE_GETESTET]: false }); ok(restore.tasks.includes("no_restore_test"), "kein Restore-Test → Aufgabe no_restore_test"); } // — Determinismus: gleiche Eingabe → gleiches Ergebnis — { const a = evalAnswers({ [Q.CLOUD]: true, [Q.EXTERNAL_IT]: true }); const b = evalAnswers({ [Q.CLOUD]: true, [Q.EXTERNAL_IT]: true }); ok(JSON.stringify(a) === JSON.stringify(b), "Determinismus: identische Auswertung bei gleicher Eingabe"); } // — Integrität: alle referenzierten Trigger-IDs existieren im Katalog (B1) — { const taskIds = new Set(ALL_RULES.flatMap((r) => r.effect.tasks ?? [])); const allExist = [...taskIds].every((id) => triggerById(id) !== null); ok(allExist, `alle Aufgaben-Trigger-IDs existieren in task-triggers.ts (${[...taskIds].join(", ")})`); } // — Abdeckung: jede Q-FEAT-02…10 hat eine Regel (Q-FEAT-01/Schutzbedarf ist zentral, A2-1) — { const covered = new Set(FEATURE_RULES.map((r) => r.source)); const missing = Array.from({ length: 9 }, (_, i) => `C2 §5 Q-FEAT-${String(i + 2).padStart(2, "0")}`).filter((s) => !covered.has(s)); ok(missing.length === 0, `alle Q-FEAT-02…10 durch Regeln abgedeckt${missing.length ? " — fehlt: " + missing.join(", ") : ""}`); } // — Follow-up: Prüfziele kommen aus WizardScope (Scoping), nicht aus dem Fragebogen — { const ds = evalAnswers({}, ["informationssicherheit", "datenschutz"]); ok(ds.flags.FLAG_PERSONAL_DATA === true, "Prüfziel Datenschutz → FLAG_PERSONAL_DATA"); ok(ds.controls.includes("7.1.2") && ds.risks.includes("R-DSGVO"), "Prüfziel Datenschutz → Control 7.1.2 + Risiko R-DSGVO"); const proto = evalAnswers({}, ["informationssicherheit", "prototypenschutz"]); ok(proto.flags.FLAG_PROTOTYPE_PROTECTION === true, "Prüfziel Prototypenschutz → FLAG_PROTOTYPE_PROTECTION"); const isOnly = evalAnswers({}, ["informationssicherheit"]); ok(isOnly.flags.FLAG_PERSONAL_DATA !== true && isOnly.flags.FLAG_PROTOTYPE_PROTECTION !== true, "nur Informationssicherheit → Datenschutz/Prototyp-Flags aus"); } console.log(failures === 0 ? "\nOK — alle Regel-Tests grün" : `\nPRUEFEN — ${failures} Fehler`); process.exit(failures === 0 ? 0 : 1);