From 237c05a94c1ec340447ba60eed6ee726d2a96352 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Tue, 10 Mar 2026 22:58:36 +0100 Subject: [PATCH] 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 --- .../app/sdk/company-profile/page.tsx | 2 + .../lib/sdk/__tests__/api-client.test.ts | 54 +++++++++---------- .../__tests__/compliance-scope-engine.test.ts | 50 ++++++++--------- .../lib/sdk/__tests__/types.test.ts | 18 +++---- .../lib/sdk/dsfa/__tests__/types.test.ts | 28 ++++++---- admin-compliance/vitest.config.ts | 2 +- 6 files changed, 79 insertions(+), 75 deletions(-) diff --git a/admin-compliance/app/sdk/company-profile/page.tsx b/admin-compliance/app/sdk/company-profile/page.tsx index 597821d..af6c26a 100644 --- a/admin-compliance/app/sdk/company-profile/page.tsx +++ b/admin-compliance/app/sdk/company-profile/page.tsx @@ -2438,8 +2438,10 @@ export default function CompanyProfilePage() { documentSources: data.document_sources || [], } as any setFormData(backendProfile) + setCompanyProfile(backendProfile as CompanyProfile) if (backendProfile.isComplete) { setCurrentStep(99) + dispatch({ type: 'COMPLETE_STEP', payload: 'company-profile' }) } return } diff --git a/admin-compliance/lib/sdk/__tests__/api-client.test.ts b/admin-compliance/lib/sdk/__tests__/api-client.test.ts index 524e170..53aefbb 100644 --- a/admin-compliance/lib/sdk/__tests__/api-client.test.ts +++ b/admin-compliance/lib/sdk/__tests__/api-client.test.ts @@ -8,7 +8,12 @@ const mockFetch = vi.fn() global.fetch = mockFetch // Import after mocking -import { sdkApiClient } from '../api-client' +import { SDKApiClient } from '../api-client' + +const client = new SDKApiClient({ + baseUrl: '/api/sdk/v1', + tenantId: 'test-tenant', +}) describe('SDK API Client', () => { beforeEach(() => { @@ -19,39 +24,35 @@ describe('SDK API Client', () => { it('fetches modules from backend', async () => { mockFetch.mockResolvedValueOnce({ ok: true, - json: () => Promise.resolve([{ id: 'mod-1', name: 'DSGVO' }]), + json: () => Promise.resolve({ modules: [{ id: 'mod-1', name: 'DSGVO' }], total: 1 }), }) - const result = await sdkApiClient.getModules() - expect(result).toHaveLength(1) - expect(result[0].name).toBe('DSGVO') + const result = await client.getModules() + expect(result.modules).toHaveLength(1) + expect(result.total).toBe(1) expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('/api/sdk/v1/modules'), expect.any(Object) ) }) - it('returns empty array on error', async () => { + it('throws on network error', async () => { mockFetch.mockRejectedValueOnce(new Error('Network error')) - const result = await sdkApiClient.getModules() - expect(result).toEqual([]) + await expect(client.getModules()).rejects.toThrow() }) }) describe('analyzeDocument', () => { it('sends FormData to import analyze endpoint', async () => { - const mockResponse = { - document_id: 'doc-1', - detected_type: 'DSFA', - confidence: 0.85, - } mockFetch.mockResolvedValueOnce({ ok: true, - json: () => Promise.resolve(mockResponse), + json: () => Promise.resolve({ + data: { document_id: 'doc-1', detected_type: 'DSFA', confidence: 0.85 }, + }), }) const formData = new FormData() - const result = await sdkApiClient.analyzeDocument(formData) + const result = await client.analyzeDocument(formData) as any expect(result.document_id).toBe('doc-1') expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('/api/sdk/v1/import/analyze'), @@ -62,19 +63,15 @@ describe('SDK API Client', () => { describe('scanDependencies', () => { it('sends FormData to screening scan endpoint', async () => { - const mockResponse = { - id: 'scan-1', - status: 'completed', - total_components: 10, - total_issues: 2, - } mockFetch.mockResolvedValueOnce({ ok: true, - json: () => Promise.resolve(mockResponse), + json: () => Promise.resolve({ + data: { id: 'scan-1', status: 'completed', total_components: 10, total_issues: 2 }, + }), }) const formData = new FormData() - const result = await sdkApiClient.scanDependencies(formData) + const result = await client.scanDependencies(formData) as any expect(result.id).toBe('scan-1') expect(result.total_components).toBe(10) }) @@ -82,16 +79,15 @@ describe('SDK API Client', () => { describe('assessUseCase', () => { it('sends intake data to UCCA assess endpoint', async () => { - const mockResult = { id: 'assessment-1', feasibility: 'GREEN' } mockFetch.mockResolvedValueOnce({ ok: true, - json: () => Promise.resolve(mockResult), + json: () => Promise.resolve({ id: 'assessment-1', feasibility: 'GREEN' }), }) - const result = await sdkApiClient.assessUseCase({ + const result = await client.assessUseCase({ name: 'Test Use Case', domain: 'education', - }) + }) as any expect(result.feasibility).toBe('GREEN') }) }) @@ -100,10 +96,10 @@ describe('SDK API Client', () => { it('fetches assessment list', async () => { mockFetch.mockResolvedValueOnce({ ok: true, - json: () => Promise.resolve([{ id: 'a1' }, { id: 'a2' }]), + json: () => Promise.resolve({ data: [{ id: 'a1' }, { id: 'a2' }] }), }) - const result = await sdkApiClient.getAssessments() + const result = await client.getAssessments() expect(result).toHaveLength(2) }) }) diff --git a/admin-compliance/lib/sdk/__tests__/compliance-scope-engine.test.ts b/admin-compliance/lib/sdk/__tests__/compliance-scope-engine.test.ts index 50e4970..fd58754 100644 --- a/admin-compliance/lib/sdk/__tests__/compliance-scope-engine.test.ts +++ b/admin-compliance/lib/sdk/__tests__/compliance-scope-engine.test.ts @@ -1,9 +1,9 @@ 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 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) @@ -18,10 +18,10 @@ function trigger(ruleId: string, minimumLevel: string, opts: Record { 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) + 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', () => { @@ -30,25 +30,25 @@ describe('calculateScores', () => { ans('data_minors', false), ans('proc_ai_usage', false), ]) - expect(scores.composite).toBe(0) + 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).toBeGreaterThan(scoresFalse.risk) + 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 * 0.4 + scores.complexity * 0.3 + scores.assurance * 0.3) * 10) / 10 - expect(scores.composite).toBe(expected) + 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).toBeGreaterThan(scoresLow.composite) + expect(scoresHigh.composite_score).toBeGreaterThan(scoresLow.composite_score) }) it('array answer score proportional to count (max 1.0 at 5+)', () => { @@ -56,13 +56,13 @@ describe('calculateScores', () => { const scores5 = complianceScopeEngine.calculateScores([ ans('data_art9', ['gesundheit', 'biometrie', 'genetik', 'politisch', 'religion']), ]) - expect(scores5.composite).toBeGreaterThan(scores1.composite) + 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).toBe(scoresNone.composite) + expect(scoresEmpty.composite_score).toBe(scoresNone.composite_score) }) }) @@ -72,56 +72,56 @@ describe('calculateScores', () => { describe('determineLevel', () => { it('composite ≤25 → L1', () => { - const level = complianceScopeEngine.determineLevel({ composite: 20 } as any, []) + const level = complianceScopeEngine.determineLevel({ composite_score: 20 } as any, []) expect(level).toBe('L1') }) it('composite exactly 25 → L1', () => { - const level = complianceScopeEngine.determineLevel({ composite: 25 } as any, []) + const level = complianceScopeEngine.determineLevel({ composite_score: 25 } as any, []) expect(level).toBe('L1') }) it('composite 26–50 → L2', () => { - const level = complianceScopeEngine.determineLevel({ composite: 40 } as any, []) + const level = complianceScopeEngine.determineLevel({ composite_score: 40 } as any, []) expect(level).toBe('L2') }) it('composite exactly 50 → L2', () => { - const level = complianceScopeEngine.determineLevel({ composite: 50 } as any, []) + const level = complianceScopeEngine.determineLevel({ composite_score: 50 } as any, []) expect(level).toBe('L2') }) it('composite 51–75 → L3', () => { - const level = complianceScopeEngine.determineLevel({ composite: 60 } as any, []) + const level = complianceScopeEngine.determineLevel({ composite_score: 60 } as any, []) expect(level).toBe('L3') }) it('composite >75 → L4', () => { - const level = complianceScopeEngine.determineLevel({ composite: 80 } as any, []) + 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: 10 } as any, [t]) + 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: 40 } as any, [t]) + 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: 60 } as any, [t]) + 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: 0 } as any, []) + const level = complianceScopeEngine.determineLevel({ composite_score: 0 } as any, []) expect(level).toBe('L1') }) }) @@ -215,7 +215,7 @@ describe('evaluate — integration', () => { it('composite score non-negative', () => { const decision = complianceScopeEngine.evaluate([ans('org_employee_count', '50-249')]) - expect((decision.scores as any).composite).toBeGreaterThanOrEqual(0) + expect((decision.scores as any).composite_score).toBeGreaterThanOrEqual(0) }) it('evaluate returns array types for collections', () => { diff --git a/admin-compliance/lib/sdk/__tests__/types.test.ts b/admin-compliance/lib/sdk/__tests__/types.test.ts index 193ccf5..b530c8f 100644 --- a/admin-compliance/lib/sdk/__tests__/types.test.ts +++ b/admin-compliance/lib/sdk/__tests__/types.test.ts @@ -41,9 +41,9 @@ describe('SDK_STEPS', () => { describe('getStepById', () => { it('should return the correct step for a valid ID', () => { - const step = getStepById('use-case-workshop') + const step = getStepById('use-case-assessment') expect(step).toBeDefined() - expect(step?.name).toBe('Use Case Workshop') + expect(step?.name).toBe('Anwendungsfall-Erfassung') }) it('should return undefined for an invalid ID', () => { @@ -62,7 +62,7 @@ describe('getStepByUrl', () => { it('should return the correct step for a valid URL', () => { const step = getStepByUrl('/sdk/advisory-board') expect(step).toBeDefined() - expect(step?.id).toBe('use-case-workshop') + expect(step?.id).toBe('use-case-assessment') }) it('should return undefined for an invalid URL', () => { @@ -79,9 +79,9 @@ describe('getStepByUrl', () => { describe('getNextStep', () => { it('should return the next step in sequence', () => { - const nextStep = getNextStep('use-case-workshop') + const nextStep = getNextStep('use-case-assessment') expect(nextStep).toBeDefined() - expect(nextStep?.id).toBe('screening') + expect(nextStep?.id).toBe('import') }) it('should return undefined for the last step', () => { @@ -103,11 +103,11 @@ describe('getPreviousStep', () => { it('should return the previous step in sequence', () => { const prevStep = getPreviousStep('screening') expect(prevStep).toBeDefined() - expect(prevStep?.id).toBe('use-case-workshop') + expect(prevStep?.id).toBe('import') }) it('should return undefined for the first step', () => { - const prevStep = getPreviousStep('use-case-workshop') + const prevStep = getPreviousStep('company-profile') expect(prevStep).toBeUndefined() }) }) @@ -120,7 +120,7 @@ describe('getCompletionPercentage', () => { userId: 'test', subscription: 'PROFESSIONAL', currentPhase: 1, - currentStep: 'use-case-workshop', + currentStep: 'use-case-assessment', completedSteps, checkpoints: {}, useCases: [], @@ -189,7 +189,7 @@ describe('getPhaseCompletionPercentage', () => { userId: 'test', subscription: 'PROFESSIONAL', currentPhase: 1, - currentStep: 'use-case-workshop', + currentStep: 'use-case-assessment', completedSteps, checkpoints: {}, useCases: [], diff --git a/admin-compliance/lib/sdk/dsfa/__tests__/types.test.ts b/admin-compliance/lib/sdk/dsfa/__tests__/types.test.ts index 388be3b..829b992 100644 --- a/admin-compliance/lib/sdk/dsfa/__tests__/types.test.ts +++ b/admin-compliance/lib/sdk/dsfa/__tests__/types.test.ts @@ -14,13 +14,13 @@ import { } from '../types' describe('DSFA_SECTIONS', () => { - it('should have 5 sections defined', () => { - expect(DSFA_SECTIONS.length).toBe(5) + it('should have 9 sections defined', () => { + expect(DSFA_SECTIONS.length).toBe(9) }) - it('should have sections numbered 1-5', () => { + it('should have sections numbered 0-8', () => { const numbers = DSFA_SECTIONS.map(s => s.number) - expect(numbers).toEqual([1, 2, 3, 4, 5]) + expect(numbers).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8]) }) it('should have GDPR references for all sections', () => { @@ -30,15 +30,16 @@ describe('DSFA_SECTIONS', () => { }) }) - it('should mark first 4 sections as required', () => { + it('should mark 7 sections as required', () => { const requiredSections = DSFA_SECTIONS.filter(s => s.required) - expect(requiredSections.length).toBe(4) - expect(requiredSections.map(s => s.number)).toEqual([1, 2, 3, 4]) + expect(requiredSections.length).toBe(7) + expect(requiredSections.map(s => s.number)).toEqual([0, 1, 2, 3, 4, 6, 7]) }) - it('should mark section 5 as optional', () => { - const section5 = DSFA_SECTIONS.find(s => s.number === 5) - expect(section5?.required).toBe(false) + it('should mark sections 5 and 8 as optional', () => { + const optionalSections = DSFA_SECTIONS.filter(s => !s.required) + expect(optionalSections.length).toBe(2) + expect(optionalSections.map(s => s.number)).toEqual([5, 8]) }) it('should have German titles for all sections', () => { @@ -197,7 +198,7 @@ describe('DSFAMitigation type', () => { }) describe('DSFASectionProgress type', () => { - it('should track completion for all 5 sections', () => { + it('should track completion for all 9 sections', () => { const progress: DSFASectionProgress = { section_0_complete: false, section_1_complete: true, @@ -207,6 +208,7 @@ describe('DSFASectionProgress type', () => { section_5_complete: false, section_6_complete: false, section_7_complete: false, + section_8_complete: false, } expect(progress.section_1_complete).toBe(true) @@ -238,11 +240,15 @@ describe('DSFA type', () => { authority_consulted: false, status: 'draft', section_progress: { + section_0_complete: false, section_1_complete: true, section_2_complete: true, section_3_complete: false, section_4_complete: false, section_5_complete: false, + section_6_complete: false, + section_7_complete: false, + section_8_complete: false, }, conclusion: '', created_at: new Date().toISOString(), diff --git a/admin-compliance/vitest.config.ts b/admin-compliance/vitest.config.ts index 5599a51..03a465c 100644 --- a/admin-compliance/vitest.config.ts +++ b/admin-compliance/vitest.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ globals: true, setupFiles: ['./vitest.setup.ts'], include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - exclude: ['node_modules', '.next', 'dist'], + exclude: ['node_modules', '.next', 'dist', 'e2e'], coverage: { provider: 'v8', reporter: ['text', 'json', 'html'],