// Copyright (c) 2025-2026, RTE (https://www.rte-france.com) // This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. // If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, // you can obtain one at http://mozilla.org/MPL/2.0/. // SPDX-License-Identifier: MPL-2.0 // This file is part of Co-Study4Grid a Power Grid Study tool Assistant Interface to help solve contigencies for a grid state under study. import { describe, it, expect } from 'vitest'; import type { AnalysisResult, ActionDetail } from '../types'; /** * Tests for the merge logic used by handleDisplayPrioritizedActions. * This verifies the regression fix: pdf_url (and other fields from the * existing result) must be preserved when merging pendingAnalysisResult. * * The actual merge logic in App.tsx is: * setResult(prev => ({ * ...prev, // keep existing fields (pdf_url, etc.) * ...pendingAnalysisResult, // overlay with analysis result * actions: { ...pendingAnalysisResult.actions, ...manualActionsData }, * })); */ function mergeAnalysisResult( prev: AnalysisResult | null, pendingAnalysisResult: AnalysisResult, selectedActionIds: Set, ): AnalysisResult { const manualActionsData: Record = {}; if (prev?.actions) { for (const [id, data] of Object.entries(prev.actions)) { if (selectedActionIds.has(id)) { manualActionsData[id] = data; } } } return { ...prev, ...pendingAnalysisResult, actions: { ...pendingAnalysisResult.actions, ...manualActionsData }, }; } describe('mergeAnalysisResult (handleDisplayPrioritizedActions logic)', () => { const makeAction = (desc: string, overrides?: Partial): ActionDetail => ({ description_unitaire: desc, rho_before: [1.0], rho_after: [0.8], max_rho: 0.8, max_rho_line: 'LINE_A', is_rho_reduction: true, ...overrides, }); it('preserves pdf_url from previous result when pending has explicit nulls', () => { // When pendingAnalysisResult explicitly includes pdf_url: null, // the spread order (...prev, ...pending) means null overwrites the URL. // This test documents that behavior — the real fix works because the // streaming 'result' event doesn't include pdf_url/pdf_path at all // (see the next test). If for any reason the API starts sending nulls, // we'd need an additional guard. const prev: AnalysisResult = { pdf_path: '/tmp/overflow.pdf', pdf_url: '/results/pdf/overflow.pdf', actions: {}, lines_overloaded: [], message: '', dc_fallback: false, }; const pending: AnalysisResult = { pdf_path: null, pdf_url: null, actions: { act_1: makeAction('New action') }, lines_overloaded: ['LINE_A'], message: 'Done', dc_fallback: false, }; const merged = mergeAnalysisResult(prev, pending, new Set()); // pending's explicit null overwrites prev's url — this is expected JS behavior. // The real scenario (no pdf fields on pending) is tested below. expect(merged.pdf_url).toBeNull(); }); it('preserves pdf_url when pending result has no pdf fields', () => { const prev: AnalysisResult = { pdf_path: '/tmp/overflow.pdf', pdf_url: '/results/pdf/overflow.pdf', actions: {}, lines_overloaded: [], message: '', dc_fallback: false, }; // Simulate what the streaming API actually sends for 'result' events: // no pdf_path or pdf_url fields at all const pending = { actions: { act_1: makeAction('Recommended') }, lines_overloaded: ['LINE_A'], message: 'Analysis complete', dc_fallback: false, } as unknown as AnalysisResult; const merged = mergeAnalysisResult(prev, pending, new Set()); expect(merged.pdf_url).toBe('/results/pdf/overflow.pdf'); expect(merged.pdf_path).toBe('/tmp/overflow.pdf'); expect(merged.actions).toHaveProperty('act_1'); expect(merged.message).toBe('Analysis complete'); }); it('merges pending actions with manually selected actions from prev', () => { const prev: AnalysisResult = { pdf_path: null, pdf_url: null, actions: { manual_1: makeAction('Manual action', { is_manual: true }), old_1: makeAction('Old recommended'), }, lines_overloaded: [], message: '', dc_fallback: false, }; const pending: AnalysisResult = { pdf_path: null, pdf_url: null, actions: { rec_1: makeAction('New recommended') }, lines_overloaded: ['LINE_B'], message: 'Done', dc_fallback: false, }; const selectedIds = new Set(['manual_1']); const merged = mergeAnalysisResult(prev, pending, selectedIds); // manual_1 should be preserved (it's in selectedActionIds) expect(merged.actions).toHaveProperty('manual_1'); // rec_1 from pending should be present expect(merged.actions).toHaveProperty('rec_1'); // old_1 should NOT be present (not in selectedActionIds) expect(merged.actions).not.toHaveProperty('old_1'); }); it('manual actions override pending actions with same ID', () => { const prev: AnalysisResult = { pdf_path: null, pdf_url: null, actions: { shared_id: makeAction('User version', { is_manual: true }), }, lines_overloaded: [], message: '', dc_fallback: false, }; const pending: AnalysisResult = { pdf_path: null, pdf_url: null, actions: { shared_id: makeAction('Computed version') }, lines_overloaded: [], message: 'Done', dc_fallback: false, }; const merged = mergeAnalysisResult(prev, pending, new Set(['shared_id'])); // Manual action should win (spread order: ...pending.actions, ...manualActionsData) expect(merged.actions.shared_id.description_unitaire).toBe('User version'); }); it('works when prev is null', () => { const pending: AnalysisResult = { pdf_path: null, pdf_url: null, actions: { act_1: makeAction('First result') }, lines_overloaded: [], message: 'Done', dc_fallback: false, }; const merged = mergeAnalysisResult(null, pending, new Set()); expect(merged.actions).toHaveProperty('act_1'); expect(merged.message).toBe('Done'); }); });