# Copyright (c) 2025-2026, RTE (https://www.rte-france.com) # SPDX-License-Identifier: MPL-2.0 """Tests for :mod:`expert_backend.recommenders.network_existence`. Covers the defensive filter that drops dict_action entries whose target VL / line / 2WT id is unknown to the loaded pypowsybl network (regression for the ``AUBE P4`` case on a small grid with a large-grid action dict). """ from __future__ import annotations from types import SimpleNamespace from unittest.mock import MagicMock import pytest # Importing any expert_backend.recommenders submodule triggers the # package __init__, which registers the concrete upstream model # classes — skip in mock-only environments. pytest.importorskip("expert_op4grid_recommender.models.base") from expert_backend.recommenders.network_existence import ( _action_targets_known_elements, _network_existence_sets, filter_to_existing_network_elements, ) def _mock_network(line_ids=("L1", "L2"), twt_ids=("T1",), vl_ids=("VL_A", "VL_B")): net = MagicMock() net.get_voltage_levels.return_value = SimpleNamespace(index=list(vl_ids)) net.get_lines.return_value = SimpleNamespace(index=list(line_ids)) net.get_2_windings_transformers.return_value = SimpleNamespace(index=list(twt_ids)) return net # --------------------------------------------------------------------- # _network_existence_sets # --------------------------------------------------------------------- def test_network_existence_sets_none_when_network_is_none(): assert _network_existence_sets(None) is None def test_network_existence_sets_returns_union_of_lines_and_twt(): net = _mock_network(line_ids=("L1",), twt_ids=("T1",), vl_ids=("VL",)) vl_ids, branch_ids = _network_existence_sets(net) assert vl_ids == {"VL"} assert branch_ids == {"L1", "T1"} def test_network_existence_sets_tolerates_missing_2wt_api(): net = _mock_network() net.get_2_windings_transformers.side_effect = AttributeError("no 2WT") vl_ids, branch_ids = _network_existence_sets(net) assert "L1" in branch_ids assert "T1" not in branch_ids def test_network_existence_sets_returns_none_on_vl_failure(): net = _mock_network() net.get_voltage_levels.side_effect = RuntimeError("boom") assert _network_existence_sets(net) is None def test_network_existence_sets_tolerates_missing_lines_api(): net = _mock_network() net.get_lines.side_effect = AttributeError("no lines") vl_ids, branch_ids = _network_existence_sets(net) # Lines unavailable → only 2WT remain. assert "L1" not in branch_ids assert "T1" in branch_ids # --------------------------------------------------------------------- # _action_targets_known_elements # --------------------------------------------------------------------- VL_IDS = {"VL_A", "VL_B"} BRANCH_IDS = {"L1", "L2", "T1"} def test_action_accepts_known_voltage_level_id(): entry = {"VoltageLevelId": "VL_A"} assert _action_targets_known_elements(entry, VL_IDS, BRANCH_IDS) is True def test_action_accepts_lowercase_voltage_level_id(): entry = {"voltage_level_id": "VL_B"} assert _action_targets_known_elements(entry, VL_IDS, BRANCH_IDS) is True def test_action_rejects_unknown_voltage_level_id(): entry = {"VoltageLevelId": "AUBE P4"} assert _action_targets_known_elements(entry, VL_IDS, BRANCH_IDS) is False def test_action_accepts_known_lines_or_id(): entry = {"content": {"set_bus": {"lines_or_id": {"L1": 1}}}} assert _action_targets_known_elements(entry, VL_IDS, BRANCH_IDS) is True def test_action_rejects_unknown_lines_or_id(): entry = {"content": {"set_bus": {"lines_or_id": {"FOREIGN_LINE": -1}}}} assert _action_targets_known_elements(entry, VL_IDS, BRANCH_IDS) is False def test_action_rejects_unknown_lines_ex_id(): entry = {"content": {"set_bus": {"lines_ex_id": {"FOREIGN_LINE": -1}}}} assert _action_targets_known_elements(entry, VL_IDS, BRANCH_IDS) is False def test_action_accepts_when_no_references(): """Conservative default: no target references → can't prove invalid.""" entry = {"description": "x", "content": {}} assert _action_targets_known_elements(entry, VL_IDS, BRANCH_IDS) is True def test_action_rejects_non_dict_entries(): assert _action_targets_known_elements("not a dict", VL_IDS, BRANCH_IDS) is False assert _action_targets_known_elements(None, VL_IDS, BRANCH_IDS) is False def test_action_short_circuits_on_first_unknown_line(): entry = {"content": {"set_bus": { "lines_or_id": {"L1": 1, "FOREIGN": -1}, "lines_ex_id": {"L2": 1}, }}} # Even though L1/L2 are present, FOREIGN makes the action invalid. assert _action_targets_known_elements(entry, VL_IDS, BRANCH_IDS) is False def test_action_accepts_transformer_id_in_branch_ids(): entry = {"content": {"set_bus": {"lines_or_id": {"T1": -1}}}} assert _action_targets_known_elements(entry, VL_IDS, BRANCH_IDS) is True # --------------------------------------------------------------------- # filter_to_existing_network_elements # --------------------------------------------------------------------- def test_filter_returns_input_when_network_is_none(): candidates = ["a1", "a2"] out = filter_to_existing_network_elements(candidates, {"a1": {}, "a2": {}}, None) assert out == ["a1", "a2"] def test_filter_drops_actions_for_unknown_vl(): """Regression: the AUBE P4 case from the small_grid bug report.""" net = _mock_network(vl_ids=("VL_A",)) dict_action = { "ok": {"VoltageLevelId": "VL_A"}, "bad": {"VoltageLevelId": "AUBE P4"}, } out = filter_to_existing_network_elements(["ok", "bad"], dict_action, net) assert out == ["ok"] def test_filter_drops_actions_for_unknown_line(): net = _mock_network(line_ids=("L1",), twt_ids=(), vl_ids=("VL",)) dict_action = { "ok": {"content": {"set_bus": {"lines_or_id": {"L1": -1}}}}, "bad": {"content": {"set_bus": {"lines_or_id": {"FOREIGN": -1}}}}, } out = filter_to_existing_network_elements(["ok", "bad"], dict_action, net) assert out == ["ok"] def test_filter_returns_input_when_dict_action_is_empty(): net = _mock_network() assert filter_to_existing_network_elements(["a"], {}, net) == ["a"] def test_filter_returns_input_when_introspection_fails(): """Conservative: never silently empty the pool on backend errors.""" net = MagicMock() net.get_voltage_levels.side_effect = RuntimeError("boom") out = filter_to_existing_network_elements( ["a"], {"a": {"VoltageLevelId": "VL"}}, net, ) assert out == ["a"] def test_filter_skips_unknown_action_ids(): net = _mock_network() out = filter_to_existing_network_elements(["unknown"], {"other": {}}, net) assert out == [] def test_filter_preserves_input_order(): net = _mock_network(vl_ids=("VL_A", "VL_B")) dict_action = { "a3": {"VoltageLevelId": "VL_A"}, "a1": {"VoltageLevelId": "VL_B"}, "a2": {"VoltageLevelId": "VL_A"}, } out = filter_to_existing_network_elements(["a3", "a1", "a2"], dict_action, net) assert out == ["a3", "a1", "a2"]