Co-Study4Grid / frontend /src /utils /mergeAnalysisResult.test.ts
github-actions[bot]
Deploy 7688ef1
13d4e44
Raw
History Blame Contribute Delete
7.06 kB
// 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<string>,
): AnalysisResult {
const manualActionsData: Record<string, ActionDetail> = {};
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>): 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');
});
});