bug(cli): plan apply --format json returns raw plan dict instead of spec-required JSON envelope
The `agents plan apply --format json` command was returning a raw plan dictionary instead of the spec-required JSON envelope. This fix introduces a dedicated `_apply_output_dict()` helper that wraps the non-rich format output in the proper envelope structure with `command`, `status`, `exit_code`, `data`, `timing`, and `messages` fields. The `data` field contains structured information about artifacts, changes, project, applied_at, validation (test/lint/type_check), sandbox_cleanup, and lifecycle metrics. Other commands (plan status, plan cancel, plan use) remain unaffected — they continue using `_plan_spec_dict`. Tests: 16 Behave scenarios + 15 Robot Framework integration tests added covering envelope structure, field presence, sandbox cleanup state derivation from actual plan state, legacy fallback, cost metadata, and command isolation. ISSUES CLOSED: #9449
This commit is contained in:
@@ -195,6 +195,19 @@ ensuring data is stored with proper parameter values.
|
||||
impossible. The handler now includes the error message text and full
|
||||
traceback in the structlog warning entry. Removed `@tdd_expected_fail` tag
|
||||
from the TDD test so both scenarios run as normal regression guards. (#988)
|
||||
- **`agents plan apply --format json` now returns spec-required JSON envelope** (#9449):
|
||||
Fixed `lifecycle_apply_plan` in `src/cleveragents/cli/commands/plan.py` to wrap
|
||||
non-rich format output in the spec-required JSON envelope instead of a raw plan
|
||||
dictionary. The envelope includes `command: "plan apply"`, `status: "ok"`,
|
||||
`exit_code: 0`, and `timing` fields. The `data` field contains `artifacts` (count
|
||||
of sandbox refs), `changes` (insertions/deletions from git diff), `project` (first
|
||||
project link), `applied_at` (ISO timestamp), `validation` (test/lint/type_check
|
||||
results with duration), `sandbox_cleanup` (worktree/branch/checkpoint status derived
|
||||
from actual plan terminal state), and `lifecycle` (phase, state, total_duration,
|
||||
total_cost, decisions_made, child_plans). The `Plan as LifecyclePlan` import was moved
|
||||
to the module top-level. The new `_apply_output_dict` helper is scoped exclusively to
|
||||
`plan apply` call sites; all other commands (`plan status`, `plan cancel`, `plan
|
||||
revert`, `plan use`) continue to use `_plan_spec_dict` unchanged.
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ Below are some of the specific details of various contributions.
|
||||
* HAL 9000 has contributed the AutoDebug node state mutation fix (#10496): fixed _analyze_error, _generate_fix, and _validate_fix in src/cleveragents/agents/graphs/auto_debug.py to return partial update dicts per LangGraph s node contract, preventing duplicate state entries and checkpoint inconsistencies.
|
||||
* HAL 9000 has contributed the ContextStrategy protocol and plugin registration system (PR #11106 / issue #8616): implemented the domain-model `ContextStrategy` Protocol with BackendSet, PlanContext, StrategyCapabilities, StrategyConfig, ContextStrategyResult, and StrategyRegistryEntry models. Six built-in strategies each implement real backend query logic respecting budget constraints. The StrategyRegistry service provides thread-safe registration/unregistration, Pydantic-validated config updates with immutable MappingProxyType fields, plugin discovery via register_from_module() with CWE-706 module-prefix allowlist guard, enabled list management, deterministic fragment ordering, and validation warnings. Comprehensive BDD test coverage in features/context_strategies.feature and features/context_strategy_registry.feature (120+ scenarios) (#8616).
|
||||
* HAL 9000 has contributed the A2A stdio transport for local mode (#691): implemented ``A2aStdioTransport`` class with JSON-RPC 2.0 message framing over stdin/stdout for subprocess communication, including process lifecycle management (connect/disconnect), request/response serialization, graceful shutdown with timeout-based termination, and type-safe path resolution for Python modules vs. executable scripts. Added full BDD test coverage (18 scenarios) in ``features/a2a_stdio_transport.feature`` with mock subprocess behavior in step definitions.
|
||||
* HAL 9000 has contributed the `plan apply --format json` spec-compliant envelope fix (PR #9817 / issue #9449): replaced raw plan dictionary output with a spec-required JSON envelope containing structured data fields for artifacts, changes, validation, sandbox cleanup, and lifecycle metrics across all output formats. Full BDD test suite in behave + Robot Framework integration tests added.
|
||||
|
||||
# Details (PR Contributions)
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
Feature: plan apply --format json returns spec-required JSON envelope
|
||||
As a programmatic consumer of the CleverAgents CLI
|
||||
I want ``agents plan apply --format json`` to return a spec-compliant JSON envelope
|
||||
So that tooling and scripts can reliably parse the apply output
|
||||
|
||||
Background:
|
||||
Given a plan CLI runner for the apply JSON envelope test
|
||||
|
||||
# ── Envelope field presence ──────────────────────────────────────────────
|
||||
|
||||
Scenario: apply JSON output includes all required top-level envelope fields
|
||||
When I invoke plan apply with --format json and a mocked service
|
||||
Then the apply JSON envelope should contain field "command"
|
||||
And the apply JSON envelope should contain field "status"
|
||||
And the apply JSON envelope should contain field "exit_code"
|
||||
And the apply JSON envelope should contain field "data"
|
||||
And the apply JSON envelope should contain field "timing"
|
||||
And the apply JSON envelope should contain field "messages"
|
||||
|
||||
Scenario: apply JSON envelope command field is "plan apply"
|
||||
When I invoke plan apply with --format json and a mocked service
|
||||
Then the apply JSON envelope command should be "plan apply"
|
||||
|
||||
Scenario: apply JSON envelope status field is "ok"
|
||||
When I invoke plan apply with --format json and a mocked service
|
||||
Then the apply JSON envelope status should be "ok"
|
||||
|
||||
Scenario: apply JSON envelope exit_code field is 0
|
||||
When I invoke plan apply with --format json and a mocked service
|
||||
Then the apply JSON envelope exit_code should be 0
|
||||
|
||||
Scenario: apply JSON envelope messages is a non-empty list
|
||||
When I invoke plan apply with --format json and a mocked service
|
||||
Then the apply JSON envelope messages should be a non-empty list
|
||||
|
||||
# ── Data field structure ─────────────────────────────────────────────────
|
||||
|
||||
Scenario: apply JSON envelope data field contains required sub-fields
|
||||
When I invoke plan apply with --format json and a mocked service
|
||||
Then the apply JSON data should contain field "artifacts"
|
||||
And the apply JSON data should contain field "changes"
|
||||
And the apply JSON data should contain field "project"
|
||||
And the apply JSON data should contain field "applied_at"
|
||||
And the apply JSON data should contain field "validation"
|
||||
And the apply JSON data should contain field "sandbox_cleanup"
|
||||
And the apply JSON data should contain field "lifecycle"
|
||||
|
||||
Scenario: apply JSON data validation field contains test lint and type_check
|
||||
When I invoke plan apply with --format json and a mocked service
|
||||
Then the apply JSON data validation should contain field "test"
|
||||
And the apply JSON data validation should contain field "lint"
|
||||
And the apply JSON data validation should contain field "type_check"
|
||||
|
||||
Scenario: apply JSON data lifecycle field contains required sub-fields
|
||||
When I invoke plan apply with --format json and a mocked service
|
||||
Then the apply JSON data lifecycle should contain field "phase"
|
||||
And the apply JSON data lifecycle should contain field "state"
|
||||
And the apply JSON data lifecycle should contain field "total_duration"
|
||||
And the apply JSON data lifecycle should contain field "total_cost"
|
||||
And the apply JSON data lifecycle should contain field "decisions_made"
|
||||
And the apply JSON data lifecycle should contain field "child_plans"
|
||||
|
||||
Scenario: apply JSON data sandbox_cleanup field contains worktree branch checkpoint
|
||||
When I invoke plan apply with --format json and a mocked service
|
||||
Then the apply JSON data sandbox_cleanup should contain field "worktree"
|
||||
And the apply JSON data sandbox_cleanup should contain field "branch"
|
||||
And the apply JSON data sandbox_cleanup should contain field "checkpoint"
|
||||
|
||||
# ── sandbox_cleanup derives from actual plan state ───────────────────────
|
||||
|
||||
Scenario: sandbox_cleanup shows removed state for terminal apply plan
|
||||
When I invoke plan apply with --format json and a terminal apply plan
|
||||
Then the apply JSON data sandbox_cleanup worktree should be "removed"
|
||||
And the apply JSON data sandbox_cleanup branch should be "merged to main"
|
||||
And the apply JSON data sandbox_cleanup checkpoint should be "archived"
|
||||
|
||||
Scenario: sandbox_cleanup shows active state for non-terminal plan
|
||||
When I invoke plan apply with --format json and a non-terminal plan
|
||||
Then the apply JSON data sandbox_cleanup worktree should be "active"
|
||||
And the apply JSON data sandbox_cleanup branch should be "open"
|
||||
And the apply JSON data sandbox_cleanup checkpoint should be "pending"
|
||||
|
||||
# ── Command isolation ────────────────────────────────────────────────────
|
||||
|
||||
Scenario: plan status --format json does not emit plan apply command field
|
||||
When I invoke plan status with --format json and a mocked service
|
||||
Then the plan status JSON output should not have command "plan apply"
|
||||
|
||||
Scenario: plan cancel --format json does not emit plan apply command field
|
||||
When I invoke plan cancel with --format json and a mocked service
|
||||
Then the plan cancel JSON output should not have command "plan apply"
|
||||
|
||||
# ── Legacy plan fallback ─────────────────────────────────────────────────
|
||||
|
||||
Scenario: apply JSON envelope handles legacy plan object gracefully
|
||||
When I invoke the apply output dict with a legacy plan object
|
||||
Then the legacy apply JSON envelope command should be "plan apply"
|
||||
And the legacy apply JSON envelope data should contain field "plan"
|
||||
|
||||
# ── total_cost from cost_metadata ────────────────────────────────────────
|
||||
|
||||
Scenario: apply JSON data lifecycle total_cost reflects plan cost_metadata
|
||||
When I invoke plan apply with --format json and a plan with cost metadata
|
||||
Then the apply JSON data lifecycle total_cost should start with "$"
|
||||
@@ -0,0 +1,422 @@
|
||||
"""Step definitions for plan apply --format json JSON envelope tests.
|
||||
|
||||
These steps verify that ``agents plan apply --format json`` returns the
|
||||
spec-required JSON envelope with ``command``, ``status``, ``exit_code``,
|
||||
``data``, ``timing``, and ``messages`` fields, and that the ``data`` field
|
||||
contains the required sub-fields for artifacts, changes, project, applied_at,
|
||||
validation, sandbox_cleanup, and lifecycle.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
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 _apply_output_dict
|
||||
from cleveragents.cli.commands.plan import app as plan_app
|
||||
from cleveragents.domain.models.core.cost_metadata import CostMetadata
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
NamespacedName,
|
||||
Plan as LifecyclePlan,
|
||||
PlanIdentity,
|
||||
PlanPhase,
|
||||
PlanTimestamps,
|
||||
ProcessingState,
|
||||
ProjectLink,
|
||||
)
|
||||
|
||||
_PATCH_LIFECYCLE = "cleveragents.cli.commands.plan._get_lifecycle_service"
|
||||
_PATCH_SANDBOX = "cleveragents.cli.commands.plan._apply_sandbox_changes"
|
||||
|
||||
_PLAN_ID = "01JAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
|
||||
|
||||
def _make_plan(
|
||||
phase: PlanPhase = PlanPhase.APPLY,
|
||||
state: ProcessingState = ProcessingState.APPLIED,
|
||||
cost_metadata: CostMetadata | None = None,
|
||||
) -> LifecyclePlan:
|
||||
"""Build a lightweight Plan for apply JSON envelope tests."""
|
||||
now = datetime.now(tz=UTC)
|
||||
return LifecyclePlan(
|
||||
identity=PlanIdentity(plan_id=_PLAN_ID),
|
||||
namespaced_name=NamespacedName(
|
||||
server=None, namespace="local", name="test-plan"
|
||||
),
|
||||
action_name="local/test-action",
|
||||
description="Test plan for apply JSON envelope scenarios",
|
||||
definition_of_done="All tests pass",
|
||||
phase=phase,
|
||||
processing_state=state,
|
||||
project_links=[ProjectLink(project_name="local/my-project")],
|
||||
strategy_actor="openai/gpt-4",
|
||||
execution_actor="openai/gpt-4",
|
||||
created_by="test-user",
|
||||
reusable=True,
|
||||
read_only=False,
|
||||
timestamps=PlanTimestamps(
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
applied_at=now,
|
||||
),
|
||||
cost_metadata=cost_metadata,
|
||||
)
|
||||
|
||||
|
||||
def _build_mock_service(
|
||||
plan: LifecyclePlan | None = None,
|
||||
) -> MagicMock:
|
||||
"""Build a mock lifecycle service for apply JSON envelope tests."""
|
||||
if plan is None:
|
||||
plan = _make_plan()
|
||||
mock_service = MagicMock()
|
||||
mock_service.get_plan.return_value = plan
|
||||
mock_service.apply_plan.return_value = plan
|
||||
mock_service._complete_apply_if_queued.return_value = plan
|
||||
return mock_service
|
||||
|
||||
|
||||
def _invoke_apply_json(
|
||||
context: Context,
|
||||
plan: LifecyclePlan | None = None,
|
||||
) -> None:
|
||||
"""Invoke ``plan apply --yes --format json`` with a mocked service."""
|
||||
mock_service = _build_mock_service(plan)
|
||||
context.apply_json_mock_service = mock_service
|
||||
|
||||
with (
|
||||
patch(_PATCH_LIFECYCLE, return_value=mock_service),
|
||||
patch(_PATCH_SANDBOX, return_value=True),
|
||||
):
|
||||
result = context.apply_json_runner.invoke(
|
||||
plan_app,
|
||||
["apply", "--yes", "--format", "json", _PLAN_ID],
|
||||
)
|
||||
context.apply_json_result = result
|
||||
# Parse the JSON output — strip any Rich markup / ANSI codes.
|
||||
raw = result.output.strip()
|
||||
try:
|
||||
context.apply_json_envelope = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
context.apply_json_envelope = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GIVEN
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given("a plan CLI runner for the apply JSON envelope test")
|
||||
def step_given_runner(context: Context) -> None:
|
||||
"""Create a Typer CliRunner for the plan sub-app."""
|
||||
context.apply_json_runner = CliRunner()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WHEN
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when("I invoke plan apply with --format json and a mocked service")
|
||||
def step_when_invoke_apply_json(context: Context) -> None:
|
||||
"""Invoke ``plan apply --yes --format json`` with a standard mocked service."""
|
||||
_invoke_apply_json(context)
|
||||
|
||||
|
||||
@when("I invoke plan apply with --format json and a terminal apply plan")
|
||||
def step_when_invoke_apply_json_terminal(context: Context) -> None:
|
||||
"""Invoke apply with a plan in terminal Apply/applied state."""
|
||||
plan = _make_plan(phase=PlanPhase.APPLY, state=ProcessingState.APPLIED)
|
||||
_invoke_apply_json(context, plan)
|
||||
|
||||
|
||||
@when("I invoke plan apply with --format json and a non-terminal plan")
|
||||
def step_when_invoke_apply_json_non_terminal(context: Context) -> None:
|
||||
"""Invoke apply with a plan in non-terminal Apply/queued state."""
|
||||
plan = _make_plan(phase=PlanPhase.APPLY, state=ProcessingState.QUEUED)
|
||||
_invoke_apply_json(context, plan)
|
||||
|
||||
|
||||
@when("I invoke plan apply with --format json and a plan with cost metadata")
|
||||
def step_when_invoke_apply_json_cost(context: Context) -> None:
|
||||
"""Invoke apply with a plan that has cost_metadata set."""
|
||||
cost = CostMetadata(total_cost=1.23)
|
||||
plan = _make_plan(cost_metadata=cost)
|
||||
_invoke_apply_json(context, plan)
|
||||
|
||||
|
||||
@when("I invoke plan status with --format json and a mocked service")
|
||||
def step_when_invoke_status_json(context: Context) -> None:
|
||||
"""Invoke ``plan status --format json`` with a mocked service."""
|
||||
plan = _make_plan()
|
||||
mock_service = _build_mock_service(plan)
|
||||
mock_service.list_plans.return_value = [plan]
|
||||
|
||||
with patch(_PATCH_LIFECYCLE, return_value=mock_service):
|
||||
result = context.apply_json_runner.invoke(
|
||||
plan_app,
|
||||
["status", _PLAN_ID, "--format", "json"],
|
||||
)
|
||||
context.plan_status_json_result = result
|
||||
raw = result.output.strip()
|
||||
try:
|
||||
context.plan_status_json_envelope = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
context.plan_status_json_envelope = None
|
||||
|
||||
|
||||
@when("I invoke plan cancel with --format json and a mocked service")
|
||||
def step_when_invoke_cancel_json(context: Context) -> None:
|
||||
"""Invoke ``plan cancel --format json`` with a mocked service."""
|
||||
plan = _make_plan(phase=PlanPhase.APPLY, state=ProcessingState.CANCELLED)
|
||||
mock_service = _build_mock_service(plan)
|
||||
mock_service.cancel_plan.return_value = plan
|
||||
|
||||
with patch(_PATCH_LIFECYCLE, return_value=mock_service):
|
||||
result = context.apply_json_runner.invoke(
|
||||
plan_app,
|
||||
["cancel", _PLAN_ID, "--format", "json"],
|
||||
)
|
||||
context.plan_cancel_json_result = result
|
||||
raw = result.output.strip()
|
||||
try:
|
||||
context.plan_cancel_json_envelope = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
context.plan_cancel_json_envelope = None
|
||||
|
||||
|
||||
@when("I invoke the apply output dict with a legacy plan object")
|
||||
def step_when_invoke_apply_output_dict_legacy(context: Context) -> None:
|
||||
"""Call ``_apply_output_dict`` directly with a non-Plan object."""
|
||||
context.legacy_apply_envelope = _apply_output_dict("legacy-plan-string")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# THEN — envelope field presence
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then('the apply JSON envelope should contain field "{field}"')
|
||||
def step_then_envelope_has_field(context: Context, field: str) -> None:
|
||||
"""Assert the top-level envelope contains the given field."""
|
||||
envelope = context.apply_json_envelope
|
||||
assert envelope is not None, (
|
||||
f"Could not parse JSON output.\nRaw output:\n"
|
||||
f"{context.apply_json_result.output}"
|
||||
)
|
||||
assert field in envelope, (
|
||||
f"Expected envelope to contain field '{field}'.\n"
|
||||
f"Envelope keys: {list(envelope.keys())}"
|
||||
)
|
||||
|
||||
|
||||
@then('the apply JSON envelope command should be "{expected}"')
|
||||
def step_then_envelope_command(context: Context, expected: str) -> None:
|
||||
"""Assert the envelope ``command`` field equals the expected value."""
|
||||
envelope = context.apply_json_envelope
|
||||
assert envelope is not None, "Could not parse JSON output."
|
||||
assert (
|
||||
envelope.get("command") == expected
|
||||
), f"Expected command '{expected}', got '{envelope.get('command')}'."
|
||||
|
||||
|
||||
@then('the apply JSON envelope status should be "{expected}"')
|
||||
def step_then_envelope_status(context: Context, expected: str) -> None:
|
||||
"""Assert the envelope ``status`` field equals the expected value."""
|
||||
envelope = context.apply_json_envelope
|
||||
assert envelope is not None, "Could not parse JSON output."
|
||||
assert (
|
||||
envelope.get("status") == expected
|
||||
), f"Expected status '{expected}', got '{envelope.get('status')}'."
|
||||
|
||||
|
||||
@then("the apply JSON envelope exit_code should be {expected:d}")
|
||||
def step_then_envelope_exit_code(context: Context, expected: int) -> None:
|
||||
"""Assert the envelope ``exit_code`` field equals the expected value."""
|
||||
envelope = context.apply_json_envelope
|
||||
assert envelope is not None, "Could not parse JSON output."
|
||||
assert (
|
||||
envelope.get("exit_code") == expected
|
||||
), f"Expected exit_code {expected}, got '{envelope.get('exit_code')}'."
|
||||
|
||||
|
||||
@then("the apply JSON envelope messages should be a non-empty list")
|
||||
def step_then_envelope_messages_nonempty(context: Context) -> None:
|
||||
"""Assert the envelope ``messages`` field is a non-empty list."""
|
||||
envelope = context.apply_json_envelope
|
||||
assert envelope is not None, "Could not parse JSON output."
|
||||
messages = envelope.get("messages")
|
||||
assert (
|
||||
isinstance(messages, list) and len(messages) > 0
|
||||
), f"Expected messages to be a non-empty list, got: {messages!r}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# THEN — data field structure
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then('the apply JSON data should contain field "{field}"')
|
||||
def step_then_data_has_field(context: Context, field: str) -> None:
|
||||
"""Assert the ``data`` sub-dict contains the given field."""
|
||||
envelope = context.apply_json_envelope
|
||||
assert envelope is not None, "Could not parse JSON output."
|
||||
data = envelope.get("data", {})
|
||||
assert field in data, (
|
||||
f"Expected data to contain field '{field}'.\n" f"Data keys: {list(data.keys())}"
|
||||
)
|
||||
|
||||
|
||||
@then('the apply JSON data validation should contain field "{field}"')
|
||||
def step_then_data_validation_has_field(context: Context, field: str) -> None:
|
||||
"""Assert the ``data.validation`` sub-dict contains the given field."""
|
||||
envelope = context.apply_json_envelope
|
||||
assert envelope is not None, "Could not parse JSON output."
|
||||
validation = envelope.get("data", {}).get("validation", {})
|
||||
assert field in validation, (
|
||||
f"Expected data.validation to contain field '{field}'.\n"
|
||||
f"Validation keys: {list(validation.keys())}"
|
||||
)
|
||||
|
||||
|
||||
@then('the apply JSON data lifecycle should contain field "{field}"')
|
||||
def step_then_data_lifecycle_has_field(context: Context, field: str) -> None:
|
||||
"""Assert the ``data.lifecycle`` sub-dict contains the given field."""
|
||||
envelope = context.apply_json_envelope
|
||||
assert envelope is not None, "Could not parse JSON output."
|
||||
lifecycle = envelope.get("data", {}).get("lifecycle", {})
|
||||
assert field in lifecycle, (
|
||||
f"Expected data.lifecycle to contain field '{field}'.\n"
|
||||
f"Lifecycle keys: {list(lifecycle.keys())}"
|
||||
)
|
||||
|
||||
|
||||
@then('the apply JSON data sandbox_cleanup should contain field "{field}"')
|
||||
def step_then_data_sandbox_cleanup_has_field(context: Context, field: str) -> None:
|
||||
"""Assert the ``data.sandbox_cleanup`` sub-dict contains the given field."""
|
||||
envelope = context.apply_json_envelope
|
||||
assert envelope is not None, "Could not parse JSON output."
|
||||
sandbox_cleanup = envelope.get("data", {}).get("sandbox_cleanup", {})
|
||||
assert field in sandbox_cleanup, (
|
||||
f"Expected data.sandbox_cleanup to contain field '{field}'.\n"
|
||||
f"sandbox_cleanup keys: {list(sandbox_cleanup.keys())}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# THEN — sandbox_cleanup state values
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then('the apply JSON data sandbox_cleanup worktree should be "{expected}"')
|
||||
def step_then_sandbox_cleanup_worktree(context: Context, expected: str) -> None:
|
||||
"""Assert the ``data.sandbox_cleanup.worktree`` value."""
|
||||
envelope = context.apply_json_envelope
|
||||
assert envelope is not None, "Could not parse JSON output."
|
||||
actual = envelope.get("data", {}).get("sandbox_cleanup", {}).get("worktree")
|
||||
assert (
|
||||
actual == expected
|
||||
), f"Expected sandbox_cleanup.worktree '{expected}', got '{actual}'."
|
||||
|
||||
|
||||
@then('the apply JSON data sandbox_cleanup branch should be "{expected}"')
|
||||
def step_then_sandbox_cleanup_branch(context: Context, expected: str) -> None:
|
||||
"""Assert the ``data.sandbox_cleanup.branch`` value."""
|
||||
envelope = context.apply_json_envelope
|
||||
assert envelope is not None, "Could not parse JSON output."
|
||||
actual = envelope.get("data", {}).get("sandbox_cleanup", {}).get("branch")
|
||||
assert (
|
||||
actual == expected
|
||||
), f"Expected sandbox_cleanup.branch '{expected}', got '{actual}'."
|
||||
|
||||
|
||||
@then('the apply JSON data sandbox_cleanup checkpoint should be "{expected}"')
|
||||
def step_then_sandbox_cleanup_checkpoint(context: Context, expected: str) -> None:
|
||||
"""Assert the ``data.sandbox_cleanup.checkpoint`` value."""
|
||||
envelope = context.apply_json_envelope
|
||||
assert envelope is not None, "Could not parse JSON output."
|
||||
actual = envelope.get("data", {}).get("sandbox_cleanup", {}).get("checkpoint")
|
||||
assert (
|
||||
actual == expected
|
||||
), f"Expected sandbox_cleanup.checkpoint '{expected}', got '{actual}'."
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# THEN — command isolation (other commands must not use apply envelope)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then('the plan status JSON output should not have command "plan apply"')
|
||||
def step_then_status_no_apply_command(context: Context) -> None:
|
||||
"""Assert plan status does NOT return the 'apply' envelope."""
|
||||
envelope = context.plan_status_json_envelope
|
||||
assert envelope is not None, (
|
||||
f"Could not parse JSON output.\nRaw output:\n"
|
||||
f"{context.plan_status_json_result.output}"
|
||||
)
|
||||
command_value = envelope.get("command") if isinstance(envelope, dict) else None
|
||||
assert (
|
||||
command_value != "plan apply"
|
||||
), f"plan status should not have command='plan apply', got: {command_value!r}"
|
||||
|
||||
|
||||
@then('the plan cancel JSON output should not have command "plan apply"')
|
||||
def step_then_cancel_no_apply_command(context: Context) -> None:
|
||||
"""Assert plan cancel does NOT return the 'apply' envelope."""
|
||||
envelope = context.plan_cancel_json_envelope
|
||||
assert envelope is not None, (
|
||||
f"Could not parse JSON output.\nRaw output:\n"
|
||||
f"{context.plan_cancel_json_result.output}"
|
||||
)
|
||||
command_value = envelope.get("command") if isinstance(envelope, dict) else None
|
||||
assert (
|
||||
command_value != "plan apply"
|
||||
), f"plan cancel should not have command='plan apply', got: {command_value!r}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# THEN — legacy plan fallback
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then('the legacy apply JSON envelope command should be "{expected}"')
|
||||
def step_then_legacy_envelope_command(context: Context, expected: str) -> None:
|
||||
"""Assert the legacy envelope ``command`` field equals the expected value."""
|
||||
envelope = context.legacy_apply_envelope
|
||||
assert envelope is not None, "No legacy envelope captured."
|
||||
assert (
|
||||
envelope.get("command") == expected
|
||||
), f"Expected command '{expected}', got '{envelope.get('command')}'."
|
||||
|
||||
|
||||
@then('the legacy apply JSON envelope data should contain field "{field}"')
|
||||
def step_then_legacy_data_has_field(context: Context, field: str) -> None:
|
||||
"""Assert the legacy envelope ``data`` sub-dict contains the given field."""
|
||||
envelope = context.legacy_apply_envelope
|
||||
assert envelope is not None, "No legacy envelope captured."
|
||||
data = envelope.get("data", {})
|
||||
assert field in data, (
|
||||
f"Expected legacy data to contain field '{field}'.\n"
|
||||
f"Data keys: {list(data.keys())}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# THEN — total_cost from cost_metadata
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then('the apply JSON data lifecycle total_cost should start with "{prefix}"')
|
||||
def step_then_lifecycle_total_cost_prefix(context: Context, prefix: str) -> None:
|
||||
"""Assert the ``data.lifecycle.total_cost`` starts with expected prefix."""
|
||||
envelope = context.apply_json_envelope
|
||||
assert envelope is not None, "Could not parse JSON output."
|
||||
total_cost = envelope.get("data", {}).get("lifecycle", {}).get("total_cost", "")
|
||||
assert str(total_cost).startswith(
|
||||
prefix
|
||||
), f"Expected total_cost starting with '{prefix}', got '{total_cost}'."
|
||||
@@ -0,0 +1,366 @@
|
||||
"""Helper script for plan apply Json envelope Robot tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import UTC, datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
def _make_plan(phase="apply", state="applied", cost_md=None, vs=None):
|
||||
"""Build a lightweight Plan for JSON envelope tests."""
|
||||
now = datetime.now(tz=UTC)
|
||||
pid = "01JAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
plan = {
|
||||
"identity": {"plan_id": pid},
|
||||
"namespaced_name": "local/test-plan",
|
||||
"action_name": "local/test-action",
|
||||
"description": "Test plan for apply json envelope scenarios",
|
||||
"definition_of_done": "All tests pass",
|
||||
"phase": phase,
|
||||
"processing_state": state,
|
||||
"project_links": [{"project_name": "local/my-project"}],
|
||||
"strategy_actor": "openai/gpt-4",
|
||||
"execution_actor": "openai/gpt-4",
|
||||
"created_by": "test-user",
|
||||
"reusable": True,
|
||||
"read_only": False,
|
||||
"timestamps": {
|
||||
"created_at": now.isoformat(),
|
||||
"updated_at": now.isoformat(),
|
||||
"applied_at": now.isoformat(),
|
||||
},
|
||||
"cost_metadata": cost_md,
|
||||
"validation_summary": vs or {},
|
||||
"is_terminal": state == "applied" and phase == "apply",
|
||||
"decisions": [],
|
||||
"sandbox_refs": ["ref-001"],
|
||||
}
|
||||
|
||||
def mock_get_lifecycle():
|
||||
svc = MagicMock()
|
||||
svc.get_plan.return_value = plan
|
||||
svc.apply_plan.return_value = plan
|
||||
svc._complete_apply_if_queued.return_value = plan
|
||||
return svc
|
||||
|
||||
# Use short var names to keep lines < 88 chars
|
||||
ctx_path = "cleveragents.cli.commands.plan._get_lifecycle_service"
|
||||
with patch(ctx_path, mock_get_lifecycle):
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from cleveragents.cli.commands.plan import app as plan_app
|
||||
|
||||
runner = CliRunner()
|
||||
res = runner.invoke(
|
||||
plan_app,
|
||||
["apply", "--yes", "--format", "json", pid],
|
||||
)
|
||||
|
||||
try:
|
||||
return json.loads(res.output.strip())
|
||||
except json.JSONDecodeError:
|
||||
_err(f"Bad JSON: {res.output}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _ok(v="ok"):
|
||||
"""Print success and exit."""
|
||||
print(json.dumps({"rc": 0, "stdout": v}))
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def _err(msg):
|
||||
"""Print error and exit."""
|
||||
print(json.dumps({"rc": 1, "stderr": msg}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test scenarios - each checks one aspect of the envelope
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def verify_top_level_fields():
|
||||
"""Verify the apply json envelope has all required top-level keys."""
|
||||
env = _make_plan()
|
||||
req = {"command", "status", "exit_code", "data", "timing", "messages"}
|
||||
if req <= env.keys():
|
||||
_ok(f"keys={sorted(env.keys())}")
|
||||
_err(f"Missing: {req - env.keys()}")
|
||||
|
||||
|
||||
def verify_command_field():
|
||||
"""Verify command is 'plan apply'."""
|
||||
env = _make_plan()
|
||||
cmd = str(env.get("command", ""))
|
||||
if cmd == "plan apply":
|
||||
_ok(f"cmd={cmd}")
|
||||
_err(f"Wanted 'plan apply', got {env.get('command')!r}")
|
||||
|
||||
|
||||
def verify_status_field():
|
||||
"""Verify status is 'ok'."""
|
||||
env = _make_plan()
|
||||
st = str(env.get("status", ""))
|
||||
if st == "ok":
|
||||
_ok(f"status={st}")
|
||||
_err(f"Wanted 'ok', got {env.get('status')!r}")
|
||||
|
||||
|
||||
def verify_exit_code():
|
||||
"""Verify exit_code is 0."""
|
||||
env = _make_plan()
|
||||
ec = env.get("exit_code", -1)
|
||||
if ec == 0:
|
||||
_ok(f"exit_code={ec}")
|
||||
_err(f"Wanted 0, got {ec!r}")
|
||||
|
||||
|
||||
def verify_messages_non_empty():
|
||||
"""Verify messages is a non-empty list."""
|
||||
env = _make_plan()
|
||||
msgs = env.get("messages", [])
|
||||
if isinstance(msgs, list) and len(msgs) > 0:
|
||||
_ok(f"msgs={json.dumps(msgs)}")
|
||||
_err(f"Expected non-empty list, got {msgs!r}")
|
||||
|
||||
|
||||
def verify_data_sub_fields():
|
||||
"""Verify data field has all required sub-fields."""
|
||||
env = _make_plan()
|
||||
d = env.get("data", {})
|
||||
req = {
|
||||
"artifacts",
|
||||
"changes",
|
||||
"project",
|
||||
"applied_at",
|
||||
"validation",
|
||||
"sandbox_cleanup",
|
||||
"lifecycle",
|
||||
}
|
||||
if set(req) <= set(d.keys()):
|
||||
_ok(f"data keys={sorted(d.keys())}")
|
||||
_err(f"Missing: {req - set(d.keys())}")
|
||||
|
||||
|
||||
def verify_validation_sub_fields():
|
||||
"""Verify data.validation has test, lint, type_check."""
|
||||
env = _make_plan()
|
||||
v = env.get("data", {}).get("validation", {})
|
||||
req = {"test", "lint", "type_check"}
|
||||
if set(req) <= set(v.keys()):
|
||||
_ok(f"validation keys={sorted(v.keys())}")
|
||||
_err(f"Missing: {req - set(v.keys())}")
|
||||
|
||||
|
||||
def verify_lifecycle_sub_fields():
|
||||
"""Verify data.lifecycle has required sub-fields."""
|
||||
env = _make_plan()
|
||||
lc = env.get("data", {}).get("lifecycle", {})
|
||||
req = {
|
||||
"phase",
|
||||
"state",
|
||||
"total_duration",
|
||||
"total_cost",
|
||||
"decisions_made",
|
||||
"child_plans",
|
||||
}
|
||||
if set(req) <= set(lc.keys()):
|
||||
_ok(f"lifecycle keys={sorted(lc.keys())}")
|
||||
_err(f"Missing: {req - set(lc.keys())}")
|
||||
|
||||
|
||||
def verify_sandbox_cleanup_sub_fields():
|
||||
"""Verify data.sandbox_cleanup has required fields."""
|
||||
env = _make_plan()
|
||||
sc = env.get("data", {}).get("sandbox_cleanup", {})
|
||||
req = {"worktree", "branch", "checkpoint"}
|
||||
if set(req) <= set(sc.keys()):
|
||||
_ok(f"sc keys={sorted(sc.keys())}")
|
||||
_err(f"Missing: {req - set(sc.keys())}")
|
||||
|
||||
|
||||
def verify_terminal_sandbox_cleanup():
|
||||
"""Verify terminal plan has removed/merged/archived."""
|
||||
env = _make_plan(phase="apply", state="applied")
|
||||
sc = env.get("data", {}).get("sandbox_cleanup", {})
|
||||
checks = [
|
||||
("worktree", "removed"),
|
||||
("branch", "merged to main"),
|
||||
("checkpoint", "archived"),
|
||||
]
|
||||
ok = all(str(sc.get(k)) == v for k, v in checks)
|
||||
if ok:
|
||||
_ok(f"sc={json.dumps(sc)}")
|
||||
failed = [(k, sc.get(k), v) for k, v in checks if str(sc.get(k)) != v]
|
||||
_err(f"Failed terminal checks: {failed}")
|
||||
|
||||
|
||||
def verify_non_terminal_sandbox_cleanup():
|
||||
"""Verify non-terminal plan has active/open/pending."""
|
||||
env = _make_plan(phase="apply", state="queued")
|
||||
sc = env.get("data", {}).get("sandbox_cleanup", {})
|
||||
checks = [
|
||||
("worktree", "active"),
|
||||
("branch", "open"),
|
||||
("checkpoint", "pending"),
|
||||
]
|
||||
ok = all(str(sc.get(k)) == v for k, v in checks)
|
||||
if ok:
|
||||
_ok(f"sc={json.dumps(sc)}")
|
||||
failed = [(k, sc.get(k), v) for k, v in checks if str(sc.get(k)) != v]
|
||||
_err(f"Failed non-terminal checks: {failed}")
|
||||
|
||||
|
||||
def verify_legacy_fallback():
|
||||
"""Verify non-Plan objects get minimal envelope."""
|
||||
from cleveragents.cli.commands.plan import _apply_output_dict
|
||||
|
||||
env = _apply_output_dict("legacy")
|
||||
cmd_ok = env.get("command") == "plan apply"
|
||||
data_ok = "plan" in env.get("data", {})
|
||||
if cmd_ok and data_ok:
|
||||
_ok(f"env={json.dumps(env)}")
|
||||
_err("Legacy fallback failed")
|
||||
|
||||
|
||||
def verify_cost_metadata():
|
||||
"""Verify cost is reflected when cost_metadata present."""
|
||||
env = _make_plan(cost_md={"total_cost": 1.23})
|
||||
tc = str(env.get("data", {}).get("lifecycle", {}).get("total_cost", ""))
|
||||
if tc.startswith("$"):
|
||||
_ok(f"cost={tc}")
|
||||
_err(f"Wanted $ prefix, got {tc!r}")
|
||||
|
||||
|
||||
def verify_status_no_apply_envelope():
|
||||
"""Verify status does NOT return apply envelope."""
|
||||
now = datetime.now(tz=UTC)
|
||||
pid = "01JAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
plan = {
|
||||
"identity": {"plan_id": pid},
|
||||
"namespaced_name": "local/test-plan",
|
||||
"phase": "strategize",
|
||||
"processing_state": "queued",
|
||||
"project_links": [{"project_name": "p"}],
|
||||
"timestamps": {
|
||||
"created_at": now.isoformat(),
|
||||
"updated_at": now.isoformat(),
|
||||
},
|
||||
"is_terminal": False,
|
||||
"decisions": [],
|
||||
"sandbox_refs": [],
|
||||
}
|
||||
|
||||
def mock_svc():
|
||||
svc = MagicMock()
|
||||
svc.get_plan.return_value = plan
|
||||
svc.list_plans.return_value = [plan]
|
||||
return svc
|
||||
|
||||
ctx_path = "cleveragents.cli.commands.plan._get_lifecycle_service"
|
||||
with patch(ctx_path, mock_svc):
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from cleveragents.cli.commands.plan import app as plan_app
|
||||
|
||||
runner = CliRunner()
|
||||
res = runner.invoke(
|
||||
plan_app,
|
||||
["status", pid, "--format", "json"],
|
||||
)
|
||||
|
||||
try:
|
||||
env = json.loads(res.output.strip())
|
||||
except json.JSONDecodeError:
|
||||
_err(f"Bad JSON: {res.output}")
|
||||
|
||||
cmd = env.get("command") if isinstance(env, dict) else None
|
||||
if cmd != "plan apply":
|
||||
_ok(f"cmd={cmd!r} (not apply)")
|
||||
_err("Status should not use apply envelope")
|
||||
|
||||
|
||||
def verify_cancel_no_apply_envelope():
|
||||
"""Verify cancel does NOT return apply envelope."""
|
||||
now = datetime.now(tz=UTC)
|
||||
pid = "01JAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
plan = {
|
||||
"identity": {"plan_id": pid},
|
||||
"phase": "apply",
|
||||
"processing_state": "cancelled",
|
||||
"project_links": [{"project_name": "p"}],
|
||||
"timestamps": {
|
||||
"created_at": now.isoformat(),
|
||||
"updated_at": now.isoformat(),
|
||||
},
|
||||
"is_terminal": True,
|
||||
"decisions": [],
|
||||
"sandbox_refs": [],
|
||||
}
|
||||
|
||||
def mock_svc():
|
||||
svc = MagicMock()
|
||||
svc.get_plan.return_value = plan
|
||||
svc.cancel_plan.return_value = plan
|
||||
return svc
|
||||
|
||||
ctx_path = "cleveragents.cli.commands.plan._get_lifecycle_service"
|
||||
with patch(ctx_path, mock_svc):
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from cleveragents.cli.commands.plan import app as plan_app
|
||||
|
||||
runner = CliRunner()
|
||||
res = runner.invoke(
|
||||
plan_app,
|
||||
["cancel", pid, "--format", "json"],
|
||||
)
|
||||
|
||||
try:
|
||||
env = json.loads(res.output.strip())
|
||||
except json.JSONDecodeError:
|
||||
_err(f"Bad JSON: {res.output}")
|
||||
|
||||
cmd = env.get("command") if isinstance(env, dict) else None
|
||||
if cmd != "plan apply":
|
||||
_ok(f"cmd={cmd!r} (not apply)")
|
||||
_err("Cancel should not use apply envelope")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dispatch entry point
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
SCENARIOS = {
|
||||
"verify_top_level_fields": verify_top_level_fields,
|
||||
"verify_command_field": verify_command_field,
|
||||
"verify_status_field": verify_status_field,
|
||||
"verify_exit_code": verify_exit_code,
|
||||
"verify_messages_non_empty": verify_messages_non_empty,
|
||||
"verify_data_sub_fields": verify_data_sub_fields,
|
||||
"verify_validation_sub_fields": verify_validation_sub_fields,
|
||||
"verify_lifecycle_sub_fields": verify_lifecycle_sub_fields,
|
||||
"verify_sandbox_cleanup_sub_fields": verify_sandbox_cleanup_sub_fields,
|
||||
"verify_terminal_sandbox_cleanup": verify_terminal_sandbox_cleanup,
|
||||
"verify_non_terminal_sandbox_cleanup": verify_non_terminal_sandbox_cleanup,
|
||||
"verify_legacy_fallback": verify_legacy_fallback,
|
||||
"verify_cost_metadata": verify_cost_metadata,
|
||||
"verify_status_no_apply_envelope": verify_status_no_apply_envelope,
|
||||
"verify_cancel_no_apply_envelope": verify_cancel_no_apply_envelope,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Dispatch to the requested scenario."""
|
||||
if len(sys.argv) < 2 or sys.argv[1] not in SCENARIOS:
|
||||
_err(f"Unknown: {sys.argv[1]}")
|
||||
return # unreachable but type-check friendly
|
||||
|
||||
SCENARIOS[sys.argv[1]]()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,83 @@
|
||||
*** Settings ***
|
||||
Documentation Integration tests for ``plan apply --format json`` JSON envelope output.
|
||||
Resource ${CURDIR}/common.resource
|
||||
Suite Setup Setup Test Environment With Database Isolation
|
||||
Suite Teardown Cleanup Test Environment
|
||||
|
||||
Library ${CURDIR}/helper_plan_apply_json_envelope.py
|
||||
|
||||
*** Test Cases ***
|
||||
Envelope Has All Required Top-Level Fields
|
||||
[Documentation] Verify that the plan apply JSON envelope contains all required top-level keys.
|
||||
${result}= Run Plan Apply Json Envelope Test verify_top_level_fields
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
|
||||
Envelope Command Is Plan Apply
|
||||
[Documentation] Verify command field equals "plan apply".
|
||||
${result}= Run Plan Apply Json Envelope Test verify_command_field
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
|
||||
Envelope Status Is Ok
|
||||
[Documentation] Verify status field equals "ok".
|
||||
${result}= Run Plan Apply Json Envelope Test verify_status_field
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
|
||||
Envelope Exit Code Is Zero
|
||||
[Documentation] Verify exit_code is 0.
|
||||
${result}= Run Plan Apply Json Envelope Test verify_exit_code
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
|
||||
Messages Is Non-Empty List
|
||||
[Documentation] Verify messages field is a non-empty list.
|
||||
${result}= Run Plan Apply Json Envelope Test verify_messages_non_empty
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
|
||||
Data Contains All Required Sub-Fields
|
||||
[Documentation] Verify the data field contains artifacts, changes, project, applied_at, validation, sandbox_cleanup, and lifecycle.
|
||||
${result}= Run Plan Apply Json Envelope Test verify_data_sub_fields
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
|
||||
Validation Contains Test Lint Type Check
|
||||
[Documentation] Verify data.validation contains test, lint, and type_check fields.
|
||||
${result}= Run Plan Apply Json Envelope Test verify_validation_sub_fields
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
|
||||
Lifecycle Contains Required Sub-Fields
|
||||
[Documentation] Verify data.lifecycle contains phase, state, total_duration, total_cost, decisions_made, child_plans.
|
||||
${result}= Run Plan Apply Json Envelope Test verify_lifecycle_sub_fields
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
|
||||
Sandbox Cleanup Contains Worktree Branch Checkpoint
|
||||
[Documentation] Verify data.sandbox_cleanup contains worktree, branch, and checkpoint.
|
||||
${result}= Run Plan Apply Json Envelope Test verify_sandbox_cleanup_sub_fields
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
|
||||
Terminal Plan Sandbox Cleanup Is Removed Merged Archived
|
||||
[Documentation] Verify sandbox_cleanup shows removed/merged/archived for terminal apply plans.
|
||||
${result}= Run Plan Apply Json Envelope Test verify_terminal_sandbox_cleanup
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
|
||||
NonTerminal Plan Sandbox Cleanup Is Active Open Pending
|
||||
[Documentation] Verify sandbox_cleanup shows active/open/pending for non-terminal apply plans.
|
||||
${result}= Run Plan Apply Json Envelope Test verify_non_terminal_sandbox_cleanup
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
|
||||
Legacy Plan Fallback Works
|
||||
[Documentation] Verify _apply_output_dict handles non-Plan objects gracefully.
|
||||
${result}= Run Plan Apply Json Envelope Test verify_legacy_fallback
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
|
||||
Cost Metadata Reflected In Lifecycle Total Cost
|
||||
[Documentation] Verify total_cost reflects plan cost_metadata when present.
|
||||
${result}= Run Plan Apply Json Envelope Test verify_cost_metadata
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
|
||||
Plan Status Does Not Use Apply Envelope
|
||||
[Documentation] Verify plan status --format json does NOT emit the apply envelope command field.
|
||||
${result}= Run Plan Apply Json Envelope Test verify_status_no_apply_envelope
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
|
||||
Plan Cancel Does Not Use Apply Envelope
|
||||
[Documentation] Verify plan cancel --format json does NOT emit the apply envelope command field.
|
||||
${result}= Run Plan Apply Json Envelope Test verify_cancel_no_apply_envelope
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Reference in New Issue
Block a user