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
386 lines
14 KiB
Python
386 lines
14 KiB
Python
"""M4 E2E domain-model verification tests.
|
|
|
|
Tests subplan spawning, plan tree field completeness, parallel execution
|
|
constraints, and parent plan subplan status tracking.
|
|
|
|
Tree *construction* logic is exercised by the CLI-level ``cli_plan_tree()``
|
|
test (which invokes ``build_decision_tree()``); this module validates the
|
|
``SubplanStatus`` and ``PlanIdentity`` data models that feed into that tree.
|
|
|
|
Merge tests (``merge-clean``, ``merge-conflict``) live in the separate
|
|
``helper_m4_e2e_merge`` module to keep this file well under the 500-line
|
|
limit.
|
|
|
|
Usage (via dispatcher):
|
|
python robot/helper_m4_e2e_verification.py spawn-subplans
|
|
python robot/helper_m4_e2e_verification.py plan-tree
|
|
python robot/helper_m4_e2e_verification.py parallel-max
|
|
python robot/helper_m4_e2e_verification.py parent-tracking
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
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 ( # noqa: E402
|
|
_CHILD_A_ULID,
|
|
_CHILD_B_ULID,
|
|
_CHILD_C_ULID,
|
|
_FROZEN_NOW,
|
|
_ROOT_ULID,
|
|
_fail,
|
|
_make_subplan_status,
|
|
_make_subplan_statuses,
|
|
_mock_child_plan,
|
|
_mock_parent_plan,
|
|
)
|
|
|
|
from cleveragents.domain.models.core.plan import ( # noqa: E402
|
|
ExecutionMode,
|
|
ProcessingState,
|
|
SubplanAttempt,
|
|
SubplanConfig,
|
|
SubplanFailureHandler,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# spawn-subplans
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def spawn_subplans() -> None:
|
|
"""Verify a parent plan can spawn multiple subplans during Execute."""
|
|
statuses = _make_subplan_statuses()
|
|
parent = _mock_parent_plan(subplan_statuses=statuses)
|
|
|
|
# Verify parent has subplan config
|
|
if parent.subplan_config is None:
|
|
_fail("parent plan missing subplan_config")
|
|
if parent.subplan_config.execution_mode != ExecutionMode.PARALLEL:
|
|
_fail(f"expected PARALLEL mode, got {parent.subplan_config.execution_mode}")
|
|
|
|
# Verify subplans are tracked
|
|
if not parent.has_subplans:
|
|
_fail("parent should have subplans")
|
|
if len(parent.subplan_statuses) != 3:
|
|
_fail(f"expected 3 subplans, got {len(parent.subplan_statuses)}")
|
|
|
|
# Create child plans and verify identity hierarchy
|
|
child_a = _mock_child_plan(_CHILD_A_ULID)
|
|
child_b = _mock_child_plan(_CHILD_B_ULID)
|
|
child_c = _mock_child_plan(_CHILD_C_ULID)
|
|
|
|
for child in [child_a, child_b, child_c]:
|
|
if not child.is_subplan:
|
|
_fail(f"child {child.identity.plan_id} should be a subplan")
|
|
if child.identity.parent_plan_id != _ROOT_ULID:
|
|
_fail(f"child parent_plan_id mismatch: {child.identity.parent_plan_id}")
|
|
if child.identity.root_plan_id != _ROOT_ULID:
|
|
_fail(f"child root_plan_id mismatch: {child.identity.root_plan_id}")
|
|
|
|
# Verify parent is root
|
|
if not parent.is_root_plan:
|
|
_fail("parent should be root plan")
|
|
if parent.depth != 0:
|
|
_fail(f"parent depth should be 0, got {parent.depth}")
|
|
|
|
# Verify child depth placeholder
|
|
if child_a.depth != -1:
|
|
_fail(f"child depth should be -1 (placeholder), got {child_a.depth}")
|
|
|
|
# Verify CLI dict includes subplan_count
|
|
cli_dict = parent.as_cli_dict()
|
|
actual = cli_dict.get("subplan_count")
|
|
if actual != 3:
|
|
_fail(f"as_cli_dict subplan_count should be 3, got {actual}")
|
|
|
|
print("m4-spawn-subplans-ok")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# plan-tree (SubplanStatus field completeness + parent linkage)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def plan_tree() -> None:
|
|
"""Verify SubplanStatus field completeness and parent→child linkage.
|
|
|
|
This test validates that the ``SubplanStatus`` data model used by
|
|
``plan tree`` has all expected fields populated (``subplan_id``,
|
|
``action_name``, ``changeset_summary``, ``files_changed``) and that
|
|
child ``PlanIdentity`` objects correctly reference the parent.
|
|
|
|
Tree *construction* logic (``build_decision_tree()``) is exercised
|
|
by the CLI-level ``cli_plan_tree()`` test, which verifies the full
|
|
structural hierarchy via JSON output.
|
|
"""
|
|
statuses = _make_subplan_statuses()
|
|
parent = _mock_parent_plan(subplan_statuses=statuses)
|
|
|
|
child_a = _mock_child_plan(_CHILD_A_ULID)
|
|
child_b = _mock_child_plan(_CHILD_B_ULID)
|
|
child_c = _mock_child_plan(_CHILD_C_ULID)
|
|
|
|
# Verify the parent reports the correct number of subplans.
|
|
if len(parent.subplan_statuses) != 3:
|
|
_fail(f"expected 3 subplan statuses, got {len(parent.subplan_statuses)}")
|
|
|
|
# Verify each subplan status has required fields populated.
|
|
for status in parent.subplan_statuses:
|
|
if not status.subplan_id:
|
|
_fail("subplan status missing subplan_id")
|
|
if not status.action_name:
|
|
_fail("subplan status missing action_name")
|
|
|
|
# Verify completed subplans have changeset summaries.
|
|
completed = [
|
|
s for s in parent.subplan_statuses if s.status == ProcessingState.COMPLETE
|
|
]
|
|
if len(completed) != 2:
|
|
_fail(f"expected 2 completed subplans, got {len(completed)}")
|
|
for s in completed:
|
|
if not s.changeset_summary:
|
|
_fail(f"completed subplan {s.subplan_id} missing changeset_summary")
|
|
if s.files_changed <= 0:
|
|
_fail(f"completed subplan {s.subplan_id} has no files_changed")
|
|
|
|
# Verify queued subplan has no changeset.
|
|
queued = [s for s in parent.subplan_statuses if s.status == ProcessingState.QUEUED]
|
|
if len(queued) != 1:
|
|
_fail(f"expected 1 queued subplan, got {len(queued)}")
|
|
if queued[0].changeset_summary is not None:
|
|
_fail("queued subplan should not have changeset_summary")
|
|
|
|
# Verify parent → child reverse link via PlanIdentity.
|
|
for child in [child_a, child_b, child_c]:
|
|
if child.identity.parent_plan_id != parent.identity.plan_id:
|
|
_fail(f"child {child.identity.plan_id} parent mismatch")
|
|
|
|
print("m4-plan-tree-ok")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# parallel-max
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def parallel_max() -> None:
|
|
"""Verify parallel subplan execution respects max_parallel."""
|
|
config = SubplanConfig(
|
|
execution_mode=ExecutionMode.PARALLEL,
|
|
max_parallel=3,
|
|
fail_fast=False,
|
|
retry_failed=True,
|
|
max_retries=2,
|
|
)
|
|
|
|
# Verify max_parallel constraints (1 <= max_parallel <= 50)
|
|
try:
|
|
SubplanConfig(execution_mode=ExecutionMode.PARALLEL, max_parallel=0)
|
|
_fail("max_parallel=0 should be rejected")
|
|
except ValueError:
|
|
pass # Expected: ge=1 constraint
|
|
|
|
try:
|
|
SubplanConfig(execution_mode=ExecutionMode.PARALLEL, max_parallel=51)
|
|
_fail("max_parallel=51 should be rejected")
|
|
except ValueError:
|
|
pass # Expected: le=50 constraint
|
|
|
|
# Verify a parent plan with parallel config and multiple subplans
|
|
statuses = [
|
|
_make_subplan_status(
|
|
_CHILD_A_ULID,
|
|
target_resources=["res-1"],
|
|
status=ProcessingState.PROCESSING,
|
|
started_at=_FROZEN_NOW,
|
|
),
|
|
_make_subplan_status(
|
|
_CHILD_B_ULID,
|
|
target_resources=["res-2"],
|
|
status=ProcessingState.PROCESSING,
|
|
started_at=_FROZEN_NOW,
|
|
),
|
|
_make_subplan_status(
|
|
_CHILD_C_ULID,
|
|
target_resources=["res-3"],
|
|
),
|
|
]
|
|
|
|
parent = _mock_parent_plan(subplan_config=config, subplan_statuses=statuses)
|
|
|
|
# Count running subplans -- should not exceed max_parallel
|
|
running = [
|
|
s for s in parent.subplan_statuses if s.status == ProcessingState.PROCESSING
|
|
]
|
|
if len(running) > config.max_parallel:
|
|
_fail(
|
|
f"running subplans ({len(running)}) exceeds "
|
|
f"max_parallel ({config.max_parallel})"
|
|
)
|
|
|
|
# Verify all execution modes are valid
|
|
for mode in ExecutionMode:
|
|
cfg = SubplanConfig(execution_mode=mode, max_parallel=5)
|
|
if cfg.execution_mode != mode:
|
|
_fail(f"execution_mode mismatch: {cfg.execution_mode} != {mode}")
|
|
|
|
# Verify SubplanFailureHandler with parallel config
|
|
handler = SubplanFailureHandler()
|
|
failed_status = _make_subplan_status(
|
|
_CHILD_A_ULID,
|
|
status=ProcessingState.ERRORED,
|
|
error="ValidationError: tests failed",
|
|
)
|
|
|
|
# fail_fast=False and parallel => should NOT stop others
|
|
if handler.should_stop_others(config, failed_status):
|
|
_fail("parallel + fail_fast=False should not stop others")
|
|
|
|
# fail_fast=True => should stop others
|
|
ff_config = SubplanConfig(
|
|
execution_mode=ExecutionMode.PARALLEL,
|
|
max_parallel=3,
|
|
fail_fast=True,
|
|
)
|
|
if not handler.should_stop_others(ff_config, failed_status):
|
|
_fail("fail_fast=True should stop others")
|
|
|
|
# Retry check: ValidationError is retriable
|
|
if not handler.should_retry(config, failed_status):
|
|
_fail("ValidationError should be retriable")
|
|
|
|
print("m4-parallel-max-ok")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# parent-tracking
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def parent_tracking() -> None:
|
|
"""Verify parent plan tracks all subplan statuses correctly."""
|
|
statuses = [
|
|
_make_subplan_status(
|
|
_CHILD_A_ULID,
|
|
target_resources=["res-api"],
|
|
status=ProcessingState.COMPLETE,
|
|
started_at=_FROZEN_NOW,
|
|
completed_at=_FROZEN_NOW,
|
|
changeset_summary="Updated API",
|
|
files_changed=3,
|
|
),
|
|
_make_subplan_status(
|
|
_CHILD_B_ULID,
|
|
target_resources=["res-ui"],
|
|
status=ProcessingState.ERRORED,
|
|
started_at=_FROZEN_NOW,
|
|
completed_at=_FROZEN_NOW,
|
|
error="TimeoutError: execution timed out",
|
|
),
|
|
_make_subplan_status(
|
|
_CHILD_C_ULID,
|
|
target_resources=["res-db"],
|
|
status=ProcessingState.PROCESSING,
|
|
started_at=_FROZEN_NOW,
|
|
attempt_number=2,
|
|
previous_attempts=[
|
|
SubplanAttempt(
|
|
attempt_number=1,
|
|
started_at=_FROZEN_NOW,
|
|
completed_at=_FROZEN_NOW,
|
|
error="ValidationError: schema mismatch",
|
|
was_retried=True,
|
|
),
|
|
],
|
|
),
|
|
]
|
|
|
|
parent = _mock_parent_plan(subplan_statuses=statuses)
|
|
|
|
# Verify parent tracks all three statuses
|
|
if len(parent.subplan_statuses) != 3:
|
|
_fail(f"expected 3 statuses, got {len(parent.subplan_statuses)}")
|
|
|
|
# Verify completed subplan
|
|
completed = [
|
|
s for s in parent.subplan_statuses if s.status == ProcessingState.COMPLETE
|
|
]
|
|
if len(completed) != 1:
|
|
_fail(f"expected 1 completed, got {len(completed)}")
|
|
if completed[0].files_changed != 3:
|
|
_fail(f"completed files_changed should be 3, got {completed[0].files_changed}")
|
|
|
|
# Verify errored subplan
|
|
errored = [
|
|
s for s in parent.subplan_statuses if s.status == ProcessingState.ERRORED
|
|
]
|
|
if len(errored) != 1:
|
|
_fail(f"expected 1 errored, got {len(errored)}")
|
|
if errored[0].error is None:
|
|
_fail("errored subplan should have error message")
|
|
if "TimeoutError" not in (errored[0].error or ""):
|
|
_fail(f"error should contain TimeoutError, got {errored[0].error}")
|
|
|
|
# Verify retry tracking on third subplan
|
|
retried = [
|
|
s for s in parent.subplan_statuses if s.status == ProcessingState.PROCESSING
|
|
]
|
|
if len(retried) != 1:
|
|
_fail(f"expected 1 processing, got {len(retried)}")
|
|
if retried[0].attempt_number != 2:
|
|
_fail(
|
|
f"retried subplan should be on attempt 2, got {retried[0].attempt_number}"
|
|
)
|
|
if len(retried[0].previous_attempts) != 1:
|
|
_fail(f"expected 1 previous attempt, got {len(retried[0].previous_attempts)}")
|
|
prev = retried[0].previous_attempts[0]
|
|
if not prev.was_retried:
|
|
_fail("previous attempt should have was_retried=True")
|
|
if prev.error is None:
|
|
_fail("previous attempt should have error message")
|
|
|
|
# Verify SubplanFailureHandler retry logic
|
|
handler = SubplanFailureHandler()
|
|
config = parent.subplan_config
|
|
if config is None:
|
|
_fail("parent should have subplan_config")
|
|
|
|
# TimeoutError is retriable
|
|
if not handler.should_retry(config, errored[0]):
|
|
_fail("TimeoutError should be retriable")
|
|
|
|
# Non-retriable error should not retry
|
|
non_retriable_status = _make_subplan_status(
|
|
_CHILD_A_ULID,
|
|
status=ProcessingState.ERRORED,
|
|
error="ConfigurationError: invalid config",
|
|
)
|
|
if handler.should_retry(config, non_retriable_status):
|
|
_fail("ConfigurationError should NOT be retriable")
|
|
|
|
# Exhausted retries should not retry
|
|
exhausted_status = _make_subplan_status(
|
|
_CHILD_A_ULID,
|
|
status=ProcessingState.ERRORED,
|
|
error="ValidationError: tests failed",
|
|
attempt_number=3,
|
|
)
|
|
if handler.should_retry(config, exhausted_status):
|
|
_fail("attempt_number > max_retries should not retry")
|
|
|
|
# Verify as_cli_dict on parent includes subplan tracking
|
|
cli_dict = parent.as_cli_dict()
|
|
if "subplan_count" not in cli_dict:
|
|
_fail("as_cli_dict should include subplan_count")
|
|
if cli_dict["subplan_count"] != 3:
|
|
_fail(f"subplan_count should be 3, got {cli_dict['subplan_count']}")
|
|
|
|
print("m4-parent-tracking-ok")
|