Files
cleveragents-core/robot/helper_m4_e2e_merge.py
T
hurui200320 317d015260
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 16s
CI / build (pull_request) Successful in 17s
CI / quality (pull_request) Successful in 18s
CI / e2e_tests (pull_request) Successful in 27s
CI / security (pull_request) Successful in 38s
CI / typecheck (pull_request) Successful in 39s
CI / unit_tests (pull_request) Successful in 3m0s
CI / docker (pull_request) Successful in 10s
CI / integration_tests (pull_request) Successful in 3m29s
CI / coverage (pull_request) Successful in 5m54s
CI / lint (push) Successful in 13s
CI / build (push) Successful in 14s
CI / quality (push) Successful in 17s
CI / security (push) Successful in 38s
CI / e2e_tests (push) Successful in 36s
CI / typecheck (push) Successful in 39s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m59s
CI / integration_tests (push) Successful in 3m24s
CI / docker (push) Successful in 38s
CI / coverage (push) Successful in 5m54s
CI / benchmark-publish (push) Successful in 19m34s
CI / benchmark-regression (pull_request) Successful in 36m21s
test(e2e): validate M4 acceptance criteria for v3.3.0 milestone closure
Strengthen M4 E2E CLI acceptance tests and address all review findings
from the self-review on PR #681 (3 blocking, 7 non-blocking issues).

Blocking fixes:
- Remove tautological domain assertions from cli_plan_execute() that
  verified values the test itself constructed via _mock_parent_plan and
  could never fail.  Replace with a comment documenting that subplan
  info cannot be verified from CLI output alone.
- Fix unguarded positional arg access in plan_diff() that would raise
  IndexError if the production CLI passes plan_id as a keyword argument.
  Now uses kwargs-with-positional-fallback pattern.
- Split the 1074-line helper_m4_e2e_verification.py into four focused
  modules under the CONTRIBUTING.md 500-line limit:
    helper_m4_e2e_common.py (237 lines) — constants, helpers, factories
    helper_m4_e2e_domain.py (495 lines) — domain model tests
    helper_m4_e2e_cli.py    (426 lines) — CLI integration tests
    helper_m4_e2e_verification.py (81 lines) — thin dispatcher

Non-blocking improvements:
- Add ProjectLink isinstance type guard before iterating project_links.
- Wrap all assert_called_once*() calls in try/except with _fail()
  pattern to produce standardised FAIL: diagnostics instead of raw
  AssertionError.
- Make project_links extraction symmetric with action_name (kwargs +
  positional fallback at index 1).
- Replace broad "execute" substring match with word-boundary regex
  r"\bexecute\b" to avoid false positives from incidental substrings.
- Extract _make_subplan_status() factory, _assert_exit_code() and
  _assert_mock_called_once*() wrappers to eliminate DRY violations
  (SubplanStatus construction repeated 3x, exit-code checks 4x).
- Replace non-deterministic datetime.now() calls with frozen constant
  _FROZEN_NOW = datetime(2026, 3, 1, 12, 0, 0) for deterministic
  fixtures.
- Rename generic _DECISION_ULID_N constants to descriptive names
  (_DECISION_PROMPT_DEF, _DECISION_STRATEGY, _DECISION_PARALLEL_SPAWN,
  _DECISION_SPAWN_API, _DECISION_SPAWN_UI).

Quality gates:
- lint: PASS
- format: PASS (1404 files unchanged)
- typecheck: PASS (0 errors)
- unit_tests: 10,431 scenarios, 0 failures
- integration_tests: 1,452 tests, 0 failures
- coverage_report: PASS (98%, threshold 97%)
- security_scan: PASS
- dead_code: PASS
- docs: PASS
- build: PASS

ISSUES CLOSED: #495
2026-03-13 14:03:37 +08:00

147 lines
5.0 KiB
Python

