Assessor_2.0/tests/compliance/ai-overview-scope-interacti...

101 lines
4.2 KiB
TypeScript

// @vitest-environment jsdom
import { act, type ReactNode } 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 { AiOverviewResponse } from '../../src/compliance/ai-overview';
import { useAiOverview } from '../../src/compliance/ui/AiOverviewPanel';
import { activitySnapshot, clearFinishedActivities } from '../../src/platform/activity-store';
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
function deferred<T>() {
let resolve!: (value: T) => void;
const promise = new Promise<T>(next => { resolve = next; });
return { promise, resolve };
}
function response(projectId: string): AiOverviewResponse {
return {
version: 1,
projectId,
scope: { assessmentIds: [`assessment-${projectId}`], scanRunIds: [] },
generationEnabled: true,
state: 'empty',
stale: false
};
}
function HookProbe({ projectId }: { projectId: string }): ReactNode {
const controller = useAiOverview(projectId, [`assessment-${projectId}`]);
return <output>{controller.response?.projectId ?? 'empty'}</output>;
}
function GenerateProbe({ onReady }: { onReady: (generate: () => Promise<void>) => void }): ReactNode {
const controller = useAiOverview('project-a', ['assessment-project-a']);
onReady(controller.generate);
return <output>{controller.response?.state ?? 'none'}</output>;
}
it('keeps one Compliance Engine activity open while the backend generates the overview', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
clearFinishedActivities();
const generationActivity = () => activitySnapshot().filter(row => row.label === 'Generating AI overview');
const running: AiOverviewResponse = { ...response('project-a'), state: 'running', pollAfterMs: 10 };
const status = vi.spyOn(AssessorApiClient.prototype, 'getAiOverview').mockResolvedValue(response('project-a'));
vi.spyOn(AssessorApiClient.prototype, 'generateAiOverview').mockResolvedValue(running);
let generate!: () => Promise<void>;
const container = document.createElement('div');
document.body.append(container);
const root = createRoot(container);
try {
await act(async () => root.render(<GenerateProbe onReady={next => { generate = next; }} />));
expect(generationActivity()).toEqual([]);
// The start request returns at once; the activity must outlive it and every status poll.
status.mockImplementation(async () => ({ ...running }));
await act(async () => { await generate(); });
await act(async () => { await new Promise(resolve => setTimeout(resolve, 30)); });
expect(generationActivity()).toMatchObject([{ area: 'Compliance Engine', state: 'running' }]);
status.mockResolvedValue({ ...response('project-a'), state: 'succeeded' });
await act(async () => { await new Promise(resolve => setTimeout(resolve, 30)); });
expect(container.textContent).toBe('succeeded');
expect(generationActivity()).toMatchObject([{ area: 'Compliance Engine', state: 'completed' }]);
} finally {
await act(async () => root.unmount());
container.remove();
}
});
it('does not show a previous project response when it resolves after the active project', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const first = deferred<AiOverviewResponse>();
const second = deferred<AiOverviewResponse>();
const request = vi.spyOn(AssessorApiClient.prototype, 'getAiOverview').mockImplementation(projectId =>
projectId === 'project-a' ? first.promise : second.promise);
const container = document.createElement('div');
document.body.append(container);
const root = createRoot(container);
try {
await act(async () => root.render(<HookProbe projectId="project-a" />));
await act(async () => root.render(<HookProbe projectId="project-b" />));
expect(request).toHaveBeenCalledTimes(2);
await act(async () => { second.resolve(response('project-b')); await Promise.resolve(); });
expect(container.textContent).toBe('project-b');
await act(async () => { first.resolve(response('project-a')); await Promise.resolve(); });
expect(container.textContent).toBe('project-b');
} finally {
await act(async () => root.unmount());
container.remove();
}
});