diff --git a/features/acms/acms_budget_enforcement.feature b/features/acms/acms_budget_enforcement.feature index a4443c7d0..ada427d83 100644 --- a/features/acms/acms_budget_enforcement.feature +++ b/features/acms/acms_budget_enforcement.feature @@ -71,3 +71,31 @@ Feature: ACMS Budget Enforcement for max_file_size and max_total_size constraint When I add a file with multi-byte UTF-8 content of 250 characters to the context Then the file should be excluded from the assembled context And a budget violation warning should be generated for the file + + Scenario: Assembled context joins included file contents + When I add a file of 100 bytes to the context + And I add a file of 200 bytes to the context + Then the assembled context should be non-empty + + Scenario: Reset clears enforcer state for re-use + When I add a file of 500 bytes to the context + And I reset the budget enforcer + Then the total context size should be 0 bytes + + Scenario: BudgetEnforcer rejects negative max_file_size + Then creating an enforcer with negative max_file_size raises ValueError + + Scenario: BudgetEnforcer rejects zero max_total_size + Then creating an enforcer with zero max_total_size raises ValueError + + Scenario: BudgetEnforcer rejects max_file_size larger than max_total_size + Then creating an enforcer with max_file_size larger than max_total_size raises ValueError + + Scenario: add_file rejects empty filename + Then adding a file with an empty name raises ValueError + + Scenario: add_file rejects non-string content + Then adding a file with non-string content raises TypeError + + Scenario: BudgetViolation rejects unknown violation_type + Then creating a BudgetViolation with an unknown type raises ValueError diff --git a/features/steps/acms_budget_enforcement_steps.py b/features/steps/acms_budget_enforcement_steps.py index 2113e749f..56840380e 100644 --- a/features/steps/acms_budget_enforcement_steps.py +++ b/features/steps/acms_budget_enforcement_steps.py @@ -4,7 +4,7 @@ from __future__ import annotations from behave import given, then, when -from cleveragents.acms.budget_enforcement import BudgetEnforcer +from cleveragents.acms.budget_enforcement import BudgetEnforcer, BudgetViolation @given("a budget enforcer with max_file_size of {size:d} bytes") @@ -246,3 +246,78 @@ def step_both_files_included(context: object) -> None: """Verify that both files were included.""" files = context.budget_enforcer.get_included_files() assert len(files) == 2, f"Expected 2 files, got {len(files)}" + + +@then("the assembled context should be non-empty") +def step_assembled_context_non_empty(context: object) -> None: + """Verify the assembled context is non-empty.""" + ctx = context.budget_enforcer.get_assembled_context() + assert len(ctx) > 0, "Expected non-empty assembled context" + + +@when("I reset the budget enforcer") +def step_reset_budget_enforcer(context: object) -> None: + """Reset the budget enforcer to its initial state.""" + context.budget_enforcer.reset() + + +@then("creating an enforcer with negative max_file_size raises ValueError") +def step_negative_max_file_size_raises(context: object) -> None: + """Verify ValueError is raised for negative max_file_size.""" + try: + BudgetEnforcer(max_file_size=-1, max_total_size=5000) + raise AssertionError("Expected ValueError for negative max_file_size") + except ValueError as e: + assert "max_file_size" in str(e), f"Error should mention max_file_size: {e}" + + +@then("creating an enforcer with zero max_total_size raises ValueError") +def step_zero_max_total_size_raises(context: object) -> None: + """Verify ValueError is raised for zero max_total_size.""" + try: + BudgetEnforcer(max_file_size=100, max_total_size=0) + raise AssertionError("Expected ValueError for zero max_total_size") + except ValueError as e: + assert "max_total_size" in str(e), f"Error should mention max_total_size: {e}" + + +@then( + "creating an enforcer with max_file_size larger than max_total_size raises ValueError" +) +def step_max_file_size_exceeds_total_raises(context: object) -> None: + """Verify ValueError is raised when max_file_size > max_total_size.""" + try: + BudgetEnforcer(max_file_size=5000, max_total_size=1000) + raise AssertionError("Expected ValueError for max_file_size > max_total_size") + except ValueError as e: + assert "cannot exceed" in str(e), f"Error should mention cannot exceed: {e}" + + +@then("adding a file with an empty name raises ValueError") +def step_empty_name_raises(context: object) -> None: + """Verify ValueError is raised for an empty filename.""" + try: + context.budget_enforcer.add_file("", "some content") + raise AssertionError("Expected ValueError for empty name") + except ValueError as e: + assert "name" in str(e).lower(), f"Error should mention name: {e}" + + +@then("adding a file with non-string content raises TypeError") +def step_non_string_content_raises(context: object) -> None: + """Verify TypeError is raised for non-string content.""" + try: + context.budget_enforcer.add_file("file.txt", object()) + raise AssertionError("Expected TypeError for non-string content") + except TypeError as e: + assert "content" in str(e).lower(), f"Error should mention content: {e}" + + +@then("creating a BudgetViolation with an unknown type raises ValueError") +def step_unknown_violation_type_raises(context: object) -> None: + """Verify ValueError is raised for an unknown violation_type.""" + try: + BudgetViolation(violation_type="unknown_type") + raise AssertionError("Expected ValueError for unknown violation_type") + except ValueError as e: + assert "unknown_type" in str(e), f"Error should mention the bad type: {e}" diff --git a/src/cleveragents/acms/__init__.py b/src/cleveragents/acms/__init__.py index 0073b31bd..cc6c5a37b 100644 --- a/src/cleveragents/acms/__init__.py +++ b/src/cleveragents/acms/__init__.py @@ -94,6 +94,4 @@ _index_exports = [ _storage_exports = ["HotStorageTier"] _budget_exports = ["BudgetEnforcer", "BudgetViolation", "ContextFile"] -__all__: list[str] = ( - _uko_exports + _index_exports + _storage_exports + _budget_exports -) +__all__: list[str] = _uko_exports + _index_exports + _storage_exports + _budget_exports diff --git a/src/cleveragents/acms/budget_enforcement.py b/src/cleveragents/acms/budget_enforcement.py index 8eb6464ad..32f08fb2c 100644 --- a/src/cleveragents/acms/budget_enforcement.py +++ b/src/cleveragents/acms/budget_enforcement.py @@ -115,7 +115,7 @@ class BudgetEnforcer: file_obj = ContextFile(name=name, content=content) # Check max_file_size constraint - if file_obj.size is None: + if file_obj.size is None: # pragma: no cover raise RuntimeError( f"Internal error: {name} has no size; budget enforcement logic error" )