diff --git a/features/steps/acms_budget_enforcement_steps.py b/features/steps/acms_budget_enforcement_steps.py index 8a491b626..d28e8e483 100644 --- a/features/steps/acms_budget_enforcement_steps.py +++ b/features/steps/acms_budget_enforcement_steps.py @@ -8,32 +8,31 @@ from cleveragents.acms.budget_enforcement import BudgetEnforcer @given("a budget enforcer with max_file_size of {size:d} bytes") -def step_create_budget_enforcer_with_max_file_size(context, size: int) -> None: - """Create a fresh budget enforcer with specified max_file_size. - - Always creates a fresh instance — the Background runs before each scenario - and must reset state to prevent leakage between scenarios within the same - feature file. - """ - context.budget_enforcer = BudgetEnforcer(max_file_size=size, max_total_size=10000) +def step_create_budget_enforcer_with_max_file_size( + context: object, size: int +) -> None: + """Create a fresh budget enforcer with specified max_file_size.""" + context.budget_enforcer = BudgetEnforcer(max_file_size=size, max_total_size=10000) # noqa: E501 context.file_counter = 0 @given("a budget enforcer with max_total_size of {size:d} bytes") -def step_create_budget_enforcer_with_max_total_size(context, size: int) -> None: +def step_create_budget_enforcer_with_max_total_size( + context: object, size: int +) -> None: """Update the budget enforcer's max_total_size. - Always reconstructs a fresh BudgetEnforcer unconditionally — + Always reconstructs a fresh BudgetEnforcer unconditionally -- the Background runs before each scenario and must reset state to prevent leakage between scenarios within the same feature file. The previously-set max_file_size from step_one determines the new enforcer's constraint since both Background steps run sequentially. """ - # Retrieve max_file_size set by the first Background step - if hasattr(context, "budget_enforcer") and context.budget_enforcer is not None: + _max_file_size: int + if hasattr(context, "budget_enforcer") and context.budget_enforcer is not None: # noqa: E501 _max_file_size = context.budget_enforcer.max_file_size else: - _max_file_size = 10000 # Default fallback + _max_file_size = 10000 context.budget_enforcer = BudgetEnforcer( max_file_size=_max_file_size, max_total_size=size, @@ -41,25 +40,27 @@ def step_create_budget_enforcer_with_max_total_size(context, size: int) -> None: @when("I add a file of {size:d} bytes to the context") -def step_add_file_of_size(context, size: int) -> None: +def step_add_file_of_size(context: object, size: int) -> None: """Add a file of specified size to the context.""" - if not hasattr(context, "file_counter"): - context.file_counter = 0 - context.file_counter += 1 + counter = getattr(context, "file_counter", 0) + counter += 1 + context.file_counter = counter - filename = f"file_{context.file_counter}.txt" + filename = f"file_{counter}.txt" content = "x" * size context.budget_enforcer.add_file(filename, content) @when("I add a file of exactly {size:d} bytes to the context") -def step_add_file_of_exact_size(context, size: int) -> None: +def step_add_file_of_exact_size(context: object, size: int) -> None: """Add a file of exactly specified size to the context.""" step_add_file_of_size(context, size) @when("I add a file named {filename} of {size:d} bytes to the context") -def step_add_named_file(context, filename: str, size: int) -> None: +def step_add_named_file( + context: object, filename: str, size: int +) -> None: """Add a named file of specified size to the context.""" filename = filename.strip('"') content = "x" * size @@ -67,18 +68,18 @@ def step_add_named_file(context, filename: str, size: int) -> None: @when("I add an empty file to the context") -def step_add_empty_file(context) -> None: +def step_add_empty_file(context: object) -> None: """Add an empty file to the context.""" - if not hasattr(context, "file_counter"): - context.file_counter = 0 - context.file_counter += 1 + counter = getattr(context, "file_counter", 0) + counter += 1 + context.file_counter = counter - filename = f"empty_file_{context.file_counter}.txt" + filename = f"empty_file_{counter}.txt" context.budget_enforcer.add_file(filename, "") @when("I add files in order: {sizes} bytes") -def step_add_files_in_order(context, sizes: str) -> None: +def step_add_files_in_order(context: object, sizes: str) -> None: """Add multiple files in order with specified sizes.""" size_list = [int(s.strip()) for s in sizes.split(",")] for size in size_list: @@ -89,134 +90,155 @@ def step_add_files_in_order(context, sizes: str) -> None: "I add a file with multi-byte UTF-8 content of {char_count:d} characters" " to the context" ) -def step_add_multibyte_utf8_file(context, char_count: int) -> None: +def step_add_multibyte_utf8_file(context: object, char_count: int) -> None: """Add a file with multi-byte UTF-8 content (emoji: 4 bytes each). - Each emoji character is 4 bytes in UTF-8, so 250 characters = 1000 bytes, - which exceeds the max_file_size of 1000 bytes (strict greater-than check - means exactly 1000 passes, but 4-byte chars * 250 = 1000 which is at the - boundary — use a character that produces > 1000 bytes total). - - We use the snowman character (U+2603, 3 bytes each) so 250 chars = 750 bytes, - but to exceed 1000 bytes we use emoji (U+1F600, 4 bytes each): 250 * 4 = 1000 - bytes which is exactly at the boundary. To ensure exclusion, we use 251 emoji - characters (251 * 4 = 1004 bytes > 1000 bytes limit). + Each emoji character is 4 bytes in UTF-8. To exceed max_file_size of + 1000 we use 251 emoji characters (251 x 4 = 1004 bytes > 1000). """ - if not hasattr(context, "file_counter"): - context.file_counter = 0 - context.file_counter += 1 + counter = getattr(context, "file_counter", 0) + counter += 1 + context.file_counter = counter - filename = f"multibyte_{context.file_counter}.txt" - # Use emoji (U+1F600, 4 bytes each in UTF-8) to create multi-byte content. - # 251 emoji * 4 bytes = 1004 bytes > max_file_size of 1000 bytes. - emoji_char = "\U0001f600" # 😀 — 4 bytes in UTF-8 - content = emoji_char * (char_count + 1) # +1 to ensure byte size > 1000 + filename = f"multibyte_{counter}.txt" + emoji_char = "\U0001f600" # U+1F600 -- 4 bytes in UTF-8 + content = emoji_char * (char_count + 1) context.budget_enforcer.add_file(filename, content) -@then("the file should be included in the assembled context") -def step_file_should_be_included(context) -> None: +@then( + "the file should be included in the assembled context" +) +def step_file_should_be_included(context: object) -> None: """Verify that the last added file was included.""" files = context.budget_enforcer.get_included_files() - assert len(files) > 0, "No files were included in the context" + assert len(files) > 0, "No files were included in the assembled context" -@then("the file should be excluded from the assembled context") -def step_file_should_be_excluded(context) -> None: +@then( + "the file should be excluded from the assembled context" +) +def step_file_should_be_excluded(context: object) -> None: """Verify that the last added file was excluded.""" violations = context.budget_enforcer.get_violations() assert len(violations) > 0, "No budget violations were recorded" @then("the total context size should be {size:d} bytes") -def step_total_size_should_be(context, size: int) -> None: +def step_total_size_should_be( + context: object, size: int +) -> None: """Verify the total context size.""" actual_size = context.budget_enforcer.get_total_size() assert actual_size == size, f"Expected total size {size}, got {actual_size}" @then("the total context size should not exceed {size:d} bytes") -def step_total_size_should_not_exceed(context, size: int) -> None: +def step_total_size_should_not_exceed( + context: object, size: int +) -> None: """Verify the total context size does not exceed limit.""" actual_size = context.budget_enforcer.get_total_size() - assert actual_size <= size, f"Total size {actual_size} exceeds limit {size}" + assert ( + actual_size <= size + ), f"Total size {actual_size} exceeds limit {size}" @then("a budget violation warning should be generated for the file") -def step_budget_violation_warning_generated(context) -> None: +def step_budget_violation_warning_generated( + context: object, +) -> None: """Verify that a budget violation warning was generated.""" violations = context.budget_enforcer.get_violations() assert len(violations) > 0, "No budget violations were recorded" -@then("a budget violation warning should be generated for exceeding max_total_size") -def step_budget_violation_for_total_size(context) -> None: +@then( + "a budget violation warning should be generated for exceeding max_total_size" +) +def step_budget_violation_for_total_size(context: object) -> None: """Verify that a budget violation for max_total_size was generated.""" violations = context.budget_enforcer.get_violations() total_size_violations = [ v for v in violations if v.violation_type == "max_total_size" ] - assert len(total_size_violations) > 0, "No max_total_size violations were recorded" + assert ( + len(total_size_violations) > 0 + ), "No max_total_size violations were recorded" @then("a budget violation error should be generated") -def step_budget_violation_error_generated(context) -> None: +def step_budget_violation_error_generated( + context: object, +) -> None: """Verify that a budget violation error was generated.""" violations = context.budget_enforcer.get_violations() assert len(violations) > 0, "No budget violations were recorded" -@then("the error message should indicate the file size exceeds max_file_size") -def step_error_message_indicates_file_size_exceeded(context) -> None: +@then( + "the error message should indicate the file size exceeds max_file_size" +) +def step_error_message_indicates_file_size_exceeded( + context: object, +) -> None: """Verify the error message indicates file size exceeded.""" violations = context.budget_enforcer.get_violations() assert len(violations) > 0, "No violations found" violation = violations[-1] assert "exceeds" in violation.message.lower(), ( - f"Error message does not indicate exceeded: {violation.message}" + f"Error message does not indicate 'exceeded': {violation.message}" ) @then("the error message should include the file size and the limit") -def step_error_message_includes_size_and_limit(context) -> None: +def step_error_message_includes_size_and_limit( + context: object, +) -> None: """Verify the error message includes file size and limit.""" violations = context.budget_enforcer.get_violations() assert len(violations) > 0, "No violations found" violation = violations[-1] - assert violation.file_size is not None, "File size not included in violation" - assert violation.limit is not None, "Limit not included in violation" + assert violation.file_size is not None, ( + "File size not included in violation" + ) + assert violation.limit is not None, ( + "Limit not included in violation" + ) @then("no budget violation should be generated") -def step_no_budget_violation(context) -> None: +def step_no_budget_violation(context: object) -> None: """Verify that no budget violations were generated.""" violations = context.budget_enforcer.get_violations() assert len(violations) == 0, f"Unexpected violations: {violations}" @then("all three files should be included in the assembled context") -def step_all_three_files_included(context) -> None: +def step_all_three_files_included(context: object) -> None: """Verify that all three files were included.""" files = context.budget_enforcer.get_included_files() assert len(files) == 3, f"Expected 3 files, got {len(files)}" @then("the first 5 files should be included") -def step_first_five_files_included(context) -> None: - """Verify that the first 5 files were included.""" +def step_first_five_files_included(context: object) -> None: + """Verify that the first five files were included.""" files = context.budget_enforcer.get_included_files() assert len(files) == 5, f"Expected 5 files, got {len(files)}" @then("no additional files should be added beyond the budget") -def step_no_additional_files_beyond_budget(context) -> None: +def step_no_additional_files_beyond_budget(context: object) -> None: """Verify no additional files were added beyond budget.""" violations = context.budget_enforcer.get_violations() assert len(violations) > 0, "Expected violations for files beyond budget" @then("the budget violation should include the filename {filename}") -def step_violation_includes_filename(context, filename: str) -> None: +def step_violation_includes_filename( + context: object, filename: str +) -> None: """Verify the violation includes the filename.""" filename = filename.strip('"') violations = context.budget_enforcer.get_violations() @@ -228,7 +250,9 @@ def step_violation_includes_filename(context, filename: str) -> None: @then("the budget violation should include the file size {size:d}") -def step_violation_includes_file_size(context, size: int) -> None: +def step_violation_includes_file_size( + context: object, size: int +) -> None: """Verify the violation includes the file size.""" violations = context.budget_enforcer.get_violations() assert len(violations) > 0, "No violations found" @@ -239,7 +263,9 @@ def step_violation_includes_file_size(context, size: int) -> None: @then("the budget violation should include the limit {limit:d}") -def step_violation_includes_limit(context, limit: int) -> None: +def step_violation_includes_limit( + context: object, limit: int +) -> None: """Verify the violation includes the limit.""" violations = context.budget_enforcer.get_violations() assert len(violations) > 0, "No violations found" @@ -248,7 +274,7 @@ def step_violation_includes_limit(context, limit: int) -> None: @then("both files should be included in the assembled context") -def step_both_files_included(context) -> None: +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)}" diff --git a/robot/acms_budget_enforcement.robot b/robot/acms_budget_enforcement.robot index d9ce50e02..5c2b0ed6d 100644 --- a/robot/acms_budget_enforcement.robot +++ b/robot/acms_budget_enforcement.robot @@ -1,8 +1,6 @@ *** Settings *** Documentation Integration tests for ACMS BudgetEnforcer max_file_size and max_total_size constraints Resource ${CURDIR}/common.resource -Suite Setup Setup Test Environment -Suite Teardown Cleanup Test Environment *** Variables *** ${HELPER_SCRIPT} robot/helper_acms_budget_enforcement.py @@ -10,60 +8,60 @@ ${HELPER_SCRIPT} robot/helper_acms_budget_enforcement.py *** Test Cases *** Budget Enforcer Includes File Within Max File Size [Documentation] BudgetEnforcer includes a file within max_file_size limit - ${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} file-within-limit cwd=${WORKSPACE} + ${result}= Run Process python3 ${WORKSPACE}/${HELPER_SCRIPT} file-within-limit cwd=${WORKSPACE} Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} file-within-limit-ok Budget Enforcer Excludes File Exceeding Max File Size [Documentation] BudgetEnforcer excludes a file that exceeds max_file_size - ${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} file-exceeds-limit cwd=${WORKSPACE} + ${result}= Run Process python3 ${WORKSPACE}/${HELPER_SCRIPT} file-exceeds-limit cwd=${WORKSPACE} Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} file-exceeds-limit-ok Budget Enforcer Enforces Max Total Size [Documentation] BudgetEnforcer cuts off files when max_total_size is reached - ${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} total-size-cutoff cwd=${WORKSPACE} + ${result}= Run Process python3 ${WORKSPACE}/${HELPER_SCRIPT} total-size-cutoff cwd=${WORKSPACE} Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} total-size-cutoff-ok Budget Enforcer Violation Message Is Clear [Documentation] BudgetViolation message clearly describes the constraint exceeded - ${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} violation-message cwd=${WORKSPACE} + ${result}= Run Process python3 ${WORKSPACE}/${HELPER_SCRIPT} violation-message cwd=${WORKSPACE} Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} violation-message-ok Budget Enforcer Returns Defensive Copies [Documentation] get_violations and get_included_files return defensive copies - ${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} defensive-copies cwd=${WORKSPACE} + ${result}= Run Process python3 ${WORKSPACE}/${HELPER_SCRIPT} defensive-copies cwd=${WORKSPACE} Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} defensive-copies-ok Budget Enforcer Validates Constructor Arguments [Documentation] BudgetEnforcer raises ValueError for invalid budget values - ${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} constructor-validation cwd=${WORKSPACE} + ${result}= Run Process python3 ${WORKSPACE}/${HELPER_SCRIPT} constructor-validation cwd=${WORKSPACE} Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} constructor-validation-ok Budget Enforcer Validates Add File Arguments [Documentation] add_file raises ValueError for empty name - ${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} add-file-validation cwd=${WORKSPACE} + ${result}= Run Process python3 ${WORKSPACE}/${HELPER_SCRIPT} add-file-validation cwd=${WORKSPACE} Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} add-file-validation-ok Budget Enforcer Handles Multi-Byte UTF-8 Content [Documentation] BudgetEnforcer measures file size in bytes not characters - ${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} multibyte-utf8 cwd=${WORKSPACE} + ${result}= Run Process python3 ${WORKSPACE}/${HELPER_SCRIPT} multibyte-utf8 cwd=${WORKSPACE} Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} multibyte-utf8-ok Budget Enforcer Reset Clears State [Documentation] reset() clears all included files, violations, and total size - ${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} reset-state cwd=${WORKSPACE} + ${result}= Run Process python3 ${WORKSPACE}/${HELPER_SCRIPT} reset-state cwd=${WORKSPACE} Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} reset-state-ok Budget Enforcer Assembled Context Joins Files [Documentation] get_assembled_context joins included file contents with newlines - ${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} assembled-context cwd=${WORKSPACE} + ${result}= Run Process python3 ${WORKSPACE}/${HELPER_SCRIPT} assembled-context cwd=${WORKSPACE} Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} assembled-context-ok diff --git a/robot/helper_acms_budget_enforcement.py b/robot/helper_acms_budget_enforcement.py index 9b51ddc41..e4ec76e88 100644 --- a/robot/helper_acms_budget_enforcement.py +++ b/robot/helper_acms_budget_enforcement.py @@ -5,13 +5,11 @@ from __future__ import annotations import sys from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) - -from cleveragents.acms.budget_enforcement import BudgetEnforcer - def _test_file_within_limit() -> None: """BudgetEnforcer includes a file within max_file_size limit.""" + from cleveragents.acms.budget_enforcement import BudgetEnforcer + enforcer = BudgetEnforcer(max_file_size=1000, max_total_size=5000) content = "x" * 500 included = enforcer.add_file("small.txt", content) @@ -30,6 +28,8 @@ def _test_file_within_limit() -> None: def _test_file_exceeds_limit() -> None: """BudgetEnforcer excludes a file that exceeds max_file_size.""" + from cleveragents.acms.budget_enforcement import BudgetEnforcer + enforcer = BudgetEnforcer(max_file_size=1000, max_total_size=5000) content = "x" * 1500 included = enforcer.add_file("large.txt", content) @@ -53,8 +53,9 @@ def _test_file_exceeds_limit() -> None: def _test_total_size_cutoff() -> None: """BudgetEnforcer cuts off files when max_total_size is reached.""" + from cleveragents.acms.budget_enforcement import BudgetEnforcer + enforcer = BudgetEnforcer(max_file_size=1000, max_total_size=2500) - # Add 3 files of 1000 bytes each; third should be cut off (total would be 3000) assert enforcer.add_file("f1.txt", "x" * 1000) is True assert enforcer.add_file("f2.txt", "x" * 1000) is True assert enforcer.add_file("f3.txt", "x" * 1000) is False @@ -62,7 +63,9 @@ def _test_total_size_cutoff() -> None: f"Expected total size 2000, got {enforcer.get_total_size()}" ) violations = enforcer.get_violations() - total_violations = [v for v in violations if v.violation_type == "max_total_size"] + total_violations = [ + v for v in violations if v.violation_type == "max_total_size" + ] assert len(total_violations) == 1, ( f"Expected 1 max_total_size violation, got {len(total_violations)}" ) @@ -71,6 +74,8 @@ def _test_total_size_cutoff() -> None: def _test_violation_message() -> None: """BudgetViolation message clearly describes the constraint exceeded.""" + from cleveragents.acms.budget_enforcement import BudgetEnforcer + enforcer = BudgetEnforcer(max_file_size=500, max_total_size=2000) enforcer.add_file("big.txt", "x" * 600) violations = enforcer.get_violations() @@ -85,11 +90,12 @@ def _test_violation_message() -> None: def _test_defensive_copies() -> None: """get_violations and get_included_files return defensive copies.""" + from cleveragents.acms.budget_enforcement import BudgetEnforcer + enforcer = BudgetEnforcer(max_file_size=1000, max_total_size=5000) enforcer.add_file("f1.txt", "x" * 500) enforcer.add_file("too_big.txt", "x" * 1500) - # Mutate the returned lists — should not affect enforcer state violations = enforcer.get_violations() violations.clear() assert len(enforcer.get_violations()) == 1, ( @@ -106,28 +112,30 @@ def _test_defensive_copies() -> None: def _test_constructor_validation() -> None: """BudgetEnforcer raises ValueError for invalid budget values.""" - # Negative max_file_size + from cleveragents.acms.budget_enforcement import BudgetEnforcer + 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}" + assert "max_file_size" in str(e), ( + f"Error should mention max_file_size: {e}" + ) - # 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}" + assert "max_total_size" in str(e), ( + f"Error should mention max_total_size: {e}" + ) - # 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}" - # Valid construction should succeed enforcer = BudgetEnforcer(max_file_size=1000, max_total_size=5000) assert enforcer.max_file_size == 1000 assert enforcer.max_total_size == 5000 @@ -136,16 +144,16 @@ def _test_constructor_validation() -> None: def _test_add_file_validation() -> None: """add_file raises ValueError for empty name.""" + from cleveragents.acms.budget_enforcement import BudgetEnforcer + enforcer = BudgetEnforcer(max_file_size=1000, max_total_size=5000) - # Empty name try: 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}" - # Non-string content try: enforcer.add_file("file.txt", object()) raise AssertionError("Expected TypeError for non-string content") @@ -157,13 +165,12 @@ def _test_add_file_validation() -> None: def _test_multibyte_utf8() -> None: """BudgetEnforcer measures file size in bytes not characters.""" + from cleveragents.acms.budget_enforcement import BudgetEnforcer + enforcer = BudgetEnforcer(max_file_size=1000, max_total_size=5000) - # Emoji character U+1F600 is 4 bytes in UTF-8 - # 251 emoji * 4 bytes = 1004 bytes > 1000 byte limit - emoji_char = "\U0001f600" - content = emoji_char * 251 # 251 chars but 1004 bytes - assert len(content) == 251, "Character count should be 251" + emoji_char = "\U0001f600" # U+1F600 -- 4 bytes in UTF-8 + content = emoji_char * 251 assert len(content.encode("utf-8")) == 1004, "Byte count should be 1004" included = enforcer.add_file("emoji.txt", content) @@ -177,20 +184,20 @@ def _test_multibyte_utf8() -> None: f"Violation should report byte size 1004, got {violations[0].file_size}" ) - # A file with 250 emoji (1000 bytes) should be at the boundary — included - # (strict greater-than: 1000 > 1000 is False) enforcer2 = BudgetEnforcer(max_file_size=1000, max_total_size=5000) - content_boundary = emoji_char * 250 # 250 chars, 1000 bytes + content_boundary = emoji_char * 250 assert len(content_boundary.encode("utf-8")) == 1000 included2 = enforcer2.add_file("boundary.txt", content_boundary) assert included2 is True, ( - "File with exactly 1000 bytes should be included (boundary is inclusive)" + "File with exactly 1000 bytes should be included" ) print("multibyte-utf8-ok") def _test_reset_state() -> None: """reset() clears all included files, violations, and total size.""" + from cleveragents.acms.budget_enforcement import BudgetEnforcer + enforcer = BudgetEnforcer(max_file_size=1000, max_total_size=5000) enforcer.add_file("f1.txt", "x" * 500) enforcer.add_file("too_big.txt", "x" * 1500) @@ -211,7 +218,6 @@ def _test_reset_state() -> None: "After reset, violations should be empty" ) - # Should be able to add files again after reset assert enforcer.add_file("new.txt", "y" * 300) is True assert enforcer.get_total_size() == 300 print("reset-state-ok") @@ -219,6 +225,8 @@ def _test_reset_state() -> None: def _test_assembled_context() -> None: """get_assembled_context joins included file contents with newlines.""" + from cleveragents.acms.budget_enforcement import BudgetEnforcer + enforcer = BudgetEnforcer(max_file_size=1000, max_total_size=5000) enforcer.add_file("a.txt", "hello") enforcer.add_file("b.txt", "world") @@ -226,7 +234,6 @@ def _test_assembled_context() -> None: context = enforcer.get_assembled_context() assert context == "hello\nworld", f"Expected 'hello\\nworld', got {context!r}" - # Empty enforcer returns empty string enforcer2 = BudgetEnforcer(max_file_size=1000, max_total_size=5000) assert enforcer2.get_assembled_context() == "", ( "Empty enforcer should return empty string" @@ -234,7 +241,7 @@ def _test_assembled_context() -> None: print("assembled-context-ok") -_TESTS = { +_TESTS: dict[str, callable] = { "file-within-limit": _test_file_within_limit, "file-exceeds-limit": _test_file_exceeds_limit, "total-size-cutoff": _test_total_size_cutoff, @@ -248,7 +255,7 @@ _TESTS = { } -if __name__ == "__main__": +if __name__ == "__main__": # noqa: C901 if len(sys.argv) < 2 or sys.argv[1] not in _TESTS: print( f"Usage: {sys.argv[0]} <{'|'.join(sorted(_TESTS))}>", diff --git a/src/cleveragents/acms/__init__.py b/src/cleveragents/acms/__init__.py index 47afbf499..ba7686ba9 100644 --- a/src/cleveragents/acms/__init__.py +++ b/src/cleveragents/acms/__init__.py @@ -1,14 +1,7 @@ -"""ACMS (Advanced Context Management System) UKO vocabulary support. +"""ACMS module. -Provides UKO Layer 2 paradigm vocabulary specializations, Layer 3 -technology-specific vocabulary extensions, and the DetailLevelMap -inheritance mechanism for resolving named detail levels across the -ontology hierarchy (Layer 3 -> Layer 2 -> Layer 1 -> Layer 0). - -Also provides budget enforcement mechanisms for max_file_size and -max_total_size constraints on assembled context. - -Based on ``docs/specification.md`` ~lines 42333-42422, 44405-44420. +Combines UKO vocabulary support, ACMS index data model, and budget enforcement +for max_file_size and max_total_size constraints on assembled context. """ from __future__ import annotations @@ -26,67 +19,21 @@ from cleveragents.acms.index import ( IndexEntry, TierLevel, ) -from cleveragents.acms.uko import ( - CODE_DETAIL_LEVEL_MAP, - FUNC_DETAIL_LEVEL_MAP, - JAVA_DETAIL_LEVELS, - JAVA_VOCABULARY, - OO_DETAIL_LEVEL_MAP, - PROC_DETAIL_LEVEL_MAP, - PYTHON_DETAIL_LEVELS, - PYTHON_VOCABULARY, - RUST_DETAIL_LEVELS, - RUST_VOCABULARY, - TYPESCRIPT_DETAIL_LEVELS, - TYPESCRIPT_VOCABULARY, - DetailLevelMapBuilder, - DuplicateVocabularyError, - JavaAnnotation, - JavaCheckedException, - JavaClass, - JavaInterface, - JavaMethod, - Layer2Dependency, - ParadigmVocabulary, - ProvenanceInfo, - PythonClass, - PythonDecorator, - PythonFunction, - PythonModule, - PythonTypeStub, - RustDeriveAttribute, - RustFunction, - RustImpl, - RustStruct, - RustTrait, - TypeScriptClass, - TypeScriptFunction, - TypeScriptInterface, - TypeScriptModule, - UKOClass, - UKOProperty, - UKOVocabulary, - VocabularyClass, - VocabularyProperty, - VocabularyRegistry, - build_detail_level_map, - build_effective_map, - get_func_vocabulary, - get_oo_vocabulary, - get_proc_vocabulary, - resolve_detail_level, -) -# Combine exports from uko, index, and budget_enforcement modules -_uko_exports = list(_uko.__all__) -_index_exports = [ +# Re-export UKO vocabulary symbols for convenience +_uko_exports: list[str] = list(_uko.__all__) + +# Index model exports +_index_exports: list[str] = [ "ACMSIndex", "FileTraversalEngine", "FileType", "IndexEntry", "TierLevel", ] -_budget_exports = [ + +# Budget enforcement exports +_budget_exports: list[str] = [ "BudgetEnforcer", "BudgetViolation", "ContextFile",