"""M4 E2E merge verification tests.
Tests three-way merge for non-conflicting changes and conflict surfacing.
Extracted from ``helper_m4_e2e_domain`` to keep that module well under
the 500-line limit (CONTRIBUTING.md compliance).
Usage (via dispatcher):
python robot/helper_m4_e2e_verification.py merge-clean
python robot/helper_m4_e2e_verification.py merge-conflict
"""
from __future__ import annotations
import shutil
import sys
from pathlib import Path
# Ensure the src directory is on the import path.
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.append(_SRC)
from helper_m4_e2e_common import _fail # noqa: E402
from cleveragents.domain.models.core.plan import SubplanMergeStrategy # noqa: E402
from cleveragents.infrastructure.sandbox.merge import ( # noqa: E402
GitMergeStrategy,
MergeResult,
SequentialMergeStrategy,
)
def _assert_git_available() -> None:
"""Pre-check that ``git`` is available on ``$PATH``.
``GitMergeStrategy.merge()`` shells out to ``git merge-file``. If
``git`` is absent, the fallback to ``SequentialMergeStrategy``
produces results that fail the merge assertions with misleading
diagnostics (e.g. "missing ours change" instead of "git not found").
"""
if shutil.which("git") is None:
_fail("git is required on PATH for merge tests")
# ---------------------------------------------------------------------------
# merge-clean
# ---------------------------------------------------------------------------
def merge_clean() -> None:
"""Verify three-way merge combines non-conflicting changes."""
_assert_git_available()
strategy = GitMergeStrategy()
base = "line1\nline2\nline3\nline4\nline5\n"
ours = "line1-modified-by-subplan-A\nline2\nline3\nline4\nline5\n"
theirs = "line1\nline2\nline3\nline4\nline5-modified-by-subplan-B\n"
result = strategy.merge(base, ours, theirs)
if not result.success:
_fail(f"clean merge should succeed, got conflicts: {result.has_conflicts}")
if result.has_conflicts:
_fail("clean merge should have no conflicts")
if "line1-modified-by-subplan-A" not in result.content:
_fail("merged content missing ours change")
if "line5-modified-by-subplan-B" not in result.content:
_fail("merged content missing theirs change")
if result.conflict_markers:
_fail(f"should have no conflict markers, got {result.conflict_markers}")
# Also verify SequentialMergeStrategy (last-wins)
seq_strategy = SequentialMergeStrategy()
seq_result = seq_strategy.merge(base, ours, theirs)
if not seq_result.success:
_fail("sequential merge should always succeed")
if seq_result.content != theirs:
_fail("sequential merge should return theirs")
# Verify merge result dataclass fields
clean_result = MergeResult(success=True, content="merged", has_conflicts=False)
if clean_result.success is not True:
_fail("MergeResult success field incorrect")
if clean_result.has_conflicts is not False:
_fail("MergeResult has_conflicts field incorrect")
print("m4-merge-clean-ok")
# ---------------------------------------------------------------------------
# merge-conflict
# ---------------------------------------------------------------------------
def merge_conflict() -> None:
"""Verify merge conflicts are surfaced correctly."""
_assert_git_available()
strategy = GitMergeStrategy()
base = "line1\nline2\nline3\n"
ours = "line1\nline2-ours\nline3\n"
theirs = "line1\nline2-theirs\nline3\n"
result = strategy.merge(base, ours, theirs)
if result.success:
_fail("conflicting merge should not succeed")
if not result.has_conflicts:
_fail("conflicting merge should have conflicts")
if not result.conflict_markers:
_fail("conflict markers should be present")
# Verify conflict markers contain standard git markers
if "<<<<<<<" not in result.content:
_fail("merged content should contain <<<<<<< marker")
if ">>>>>>>" not in result.content:
_fail("merged content should contain >>>>>>> marker")
if "=======" not in result.content:
_fail("merged content should contain ======= marker")
# Verify conflict_markers list has at least one region
for start, end in result.conflict_markers:
if start < 1:
_fail(f"conflict marker start should be >= 1, got {start}")
if end < start:
_fail(f"conflict marker end ({end}) should be >= start ({start})")
# Verify SubplanMergeStrategy enum values
if SubplanMergeStrategy.FAIL_ON_CONFLICT != "fail_on_conflict":
_fail("FAIL_ON_CONFLICT value mismatch")
expected_strategies = {
"git_three_way",
"sequential_apply",
"fail_on_conflict",
"last_wins",
}
actual_strategies = {s.value for s in SubplanMergeStrategy}
if actual_strategies != expected_strategies:
_fail(
f"merge strategies mismatch: {actual_strategies} != {expected_strategies}"
)
print("m4-merge-conflict-ok")