From 609cb68856cc6aeec31e3977602d63f84032e8a5 Mon Sep 17 00:00:00 2001 From: AngeloDanducci Date: Tue, 3 Feb 2026 14:27:16 -0500 Subject: [PATCH 1/2] ensure proper ordering of decompose vars --- cli/decompose/decompose.py | 131 ++++++- test/decompose/test_decompose.py | 648 +++++++++++++++++++++++++++++++ 2 files changed, 778 insertions(+), 1 deletion(-) create mode 100644 test/decompose/test_decompose.py diff --git a/cli/decompose/decompose.py b/cli/decompose/decompose.py index 3b0e892a..652525d2 100644 --- a/cli/decompose/decompose.py +++ b/cli/decompose/decompose.py @@ -1,12 +1,13 @@ import json import keyword from enum import Enum +from graphlib import TopologicalSorter from pathlib import Path from typing import Annotated import typer -from .pipeline import DecompBackend +from .pipeline import DecompBackend, DecompPipelineResult, DecompSubtasksResult # Must maintain declaration order @@ -20,6 +21,130 @@ class DecompVersion(str, Enum): this_file_dir = Path(__file__).resolve().parent +def reorder_subtasks( + subtasks: list[DecompSubtasksResult], +) -> list[DecompSubtasksResult]: + """Reorder subtasks based on their dependencies using topological sort. + + Uses Python's graphlib.TopologicalSorter to perform a topological sort of + subtasks based on their depends_on relationships. + + Args: + subtasks: List of subtask dictionaries with 'tag' and 'depends_on' fields + + Returns: + Reordered list of subtasks where all dependencies appear before dependents + + Raises: + ValueError: If a circular dependency is detected + """ + # Build dependency graph + subtask_map = {subtask["tag"].lower(): subtask for subtask in subtasks} + + # Build graph for TopologicalSorter + # Format: {node: {dependencies}} + graph = {} + for tag, subtask in subtask_map.items(): + deps = subtask.get("depends_on", []) + # Filter to only include dependencies that exist in subtask_map + valid_deps = {dep.lower() for dep in deps if dep.lower() in subtask_map} + graph[tag] = valid_deps + + # Perform topological sort + try: + ts = TopologicalSorter(graph) + sorted_tags = list(ts.static_order()) + except ValueError as e: + # TopologicalSorter raises ValueError for circular dependencies + raise ValueError( + "Circular dependency detected in subtasks. Cannot automatically reorder." + ) from e + + # Return reordered subtasks + return [subtask_map[tag] for tag in sorted_tags] + + +def verify_user_variables( + decomp_data: DecompPipelineResult, input_var: list[str] | None +) -> DecompPipelineResult: + """Verify and fix user variable ordering in subtasks. + + Validates that: + 1. All input_vars_required exist in the provided input_var list + 2. All depends_on variables reference existing subtasks + 3. Subtasks are ordered so dependencies appear before dependents + + If dependencies are out of order, automatically reorders them using topological sort. + + Args: + decomp_data: The decomposition pipeline result containing subtasks + input_var: List of user-provided input variable names + + Returns: + The decomp_data with potentially reordered subtasks + + Raises: + ValueError: If a required input variable is missing or dependencies are invalid + """ + if input_var is None: + input_var = [] + + # Normalize input variables to lowercase for comparison + available_input_vars = {var.lower() for var in input_var} + + # Build set of all subtask tags + all_subtask_tags = {subtask["tag"].lower() for subtask in decomp_data["subtasks"]} + + # Validate that all required variables exist + for subtask in decomp_data["subtasks"]: + subtask_tag = subtask["tag"].lower() + + # Check input_vars_required exist in provided input variables + for required_var in subtask.get("input_vars_required", []): + var_lower = required_var.lower() + if var_lower not in available_input_vars: + raise ValueError( + f'Subtask "{subtask_tag}" requires input variable ' + f'"{required_var}" which was not provided in --input-var. ' + f"Available input variables: {sorted(available_input_vars) if available_input_vars else 'none'}" + ) + + # Check that all dependencies exist somewhere in the subtasks + for dep_var in subtask.get("depends_on", []): + dep_lower = dep_var.lower() + if dep_lower not in all_subtask_tags: + raise ValueError( + f'Subtask "{subtask_tag}" depends on variable ' + f'"{dep_var}" which does not exist in any subtask. ' + f"Available subtask tags: {sorted(all_subtask_tags)}" + ) + + # Check if reordering is needed + needs_reordering = False + defined_subtask_tags = set() + + for subtask in decomp_data["subtasks"]: + subtask_tag = subtask["tag"].lower() + + # Check if any dependency hasn't been defined yet + for dep_var in subtask.get("depends_on", []): + dep_lower = dep_var.lower() + if dep_lower not in defined_subtask_tags: + needs_reordering = True + break + + if needs_reordering: + break + + defined_subtask_tags.add(subtask_tag) + + # Reorder if needed + if needs_reordering: + decomp_data["subtasks"] = reorder_subtasks(decomp_data["subtasks"]) + + return decomp_data + + def run( out_dir: Annotated[ Path, @@ -170,6 +295,10 @@ def run( backend_api_key=backend_api_key, ) + # Verify that all user variables are properly defined before use + # This may reorder subtasks if dependencies are out of order + decomp_data = verify_user_variables(decomp_data, input_var) + with open(out_dir / f"{out_name}.json", "w") as f: json.dump(decomp_data, f, indent=2) diff --git a/test/decompose/test_decompose.py b/test/decompose/test_decompose.py new file mode 100644 index 00000000..82ba11d6 --- /dev/null +++ b/test/decompose/test_decompose.py @@ -0,0 +1,648 @@ +"""Tests for cli/decompose/decompose.py functions. + +This module tests the reorder_subtasks and verify_user_variables functions +which handle dependency ordering and validation of decomposition results. +""" + +import pytest + +from cli.decompose.decompose import reorder_subtasks, verify_user_variables +from cli.decompose.pipeline import DecompPipelineResult, DecompSubtasksResult + +# ============================================================================ +# Tests for reorder_subtasks +# ============================================================================ + + +class TestReorderSubtasksHappyPath: + """Happy path tests for reorder_subtasks function.""" + + def test_no_dependencies(self): + """Test subtasks with no dependencies remain in original order.""" + subtasks: list[DecompSubtasksResult] = [ + { + "subtask": "Task A", + "tag": "TASK_A", + "constraints": [], + "prompt_template": "Do A", + "input_vars_required": [], + "depends_on": [], + }, + { + "subtask": "Task B", + "tag": "TASK_B", + "constraints": [], + "prompt_template": "Do B", + "input_vars_required": [], + "depends_on": [], + }, + { + "subtask": "Task C", + "tag": "TASK_C", + "constraints": [], + "prompt_template": "Do C", + "input_vars_required": [], + "depends_on": [], + }, + ] + + result = reorder_subtasks(subtasks) + + # Should maintain alphabetical order (topological sort is stable) + assert len(result) == 3 + assert result[0]["tag"] == "TASK_A" + assert result[1]["tag"] == "TASK_B" + assert result[2]["tag"] == "TASK_C" + + def test_simple_linear_dependency(self): + """Test simple linear dependency chain: C -> B -> A.""" + subtasks: list[DecompSubtasksResult] = [ + { + "subtask": "Task C", + "tag": "TASK_C", + "constraints": [], + "prompt_template": "Do C", + "input_vars_required": [], + "depends_on": ["TASK_B"], + }, + { + "subtask": "Task B", + "tag": "TASK_B", + "constraints": [], + "prompt_template": "Do B", + "input_vars_required": [], + "depends_on": ["TASK_A"], + }, + { + "subtask": "Task A", + "tag": "TASK_A", + "constraints": [], + "prompt_template": "Do A", + "input_vars_required": [], + "depends_on": [], + }, + ] + + result = reorder_subtasks(subtasks) + + # Should reorder to A, B, C + assert len(result) == 3 + assert result[0]["tag"] == "TASK_A" + assert result[1]["tag"] == "TASK_B" + assert result[2]["tag"] == "TASK_C" + + def test_diamond_dependency(self): + """Test diamond dependency: D depends on B and C, both depend on A.""" + subtasks: list[DecompSubtasksResult] = [ + { + "subtask": "Task D", + "tag": "TASK_D", + "constraints": [], + "prompt_template": "Do D", + "input_vars_required": [], + "depends_on": ["TASK_B", "TASK_C"], + }, + { + "subtask": "Task C", + "tag": "TASK_C", + "constraints": [], + "prompt_template": "Do C", + "input_vars_required": [], + "depends_on": ["TASK_A"], + }, + { + "subtask": "Task B", + "tag": "TASK_B", + "constraints": [], + "prompt_template": "Do B", + "input_vars_required": [], + "depends_on": ["TASK_A"], + }, + { + "subtask": "Task A", + "tag": "TASK_A", + "constraints": [], + "prompt_template": "Do A", + "input_vars_required": [], + "depends_on": [], + }, + ] + + result = reorder_subtasks(subtasks) + + # A must be first, D must be last, B and C can be in either order + assert len(result) == 4 + assert result[0]["tag"] == "TASK_A" + assert result[3]["tag"] == "TASK_D" + assert {result[1]["tag"], result[2]["tag"]} == {"TASK_B", "TASK_C"} + + def test_case_insensitive_dependencies(self): + """Test that dependencies are case-insensitive.""" + subtasks: list[DecompSubtasksResult] = [ + { + "subtask": "Task B", + "tag": "task_b", + "constraints": [], + "prompt_template": "Do B", + "input_vars_required": [], + "depends_on": ["TASK_A"], # Uppercase reference + }, + { + "subtask": "Task A", + "tag": "TASK_A", + "constraints": [], + "prompt_template": "Do A", + "input_vars_required": [], + "depends_on": [], + }, + ] + + result = reorder_subtasks(subtasks) + + assert len(result) == 2 + assert result[0]["tag"] == "TASK_A" + assert result[1]["tag"] == "task_b" + + def test_multiple_independent_chains(self): + """Test multiple independent dependency chains.""" + subtasks: list[DecompSubtasksResult] = [ + # Chain 1: B -> A + { + "subtask": "Task B", + "tag": "TASK_B", + "constraints": [], + "prompt_template": "Do B", + "input_vars_required": [], + "depends_on": ["TASK_A"], + }, + { + "subtask": "Task A", + "tag": "TASK_A", + "constraints": [], + "prompt_template": "Do A", + "input_vars_required": [], + "depends_on": [], + }, + # Chain 2: D -> C + { + "subtask": "Task D", + "tag": "TASK_D", + "constraints": [], + "prompt_template": "Do D", + "input_vars_required": [], + "depends_on": ["TASK_C"], + }, + { + "subtask": "Task C", + "tag": "TASK_C", + "constraints": [], + "prompt_template": "Do C", + "input_vars_required": [], + "depends_on": [], + }, + ] + + result = reorder_subtasks(subtasks) + + # A before B, C before D + assert len(result) == 4 + a_idx = next(i for i, t in enumerate(result) if t["tag"] == "TASK_A") + b_idx = next(i for i, t in enumerate(result) if t["tag"] == "TASK_B") + c_idx = next(i for i, t in enumerate(result) if t["tag"] == "TASK_C") + d_idx = next(i for i, t in enumerate(result) if t["tag"] == "TASK_D") + assert a_idx < b_idx + assert c_idx < d_idx + + def test_nonexistent_dependency_ignored(self): + """Test that dependencies referencing non-existent tasks are ignored.""" + subtasks: list[DecompSubtasksResult] = [ + { + "subtask": "Task B", + "tag": "TASK_B", + "constraints": [], + "prompt_template": "Do B", + "input_vars_required": [], + "depends_on": [ + "TASK_A", + "NONEXISTENT", + ], # NONEXISTENT should be ignored + }, + { + "subtask": "Task A", + "tag": "TASK_A", + "constraints": [], + "prompt_template": "Do A", + "input_vars_required": [], + "depends_on": [], + }, + ] + + result = reorder_subtasks(subtasks) + + # Should still work, ignoring the nonexistent dependency + assert len(result) == 2 + assert result[0]["tag"] == "TASK_A" + assert result[1]["tag"] == "TASK_B" + + +class TestReorderSubtasksUnhappyPath: + """Negative tests for reorder_subtasks function.""" + + def test_circular_dependency_two_nodes(self): + """Test circular dependency between two nodes.""" + subtasks: list[DecompSubtasksResult] = [ + { + "subtask": "Task A", + "tag": "TASK_A", + "constraints": [], + "prompt_template": "Do A", + "input_vars_required": [], + "depends_on": ["TASK_B"], + }, + { + "subtask": "Task B", + "tag": "TASK_B", + "constraints": [], + "prompt_template": "Do B", + "input_vars_required": [], + "depends_on": ["TASK_A"], + }, + ] + + with pytest.raises(ValueError, match="Circular dependency detected"): + reorder_subtasks(subtasks) + + def test_circular_dependency_three_nodes(self): + """Test circular dependency in a chain of three nodes.""" + subtasks: list[DecompSubtasksResult] = [ + { + "subtask": "Task A", + "tag": "TASK_A", + "constraints": [], + "prompt_template": "Do A", + "input_vars_required": [], + "depends_on": ["TASK_C"], + }, + { + "subtask": "Task B", + "tag": "TASK_B", + "constraints": [], + "prompt_template": "Do B", + "input_vars_required": [], + "depends_on": ["TASK_A"], + }, + { + "subtask": "Task C", + "tag": "TASK_C", + "constraints": [], + "prompt_template": "Do C", + "input_vars_required": [], + "depends_on": ["TASK_B"], + }, + ] + + with pytest.raises(ValueError, match="Circular dependency detected"): + reorder_subtasks(subtasks) + + def test_self_dependency(self): + """Test task depending on itself.""" + subtasks: list[DecompSubtasksResult] = [ + { + "subtask": "Task A", + "tag": "TASK_A", + "constraints": [], + "prompt_template": "Do A", + "input_vars_required": [], + "depends_on": ["TASK_A"], + } + ] + + with pytest.raises(ValueError, match="Circular dependency detected"): + reorder_subtasks(subtasks) + + def test_empty_subtasks_list(self): + """Test with empty subtasks list.""" + subtasks: list[DecompSubtasksResult] = [] + + result = reorder_subtasks(subtasks) + + assert result == [] + + +# ============================================================================ +# Tests for verify_user_variables +# ============================================================================ + + +class TestVerifyUserVariablesHappyPath: + """Happy path tests for verify_user_variables function.""" + + def test_no_input_vars_no_dependencies(self): + """Test with no input variables and no dependencies.""" + decomp_data: DecompPipelineResult = { + "original_task_prompt": "Test task", + "subtask_list": ["Task A"], + "identified_constraints": [], + "subtasks": [ + { + "subtask": "Task A", + "tag": "TASK_A", + "constraints": [], + "prompt_template": "Do A", + "input_vars_required": [], + "depends_on": [], + } + ], + } + + result = verify_user_variables(decomp_data, None) + + assert result == decomp_data + assert len(result["subtasks"]) == 1 + + def test_valid_input_vars(self): + """Test with valid input variables.""" + decomp_data: DecompPipelineResult = { + "original_task_prompt": "Test task", + "subtask_list": ["Task A"], + "identified_constraints": [], + "subtasks": [ + { + "subtask": "Task A", + "tag": "TASK_A", + "constraints": [], + "prompt_template": "Do A with {{ USER_INPUT }}", + "input_vars_required": ["USER_INPUT"], + "depends_on": [], + } + ], + } + + result = verify_user_variables(decomp_data, ["USER_INPUT"]) + + assert result == decomp_data + + def test_case_insensitive_input_vars(self): + """Test that input variable matching is case-insensitive.""" + decomp_data: DecompPipelineResult = { + "original_task_prompt": "Test task", + "subtask_list": ["Task A"], + "identified_constraints": [], + "subtasks": [ + { + "subtask": "Task A", + "tag": "TASK_A", + "constraints": [], + "prompt_template": "Do A", + "input_vars_required": ["user_input"], # lowercase + "depends_on": [], + } + ], + } + + # Should work with uppercase input + result = verify_user_variables(decomp_data, ["USER_INPUT"]) + + assert result == decomp_data + + def test_valid_dependencies_in_order(self): + """Test with valid dependencies already in correct order.""" + decomp_data: DecompPipelineResult = { + "original_task_prompt": "Test task", + "subtask_list": ["Task A", "Task B"], + "identified_constraints": [], + "subtasks": [ + { + "subtask": "Task A", + "tag": "TASK_A", + "constraints": [], + "prompt_template": "Do A", + "input_vars_required": [], + "depends_on": [], + }, + { + "subtask": "Task B", + "tag": "TASK_B", + "constraints": [], + "prompt_template": "Do B", + "input_vars_required": [], + "depends_on": ["TASK_A"], + }, + ], + } + + result = verify_user_variables(decomp_data, None) + + # Should not reorder since already correct + assert result["subtasks"][0]["tag"] == "TASK_A" + assert result["subtasks"][1]["tag"] == "TASK_B" + + def test_dependencies_out_of_order_triggers_reorder(self): + """Test that out-of-order dependencies trigger automatic reordering.""" + decomp_data: DecompPipelineResult = { + "original_task_prompt": "Test task", + "subtask_list": ["Task B", "Task A"], + "identified_constraints": [], + "subtasks": [ + { + "subtask": "Task B", + "tag": "TASK_B", + "constraints": [], + "prompt_template": "Do B", + "input_vars_required": [], + "depends_on": ["TASK_A"], + }, + { + "subtask": "Task A", + "tag": "TASK_A", + "constraints": [], + "prompt_template": "Do A", + "input_vars_required": [], + "depends_on": [], + }, + ], + } + + result = verify_user_variables(decomp_data, None) + + # Should reorder to A, B + assert result["subtasks"][0]["tag"] == "TASK_A" + assert result["subtasks"][1]["tag"] == "TASK_B" + + def test_complex_reordering(self): + """Test complex dependency reordering.""" + decomp_data: DecompPipelineResult = { + "original_task_prompt": "Test task", + "subtask_list": ["Task D", "Task C", "Task B", "Task A"], + "identified_constraints": [], + "subtasks": [ + { + "subtask": "Task D", + "tag": "TASK_D", + "constraints": [], + "prompt_template": "Do D", + "input_vars_required": [], + "depends_on": ["TASK_B", "TASK_C"], + }, + { + "subtask": "Task C", + "tag": "TASK_C", + "constraints": [], + "prompt_template": "Do C", + "input_vars_required": [], + "depends_on": ["TASK_A"], + }, + { + "subtask": "Task B", + "tag": "TASK_B", + "constraints": [], + "prompt_template": "Do B", + "input_vars_required": [], + "depends_on": ["TASK_A"], + }, + { + "subtask": "Task A", + "tag": "TASK_A", + "constraints": [], + "prompt_template": "Do A", + "input_vars_required": [], + "depends_on": [], + }, + ], + } + + result = verify_user_variables(decomp_data, None) + + # A must be first, D must be last + assert result["subtasks"][0]["tag"] == "TASK_A" + assert result["subtasks"][3]["tag"] == "TASK_D" + + +class TestVerifyUserVariablesUnHappyPath: + """Negative tests for verify_user_variables function.""" + + def test_missing_required_input_var(self): + """Test error when required input variable is not provided.""" + decomp_data: DecompPipelineResult = { + "original_task_prompt": "Test task", + "subtask_list": ["Task A"], + "identified_constraints": [], + "subtasks": [ + { + "subtask": "Task A", + "tag": "TASK_A", + "constraints": [], + "prompt_template": "Do A", + "input_vars_required": ["MISSING_VAR"], + "depends_on": [], + } + ], + } + + with pytest.raises( + ValueError, + match='Subtask "task_a" requires input variable "MISSING_VAR" which was not provided', + ): + verify_user_variables(decomp_data, None) + + def test_missing_required_input_var_with_some_provided(self): + """Test error when one of multiple required variables is missing.""" + decomp_data: DecompPipelineResult = { + "original_task_prompt": "Test task", + "subtask_list": ["Task A"], + "identified_constraints": [], + "subtasks": [ + { + "subtask": "Task A", + "tag": "TASK_A", + "constraints": [], + "prompt_template": "Do A", + "input_vars_required": ["VAR1", "VAR2"], + "depends_on": [], + } + ], + } + + with pytest.raises( + ValueError, + match='Subtask "task_a" requires input variable "VAR2" which was not provided', + ): + verify_user_variables(decomp_data, ["VAR1"]) + + def test_dependency_on_nonexistent_subtask(self): + """Test error when subtask depends on non-existent subtask.""" + decomp_data: DecompPipelineResult = { + "original_task_prompt": "Test task", + "subtask_list": ["Task A"], + "identified_constraints": [], + "subtasks": [ + { + "subtask": "Task A", + "tag": "TASK_A", + "constraints": [], + "prompt_template": "Do A", + "input_vars_required": [], + "depends_on": ["NONEXISTENT_TASK"], + } + ], + } + + with pytest.raises( + ValueError, + match='Subtask "task_a" depends on variable "NONEXISTENT_TASK" which does not exist', + ): + verify_user_variables(decomp_data, None) + + def test_circular_dependency_detected(self): + """Test that circular dependencies are caught during reordering.""" + decomp_data: DecompPipelineResult = { + "original_task_prompt": "Test task", + "subtask_list": ["Task A", "Task B"], + "identified_constraints": [], + "subtasks": [ + { + "subtask": "Task B", + "tag": "TASK_B", + "constraints": [], + "prompt_template": "Do B", + "input_vars_required": [], + "depends_on": ["TASK_A"], + }, + { + "subtask": "Task A", + "tag": "TASK_A", + "constraints": [], + "prompt_template": "Do A", + "input_vars_required": [], + "depends_on": ["TASK_B"], + }, + ], + } + + with pytest.raises(ValueError, match="Circular dependency detected"): + verify_user_variables(decomp_data, None) + + def test_empty_input_var_list_treated_as_none(self): + """Test that empty input_var list is treated same as None.""" + decomp_data: DecompPipelineResult = { + "original_task_prompt": "Test task", + "subtask_list": ["Task A"], + "identified_constraints": [], + "subtasks": [ + { + "subtask": "Task A", + "tag": "TASK_A", + "constraints": [], + "prompt_template": "Do A", + "input_vars_required": ["REQUIRED_VAR"], + "depends_on": [], + } + ], + } + + # Both should raise the same error + with pytest.raises(ValueError, match="requires input variable"): + verify_user_variables(decomp_data, []) + + with pytest.raises(ValueError, match="requires input variable"): + verify_user_variables(decomp_data, None) From 03b8f7687a3765fdfe666f00332d0f72e949c8d4 Mon Sep 17 00:00:00 2001 From: AngeloDanducci Date: Wed, 4 Feb 2026 15:50:02 -0500 Subject: [PATCH 2/2] handle renumbering when reordering --- cli/decompose/decompose.py | 20 ++++-- test/decompose/test_decompose.py | 112 +++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 4 deletions(-) diff --git a/cli/decompose/decompose.py b/cli/decompose/decompose.py index 652525d2..0b193d35 100644 --- a/cli/decompose/decompose.py +++ b/cli/decompose/decompose.py @@ -1,5 +1,6 @@ import json import keyword +import re from enum import Enum from graphlib import TopologicalSorter from pathlib import Path @@ -27,13 +28,15 @@ def reorder_subtasks( """Reorder subtasks based on their dependencies using topological sort. Uses Python's graphlib.TopologicalSorter to perform a topological sort of - subtasks based on their depends_on relationships. + subtasks based on their depends_on relationships. Also renumbers the subtask + descriptions if they start with a number pattern (e.g., "1. ", "2. "). Args: subtasks: List of subtask dictionaries with 'tag' and 'depends_on' fields Returns: - Reordered list of subtasks where all dependencies appear before dependents + Reordered list of subtasks where all dependencies appear before dependents, + with subtask descriptions renumbered to match the new order Raises: ValueError: If a circular dependency is detected @@ -60,8 +63,17 @@ def reorder_subtasks( "Circular dependency detected in subtasks. Cannot automatically reorder." ) from e - # Return reordered subtasks - return [subtask_map[tag] for tag in sorted_tags] + # Get reordered subtasks + reordered = [subtask_map[tag] for tag in sorted_tags] + + # Renumber subtask descriptions if they start with "N. " pattern + number_pattern = re.compile(r"^\d+\.\s+") + for i, subtask in enumerate(reordered, start=1): + if number_pattern.match(subtask["subtask"]): + # Replace the number at the start with the new position + subtask["subtask"] = number_pattern.sub(f"{i}. ", subtask["subtask"]) + + return reordered def verify_user_variables( diff --git a/test/decompose/test_decompose.py b/test/decompose/test_decompose.py index 82ba11d6..a04f92b2 100644 --- a/test/decompose/test_decompose.py +++ b/test/decompose/test_decompose.py @@ -244,6 +244,118 @@ def test_nonexistent_dependency_ignored(self): assert result[0]["tag"] == "TASK_A" assert result[1]["tag"] == "TASK_B" + def test_renumbers_subtask_descriptions(self): + """Test that subtask descriptions with numbers are renumbered after reordering.""" + subtasks: list[DecompSubtasksResult] = [ + { + "subtask": "3. Do task C", + "tag": "TASK_C", + "constraints": [], + "prompt_template": "Do C", + "input_vars_required": [], + "depends_on": ["TASK_B"], + }, + { + "subtask": "2. Do task B", + "tag": "TASK_B", + "constraints": [], + "prompt_template": "Do B", + "input_vars_required": [], + "depends_on": ["TASK_A"], + }, + { + "subtask": "1. Do task A", + "tag": "TASK_A", + "constraints": [], + "prompt_template": "Do A", + "input_vars_required": [], + "depends_on": [], + }, + ] + + result = reorder_subtasks(subtasks) + + # Should reorder and renumber + assert len(result) == 3 + assert result[0]["subtask"] == "1. Do task A" + assert result[1]["subtask"] == "2. Do task B" + assert result[2]["subtask"] == "3. Do task C" + + def test_renumbers_only_numbered_subtasks(self): + """Test that only subtasks starting with numbers are renumbered.""" + subtasks: list[DecompSubtasksResult] = [ + { + "subtask": "2. Numbered task B", + "tag": "TASK_B", + "constraints": [], + "prompt_template": "Do B", + "input_vars_required": [], + "depends_on": ["TASK_A"], + }, + { + "subtask": "Unnumbered task A", + "tag": "TASK_A", + "constraints": [], + "prompt_template": "Do A", + "input_vars_required": [], + "depends_on": [], + }, + ] + + result = reorder_subtasks(subtasks) + + # A should stay unnumbered, B should be renumbered to 2 + assert len(result) == 2 + assert result[0]["subtask"] == "Unnumbered task A" + assert result[1]["subtask"] == "2. Numbered task B" + + def test_renumbers_with_complex_reordering(self): + """Test renumbering with reordering.""" + subtasks: list[DecompSubtasksResult] = [ + { + "subtask": "4. Final task", + "tag": "TASK_D", + "constraints": [], + "prompt_template": "Do D", + "input_vars_required": [], + "depends_on": ["TASK_B", "TASK_C"], + }, + { + "subtask": "3. Third task", + "tag": "TASK_C", + "constraints": [], + "prompt_template": "Do C", + "input_vars_required": [], + "depends_on": ["TASK_A"], + }, + { + "subtask": "2. Second task", + "tag": "TASK_B", + "constraints": [], + "prompt_template": "Do B", + "input_vars_required": [], + "depends_on": ["TASK_A"], + }, + { + "subtask": "1. First task", + "tag": "TASK_A", + "constraints": [], + "prompt_template": "Do A", + "input_vars_required": [], + "depends_on": [], + }, + ] + + result = reorder_subtasks(subtasks) + + # Should maintain correct numbering after reorder + assert len(result) == 4 + assert result[0]["subtask"] == "1. First task" + assert result[3]["subtask"] == "4. Final task" + # B and C can be in either order but should be numbered 2 and 3 + middle_numbers = {result[1]["subtask"][:2], result[2]["subtask"][:2]} + assert middle_numbers == {"2.", "3."} + class TestReorderSubtasksUnhappyPath: """Negative tests for reorder_subtasks function."""