forked from HAL9000/cleveragents-core
3a2b134f3c
Added Behave BDD feature files and step definitions targeting coverage gaps in six modules: - container.py: exercise get_database_url env-var fallback, AI provider None path, cached container singleton, override_providers edge cases (lines 66-69, 125-130; branches at 51, 57, 82, 87, 256, 284-288) - correction_service.py: exercise exception-handling paths in execute_revert and execute_append via monkeypatched analyze_impact and ULID failures (lines 254-262, 320-328) - plan_lifecycle_service.py: exercise _persisted UoW commit paths, InvalidPhaseTransitionError custom message branch, non-reusable action archive, and error_details merge logic (branches at 100, 216, 237, 327, 461, 570, 576, 607) - plan.py (CLI): exercise spec-dict optional field branches, _print_lifecycle_plan conditional rendering, use_action argument parsing, auto-resolve paths, legacy wrappers, and validation error branches across 66 scenarios - skill.py (CLI): exercise singleton cache, timestamp-absent show, no-tools MCP, add/remove/list/show format and error branches across 26 scenarios - models.py (DB): exercise to_domain/from_domain None-field branches in SkillModel, SessionModel, ToolModel, LifecycleActionModel, LifecyclePlanModel, NamespacedProjectModel, and SessionMessageModel across 41 scenarios All 302 features, 6503 scenarios, 28271 steps pass (nox -e unit_tests). ISSUES CLOSED: #446
397 lines
13 KiB
Python
397 lines
13 KiB
Python
"""Step definitions for plan_cli_spec_print_r2.feature.
|
|
|
|
Targets remaining partial branches in
|
|
``cleveragents.cli.commands.plan`` (plan.py) - round 2, split 1 of 3.
|
|
|
|
Covers:
|
|
- ``_plan_spec_dict``: project link alias/read_only, automation_profile,
|
|
invariants, validation_summary/dod, last_completed_step, last_checkpoint_id
|
|
- ``_print_lifecycle_plan``: definition_of_done, dod evaluation pass/fail,
|
|
arguments with/without order, automation profile, invariants, resume
|
|
metadata, project link alias/read_only, long description
|
|
|
|
All step text uses the ``r2plan-`` prefix to avoid collisions.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from io import StringIO
|
|
from typing import Any
|
|
from unittest.mock import patch
|
|
|
|
from behave import given, then, when
|
|
from typer.testing import CliRunner
|
|
|
|
from cleveragents.cli.commands import plan as plan_module
|
|
from cleveragents.cli.commands.plan import (
|
|
_plan_spec_dict,
|
|
_print_lifecycle_plan,
|
|
)
|
|
from cleveragents.domain.models.core.plan import (
|
|
AutomationProfileProvenance,
|
|
AutomationProfileRef,
|
|
InvariantSource,
|
|
NamespacedName,
|
|
Plan,
|
|
PlanIdentity,
|
|
PlanInvariant,
|
|
PlanPhase,
|
|
PlanTimestamps,
|
|
ProcessingState,
|
|
ProjectLink,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_ULID_BASE = "01ARZ3NDEKTSV4RRFFQ69G5F"
|
|
_runner = CliRunner()
|
|
|
|
|
|
def _ulid(suffix: str = "A1") -> str:
|
|
"""Return a valid 26-char ULID for tests.
|
|
|
|
ULIDs use Crockford's Base32 (0-9, A-H, J-K, M-N, P-T, V-Z; no I/L/O/U).
|
|
"""
|
|
# Map potentially invalid chars to valid Crockford Base32
|
|
cleaned = (
|
|
suffix.replace("I", "J").replace("L", "K").replace("O", "P").replace("U", "V")
|
|
)
|
|
base = _ULID_BASE + cleaned
|
|
return base[:26]
|
|
|
|
|
|
def _make_plan(
|
|
*,
|
|
plan_id: str | None = None,
|
|
name: str = "local/r2-plan",
|
|
description: str = "Test plan for r2 coverage",
|
|
phase: PlanPhase = PlanPhase.STRATEGIZE,
|
|
processing_state: ProcessingState = ProcessingState.QUEUED,
|
|
project_links: list[ProjectLink] | None = None,
|
|
automation_profile: AutomationProfileRef | None = None,
|
|
invariants: list[PlanInvariant] | None = None,
|
|
validation_summary: dict[str, Any] | None = None,
|
|
error_message: str | None = None,
|
|
last_completed_step: int = -1,
|
|
last_checkpoint_id: str | None = None,
|
|
definition_of_done: str | None = None,
|
|
arguments: dict[str, Any] | None = None,
|
|
arguments_order: list[str] | None = None,
|
|
estimation_actor: str | None = None,
|
|
invariant_actor: str | None = None,
|
|
timestamps: PlanTimestamps | None = None,
|
|
action_name: str = "local/test-action",
|
|
) -> Plan:
|
|
if timestamps is None:
|
|
timestamps = PlanTimestamps(
|
|
created_at=datetime.now(),
|
|
updated_at=datetime.now(),
|
|
)
|
|
return Plan(
|
|
identity=PlanIdentity(plan_id=plan_id or _ulid("A1")),
|
|
namespaced_name=NamespacedName.parse(name),
|
|
action_name=action_name,
|
|
description=description,
|
|
definition_of_done=definition_of_done,
|
|
phase=phase,
|
|
processing_state=processing_state,
|
|
strategy_actor=None,
|
|
execution_actor=None,
|
|
project_links=project_links or [],
|
|
automation_profile=automation_profile,
|
|
invariants=invariants or [],
|
|
validation_summary=validation_summary,
|
|
error_message=error_message,
|
|
last_completed_step=last_completed_step,
|
|
last_checkpoint_id=last_checkpoint_id,
|
|
arguments=arguments or {},
|
|
arguments_order=arguments_order or [],
|
|
estimation_actor=estimation_actor,
|
|
invariant_actor=invariant_actor,
|
|
timestamps=timestamps,
|
|
created_by=None,
|
|
reusable=True,
|
|
read_only=False,
|
|
)
|
|
|
|
|
|
def _capture_print(plan: Any) -> str:
|
|
"""Call _print_lifecycle_plan and capture the console output."""
|
|
buf = StringIO()
|
|
from rich.console import Console as RichConsole
|
|
|
|
test_console = RichConsole(file=buf, width=200, no_color=True)
|
|
with patch.object(plan_module, "console", test_console):
|
|
_print_lifecycle_plan(plan, title="R2 Test")
|
|
return buf.getvalue()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Given steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given('r2plan-a v3 Plan with a project link that has alias "{alias}"')
|
|
def step_plan_link_alias(context: Any, alias: str) -> None:
|
|
link = ProjectLink(project_name="local/api", alias=alias)
|
|
context.r2_plan = _make_plan(project_links=[link])
|
|
|
|
|
|
@given("r2plan-a v3 Plan with a project link that is read_only")
|
|
def step_plan_link_readonly(context: Any) -> None:
|
|
link = ProjectLink(project_name="local/docs", read_only=True)
|
|
context.r2_plan = _make_plan(project_links=[link])
|
|
|
|
|
|
@given("r2plan-a v3 Plan with a plain project link")
|
|
def step_plan_link_plain(context: Any) -> None:
|
|
link = ProjectLink(project_name="local/plain")
|
|
context.r2_plan = _make_plan(project_links=[link])
|
|
|
|
|
|
@given('r2plan-a v3 Plan with automation_profile "{profile}"')
|
|
def step_plan_with_automation_profile(context: Any, profile: str) -> None:
|
|
ap = AutomationProfileRef(
|
|
profile_name=profile,
|
|
provenance=AutomationProfileProvenance.PLAN,
|
|
)
|
|
context.r2_plan = _make_plan(automation_profile=ap)
|
|
|
|
|
|
@given("r2plan-a v3 Plan without automation_profile")
|
|
def step_plan_without_automation_profile(context: Any) -> None:
|
|
context.r2_plan = _make_plan(automation_profile=None)
|
|
|
|
|
|
@given("r2plan-a v3 Plan with invariants")
|
|
def step_plan_with_invariants(context: Any) -> None:
|
|
invs = [
|
|
PlanInvariant(text="No new warnings", source=InvariantSource.PLAN),
|
|
PlanInvariant(text="Coverage >= 80%", source=InvariantSource.ACTION),
|
|
]
|
|
context.r2_plan = _make_plan(invariants=invs)
|
|
|
|
|
|
@given("r2plan-a v3 Plan without invariants")
|
|
def step_plan_without_invariants(context: Any) -> None:
|
|
context.r2_plan = _make_plan(invariants=[])
|
|
|
|
|
|
@given("r2plan-a v3 Plan with dod validation summary")
|
|
def step_plan_with_dod_validation(context: Any) -> None:
|
|
vs = {
|
|
"dod_evaluated": True,
|
|
"dod_all_passed": True,
|
|
"required_passed": 3,
|
|
"required_failed": 0,
|
|
}
|
|
context.r2_plan = _make_plan(validation_summary=vs)
|
|
|
|
|
|
@given("r2plan-a v3 Plan without validation_summary")
|
|
def step_plan_without_validation_summary(context: Any) -> None:
|
|
context.r2_plan = _make_plan(validation_summary=None)
|
|
|
|
|
|
@given("r2plan-a v3 Plan with last_completed_step {n:d}")
|
|
def step_plan_with_step(context: Any, n: int) -> None:
|
|
context.r2_plan = _make_plan(last_completed_step=n)
|
|
|
|
|
|
@given("r2plan-a v3 Plan with last_completed_step default")
|
|
def step_plan_with_step_default(context: Any) -> None:
|
|
context.r2_plan = _make_plan(last_completed_step=-1)
|
|
|
|
|
|
@given('r2plan-a v3 Plan with last_checkpoint_id "{chk}"')
|
|
def step_plan_with_checkpoint(context: Any, chk: str) -> None:
|
|
context.r2_plan = _make_plan(last_checkpoint_id=chk)
|
|
|
|
|
|
@given("r2plan-a v3 Plan without last_checkpoint_id")
|
|
def step_plan_without_checkpoint(context: Any) -> None:
|
|
context.r2_plan = _make_plan(last_checkpoint_id=None)
|
|
|
|
|
|
@given('r2plan-a v3 Plan with definition_of_done "{dod}"')
|
|
def step_plan_with_dod(context: Any, dod: str) -> None:
|
|
context.r2_plan = _make_plan(definition_of_done=dod)
|
|
|
|
|
|
@given("r2plan-a v3 Plan with definition_of_done longer than 200 chars")
|
|
def step_plan_with_long_dod(context: Any) -> None:
|
|
dod = "x" * 250
|
|
context.r2_plan = _make_plan(definition_of_done=dod)
|
|
|
|
|
|
@given("r2plan-a v3 Plan without definition_of_done")
|
|
def step_plan_without_dod(context: Any) -> None:
|
|
context.r2_plan = _make_plan(definition_of_done=None)
|
|
|
|
|
|
@given("r2plan-a v3 Plan with dod evaluated as passed")
|
|
def step_plan_dod_passed(context: Any) -> None:
|
|
vs = {
|
|
"dod_evaluated": True,
|
|
"dod_all_passed": True,
|
|
"required_passed": 5,
|
|
"required_failed": 0,
|
|
}
|
|
context.r2_plan = _make_plan(validation_summary=vs)
|
|
|
|
|
|
@given("r2plan-a v3 Plan with dod evaluated as failed")
|
|
def step_plan_dod_failed(context: Any) -> None:
|
|
vs = {
|
|
"dod_evaluated": True,
|
|
"dod_all_passed": False,
|
|
"required_passed": 2,
|
|
"required_failed": 3,
|
|
}
|
|
context.r2_plan = _make_plan(validation_summary=vs)
|
|
|
|
|
|
@given("r2plan-a v3 Plan with arguments and arguments_order")
|
|
def step_plan_with_args_order(context: Any) -> None:
|
|
context.r2_plan = _make_plan(
|
|
arguments={"target_coverage": 80, "format": "json"},
|
|
arguments_order=["target_coverage", "format"],
|
|
)
|
|
|
|
|
|
@given("r2plan-a v3 Plan with arguments but no arguments_order")
|
|
def step_plan_with_args_no_order(context: Any) -> None:
|
|
context.r2_plan = _make_plan(
|
|
arguments={"beta_key": "val", "alpha_key": "val2"},
|
|
arguments_order=[],
|
|
)
|
|
|
|
|
|
@given("r2plan-a v3 Plan with description longer than 200 chars")
|
|
def step_plan_with_long_description(context: Any) -> None:
|
|
context.r2_plan = _make_plan(description="D" * 250)
|
|
|
|
|
|
@given("r2plan-a v3 Plan with resume metadata")
|
|
def step_plan_with_resume_metadata(context: Any) -> None:
|
|
context.r2_plan = _make_plan(
|
|
last_completed_step=5,
|
|
last_checkpoint_id="01CHKPTRESUME000000000000",
|
|
)
|
|
|
|
|
|
@given("r2plan-a v3 Plan with project link alias and read_only")
|
|
def step_plan_with_link_alias_readonly(context: Any) -> None:
|
|
link = ProjectLink(project_name="local/ref-data", alias="data", read_only=True)
|
|
context.r2_plan = _make_plan(project_links=[link])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("r2plan-I call _plan_spec_dict")
|
|
def step_call_spec_dict(context: Any) -> None:
|
|
context.r2_spec = _plan_spec_dict(context.r2_plan)
|
|
|
|
|
|
@when("r2plan-I call _print_lifecycle_plan")
|
|
def step_call_print_plan(context: Any) -> None:
|
|
context.r2_output = _capture_print(context.r2_plan)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then steps - spec dict assertions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then('r2plan-the spec dict project_links should include alias "{alias}"')
|
|
def step_spec_alias(context: Any, alias: str) -> None:
|
|
links = context.r2_spec["project_links"]
|
|
assert any(link.get("alias") == alias for link in links), (
|
|
f"No link with alias '{alias}' in {links}"
|
|
)
|
|
|
|
|
|
@then("r2plan-the spec dict project_links should include read_only true")
|
|
def step_spec_readonly(context: Any) -> None:
|
|
links = context.r2_spec["project_links"]
|
|
assert any(link.get("read_only") is True for link in links), (
|
|
f"No link with read_only=True in {links}"
|
|
)
|
|
|
|
|
|
@then("r2plan-the spec dict project_links should not include alias")
|
|
def step_spec_no_alias(context: Any) -> None:
|
|
links = context.r2_spec["project_links"]
|
|
assert all("alias" not in link for link in links), f"Unexpected alias in {links}"
|
|
|
|
|
|
@then("r2plan-the spec dict project_links should not include read_only")
|
|
def step_spec_no_readonly(context: Any) -> None:
|
|
links = context.r2_spec["project_links"]
|
|
assert all("read_only" not in link for link in links), (
|
|
f"Unexpected read_only in {links}"
|
|
)
|
|
|
|
|
|
@then('r2plan-the spec dict automation_profile should be "{profile}"')
|
|
def step_spec_profile(context: Any, profile: str) -> None:
|
|
assert context.r2_spec["automation_profile"] == profile, (
|
|
f"Expected '{profile}', got '{context.r2_spec['automation_profile']}'"
|
|
)
|
|
|
|
|
|
@then("r2plan-the spec dict automation_profile should be null")
|
|
def step_spec_profile_null(context: Any) -> None:
|
|
assert context.r2_spec["automation_profile"] is None
|
|
|
|
|
|
@then('r2plan-the spec dict should contain key "{key}"')
|
|
def step_spec_has_key(context: Any, key: str) -> None:
|
|
assert key in context.r2_spec, (
|
|
f"Key '{key}' not in spec dict: {list(context.r2_spec.keys())}"
|
|
)
|
|
|
|
|
|
@then('r2plan-the spec dict should not contain key "{key}"')
|
|
def step_spec_no_key(context: Any, key: str) -> None:
|
|
assert key not in context.r2_spec, f"Key '{key}' should not be in spec dict"
|
|
|
|
|
|
@then("r2plan-the spec dict invariants count should be {n:d}")
|
|
def step_spec_inv_count(context: Any, n: int) -> None:
|
|
actual = len(context.r2_spec["invariants"])
|
|
assert actual == n, f"Expected {n} invariants, got {actual}"
|
|
|
|
|
|
@then("r2plan-the spec dict dod_evaluation all_passed should be true")
|
|
def step_spec_dod_passed(context: Any) -> None:
|
|
assert context.r2_spec["dod_evaluation"]["all_passed"] is True
|
|
|
|
|
|
@then("r2plan-the spec dict last_completed_step should be {n:d}")
|
|
def step_spec_step(context: Any, n: int) -> None:
|
|
assert context.r2_spec["last_completed_step"] == n
|
|
|
|
|
|
# -- print output assertions --
|
|
|
|
|
|
@then('r2plan-the printed output should contain "{text}"')
|
|
def step_output_contains(context: Any, text: str) -> None:
|
|
assert text in context.r2_output, (
|
|
f"Expected '{text}' in output:\n{context.r2_output}"
|
|
)
|
|
|
|
|
|
@then('r2plan-the printed output should not contain "{text}"')
|
|
def step_output_not_contains(context: Any, text: str) -> None:
|
|
assert text not in context.r2_output, (
|
|
f"Did not expect '{text}' in output:\n{context.r2_output}"
|
|
)
|