import { describe, expect, it, vi } from 'vitest'; import type { UnifiedResultProjection } from '../../src/domain/unified-result'; import { makeSheet } from '../../src/importers/sheet-factory'; import { loadCanonicalProjectWorkspace } from '../../src/features/project-import/load-canonical-project-workspace'; import { reportReadPortFrom, type ReportReadMethods } from '../../src/reporting/report-read-port'; import { assembleConsistentReportInput, createReportRun, renderReportFiles, REPORT_ENGINE_VERSION, ReportStateChangedError } from '../../src/reporting/report-run'; import { buildReportProjection } from '../../src/reporting/report-projection'; import { ReportContextRequiredError } from '../../src/reporting/report-context'; import { reportContext } from './context-report-fixture'; vi.mock('../../src/features/project-import/load-canonical-project-workspace', () => ({ loadCanonicalProjectWorkspace: vi.fn() })); const scope = { projectId: 'dummy-project', assessmentIds: ['dummy-assessment'], generatedAt: '2026-09-30T08:00:00Z' }; function dummyRepository(): ReportReadMethods & { saveReview: () => Promise } { const sheet = makeSheet('Dummy', [['Title', 'Source: Scan'], ['Synthetic check', 'PingCastle']], '', 'dummy.csv', { isProjectSheet: true }); vi.mocked(loadCanonicalProjectWorkspace).mockResolvedValue({ models: [], assessmentIds: ['dummy-assessment'], dataset: { fileName: 'Dummy', sheets: [sheet], scanTool: 'PingCastle' } as never }); const result = { resultProjectionId: 'dummy-result', sourceKind: 'scanner-finding', sourceResultId: 'dummy-source', projectId: 'dummy-project', assessmentId: 'dummy-assessment', scanRunId: 'dummy-run', collectionRunId: 'dummy-run', title: 'Synthetic hardening check', sourceTool: 'PingCastle', sourceRuleId: 'DUMMY-RULE-7', severityNormalized: 'medium', outcomeNormalized: 'fail', isDeviation: true, categories: [], frameworkReferences: [] } as unknown as UnifiedResultProjection; return { getProject: vi.fn(async () => ({ id: 'dummy-project', name: 'Dummy Customer', attributes: {}, createdAt: '', updatedAt: '' })), listAssessments: vi.fn(async () => []), loadAssessmentModel: vi.fn(async () => null), queryUnifiedResults: vi.fn(async (query?: { sourceKind?: string[] }) => { const items = query?.sourceKind?.includes('scanner-finding') ? [result] : []; return { items, total: items.length, offset: 0, limit: 500 }; }), listPolicyEvaluations: vi.fn(async () => []), listMicrosoftSecurityBaselineAnalyses: vi.fn(async () => []), getEnvironmentSnapshot: vi.fn(async () => ({ observations: [] })), listReviews: vi.fn(async () => []), listProjectProcessingRuns: vi.fn(async () => []), listGpoPolicyAnalyses: vi.fn(async () => []), listPolicySnapshotAggregations: vi.fn(async () => []), saveReview: vi.fn(async () => undefined) } as unknown as ReportReadMethods & { saveReview: () => Promise }; } describe('Report Engine run', () => { it('requires published Context for the new management run and validates scope and revision before rendering', async () => { const repository = dummyRepository(); const port = reportReadPortFrom(repository, async () => 9); await expect(createReportRun(port, scope, { requireContext: true })).rejects.toBeInstanceOf(ReportContextRequiredError); await expect(createReportRun(port, scope, { requireContext: true, readContext: async () => ({ ...reportContext(), assessmentIds: ['wrong-slot'] }) })) .rejects.toBeInstanceOf(ReportContextRequiredError); await expect(createReportRun(port, scope, { requireContext: true, readContext: async () => reportContext(8) })) .rejects.toBeInstanceOf(ReportStateChangedError); const readContext = vi.fn(async () => reportContext()); const run = await createReportRun(port, scope, { requireContext: true, readContext }); expect(run.files.management).toContain('Context-Heatmap'); expect(run.files.management).not.toContain('secret-result-one'); expect(run.files.technical).toContain('secret-result-one'); expect(renderReportFiles(JSON.parse(JSON.stringify(run.projection)))).toEqual(run.files); expect(repository.saveReview).not.toHaveBeenCalled(); const technicalOnly = await createReportRun(port, scope, { requireContext: false }); expect(technicalOnly.files.management).toBe(''); expect(technicalOnly.files.technical.length).toBeGreaterThan(0); const retry = vi.fn().mockResolvedValueOnce(reportContext(8)).mockResolvedValue(reportContext(9)); const consistent = await createReportRun(port, scope, { readContext: retry }); expect(consistent.projection.context?.repositoryRevision).toBe(consistent.revision); expect(retry).toHaveBeenCalledTimes(2); }); it('exposes only the read methods, so a writing method cannot be reached through the port', () => { const repository = dummyRepository(); const port = reportReadPortFrom(repository, async () => 1); expect((port as unknown as Record).saveReview).toBeUndefined(); expect(Object.isFrozen(port)).toBe(true); expect(Object.keys(port).sort()).toEqual([ 'currentRevision', 'getEnvironmentSnapshot', 'getProject', 'listAssessments', 'listGpoPolicyAnalyses', 'listMicrosoftSecurityBaselineAnalyses', 'listPolicyEvaluations', 'listPolicySnapshotAggregations', 'listProjectProcessingRuns', 'listReferenceRuns', 'listReviews', 'loadAssessmentModel', 'queryUnifiedResults' ]); }); it('repeats the read when the committed revision changed during assembly and returns one consistent state', async () => { const revisions = [4, 5, 5, 5]; const repository = dummyRepository(); const port = reportReadPortFrom(repository, async () => revisions.shift() ?? 5); const { input, revision } = await assembleConsistentReportInput(port, scope); expect(revision).toBe(5); expect(input.project.name).toBe('Dummy Customer'); expect(repository.getProject).toHaveBeenCalledTimes(2); expect(repository.saveReview).not.toHaveBeenCalled(); }); it('refuses a report instead of mixing states when the data keeps changing', async () => { let revision = 0; const port = reportReadPortFrom(dummyRepository(), async () => revision++); await expect(assembleConsistentReportInput(port, scope)).rejects.toBeInstanceOf(ReportStateChangedError); }); it('renders byte-identical files again from the archived projection JSON', async () => { const run = await createReportRun(reportReadPortFrom(dummyRepository(), async () => 9), scope, { readContext: async () => reportContext() }); expect(run.revision).toBe(9); expect(run.versions.engine).toBe(REPORT_ENGINE_VERSION); const restored = JSON.parse(JSON.stringify(run.projection, null, 2)); expect(renderReportFiles(restored)).toEqual(run.files); const rebuilt = buildReportProjection((await assembleConsistentReportInput(reportReadPortFrom(dummyRepository(), async () => 9), scope)).input); rebuilt.context = reportContext(); rebuilt.managementContextRequired = true; expect(run.files).toEqual(renderReportFiles(rebuilt)); expect(run.files.management).not.toContain('DUMMY-RULE-7'); expect(run.files.technical).toContain('DUMMY-RULE-7'); }); });