forked from HAL9000/cleveragents-core
a808c395f9
Add 53 new .feature files and corresponding step definition files targeting uncovered lines identified in build/coverage.xml. Fix AmbiguousStep conflicts in 7 pre-existing step files by disambiguating step text. New tests cover: ACP clients/facade, actor CLI/config, application container, ACMS service/strategies, async worker, automation profile CLI, autonomy guardrail, bridge, change model, config CLI/service, context service, cross-plan correction, database models, decision service, decomposition clustering/service, discovery handler, langchain chat provider, langgraph nodes, materializers, multi-project service, plan apply/CLI/lifecycle/model/ preflight/resume/service, PostgreSQL analyzer, project CLI/context CLI, provider registry, reactive application/route, repositories, resolver handler, resource registry service, resume model, retry patterns, sandbox protocol, server CLI, skill CLI/service, skills registry, subplan execution/service, system CLI, UKO loader, UoW, and YAML template engine. Closes #645
235 lines
8.0 KiB
Python
235 lines
8.0 KiB
Python
"""Step definitions for Resume model coverage boost tests.
|
|
|
|
Targets uncovered lines in resume.py:
|
|
- Line 150: ValueError in record_checkpoint for negative step_index
|
|
- Lines 219-233: ResumeSummary.as_cli_dict method
|
|
"""
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
from ulid import ULID
|
|
|
|
from cleveragents.domain.models.core.resume import (
|
|
ResumeCheckpoint,
|
|
ResumeMetadata,
|
|
ResumeSummary,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_VALID_ULID = str(ULID())
|
|
_PLAN_ULID = str(ULID())
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Given steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("I have a ResumeMetadata instance with {total:d} total steps")
|
|
def step_create_resume_metadata(context: Context, total: int) -> None:
|
|
"""Create a ResumeMetadata for checkpoint recording tests."""
|
|
context.resume_metadata = ResumeMetadata(total_steps=total)
|
|
context.error = None
|
|
|
|
|
|
@given("I have a ResumeSummary with no optional fields")
|
|
def step_summary_no_optionals(context: Context) -> None:
|
|
"""Create a ResumeSummary with all optional fields as None."""
|
|
context.summary = ResumeSummary(
|
|
plan_id=_VALID_ULID,
|
|
phase="execute",
|
|
processing_state="processing",
|
|
last_completed_step=0,
|
|
next_step_index=1,
|
|
total_steps=5,
|
|
decision_id=None,
|
|
last_checkpoint_id=None,
|
|
sandbox_ref=None,
|
|
)
|
|
context.cli_dict = None
|
|
|
|
|
|
@given('I have a ResumeSummary with decision_id "{decision_id}"')
|
|
def step_summary_with_decision(context: Context, decision_id: str) -> None:
|
|
"""Create a ResumeSummary with only decision_id set."""
|
|
context.summary = ResumeSummary(
|
|
plan_id=_VALID_ULID,
|
|
phase="execute",
|
|
processing_state="processing",
|
|
last_completed_step=0,
|
|
next_step_index=1,
|
|
total_steps=5,
|
|
decision_id=decision_id,
|
|
)
|
|
context.cli_dict = None
|
|
|
|
|
|
@given('I have a ResumeSummary with last_checkpoint_id "{chk_id}"')
|
|
def step_summary_with_checkpoint(context: Context, chk_id: str) -> None:
|
|
"""Create a ResumeSummary with only last_checkpoint_id set."""
|
|
context.summary = ResumeSummary(
|
|
plan_id=_VALID_ULID,
|
|
phase="execute",
|
|
processing_state="processing",
|
|
last_completed_step=1,
|
|
next_step_index=2,
|
|
total_steps=5,
|
|
last_checkpoint_id=chk_id,
|
|
)
|
|
context.cli_dict = None
|
|
|
|
|
|
@given('I have a ResumeSummary with sandbox_ref "{sandbox_ref}"')
|
|
def step_summary_with_sandbox(context: Context, sandbox_ref: str) -> None:
|
|
"""Create a ResumeSummary with only sandbox_ref set."""
|
|
context.summary = ResumeSummary(
|
|
plan_id=_VALID_ULID,
|
|
phase="execute",
|
|
processing_state="processing",
|
|
last_completed_step=1,
|
|
next_step_index=2,
|
|
total_steps=5,
|
|
sandbox_ref=sandbox_ref,
|
|
)
|
|
context.cli_dict = None
|
|
|
|
|
|
@given("I have a ResumeSummary with all optional fields set")
|
|
def step_summary_all_optionals(context: Context) -> None:
|
|
"""Create a ResumeSummary with every optional field populated."""
|
|
context.summary = ResumeSummary(
|
|
plan_id=_VALID_ULID,
|
|
phase="execute",
|
|
processing_state="processing",
|
|
last_completed_step=2,
|
|
next_step_index=3,
|
|
total_steps=5,
|
|
decision_id="DEC_ALL",
|
|
last_checkpoint_id=str(ULID()),
|
|
sandbox_ref="/tmp/sandbox-all",
|
|
)
|
|
context.cli_dict = None
|
|
|
|
|
|
@given(
|
|
'I have a ResumeSummary with plan_id "{plan_id}" and phase "{phase}" '
|
|
'and state "{state}" and last_step {last:d} and next_step {nxt:d} '
|
|
"and total {total:d}"
|
|
)
|
|
def step_summary_with_specific_values(
|
|
context: Context,
|
|
plan_id: str,
|
|
phase: str,
|
|
state: str,
|
|
last: int,
|
|
nxt: int,
|
|
total: int,
|
|
) -> None:
|
|
"""Create a ResumeSummary with explicit base field values."""
|
|
context.summary = ResumeSummary(
|
|
plan_id=plan_id,
|
|
phase=phase,
|
|
processing_state=state,
|
|
last_completed_step=last,
|
|
next_step_index=nxt,
|
|
total_steps=total,
|
|
)
|
|
context.cli_dict = None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I record a checkpoint with step_index {index:d}")
|
|
def step_record_bad_checkpoint(context: Context, index: int) -> None:
|
|
"""Attempt to record a checkpoint (may raise ValueError for negative index)."""
|
|
try:
|
|
checkpoint = ResumeCheckpoint(
|
|
checkpoint_id=str(ULID()),
|
|
plan_id=_PLAN_ULID,
|
|
decision_id="DEC_NEG",
|
|
step_index=max(index, 0), # Pydantic ge=0 would reject negative
|
|
step_text="Negative step",
|
|
)
|
|
# Manually set to negative to bypass Pydantic field validation
|
|
# and hit the domain guard inside record_checkpoint
|
|
object.__setattr__(checkpoint, "step_index", index)
|
|
context.resume_metadata.record_checkpoint(checkpoint)
|
|
except ValueError as exc:
|
|
context.error = exc
|
|
|
|
|
|
@when("I call as_cli_dict on the resume summary")
|
|
def step_call_as_cli_dict(context: Context) -> None:
|
|
"""Call as_cli_dict on the stored ResumeSummary."""
|
|
context.cli_dict = context.summary.as_cli_dict()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then('a ValueError should be raised with message "{msg}"')
|
|
def step_assert_value_error(context: Context, msg: str) -> None:
|
|
"""Assert a ValueError was raised with the expected message."""
|
|
assert context.error is not None, "Expected a ValueError but none was raised"
|
|
assert isinstance(context.error, ValueError), (
|
|
f"Expected ValueError, got {type(context.error).__name__}"
|
|
)
|
|
assert msg in str(context.error), f"Expected message '{msg}' in '{context.error}'"
|
|
|
|
|
|
@then('the cli dict should have key "{key}"')
|
|
def step_assert_dict_has_key(context: Context, key: str) -> None:
|
|
"""Assert the cli dict contains the given key."""
|
|
assert context.cli_dict is not None, "cli_dict is None; was as_cli_dict called?"
|
|
assert key in context.cli_dict, (
|
|
f"Key '{key}' not found in cli_dict: {list(context.cli_dict.keys())}"
|
|
)
|
|
|
|
|
|
@then('the cli dict should lack key "{key}"')
|
|
def step_assert_dict_missing_key(context: Context, key: str) -> None:
|
|
"""Assert the cli dict does NOT contain the given key."""
|
|
assert context.cli_dict is not None, "cli_dict is None; was as_cli_dict called?"
|
|
assert key not in context.cli_dict, (
|
|
f"Key '{key}' should not be in cli_dict but was found with value "
|
|
f"'{context.cli_dict[key]}'"
|
|
)
|
|
|
|
|
|
@then('the cli dict value for "{key}" should be "{value}"')
|
|
def step_assert_dict_key_value(context: Context, key: str, value: str) -> None:
|
|
"""Assert the cli dict contains the key with the expected string value."""
|
|
assert context.cli_dict is not None
|
|
assert key in context.cli_dict, f"Key '{key}' not in cli_dict"
|
|
assert str(context.cli_dict[key]) == value, (
|
|
f"Expected '{value}', got '{context.cli_dict[key]}'"
|
|
)
|
|
|
|
|
|
@then("the cli dict should have exactly {count:d} keys")
|
|
def step_assert_dict_key_count(context: Context, count: int) -> None:
|
|
"""Assert the cli dict has exactly N keys."""
|
|
assert context.cli_dict is not None
|
|
actual = len(context.cli_dict)
|
|
assert actual == count, (
|
|
f"Expected {count} keys, got {actual}: {list(context.cli_dict.keys())}"
|
|
)
|
|
|
|
|
|
@then('the cli dict integer for "{key}" should be {value:d}')
|
|
def step_assert_dict_key_equals_int(context: Context, key: str, value: int) -> None:
|
|
"""Assert a cli dict key equals an integer value."""
|
|
assert context.cli_dict is not None
|
|
assert key in context.cli_dict, f"Key '{key}' not in cli_dict"
|
|
assert context.cli_dict[key] == value, (
|
|
f"Expected {value}, got {context.cli_dict[key]}"
|
|
)
|