Assessor_2.0/tests/features/gpo-source-recovery-interac...

104 lines
6.7 KiB
TypeScript

// @vitest-environment jsdom
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, expect, it, vi } from 'vitest';
import { AssessorApiClient } from '../../src/api/assessor-api-client';
import type { GpoBackupStatusRecord } from '../../src/api/assessor-api-contract';
import { GpoFolderImport } from '../../src/features/project-import/GpoFolderImport';
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-source', assessmentId: 'synthetic-slot', jobId: 'synthetic-job',
fileName: 'Synthetic.zip', sizeBytes: 3, submittedAt: '2026-09-17T10:00:00Z', updatedAt: '2026-09-17T10:00:00Z',
state: 'upload-required', attempts: 0, maxAttempts: 3, gpoCount: 1, backupInstanceCount: 1,
analysisUpdate: { identityAnalysisRunId: 'analysis', expectedLatestProcessingRunId: 'run',
previousAnalyzerVersion: 'old', currentAnalyzerVersion: 'new' }
};
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); });
it.each(['row ZIP', 'row folder', 'panel ZIP', 'panel folder'])('restores the exact missing source through the %s action', async path => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
let current = record;
const list = vi.spyOn(AssessorApiClient.prototype, 'listProjectGpoBackupStatus').mockImplementation(async () => ({
version: 1, projectId: 'synthetic-project', generatedAt: record.updatedAt, records: [current]
}));
const submit = vi.spyOn(AssessorApiClient.prototype, 'submitGpoBackup').mockImplementation(async () => {
current = { ...record, state: 'partial', analysisUpdate: undefined };
return { version: 1, artifactId: record.artifactId, jobId: record.jobId!, sha256: 'a'.repeat(64), sizeBytes: 3,
status: 'queued', deduplicated: true, sourceRestored: true, repositoryRevision: 1 };
});
const retry = vi.fn();
const rootElement = document.createElement('div');
const root = createRoot(rootElement);
const renderRow = () => <GpoStatusRow projectId="synthetic-project" record={current} imported="Synthetic date" busy={false}
onRetry={retry} onEvaluate={vi.fn()} onReprocess={vi.fn()} onDelete={vi.fn()}/>;
try {
await act(async () => root.render(path.startsWith('row') ? renderRow() : <GpoFolderImport projectId="synthetic-project"/>));
expect(rootElement.textContent).toContain('Upload required');
expect(rootElement.textContent).toContain('Saved results remain available');
expect(rootElement.textContent).not.toContain('no file upload is required');
expect([...rootElement.querySelectorAll('button')].some(button => button.textContent === 'Retry')).toBe(false);
const isFolder = path.endsWith('folder');
const button = [...rootElement.querySelectorAll('button')].find(item => item.textContent === `Upload original ${isFolder ? 'folder' : 'ZIP'}`)!;
expect(button).toBeTruthy();
const input = button.closest('.gpo-folder-actions')!.querySelector<HTMLInputElement>(isFolder ? 'input[multiple]' : 'input[accept]')!;
const click = vi.spyOn(input, 'click').mockImplementation(() => undefined);
await act(async () => button.click());
expect(click).toHaveBeenCalledOnce();
const file = new File(['abc'], isFolder ? 'Backup.xml' : 'renamed-original.zip', { type: 'application/zip' });
Object.defineProperty(file, 'arrayBuffer', { value: async () => new TextEncoder().encode('abc').buffer });
Object.defineProperty(file, 'webkitRelativePath', { value: isFolder ? 'Synthetic/instance/Backup.xml' : '' });
Object.defineProperty(input, 'files', { value: [file], configurable: true });
await act(async () => {
input.dispatchEvent(new Event('change', { bubbles: true }));
await vi.waitFor(() => expect(submit).toHaveBeenCalledOnce());
});
expect(submit).toHaveBeenCalledOnce();
expect(submit.mock.calls[0][0]).toBe('synthetic-project');
expect(submit.mock.calls[0][2]).toBe(record.assessmentId);
expect(submit.mock.calls[0][3]).toBe(record.artifactId);
expect(retry).not.toHaveBeenCalled();
expect(list).toHaveBeenCalled();
if (path.startsWith('row')) await act(async () => root.render(renderRow()));
expect(rootElement.textContent).not.toContain('Upload required');
expect(summarizeClientProcessingReadiness([], [], [current]).state).toBe('ready');
} finally { await act(async () => root.unmount()); }
});
it('keeps unavailable storage actionable without suggesting an upload or reprocess', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const unavailable = { ...record, state: 'storage-unavailable' as const };
const container = document.createElement('div');
const root = createRoot(container);
try {
await act(async () => root.render(<GpoStatusRow projectId="synthetic-project" record={unavailable}
imported="Synthetic date" busy={false} onRetry={vi.fn()} onEvaluate={vi.fn()} onReprocess={vi.fn()} onDelete={vi.fn()}/>));
expect(container.textContent).toContain('Storage unavailable');
expect(container.textContent).toContain('connection and permissions');
expect(container.querySelector('input[type="file"]')).toBeNull();
expect(container.textContent).not.toContain('no file upload is required');
expect(summarizeClientProcessingReadiness([], [], [unavailable]).state).toBe('error');
expect(summarizeClientProcessingReadiness([], [], [record]).state).toBe('error');
} finally { await act(async () => root.unmount()); }
});
it('still allows an evaluation-only update from retained inputs when the original file is missing', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const current: GpoBackupStatusRecord = { ...record, analysisUpdate: undefined, jobStatus: 'succeeded',
evaluationUpdates: [{ evaluationRunId: 'evaluation', aggregationRunId: 'aggregation', baselineAnalysisIds: ['baseline'],
artifactIds: [record.artifactId], previousVersion: '1.8.0', currentVersion: '1.9.0', reasons: ['engine-version'] }] };
const container = document.createElement('div');
const root = createRoot(container);
const evaluate = vi.fn();
try {
await act(async () => root.render(<GpoStatusRow projectId="synthetic-project" record={current} imported="Synthetic date"
busy={false} onRetry={vi.fn()} onEvaluate={evaluate} onReprocess={vi.fn()} onDelete={vi.fn()}/>));
const button = [...container.querySelectorAll('button')].find(item => item.textContent === 'Re-evaluate')!;
expect(button.disabled).toBe(false);
await act(async () => button.click());
expect(evaluate).toHaveBeenCalledOnce();
expect(container.textContent).toContain('Retained inputs can still be re-evaluated');
} finally { await act(async () => root.unmount()); }
});