@@ -1103,3 +1103,150 @@ def test_deps_archived_direct_target_error(tmp_path: Path) -> None:
11031103 import shutil
11041104
11051105 shutil .rmtree (str (tracker_dir .parent ), ignore_errors = True )
1106+
1107+
1108+ # ── RED MARKER BOUNDARY ──────────────────────────────────────────────────────
1109+ # Tests below this line are expected to FAIL (RED) until ticket-graph.py is
1110+ # refactored to use a single reduce_all_tickets call for deps operations.
1111+ # The .test-index RED marker points to the first test below:
1112+ # test_build_dep_graph_single_batch_scan
1113+ # Tests ABOVE this line are GREEN and must always pass.
1114+
1115+
1116+ @pytest .mark .unit
1117+ @pytest .mark .scripts
1118+ def test_build_dep_graph_single_batch_scan (graph : ModuleType , tmp_path : Path ) -> None :
1119+ """build_dep_graph must use a single reduce_all_tickets call instead of per-ticket scans.
1120+
1121+ Setup:
1122+ - A tracker with 5 tickets: ticket-a (closed, blocks ticket-e), ticket-b,
1123+ ticket-c, ticket-d (all open), ticket-e (open, target ticket).
1124+
1125+ Expected: reduce_all_tickets is called exactly once during build_dep_graph.
1126+
1127+ Currently RED: build_dep_graph calls _reduce_ticket per-ticket via
1128+ _compute_dep_graph and _find_direct_blockers. It does not call reduce_all_tickets.
1129+ """
1130+ from unittest .mock import patch
1131+
1132+ tracker_dir = tmp_path / "tracker"
1133+ tracker_dir .mkdir ()
1134+
1135+ _write_ticket (tracker_dir , "ticket-a" , status = "closed" )
1136+ _write_ticket (tracker_dir , "ticket-b" , status = "open" )
1137+ _write_ticket (tracker_dir , "ticket-c" , status = "open" )
1138+ _write_ticket (tracker_dir , "ticket-d" , status = "open" )
1139+ _write_ticket (tracker_dir , "ticket-e" , status = "open" )
1140+ _write_blocks_link (tracker_dir , "ticket-a" , "ticket-e" )
1141+
1142+ # Capture the real reduce_all_tickets so the patch can delegate to it
1143+ real_reduce_all = graph ._reducer .reduce_all_tickets
1144+
1145+ call_count = []
1146+
1147+ def counting_reduce_all (* args , ** kwargs ): # type: ignore[no-untyped-def]
1148+ call_count .append (1 )
1149+ return real_reduce_all (* args , ** kwargs )
1150+
1151+ with patch .object (
1152+ graph ._reducer , "reduce_all_tickets" , side_effect = counting_reduce_all
1153+ ):
1154+ graph .build_dep_graph ("ticket-e" , str (tracker_dir ))
1155+
1156+ assert len (call_count ) == 1 , (
1157+ f"Expected reduce_all_tickets to be called exactly once during build_dep_graph, "
1158+ f"but it was called { len (call_count )} time(s). "
1159+ "build_dep_graph must pre-load all ticket states via a single reduce_all_tickets "
1160+ "call instead of calling _reduce_ticket per-ticket in _find_direct_blockers and "
1161+ "_compute_dep_graph."
1162+ )
1163+
1164+
1165+ @pytest .mark .unit
1166+ @pytest .mark .scripts
1167+ def test_find_direct_blockers_no_per_ticket_scan (
1168+ graph : ModuleType , tmp_path : Path
1169+ ) -> None :
1170+ """_find_direct_blockers must not call _reduce_ticket directly — use pre-loaded state.
1171+
1172+ Setup:
1173+ - ticket-blocker: open, blocks ticket-target
1174+ - ticket-target: open
1175+
1176+ Pre-loaded state dict is passed in. _reduce_ticket must NOT be called.
1177+
1178+ Currently RED: _find_direct_blockers calls _reduce_ticket directly for each
1179+ ticket dir it scans. After refactor, it must accept a pre-loaded all_states
1180+ dict and use that instead.
1181+ """
1182+ from unittest .mock import patch
1183+
1184+ tracker_dir = tmp_path / "tracker"
1185+ tracker_dir .mkdir ()
1186+
1187+ _write_ticket (tracker_dir , "ticket-blocker" , status = "open" )
1188+ _write_ticket (tracker_dir , "ticket-target" , status = "open" )
1189+ _write_blocks_link (tracker_dir , "ticket-blocker" , "ticket-target" )
1190+
1191+ reduce_ticket_calls = []
1192+
1193+ def spy_reduce_ticket (* args , ** kwargs ): # type: ignore[no-untyped-def]
1194+ reduce_ticket_calls .append (args )
1195+ return graph ._reduce_ticket (* args , ** kwargs )
1196+
1197+ with patch .object (graph , "_reduce_ticket" , side_effect = spy_reduce_ticket ):
1198+ # After refactor, _find_direct_blockers should accept all_states and not call _reduce_ticket
1199+ graph ._find_direct_blockers ("ticket-target" , str (tracker_dir ))
1200+
1201+ assert len (reduce_ticket_calls ) == 0 , (
1202+ f"Expected _reduce_ticket to be called 0 times in _find_direct_blockers "
1203+ f"(should use pre-loaded state), but it was called { len (reduce_ticket_calls )} time(s). "
1204+ "_find_direct_blockers must be refactored to accept a pre-loaded all_states dict "
1205+ "and look up ticket states from it instead of calling _reduce_ticket per ticket."
1206+ )
1207+
1208+
1209+ @pytest .mark .unit
1210+ @pytest .mark .scripts
1211+ def test_compute_dep_graph_children_use_preloaded_state (
1212+ graph : ModuleType , tmp_path : Path
1213+ ) -> None :
1214+ """_compute_dep_graph must not call _reduce_ticket for children discovery.
1215+
1216+ Setup:
1217+ - parent-epic: epic with 3 child stories
1218+ - story-a, story-b, story-c: open stories with parent_id=parent-epic
1219+
1220+ Expected: _reduce_ticket is NOT called during _compute_dep_graph. All state
1221+ lookups should use a pre-loaded all_states dict passed in from build_dep_graph.
1222+
1223+ Currently RED: _compute_dep_graph calls _reduce_ticket for each directory entry
1224+ to discover children. After refactor, it must use pre-loaded state.
1225+ """
1226+ from unittest .mock import patch
1227+
1228+ tracker_dir = tmp_path / "tracker"
1229+ tracker_dir .mkdir ()
1230+
1231+ _write_ticket (tracker_dir , "parent-epic" , ticket_type = "epic" )
1232+ _write_ticket (tracker_dir , "story-a" , parent_id = "parent-epic" , ticket_type = "story" )
1233+ _write_ticket (tracker_dir , "story-b" , parent_id = "parent-epic" , ticket_type = "story" )
1234+ _write_ticket (tracker_dir , "story-c" , parent_id = "parent-epic" , ticket_type = "story" )
1235+
1236+ reduce_ticket_calls = []
1237+
1238+ def spy_reduce_ticket (* args , ** kwargs ): # type: ignore[no-untyped-def]
1239+ reduce_ticket_calls .append (args )
1240+ return graph ._reduce_ticket (* args , ** kwargs )
1241+
1242+ with patch .object (graph , "_reduce_ticket" , side_effect = spy_reduce_ticket ):
1243+ graph ._compute_dep_graph ("parent-epic" , str (tracker_dir ))
1244+
1245+ assert len (reduce_ticket_calls ) == 0 , (
1246+ f"Expected _reduce_ticket to be called 0 times in _compute_dep_graph "
1247+ f"(should use pre-loaded state for children discovery), "
1248+ f"but it was called { len (reduce_ticket_calls )} time(s). "
1249+ "_compute_dep_graph must be refactored to receive a pre-loaded all_states dict "
1250+ "and use it for both children discovery and blocker resolution instead of "
1251+ "calling _reduce_ticket per directory entry."
1252+ )
0 commit comments