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
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
147 lines
5.0 KiB
Python
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")
|