f74393bcd5
Ruff format was not applied to 2 files added by the checkpoint CLI implementation commit. This ensures CI format check passes. ISSUES CLOSED: #8559
261 lines
10 KiB
Python
261 lines
10 KiB
Python
"""Step definitions for checkpoint_cli_commands.feature."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
from typer.testing import CliRunner
|
|
|
|
from cleveragents.cli.commands.plan import app as plan_app
|
|
from cleveragents.core.exceptions import CleverAgentsError, ResourceNotFoundError
|
|
from cleveragents.domain.models.core.checkpoint import Checkpoint, CheckpointMetadata
|
|
|
|
_PLAN_ID = "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
_CP_ID_1 = "01ARZ3NDEKTSV4RRFFQ69G5FB1"
|
|
_CP_ID_2 = "01ARZ3NDEKTSV4RRFFQ69G5FB2"
|
|
_CP_ID_3 = "01ARZ3NDEKTSV4RRFFQ69G5FB3"
|
|
|
|
_PATCH_CONTAINER = "cleveragents.application.container.get_container"
|
|
|
|
|
|
def _make_checkpoint(
|
|
checkpoint_id: str = _CP_ID_1,
|
|
plan_id: str = _PLAN_ID,
|
|
checkpoint_type: str = "manual",
|
|
reason: str = "test checkpoint",
|
|
phase: str = "execute",
|
|
created_at: datetime | None = None,
|
|
) -> Checkpoint:
|
|
"""Build a Checkpoint domain object for testing."""
|
|
return Checkpoint(
|
|
checkpoint_id=checkpoint_id,
|
|
plan_id=plan_id,
|
|
sandbox_ref="abc123",
|
|
checkpoint_type=checkpoint_type,
|
|
created_at=created_at or datetime(2026, 4, 1, 10, 0, 0, tzinfo=UTC),
|
|
metadata=CheckpointMetadata(
|
|
reason=reason,
|
|
source_tool="test",
|
|
phase=phase,
|
|
),
|
|
)
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Given steps
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
@given('a cp-cli mocked checkpoint service with no checkpoints for plan "{plan_id}"')
|
|
def step_cp_cli_no_checkpoints(context: Context, plan_id: str) -> None:
|
|
"""Set up a checkpoint service that returns no checkpoints."""
|
|
context.cp_cli_runner = CliRunner()
|
|
mock_container = MagicMock()
|
|
mock_svc = MagicMock()
|
|
mock_svc.list_checkpoints.return_value = []
|
|
mock_container.checkpoint_service.return_value = mock_svc
|
|
context.cp_cli_mock_container = mock_container
|
|
|
|
|
|
@given(
|
|
'a cp-cli mocked checkpoint service with {count:d} checkpoints for plan "{plan_id}"'
|
|
)
|
|
def step_cp_cli_n_checkpoints(context: Context, count: int, plan_id: str) -> None:
|
|
"""Set up a checkpoint service that returns N checkpoints."""
|
|
context.cp_cli_runner = CliRunner()
|
|
checkpoints = [
|
|
_make_checkpoint(
|
|
checkpoint_id=f"01ARZ3NDEKTSV4RRFFQ69G5F{i:02d}",
|
|
plan_id=plan_id,
|
|
created_at=datetime(2026, 4, 1, 10, i, 0, tzinfo=UTC),
|
|
)
|
|
for i in range(1, count + 1)
|
|
]
|
|
mock_container = MagicMock()
|
|
mock_svc = MagicMock()
|
|
mock_svc.list_checkpoints.return_value = checkpoints
|
|
mock_container.checkpoint_service.return_value = mock_svc
|
|
context.cp_cli_mock_container = mock_container
|
|
|
|
|
|
@given(
|
|
'a cp-cli mocked checkpoint service with checkpoints of mixed types for plan "{plan_id}"'
|
|
)
|
|
def step_cp_cli_mixed_type_checkpoints(context: Context, plan_id: str) -> None:
|
|
"""Set up a checkpoint service with checkpoints of different types."""
|
|
context.cp_cli_runner = CliRunner()
|
|
checkpoints = [
|
|
_make_checkpoint(
|
|
checkpoint_id=_CP_ID_1, plan_id=plan_id, checkpoint_type="manual"
|
|
),
|
|
_make_checkpoint(
|
|
checkpoint_id=_CP_ID_2, plan_id=plan_id, checkpoint_type="pre_write"
|
|
),
|
|
_make_checkpoint(
|
|
checkpoint_id=_CP_ID_3, plan_id=plan_id, checkpoint_type="post_step"
|
|
),
|
|
]
|
|
mock_container = MagicMock()
|
|
mock_svc = MagicMock()
|
|
mock_svc.list_checkpoints.return_value = checkpoints
|
|
mock_container.checkpoint_service.return_value = mock_svc
|
|
context.cp_cli_mock_container = mock_container
|
|
|
|
|
|
@given("a cp-cli mocked checkpoint service that raises CleverAgentsError on list")
|
|
def step_cp_cli_list_error(context: Context) -> None:
|
|
"""Set up a checkpoint service that raises CleverAgentsError on list."""
|
|
context.cp_cli_runner = CliRunner()
|
|
mock_container = MagicMock()
|
|
mock_svc = MagicMock()
|
|
mock_svc.list_checkpoints.side_effect = CleverAgentsError("service error")
|
|
mock_container.checkpoint_service.return_value = mock_svc
|
|
context.cp_cli_mock_container = mock_container
|
|
|
|
|
|
@given("a cp-cli mocked checkpoint service that can delete checkpoints")
|
|
def step_cp_cli_delete_ok(context: Context) -> None:
|
|
"""Set up a checkpoint service that successfully deletes checkpoints."""
|
|
context.cp_cli_runner = CliRunner()
|
|
mock_container = MagicMock()
|
|
mock_svc = MagicMock()
|
|
mock_svc.delete_checkpoint.return_value = None
|
|
mock_container.checkpoint_service.return_value = mock_svc
|
|
context.cp_cli_mock_container = mock_container
|
|
|
|
|
|
@given("a cp-cli mocked checkpoint service that raises ResourceNotFoundError on delete")
|
|
def step_cp_cli_delete_not_found(context: Context) -> None:
|
|
"""Set up a checkpoint service that raises ResourceNotFoundError on delete."""
|
|
context.cp_cli_runner = CliRunner()
|
|
mock_container = MagicMock()
|
|
mock_svc = MagicMock()
|
|
mock_svc.delete_checkpoint.side_effect = ResourceNotFoundError(
|
|
resource_type="checkpoint", resource_id=_CP_ID_1
|
|
)
|
|
mock_container.checkpoint_service.return_value = mock_svc
|
|
context.cp_cli_mock_container = mock_container
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
# When steps
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
@when('I invoke cp-cli checkpoint-list for plan "{plan_id}" in default format')
|
|
def step_cp_cli_invoke_list(context: Context, plan_id: str) -> None:
|
|
"""Invoke checkpoint-list command in default rich format."""
|
|
with patch(_PATCH_CONTAINER, return_value=context.cp_cli_mock_container):
|
|
context.cp_cli_result = context.cp_cli_runner.invoke(
|
|
plan_app, ["checkpoint-list", plan_id]
|
|
)
|
|
|
|
|
|
@when('I invoke cp-cli checkpoint-list for plan "{plan_id}" with format "{fmt}"')
|
|
def step_cp_cli_invoke_list_fmt(context: Context, plan_id: str, fmt: str) -> None:
|
|
"""Invoke checkpoint-list command with a specific output format."""
|
|
with patch(_PATCH_CONTAINER, return_value=context.cp_cli_mock_container):
|
|
context.cp_cli_result = context.cp_cli_runner.invoke(
|
|
plan_app, ["checkpoint-list", plan_id, "--format", fmt]
|
|
)
|
|
|
|
|
|
@when('I invoke cp-cli checkpoint-list for plan "{plan_id}" with sort "{sort}"')
|
|
def step_cp_cli_invoke_list_sort(context: Context, plan_id: str, sort: str) -> None:
|
|
"""Invoke checkpoint-list command with a sort option."""
|
|
with patch(_PATCH_CONTAINER, return_value=context.cp_cli_mock_container):
|
|
context.cp_cli_result = context.cp_cli_runner.invoke(
|
|
plan_app, ["checkpoint-list", plan_id, "--sort", sort]
|
|
)
|
|
|
|
|
|
@when(
|
|
'I invoke cp-cli checkpoint-list for plan "{plan_id}" with type filter "{cp_type}"'
|
|
)
|
|
def step_cp_cli_invoke_list_type(context: Context, plan_id: str, cp_type: str) -> None:
|
|
"""Invoke checkpoint-list command with a type filter."""
|
|
with patch(_PATCH_CONTAINER, return_value=context.cp_cli_mock_container):
|
|
context.cp_cli_result = context.cp_cli_runner.invoke(
|
|
plan_app, ["checkpoint-list", plan_id, "--type", cp_type]
|
|
)
|
|
|
|
|
|
@when('I invoke cp-cli checkpoint-delete "{cp_id}" with yes flag')
|
|
def step_cp_cli_invoke_delete_yes(context: Context, cp_id: str) -> None:
|
|
"""Invoke checkpoint-delete command with --yes flag."""
|
|
with patch(_PATCH_CONTAINER, return_value=context.cp_cli_mock_container):
|
|
context.cp_cli_result = context.cp_cli_runner.invoke(
|
|
plan_app, ["checkpoint-delete", cp_id, "--yes"]
|
|
)
|
|
|
|
|
|
@when('I invoke cp-cli checkpoint-delete "{cp_id}" with yes flag and format "{fmt}"')
|
|
def step_cp_cli_invoke_delete_yes_fmt(context: Context, cp_id: str, fmt: str) -> None:
|
|
"""Invoke checkpoint-delete command with --yes flag and format."""
|
|
with patch(_PATCH_CONTAINER, return_value=context.cp_cli_mock_container):
|
|
context.cp_cli_result = context.cp_cli_runner.invoke(
|
|
plan_app, ["checkpoint-delete", cp_id, "--yes", "--format", fmt]
|
|
)
|
|
|
|
|
|
@when("I invoke cp-cli checkpoint-delete multiple IDs with yes flag")
|
|
def step_cp_cli_invoke_delete_multiple(context: Context) -> None:
|
|
"""Invoke checkpoint-delete command with multiple checkpoint IDs."""
|
|
with patch(_PATCH_CONTAINER, return_value=context.cp_cli_mock_container):
|
|
context.cp_cli_result = context.cp_cli_runner.invoke(
|
|
plan_app, ["checkpoint-delete", _CP_ID_1, _CP_ID_2, "--yes"]
|
|
)
|
|
|
|
|
|
@when("I invoke cp-cli checkpoint-delete with no IDs")
|
|
def step_cp_cli_invoke_delete_no_ids(context: Context) -> None:
|
|
"""Invoke checkpoint-delete command with no checkpoint IDs."""
|
|
with patch(_PATCH_CONTAINER, return_value=context.cp_cli_mock_container):
|
|
context.cp_cli_result = context.cp_cli_runner.invoke(
|
|
plan_app, ["checkpoint-delete"]
|
|
)
|
|
|
|
|
|
@when('I invoke cp-cli checkpoint-delete "{cp_id}" and decline confirmation')
|
|
def step_cp_cli_invoke_delete_decline(context: Context, cp_id: str) -> None:
|
|
"""Invoke checkpoint-delete command and decline the confirmation prompt."""
|
|
with patch(_PATCH_CONTAINER, return_value=context.cp_cli_mock_container):
|
|
context.cp_cli_result = context.cp_cli_runner.invoke(
|
|
plan_app, ["checkpoint-delete", cp_id], input="n\n"
|
|
)
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Then steps
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
@then("the cp-cli command should succeed")
|
|
def step_cp_cli_assert_success(context: Context) -> None:
|
|
"""Assert the CLI exited with code 0."""
|
|
assert context.cp_cli_result.exit_code == 0, (
|
|
f"Expected exit_code=0, got {context.cp_cli_result.exit_code}. "
|
|
f"Output: {context.cp_cli_result.output}"
|
|
)
|
|
|
|
|
|
@then("the cp-cli command should abort")
|
|
def step_cp_cli_assert_abort(context: Context) -> None:
|
|
"""Assert the CLI exited with a non-zero code."""
|
|
assert context.cp_cli_result.exit_code != 0, (
|
|
f"Expected non-zero exit_code, got {context.cp_cli_result.exit_code}. "
|
|
f"Output: {context.cp_cli_result.output}"
|
|
)
|
|
|
|
|
|
@then('the cp-cli output should contain "{text}"')
|
|
def step_cp_cli_assert_output_contains(context: Context, text: str) -> None:
|
|
"""Assert the CLI output contains the expected text."""
|
|
assert text in context.cp_cli_result.output, (
|
|
f"Expected output to contain {text!r}. "
|
|
f"Actual output: {context.cp_cli_result.output}"
|
|
)
|