import { describe, it, expect } from 'vitest' import { complianceScopeEngine } from '../compliance-scope-engine' // Helper: create an answer object (engine reads answerValue + questionId) function ans(questionId: string, answerValue: unknown) { return { questionId, answerValue } as any } // Helper: create a minimal triggered-trigger (engine shape, not types shape) function trigger(ruleId: string, minimumLevel: string, opts: Record = {}) { return { ruleId, minimumLevel, mandatoryDocuments: [], requiresDSFA: false, category: 'test', ...opts } as any } // ============================================================================ // calculateScores // ============================================================================ describe('calculateScores', () => { it('returns zero composite for empty answers', () => { const scores = complianceScopeEngine.calculateScores([]) expect(scores.composite).toBe(0) expect(scores.risk).toBe(0) expect(scores.complexity).toBe(0) expect(scores.assurance).toBe(0) }) it('all-false boolean answers → zero composite', () => { const scores = complianceScopeEngine.calculateScores([ ans('data_art9', false), ans('data_minors', false), ans('proc_ai_usage', false), ]) expect(scores.composite).toBe(0) }) it('boolean true answer increases risk score', () => { const scoresFalse = complianceScopeEngine.calculateScores([ans('data_art9', false)]) const scoresTrue = complianceScopeEngine.calculateScores([ans('data_art9', true)]) expect(scoresTrue.risk).toBeGreaterThan(scoresFalse.risk) }) it('composite is weighted sum: risk×0.4 + complexity×0.3 + assurance×0.3', () => { const scores = complianceScopeEngine.calculateScores([ans('data_art9', true)]) const expected = Math.round((scores.risk * 0.4 + scores.complexity * 0.3 + scores.assurance * 0.3) * 10) / 10 expect(scores.composite).toBe(expected) }) it('numeric answer uses logarithmic normalization — higher value → higher score', () => { const scoresLow = complianceScopeEngine.calculateScores([ans('data_volume', 10)]) const scoresHigh = complianceScopeEngine.calculateScores([ans('data_volume', 999)]) expect(scoresHigh.composite).toBeGreaterThan(scoresLow.composite) }) it('array answer score proportional to count (max 1.0 at 5+)', () => { const scores1 = complianceScopeEngine.calculateScores([ans('data_art9', ['gesundheit'])]) const scores5 = complianceScopeEngine.calculateScores([ ans('data_art9', ['gesundheit', 'biometrie', 'genetik', 'politisch', 'religion']), ]) expect(scores5.composite).toBeGreaterThan(scores1.composite) }) it('empty array answer → zero contribution', () => { const scoresEmpty = complianceScopeEngine.calculateScores([ans('data_art9', [])]) const scoresNone = complianceScopeEngine.calculateScores([]) expect(scoresEmpty.composite).toBe(scoresNone.composite) }) }) // ============================================================================ // determineLevel // ============================================================================ describe('determineLevel', () => { it('composite ≤25 → L1', () => { const level = complianceScopeEngine.determineLevel({ composite: 20 } as any, []) expect(level).toBe('L1') }) it('composite exactly 25 → L1', () => { const level = complianceScopeEngine.determineLevel({ composite: 25 } as any, []) expect(level).toBe('L1') }) it('composite 26–50 → L2', () => { const level = complianceScopeEngine.determineLevel({ composite: 40 } as any, []) expect(level).toBe('L2') }) it('composite exactly 50 → L2', () => { const level = complianceScopeEngine.determineLevel({ composite: 50 } as any, []) expect(level).toBe('L2') }) it('composite 51–75 → L3', () => { const level = complianceScopeEngine.determineLevel({ composite: 60 } as any, []) expect(level).toBe('L3') }) it('composite >75 → L4', () => { const level = complianceScopeEngine.determineLevel({ composite: 80 } as any, []) expect(level).toBe('L4') }) it('hard trigger with minimumLevel L3 overrides score-based L1', () => { const t = trigger('HT-A01', 'L3') const level = complianceScopeEngine.determineLevel({ composite: 10 } as any, [t]) expect(level).toBe('L3') }) it('hard trigger with minimumLevel L4 overrides score-based L2', () => { const t = trigger('HT-F01', 'L4') const level = complianceScopeEngine.determineLevel({ composite: 40 } as any, [t]) expect(level).toBe('L4') }) it('level = max(score-level, trigger-level) — score wins when higher', () => { const t = trigger('HT-B01', 'L2') // score gives L3, trigger gives L2 → max = L3 const level = complianceScopeEngine.determineLevel({ composite: 60 } as any, [t]) expect(level).toBe('L3') }) it('no triggers with zero composite → L1', () => { const level = complianceScopeEngine.determineLevel({ composite: 0 } as any, []) expect(level).toBe('L1') }) }) // ============================================================================ // evaluateHardTriggers // ============================================================================ describe('evaluateHardTriggers', () => { it('empty answers → no triggers', () => { const triggers = complianceScopeEngine.evaluateHardTriggers([]) expect(triggers).toHaveLength(0) }) it('art9 health data answer triggers category art9 with minimumLevel L3', () => { const triggers = complianceScopeEngine.evaluateHardTriggers([ ans('data_art9', ['gesundheit']), ]) const art9 = triggers.find((t: any) => t.category === 'art9') expect(art9).toBeDefined() expect((art9 as any).minimumLevel).toBe('L3') }) it('minors answer sets requiresDSFA = true', () => { const triggers = complianceScopeEngine.evaluateHardTriggers([ ans('data_minors', true), ]) const minorsTrigger = triggers.find((t: any) => t.category === 'vulnerable') expect(minorsTrigger).toBeDefined() expect((minorsTrigger as any).requiresDSFA).toBe(true) }) it('AI scoring usage answer triggers minimumLevel L2 (adm category)', () => { const triggers = complianceScopeEngine.evaluateHardTriggers([ ans('proc_ai_usage', ['scoring']), ]) const aiTrigger = triggers.find((t: any) => t.category === 'adm') expect(aiTrigger).toBeDefined() expect(['L2', 'L3', 'L4']).toContain((aiTrigger as any).minimumLevel) }) it('multiple triggers all returned when multiple questions answered', () => { const triggers = complianceScopeEngine.evaluateHardTriggers([ ans('data_art9', ['gesundheit']), ans('data_minors', true), ]) expect(triggers.length).toBeGreaterThanOrEqual(2) }) it('false answer for boolean trigger does not fire it', () => { const triggersBefore = complianceScopeEngine.evaluateHardTriggers([]) const triggersWithFalse = complianceScopeEngine.evaluateHardTriggers([ ans('data_minors', false), ]) expect(triggersWithFalse.length).toBe(triggersBefore.length) }) }) // ============================================================================ // evaluate — integration // ============================================================================ describe('evaluate — integration', () => { it('empty answers → L1 ScopeDecision with all required fields', () => { const decision = complianceScopeEngine.evaluate([]) expect(decision.scores).toBeDefined() expect(decision.triggeredHardTriggers).toBeDefined() expect(decision.requiredDocuments).toBeDefined() expect(decision.riskFlags).toBeDefined() expect(decision.gaps).toBeDefined() expect(decision.nextActions).toBeDefined() expect(decision.reasoning).toBeDefined() expect(decision.determinedLevel).toBe('L1') expect(decision.evaluatedAt).toBeDefined() }) it('art9 + minors answers → determinedLevel L3 or L4', () => { const decision = complianceScopeEngine.evaluate([ ans('data_art9', ['gesundheit', 'biometrie']), ans('data_minors', true), ]) expect(['L3', 'L4']).toContain(decision.determinedLevel) }) it('triggeredHardTriggers populated on evaluate with art9 answer', () => { const decision = complianceScopeEngine.evaluate([ ans('data_art9', ['gesundheit']), ]) expect(decision.triggeredHardTriggers.length).toBeGreaterThan(0) }) it('composite score non-negative', () => { const decision = complianceScopeEngine.evaluate([ans('org_employee_count', '50-249')]) expect((decision.scores as any).composite).toBeGreaterThanOrEqual(0) }) it('evaluate returns array types for collections', () => { const decision = complianceScopeEngine.evaluate([]) expect(Array.isArray(decision.triggeredHardTriggers)).toBe(true) expect(Array.isArray(decision.requiredDocuments)).toBe(true) expect(Array.isArray(decision.riskFlags)).toBe(true) expect(Array.isArray(decision.gaps)).toBe(true) expect(Array.isArray(decision.nextActions)).toBe(true) }) }) // ============================================================================ // buildDocumentScope // ============================================================================ describe('buildDocumentScope', () => { it('returns an array', () => { const docs = complianceScopeEngine.buildDocumentScope('L1', [], []) expect(Array.isArray(docs)).toBe(true) }) it('each returned document has documentType, label, estimatedEffort', () => { // Use a trigger with uppercase docType to test engine behaviour const t = trigger('HT-A01', 'L3', { category: 'art9', mandatoryDocuments: ['VVT'], }) const docs = complianceScopeEngine.buildDocumentScope('L3', [t], []) docs.forEach((doc: any) => { expect(doc.documentType).toBeDefined() expect(doc.label).toBeDefined() expect(typeof doc.estimatedEffort).toBe('number') expect(doc.estimatedEffort).toBeGreaterThan(0) }) }) it('mandatory documents have high priority', () => { const t = trigger('HT-A01', 'L3', { category: 'art9', mandatoryDocuments: ['VVT'], }) const docs = complianceScopeEngine.buildDocumentScope('L3', [t], []) const mandatoryDocs = docs.filter((d: any) => d.requirement === 'mandatory') mandatoryDocs.forEach((doc: any) => { expect(doc.priority).toBe('high') }) }) it('documents sorted: mandatory first', () => { const decision = complianceScopeEngine.evaluate([ ans('data_art9', ['gesundheit']), ]) const docs = decision.requiredDocuments if (docs.length > 1) { // mandatory docs should appear before recommended ones let seenNonMandatory = false for (const doc of docs) { if ((doc as any).requirement !== 'mandatory') seenNonMandatory = true if (seenNonMandatory) { expect((doc as any).requirement).not.toBe('mandatory') } } } }) it('sdkStepUrl present for known document types when available', () => { const decision = complianceScopeEngine.evaluate([ ans('data_art9', ['gesundheit']), ]) // sdkStepUrl is optional — just verify it's a string when present decision.requiredDocuments.forEach((doc: any) => { if (doc.sdkStepUrl !== undefined) { expect(typeof doc.sdkStepUrl).toBe('string') } }) }) }) // ============================================================================ // evaluateRiskFlags // ============================================================================ describe('evaluateRiskFlags', () => { it('no answers → empty flags array', () => { const flags = complianceScopeEngine.evaluateRiskFlags([], 'L1') expect(Array.isArray(flags)).toBe(true) }) it('no encryption transit at L2+ → high risk flag (technical)', () => { const flags = complianceScopeEngine.evaluateRiskFlags( [ans('tech_encryption_transit', false)], 'L2', ) const encFlag = flags.find((f: any) => f.category === 'technical') expect(encFlag).toBeDefined() expect((encFlag as any).severity).toBe('high') }) it('no encryption rest at L2+ → high risk flag (technical)', () => { const flags = complianceScopeEngine.evaluateRiskFlags( [ans('tech_encryption_rest', false)], 'L2', ) const encFlag = flags.find((f: any) => f.category === 'technical') expect(encFlag).toBeDefined() expect((encFlag as any).severity).toBe('high') }) it('encryption flags not raised at L1', () => { const flags = complianceScopeEngine.evaluateRiskFlags( [ans('tech_encryption_transit', false)], 'L1', ) const encFlag = flags.find((f: any) => f.message?.includes('Verschlüsselung')) expect(encFlag).toBeUndefined() }) it('third country transfer without guarantees → legal flag', () => { const flags = complianceScopeEngine.evaluateRiskFlags( [ ans('tech_third_country', true), ans('tech_hosting_location', 'drittland'), ], 'L1', ) const legalFlag = flags.find((f: any) => f.category === 'legal') expect(legalFlag).toBeDefined() }) it('≥250 employees without DPO → organizational flag', () => { const flags = complianceScopeEngine.evaluateRiskFlags( [ ans('org_has_dpo', false), ans('org_employee_count', '250-999'), ], 'L2', ) const orgFlag = flags.find((f: any) => f.category === 'organizational') expect(orgFlag).toBeDefined() }) it('DPO present with 250+ employees → no DPO flag', () => { const flags = complianceScopeEngine.evaluateRiskFlags( [ ans('org_has_dpo', true), ans('org_employee_count', '250-999'), ], 'L2', ) const orgFlag = flags.find((f: any) => f.category === 'organizational') expect(orgFlag).toBeUndefined() }) })