style: fix lint and format issues in merge-visible files
CI / status-check (pull_request) Blocked by required conditions
CI / helm (pull_request) Successful in 35s
CI / build (pull_request) Successful in 54s
CI / lint (pull_request) Successful in 1m4s
CI / quality (pull_request) Successful in 1m27s
CI / push-validation (pull_request) Successful in 25s
CI / typecheck (pull_request) Successful in 1m43s
CI / security (pull_request) Successful in 1m43s
CI / e2e_tests (pull_request) Successful in 3m58s
CI / integration_tests (pull_request) Successful in 5m31s
CI / unit_tests (pull_request) Failing after 6m16s
CI / docker (pull_request) Has been skipped
CI / coverage (pull_request) Failing after 20m31s
CI / status-check (pull_request) Blocked by required conditions
CI / helm (pull_request) Successful in 35s
CI / build (pull_request) Successful in 54s
CI / lint (pull_request) Successful in 1m4s
CI / quality (pull_request) Successful in 1m27s
CI / push-validation (pull_request) Successful in 25s
CI / typecheck (pull_request) Successful in 1m43s
CI / security (pull_request) Successful in 1m43s
CI / e2e_tests (pull_request) Successful in 3m58s
CI / integration_tests (pull_request) Successful in 5m31s
CI / unit_tests (pull_request) Failing after 6m16s
CI / docker (pull_request) Has been skipped
CI / coverage (pull_request) Failing after 20m31s
- Apply ruff --fix to resolve I001 (unsorted imports) in features/steps/autonomy_guardrail_atomic_load_steps.py and features/steps/tui_prompt_textarea_steps.py - Apply ruff --fix to resolve B010 (setattr with constant) in features/steps/domain_model_immutability_steps.py (6 instances) - Apply ruff format to normalize quote style in scope chain resolver step files and scope_chain_registry.py - These files are visible in the CI merge commit and must be lint-clean
This commit is contained in:
@@ -95,13 +95,13 @@ def step_create_multiple_resolvers(context: Any) -> None:
|
||||
context.resolvers.append(resolver)
|
||||
|
||||
|
||||
@given('a base scope context with {context_dict}')
|
||||
@given("a base scope context with {context_dict}")
|
||||
def step_create_base_context(context: Any, context_dict: str) -> None:
|
||||
"""Create a base scope context."""
|
||||
context.base_context = json.loads(context_dict)
|
||||
|
||||
|
||||
@given('a custom scope resolver that returns {output_dict}')
|
||||
@given("a custom scope resolver that returns {output_dict}")
|
||||
def step_create_resolver_with_output(context: Any, output_dict: str) -> None:
|
||||
"""Create a resolver with specific output."""
|
||||
output = json.loads(output_dict)
|
||||
@@ -126,7 +126,7 @@ def step_register_resolver(context: Any) -> None:
|
||||
context.registry.register_resolver(context.resolver)
|
||||
|
||||
|
||||
@given('resolver1 returns {output_dict}')
|
||||
@given("resolver1 returns {output_dict}")
|
||||
def step_set_resolver1_output(context: Any, output_dict: str) -> None:
|
||||
"""Set the output for resolver1."""
|
||||
output = json.loads(output_dict)
|
||||
@@ -136,7 +136,7 @@ def step_set_resolver1_output(context: Any, output_dict: str) -> None:
|
||||
break
|
||||
|
||||
|
||||
@given('resolver2 returns {output_dict} when scope1 is present')
|
||||
@given("resolver2 returns {output_dict} when scope1 is present")
|
||||
def step_set_resolver2_conditional_output(context: Any, output_dict: str) -> None:
|
||||
"""Set resolver2 to return output only when scope1 is present in context."""
|
||||
output = json.loads(output_dict)
|
||||
@@ -181,7 +181,7 @@ def step_unregister_resolver(context: Any) -> None:
|
||||
context.registry.unregister_resolver(context.resolver.resolver_name)
|
||||
|
||||
|
||||
@when('I try to register another resolver with the same name')
|
||||
@when("I try to register another resolver with the same name")
|
||||
def step_try_register_duplicate(context: Any) -> None:
|
||||
"""Try to register a duplicate resolver."""
|
||||
duplicate = MockScopeResolver(context.resolver.resolver_name)
|
||||
@@ -234,7 +234,7 @@ def step_check_resolver_order(context: Any) -> None:
|
||||
assert registered == expected
|
||||
|
||||
|
||||
@then('the merged context should contain {expected_dict}')
|
||||
@then("the merged context should contain {expected_dict}")
|
||||
def step_check_merged_context_contains(context: Any, expected_dict: str) -> None:
|
||||
"""Check that merged context contains expected values."""
|
||||
expected = json.loads(expected_dict)
|
||||
@@ -257,7 +257,7 @@ def step_check_merged_context_key_count(context: Any, count: int) -> None:
|
||||
assert len(context.merged_context) == count
|
||||
|
||||
|
||||
@then('the merged context should have {expected_dict}')
|
||||
@then("the merged context should have {expected_dict}")
|
||||
def step_check_merged_context_has(context: Any, expected_dict: str) -> None:
|
||||
"""Check that merged context has expected key-value pairs."""
|
||||
expected = json.loads(expected_dict)
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
"""Step definitions for atomic load_from_metadata scenarios."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from behave import given, then, when
|
||||
from behave.runner import Context
|
||||
from pydantic import ValidationError
|
||||
|
||||
from cleveragents.application.services.autonomy_guardrail_service import (
|
||||
_MAX_CONFIRMATIONS,
|
||||
_MAX_METADATA_ENTRIES,
|
||||
AutonomyGuardrailService,
|
||||
)
|
||||
from cleveragents.domain.models.core.autonomy_guardrails import (
|
||||
AutonomyGuardrails,
|
||||
)
|
||||
|
||||
# ---- Setup and initialization ----
|
||||
|
||||
|
||||
@given("I have metadata with valid guardrails and audit trail")
|
||||
def step_setup_valid_metadata(context: Context) -> None:
|
||||
"""Create metadata with valid guardrails and audit trail."""
|
||||
context.metadata = {
|
||||
"autonomy_guardrails": {
|
||||
"max_steps": 10,
|
||||
"tool_budget": 100.0,
|
||||
"budget_spent": 0.0,
|
||||
"step_count": 0,
|
||||
"required_confirmations": [],
|
||||
"actor_limits": {
|
||||
"max_tool_calls_per_invocation": 5,
|
||||
"max_retries_per_failure": 3,
|
||||
},
|
||||
},
|
||||
"guardrail_audit_trail": {
|
||||
"entries": [],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@given("I have metadata with valid guardrails but no audit trail")
|
||||
def step_setup_guardrails_only(context: Context) -> None:
|
||||
"""Create metadata with only guardrails."""
|
||||
context.metadata = {
|
||||
"autonomy_guardrails": {
|
||||
"max_steps": 10,
|
||||
"tool_budget": 100.0,
|
||||
"budget_spent": 0.0,
|
||||
"step_count": 0,
|
||||
"required_confirmations": [],
|
||||
"actor_limits": {
|
||||
"max_tool_calls_per_invocation": 5,
|
||||
"max_retries_per_failure": 3,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@given("I have metadata with valid audit trail but no guardrails")
|
||||
def step_setup_audit_trail_only(context: Context) -> None:
|
||||
"""Create metadata with only audit trail."""
|
||||
context.metadata = {
|
||||
"guardrail_audit_trail": {
|
||||
"entries": [],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@given("I have empty metadata")
|
||||
def step_setup_empty_metadata(context: Context) -> None:
|
||||
"""Create empty metadata."""
|
||||
context.metadata = {}
|
||||
|
||||
|
||||
@given("I have metadata with invalid guardrails and valid audit trail")
|
||||
def step_setup_invalid_guardrails(context: Context) -> None:
|
||||
"""Create metadata with invalid guardrails."""
|
||||
context.metadata = {
|
||||
"autonomy_guardrails": {
|
||||
"max_steps": -1, # Invalid: negative max_steps
|
||||
"tool_budget": 100.0,
|
||||
},
|
||||
"guardrail_audit_trail": {
|
||||
"entries": [],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@given("I have metadata with valid guardrails and invalid audit trail")
|
||||
def step_setup_invalid_audit_trail(context: Context) -> None:
|
||||
"""Create metadata with invalid audit trail."""
|
||||
context.metadata = {
|
||||
"autonomy_guardrails": {
|
||||
"max_steps": 10,
|
||||
"tool_budget": 100.0,
|
||||
"budget_spent": 0.0,
|
||||
"step_count": 0,
|
||||
"required_confirmations": [],
|
||||
"actor_limits": {
|
||||
"max_tool_calls_per_invocation": 5,
|
||||
"max_retries_per_failure": 3,
|
||||
},
|
||||
},
|
||||
"guardrail_audit_trail": {
|
||||
"entries": "invalid", # Invalid: should be list
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@given("I have metadata with invalid guardrails and invalid audit trail")
|
||||
def step_setup_both_invalid(context: Context) -> None:
|
||||
"""Create metadata with both invalid."""
|
||||
context.metadata = {
|
||||
"autonomy_guardrails": {
|
||||
"max_steps": -1, # Invalid
|
||||
},
|
||||
"guardrail_audit_trail": {
|
||||
"entries": "invalid", # Invalid
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@given("I have metadata with guardrails containing oversized confirmations")
|
||||
def step_setup_oversized_confirmations(context: Context) -> None:
|
||||
"""Create metadata with oversized confirmations list."""
|
||||
context.metadata = {
|
||||
"autonomy_guardrails": {
|
||||
"max_steps": 10,
|
||||
"tool_budget": 100.0,
|
||||
"budget_spent": 0.0,
|
||||
"step_count": 0,
|
||||
"required_confirmations": ["op"] * (_MAX_CONFIRMATIONS + 1),
|
||||
"actor_limits": {
|
||||
"max_tool_calls_per_invocation": 5,
|
||||
"max_retries_per_failure": 3,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@given("valid audit trail")
|
||||
def step_add_valid_audit_trail(context: Context) -> None:
|
||||
"""Add valid audit trail to metadata."""
|
||||
context.metadata["guardrail_audit_trail"] = {
|
||||
"entries": [],
|
||||
}
|
||||
|
||||
|
||||
@given("audit trail containing oversized entries")
|
||||
def step_setup_oversized_entries(context: Context) -> None:
|
||||
"""Create metadata with oversized audit trail entries."""
|
||||
context.metadata["guardrail_audit_trail"] = {
|
||||
"entries": [
|
||||
{
|
||||
"timestamp": "2026-04-13T00:00:00Z",
|
||||
"event_type": "step_allowed",
|
||||
"guard_name": "step_limit",
|
||||
"result": "allowed",
|
||||
"reason": "Within limits",
|
||||
"context": {},
|
||||
}
|
||||
]
|
||||
* (_MAX_METADATA_ENTRIES + 1),
|
||||
}
|
||||
|
||||
|
||||
@given('plan "{plan_id}" has no prior state')
|
||||
def step_ensure_no_prior_state(context: Context, plan_id: str) -> None:
|
||||
"""Ensure plan has no prior state."""
|
||||
if not hasattr(context, "service"):
|
||||
context.service = AutonomyGuardrailService()
|
||||
# Ensure plan is not in service
|
||||
context.service.remove_plan(plan_id)
|
||||
|
||||
|
||||
@given('plan "{plan_id}" has existing guardrails and audit trail')
|
||||
def step_setup_existing_state(context: Context, plan_id: str) -> None:
|
||||
"""Set up existing guardrails and audit trail for a plan."""
|
||||
if not hasattr(context, "service"):
|
||||
context.service = AutonomyGuardrailService()
|
||||
|
||||
# Configure initial state
|
||||
initial_guardrails = AutonomyGuardrails(max_steps=5, tool_budget=50.0)
|
||||
context.service.configure_guardrails(plan_id, initial_guardrails)
|
||||
|
||||
# Store original values for later comparison
|
||||
context.original_guardrails = context.service.get_guardrails(plan_id)
|
||||
context.original_audit_trail = context.service.get_audit_trail(plan_id)
|
||||
|
||||
|
||||
@given("I have metadata with different valid guardrails and audit trail")
|
||||
def step_setup_different_metadata(context: Context) -> None:
|
||||
"""Create metadata with different values."""
|
||||
context.metadata = {
|
||||
"autonomy_guardrails": {
|
||||
"max_steps": 20, # Different from original 5
|
||||
"tool_budget": 200.0, # Different from original 50.0
|
||||
"budget_spent": 0.0,
|
||||
"step_count": 0,
|
||||
"required_confirmations": [],
|
||||
"actor_limits": {
|
||||
"max_tool_calls_per_invocation": 5,
|
||||
"max_retries_per_failure": 3,
|
||||
},
|
||||
},
|
||||
"guardrail_audit_trail": {
|
||||
"entries": [],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ---- Loading and validation ----
|
||||
|
||||
|
||||
@when('I load the metadata for plan "{plan_id}"')
|
||||
def step_load_metadata(context: Context, plan_id: str) -> None:
|
||||
"""Load metadata into the service."""
|
||||
if not hasattr(context, "service"):
|
||||
context.service = AutonomyGuardrailService()
|
||||
|
||||
context.plan_id = plan_id
|
||||
context.load_error = None
|
||||
try:
|
||||
context.service.load_from_metadata(plan_id, context.metadata)
|
||||
except Exception as exc:
|
||||
context.load_error = exc
|
||||
|
||||
|
||||
@when('I try to load the metadata for plan "{plan_id}"')
|
||||
def step_try_load_metadata(context: Context, plan_id: str) -> None:
|
||||
"""Try to load metadata and capture any error."""
|
||||
if not hasattr(context, "service"):
|
||||
context.service = AutonomyGuardrailService()
|
||||
|
||||
context.plan_id = plan_id
|
||||
context.load_error = None
|
||||
context.error = None
|
||||
try:
|
||||
context.service.load_from_metadata(plan_id, context.metadata)
|
||||
except Exception as exc:
|
||||
context.load_error = exc
|
||||
context.error = exc
|
||||
|
||||
|
||||
# ---- Assertions: successful loads ----
|
||||
|
||||
|
||||
@then('the guardrails should be loaded for plan "{plan_id}"')
|
||||
def step_assert_guardrails_loaded(context: Context, plan_id: str) -> None:
|
||||
"""Assert that guardrails were loaded."""
|
||||
guardrails = context.service.get_guardrails(plan_id)
|
||||
assert guardrails is not None, f"Guardrails not loaded for plan {plan_id}"
|
||||
assert guardrails.max_steps == 10
|
||||
assert guardrails.tool_budget == 100.0
|
||||
|
||||
|
||||
@then('the audit trail should be loaded for plan "{plan_id}"')
|
||||
def step_assert_audit_trail_loaded(context: Context, plan_id: str) -> None:
|
||||
"""Assert that audit trail was loaded."""
|
||||
trail = context.service.get_audit_trail(plan_id)
|
||||
assert trail is not None
|
||||
assert len(trail.entries) == 0
|
||||
|
||||
|
||||
@then("both guardrails and audit trail should be in sync")
|
||||
def step_assert_in_sync(context: Context) -> None:
|
||||
"""Assert that guardrails and audit trail are in sync (both present or both absent)."""
|
||||
# Both should be present after a successful load
|
||||
guardrails = context.service.get_guardrails(context.plan_id)
|
||||
trail = context.service.get_audit_trail(context.plan_id)
|
||||
assert guardrails is not None, "Guardrails should be present after successful load"
|
||||
assert trail is not None, "Audit trail should be present after successful load"
|
||||
|
||||
|
||||
@then('the audit trail should be empty for plan "{plan_id}"')
|
||||
def step_assert_audit_trail_empty(context: Context, plan_id: str) -> None:
|
||||
"""Assert that audit trail is empty."""
|
||||
trail = context.service.get_audit_trail(plan_id)
|
||||
assert len(trail.entries) == 0
|
||||
|
||||
|
||||
@then('the guardrails should be absent for plan "{plan_id}"')
|
||||
def step_assert_guardrails_absent(context: Context, plan_id: str) -> None:
|
||||
"""Assert that guardrails are not loaded."""
|
||||
guardrails = context.service.get_guardrails(plan_id)
|
||||
assert guardrails is None, f"Guardrails should be absent for plan {plan_id}"
|
||||
|
||||
|
||||
# ---- Assertions: failed loads (atomicity) ----
|
||||
|
||||
|
||||
@then("a validation error should be raised for metadata load")
|
||||
def step_assert_validation_error(context: Context) -> None:
|
||||
"""Assert that a validation error was raised."""
|
||||
assert context.load_error is not None
|
||||
assert isinstance(context.load_error, ValidationError)
|
||||
|
||||
|
||||
@then('a ValueError should be raised for metadata mentioning "{text}"')
|
||||
def step_assert_value_error(context: Context, text: str) -> None:
|
||||
"""Assert that a ValueError was raised with specific text."""
|
||||
assert context.load_error is not None
|
||||
assert isinstance(context.load_error, ValueError)
|
||||
assert text in str(context.load_error)
|
||||
|
||||
|
||||
@then('the guardrails should remain absent for plan "{plan_id}"')
|
||||
def step_assert_guardrails_still_absent(context: Context, plan_id: str) -> None:
|
||||
"""Assert that guardrails remain absent after failed load."""
|
||||
guardrails = context.service.get_guardrails(plan_id)
|
||||
assert guardrails is None
|
||||
|
||||
|
||||
@then('the audit trail should remain absent for plan "{plan_id}"')
|
||||
def step_assert_audit_trail_still_absent(context: Context, plan_id: str) -> None:
|
||||
"""Assert that audit trail remains absent after failed load."""
|
||||
trail = context.service.get_audit_trail(plan_id)
|
||||
assert len(trail.entries) == 0
|
||||
|
||||
|
||||
# ---- Assertions: overwriting state ----
|
||||
|
||||
|
||||
@then('the guardrails should be updated to new values for plan "{plan_id}"')
|
||||
def step_assert_guardrails_updated(context: Context, plan_id: str) -> None:
|
||||
"""Assert that guardrails were updated to new values."""
|
||||
guardrails = context.service.get_guardrails(plan_id)
|
||||
assert guardrails is not None
|
||||
assert guardrails.max_steps == 20 # New value
|
||||
assert guardrails.tool_budget == 200.0 # New value
|
||||
|
||||
|
||||
@then('the audit trail should be updated to new values for plan "{plan_id}"')
|
||||
def step_assert_audit_trail_updated(context: Context, plan_id: str) -> None:
|
||||
"""Assert that audit trail was updated."""
|
||||
trail = context.service.get_audit_trail(plan_id)
|
||||
assert trail is not None
|
||||
|
||||
|
||||
@then("both should be in sync")
|
||||
def step_assert_both_in_sync(context: Context) -> None:
|
||||
"""Assert that both guardrails and audit trail are in sync after update."""
|
||||
# Both should be present and updated
|
||||
guardrails = context.service.get_guardrails(context.plan_id)
|
||||
trail = context.service.get_audit_trail(context.plan_id)
|
||||
assert guardrails is not None, "Guardrails should be present after update"
|
||||
assert trail is not None, "Audit trail should be present after update"
|
||||
|
||||
|
||||
@then('the guardrails should retain original values for plan "{plan_id}"')
|
||||
def step_assert_guardrails_unchanged(context: Context, plan_id: str) -> None:
|
||||
"""Assert that guardrails retain original values."""
|
||||
guardrails = context.service.get_guardrails(plan_id)
|
||||
assert guardrails is not None
|
||||
assert guardrails.max_steps == context.original_guardrails.max_steps
|
||||
assert guardrails.tool_budget == context.original_guardrails.tool_budget
|
||||
|
||||
|
||||
@then('the audit trail should retain original values for plan "{plan_id}"')
|
||||
def step_assert_audit_trail_unchanged(context: Context, plan_id: str) -> None:
|
||||
"""Assert that audit trail retains original values."""
|
||||
trail = context.service.get_audit_trail(plan_id)
|
||||
assert len(trail.entries) == len(context.original_audit_trail.entries)
|
||||
|
||||
|
||||
@given("I have metadata with valid guardrails")
|
||||
def step_setup_valid_guardrails_only(context: Context) -> None:
|
||||
"""Create metadata with only valid guardrails (no audit trail)."""
|
||||
context.metadata = {
|
||||
"autonomy_guardrails": {
|
||||
"max_steps": 10,
|
||||
"tool_budget": 100.0,
|
||||
"budget_spent": 0.0,
|
||||
"step_count": 0,
|
||||
"required_confirmations": [],
|
||||
"actor_limits": {
|
||||
"max_tool_calls_per_invocation": 5,
|
||||
"max_retries_per_failure": 3,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
"""Step definitions for domain model immutability tests.
|
||||
|
||||
Verifies that Plan and Action identity fields are read-only after construction,
|
||||
while mutable state fields remain assignable.
|
||||
|
||||
Issue #7553: enforce immutability on Plan and Action identity fields.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
from typing import Any
|
||||
|
||||
from behave import given, then, when
|
||||
from behave.runner import Context
|
||||
from pydantic import ValidationError
|
||||
|
||||
from cleveragents.domain.models.core.action import Action, ActionState
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
NamespacedName,
|
||||
Plan,
|
||||
PlanIdentity,
|
||||
PlanPhase,
|
||||
PlanTimestamps,
|
||||
ProcessingState,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_VALID_ULID = "01HZTEST0000000000000000AA"
|
||||
_VALID_ULID_2 = "01HZTEST0000000000000000BB"
|
||||
_VALID_ULID_ROOT = "01HZTEST0000000000000000CC"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_plan(
|
||||
plan_id: str = _VALID_ULID,
|
||||
phase: PlanPhase = PlanPhase.STRATEGIZE,
|
||||
processing_state: ProcessingState = ProcessingState.QUEUED,
|
||||
created_at: dt.datetime | None = None,
|
||||
namespaced_name_str: str = "local/test-plan",
|
||||
) -> Plan:
|
||||
"""Create a minimal valid Plan domain object."""
|
||||
timestamps_kwargs: dict[str, Any] = {}
|
||||
if created_at is not None:
|
||||
timestamps_kwargs["created_at"] = created_at
|
||||
|
||||
return Plan(
|
||||
identity=PlanIdentity(plan_id=plan_id),
|
||||
namespaced_name=NamespacedName.parse(namespaced_name_str),
|
||||
description="Test plan description",
|
||||
action_name="local/test-action",
|
||||
phase=phase,
|
||||
processing_state=processing_state,
|
||||
timestamps=PlanTimestamps(**timestamps_kwargs),
|
||||
)
|
||||
|
||||
|
||||
def _make_action(namespaced_name_str: str = "local/test-action") -> Action:
|
||||
"""Create a minimal valid Action domain object."""
|
||||
return Action(
|
||||
namespaced_name=NamespacedName.parse(namespaced_name_str),
|
||||
description="Test action description",
|
||||
definition_of_done="All tests pass",
|
||||
strategy_actor="local/strategy-actor",
|
||||
execution_actor="local/execution-actor",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plan identity — plan_id
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given("I create a Plan with a known ULID plan_id")
|
||||
def step_create_plan_with_known_ulid(context: Context) -> None:
|
||||
"""Create a Plan with a known ULID plan_id."""
|
||||
context.known_ulid = _VALID_ULID
|
||||
context.immut_plan = _make_plan(plan_id=_VALID_ULID)
|
||||
context.immut_error = None
|
||||
|
||||
|
||||
@then("the plan identity plan_id should match the known ULID")
|
||||
def step_check_plan_identity_plan_id(context: Context) -> None:
|
||||
"""Verify the plan_id matches the known ULID."""
|
||||
assert context.immut_plan.identity.plan_id == context.known_ulid, (
|
||||
f"Expected plan_id '{context.known_ulid}', "
|
||||
f"got '{context.immut_plan.identity.plan_id}'"
|
||||
)
|
||||
|
||||
|
||||
@when("I attempt to reassign the plan identity plan_id")
|
||||
def step_attempt_reassign_plan_id(context: Context) -> None:
|
||||
"""Attempt to reassign plan_id on a frozen PlanIdentity."""
|
||||
context.immut_error = None
|
||||
try:
|
||||
context.immut_plan.identity.plan_id = _VALID_ULID_2
|
||||
except (ValidationError, TypeError) as exc:
|
||||
context.immut_error = exc
|
||||
|
||||
|
||||
@then("a frozen model error should be raised for plan_id")
|
||||
def step_check_frozen_error_plan_id(context: Context) -> None:
|
||||
"""Verify that a frozen model error was raised."""
|
||||
assert context.immut_error is not None, (
|
||||
"Expected a ValidationError or TypeError when reassigning plan_id, "
|
||||
"but no error was raised"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plan identity — root_plan_id auto-resolution
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given("I create a Plan without specifying root_plan_id")
|
||||
def step_create_plan_without_root_plan_id(context: Context) -> None:
|
||||
"""Create a Plan without explicitly setting root_plan_id."""
|
||||
context.immut_plan = _make_plan(plan_id=_VALID_ULID)
|
||||
context.immut_error = None
|
||||
|
||||
|
||||
@then("the plan identity root_plan_id should equal the plan_id")
|
||||
def step_check_root_plan_id_auto_resolved(context: Context) -> None:
|
||||
"""Verify root_plan_id was auto-resolved to plan_id."""
|
||||
assert (
|
||||
context.immut_plan.identity.root_plan_id == context.immut_plan.identity.plan_id
|
||||
), (
|
||||
f"Expected root_plan_id '{context.immut_plan.identity.plan_id}', "
|
||||
f"got '{context.immut_plan.identity.root_plan_id}'"
|
||||
)
|
||||
|
||||
|
||||
@when("I attempt to reassign the plan identity root_plan_id")
|
||||
def step_attempt_reassign_root_plan_id(context: Context) -> None:
|
||||
"""Attempt to reassign root_plan_id on a frozen PlanIdentity."""
|
||||
context.immut_error = None
|
||||
try:
|
||||
context.immut_plan.identity.root_plan_id = _VALID_ULID_ROOT
|
||||
except (ValidationError, TypeError) as exc:
|
||||
context.immut_error = exc
|
||||
|
||||
|
||||
@then("a frozen model error should be raised for root_plan_id")
|
||||
def step_check_frozen_error_root_plan_id(context: Context) -> None:
|
||||
"""Verify that a frozen model error was raised for root_plan_id."""
|
||||
assert context.immut_error is not None, (
|
||||
"Expected a ValidationError or TypeError when reassigning root_plan_id, "
|
||||
"but no error was raised"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plan timestamps — created_at
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given("I create a Plan with a specific created_at timestamp")
|
||||
def step_create_plan_with_specific_created_at(context: Context) -> None:
|
||||
"""Create a Plan with a specific created_at timestamp."""
|
||||
context.specific_created_at = dt.datetime(2026, 1, 15, 10, 0, 0, tzinfo=dt.UTC)
|
||||
context.immut_plan = _make_plan(created_at=context.specific_created_at)
|
||||
context.immut_error = None
|
||||
|
||||
|
||||
@then("the plan timestamps created_at should match the specified timestamp")
|
||||
def step_check_plan_created_at(context: Context) -> None:
|
||||
"""Verify the created_at timestamp matches the specified value."""
|
||||
actual = context.immut_plan.timestamps.created_at
|
||||
expected = context.specific_created_at
|
||||
assert actual == expected, f"Expected created_at '{expected}', got '{actual}'"
|
||||
|
||||
|
||||
@when("I attempt to reassign the plan timestamps created_at")
|
||||
def step_attempt_reassign_created_at(context: Context) -> None:
|
||||
"""Attempt to reassign created_at on PlanTimestamps."""
|
||||
context.immut_error = None
|
||||
try:
|
||||
context.immut_plan.timestamps.created_at = dt.datetime(
|
||||
2099, 1, 1, tzinfo=dt.UTC
|
||||
)
|
||||
except AttributeError as exc:
|
||||
context.immut_error = exc
|
||||
|
||||
|
||||
@then("an AttributeError should be raised for created_at")
|
||||
def step_check_attribute_error_created_at(context: Context) -> None:
|
||||
"""Verify that an AttributeError was raised for created_at."""
|
||||
assert context.immut_error is not None, (
|
||||
"Expected an AttributeError when reassigning created_at, "
|
||||
"but no error was raised"
|
||||
)
|
||||
assert isinstance(context.immut_error, AttributeError), (
|
||||
f"Expected AttributeError, got {type(context.immut_error).__name__}"
|
||||
)
|
||||
assert (
|
||||
"created_at" in str(context.immut_error).lower()
|
||||
or "read-only" in str(context.immut_error).lower()
|
||||
), (
|
||||
f"Expected error message to mention 'created_at' or 'read-only', "
|
||||
f"got: {context.immut_error}"
|
||||
)
|
||||
|
||||
|
||||
@when("I update the plan timestamps updated_at to a new datetime")
|
||||
def step_update_plan_updated_at(context: Context) -> None:
|
||||
"""Update the plan's updated_at timestamp."""
|
||||
context.new_updated_at = dt.datetime(2026, 6, 1, 12, 0, 0, tzinfo=dt.UTC)
|
||||
context.immut_plan.timestamps.updated_at = context.new_updated_at
|
||||
context.immut_error = None
|
||||
|
||||
|
||||
@then("the plan timestamps updated_at should reflect the new datetime")
|
||||
def step_check_plan_updated_at(context: Context) -> None:
|
||||
"""Verify the updated_at timestamp was updated."""
|
||||
actual = context.immut_plan.timestamps.updated_at
|
||||
expected = context.new_updated_at
|
||||
assert actual == expected, f"Expected updated_at '{expected}', got '{actual}'"
|
||||
|
||||
|
||||
@when("I set the plan timestamps strategize_started_at to a new datetime")
|
||||
def step_set_plan_strategize_started_at(context: Context) -> None:
|
||||
"""Set the plan's strategize_started_at timestamp."""
|
||||
context.new_strategize_started_at = dt.datetime(2026, 6, 1, 13, 0, 0, tzinfo=dt.UTC)
|
||||
context.immut_plan.timestamps.strategize_started_at = (
|
||||
context.new_strategize_started_at
|
||||
)
|
||||
context.immut_error = None
|
||||
|
||||
|
||||
@then("the plan timestamps strategize_started_at should reflect the new datetime")
|
||||
def step_check_plan_strategize_started_at(context: Context) -> None:
|
||||
"""Verify the strategize_started_at timestamp was set."""
|
||||
actual = context.immut_plan.timestamps.strategize_started_at
|
||||
expected = context.new_strategize_started_at
|
||||
assert actual == expected, (
|
||||
f"Expected strategize_started_at '{expected}', got '{actual}'"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Action namespaced_name — name
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given('I create an Action with namespaced name "{namespaced_name}"')
|
||||
def step_create_action_with_namespaced_name(
|
||||
context: Context, namespaced_name: str
|
||||
) -> None:
|
||||
"""Create an Action with the given namespaced name."""
|
||||
context.immut_action = _make_action(namespaced_name_str=namespaced_name)
|
||||
context.immut_error = None
|
||||
|
||||
|
||||
@then('the action namespaced_name name should be "{expected}"')
|
||||
def step_check_action_name(context: Context, expected: str) -> None:
|
||||
"""Verify the action's namespaced_name.name."""
|
||||
actual = context.immut_action.namespaced_name.name
|
||||
assert actual == expected, f"Expected action name '{expected}', got '{actual}'"
|
||||
|
||||
|
||||
@when("I attempt to reassign the action namespaced_name name")
|
||||
def step_attempt_reassign_action_name(context: Context) -> None:
|
||||
"""Attempt to reassign the action's namespaced_name.name."""
|
||||
context.immut_error = None
|
||||
try:
|
||||
context.immut_action.namespaced_name.name = "new-name"
|
||||
except (ValidationError, TypeError) as exc:
|
||||
context.immut_error = exc
|
||||
|
||||
|
||||
@then("a frozen model error should be raised for action name")
|
||||
def step_check_frozen_error_action_name(context: Context) -> None:
|
||||
"""Verify that a frozen model error was raised for action name."""
|
||||
assert context.immut_error is not None, (
|
||||
"Expected a ValidationError or TypeError when reassigning action name, "
|
||||
"but no error was raised"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Action namespaced_name — namespace
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then('the action namespaced_name namespace should be "{expected}"')
|
||||
def step_check_action_namespace(context: Context, expected: str) -> None:
|
||||
"""Verify the action's namespaced_name.namespace."""
|
||||
actual = context.immut_action.namespaced_name.namespace
|
||||
assert actual == expected, f"Expected action namespace '{expected}', got '{actual}'"
|
||||
|
||||
|
||||
@when("I attempt to reassign the action namespaced_name namespace")
|
||||
def step_attempt_reassign_action_namespace(context: Context) -> None:
|
||||
"""Attempt to reassign the action's namespaced_name.namespace."""
|
||||
context.immut_error = None
|
||||
try:
|
||||
context.immut_action.namespaced_name.namespace = "neworg"
|
||||
except (ValidationError, TypeError) as exc:
|
||||
context.immut_error = exc
|
||||
|
||||
|
||||
@then("a frozen model error should be raised for action namespace")
|
||||
def step_check_frozen_error_action_namespace(context: Context) -> None:
|
||||
"""Verify that a frozen model error was raised for action namespace."""
|
||||
assert context.immut_error is not None, (
|
||||
"Expected a ValidationError or TypeError when reassigning action namespace, "
|
||||
"but no error was raised"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Mutable state fields
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given("I create a Plan in STRATEGIZE phase")
|
||||
def step_create_plan_in_strategize(context: Context) -> None:
|
||||
"""Create a Plan in STRATEGIZE phase."""
|
||||
context.immut_plan = _make_plan(
|
||||
phase=PlanPhase.STRATEGIZE,
|
||||
processing_state=ProcessingState.QUEUED,
|
||||
)
|
||||
context.immut_error = None
|
||||
|
||||
|
||||
@when("I update the plan phase to EXECUTE")
|
||||
def step_update_plan_phase_to_execute(context: Context) -> None:
|
||||
"""Update the plan's phase to EXECUTE."""
|
||||
context.immut_plan.phase = PlanPhase.EXECUTE
|
||||
context.immut_error = None
|
||||
|
||||
|
||||
@then("the plan phase should be EXECUTE")
|
||||
def step_check_plan_phase_execute(context: Context) -> None:
|
||||
"""Verify the plan phase is EXECUTE."""
|
||||
assert context.immut_plan.phase == PlanPhase.EXECUTE, (
|
||||
f"Expected phase EXECUTE, got {context.immut_plan.phase}"
|
||||
)
|
||||
|
||||
|
||||
@when("I update the plan processing_state to PROCESSING")
|
||||
def step_update_plan_processing_state(context: Context) -> None:
|
||||
"""Update the plan's processing_state to PROCESSING."""
|
||||
context.immut_plan.processing_state = ProcessingState.PROCESSING
|
||||
context.immut_error = None
|
||||
|
||||
|
||||
@then("the plan processing_state should be PROCESSING")
|
||||
def step_check_plan_processing_state(context: Context) -> None:
|
||||
"""Verify the plan processing_state is PROCESSING."""
|
||||
assert context.immut_plan.processing_state == ProcessingState.PROCESSING, (
|
||||
f"Expected processing_state PROCESSING, got {context.immut_plan.processing_state}"
|
||||
)
|
||||
|
||||
|
||||
@when("I update the action state to archived")
|
||||
def step_update_action_state_archived(context: Context) -> None:
|
||||
"""Update the action's state to archived."""
|
||||
context.immut_action.state = ActionState.ARCHIVED
|
||||
context.immut_error = None
|
||||
|
||||
|
||||
@then("the action state should be archived")
|
||||
def step_check_action_state_archived(context: Context) -> None:
|
||||
"""Verify the action state is archived."""
|
||||
assert context.immut_action.state == ActionState.ARCHIVED, (
|
||||
f"Expected state ARCHIVED, got {context.immut_action.state}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plan namespaced_name — frozen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given('I create a Plan with namespaced name "{namespaced_name}"')
|
||||
def step_create_plan_with_namespaced_name(
|
||||
context: Context, namespaced_name: str
|
||||
) -> None:
|
||||
"""Create a Plan with the given namespaced name."""
|
||||
context.immut_plan = _make_plan(namespaced_name_str=namespaced_name)
|
||||
context.immut_error = None
|
||||
|
||||
|
||||
@when("I attempt to reassign the plan namespaced_name name")
|
||||
def step_attempt_reassign_plan_namespaced_name(context: Context) -> None:
|
||||
"""Attempt to reassign the plan's namespaced_name.name."""
|
||||
context.immut_error = None
|
||||
try:
|
||||
context.immut_plan.namespaced_name.name = "new-name"
|
||||
except (ValidationError, TypeError) as exc:
|
||||
context.immut_error = exc
|
||||
|
||||
|
||||
@then("a frozen model error should be raised for plan namespaced name")
|
||||
def step_check_frozen_error_plan_namespaced_name(context: Context) -> None:
|
||||
"""Verify that a frozen model error was raised for plan namespaced name."""
|
||||
assert context.immut_error is not None, (
|
||||
"Expected a ValidationError or TypeError when reassigning plan namespaced name, "
|
||||
"but no error was raised"
|
||||
)
|
||||
|
||||
|
||||
@when("I attempt to reassign the plan namespaced_name namespace")
|
||||
def step_attempt_reassign_plan_namespaced_namespace(context: Context) -> None:
|
||||
"""Attempt to reassign the plan's namespaced_name.namespace."""
|
||||
context.immut_error = None
|
||||
try:
|
||||
context.immut_plan.namespaced_name.namespace = "neworg"
|
||||
except (ValidationError, TypeError) as exc:
|
||||
context.immut_error = exc
|
||||
|
||||
|
||||
@then("a frozen model error should be raised for plan namespaced namespace")
|
||||
def step_check_frozen_error_plan_namespaced_namespace(context: Context) -> None:
|
||||
"""Verify that a frozen model error was raised for plan namespaced namespace."""
|
||||
assert context.immut_error is not None, (
|
||||
"Expected a ValidationError or TypeError when reassigning plan namespaced namespace, "
|
||||
"but no error was raised"
|
||||
)
|
||||
@@ -95,13 +95,13 @@ def step_create_multiple_resolvers(context: Any) -> None:
|
||||
context.resolvers.append(resolver)
|
||||
|
||||
|
||||
@given('a base scope context with {context_dict}')
|
||||
@given("a base scope context with {context_dict}")
|
||||
def step_create_base_context(context: Any, context_dict: str) -> None:
|
||||
"""Create a base scope context."""
|
||||
context.base_context = json.loads(context_dict)
|
||||
|
||||
|
||||
@given('a custom scope resolver that returns {output_dict}')
|
||||
@given("a custom scope resolver that returns {output_dict}")
|
||||
def step_create_resolver_with_output(context: Any, output_dict: str) -> None:
|
||||
"""Create a resolver with specific output."""
|
||||
output = json.loads(output_dict)
|
||||
@@ -126,7 +126,7 @@ def step_register_resolver(context: Any) -> None:
|
||||
context.registry.register_resolver(context.resolver)
|
||||
|
||||
|
||||
@given('resolver1 returns {output_dict}')
|
||||
@given("resolver1 returns {output_dict}")
|
||||
def step_set_resolver1_output(context: Any, output_dict: str) -> None:
|
||||
"""Set the output for resolver1."""
|
||||
output = json.loads(output_dict)
|
||||
@@ -136,7 +136,7 @@ def step_set_resolver1_output(context: Any, output_dict: str) -> None:
|
||||
break
|
||||
|
||||
|
||||
@given('resolver2 returns {output_dict} when scope1 is present')
|
||||
@given("resolver2 returns {output_dict} when scope1 is present")
|
||||
def step_set_resolver2_conditional_output(context: Any, output_dict: str) -> None:
|
||||
"""Set resolver2 to return output only when scope1 is present in context."""
|
||||
output = json.loads(output_dict)
|
||||
@@ -181,7 +181,7 @@ def step_unregister_resolver(context: Any) -> None:
|
||||
context.registry.unregister_resolver(context.resolver.resolver_name)
|
||||
|
||||
|
||||
@when('I try to register another resolver with the same name')
|
||||
@when("I try to register another resolver with the same name")
|
||||
def step_try_register_duplicate(context: Any) -> None:
|
||||
"""Try to register a duplicate resolver."""
|
||||
duplicate = MockScopeResolver(context.resolver.resolver_name)
|
||||
@@ -234,7 +234,7 @@ def step_check_resolver_order(context: Any) -> None:
|
||||
assert registered == expected
|
||||
|
||||
|
||||
@then('the merged context should contain {expected_dict}')
|
||||
@then("the merged context should contain {expected_dict}")
|
||||
def step_check_merged_context_contains(context: Any, expected_dict: str) -> None:
|
||||
"""Check that merged context contains expected values."""
|
||||
expected = json.loads(expected_dict)
|
||||
@@ -257,7 +257,7 @@ def step_check_merged_context_key_count(context: Any, count: int) -> None:
|
||||
assert len(context.merged_context) == count
|
||||
|
||||
|
||||
@then('the merged context should have {expected_dict}')
|
||||
@then("the merged context should have {expected_dict}")
|
||||
def step_check_merged_context_has(context: Any, expected_dict: str) -> None:
|
||||
"""Check that merged context has expected key-value pairs."""
|
||||
expected = json.loads(expected_dict)
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
"""Step definitions for tui_prompt_textarea.feature.
|
||||
|
||||
Tests that PromptInput uses TextArea (multi-line) instead of Input (single-line).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
|
||||
from behave import given, then, when
|
||||
|
||||
_MOCK_TEXTUAL_KEYS = [
|
||||
"textual",
|
||||
"textual.app",
|
||||
"textual.containers",
|
||||
"textual.widgets",
|
||||
]
|
||||
|
||||
|
||||
def _build_mock_textual_with_textarea():
|
||||
"""Build mock textual modules that expose TextArea."""
|
||||
mock_textual = ModuleType("textual")
|
||||
mock_textual_app = ModuleType("textual.app")
|
||||
mock_textual_containers = ModuleType("textual.containers")
|
||||
mock_textual_widgets = ModuleType("textual.widgets")
|
||||
|
||||
class MockTextArea:
|
||||
"""Minimal TextArea stand-in for the Textual base class."""
|
||||
|
||||
text: str = ""
|
||||
|
||||
def __init__(self, *args: object, **kwargs: object) -> None:
|
||||
self.text = ""
|
||||
|
||||
mock_textual_app.App = object
|
||||
mock_textual_containers.Vertical = object
|
||||
mock_textual_widgets.Header = object
|
||||
mock_textual_widgets.Footer = object
|
||||
mock_textual_widgets.Static = object
|
||||
mock_textual_widgets.TextArea = MockTextArea
|
||||
|
||||
return {
|
||||
"textual": mock_textual,
|
||||
"textual.app": mock_textual_app,
|
||||
"textual.containers": mock_textual_containers,
|
||||
"textual.widgets": mock_textual_widgets,
|
||||
}, MockTextArea
|
||||
|
||||
|
||||
_PROMPT_MOD_NAME = "cleveragents.tui.widgets.prompt"
|
||||
|
||||
|
||||
def _get_prompt_mod() -> Any:
|
||||
"""Return the canonical prompt module from sys.modules.
|
||||
|
||||
Uses ``importlib.import_module`` (which always returns
|
||||
``sys.modules[name]``) instead of ``import cleveragents.tui.widgets.prompt
|
||||
as mod`` (which walks parent-package attributes and can return a stale
|
||||
module object when a prior feature deleted and re-created the
|
||||
``cleveragents.tui.*`` namespace). The stale object causes
|
||||
``importlib.reload()`` to fail with
|
||||
``ImportError: module ... not in sys.modules`` because Python 3.13's
|
||||
reload checks ``sys.modules.get(name) is module``.
|
||||
"""
|
||||
return importlib.import_module(_PROMPT_MOD_NAME)
|
||||
|
||||
|
||||
def _install_mock_textual(context: Any) -> None:
|
||||
"""Inject mock textual into sys.modules and reload the prompt module."""
|
||||
mocks, mock_textarea_cls = _build_mock_textual_with_textarea()
|
||||
context._prompt_saved_modules = {}
|
||||
for key in _MOCK_TEXTUAL_KEYS:
|
||||
context._prompt_saved_modules[key] = sys.modules.pop(key, None)
|
||||
for key, mod in mocks.items():
|
||||
sys.modules[key] = mod
|
||||
|
||||
prompt_mod = _get_prompt_mod()
|
||||
importlib.reload(prompt_mod)
|
||||
context._prompt_mod = prompt_mod
|
||||
context._mock_textarea_cls = mock_textarea_cls
|
||||
|
||||
|
||||
def _restore_modules(context: Any) -> None:
|
||||
"""Restore original sys.modules and reload the prompt module."""
|
||||
for key, val in getattr(context, "_prompt_saved_modules", {}).items():
|
||||
if val is None:
|
||||
sys.modules.pop(key, None)
|
||||
else:
|
||||
sys.modules[key] = val
|
||||
|
||||
importlib.reload(_get_prompt_mod())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Background
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given("the prompt module is loaded with a mocked TextArea")
|
||||
def step_load_prompt_with_mock_textarea(context):
|
||||
"""Install mock Textual with TextArea, reload prompt module."""
|
||||
_install_mock_textual(context)
|
||||
context.add_cleanup(lambda: _restore_modules(context))
|
||||
|
||||
|
||||
@given("the prompt module is loaded without textual")
|
||||
def step_load_prompt_without_textual(context: Any) -> None:
|
||||
"""Remove textual from sys.modules so the fallback path is used."""
|
||||
context._prompt_saved_modules_fallback = {}
|
||||
for key in _MOCK_TEXTUAL_KEYS:
|
||||
context._prompt_saved_modules_fallback[key] = sys.modules.pop(key, None)
|
||||
|
||||
prompt_mod = _get_prompt_mod()
|
||||
importlib.reload(prompt_mod)
|
||||
context._prompt_mod_fallback = prompt_mod
|
||||
|
||||
def restore() -> None:
|
||||
for key, val in context._prompt_saved_modules_fallback.items():
|
||||
if val is None:
|
||||
sys.modules.pop(key, None)
|
||||
else:
|
||||
sys.modules[key] = val
|
||||
importlib.reload(_get_prompt_mod())
|
||||
|
||||
context.add_cleanup(restore)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scenario: PromptInput base class is TextArea not Input
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then("the PromptInput base class should be the mocked TextArea")
|
||||
def step_base_class_is_textarea(context):
|
||||
PromptInput = context._prompt_mod.PromptInput
|
||||
assert issubclass(PromptInput, context._mock_textarea_cls), (
|
||||
f"Expected PromptInput to subclass MockTextArea, "
|
||||
f"but got bases: {PromptInput.__bases__}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scenario: PromptInput exposes a text property not value
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when("I create a PromptInput instance")
|
||||
def step_create_prompt_input(context):
|
||||
context._prompt_instance = context._prompt_mod.PromptInput()
|
||||
|
||||
|
||||
@then("the PromptInput instance should have a text attribute")
|
||||
def step_has_text_attribute(context):
|
||||
assert hasattr(context._prompt_instance, "text"), (
|
||||
"PromptInput instance should have a 'text' attribute"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scenario: consume_text returns the current text content
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when('I set the PromptInput text to "{text}"')
|
||||
def step_set_prompt_input_text(context, text):
|
||||
context._prompt_instance.text = text
|
||||
|
||||
|
||||
@when("I call consume_text on the PromptInput")
|
||||
def step_call_consume_text(context):
|
||||
context._prompt_submitted = context._prompt_instance.consume_text()
|
||||
|
||||
|
||||
@then('the PromptSubmitted text should be "{expected}"')
|
||||
def step_prompt_submitted_text(context, expected):
|
||||
assert context._prompt_submitted.text == expected, (
|
||||
f"Expected '{expected}', got '{context._prompt_submitted.text}'"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scenario: consume_text clears the text after consuming
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then("the PromptInput text should be empty")
|
||||
def step_prompt_input_text_empty(context):
|
||||
assert context._prompt_instance.text == "", (
|
||||
f"Expected empty text, got '{context._prompt_instance.text}'"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scenario: PromptInput fallback uses text attribute when TextArea unavailable
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when("I create a PromptInput instance from the fallback")
|
||||
def step_create_fallback_prompt_input(context):
|
||||
context._fallback_prompt_instance = context._prompt_mod_fallback.PromptInput()
|
||||
|
||||
|
||||
@then("the fallback PromptInput instance should have a text attribute")
|
||||
def step_fallback_has_text_attribute(context):
|
||||
assert hasattr(context._fallback_prompt_instance, "text"), (
|
||||
"Fallback PromptInput instance should have a 'text' attribute"
|
||||
)
|
||||
|
||||
|
||||
@then("the fallback PromptInput text should be empty string")
|
||||
def step_fallback_text_empty(context):
|
||||
assert context._fallback_prompt_instance.text == "", (
|
||||
f"Expected empty string, got '{context._fallback_prompt_instance.text}'"
|
||||
)
|
||||
@@ -137,9 +137,7 @@ class ScopeChainRegistry:
|
||||
with self._lock:
|
||||
return list(self._resolvers.keys())
|
||||
|
||||
def resolve_all(
|
||||
self, scope_context: Mapping[str, Any]
|
||||
) -> Mapping[str, Any]:
|
||||
def resolve_all(self, scope_context: Mapping[str, Any]) -> Mapping[str, Any]:
|
||||
"""Invoke all registered resolvers and merge their output.
|
||||
|
||||
Resolvers are invoked in registration order. Each resolver receives
|
||||
|
||||
Reference in New Issue
Block a user