Files
cleveragents-core/features/steps/automation_profile_steps.py
CoreRasurae 007af498b8
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 17s
CI / helm (pull_request) Successful in 33s
CI / lint (pull_request) Successful in 3m42s
CI / security (pull_request) Successful in 4m8s
CI / quality (pull_request) Successful in 4m9s
CI / typecheck (pull_request) Successful in 4m20s
CI / integration_tests (pull_request) Successful in 7m2s
CI / unit_tests (pull_request) Successful in 7m55s
CI / docker (pull_request) Successful in 1m19s
CI / coverage (pull_request) Successful in 8m46s
CI / e2e_tests (pull_request) Successful in 16m1s
CI / status-check (pull_request) Successful in 1s
CI / build (push) Successful in 17s
CI / helm (push) Successful in 22s
CI / quality (push) Successful in 31s
CI / lint (push) Successful in 3m28s
CI / typecheck (push) Successful in 3m54s
CI / benchmark-regression (push) Has been skipped
CI / security (push) Successful in 4m5s
CI / integration_tests (push) Successful in 6m13s
CI / unit_tests (push) Successful in 6m28s
CI / docker (push) Successful in 1m34s
CI / coverage (push) Successful in 12m5s
CI / e2e_tests (push) Successful in 18m47s
CI / status-check (push) Successful in 1s
CI / benchmark-publish (push) Successful in 28m0s
CI / benchmark-regression (pull_request) Successful in 59m48s
refactor(autonomy): rename automation profile task flags to spec names
Renamed all 11 task-type confidence threshold fields in AutomationProfile
from phase-transition semantics to spec-defined task-type semantics.
Updated all 8 built-in profiles, CLI formatting, YAML schema, services,
and all Behave/Robot tests referencing the old field names.

Post-review fixes:
- Fixed 24 stale old field names in M6 fixture files
  (automation_profiles.json, autonomy_guardrails.json)
- Added model_validator(mode='before') to detect legacy field names
  and raise actionable ValueError with rename mapping
- Added semantic bridge comments in PlanLifecycleService mapping
  task-type thresholds to phase-transition gates
- Added threshold_field to structured log messages for observability
- Restored categorised CLI automation-profile show output to match
  spec (Phase Transitions / Decision Automation / Self-Repair /
  Execution Controls) instead of flat list
- Added missing access_network field to spec show output examples
  (Rich, Plain, JSON, YAML variants)
- Aligned ADR-017 profile fields table to all 11 fields with
  descriptions matching spec Automatable Tasks table
- Aligned automation_profiles.md threshold descriptions with spec
- Added spec section references in phase_reversion.md, error_recovery.md,
  and plan_execute.md for field naming context
- Extended repository roundtrip test to assert all 11 threshold fields
- Fixed benchmark _make_profile() passing safety fields as top-level
  kwargs instead of via SafetyProfile sub-model (incompatible with
  extra="forbid")
- Aligned CLI JSON/YAML output structure for automation-profile show
  with the specification grouped format (phase_transitions,
  decision_automation, self_repair, execution_controls)
- Moved safety boolean fields into the Execution Controls section
  of Rich output per spec examples
- Reverted auto profile description to "Fully automatic except apply"
  per specification (line 16703, line 28406)
- Improved bridge comments in test steps with semantic context for
  threshold-to-gate mappings

ISSUES CLOSED: #902
2026-03-30 13:18:07 +01:00

529 lines
19 KiB
Python

