diff --git a/graph/planner/direct_acyclic_graph.py b/graph/planner/direct_acyclic_graph.py index 4dde647..234e5b8 100644 --- a/graph/planner/direct_acyclic_graph.py +++ b/graph/planner/direct_acyclic_graph.py @@ -81,6 +81,9 @@ def _topsort(self): result.reverse() self._topological_order = result + def has_node(self, node) -> bool: + return node in self._g + def nodes(self) -> set[str]: return {item[0] for item in self._g.items()} @@ -90,6 +93,9 @@ def topological_order(self) -> list[str]: def inverse(self) -> dict[str, list[AdjacentEdge]]: return self._g_inverse + def dependent_edge_types(self, node) -> set[EdgeType]: + return {adjacent[1] for adjacent in self._g_inverse[node]} + def childs( self, node: str, diff --git a/graph/planner/planner.py b/graph/planner/planner.py index 408c29f..2c3273a 100644 --- a/graph/planner/planner.py +++ b/graph/planner/planner.py @@ -1,8 +1,19 @@ -from typing import Union, Tuple, List +from dataclasses import dataclass, field +from typing import Union from direct_acyclic_graph import DirectAcyclicGraph, EdgeType +@dataclass +class NodeState: + user_selected: bool = True + primary_action: None | str = None + transitive_action: None | str = None + state: set[str] = field(default_factory=set) + transitive_state: set[str] = field(default_factory=set) + dependent_edge_types: set[EdgeType] = field(default_factory=set) + + class Planner: _state: DirectAcyclicGraph _target: DirectAcyclicGraph @@ -24,6 +35,114 @@ def __init__( self._unhealthy = unhealthy or [] self._selected = selected + def propagate(self) -> tuple[dict[str, NodeState], list[str]]: + nodes_state = self._state.nodes() + nodes_target = self._target.nodes() + + nodes_add = (nodes_target - nodes_state).intersection( + (nodes_target - nodes_state) + if self._selected == "*" + else {node for node in self._selected if not node.startswith("-")} + ) + nodes_delete = ( + (nodes_state - nodes_target) + if self._selected == "*" + else {node[1:] for node in self._selected if node.startswith("-")} + ) + nodes_selected = ( + nodes_state.union(nodes_target) + if self._selected == "*" + else { + node if not node.startswith("-") else node[1:] + for node in self._selected + } + ) + + nodes_state = { + node: NodeState( + user_selected=node in nodes_selected, + primary_action=( + "add" + if node in nodes_add + else "delete" + if node in nodes_delete + else None + ), + state=({"modified"} if node in self._modified else set()).union( + {"unhealthy"} if node in self._unhealthy else set() + ), + dependent_edge_types=( + self._state.dependent_edge_types(node) + if self._state.has_node(node) + else set() + ).union( + self._target.dependent_edge_types(node) + if self._target.has_node(node) + else set() + ), + ) + for node in (nodes_state.union(nodes_target)) + } + + for node in nodes_delete: + childs_transitive = self._state.childs_transitive( + node, + accept=lambda adjacent: adjacent[1] != EdgeType.TOOL, + ) + for node_transitive_delete in childs_transitive: + # if nodes[node_transitive_delete].primary_action != "delete": + nodes_state[node_transitive_delete].transitive_action = "delete" + + unhealthy_adjacent_tool_edge_nodes = set() + for key, value in nodes_state.items(): + node = key + state = value.state + if "unhealthy" in state: + # propagate 'unhealthy' state to children != EdgeType.TOOL + childs_transitive = self._state.childs_transitive( + node, + accept=lambda adjacent: adjacent[1] != EdgeType.TOOL, + ) + for node_transitive_state in childs_transitive: + nodes_state[node_transitive_state].transitive_state = nodes_state[ + node_transitive_state + ].transitive_state.union({"unhealthy"}) + + # collect nodes ('unhealthy')' w/ EdgeType.TOOL in reverse graph ~> incoming edge(s) + if EdgeType.TOOL in value.dependent_edge_types: + unhealthy_adjacent_tool_edge_nodes.add(node) + + operations = [] + nodes_re_add_modified_or_unhealthy = [] + if len(unhealthy_adjacent_tool_edge_nodes) > 0: + for node in reversed(self._state.topological_order()): + if node in unhealthy_adjacent_tool_edge_nodes: + operations.append(f"-{node}") + for node in self._state.topological_order(): + if node in unhealthy_adjacent_tool_edge_nodes: + operations.append(f"+{node}") + + # for node in self._state.topological_order(): + # value = nodes_state[node] + # + # if node not in unhealthy_adjacent_tool_edge_nodes: + # modified = "modified" in value.state and value.user_selected + # unhealthy = ( + # "unhealthy" in value.state and value.user_selected + # ) or "unhealthy" in value.transitive_state + # if modified or unhealthy: + # operations.append(f"-{node}") + # nodes_re_add_modified_or_unhealthy.append(node) + # + # for node in reversed(self._state.topological_order()): + # value = nodes_state[node] + # + # if node not in unhealthy_adjacent_tool_edge_nodes: + # if node in nodes_re_add_modified_or_unhealthy: + # operations.append(f"+{node}") + + return nodes_state, operations + def apply(self) -> list[str]: nodes_state = self._state.nodes() nodes_target = self._target.nodes() diff --git a/graph/planner/planner_test.py b/graph/planner/planner_test.py index 19ae392..4b1d600 100644 --- a/graph/planner/planner_test.py +++ b/graph/planner/planner_test.py @@ -1,8 +1,7 @@ import unittest -from direct_acyclic_graph import DirectAcyclicGraph, dependency, tool -from planner import Planner - +from direct_acyclic_graph import DirectAcyclicGraph, dependency, tool, EdgeType +from planner import Planner, NodeState DAG1 = DirectAcyclicGraph( g={ @@ -45,6 +44,19 @@ def test_no_action(self): planner = Planner(state=DAG1, target=DAG1) self.assertEqual([], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState(), + "b": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + "c": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + "d": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + "e": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + }, + [], + ), + planner.propagate(), + ) def test_apply_DAG1_modified_multiple(self): planner = Planner(state=DAG1, target=DAG1, modified=["b", "d"]) @@ -53,6 +65,27 @@ def test_apply_DAG1_modified_multiple(self): ["-a", "-b", "-c", "-d", "+d", "+c", "+b", "+a"], planner.apply(), ) + self.assertEqual( + ( + { + "a": NodeState(state=set()), + "b": NodeState( + state={"modified"}, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "c": NodeState( + state=set(), dependent_edge_types={EdgeType.DEPENDENCY} + ), + "d": NodeState( + state={"modified"}, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "e": NodeState( + state=set(), dependent_edge_types={EdgeType.DEPENDENCY} + ), + }, + [], + ), + planner.propagate(), + ) def test_apply_DAG1_unhealthy_multiple(self): planner = Planner(state=DAG1, target=DAG1, unhealthy=["b", "d"]) @@ -61,6 +94,28 @@ def test_apply_DAG1_unhealthy_multiple(self): ["-a", "-b", "-c", "-d", "+d", "+c", "+b", "+a"], planner.apply(), ) + self.assertEqual( + ( + { + "a": NodeState(transitive_state={"unhealthy"}), + "b": NodeState( + state={"unhealthy"}, + transitive_state={"unhealthy"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c": NodeState( + transitive_state={"unhealthy"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d": NodeState( + state={"unhealthy"}, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "e": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + }, + [], + ), + planner.propagate(), + ) def test_apply_DAG1_unhealthy_and_modified(self): planner = Planner( @@ -71,6 +126,29 @@ def test_apply_DAG1_unhealthy_and_modified(self): ["-a", "-b", "-c", "-d", "+d", "+c", "+b", "+a"], planner.apply(), ) + self.assertEqual( + ( + { + "a": NodeState(transitive_state={"unhealthy"}), + "b": NodeState( + state={"modified", "unhealthy"}, + transitive_state={"unhealthy"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c": NodeState( + transitive_state={"unhealthy"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d": NodeState( + state={"modified", "unhealthy"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "e": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + }, + [], + ), + planner.propagate(), + ) def test_apply_DAG1_unhealthy_or_modified(self): planner = Planner(state=DAG1, target=DAG1, modified=["b"], unhealthy=["d"]) @@ -79,6 +157,28 @@ def test_apply_DAG1_unhealthy_or_modified(self): ["-a", "-b", "-c", "-d", "+d", "+c", "+b", "+a"], planner.apply(), ) + self.assertEqual( + ( + { + "a": NodeState(transitive_state={"unhealthy"}), + "b": NodeState( + state={"modified"}, + transitive_state={"unhealthy"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c": NodeState( + transitive_state={"unhealthy"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d": NodeState( + state={"unhealthy"}, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "e": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + }, + [], + ), + planner.propagate(), + ) def test_apply_DAG1_modified_root(self): planner = Planner(state=DAG1, target=DAG1, modified=["e"]) @@ -87,6 +187,21 @@ def test_apply_DAG1_modified_root(self): ["-a", "-b", "-c", "-d", "-e", "+e", "+d", "+c", "+b", "+a"], planner.apply(), ) + self.assertEqual( + ( + { + "a": NodeState(), + "b": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + "c": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + "d": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + "e": NodeState( + state={"modified"}, dependent_edge_types={EdgeType.DEPENDENCY} + ), + }, + [], + ), + planner.propagate(), + ) def test_apply_DAG1_unhealthy_root(self): planner = Planner(state=DAG1, target=DAG1, unhealthy=["e"]) @@ -95,16 +210,70 @@ def test_apply_DAG1_unhealthy_root(self): ["-a", "-b", "-c", "-d", "-e", "+e", "+d", "+c", "+b", "+a"], planner.apply(), ) + self.assertEqual( + ( + { + "a": NodeState(transitive_state={"unhealthy"}), + "b": NodeState( + transitive_state={"unhealthy"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c": NodeState( + transitive_state={"unhealthy"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d": NodeState( + transitive_state={"unhealthy"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "e": NodeState( + state={"unhealthy"}, dependent_edge_types={EdgeType.DEPENDENCY} + ), + }, + [], + ), + planner.propagate(), + ) def test_apply_DAG3_modified_single(self): planner = Planner(state=DAG3, target=DAG3, modified=["d"]) self.assertEqual(["-d", "+d"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + "b": NodeState( + dependent_edge_types={EdgeType.DEPENDENCY, EdgeType.TOOL} + ), + "c": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + "d": NodeState(state={"modified"}), + }, + [], + ), + planner.propagate(), + ) def test_apply_DAG3_modified_root(self): planner = Planner(state=DAG3, target=DAG3, modified=["a"]) self.assertEqual(["-d", "-b", "-a", "+a", "+b", "+d"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + state={"modified"}, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "b": NodeState( + dependent_edge_types={EdgeType.DEPENDENCY, EdgeType.TOOL} + ), + "c": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + "d": NodeState(), + }, + [], + ), + planner.propagate(), + ) def test_apply_add_to_empty(self): planner = Planner( @@ -116,6 +285,18 @@ def test_apply_add_to_empty(self): ["+b", "+a"], planner.apply(), ) + self.assertEqual( + ( + { + "a": NodeState(primary_action="add"), + "b": NodeState( + primary_action="add", dependent_edge_types={EdgeType.DEPENDENCY} + ), + }, + [], + ), + planner.propagate(), + ) def test_apply_add_to_existing(self): planner = Planner( @@ -126,6 +307,19 @@ def test_apply_add_to_existing(self): ) self.assertEqual(["+c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState(), + "b": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + "c": NodeState( + primary_action="add", dependent_edge_types={EdgeType.DEPENDENCY} + ), + }, + [], + ), + planner.propagate(), + ) def test_apply_remove_all(self): planner = Planner( @@ -137,6 +331,19 @@ def test_apply_remove_all(self): ["-a", "-b"], planner.apply(), ) + self.assertEqual( + ( + { + "a": NodeState(primary_action="delete", transitive_action="delete"), + "b": NodeState( + primary_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + }, + [], + ), + planner.propagate(), + ) def test_apply_DAG3_remove_all(self): planner = Planner(state=DAG3, target=DirectAcyclicGraph(g={})) @@ -145,6 +352,28 @@ def test_apply_DAG3_remove_all(self): ["-d", "-c", "-b", "-a"], planner.apply(), ) + self.assertEqual( + ( + { + "a": NodeState( + primary_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "b": NodeState( + primary_action="delete", + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY, EdgeType.TOOL}, + ), + "c": NodeState( + primary_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d": NodeState(primary_action="delete", transitive_action="delete"), + }, + [], + ), + planner.propagate(), + ) def test_add_and_delete(self): planner = Planner( @@ -156,6 +385,17 @@ def test_add_and_delete(self): ["-b", "+c"], planner.apply(), ) + self.assertEqual( + ( + { + "a": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + "b": NodeState(primary_action="delete"), + "c": NodeState(primary_action="add"), + }, + [], + ), + planner.propagate(), + ) def test_modified_but_delete(self): planner = Planner( @@ -167,6 +407,17 @@ def test_modified_but_delete(self): ) self.assertEqual(["-c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + "b": NodeState(), + "c": NodeState(primary_action="delete", state={"modified"}), + }, + [], + ), + planner.propagate(), + ) def test_apply_delete_DAG2_temporarily(self): planner = Planner(state=DAG2, target=DAG2, selected=["-c1d1"]) @@ -175,6 +426,56 @@ def test_apply_delete_DAG2_temporarily(self): ["-a", "-b", "-d", "-d1", "-c", "-c1", "-c1d1"], planner.apply(), ) + self.assertEqual( + ( + { + "a": NodeState(user_selected=False, transitive_action="delete"), + "b": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c1": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c1d1": NodeState( + primary_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c2": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "c2d2": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "d": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d1": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d2": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "e": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + }, + [], + ), + planner.propagate(), + ) def test_apply_delete_DAG2_temporarily_with_target_changes(self): planner = Planner( @@ -198,6 +499,56 @@ def test_apply_delete_DAG2_temporarily_with_target_changes(self): ["-a", "-b", "-d", "-d1", "-c", "-c1", "-c1d1"], planner.apply(), ) + self.assertEqual( + ( + { + "a": NodeState(user_selected=False, transitive_action="delete"), + "b": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c1": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c1d1": NodeState( + primary_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c2": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "c2d2": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "d": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d1": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d2": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "e": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + }, + [], + ), + planner.propagate(), + ) def test_apply_delete_roots_DAG2_temporarily(self): planner = Planner(state=DAG2, target=DAG2, selected=["-c1d1", "-c2d2"]) @@ -208,6 +559,61 @@ def test_apply_delete_roots_DAG2_temporarily(self): actual, ) self.assertNotIn("e", actual) + self.assertEqual( + ( + { + "a": NodeState(user_selected=False, transitive_action="delete"), + "b": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c1": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c1d1": NodeState( + primary_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c2": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c2d2": NodeState( + primary_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d1": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d2": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "e": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + }, + [], + ), + planner.propagate(), + ) def test_apply_delete_with_modified_one_TOOL_edge(self): planner = Planner( @@ -219,6 +625,23 @@ def test_apply_delete_with_modified_one_TOOL_edge(self): selected=["-c"], ) self.assertEqual(["-c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "b": NodeState( + user_selected=False, + state={"modified"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "c": NodeState(primary_action="delete"), + }, + [], + ), + planner.propagate(), + ) def test_apply_delete_with_unhealthy_one_TOOL_edge(self): planner = Planner( @@ -230,6 +653,23 @@ def test_apply_delete_with_unhealthy_one_TOOL_edge(self): selected=["-c"], ) self.assertEqual(["-b", "+b", "-c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "b": NodeState( + user_selected=False, + state={"unhealthy"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "c": NodeState(primary_action="delete"), + }, + ["-b", "+b"], + ), + planner.propagate(), + ) def test_apply_delete_with_one_modified_two_TOOL_edge_in_chain(self): planner = Planner( @@ -243,6 +683,26 @@ def test_apply_delete_with_one_modified_two_TOOL_edge_in_chain(self): selected=["-c"], ) self.assertEqual(["-c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "b1": NodeState( + user_selected=False, dependent_edge_types={EdgeType.TOOL} + ), + "b2": NodeState( + user_selected=False, + state={"modified"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "c": NodeState(primary_action="delete"), + }, + [], + ), + planner.propagate(), + ) def test_apply_delete_with_one_unhealthy_two_TOOL_edge_in_chain(self): planner = Planner( @@ -256,6 +716,26 @@ def test_apply_delete_with_one_unhealthy_two_TOOL_edge_in_chain(self): selected=["-c"], ) self.assertEqual(["-b2", "+b2", "-c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "b1": NodeState( + user_selected=False, dependent_edge_types={EdgeType.TOOL} + ), + "b2": NodeState( + user_selected=False, + state={"unhealthy"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "c": NodeState(primary_action="delete"), + }, + ["-b2", "+b2"], + ), + planner.propagate(), + ) def test_apply_delete_with_two_modified_two_TOOL_edge_in_chain(self): planner = Planner( @@ -269,6 +749,28 @@ def test_apply_delete_with_two_modified_two_TOOL_edge_in_chain(self): selected=["-c"], ) self.assertEqual(["-c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "b1": NodeState( + user_selected=False, + state={"modified"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "b2": NodeState( + user_selected=False, + state={"modified"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "c": NodeState(primary_action="delete"), + }, + [], + ), + planner.propagate(), + ) def test_apply_delete_with_two_unhealthy_two_TOOL_edge_in_chain(self): planner = Planner( @@ -282,6 +784,28 @@ def test_apply_delete_with_two_unhealthy_two_TOOL_edge_in_chain(self): selected=["-c"], ) self.assertEqual(["-b2", "-b1", "+b1", "+b2", "-c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "b1": NodeState( + user_selected=False, + state={"unhealthy"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "b2": NodeState( + user_selected=False, + state={"unhealthy"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "c": NodeState(primary_action="delete"), + }, + ["-b1", "-b2", "+b2", "+b1"], + ), + planner.propagate(), + ) def test_apply_delete_with_one_modified_two_TOOL_edge_in_chain_skip(self): planner = Planner( @@ -295,16 +819,78 @@ def test_apply_delete_with_one_modified_two_TOOL_edge_in_chain_skip(self): selected=["-c"], ) self.assertEqual(["-c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "b1": NodeState( + user_selected=False, + state={"modified"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "b2": NodeState( + user_selected=False, dependent_edge_types={EdgeType.TOOL} + ), + "c": NodeState(primary_action="delete"), + }, + [], + ), + planner.propagate(), + ) def test_no_action_apply_selected(self): planner = Planner(state=DAG1, target=DAG1, modified=[], selected=["c"]) self.assertEqual([], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState(user_selected=False), + "b": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "c": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + "d": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "e": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + }, + [], + ), + planner.propagate(), + ) def test_apply_DAG1_modified_multiple_single_selected(self): planner = Planner(state=DAG1, target=DAG1, modified=["b", "d"], selected=["b"]) self.assertEqual(["-a", "-b", "+b", "+a"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState(user_selected=False), + "b": NodeState( + state={"modified"}, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "c": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "d": NodeState( + user_selected=False, + state={"modified"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "e": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + }, + [], + ), + planner.propagate(), + ) def test_apply_DAG3_selected_target(self): planner = Planner( @@ -312,6 +898,25 @@ def test_apply_DAG3_selected_target(self): ) self.assertEqual(["+b", "+c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "b": NodeState( + user_selected=False, + dependent_edge_types={EdgeType.DEPENDENCY, EdgeType.TOOL}, + ), + "c": NodeState( + primary_action="add", dependent_edge_types={EdgeType.DEPENDENCY} + ), + "d": NodeState(user_selected=False), + }, + [], + ), + planner.propagate(), + ) def test_apply_DAG3_selected_target_modified(self): planner = Planner( @@ -322,6 +927,27 @@ def test_apply_DAG3_selected_target_modified(self): ) self.assertEqual(["+b", "+c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, + state={"modified"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "b": NodeState( + user_selected=False, + dependent_edge_types={EdgeType.DEPENDENCY, EdgeType.TOOL}, + ), + "c": NodeState( + primary_action="add", dependent_edge_types={EdgeType.DEPENDENCY} + ), + "d": NodeState(user_selected=False), + }, + [], + ), + planner.propagate(), + ) def test_apply_DAG3_selected_target_unhealthy(self): planner = Planner( @@ -332,6 +958,27 @@ def test_apply_DAG3_selected_target_unhealthy(self): ) self.assertEqual(["-a", "+a", "+b", "+c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, + state={"unhealthy"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "b": NodeState( + user_selected=False, + dependent_edge_types={EdgeType.DEPENDENCY, EdgeType.TOOL}, + ), + "c": NodeState( + primary_action="add", dependent_edge_types={EdgeType.DEPENDENCY} + ), + "d": NodeState(user_selected=False), + }, + [], + ), + planner.propagate(), + ) def test_apply_DAG3_selected_target_modified_one_TOOL_edge(self): planner = Planner( @@ -344,6 +991,23 @@ def test_apply_DAG3_selected_target_modified_one_TOOL_edge(self): ) self.assertEqual(["+c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "b": NodeState( + user_selected=False, + state={"modified"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "c": NodeState(primary_action="add"), + }, + [], + ), + planner.propagate(), + ) def test_apply_DAG3_selected_target_unhealthy_one_TOOL_edge(self): planner = Planner( @@ -356,6 +1020,23 @@ def test_apply_DAG3_selected_target_unhealthy_one_TOOL_edge(self): ) self.assertEqual(["-b", "+b", "+c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "b": NodeState( + user_selected=False, + state={"unhealthy"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "c": NodeState(primary_action="add"), + }, + ["-b", "+b"], + ), + planner.propagate(), + ) def test_apply_DAG3_selected_target_modified_one_TOOL_edge_chain_skip(self): planner = Planner( @@ -374,6 +1055,29 @@ def test_apply_DAG3_selected_target_modified_one_TOOL_edge_chain_skip(self): ) self.assertEqual(["+d", "+e"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "b": NodeState( + user_selected=False, + state={"modified"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "c": NodeState( + user_selected=False, dependent_edge_types={EdgeType.TOOL} + ), + "d": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "e": NodeState(primary_action="add"), + }, + [], + ), + planner.propagate(), + ) def test_apply_selected_TOOL_edge_modified(self): planner = Planner( @@ -384,6 +1088,20 @@ def test_apply_selected_TOOL_edge_modified(self): ) self.assertEqual(["-a"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState(primary_action="delete"), + "b": NodeState( + user_selected=False, + state={"modified"}, + dependent_edge_types={EdgeType.TOOL}, + ), + }, + [], + ), + planner.propagate(), + ) def test_apply_selected_TOOL_edge_unhealthy(self): planner = Planner( @@ -394,3 +1112,53 @@ def test_apply_selected_TOOL_edge_unhealthy(self): ) self.assertEqual(["-b", "+b", "-a"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState(primary_action="delete"), + "b": NodeState( + user_selected=False, + state={"unhealthy"}, + dependent_edge_types={EdgeType.TOOL}, + ), + }, + ["-b", "+b"], + ), + planner.propagate(), + ) + + def test_apply_unhealthy_tool(self): + planner = Planner( + state=DirectAcyclicGraph( + g={"a": dependency("c") + tool("b"), "b": dependency("c"), "c": []} + ), + target=DirectAcyclicGraph( + g={"a": dependency("c") + tool("b"), "b": dependency("c"), "c": []} + ), + unhealthy=["b"], + selected=["-a", "-b", "-c"], + ) + + # self.assertEqual(["-b", "+b", "-a", "-b", "-c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + primary_action="delete", + transitive_action="delete", + ), + "b": NodeState( + primary_action="delete", + transitive_action="delete", + state={"unhealthy"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "c": NodeState( + primary_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + }, + ["-b", "+b"], + ), + planner.propagate(), + )