feat(plans): implement configurable merge strategy (prefer-parent, prefer-subplan, manual)

Implemented a configurable merge strategy framework for three-way merges.

- New module: src/cleveragents/domain/models/core/merge_strategy.py
  - MergeStrategy StrEnum with options: prefer-parent, prefer-subplan, manual
  - Helper methods: is_auto_resolve(), is_manual(), from_string()

- New module: src/cleveragents/domain/models/core/merge_strategy_service.py
  - MergeConflict class with __eq__ for value equality comparison
  - MergeStrategyService to apply strategies and resolve conflicts
  - Proper type annotations (dict, Any) with no unused imports

- BDD test suite: features/plan_merge_strategy.feature (8 scenarios)
- Step definitions: features/steps/plan_merge_strategy_steps.py

- Robot Framework integration tests: robot/merge_strategy.robot
- Helper script: robot/helper_merge_strategy.py

- Updated src/cleveragents/domain/models/core/__init__.py exports

ISSUES CLOSED: #9559
This commit is contained in:
2026-04-15 00:38:39 +00:00
committed by drew
parent 84ca1d0764
commit 7279ce64dd
9 changed files with 606 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
"""Helper utilities for merge strategy Robot tests."""
from __future__ import annotations
import sys
from cleveragents.domain.models.core.merge_strategy import MergeStrategy
from cleveragents.domain.models.core.merge_strategy_service import (
MergeConflict,
MergeStrategyService,
)
def _prefer_parent_test() -> None:
"""Test prefer-parent strategy auto-resolves conflicts."""
service = MergeStrategyService(strategy=MergeStrategy.PREFER_PARENT)
conflicts = [
MergeConflict(path="field1", parent_value="p1", subplan_value="s1"),
MergeConflict(path="field2", parent_value="p2", subplan_value="s2"),
]
resolved = service.resolve_conflicts(conflicts)
assert resolved["field1"] == "p1"
assert resolved["field2"] == "p2"
assert service.can_auto_resolve() is True
print("merge-strategy-prefer-parent-ok")
def _prefer_subplan_test() -> None:
"""Test prefer-subplan strategy auto-resolves conflicts."""
service = MergeStrategyService(strategy=MergeStrategy.PREFER_SUBPLAN)
conflicts = [
MergeConflict(path="field1", parent_value="p1", subplan_value="s1"),
MergeConflict(path="field2", parent_value="p2", subplan_value="s2"),
]
resolved = service.resolve_conflicts(conflicts)
assert resolved["field1"] == "s1"
assert resolved["field2"] == "s2"
assert service.can_auto_resolve() is True
print("merge-strategy-prefer-subplan-ok")
def _manual_test() -> None:
"""Test manual strategy raises error on auto-resolution."""
service = MergeStrategyService(strategy=MergeStrategy.MANUAL)
conflicts = [
MergeConflict(path="field1", parent_value="p1", subplan_value="s1"),
]
try:
service.resolve_conflicts(conflicts)
raise AssertionError("Expected ValueError for manual strategy")
except ValueError as e:
assert "manual" in str(e).lower()
assert service.can_auto_resolve() is False
print("merge-strategy-manual-ok")
def _no_conflicts_test() -> None:
"""Test no conflicts returns empty result."""
service = MergeStrategyService(strategy=MergeStrategy.PREFER_PARENT)
resolved = service.resolve_conflicts([])
assert resolved == {}
assert not service.has_conflicts([])
print("merge-strategy-no-conflicts-ok")
def _from_string_test() -> None:
"""Test MergeStrategy.from_string creates correct enum."""
strategy = MergeStrategy.from_string("prefer-parent")
assert strategy.value == "prefer-parent"
assert str(strategy) == "prefer-parent"
strategy_subplan = MergeStrategy.from_string("prefer-subplan")
assert strategy_subplan.value == "prefer-subplan"
strategy_manual = MergeStrategy.from_string("manual")
assert strategy_manual.is_manual() is True
assert strategy_manual.is_auto_resolve() is False
print("merge-strategy-from-string-ok")
def _invalid_string_test() -> None:
"""Test invalid string raises ValueError."""
try:
MergeStrategy.from_string("invalid-strategy")
raise AssertionError("Expected ValueError for invalid strategy string")
except ValueError as e:
assert "Invalid merge strategy" in str(e)
print("merge-strategy-invalid-string-ok")
def _enums_test() -> None:
"""Test MergeStrategy enum values and methods."""
assert MergeStrategy.PREFER_PARENT == "prefer-parent"
assert MergeStrategy.PREFER_SUBPLAN == "prefer-subplan"
assert MergeStrategy.MANUAL == "manual"
assert MergeStrategy.PREFER_PARENT.is_auto_resolve() is True
assert MergeStrategy.PREFER_SUBPLAN.is_auto_resolve() is True
assert MergeStrategy.MANUAL.is_auto_resolve() is False
assert MergeStrategy.PREFER_PARENT.is_manual() is False
assert MergeStrategy.PREFER_SUBPLAN.is_manual() is False
assert MergeStrategy.MANUAL.is_manual() is True
print("merge-strategy-enums-ok")
def main() -> None:
if len(sys.argv) < 2:
raise SystemExit("Expected command argument")
command = sys.argv[1]
tests = {
"prefer_parent": _prefer_parent_test,
"prefer_subplan": _prefer_subplan_test,
"manual": _manual_test,
"no_conflicts": _no_conflicts_test,
"from_string": _from_string_test,
"invalid_string": _invalid_string_test,
"enums": _enums_test,
}
if command not in tests:
raise SystemExit(f"Unknown command: {command}")
tests[command]()
+49
View File
@@ -0,0 +1,49 @@
*** Settings ***
Documentation Integration tests for merge strategy configuration (prefer-parent, prefer-subplan, manual)
Resource ${CURDIR}/common.resource
*** Variables ***
${HELPER_SCRIPT} robot/helper_merge_strategy.py
*** Test Cases ***
Prefer-Parent Auto-Resolves Conflicts
[Documentation] Verify prefer-parent strategy resolves all conflicts in favor of parent values
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} prefer_parent cwd=${WORKSPACE}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} merge-strategy-prefer-parent-ok
Prefer-Subplan Auto-Resolves Conflicts
[Documentation] Verify prefer-subplan strategy resolves all conflicts in favor of subplan values
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} prefer_subplan cwd=${WORKSPACE}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} merge-strategy-prefer-subplan-ok
Manual Strategy Requires Resolution
[Documentation] Verify manual strategy raises ValueError on attempted auto-resolution
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} manual cwd=${WORKSPACE}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} merge-strategy-manual-ok
No Conflicts Returns Empty Result
[Documentation] Verify empty conflict list returns empty resolution
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} no_conflicts cwd=${WORKSPACE}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} merge-strategy-no-conflicts-ok
Strategy From String Creates Correct Enum
[Documentation] Verify MergeStrategy.from_string creates proper enum values
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} from_string cwd=${WORKSPACE}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} merge-strategy-from-string-ok
Invalid String Raises Error
[Documentation] Verify invalid string raises ValueError with descriptive message
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} invalid_string cwd=${WORKSPACE}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} merge-strategy-invalid-string-ok
Enum Values And Methods Validated
[Documentation] Verify all enum member values and helper methods are correct
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} enums cwd=${WORKSPACE}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} merge-strategy-enums-ok