import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { expect, it } from 'vitest'; import type { GpoBackupStatusRecord } from '../../src/api/assessor-api-contract'; import { GpoStatusRow } from '../../src/features/project-import/GpoStatusRow'; import { summarizeClientProcessingReadiness } from '../../src/features/project-import/datahub-processing-readiness'; const record: GpoBackupStatusRecord = { version: 1, artifactId: 'synthetic-artifact', fileName: 'Synthetic.zip', sizeBytes: 12, submittedAt: '', updatedAt: '', state: 'succeeded', attempts: 1, maxAttempts: 3, gpoCount: 1, backupInstanceCount: 1, evaluationUpdates: [{ evaluationRunId: 'old', aggregationRunId: 'aggregation', baselineAnalysisIds: ['baseline'], artifactIds: ['synthetic-artifact'], previousVersion: '1.3.3', currentVersion: '1.4.0', reasons: ['engine-version'] }] }; it('makes the client actionable and renders an explicit evaluation action, not an upload request', () => { expect(summarizeClientProcessingReadiness([], [], [record])).toMatchObject({ state: 'update' }); const html = renderToStaticMarkup(createElement(GpoStatusRow, { projectId: 'synthetic-project', record, imported: 'Synthetic date', busy: false, onEvaluate() {}, onReprocess() {}, onRetry() {}, onDelete() {} })); expect(html).toContain('Re-evaluation required'); expect(html).toContain('>Re-evaluate'); expect(html).not.toContain('>Ready<'); expect(html).toContain('no file upload is required'); expect(summarizeClientProcessingReadiness([], [], [{ ...record, evaluationUpdates: [] }])).toMatchObject({ state: 'ready' }); expect(summarizeClientProcessingReadiness([], [], [{ ...record, state: 'dead-letter' }])).toMatchObject({ state: 'error' }); }); it('shows partial GPO processing as a technical analysis limit without requesting manual review', () => { const html = renderToStaticMarkup(createElement(GpoStatusRow, { projectId: 'synthetic-project', record: { ...record, state: 'partial', evaluationUpdates: [] }, imported: 'Synthetic date', busy: false, onEvaluate() {}, onReprocess() {}, onRetry() {}, onDelete() {} })); expect(html).toContain('Limited analysis'); expect(html).not.toContain('review required'); expect(html).not.toContain('>Imported<'); }); it('offers the same re-evaluation when only the baseline analysis generation changed', () => { const current: GpoBackupStatusRecord = { ...record, evaluationUpdates: [{ ...record.evaluationUpdates![0], previousVersion: '1.4.0', currentVersion: '1.4.0', baselineAnalysisIds: ['baseline-current'], reasons: ['baseline-analysis'] }] }; expect(summarizeClientProcessingReadiness([], [], [current])).toMatchObject({ state: 'update' }); const html = renderToStaticMarkup(createElement(GpoStatusRow, { projectId: 'synthetic-project', record: current, imported: 'Synthetic date', busy: false, onEvaluate() {}, onReprocess() {}, onRetry() {}, onDelete() {} })); expect(html).toContain('Re-evaluation required'); expect(html).toContain('Updated Microsoft baseline analysis'); expect(html).not.toContain('Engine 1.4.0'); expect(html).toContain('>Re-evaluate'); }); it('asks for reprocessing, not re-evaluation, when the retained analysis uses an older analyzer', () => { const outdated: GpoBackupStatusRecord = { ...record, evaluationUpdates: [], analysisUpdate: { identityAnalysisRunId: 'analysis-1', expectedLatestProcessingRunId: 'processing-1', previousAnalyzerVersion: 'gpo-backup-policy-analysis:2.2.3', currentAnalyzerVersion: 'gpo-backup-policy-analysis:2.2.4' } }; expect(summarizeClientProcessingReadiness([], [], [outdated])).toMatchObject({ state: 'update' }); const html = renderToStaticMarkup(createElement(GpoStatusRow, { projectId: 'synthetic-project', record: outdated, imported: 'Synthetic date', busy: false, onEvaluate() {}, onReprocess() {}, onRetry() {}, onDelete() {} })); expect(html).toContain('Reprocessing required'); expect(html).toContain('Analyzer gpo-backup-policy-analysis:2.2.3 → gpo-backup-policy-analysis:2.2.4'); // Reprocessing rebuilds retained parsing output, so it replaces the re-evaluation action. expect(html).toContain('>Reprocess'); expect(html).not.toContain('>Re-evaluate'); expect(html).toContain('no file upload is required'); }); it('shows the number of GPOs without a link once link facts are retained', () => { const withLinks: GpoBackupStatusRecord = { ...record, evaluationUpdates: [], gpoCount: 184, backupInstanceCount: 184, notLinkedGpoCount: 29 }; const html = renderToStaticMarkup(createElement(GpoStatusRow, { projectId: 'synthetic-project', record: withLinks, imported: 'Synthetic date', busy: false, onEvaluate() {}, onReprocess() {}, onRetry() {}, onDelete() {} })); expect(html).toContain('184 GPOs'); expect(html).toContain(' · 29 not linked'); const unknown = renderToStaticMarkup(createElement(GpoStatusRow, { projectId: 'synthetic-project', record: { ...withLinks, notLinkedGpoCount: undefined }, imported: 'Synthetic date', busy: false, onEvaluate() {}, onReprocess() {}, onRetry() {}, onDelete() {} })); expect(unknown).not.toContain('not linked'); });