b41f536da6
CI / helm (push) Successful in 47s
CI / build (push) Successful in 1m11s
CI / push-validation (push) Successful in 40s
CI / benchmark-publish (push) Has started running
CI / lint (push) Successful in 1m26s
CI / typecheck (push) Successful in 1m55s
CI / e2e_tests (push) Successful in 1m12s
CI / quality (push) Successful in 2m6s
CI / security (push) Successful in 2m9s
CI / benchmark-regression (push) Failing after 1m28s
CI / integration_tests (push) Successful in 3m48s
CI / unit_tests (push) Successful in 7m31s
CI / coverage (push) Has started running
CI / docker (push) Successful in 1m35s
CI / helm (pull_request) Successful in 39s
CI / push-validation (pull_request) Successful in 39s
CI / build (pull_request) Successful in 1m13s
CI / lint (pull_request) Successful in 1m32s
CI / quality (pull_request) Successful in 1m49s
CI / typecheck (pull_request) Successful in 1m48s
CI / security (pull_request) Successful in 1m58s
CI / status-check (push) Has been cancelled
CI / integration_tests (pull_request) Successful in 4m48s
CI / unit_tests (pull_request) Successful in 5m11s
CI / coverage (pull_request) Has started running
CI / docker (pull_request) Successful in 1m44s
CI / status-check (pull_request) Has been cancelled
Add BDD regression tests for issue #4328 automation profile gates Tests verify that complete_strategize() and complete_execute() respect automation profile thresholds and do NOT unconditionally call auto_progress(). Covers 8 built-in profiles: manual, full-auto, supervised, auto, review_before_apply, ci, trusted ISSUES CLOSED: #4328
770 lines
26 KiB
Python
770 lines
26 KiB
Python
"""Helper utilities for v3 plan lifecycle Robot integration tests.
|
|
|
|
Covers the integration between:
|
|
- Plan and Action domain models (plan.py, action.py)
|
|
- PlanLifecycleService (plan_lifecycle_service.py)
|
|
- Automation levels
|
|
- Subplan support
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from datetime import UTC, datetime
|
|
|
|
from cleveragents.application.services.plan_lifecycle_service import (
|
|
InvalidPhaseTransitionError,
|
|
PlanLifecycleService,
|
|
PlanNotReadyError,
|
|
)
|
|
from cleveragents.config.settings import Settings
|
|
from cleveragents.domain.models.core.action import (
|
|
Action,
|
|
ActionArgument,
|
|
ActionState,
|
|
ArgumentRequirement,
|
|
ArgumentType,
|
|
)
|
|
from cleveragents.domain.models.core.plan import (
|
|
AutomationProfileProvenance,
|
|
AutomationProfileRef,
|
|
ExecutionMode,
|
|
NamespacedName,
|
|
PlanPhase,
|
|
ProcessingState,
|
|
ProjectLink,
|
|
SubplanConfig,
|
|
SubplanFailureHandler,
|
|
SubplanMergeStrategy,
|
|
SubplanStatus,
|
|
can_transition,
|
|
)
|
|
|
|
|
|
def _create_service() -> PlanLifecycleService:
|
|
settings = Settings()
|
|
return PlanLifecycleService(settings=settings)
|
|
|
|
|
|
def _lifecycle_full_cycle() -> None:
|
|
"""Integration test: create action -> use -> strategize -> execute -> apply."""
|
|
service = _create_service()
|
|
|
|
# Create action (state is AVAILABLE immediately per new spec)
|
|
action = service.create_action(
|
|
name="local/code-review",
|
|
definition_of_done="All files reviewed and approved",
|
|
strategy_actor="local/planner",
|
|
execution_actor="local/executor",
|
|
description="Code review action",
|
|
)
|
|
assert action.state == ActionState.AVAILABLE, (
|
|
f"Expected AVAILABLE, got {action.state}"
|
|
)
|
|
|
|
# Use action to create plan (enters STRATEGIZE, QUEUED)
|
|
plan = service.use_action(
|
|
action_name=str(action.namespaced_name),
|
|
project_links=[ProjectLink(project_name="proj-001")],
|
|
created_by="integration-test",
|
|
)
|
|
assert plan.phase == PlanPhase.STRATEGIZE
|
|
assert plan.state == ProcessingState.QUEUED
|
|
plan_id = plan.identity.plan_id
|
|
|
|
# Strategize lifecycle
|
|
plan = service.start_strategize(plan_id)
|
|
assert plan.state == ProcessingState.PROCESSING
|
|
|
|
plan = service.complete_strategize(plan_id)
|
|
assert plan.state == ProcessingState.COMPLETE
|
|
|
|
# Execute transition
|
|
plan = service.execute_plan(plan_id)
|
|
assert plan.phase == PlanPhase.EXECUTE
|
|
assert plan.state == ProcessingState.QUEUED
|
|
|
|
plan = service.start_execute(plan_id)
|
|
assert plan.state == ProcessingState.PROCESSING
|
|
|
|
plan = service.complete_execute(plan_id)
|
|
assert plan.state == ProcessingState.COMPLETE
|
|
|
|
# Apply transition
|
|
plan = service.apply_plan(plan_id)
|
|
assert plan.phase == PlanPhase.APPLY
|
|
assert plan.state == ProcessingState.QUEUED
|
|
|
|
plan = service.start_apply(plan_id)
|
|
assert plan.state == ProcessingState.PROCESSING
|
|
|
|
plan = service.complete_apply(plan_id)
|
|
assert plan.phase == PlanPhase.APPLY
|
|
assert plan.state == ProcessingState.APPLIED
|
|
assert plan.is_terminal
|
|
|
|
print("lifecycle-full-cycle-ok")
|
|
|
|
|
|
def _action_crud() -> None:
|
|
"""Integration test: action CRUD operations via service."""
|
|
service = _create_service()
|
|
|
|
# Create multiple actions (both AVAILABLE immediately)
|
|
a1 = service.create_action(
|
|
name="local/action-one",
|
|
description="Action one",
|
|
definition_of_done="Done 1",
|
|
strategy_actor="local/s1",
|
|
execution_actor="local/e1",
|
|
)
|
|
service.create_action(
|
|
name="local/action-two",
|
|
description="Action two",
|
|
definition_of_done="Done 2",
|
|
strategy_actor="local/s2",
|
|
execution_actor="local/e2",
|
|
)
|
|
|
|
# List all
|
|
actions = service.list_actions()
|
|
assert len(actions) == 2, f"Expected 2 actions, got {len(actions)}"
|
|
|
|
# List by namespace
|
|
local_actions = service.list_actions(namespace="local")
|
|
assert len(local_actions) == 2
|
|
|
|
# Get by name
|
|
found = service.get_action_by_name("local/action-one")
|
|
assert str(found.namespaced_name) == str(a1.namespaced_name)
|
|
|
|
# Archive
|
|
archived = service.archive_action(str(a1.namespaced_name))
|
|
assert archived.state == ActionState.ARCHIVED
|
|
|
|
# List by state
|
|
available = service.list_actions(state=ActionState.AVAILABLE)
|
|
assert len(available) == 1 # Only action-two remains available
|
|
|
|
archived_list = service.list_actions(state=ActionState.ARCHIVED)
|
|
assert len(archived_list) == 1
|
|
|
|
print("action-crud-ok")
|
|
|
|
|
|
def _plan_cancellation() -> None:
|
|
"""Integration test: plan cancellation at various phases."""
|
|
service = _create_service()
|
|
|
|
action = service.create_action(
|
|
name="local/cancel-test",
|
|
description="Cancel test action",
|
|
definition_of_done="Test cancellation",
|
|
strategy_actor="local/s",
|
|
execution_actor="local/e",
|
|
)
|
|
|
|
plan = service.use_action(
|
|
action_name=str(action.namespaced_name),
|
|
project_links=[ProjectLink(project_name="proj-001")],
|
|
)
|
|
plan_id = plan.identity.plan_id
|
|
|
|
# Cancel while in STRATEGIZE
|
|
cancelled = service.cancel_plan(plan_id, reason="User requested")
|
|
assert cancelled.state == ProcessingState.CANCELLED
|
|
assert cancelled.error_message == "User requested"
|
|
|
|
print("plan-cancellation-ok")
|
|
|
|
|
|
def _plan_failure_recovery() -> None:
|
|
"""Integration test: plan failure and error states."""
|
|
service = _create_service()
|
|
|
|
action = service.create_action(
|
|
name="local/fail-test",
|
|
description="Fail test action",
|
|
definition_of_done="Test failure",
|
|
strategy_actor="local/s",
|
|
execution_actor="local/e",
|
|
)
|
|
|
|
plan = service.use_action(
|
|
action_name=str(action.namespaced_name),
|
|
project_links=[ProjectLink(project_name="proj-001")],
|
|
)
|
|
plan_id = plan.identity.plan_id
|
|
|
|
# Start strategize then fail it
|
|
service.start_strategize(plan_id)
|
|
plan = service.fail_strategize(plan_id, "Resource unavailable")
|
|
assert plan.state == ProcessingState.ERRORED
|
|
assert plan.is_errored
|
|
assert plan.error_message == "Resource unavailable"
|
|
|
|
print("plan-failure-recovery-ok")
|
|
|
|
|
|
def _invalid_transitions() -> None:
|
|
"""Integration test: invalid phase transitions raise proper errors."""
|
|
service = _create_service()
|
|
|
|
action = service.create_action(
|
|
name="local/invalid-trans",
|
|
description="Invalid transitions test action",
|
|
definition_of_done="Test invalid transitions",
|
|
strategy_actor="local/s",
|
|
execution_actor="local/e",
|
|
)
|
|
|
|
plan = service.use_action(
|
|
action_name=str(action.namespaced_name),
|
|
project_links=[ProjectLink(project_name="proj-001")],
|
|
)
|
|
plan_id = plan.identity.plan_id
|
|
|
|
# Try to execute before completing strategize (should fail)
|
|
try:
|
|
service.execute_plan(plan_id)
|
|
print("FAIL: Expected PlanNotReadyError")
|
|
return
|
|
except PlanNotReadyError:
|
|
pass
|
|
|
|
# Try to apply before execute (should fail)
|
|
try:
|
|
service.apply_plan(plan_id)
|
|
print("FAIL: Expected InvalidPhaseTransitionError or PlanNotReadyError")
|
|
return
|
|
except (InvalidPhaseTransitionError, PlanNotReadyError):
|
|
pass
|
|
|
|
print("invalid-transitions-ok")
|
|
|
|
|
|
def _automation_full_auto() -> None:
|
|
"""Integration test: full automation auto-progresses through all phases."""
|
|
service = _create_service()
|
|
|
|
action = service.create_action(
|
|
name="local/full-auto",
|
|
description="Full automation test action",
|
|
definition_of_done="Test full automation",
|
|
strategy_actor="local/s",
|
|
execution_actor="local/e",
|
|
)
|
|
|
|
plan = service.use_action(
|
|
action_name=str(action.namespaced_name),
|
|
project_links=[ProjectLink(project_name="proj-001")],
|
|
)
|
|
plan_id = plan.identity.plan_id
|
|
|
|
# Set full-auto profile so auto_progress will advance the plan
|
|
plan.automation_profile = AutomationProfileRef(
|
|
profile_name="full-auto",
|
|
provenance=AutomationProfileProvenance.PLAN,
|
|
)
|
|
|
|
# Start and complete strategize - should auto-progress to EXECUTE
|
|
service.start_strategize(plan_id)
|
|
plan = service.complete_strategize(plan_id)
|
|
|
|
# After complete_strategize with full-auto profile, auto_progress kicks in
|
|
plan = service.get_plan(plan_id)
|
|
assert plan.phase == PlanPhase.EXECUTE, f"Expected EXECUTE, got {plan.phase}"
|
|
|
|
# Start and complete execute - should auto-progress to APPLY
|
|
service.start_execute(plan_id)
|
|
plan = service.complete_execute(plan_id)
|
|
|
|
plan = service.get_plan(plan_id)
|
|
assert plan.phase == PlanPhase.APPLY, f"Expected APPLY, got {plan.phase}"
|
|
|
|
print("automation-full-auto-ok")
|
|
|
|
|
|
def _automation_review_before_apply() -> None:
|
|
"""Integration test: review-before-apply pauses before apply."""
|
|
service = _create_service()
|
|
|
|
action = service.create_action(
|
|
name="local/review-mode",
|
|
description="Review mode test action",
|
|
definition_of_done="Test review mode",
|
|
strategy_actor="local/s",
|
|
execution_actor="local/e",
|
|
)
|
|
|
|
plan = service.use_action(
|
|
action_name=str(action.namespaced_name),
|
|
project_links=[ProjectLink(project_name="proj-001")],
|
|
)
|
|
plan_id = plan.identity.plan_id
|
|
|
|
# Set "auto" profile (review-before-apply equivalent):
|
|
# create_tool=0.0, select_tool=1.0 → advances after strategize but pauses at apply
|
|
plan.automation_profile = AutomationProfileRef(
|
|
profile_name="auto",
|
|
provenance=AutomationProfileProvenance.PLAN,
|
|
)
|
|
|
|
# Complete strategize - should auto-progress to EXECUTE
|
|
service.start_strategize(plan_id)
|
|
plan = service.complete_strategize(plan_id)
|
|
|
|
plan = service.get_plan(plan_id)
|
|
assert plan.phase == PlanPhase.EXECUTE, f"Expected EXECUTE, got {plan.phase}"
|
|
|
|
# Complete execute - should NOT auto-progress to APPLY (requires review)
|
|
service.start_execute(plan_id)
|
|
plan = service.complete_execute(plan_id)
|
|
|
|
plan = service.get_plan(plan_id)
|
|
# In review/auto mode, execute completes but does not auto-transition to APPLY
|
|
assert plan.phase == PlanPhase.EXECUTE, (
|
|
f"Expected EXECUTE (paused), got {plan.phase}"
|
|
)
|
|
assert plan.state == ProcessingState.COMPLETE
|
|
|
|
print("automation-review-before-apply-ok")
|
|
|
|
|
|
def _automation_pause_resume() -> None:
|
|
"""Integration test: pause and resume automation."""
|
|
service = _create_service()
|
|
|
|
action = service.create_action(
|
|
name="local/pause-resume",
|
|
description="Pause/resume test action",
|
|
definition_of_done="Test pause/resume",
|
|
strategy_actor="local/s",
|
|
execution_actor="local/e",
|
|
)
|
|
|
|
plan = service.use_action(
|
|
action_name=str(action.namespaced_name),
|
|
project_links=[ProjectLink(project_name="proj-001")],
|
|
)
|
|
plan_id = plan.identity.plan_id
|
|
|
|
# Pause - should set profile to manual
|
|
plan = service.pause_plan(plan_id)
|
|
assert plan.automation_profile is not None
|
|
assert plan.automation_profile.profile_name == "manual"
|
|
|
|
# Resume with auto profile
|
|
plan = service.resume_plan(plan_id, automation_profile="auto")
|
|
assert plan.automation_profile is not None
|
|
assert plan.automation_profile.profile_name == "auto"
|
|
|
|
print("automation-pause-resume-ok")
|
|
|
|
|
|
def _subplan_model() -> None:
|
|
"""Integration test: subplan configuration and failure handling."""
|
|
config = SubplanConfig(
|
|
execution_mode=ExecutionMode.PARALLEL,
|
|
merge_strategy=SubplanMergeStrategy.GIT_THREE_WAY,
|
|
max_parallel=3,
|
|
fail_fast=True,
|
|
timeout_per_subplan_seconds=600,
|
|
retry_failed=True,
|
|
max_retries=2,
|
|
)
|
|
assert config.execution_mode == ExecutionMode.PARALLEL
|
|
assert config.max_parallel == 3
|
|
|
|
# Create a subplan status (using valid ULID-format IDs)
|
|
# Error strings must match RETRIABLE_FAILURES (PascalCase: TimeoutError, etc.)
|
|
status = SubplanStatus(
|
|
subplan_id="01KH72MDGQMPRW3R5WPZVB8KC1",
|
|
action_name="local/sub-task",
|
|
target_resources=["src/main.py"],
|
|
status=ProcessingState.ERRORED,
|
|
error="TimeoutError: Operation timed out",
|
|
)
|
|
|
|
handler = SubplanFailureHandler()
|
|
should_stop = handler.should_stop_others(config, status)
|
|
assert should_stop is True, "fail_fast=True should stop others"
|
|
|
|
should_retry = handler.should_retry(config, status)
|
|
assert should_retry is True, "TimeoutError is retriable and retries enabled"
|
|
|
|
# Non-retriable error (must match NON_RETRIABLE_ERRORS: ConfigurationError, etc.)
|
|
status_perm = SubplanStatus(
|
|
subplan_id="01KH72MDGQMPRW3R5WPZVB8KC2",
|
|
action_name="local/sub-task",
|
|
target_resources=["src/other.py"],
|
|
status=ProcessingState.ERRORED,
|
|
error="ConfigurationError: Invalid config",
|
|
)
|
|
should_retry_perm = handler.should_retry(config, status_perm)
|
|
assert should_retry_perm is False, "ConfigurationError is not retriable"
|
|
|
|
print("subplan-model-ok")
|
|
|
|
|
|
def _plan_with_subplans() -> None:
|
|
"""Integration test: plan with subplan config via service."""
|
|
service = _create_service()
|
|
|
|
action = service.create_action(
|
|
name="local/parent-action",
|
|
description="Parent action with subplans",
|
|
definition_of_done="Parent task with subplans",
|
|
strategy_actor="local/s",
|
|
execution_actor="local/e",
|
|
)
|
|
|
|
plan = service.use_action(
|
|
action_name=str(action.namespaced_name),
|
|
project_links=[ProjectLink(project_name="proj-001")],
|
|
)
|
|
|
|
# Add subplan config to verify Plan model accepts it
|
|
plan_copy = plan.model_copy(
|
|
update={
|
|
"subplan_config": SubplanConfig(
|
|
execution_mode=ExecutionMode.SEQUENTIAL,
|
|
merge_strategy=SubplanMergeStrategy.SEQUENTIAL_APPLY,
|
|
),
|
|
"subplan_statuses": [
|
|
SubplanStatus(
|
|
subplan_id="01KH72MDGQMPRW3R5WPZVB8KC3",
|
|
action_name="local/sub-task",
|
|
target_resources=["src/main.py"],
|
|
status=ProcessingState.QUEUED,
|
|
),
|
|
],
|
|
}
|
|
)
|
|
|
|
assert plan_copy.has_subplans
|
|
assert plan_copy.subplan_config is not None
|
|
assert plan_copy.subplan_config.execution_mode == ExecutionMode.SEQUENTIAL
|
|
assert len(plan_copy.subplan_statuses) == 1
|
|
|
|
# The original plan has no parent => is_root_plan
|
|
assert not plan_copy.is_subplan # no parent_plan_id set
|
|
|
|
print("plan-with-subplans-ok")
|
|
|
|
|
|
def _phase_transitions_model() -> None:
|
|
"""Integration test: verify can_transition function for all valid/invalid paths."""
|
|
# Valid transitions (four-phase: ACTION -> STRATEGIZE -> EXECUTE -> APPLY)
|
|
assert can_transition(PlanPhase.ACTION, PlanPhase.STRATEGIZE)
|
|
assert can_transition(PlanPhase.STRATEGIZE, PlanPhase.EXECUTE)
|
|
assert can_transition(PlanPhase.EXECUTE, PlanPhase.APPLY)
|
|
assert can_transition(PlanPhase.EXECUTE, PlanPhase.STRATEGIZE)
|
|
assert can_transition(PlanPhase.APPLY, PlanPhase.STRATEGIZE)
|
|
|
|
# Invalid transitions
|
|
assert not can_transition(PlanPhase.STRATEGIZE, PlanPhase.APPLY)
|
|
assert not can_transition(PlanPhase.ACTION, PlanPhase.EXECUTE)
|
|
assert not can_transition(PlanPhase.ACTION, PlanPhase.APPLY)
|
|
assert not can_transition(PlanPhase.APPLY, PlanPhase.EXECUTE)
|
|
|
|
print("phase-transitions-ok")
|
|
|
|
|
|
def _namespaced_name_parsing() -> None:
|
|
"""Integration test: NamespacedName parse and string round-trip."""
|
|
# Simple local name
|
|
n1 = NamespacedName.parse("local/my-action")
|
|
assert n1.namespace == "local"
|
|
assert n1.name == "my-action"
|
|
assert str(n1) == "local/my-action"
|
|
|
|
# Server-prefixed name
|
|
n2 = NamespacedName.parse("server:org/my-action")
|
|
assert n2.server == "server"
|
|
assert n2.namespace == "org"
|
|
assert n2.name == "my-action"
|
|
assert str(n2) == "server:org/my-action"
|
|
|
|
# Default namespace
|
|
n3 = NamespacedName.parse("standalone-action")
|
|
assert n3.namespace == "local"
|
|
assert n3.name == "standalone-action"
|
|
|
|
print("namespaced-name-ok")
|
|
|
|
|
|
def _action_argument_parsing() -> None:
|
|
"""Integration test: ActionArgument parse and validation."""
|
|
arg = ActionArgument.parse("target:str:required:The target directory")
|
|
assert arg.name == "target"
|
|
assert arg.arg_type.value == "string"
|
|
assert arg.requirement.value == "required"
|
|
assert arg.description == "The target directory"
|
|
|
|
# Test Action argument validation
|
|
action = Action(
|
|
namespaced_name=NamespacedName.parse("local/test-action"),
|
|
description="Test",
|
|
definition_of_done="Done",
|
|
strategy_actor="local/s",
|
|
execution_actor="local/e",
|
|
arguments=[arg],
|
|
created_at=datetime.now(UTC),
|
|
updated_at=datetime.now(UTC),
|
|
)
|
|
|
|
# Valid arguments
|
|
errors = action.validate_arguments({"target": "/tmp/dir"})
|
|
assert len(errors) == 0, f"Expected no errors, got {errors}"
|
|
|
|
# Missing required argument
|
|
errors = action.validate_arguments({})
|
|
assert len(errors) > 0, "Expected validation error for missing required arg"
|
|
|
|
print("action-argument-ok")
|
|
|
|
|
|
def _action_yaml_config_loading() -> None:
|
|
"""Integration test: Action.from_config() loads YAML fields
|
|
and as_cli_dict() renders them."""
|
|
# Simulate a full YAML config dict with all supported fields
|
|
config = {
|
|
"name": "org/code-coverage",
|
|
"description": "Increase code coverage to target level",
|
|
"long_description": "Comprehensive code coverage improvement action",
|
|
"definition_of_done": "Coverage meets ${target_coverage}% threshold",
|
|
"strategy_actor": "openai/gpt-4",
|
|
"execution_actor": "anthropic/claude-3",
|
|
"review_actor": "local/reviewer",
|
|
"apply_actor": "local/applier",
|
|
"estimation_actor": "local/estimator",
|
|
"invariant_actor": "local/invariant-checker",
|
|
"automation_profile": "org/high-trust",
|
|
"invariants": [
|
|
"No test deletions allowed",
|
|
"Coverage must not decrease",
|
|
"All new code must have tests",
|
|
],
|
|
"arguments": [
|
|
{
|
|
"name": "target_coverage",
|
|
"type": "integer",
|
|
"required": True,
|
|
"description": "Target coverage percentage",
|
|
"default": 80,
|
|
"min_value": 0,
|
|
"max_value": 100,
|
|
},
|
|
{
|
|
"name": "test_framework",
|
|
"type": "string",
|
|
"required": False,
|
|
"description": "Test framework to use",
|
|
},
|
|
{
|
|
"name": "include_branches",
|
|
"type": "boolean",
|
|
"required": False,
|
|
"description": "Include branch coverage",
|
|
},
|
|
],
|
|
"reusable": True,
|
|
"read_only": False,
|
|
"tags": ["coverage", "testing"],
|
|
"created_by": "yaml-test",
|
|
}
|
|
|
|
# --- Test from_config() loads all fields correctly ---
|
|
action = Action.from_config(config)
|
|
|
|
assert str(action.namespaced_name) == "org/code-coverage", (
|
|
f"Expected 'org/code-coverage', got '{action.namespaced_name}'"
|
|
)
|
|
assert action.description == "Increase code coverage to target level"
|
|
assert action.long_description == "Comprehensive code coverage improvement action"
|
|
assert action.definition_of_done == "Coverage meets ${target_coverage}% threshold"
|
|
assert action.strategy_actor == "openai/gpt-4"
|
|
assert action.execution_actor == "anthropic/claude-3"
|
|
assert action.review_actor == "local/reviewer"
|
|
assert action.apply_actor == "local/applier"
|
|
assert action.estimation_actor == "local/estimator"
|
|
assert action.invariant_actor == "local/invariant-checker"
|
|
assert action.automation_profile == "org/high-trust"
|
|
assert action.reusable is True
|
|
assert action.read_only is False
|
|
assert action.state == ActionState.AVAILABLE
|
|
assert action.created_by == "yaml-test"
|
|
assert action.tags == ["coverage", "testing"]
|
|
|
|
# Invariants
|
|
assert len(action.invariants) == 3
|
|
assert action.invariants[0] == "No test deletions allowed"
|
|
assert action.invariants[1] == "Coverage must not decrease"
|
|
assert action.invariants[2] == "All new code must have tests"
|
|
|
|
# Arguments
|
|
assert len(action.arguments) == 3
|
|
arg_coverage = action.arguments[0]
|
|
assert arg_coverage.name == "target_coverage"
|
|
assert arg_coverage.arg_type == ArgumentType.INTEGER
|
|
assert arg_coverage.requirement == ArgumentRequirement.REQUIRED
|
|
assert arg_coverage.description == "Target coverage percentage"
|
|
assert arg_coverage.default_value == 80
|
|
assert arg_coverage.min_value == 0
|
|
assert arg_coverage.max_value == 100
|
|
|
|
arg_framework = action.arguments[1]
|
|
assert arg_framework.name == "test_framework"
|
|
assert arg_framework.arg_type == ArgumentType.STRING
|
|
assert arg_framework.requirement == ArgumentRequirement.OPTIONAL
|
|
|
|
arg_branches = action.arguments[2]
|
|
assert arg_branches.name == "include_branches"
|
|
assert arg_branches.arg_type == ArgumentType.BOOLEAN
|
|
assert arg_branches.requirement == ArgumentRequirement.OPTIONAL
|
|
|
|
# --- Test as_cli_dict() renders expected keys ---
|
|
cli = action.as_cli_dict()
|
|
|
|
# Core identity
|
|
assert cli["name"] == "org/code-coverage"
|
|
assert cli["description"] == "Increase code coverage to target level"
|
|
assert cli["state"] == "available"
|
|
|
|
# Actors
|
|
assert cli["strategy_actor"] == "openai/gpt-4"
|
|
assert cli["execution_actor"] == "anthropic/claude-3"
|
|
assert cli["review_actor"] == "local/reviewer"
|
|
assert cli["apply_actor"] == "local/applier"
|
|
assert cli["estimation_actor"] == "local/estimator"
|
|
assert cli["invariant_actor"] == "local/invariant-checker"
|
|
|
|
# Automation profile
|
|
assert "automation_profile" in cli, "automation_profile missing from cli dict"
|
|
assert cli["automation_profile"] == "org/high-trust"
|
|
|
|
# Invariants
|
|
assert "invariants" in cli, "invariants missing from cli dict"
|
|
assert len(cli["invariants"]) == 3
|
|
assert cli["invariants"][0] == "No test deletions allowed"
|
|
assert cli["invariants"][2] == "All new code must have tests"
|
|
|
|
# Arguments rendered
|
|
assert "arguments" in cli, "arguments missing from cli dict"
|
|
assert len(cli["arguments"]) == 3
|
|
assert cli["arguments"][0]["name"] == "target_coverage"
|
|
assert cli["arguments"][0]["type"] == "integer"
|
|
assert cli["arguments"][0]["required"] is True
|
|
assert cli["arguments"][1]["name"] == "test_framework"
|
|
assert cli["arguments"][1]["required"] is False
|
|
assert cli["arguments"][2]["name"] == "include_branches"
|
|
assert cli["arguments"][2]["type"] == "boolean"
|
|
|
|
# Behavior flags
|
|
assert cli["reusable"] is True
|
|
assert cli["read_only"] is False
|
|
|
|
# Tags
|
|
assert cli["tags"] == ["coverage", "testing"]
|
|
|
|
# Definition of done
|
|
assert cli["definition_of_done"] == "Coverage meets ${target_coverage}% threshold"
|
|
|
|
print("action-yaml-config-ok")
|
|
|
|
|
|
def _plan_status_rendering() -> None:
|
|
"""Plan status renders action_name + automation profile.
|
|
|
|
Verifies that Plan.as_cli_dict() includes the correct action_name and
|
|
automation_profile fields with provenance tags.
|
|
"""
|
|
service = _create_service()
|
|
|
|
action = service.create_action(
|
|
name="local/status-test",
|
|
description="Status rendering test action",
|
|
definition_of_done="Test plan status rendering",
|
|
strategy_actor="local/s",
|
|
execution_actor="local/e",
|
|
automation_profile="manual",
|
|
)
|
|
|
|
plan = service.use_action(
|
|
action_name=str(action.namespaced_name),
|
|
project_links=[
|
|
ProjectLink(project_name="local/api-service", alias="api"),
|
|
],
|
|
)
|
|
|
|
# Verify action_name is set on the plan
|
|
assert plan.action_name, "Plan must have action_name set"
|
|
assert "status-test" in plan.action_name, (
|
|
f"Expected action_name to contain 'status-test', got '{plan.action_name}'"
|
|
)
|
|
|
|
# Verify as_cli_dict renders action_name
|
|
cli_dict = plan.as_cli_dict()
|
|
assert "action" in cli_dict, "CLI dict must include 'action' key"
|
|
assert cli_dict["action"] == plan.action_name
|
|
|
|
# Verify phase and state are rendered
|
|
assert cli_dict["phase"] == "strategize"
|
|
assert cli_dict["state"] == "queued"
|
|
|
|
# Verify projects are rendered
|
|
assert "projects" in cli_dict, "CLI dict must include 'projects' key"
|
|
assert len(cli_dict["projects"]) == 1
|
|
assert cli_dict["projects"][0]["name"] == "local/api-service"
|
|
assert cli_dict["projects"][0]["alias"] == "api"
|
|
|
|
# If automation profile was resolved, verify provenance
|
|
if plan.automation_profile:
|
|
assert "automation_profile" in cli_dict
|
|
assert "automation_profile_source" in cli_dict
|
|
assert cli_dict["automation_profile_source"] in (
|
|
"plan",
|
|
"action",
|
|
"project",
|
|
"global",
|
|
)
|
|
|
|
# Verify timestamps are present
|
|
assert "created_at" in cli_dict
|
|
assert "updated_at" in cli_dict
|
|
|
|
print("plan-status-rendering-ok")
|
|
|
|
|
|
def main() -> None:
|
|
if len(sys.argv) < 2:
|
|
raise SystemExit("Expected command argument")
|
|
command = sys.argv[1]
|
|
commands: dict[str, object] = {
|
|
"lifecycle-full-cycle": _lifecycle_full_cycle,
|
|
"action-crud": _action_crud,
|
|
"plan-cancellation": _plan_cancellation,
|
|
"plan-failure-recovery": _plan_failure_recovery,
|
|
"invalid-transitions": _invalid_transitions,
|
|
"automation-full-auto": _automation_full_auto,
|
|
"automation-review-before-apply": _automation_review_before_apply,
|
|
"automation-pause-resume": _automation_pause_resume,
|
|
"subplan-model": _subplan_model,
|
|
"plan-with-subplans": _plan_with_subplans,
|
|
"phase-transitions": _phase_transitions_model,
|
|
"namespaced-name": _namespaced_name_parsing,
|
|
"action-argument": _action_argument_parsing,
|
|
"action-yaml-config": _action_yaml_config_loading,
|
|
"plan-status-rendering": _plan_status_rendering,
|
|
}
|
|
if command not in commands:
|
|
raise SystemExit(f"Unknown command: {command}")
|
|
func = commands[command]
|
|
if callable(func):
|
|
func()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|