"""Step definitions for Automation Profile domain model tests."""
from typing import Any
from behave import then, when
from behave.runner import Context
from pydantic import ValidationError
from cleveragents.domain.models.core.automation_profile import (
BUILTIN_PROFILES,
AutomationProfile,
get_builtin_profile,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_profile(**overrides: Any) -> AutomationProfile:
"""Create a profile with sensible defaults."""
defaults: dict[str, Any] = {
"name": "test/default",
}
defaults.update(overrides)
return AutomationProfile(**defaults)
# ---------------------------------------------------------------------------
# Profile creation with specific thresholds
# ---------------------------------------------------------------------------
@when("I create a profile with decompose_task {value:g}")
def step_create_profile_decompose_task(context: Context, value: float) -> None:
"""Create a profile with a specific decompose_task."""
context.profile_model = _make_profile(decompose_task=value)
context.profile_error = None
@when("I try to create a profile with decompose_task {value:g}")
def step_try_create_profile_decompose_task(context: Context, value: float) -> None:
"""Try creating a profile with invalid decompose_task."""
context.profile_error = None
context.profile_model = None
try:
context.profile_model = _make_profile(decompose_task=value)
except ValidationError as e:
context.profile_error = e
@when("I try to create a profile with create_tool {value:g}")
def step_try_create_profile_create_tool(context: Context, value: float) -> None:
"""Try creating a profile with invalid create_tool."""
context.profile_error = None
context.profile_model = None
try:
context.profile_model = _make_profile(create_tool=value)
except ValidationError as e:
context.profile_error = e
@when("I try to create a profile with select_tool {value:g}")
def step_try_create_profile_select_tool(context: Context, value: float) -> None:
"""Try creating with invalid select_tool."""
context.profile_error = None
context.profile_model = None
try:
context.profile_model = _make_profile(select_tool=value)
except ValidationError as e:
context.profile_error = e
@when("I try to create a profile with edit_code {value:g}")
def step_try_create_profile_auto_dec_strat(context: Context, value: float) -> None:
"""Try creating with invalid edit_code."""
context.profile_error = None
context.profile_model = None
try:
context.profile_model = _make_profile(edit_code=value)
except ValidationError as e:
context.profile_error = e
@when("I try to create a profile with execute_command {value:g}")
def step_try_create_profile_auto_dec_exec(context: Context, value: float) -> None:
"""Try creating with invalid execute_command."""
context.profile_error = None
context.profile_model = None
try:
context.profile_model = _make_profile(execute_command=value)
except ValidationError as e:
context.profile_error = e
@when("I try to create a profile with create_file {value:g}")
def step_try_create_profile_auto_val_fix(context: Context, value: float) -> None:
"""Try creating with invalid create_file."""
context.profile_error = None
context.profile_model = None
try:
context.profile_model = _make_profile(create_file=value)
except ValidationError as e:
context.profile_error = e
@when("I try to create a profile with delete_content {value:g}")
def step_try_create_profile_auto_strat_rev(context: Context, value: float) -> None:
"""Try creating with invalid delete_content."""
context.profile_error = None
context.profile_model = None
try:
context.profile_model = _make_profile(delete_content=value)
except ValidationError as e:
context.profile_error = e
@when("I try to create a profile with access_network {value:g}")
def step_try_create_profile_auto_rev_apply(context: Context, value: float) -> None:
"""Try creating with invalid access_network."""
context.profile_error = None
context.profile_model = None
try:
context.profile_model = _make_profile(access_network=value)
except ValidationError as e:
context.profile_error = e
@when("I try to create a profile with install_dependency {value:g}")
def step_try_create_profile_auto_child(context: Context, value: float) -> None:
"""Try creating with invalid install_dependency."""
context.profile_error = None
context.profile_model = None
try:
context.profile_model = _make_profile(install_dependency=value)
except ValidationError as e:
context.profile_error = e
@when("I try to create a profile with modify_config {value:g}")
def step_try_create_profile_auto_retry(context: Context, value: float) -> None:
"""Try creating with invalid modify_config."""
context.profile_error = None
context.profile_model = None
try:
context.profile_model = _make_profile(modify_config=value)
except ValidationError as e:
context.profile_error = e
@when("I try to create a profile with approve_plan {value:g}")
def step_try_create_profile_auto_ckpt(context: Context, value: float) -> None:
"""Try creating with invalid approve_plan."""
context.profile_error = None
context.profile_model = None
try:
context.profile_model = _make_profile(approve_plan=value)
except ValidationError as e:
context.profile_error = e
# ---------------------------------------------------------------------------
# Profile assertions
# ---------------------------------------------------------------------------
@then("the profile model should be created")
def step_check_profile_created(context: Context) -> None:
"""Verify the profile was created."""
assert context.profile_model is not None, "Profile should be created"
@then("the profile decompose_task should be {expected:g}")
def step_check_decompose_task(context: Context, expected: float) -> None:
"""Check decompose_task value."""
actual = context.profile_model.decompose_task
assert actual == expected, f"Expected decompose_task {expected}, got {actual}"
@then("the profile create_tool should be {expected:g}")
def step_check_create_tool(context: Context, expected: float) -> None:
"""Check create_tool value."""
actual = context.profile_model.create_tool
assert actual == expected, f"Expected create_tool {expected}, got {actual}"
@then("the profile select_tool should be {expected:g}")
def step_check_select_tool(context: Context, expected: float) -> None:
"""Check select_tool value."""
actual = context.profile_model.select_tool
assert actual == expected, f"Expected select_tool {expected}, got {actual}"
@then("the profile edit_code should be {expected:g}")
def step_check_auto_dec_strat(context: Context, expected: float) -> None:
"""Check edit_code value."""
actual = context.profile_model.edit_code
assert actual == expected, f"Expected {expected}, got {actual}"
@then("the profile execute_command should be {expected:g}")
def step_check_auto_dec_exec(context: Context, expected: float) -> None:
"""Check execute_command value."""
actual = context.profile_model.execute_command
assert actual == expected, f"Expected {expected}, got {actual}"
@then("the profile create_file should be {expected:g}")
def step_check_auto_val_fix(context: Context, expected: float) -> None:
"""Check create_file value."""
actual = context.profile_model.create_file
assert actual == expected, f"Expected {expected}, got {actual}"
@then("the profile delete_content should be {expected:g}")
def step_check_auto_strat_rev(context: Context, expected: float) -> None:
"""Check delete_content value."""
actual = context.profile_model.delete_content
assert actual == expected, f"Expected {expected}, got {actual}"
@then("the profile access_network should be {expected:g}")
def step_check_auto_rev_apply(context: Context, expected: float) -> None:
"""Check access_network value."""
actual = context.profile_model.access_network
assert actual == expected, f"Expected {expected}, got {actual}"
@then("the profile install_dependency should be {expected:g}")
def step_check_auto_child(context: Context, expected: float) -> None:
"""Check install_dependency value."""
actual = context.profile_model.install_dependency
assert actual == expected, f"Expected {expected}, got {actual}"
@then("the profile modify_config should be {expected:g}")
def step_check_auto_retry(context: Context, expected: float) -> None:
"""Check modify_config value."""
actual = context.profile_model.modify_config
assert actual == expected, f"Expected {expected}, got {actual}"
@then("the profile approve_plan should be {expected:g}")
def step_check_auto_ckpt(context: Context, expected: float) -> None:
"""Check approve_plan value."""
actual = context.profile_model.approve_plan
assert actual == expected, f"Expected {expected}, got {actual}"
@then("the profile safety require_sandbox should be {expected}")
def step_check_safety_require_sandbox(context: Context, expected: str) -> None:
"""Check safety.require_sandbox value via composed SafetyProfile."""
exp_bool = expected.lower() == "true"
actual = context.profile_model.safety.require_sandbox
assert actual is exp_bool, (
f"Expected safety.require_sandbox {exp_bool}, got {actual}"
)
@then("the profile safety require_checkpoints should be {expected}")
def step_check_safety_require_checkpoints(context: Context, expected: str) -> None:
"""Check safety.require_checkpoints value via composed SafetyProfile."""
exp_bool = expected.lower() == "true"
actual = context.profile_model.safety.require_checkpoints
assert actual is exp_bool, (
f"Expected safety.require_checkpoints {exp_bool}, got {actual}"
)
@then("the profile safety allow_unsafe_tools should be {expected}")
def step_check_safety_allow_unsafe(context: Context, expected: str) -> None:
"""Check safety.allow_unsafe_tools value via composed SafetyProfile."""
exp_bool = expected.lower() == "true"
actual = context.profile_model.safety.allow_unsafe_tools
assert actual is exp_bool, (
f"Expected safety.allow_unsafe_tools {exp_bool}, got {actual}"
)
# ---------------------------------------------------------------------------
# Validation errors
# ---------------------------------------------------------------------------
@then("a profile validation error should be raised")
def step_check_profile_validation_error(
context: Context,
) -> None:
"""Verify that a validation error was raised."""
assert context.profile_error is not None, "Expected a validation error to be raised"
@then('the profile error should mention "{text}"')
def step_check_profile_error_message(context: Context, text: str) -> None:
"""Check the error message contains expected text."""
error_str = str(context.profile_error)
assert text in error_str, f"Expected error to mention '{text}', got: {error_str}"
# ---------------------------------------------------------------------------
# Built-in profiles
# ---------------------------------------------------------------------------
@when('I load the built-in profile "{name}"')
def step_load_builtin_profile(context: Context, name: str) -> None:
"""Load a built-in profile by name."""
context.profile_model = get_builtin_profile(name)
context.profile_error = None
@then("there should be {count:d} built-in profiles")
def step_check_builtin_count(context: Context, count: int) -> None:
"""Check the number of built-in profiles."""
actual = len(BUILTIN_PROFILES)
assert actual == count, f"Expected {count} built-in profiles, got {actual}"
@then('built-in profile "{name}" should exist')
def step_check_builtin_exists(context: Context, name: str) -> None:
"""Check a built-in profile exists."""
assert name in BUILTIN_PROFILES, f"Expected built-in profile '{name}' to exist"
# ---------------------------------------------------------------------------
# Custom profile from config
# ---------------------------------------------------------------------------
@when('I load a profile from config with name "{name}" and select_tool {value:g}')
def step_load_profile_from_config(context: Context, name: str, value: float) -> None:
"""Load a profile from config dict."""
config = {
"name": name,
"description": "Custom profile",
"select_tool": value,
}
context.profile_model = AutomationProfile.from_config(config)
context.profile_error = None
@when("I try to load a profile from config missing name")
def step_try_load_profile_config_missing_name(
context: Context,
) -> None:
"""Try loading profile config without name."""
context.profile_config_error = None
try:
AutomationProfile.from_config({"description": "No name"})
except ValueError as e:
context.profile_config_error = e
@then('a profile config error should be raised with "{field}"')
def step_check_profile_config_error(context: Context, field: str) -> None:
"""Check config error mentions field."""
assert context.profile_config_error is not None, (
f"Expected ValueError for missing '{field}'"
)
assert field in str(context.profile_config_error), (
f"Expected error to mention '{field}', got: {context.profile_config_error}"
)
# ---------------------------------------------------------------------------
# Name validation
# ---------------------------------------------------------------------------
@when('I create a profile with name "{name}"')
def step_create_profile_by_name(context: Context, name: str) -> None:
"""Create a profile with specific name."""
context.profile_model = AutomationProfile(name=name)
context.profile_error = None
@when('I try to create a profile with invalid name "{name}"')
def step_try_create_profile_invalid_name(context: Context, name: str) -> None:
"""Attempt to create a profile with invalid name."""
context.profile_error = None
context.profile_model = None
try:
context.profile_model = AutomationProfile(name=name)
except ValidationError as e:
context.profile_error = e
@when("I try to create a profile with empty name")
def step_try_create_profile_empty_name(
context: Context,
) -> None:
"""Attempt to create a profile with empty name."""
context.profile_error = None
context.profile_model = None
try:
context.profile_model = AutomationProfile(name="")
except ValidationError as e:
context.profile_error = e
@then('the profile name should be "{expected}"')
def step_check_profile_name(context: Context, expected: str) -> None:
"""Check profile name."""
assert context.profile_model.name == expected, (
f"Expected name '{expected}', got '{context.profile_model.name}'"
)
# ---------------------------------------------------------------------------
# get_builtin_profile helper
# ---------------------------------------------------------------------------
@when('I call get_builtin_profile with "{name}"')
def step_call_get_builtin(context: Context, name: str) -> None:
"""Call get_builtin_profile."""
context.profile_model = get_builtin_profile(name)
context.profile_error = None
@when('I try to call get_builtin_profile with "{name}"')
def step_try_call_get_builtin(context: Context, name: str) -> None:
"""Try calling get_builtin_profile with bad name."""
context.profile_key_error = None
try:
get_builtin_profile(name)
except KeyError as e:
context.profile_key_error = e
@then("a profile key error should be raised")
def step_check_profile_key_error(
context: Context,
) -> None:
"""Verify KeyError was raised."""
assert context.profile_key_error is not None, "Expected a KeyError to be raised"
# ---------------------------------------------------------------------------
# Schema version
# ---------------------------------------------------------------------------
@then('the profile schema_version should be "{expected}"')
def step_check_schema_version(context: Context, expected: str) -> None:
"""Check profile schema_version."""
actual = context.profile_model.schema_version
assert actual == expected, f"Expected schema_version '{expected}', got '{actual}'"
@when('I create a profile with schema_version "{version}"')
def step_create_profile_with_version(context: Context, version: str) -> None:
"""Create a profile with custom schema_version."""
context.profile_model = AutomationProfile(
name="test-profile", schema_version=version
)
context.profile_error = None
# ---------------------------------------------------------------------------
# Description
# ---------------------------------------------------------------------------
@then('the profile description should be "{expected}"')
def step_check_profile_description(context: Context, expected: str) -> None:
"""Check profile description."""
actual = context.profile_model.description
assert actual == expected, f"Expected description '{expected}', got '{actual}'"
@then("the profile description should be empty")
def step_check_profile_description_empty(
context: Context,
) -> None:
"""Check profile description is empty."""
actual = context.profile_model.description
assert actual == "", f"Expected empty description, got '{actual}'"
@when('I create a profile with description "{desc}"')
def step_create_profile_with_desc(context: Context, desc: str) -> None:
"""Create a profile with custom description."""
context.profile_model = AutomationProfile(name="desc-test", description=desc)
context.profile_error = None
# ---------------------------------------------------------------------------
# validate_assignment
# ---------------------------------------------------------------------------
@when("I try to assign decompose_task {value:g} on the profile")
def step_try_assign_threshold(context: Context, value: float) -> None:
"""Try to assign an invalid threshold on existing profile."""
context.profile_error = None
try:
context.profile_model.decompose_task = value
except ValidationError as e:
context.profile_error = e
# ---------------------------------------------------------------------------
# model_dump
# ---------------------------------------------------------------------------
@when("I create a profile and dump it with select_tool {value:g}")
def step_create_profile_with_select_tool(context: Context, value: float) -> None:
"""Create a profile and store model dump."""
context.profile_model = AutomationProfile(name="dump-test", select_tool=value)
context.profile_dump = context.profile_model.model_dump()
context.profile_error = None
@then('the profile model dump should have key "{key}"')
def step_check_dump_key(context: Context, key: str) -> None:
"""Check profile model dump has key."""
assert key in context.profile_dump, (
f"Expected key '{key}' in model dump, keys: {list(context.profile_dump)}"
)
@then("the profile model dump select_tool should be {expected:g}")
def step_check_dump_select_tool(context: Context, expected: float) -> None:
"""Check select_tool in model dump."""
actual = context.profile_dump["select_tool"]
assert actual == expected, f"Expected select_tool {expected}, got {actual}"