Files
breakpilot-compliance/admin-compliance/lib/sdk/__tests__/compliance-scope-engine.test.ts
Benjamin Admin 237c05a94c
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 36s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 26s
CI / test-python-dsms-gateway (push) Successful in 20s
fix: Profil-State nach Backend-Load in SDK-Context sync + alle Tests fixen
- Company Profile: setCompanyProfile() und COMPLETE_STEP dispatch nach Backend-Load,
  damit Sidebar-Checkmarks nach Refresh erhalten bleiben
- compliance-scope-engine.test.ts: Property-Namen anpassen (composite_score, risk_score, etc.)
- dsfa/types.test.ts: 9 Sections (0-8), 7 required, 2 optional
- api-client.test.ts: SDKApiClient-Klasse statt nicht-existierendem Singleton
- types.test.ts: use-case-assessment statt use-case-workshop
- vitest.config.ts: E2E-Verzeichnis aus Vitest excluden

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 22:58:36 +01:00

373 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from 'vitest'
import { complianceScopeEngine } from '../compliance-scope-engine'
// Helper: create an answer object (engine reads value + questionId)
function ans(questionId: string, value: unknown) {
return { questionId, value } as any
}
// Helper: create a minimal triggered-trigger (engine shape, not types shape)
function trigger(ruleId: string, minimumLevel: string, opts: Record<string, unknown> = {}) {
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_score).toBe(0)
expect(scores.risk_score).toBe(0)
expect(scores.complexity_score).toBe(0)
expect(scores.assurance_need).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_score).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_score).toBeGreaterThan(scoresFalse.risk_score)
})
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_score * 0.4 + scores.complexity_score * 0.3 + scores.assurance_need * 0.3) * 10) / 10
expect(scores.composite_score).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_score).toBeGreaterThan(scoresLow.composite_score)
})
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_score).toBeGreaterThan(scores1.composite_score)
})
it('empty array answer → zero contribution', () => {
const scoresEmpty = complianceScopeEngine.calculateScores([ans('data_art9', [])])
const scoresNone = complianceScopeEngine.calculateScores([])
expect(scoresEmpty.composite_score).toBe(scoresNone.composite_score)
})
})
// ============================================================================
// determineLevel
// ============================================================================
describe('determineLevel', () => {
it('composite ≤25 → L1', () => {
const level = complianceScopeEngine.determineLevel({ composite_score: 20 } as any, [])
expect(level).toBe('L1')
})
it('composite exactly 25 → L1', () => {
const level = complianceScopeEngine.determineLevel({ composite_score: 25 } as any, [])
expect(level).toBe('L1')
})
it('composite 2650 → L2', () => {
const level = complianceScopeEngine.determineLevel({ composite_score: 40 } as any, [])
expect(level).toBe('L2')
})
it('composite exactly 50 → L2', () => {
const level = complianceScopeEngine.determineLevel({ composite_score: 50 } as any, [])
expect(level).toBe('L2')
})
it('composite 5175 → L3', () => {
const level = complianceScopeEngine.determineLevel({ composite_score: 60 } as any, [])
expect(level).toBe('L3')
})
it('composite >75 → L4', () => {
const level = complianceScopeEngine.determineLevel({ composite_score: 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_score: 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_score: 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_score: 60 } as any, [t])
expect(level).toBe('L3')
})
it('no triggers with zero composite → L1', () => {
const level = complianceScopeEngine.determineLevel({ composite_score: 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_score).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()
})
})