feat: implement 4-level automation profile precedence chain (plan > action > project > global)
CI / push-validation (pull_request) Successful in 24s
CI / helm (pull_request) Successful in 31s
CI / lint (pull_request) Failing after 57s
CI / build (pull_request) Successful in 3m47s
CI / quality (pull_request) Successful in 4m18s
CI / typecheck (pull_request) Successful in 4m35s
CI / security (pull_request) Successful in 4m55s
CI / coverage (pull_request) Has been skipped
CI / unit_tests (pull_request) Failing after 6m5s
CI / docker (pull_request) Has been skipped
CI / e2e_tests (pull_request) Successful in 7m3s
CI / integration_tests (pull_request) Successful in 8m46s
CI / status-check (pull_request) Failing after 3s
CI / benchmark-regression (pull_request) Has been cancelled
CI / benchmark-publish (pull_request) Has been cancelled
CI / push-validation (pull_request) Successful in 24s
CI / helm (pull_request) Successful in 31s
CI / lint (pull_request) Failing after 57s
CI / build (pull_request) Successful in 3m47s
CI / quality (pull_request) Successful in 4m18s
CI / typecheck (pull_request) Successful in 4m35s
CI / security (pull_request) Successful in 4m55s
CI / coverage (pull_request) Has been skipped
CI / unit_tests (pull_request) Failing after 6m5s
CI / docker (pull_request) Has been skipped
CI / e2e_tests (pull_request) Successful in 7m3s
CI / integration_tests (pull_request) Successful in 8m46s
CI / status-check (pull_request) Failing after 3s
CI / benchmark-regression (pull_request) Has been cancelled
CI / benchmark-publish (pull_request) Has been cancelled
- Add PROJECT level to PrecedenceSource enum - Update resolve_precedence_chain() to accept project_profile parameter - Implement 4-level precedence logic: plan > action > project > global - Expand BDD scenarios from 8 to 16 combinations to cover all permutations - Update step definitions to support project-level profile resolution - Update module docstring and precedence table to reflect 4-level chain - Maintain backward compatibility with existing code patterns Closes #8234
This commit is contained in:
@@ -1,73 +1,137 @@
|
|||||||
Feature: Automation Profile Precedence Chain (plan > action > global)
|
Feature: Automation Profile Precedence Chain (plan > action > project > global)
|
||||||
The automation profile resolution follows a strict three-level precedence
|
The automation profile resolution follows a strict four-level precedence
|
||||||
chain: plan-level > action-level > global-level. This ensures that
|
chain: plan-level > action-level > project-level > global-level. This ensures that
|
||||||
fine-grained overrides work as intended for autonomous plan execution.
|
fine-grained overrides work as intended for autonomous plan execution.
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# All 8 combinations: plan(set/unset) × action(set/unset) × global(set/default)
|
# All 16 combinations: plan(set/unset) × action(set/unset) × project(set/unset) × global(set/default)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
# --- Combination 1: plan=set, action=set, global=set → plan wins ---
|
# --- Combination 1: plan=set, action=set, project=set, global=set → plan wins ---
|
||||||
|
|
||||||
Scenario: Combination 1 - plan overrides action and explicit global
|
Scenario: Combination 1 - plan overrides action, project, and explicit global
|
||||||
Given the global profile is configured as "cautious"
|
Given the global profile is configured as "cautious"
|
||||||
When I resolve the precedence chain with plan "ci" action "auto"
|
When I resolve the precedence chain with plan "ci" action "auto" project "trusted"
|
||||||
Then the resolved precedence profile name should be "ci"
|
Then the resolved precedence profile name should be "ci"
|
||||||
And the precedence source should be "plan"
|
And the precedence source should be "plan"
|
||||||
|
|
||||||
# --- Combination 2: plan=set, action=set, global=default → plan wins ---
|
# --- Combination 2: plan=set, action=set, project=set, global=default → plan wins ---
|
||||||
|
|
||||||
Scenario: Combination 2 - plan overrides action when global is default
|
Scenario: Combination 2 - plan overrides action and project when global is default
|
||||||
Given the global profile is the default
|
Given the global profile is the default
|
||||||
When I resolve the precedence chain with plan "ci" action "auto"
|
When I resolve the precedence chain with plan "ci" action "auto" project "trusted"
|
||||||
Then the resolved precedence profile name should be "ci"
|
Then the resolved precedence profile name should be "ci"
|
||||||
And the precedence source should be "plan"
|
And the precedence source should be "plan"
|
||||||
|
|
||||||
# --- Combination 3: plan=set, action=unset, global=set → plan wins ---
|
# --- Combination 3: plan=set, action=set, project=unset, global=set → plan wins ---
|
||||||
|
|
||||||
Scenario: Combination 3 - plan overrides explicit global when action is absent
|
Scenario: Combination 3 - plan overrides action and explicit global when project is absent
|
||||||
Given the global profile is configured as "cautious"
|
Given the global profile is configured as "cautious"
|
||||||
When I resolve the precedence chain with plan "ci" and no action
|
When I resolve the precedence chain with plan "ci" action "auto" and no project
|
||||||
Then the resolved precedence profile name should be "ci"
|
Then the resolved precedence profile name should be "ci"
|
||||||
And the precedence source should be "plan"
|
And the precedence source should be "plan"
|
||||||
|
|
||||||
# --- Combination 4: plan=set, action=unset, global=default → plan wins ---
|
# --- Combination 4: plan=set, action=set, project=unset, global=default → plan wins ---
|
||||||
|
|
||||||
Scenario: Combination 4 - plan overrides default global when action is absent
|
Scenario: Combination 4 - plan overrides action when project and global are default
|
||||||
Given the global profile is the default
|
Given the global profile is the default
|
||||||
When I resolve the precedence chain with plan "ci" and no action
|
When I resolve the precedence chain with plan "ci" action "auto" and no project
|
||||||
Then the resolved precedence profile name should be "ci"
|
Then the resolved precedence profile name should be "ci"
|
||||||
And the precedence source should be "plan"
|
And the precedence source should be "plan"
|
||||||
|
|
||||||
# --- Combination 5: plan=unset, action=set, global=set → action wins ---
|
# --- Combination 5: plan=set, action=unset, project=set, global=set → plan wins ---
|
||||||
|
|
||||||
Scenario: Combination 5 - action overrides explicit global when plan is absent
|
Scenario: Combination 5 - plan overrides project and explicit global when action is absent
|
||||||
Given the global profile is configured as "cautious"
|
Given the global profile is configured as "cautious"
|
||||||
When I resolve the precedence chain with no plan and action "auto"
|
When I resolve the precedence chain with plan "ci" and no action project "trusted"
|
||||||
|
Then the resolved precedence profile name should be "ci"
|
||||||
|
And the precedence source should be "plan"
|
||||||
|
|
||||||
|
# --- Combination 6: plan=set, action=unset, project=set, global=default → plan wins ---
|
||||||
|
|
||||||
|
Scenario: Combination 6 - plan overrides project when action and global are default
|
||||||
|
Given the global profile is the default
|
||||||
|
When I resolve the precedence chain with plan "ci" and no action project "trusted"
|
||||||
|
Then the resolved precedence profile name should be "ci"
|
||||||
|
And the precedence source should be "plan"
|
||||||
|
|
||||||
|
# --- Combination 7: plan=set, action=unset, project=unset, global=set → plan wins ---
|
||||||
|
|
||||||
|
Scenario: Combination 7 - plan overrides explicit global when action and project are absent
|
||||||
|
Given the global profile is configured as "cautious"
|
||||||
|
When I resolve the precedence chain with plan "ci" and no action and no project
|
||||||
|
Then the resolved precedence profile name should be "ci"
|
||||||
|
And the precedence source should be "plan"
|
||||||
|
|
||||||
|
# --- Combination 8: plan=set, action=unset, project=unset, global=default → plan wins ---
|
||||||
|
|
||||||
|
Scenario: Combination 8 - plan overrides default global when action and project are absent
|
||||||
|
Given the global profile is the default
|
||||||
|
When I resolve the precedence chain with plan "ci" and no action and no project
|
||||||
|
Then the resolved precedence profile name should be "ci"
|
||||||
|
And the precedence source should be "plan"
|
||||||
|
|
||||||
|
# --- Combination 9: plan=unset, action=set, project=set, global=set → action wins ---
|
||||||
|
|
||||||
|
Scenario: Combination 9 - action overrides project and explicit global when plan is absent
|
||||||
|
Given the global profile is configured as "cautious"
|
||||||
|
When I resolve the precedence chain with no plan and action "auto" project "trusted"
|
||||||
Then the resolved precedence profile name should be "auto"
|
Then the resolved precedence profile name should be "auto"
|
||||||
And the precedence source should be "action"
|
And the precedence source should be "action"
|
||||||
|
|
||||||
# --- Combination 6: plan=unset, action=set, global=default → action wins ---
|
# --- Combination 10: plan=unset, action=set, project=set, global=default → action wins ---
|
||||||
|
|
||||||
Scenario: Combination 6 - action overrides default global when plan is absent
|
Scenario: Combination 10 - action overrides project when plan and global are default
|
||||||
Given the global profile is the default
|
Given the global profile is the default
|
||||||
When I resolve the precedence chain with no plan and action "auto"
|
When I resolve the precedence chain with no plan and action "auto" project "trusted"
|
||||||
Then the resolved precedence profile name should be "auto"
|
Then the resolved precedence profile name should be "auto"
|
||||||
And the precedence source should be "action"
|
And the precedence source should be "action"
|
||||||
|
|
||||||
# --- Combination 7: plan=unset, action=unset, global=set → global wins ---
|
# --- Combination 11: plan=unset, action=set, project=unset, global=set → action wins ---
|
||||||
|
|
||||||
Scenario: Combination 7 - explicit global used when plan and action are absent
|
Scenario: Combination 11 - action overrides explicit global when plan and project are absent
|
||||||
Given the global profile is configured as "cautious"
|
Given the global profile is configured as "cautious"
|
||||||
When I resolve the precedence chain with no plan and no action
|
When I resolve the precedence chain with no plan and action "auto" and no project
|
||||||
|
Then the resolved precedence profile name should be "auto"
|
||||||
|
And the precedence source should be "action"
|
||||||
|
|
||||||
|
# --- Combination 12: plan=unset, action=set, project=unset, global=default → action wins ---
|
||||||
|
|
||||||
|
Scenario: Combination 12 - action overrides default global when plan and project are absent
|
||||||
|
Given the global profile is the default
|
||||||
|
When I resolve the precedence chain with no plan and action "auto" and no project
|
||||||
|
Then the resolved precedence profile name should be "auto"
|
||||||
|
And the precedence source should be "action"
|
||||||
|
|
||||||
|
# --- Combination 13: plan=unset, action=unset, project=set, global=set → project wins ---
|
||||||
|
|
||||||
|
Scenario: Combination 13 - project overrides explicit global when plan and action are absent
|
||||||
|
Given the global profile is configured as "cautious"
|
||||||
|
When I resolve the precedence chain with no plan and no action project "trusted"
|
||||||
|
Then the resolved precedence profile name should be "trusted"
|
||||||
|
And the precedence source should be "project"
|
||||||
|
|
||||||
|
# --- Combination 14: plan=unset, action=unset, project=set, global=default → project wins ---
|
||||||
|
|
||||||
|
Scenario: Combination 14 - project overrides default global when plan and action are absent
|
||||||
|
Given the global profile is the default
|
||||||
|
When I resolve the precedence chain with no plan and no action project "trusted"
|
||||||
|
Then the resolved precedence profile name should be "trusted"
|
||||||
|
And the precedence source should be "project"
|
||||||
|
|
||||||
|
# --- Combination 15: plan=unset, action=unset, project=unset, global=set → global wins ---
|
||||||
|
|
||||||
|
Scenario: Combination 15 - explicit global used when plan, action, and project are absent
|
||||||
|
Given the global profile is configured as "cautious"
|
||||||
|
When I resolve the precedence chain with no plan and no action and no project
|
||||||
Then the resolved precedence profile name should be "cautious"
|
Then the resolved precedence profile name should be "cautious"
|
||||||
And the precedence source should be "global"
|
And the precedence source should be "global"
|
||||||
|
|
||||||
# --- Combination 8: plan=unset, action=unset, global=default → global default ---
|
# --- Combination 16: plan=unset, action=unset, project=unset, global=default → global default ---
|
||||||
|
|
||||||
Scenario: Combination 8 - default global (manual) used when nothing is set
|
Scenario: Combination 16 - default global (manual) used when nothing is set
|
||||||
Given the global profile is the default
|
Given the global profile is the default
|
||||||
When I resolve the precedence chain with no plan and no action
|
When I resolve the precedence chain with no plan and no action and no project
|
||||||
Then the resolved precedence profile name should be "manual"
|
Then the resolved precedence profile name should be "manual"
|
||||||
And the precedence source should be "global"
|
And the precedence source should be "global"
|
||||||
|
|
||||||
@@ -78,7 +142,7 @@ Feature: Automation Profile Precedence Chain (plan > action > global)
|
|||||||
Scenario: Resolution chain is logged at debug level
|
Scenario: Resolution chain is logged at debug level
|
||||||
Given the global profile is the default
|
Given the global profile is the default
|
||||||
And debug logging is captured for the precedence module
|
And debug logging is captured for the precedence module
|
||||||
When I resolve the precedence chain with plan "ci" action "auto"
|
When I resolve the precedence chain with plan "ci" action "auto" project "trusted"
|
||||||
Then a debug log entry should mention the resolved profile name "ci"
|
Then a debug log entry should mention the resolved profile name "ci"
|
||||||
And the debug log entry should mention the source "plan"
|
And the debug log entry should mention the source "plan"
|
||||||
|
|
||||||
@@ -88,18 +152,19 @@ Feature: Automation Profile Precedence Chain (plan > action > global)
|
|||||||
|
|
||||||
Scenario: Resolution result exposes plan profile field
|
Scenario: Resolution result exposes plan profile field
|
||||||
Given the global profile is the default
|
Given the global profile is the default
|
||||||
When I resolve the precedence chain with plan "ci" action "auto"
|
When I resolve the precedence chain with plan "ci" action "auto" project "trusted"
|
||||||
Then the resolution plan_profile field should be "ci"
|
Then the resolution plan_profile field should be "ci"
|
||||||
And the resolution action_profile field should be "auto"
|
And the resolution action_profile field should be "auto"
|
||||||
|
And the resolution project_profile field should be "trusted"
|
||||||
|
|
||||||
Scenario: Resolution result exposes global profile field
|
Scenario: Resolution result exposes global profile field
|
||||||
Given the global profile is configured as "cautious"
|
Given the global profile is configured as "cautious"
|
||||||
When I resolve the precedence chain with no plan and no action
|
When I resolve the precedence chain with no plan and no action and no project
|
||||||
Then the resolution global_profile field should be "cautious"
|
Then the resolution global_profile field should be "cautious"
|
||||||
|
|
||||||
Scenario: Resolution result exposes the full AutomationProfile object
|
Scenario: Resolution result exposes the full AutomationProfile object
|
||||||
Given the global profile is the default
|
Given the global profile is the default
|
||||||
When I resolve the precedence chain with plan "manual" and no action
|
When I resolve the precedence chain with plan "manual" and no action and no project
|
||||||
Then the resolved precedence profile object should have name "manual"
|
Then the resolved precedence profile object should have name "manual"
|
||||||
And the resolved precedence profile decompose_task should be 1.0
|
And the resolved precedence profile decompose_task should be 1.0
|
||||||
|
|
||||||
@@ -110,14 +175,14 @@ Feature: Automation Profile Precedence Chain (plan > action > global)
|
|||||||
Scenario: Env var sets global profile when no explicit global is configured
|
Scenario: Env var sets global profile when no explicit global is configured
|
||||||
Given the global profile is the default
|
Given the global profile is the default
|
||||||
And the env var CLEVERAGENTS_AUTOMATION_PROFILE is "trusted"
|
And the env var CLEVERAGENTS_AUTOMATION_PROFILE is "trusted"
|
||||||
When I resolve the precedence chain with no plan and no action
|
When I resolve the precedence chain with no plan and no action and no project
|
||||||
Then the resolved precedence profile name should be "trusted"
|
Then the resolved precedence profile name should be "trusted"
|
||||||
And the precedence source should be "global"
|
And the precedence source should be "global"
|
||||||
|
|
||||||
Scenario: Plan-level still overrides env var global
|
Scenario: Plan-level still overrides env var global
|
||||||
Given the global profile is the default
|
Given the global profile is the default
|
||||||
And the env var CLEVERAGENTS_AUTOMATION_PROFILE is "trusted"
|
And the env var CLEVERAGENTS_AUTOMATION_PROFILE is "trusted"
|
||||||
When I resolve the precedence chain with plan "ci" and no action
|
When I resolve the precedence chain with plan "ci" and no action and no project
|
||||||
Then the resolved precedence profile name should be "ci"
|
Then the resolved precedence profile name should be "ci"
|
||||||
And the precedence source should be "plan"
|
And the precedence source should be "plan"
|
||||||
|
|
||||||
@@ -128,7 +193,7 @@ Feature: Automation Profile Precedence Chain (plan > action > global)
|
|||||||
Scenario: Custom profile registry is used for resolution
|
Scenario: Custom profile registry is used for resolution
|
||||||
Given the global profile is the default
|
Given the global profile is the default
|
||||||
And a custom profile registry contains "acme/deploy" with select_tool 0.5
|
And a custom profile registry contains "acme/deploy" with select_tool 0.5
|
||||||
When I resolve the precedence chain with plan "acme/deploy" and no action
|
When I resolve the precedence chain with plan "acme/deploy" and no action and no project
|
||||||
Then the resolved precedence profile name should be "acme/deploy"
|
Then the resolved precedence profile name should be "acme/deploy"
|
||||||
And the precedence source should be "plan"
|
And the precedence source should be "plan"
|
||||||
|
|
||||||
@@ -138,13 +203,19 @@ Feature: Automation Profile Precedence Chain (plan > action > global)
|
|||||||
|
|
||||||
Scenario: Empty string plan profile is treated as absent
|
Scenario: Empty string plan profile is treated as absent
|
||||||
Given the global profile is the default
|
Given the global profile is the default
|
||||||
When I resolve the precedence chain with empty plan and action "auto"
|
When I resolve the precedence chain with empty plan and action "auto" and no project
|
||||||
Then the resolved precedence profile name should be "auto"
|
Then the resolved precedence profile name should be "auto"
|
||||||
And the precedence source should be "action"
|
And the precedence source should be "action"
|
||||||
|
|
||||||
Scenario: Empty string action profile is treated as absent
|
Scenario: Empty string action profile is treated as absent
|
||||||
Given the global profile is the default
|
Given the global profile is the default
|
||||||
When I resolve the precedence chain with plan absent and empty action
|
When I resolve the precedence chain with plan absent and empty action and no project
|
||||||
|
Then the resolved precedence profile name should be "manual"
|
||||||
|
And the precedence source should be "global"
|
||||||
|
|
||||||
|
Scenario: Empty string project profile is treated as absent
|
||||||
|
Given the global profile is the default
|
||||||
|
When I resolve the precedence chain with plan absent and action absent and empty project
|
||||||
Then the resolved precedence profile name should be "manual"
|
Then the resolved precedence profile name should be "manual"
|
||||||
And the precedence source should be "global"
|
And the precedence source should be "global"
|
||||||
|
|
||||||
@@ -158,5 +229,8 @@ Feature: Automation Profile Precedence Chain (plan > action > global)
|
|||||||
Scenario: PrecedenceSource ACTION has value "action"
|
Scenario: PrecedenceSource ACTION has value "action"
|
||||||
Then the PrecedenceSource ACTION value should be "action"
|
Then the PrecedenceSource ACTION value should be "action"
|
||||||
|
|
||||||
|
Scenario: PrecedenceSource PROJECT has value "project"
|
||||||
|
Then the PrecedenceSource PROJECT value should be "project"
|
||||||
|
|
||||||
Scenario: PrecedenceSource GLOBAL has value "global"
|
Scenario: PrecedenceSource GLOBAL has value "global"
|
||||||
Then the PrecedenceSource GLOBAL value should be "global"
|
Then the PrecedenceSource GLOBAL value should be "global"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"""Step definitions for automation profile precedence chain tests.
|
"""Step definitions for automation profile precedence chain tests.
|
||||||
|
|
||||||
Tests the three-level precedence chain: plan > action > global.
|
Tests the four-level precedence chain: plan > action > project > global.
|
||||||
Covers all 8 combinations of (plan set/unset) x (action set/unset) x
|
Covers all 16 combinations of (plan set/unset) x (action set/unset) x
|
||||||
(global set/default).
|
(project set/unset) x (global set/default).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -108,11 +108,11 @@ def step_given_custom_registry(context: Context, name: str, threshold: float) ->
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@when('I resolve the precedence chain with plan "{plan_name}" action "{action_name}"')
|
@when('I resolve the precedence chain with plan "{plan_name}" action "{action_name}" project "{project_name}"')
|
||||||
def step_when_resolve_plan_and_action(
|
def step_when_resolve_plan_action_project(
|
||||||
context: Context, plan_name: str, action_name: str
|
context: Context, plan_name: str, action_name: str, project_name: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Resolve with both plan and action set."""
|
"""Resolve with plan, action, and project all set."""
|
||||||
global_override: str | None = getattr(context, "global_override", None)
|
global_override: str | None = getattr(context, "global_override", None)
|
||||||
registry: dict[str, AutomationProfile] | None = getattr(
|
registry: dict[str, AutomationProfile] | None = getattr(
|
||||||
context, "custom_registry", None
|
context, "custom_registry", None
|
||||||
@@ -120,14 +120,51 @@ def step_when_resolve_plan_and_action(
|
|||||||
context.resolution: PrecedenceResolution = resolve_precedence_chain(
|
context.resolution: PrecedenceResolution = resolve_precedence_chain(
|
||||||
plan_profile=plan_name,
|
plan_profile=plan_name,
|
||||||
action_profile=action_name,
|
action_profile=action_name,
|
||||||
|
project_profile=project_name,
|
||||||
global_override=global_override,
|
global_override=global_override,
|
||||||
profile_registry=registry,
|
profile_registry=registry,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@when('I resolve the precedence chain with plan "{plan_name}" and no action')
|
@when('I resolve the precedence chain with plan "{plan_name}" action "{action_name}" and no project')
|
||||||
|
def step_when_resolve_plan_and_action(
|
||||||
|
context: Context, plan_name: str, action_name: str
|
||||||
|
) -> None:
|
||||||
|
"""Resolve with plan and action set, project absent."""
|
||||||
|
global_override: str | None = getattr(context, "global_override", None)
|
||||||
|
registry: dict[str, AutomationProfile] | None = getattr(
|
||||||
|
context, "custom_registry", None
|
||||||
|
)
|
||||||
|
context.resolution: PrecedenceResolution = resolve_precedence_chain(
|
||||||
|
plan_profile=plan_name,
|
||||||
|
action_profile=action_name,
|
||||||
|
project_profile=None,
|
||||||
|
global_override=global_override,
|
||||||
|
profile_registry=registry,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@when('I resolve the precedence chain with plan "{plan_name}" and no action project "{project_name}"')
|
||||||
|
def step_when_resolve_plan_and_project(
|
||||||
|
context: Context, plan_name: str, project_name: str
|
||||||
|
) -> None:
|
||||||
|
"""Resolve with plan and project set, action absent."""
|
||||||
|
global_override: str | None = getattr(context, "global_override", None)
|
||||||
|
registry: dict[str, AutomationProfile] | None = getattr(
|
||||||
|
context, "custom_registry", None
|
||||||
|
)
|
||||||
|
context.resolution: PrecedenceResolution = resolve_precedence_chain(
|
||||||
|
plan_profile=plan_name,
|
||||||
|
action_profile=None,
|
||||||
|
project_profile=project_name,
|
||||||
|
global_override=global_override,
|
||||||
|
profile_registry=registry,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@when('I resolve the precedence chain with plan "{plan_name}" and no action and no project')
|
||||||
def step_when_resolve_plan_only(context: Context, plan_name: str) -> None:
|
def step_when_resolve_plan_only(context: Context, plan_name: str) -> None:
|
||||||
"""Resolve with plan set, action absent."""
|
"""Resolve with plan set, action and project absent."""
|
||||||
global_override: str | None = getattr(context, "global_override", None)
|
global_override: str | None = getattr(context, "global_override", None)
|
||||||
registry: dict[str, AutomationProfile] | None = getattr(
|
registry: dict[str, AutomationProfile] | None = getattr(
|
||||||
context, "custom_registry", None
|
context, "custom_registry", None
|
||||||
@@ -135,14 +172,33 @@ def step_when_resolve_plan_only(context: Context, plan_name: str) -> None:
|
|||||||
context.resolution = resolve_precedence_chain(
|
context.resolution = resolve_precedence_chain(
|
||||||
plan_profile=plan_name,
|
plan_profile=plan_name,
|
||||||
action_profile=None,
|
action_profile=None,
|
||||||
|
project_profile=None,
|
||||||
global_override=global_override,
|
global_override=global_override,
|
||||||
profile_registry=registry,
|
profile_registry=registry,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@when('I resolve the precedence chain with no plan and action "{action_name}"')
|
@when('I resolve the precedence chain with no plan and action "{action_name}" project "{project_name}"')
|
||||||
|
def step_when_resolve_action_and_project(
|
||||||
|
context: Context, action_name: str, project_name: str
|
||||||
|
) -> None:
|
||||||
|
"""Resolve with action and project set, plan absent."""
|
||||||
|
global_override: str | None = getattr(context, "global_override", None)
|
||||||
|
registry: dict[str, AutomationProfile] | None = getattr(
|
||||||
|
context, "custom_registry", None
|
||||||
|
)
|
||||||
|
context.resolution: PrecedenceResolution = resolve_precedence_chain(
|
||||||
|
plan_profile=None,
|
||||||
|
action_profile=action_name,
|
||||||
|
project_profile=project_name,
|
||||||
|
global_override=global_override,
|
||||||
|
profile_registry=registry,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@when('I resolve the precedence chain with no plan and action "{action_name}" and no project')
|
||||||
def step_when_resolve_action_only(context: Context, action_name: str) -> None:
|
def step_when_resolve_action_only(context: Context, action_name: str) -> None:
|
||||||
"""Resolve with action set, plan absent."""
|
"""Resolve with action set, plan and project absent."""
|
||||||
global_override: str | None = getattr(context, "global_override", None)
|
global_override: str | None = getattr(context, "global_override", None)
|
||||||
registry: dict[str, AutomationProfile] | None = getattr(
|
registry: dict[str, AutomationProfile] | None = getattr(
|
||||||
context, "custom_registry", None
|
context, "custom_registry", None
|
||||||
@@ -150,14 +206,15 @@ def step_when_resolve_action_only(context: Context, action_name: str) -> None:
|
|||||||
context.resolution = resolve_precedence_chain(
|
context.resolution = resolve_precedence_chain(
|
||||||
plan_profile=None,
|
plan_profile=None,
|
||||||
action_profile=action_name,
|
action_profile=action_name,
|
||||||
|
project_profile=None,
|
||||||
global_override=global_override,
|
global_override=global_override,
|
||||||
profile_registry=registry,
|
profile_registry=registry,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@when("I resolve the precedence chain with no plan and no action")
|
@when('I resolve the precedence chain with no plan and no action project "{project_name}"')
|
||||||
def step_when_resolve_global_only(context: Context) -> None:
|
def step_when_resolve_project_only(context: Context, project_name: str) -> None:
|
||||||
"""Resolve with neither plan nor action set."""
|
"""Resolve with project set, plan and action absent."""
|
||||||
global_override: str | None = getattr(context, "global_override", None)
|
global_override: str | None = getattr(context, "global_override", None)
|
||||||
registry: dict[str, AutomationProfile] | None = getattr(
|
registry: dict[str, AutomationProfile] | None = getattr(
|
||||||
context, "custom_registry", None
|
context, "custom_registry", None
|
||||||
@@ -165,12 +222,29 @@ def step_when_resolve_global_only(context: Context) -> None:
|
|||||||
context.resolution = resolve_precedence_chain(
|
context.resolution = resolve_precedence_chain(
|
||||||
plan_profile=None,
|
plan_profile=None,
|
||||||
action_profile=None,
|
action_profile=None,
|
||||||
|
project_profile=project_name,
|
||||||
global_override=global_override,
|
global_override=global_override,
|
||||||
profile_registry=registry,
|
profile_registry=registry,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@when('I resolve the precedence chain with empty plan and action "{action_name}"')
|
@when("I resolve the precedence chain with no plan and no action and no project")
|
||||||
|
def step_when_resolve_global_only(context: Context) -> None:
|
||||||
|
"""Resolve with plan, action, and project all absent."""
|
||||||
|
global_override: str | None = getattr(context, "global_override", None)
|
||||||
|
registry: dict[str, AutomationProfile] | None = getattr(
|
||||||
|
context, "custom_registry", None
|
||||||
|
)
|
||||||
|
context.resolution = resolve_precedence_chain(
|
||||||
|
plan_profile=None,
|
||||||
|
action_profile=None,
|
||||||
|
project_profile=None,
|
||||||
|
global_override=global_override,
|
||||||
|
profile_registry=registry,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@when('I resolve the precedence chain with empty plan and action "{action_name}" and no project')
|
||||||
def step_when_resolve_empty_plan_with_action(
|
def step_when_resolve_empty_plan_with_action(
|
||||||
context: Context, action_name: str
|
context: Context, action_name: str
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -179,17 +253,31 @@ def step_when_resolve_empty_plan_with_action(
|
|||||||
context.resolution = resolve_precedence_chain(
|
context.resolution = resolve_precedence_chain(
|
||||||
plan_profile="",
|
plan_profile="",
|
||||||
action_profile=action_name,
|
action_profile=action_name,
|
||||||
|
project_profile=None,
|
||||||
global_override=global_override,
|
global_override=global_override,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@when("I resolve the precedence chain with plan absent and empty action")
|
@when("I resolve the precedence chain with plan absent and empty action and no project")
|
||||||
def step_when_resolve_absent_plan_empty_action(context: Context) -> None:
|
def step_when_resolve_absent_plan_empty_action(context: Context) -> None:
|
||||||
"""Resolve with plan absent and empty string action (treated as absent)."""
|
"""Resolve with plan absent and empty string action (treated as absent)."""
|
||||||
global_override: str | None = getattr(context, "global_override", None)
|
global_override: str | None = getattr(context, "global_override", None)
|
||||||
context.resolution = resolve_precedence_chain(
|
context.resolution = resolve_precedence_chain(
|
||||||
plan_profile=None,
|
plan_profile=None,
|
||||||
action_profile="",
|
action_profile="",
|
||||||
|
project_profile=None,
|
||||||
|
global_override=global_override,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@when("I resolve the precedence chain with plan absent and action absent and empty project")
|
||||||
|
def step_when_resolve_absent_plan_action_empty_project(context: Context) -> None:
|
||||||
|
"""Resolve with plan and action absent and empty string project (treated as absent)."""
|
||||||
|
global_override: str | None = getattr(context, "global_override", None)
|
||||||
|
context.resolution = resolve_precedence_chain(
|
||||||
|
plan_profile=None,
|
||||||
|
action_profile=None,
|
||||||
|
project_profile="",
|
||||||
global_override=global_override,
|
global_override=global_override,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -210,7 +298,7 @@ def step_then_resolved_name(context: Context, expected: str) -> None:
|
|||||||
|
|
||||||
@then('the precedence source should be "{expected_source}"')
|
@then('the precedence source should be "{expected_source}"')
|
||||||
def step_then_precedence_source(context: Context, expected_source: str) -> None:
|
def step_then_precedence_source(context: Context, expected_source: str) -> None:
|
||||||
"""Assert the precedence source (plan, action, or global)."""
|
"""Assert the precedence source (plan, action, project, or global)."""
|
||||||
actual = context.resolution.source.value
|
actual = context.resolution.source.value
|
||||||
assert actual == expected_source, (
|
assert actual == expected_source, (
|
||||||
f"Expected precedence source '{expected_source}', got '{actual}'"
|
f"Expected precedence source '{expected_source}', got '{actual}'"
|
||||||
@@ -235,6 +323,15 @@ def step_then_resolution_action_field(context: Context, expected: str) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@then('the resolution project_profile field should be "{expected}"')
|
||||||
|
def step_then_resolution_project_field(context: Context, expected: str) -> None:
|
||||||
|
"""Assert the project_profile field on the resolution result."""
|
||||||
|
actual = context.resolution.project_profile
|
||||||
|
assert actual == expected, (
|
||||||
|
f"Expected resolution.project_profile '{expected}', got '{actual}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@then('the resolution global_profile field should be "{expected}"')
|
@then('the resolution global_profile field should be "{expected}"')
|
||||||
def step_then_resolution_global_field(context: Context, expected: str) -> None:
|
def step_then_resolution_global_field(context: Context, expected: str) -> None:
|
||||||
"""Assert the global_profile field on the resolution result."""
|
"""Assert the global_profile field on the resolution result."""
|
||||||
@@ -310,6 +407,15 @@ def step_then_precedence_source_action_value(context: Context, expected: str) ->
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@then('the PrecedenceSource PROJECT value should be "{expected}"')
|
||||||
|
def step_then_precedence_source_project_value(context: Context, expected: str) -> None:
|
||||||
|
"""Assert PrecedenceSource.PROJECT has the expected string value."""
|
||||||
|
assert PrecedenceSource.PROJECT.value == expected, (
|
||||||
|
f"Expected PrecedenceSource.PROJECT.value '{expected}', "
|
||||||
|
f"got '{PrecedenceSource.PROJECT.value}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@then('the PrecedenceSource GLOBAL value should be "{expected}"')
|
@then('the PrecedenceSource GLOBAL value should be "{expected}"')
|
||||||
def step_then_precedence_source_global_value(context: Context, expected: str) -> None:
|
def step_then_precedence_source_global_value(context: Context, expected: str) -> None:
|
||||||
"""Assert PrecedenceSource.GLOBAL has the expected string value."""
|
"""Assert PrecedenceSource.GLOBAL has the expected string value."""
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""Automation Profile Precedence Chain for CleverAgents v3.5.
|
"""Automation Profile Precedence Chain for CleverAgents v3.5.
|
||||||
|
|
||||||
Implements and validates the three-level precedence chain for automation
|
Implements and validates the four-level precedence chain for automation
|
||||||
profile resolution:
|
profile resolution:
|
||||||
|
|
||||||
plan > action > global
|
plan > action > project > global
|
||||||
|
|
||||||
This module provides a dedicated, testable implementation of the precedence
|
This module provides a dedicated, testable implementation of the precedence
|
||||||
chain that is independent of the full ``AutomationProfileService`` CRUD
|
chain that is independent of the full ``AutomationProfileService`` CRUD
|
||||||
@@ -14,38 +14,56 @@ Precedence Rules
|
|||||||
----------------
|
----------------
|
||||||
|
|
||||||
1. **Plan-level** — explicitly set via ``--automation-profile`` on plan use.
|
1. **Plan-level** — explicitly set via ``--automation-profile`` on plan use.
|
||||||
Highest priority; overrides action and global.
|
Highest priority; overrides action, project, and global.
|
||||||
2. **Action-level** — default profile set on the action template.
|
2. **Action-level** — default profile set on the action template.
|
||||||
Overrides global; used when no plan-level override is set.
|
Overrides project and global; used when no plan-level override is set.
|
||||||
3. **Global-level** — set via config key or
|
3. **Project-level** — default profile set at the project level.
|
||||||
|
Overrides global; used when neither plan nor action override is set.
|
||||||
|
4. **Global-level** — set via config key or
|
||||||
``CLEVERAGENTS_AUTOMATION_PROFILE`` env var, falling back to ``"manual"``.
|
``CLEVERAGENTS_AUTOMATION_PROFILE`` env var, falling back to ``"manual"``.
|
||||||
Used when neither plan nor action override is set.
|
Used when none of plan, action, or project override is set.
|
||||||
|
|
||||||
All 8 Combinations
|
All 16 Combinations
|
||||||
------------------
|
-------------------
|
||||||
|
|
||||||
The precedence chain covers all 8 combinations of
|
The precedence chain covers all 16 combinations of:
|
||||||
(plan set / not set) x (action set / not set) x (global set / default):
|
(plan set/unset) x (action set/unset) x (project set/unset) x (global set/default)
|
||||||
|
|
||||||
+-------+--------+--------+------------------+
|
+-------+--------+---------+--------+------------------+
|
||||||
| plan | action | global | resolved source |
|
| plan | action | project | global | resolved source |
|
||||||
+=======+========+========+==================+
|
+=======+========+=========+========+==================+
|
||||||
| set | set | set | plan |
|
| set | set | set | set | plan |
|
||||||
+-------+--------+--------+------------------+
|
+-------+--------+---------+--------+------------------+
|
||||||
| set | set | default| plan |
|
| set | set | set | default| plan |
|
||||||
+-------+--------+--------+------------------+
|
+-------+--------+---------+--------+------------------+
|
||||||
| set | unset | set | plan |
|
| set | set | unset | set | plan |
|
||||||
+-------+--------+--------+------------------+
|
+-------+--------+---------+--------+------------------+
|
||||||
| set | unset | default| plan |
|
| set | set | unset | default| plan |
|
||||||
+-------+--------+--------+------------------+
|
+-------+--------+---------+--------+------------------+
|
||||||
| unset | set | set | action |
|
| set | unset | set | set | plan |
|
||||||
+-------+--------+--------+------------------+
|
+-------+--------+---------+--------+------------------+
|
||||||
| unset | set | default| action |
|
| set | unset | set | default| plan |
|
||||||
+-------+--------+--------+------------------+
|
+-------+--------+---------+--------+------------------+
|
||||||
| unset | unset | set | global |
|
| set | unset | unset | set | plan |
|
||||||
+-------+--------+--------+------------------+
|
+-------+--------+---------+--------+------------------+
|
||||||
| unset | unset | default| global (manual) |
|
| set | unset | unset | default| plan |
|
||||||
+-------+--------+--------+------------------+
|
+-------+--------+---------+--------+------------------+
|
||||||
|
| unset | set | set | set | action |
|
||||||
|
+-------+--------+---------+--------+------------------+
|
||||||
|
| unset | set | set | default| action |
|
||||||
|
+-------+--------+---------+--------+------------------+
|
||||||
|
| unset | set | unset | set | action |
|
||||||
|
+-------+--------+---------+--------+------------------+
|
||||||
|
| unset | set | unset | default| action |
|
||||||
|
+-------+--------+---------+--------+------------------+
|
||||||
|
| unset | unset | set | set | project |
|
||||||
|
+-------+--------+---------+--------+------------------+
|
||||||
|
| unset | unset | set | default| project |
|
||||||
|
+-------+--------+---------+--------+------------------+
|
||||||
|
| unset | unset | unset | set | global |
|
||||||
|
+-------+--------+---------+--------+------------------+
|
||||||
|
| unset | unset | unset | default| global (manual) |
|
||||||
|
+-------+--------+---------+--------+------------------+
|
||||||
|
|
||||||
Based on ``docs/specification.md`` Section "Automation Profiles".
|
Based on ``docs/specification.md`` Section "Automation Profiles".
|
||||||
"""
|
"""
|
||||||
@@ -71,11 +89,13 @@ _DEFAULT_GLOBAL_PROFILE = "manual"
|
|||||||
class PrecedenceSource(StrEnum):
|
class PrecedenceSource(StrEnum):
|
||||||
"""Indicates which level of the precedence chain resolved the profile.
|
"""Indicates which level of the precedence chain resolved the profile.
|
||||||
|
|
||||||
Values correspond to the three levels of the plan > action > global chain.
|
Values correspond to the four levels of the plan > action > project > global
|
||||||
|
chain.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
PLAN = "plan"
|
PLAN = "plan"
|
||||||
ACTION = "action"
|
ACTION = "action"
|
||||||
|
PROJECT = "project"
|
||||||
GLOBAL = "global"
|
GLOBAL = "global"
|
||||||
|
|
||||||
|
|
||||||
@@ -89,6 +109,7 @@ class PrecedenceResolution:
|
|||||||
profile_name: The resolved profile name string.
|
profile_name: The resolved profile name string.
|
||||||
plan_profile: The plan-level override (may be ``None``).
|
plan_profile: The plan-level override (may be ``None``).
|
||||||
action_profile: The action-level override (may be ``None``).
|
action_profile: The action-level override (may be ``None``).
|
||||||
|
project_profile: The project-level override (may be ``None``).
|
||||||
global_profile: The effective global profile name used as fallback.
|
global_profile: The effective global profile name used as fallback.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -97,6 +118,7 @@ class PrecedenceResolution:
|
|||||||
profile_name: str
|
profile_name: str
|
||||||
plan_profile: str | None
|
plan_profile: str | None
|
||||||
action_profile: str | None
|
action_profile: str | None
|
||||||
|
project_profile: str | None
|
||||||
global_profile: str
|
global_profile: str
|
||||||
|
|
||||||
|
|
||||||
@@ -129,12 +151,13 @@ def resolve_precedence_chain(
|
|||||||
*,
|
*,
|
||||||
plan_profile: str | None = None,
|
plan_profile: str | None = None,
|
||||||
action_profile: str | None = None,
|
action_profile: str | None = None,
|
||||||
|
project_profile: str | None = None,
|
||||||
global_override: str | None = None,
|
global_override: str | None = None,
|
||||||
profile_registry: dict[str, AutomationProfile] | None = None,
|
profile_registry: dict[str, AutomationProfile] | None = None,
|
||||||
) -> PrecedenceResolution:
|
) -> PrecedenceResolution:
|
||||||
"""Resolve the effective automation profile using the precedence chain.
|
"""Resolve the effective automation profile using the precedence chain.
|
||||||
|
|
||||||
Implements the canonical plan > action > global precedence chain.
|
Implements the canonical plan > action > project > global precedence chain.
|
||||||
The resolution is logged at ``DEBUG`` level so callers can trace
|
The resolution is logged at ``DEBUG`` level so callers can trace
|
||||||
which level provided the profile.
|
which level provided the profile.
|
||||||
|
|
||||||
@@ -143,6 +166,8 @@ def resolve_precedence_chain(
|
|||||||
``None`` or empty string means no plan-level override.
|
``None`` or empty string means no plan-level override.
|
||||||
action_profile: Profile name set on the action template.
|
action_profile: Profile name set on the action template.
|
||||||
``None`` or empty string means no action-level override.
|
``None`` or empty string means no action-level override.
|
||||||
|
project_profile: Profile name set at the project level.
|
||||||
|
``None`` or empty string means no project-level override.
|
||||||
global_override: Explicit global profile name from config service.
|
global_override: Explicit global profile name from config service.
|
||||||
Falls back to ``CLEVERAGENTS_AUTOMATION_PROFILE`` env var,
|
Falls back to ``CLEVERAGENTS_AUTOMATION_PROFILE`` env var,
|
||||||
then ``"manual"``.
|
then ``"manual"``.
|
||||||
@@ -169,6 +194,10 @@ def resolve_precedence_chain(
|
|||||||
>>> result.source
|
>>> result.source
|
||||||
<PrecedenceSource.ACTION: 'action'>
|
<PrecedenceSource.ACTION: 'action'>
|
||||||
|
|
||||||
|
>>> result = resolve_precedence_chain(project_profile="trusted")
|
||||||
|
>>> result.source
|
||||||
|
<PrecedenceSource.PROJECT: 'project'>
|
||||||
|
|
||||||
>>> result = resolve_precedence_chain()
|
>>> result = resolve_precedence_chain()
|
||||||
>>> result.source
|
>>> result.source
|
||||||
<PrecedenceSource.GLOBAL: 'global'>
|
<PrecedenceSource.GLOBAL: 'global'>
|
||||||
@@ -186,26 +215,33 @@ def resolve_precedence_chain(
|
|||||||
norm_action = action_profile.strip() if isinstance(action_profile, str) else None
|
norm_action = action_profile.strip() if isinstance(action_profile, str) else None
|
||||||
norm_action = norm_action if norm_action else None
|
norm_action = norm_action if norm_action else None
|
||||||
|
|
||||||
|
norm_project = project_profile.strip() if isinstance(project_profile, str) else None
|
||||||
|
norm_project = norm_project if norm_project else None
|
||||||
|
|
||||||
effective_global = _resolve_global_profile(global_override)
|
effective_global = _resolve_global_profile(global_override)
|
||||||
|
|
||||||
# Apply precedence chain: plan > action > global
|
# Apply precedence chain: plan > action > project > global
|
||||||
if norm_plan is not None:
|
if norm_plan is not None:
|
||||||
chosen_name = norm_plan
|
chosen_name = norm_plan
|
||||||
source = PrecedenceSource.PLAN
|
source = PrecedenceSource.PLAN
|
||||||
elif norm_action is not None:
|
elif norm_action is not None:
|
||||||
chosen_name = norm_action
|
chosen_name = norm_action
|
||||||
source = PrecedenceSource.ACTION
|
source = PrecedenceSource.ACTION
|
||||||
|
elif norm_project is not None:
|
||||||
|
chosen_name = norm_project
|
||||||
|
source = PrecedenceSource.PROJECT
|
||||||
else:
|
else:
|
||||||
chosen_name = effective_global
|
chosen_name = effective_global
|
||||||
source = PrecedenceSource.GLOBAL
|
source = PrecedenceSource.GLOBAL
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Automation profile precedence chain resolved: "
|
"Automation profile precedence chain resolved: "
|
||||||
"name=%r source=%s plan=%r action=%r global=%r",
|
"name=%r source=%s plan=%r action=%r project=%r global=%r",
|
||||||
chosen_name,
|
chosen_name,
|
||||||
source.value,
|
source.value,
|
||||||
norm_plan,
|
norm_plan,
|
||||||
norm_action,
|
norm_action,
|
||||||
|
norm_project,
|
||||||
effective_global,
|
effective_global,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -217,5 +253,6 @@ def resolve_precedence_chain(
|
|||||||
profile_name=chosen_name,
|
profile_name=chosen_name,
|
||||||
plan_profile=norm_plan,
|
plan_profile=norm_plan,
|
||||||
action_profile=norm_action,
|
action_profile=norm_action,
|
||||||
|
project_profile=norm_project,
|
||||||
global_profile=effective_global,
|
global_profile=effective_global,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user