Files
cleveragents-core/features/consolidated_change_tracking.feature
T
freemo b2e923173f
CI / lint (pull_request) Successful in 18s
CI / typecheck (pull_request) Successful in 32s
CI / quality (pull_request) Successful in 19s
CI / security (pull_request) Successful in 33s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 16s
CI / unit_tests (pull_request) Successful in 2m5s
CI / integration_tests (pull_request) Successful in 2m50s
CI / docker (pull_request) Successful in 15s
CI / coverage (pull_request) Successful in 4m19s
CI / lint (push) Successful in 12s
CI / quality (push) Successful in 18s
CI / security (push) Successful in 30s
CI / build (push) Successful in 22s
CI / typecheck (push) Successful in 58s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m6s
CI / docker (push) Successful in 50s
CI / integration_tests (push) Successful in 3m21s
CI / coverage (push) Successful in 4m12s
CI / benchmark-publish (push) Has been cancelled
CI / benchmark-regression (pull_request) Successful in 24m46s
perf(tests): consolidate 141 trivially small feature files into 34 domain groups
Consolidate 141 BDD feature files that each complete in under 0.1 seconds
into 25 domain-grouped feature files, reducing subprocess count from 339
to ~223. Each consolidated file groups scenarios from the same domain/module
that share step definitions and fixtures. All scenarios are preserved with
clear comment headers indicating their original source file.

This reduces subprocess overhead by ~116 invocations (141 original files
replaced by 25 consolidated files), targeting the 42% of subprocess count
that contributed only 0.2% of actual test runtime.

ISSUES CLOSED: #485
2026-03-02 14:56:13 +00:00

555 lines
21 KiB
Gherkin

Feature: Consolidated Change Tracking
Combined scenarios from: change_tracking, changeset_capture, streamedchange_coverage
# ============================================================
# Originally from: change_tracking.feature
# Feature: ChangeSet Models and Invocation Tracking
# ============================================================
Scenario: ChangeType is an alias for ChangeOperation
Given I import ChangeType and ChangeOperation
Then ChangeType should be the same class as ChangeOperation
And ChangeType CREATE should equal "create"
# ---- ChangeEntry Validators ----
Scenario: CREATE entry rejects before_hash
When I create a ChangeEntry with CREATE and before_hash "abc"
Then a validation error should be raised mentioning "before_hash"
Scenario: DELETE entry rejects after_hash
When I create a ChangeEntry with DELETE and after_hash "abc"
Then a validation error should be raised mentioning "after_hash"
Scenario: CREATE entry accepts after_hash only
When I create a valid CREATE ChangeEntry with after_hash "abc123"
Then the change entry operation should be "create"
And the change entry after_hash should be "abc123"
Scenario: MODIFY entry accepts both hashes
When I create a valid MODIFY ChangeEntry with both hashes
Then the change entry operation should be "modify"
And the change entry has_integrity_hashes should be true
Scenario: RENAME entry without hashes is valid
When I create a RENAME ChangeEntry without hashes
Then the change entry operation should be "rename"
And the change entry has_integrity_hashes should be true
Scenario: CREATE entry without after_hash has no integrity
When I create a CREATE ChangeEntry without any hashes
Then the change entry has_integrity_hashes should be false
# ---- Path Normalization ----
Scenario: Normalize absolute path with repo root
Given a repo root "/home/user/project"
When I normalize the path "/home/user/project/src/main.py"
Then the normalized path should be "src/main.py"
Scenario: Normalize relative path stays unchanged
Given an empty repo root
When I normalize the path "src/main.py"
Then the normalized path should be "src/main.py"
Scenario: Normalize path outside repo root is unchanged
Given a repo root "/other/project"
When I normalize the path "/home/user/project/src/main.py"
Then the normalized path should be "home/user/project/src/main.py"
# ---- SpecChangeSet Ordering ----
Scenario: sorted_entries returns deterministic order
Given a SpecChangeSet with entries in random order
When I call sorted_entries
Then entries should be sorted by resource_id then path then timestamp
Scenario: grouped_by_resource groups correctly
Given a SpecChangeSet with entries for multiple resources
When I call grouped_by_resource
Then entries should be grouped by resource_id
And each group should be sorted by path and timestamp
Scenario: add_change appends to entries
Given an empty SpecChangeSet
When I add a change entry to it
Then the spec changeset should contain 1 entry
# ---- ToolInvocation Model ----
Scenario: Create a ToolInvocation with minimal fields
When I create a ToolInvocation for plan "plan-1" and tool "builtin/file-write"
Then the invocation should have a ULID invocation_id
And the invocation plan_id should be "plan-1"
And the invocation tool_name should be "builtin/file-write"
And the invocation success should be true
Scenario: Create a failed ToolInvocation
When I create a failed ToolInvocation with error "file not found"
Then the invocation success should be false
And the invocation error should be "file not found"
Scenario: ToolInvocation with change_ids
When I create a ToolInvocation with change_ids
Then the invocation should have 2 change_ids
Scenario: ToolInvocation with provider metadata
When I create a ToolInvocation with provider metadata
Then the invocation provider_metadata should contain model name
# ---- InMemoryInvocationTracker ----
Scenario: Track invocations and retrieve by plan
Given an InMemoryInvocationTracker
When I track 3 invocations for plan "plan-A"
And I track 2 invocations for plan "plan-B"
Then get_invocations for "plan-A" should return 3
And get_invocations for "plan-B" should return 2
Scenario: Retrieve invocations for a specific skill
Given an InMemoryInvocationTracker
When I track invocations for different skills
Then get_invocations_for_skill should filter correctly
Scenario: Get change IDs across invocations
Given an InMemoryInvocationTracker
When I track invocations with change_ids
Then get_changes should return all change_ids for the plan
Scenario: Invocations are ordered by sequence number
Given an InMemoryInvocationTracker
When I track invocations out of order
Then get_invocations should return them in sequence order
# ---- SpecChangeSet summary ----
Scenario: summary returns correct counts for mixed operations
Given a SpecChangeSet with mixed operation entries
When I call summary on the changeset
Then the summary total should be 4
And the summary creates should be 1
And the summary modifies should be 1
And the summary deletes should be 1
And the summary renames should be 1
And the summary paths_changed should be 4
And the summary resources_involved should be 2
# ---- add_change plan_id validation ----
Scenario: add_change rejects entry with mismatched plan_id
Given an empty SpecChangeSet
When I add a change entry with a different plan_id
Then a validation error should be raised mentioning "plan_id"
# ---- Empty tracker queries ----
Scenario: get_invocations returns empty for unknown plan
Given an InMemoryInvocationTracker
Then get_invocations for "nonexistent-plan" should return 0
Scenario: get_changes returns empty for unknown plan
Given an InMemoryInvocationTracker
Then get_changes for "nonexistent-plan" should return 0 ids
Scenario: get_invocations_for_skill returns empty for unknown skill
Given an InMemoryInvocationTracker
When I track 1 invocations for plan "plan-X"
Then get_invocations_for_skill "plan-X" "no-such-skill" should return 0
# ---- get_changes ordering proof ----
Scenario: get_changes respects sequence order not insertion order
Given an InMemoryInvocationTracker
When I track change invocations in reverse sequence order
Then get_changes should return change_ids in sequence order
# ---- Path normalization edge cases ----
Scenario: Normalize path that equals repo root returns dot
Given a repo root "/home/user/project"
When I normalize the path "/home/user/project"
Then the normalized path should be "."
Scenario: Normalize path with dot-dot components preserves them
Given a repo root "/repo"
When I normalize the path "/repo/foo/../bar/file.py"
Then the normalized path should be "foo/../bar/file.py"
Scenario: Normalize empty path returns dot
Given an empty repo root
When I normalize the path "."
Then the normalized path should be "."
# ============================================================
# Originally from: changeset_capture.feature
# Feature: ChangeSet capture and domain model
# ============================================================
Scenario: ChangeOperation enum has expected members
Given I import the ChangeOperation enum
Then it should have members CREATE, MODIFY, DELETE, RENAME
# ---- ChangeEntry model ----
Scenario: Create a ChangeEntry with all fields
Given I import the ChangeEntry model
When I create a ChangeEntry for a create operation
Then the entry should have a ULID entry_id
And the entry should have operation "create"
And the entry should have a UTC timestamp
And the before_hash should be None
Scenario: ChangeEntry for modify operation has before and after hashes
Given I import the ChangeEntry model
When I create a ChangeEntry for a modify operation with hashes
Then the before_hash should not be None
And the after_hash should not be None
Scenario: ChangeEntry for delete operation has no after_hash
Given I import the ChangeEntry model
When I create a ChangeEntry for a delete operation
Then the after_hash should be None
And the before_hash should not be None
Scenario: ChangeEntry for rename operation
Given I import the ChangeEntry model
When I create a ChangeEntry for a rename operation
Then the entry should have operation "rename"
# ---- SpecChangeSet model ----
Scenario: SpecChangeSet summary counts are correct
Given I import the SpecChangeSet model
When I create a SpecChangeSet with mixed operations
Then the creates count should be 1
And the modifies count should be 2
And the deletes count should be 1
And the renames count should be 1
And paths_changed should have 5 entries
And resources_involved should have 2 entries
Scenario: SpecChangeSet summary dict
Given I import the SpecChangeSet model
When I create a SpecChangeSet with mixed operations
Then the summary dict should have correct totals
Scenario: Empty SpecChangeSet has zero counts
Given I import the SpecChangeSet model
When I create an empty SpecChangeSet
Then the creates count should be 0
And the modifies count should be 0
And the deletes count should be 0
And the renames count should be 0
# ---- InMemoryChangeSetStore ----
Scenario: InMemoryChangeSetStore start/record/get flow
Given I have an InMemoryChangeSetStore
When I start a changeset for plan "plan-abc"
And I record a create entry in the changeset
Then I can get the changeset by ID
And the store changeset should have 1 entry
Scenario: InMemoryChangeSetStore get_for_plan
Given I have an InMemoryChangeSetStore
When I start a changeset for plan "plan-abc"
And I start a changeset for plan "plan-abc"
And I start a changeset for plan "plan-xyz"
Then get_for_plan "plan-abc" should return 2 changesets
And get_for_plan "plan-xyz" should return 1 changeset
Scenario: InMemoryChangeSetStore summarize
Given I have an InMemoryChangeSetStore
When I start a changeset for plan "plan-s"
And I record a create entry in the changeset
And I record a modify entry in the changeset
Then the summarized changeset should show 2 total
Scenario: InMemoryChangeSetStore get returns None for unknown ID
Given I have an InMemoryChangeSetStore
Then getting a non-existent changeset returns None
Scenario: InMemoryChangeSetStore record raises on unknown ID
Given I have an InMemoryChangeSetStore
Then recording to a non-existent changeset raises KeyError
Scenario: InMemoryChangeSetStore summarize returns empty for unknown
Given I have an InMemoryChangeSetStore
Then summarizing a non-existent changeset returns empty dict
# ---- ChangeSetCapture integration ----
Scenario: ChangeSetCapture records resource_id and tool_name
Given I have a ChangeSetCapture with resource_id "res-1"
When I create a ChangeSetEntry via capture
Then the entry resource_id should be "res-1"
And the entry tool_name should be set
Scenario: ChangeSetCapture normalizes paths
Given I have a ChangeSetCapture with sandbox_root
When I capture a write to a nested path
Then the captured path should be repo-relative
Scenario: ChangeSetCapture converts to spec changeset
Given I have a ChangeSetCapture with resource_id "res-2"
When I add several entries via capture
Then to_spec_changeset should return a SpecChangeSet
# ---- Multi-resource plan ----
Scenario: Multi-resource plan captures correctly
Given I have two ChangeSetCapture instances for different resources
When each capture records changes for its resource
Then each changeset should only have its resource entries
# ============================================================
# Originally from: streamedchange_coverage.feature
# Feature: Streamed Change Models Coverage
# ============================================================
Scenario: Create StreamedChangeSection with valid line numbers
Given I import the StreamedChangeSection class
When I create a StreamedChangeSection with start_line 10
And I set end_line to 20
And I set start_line_string to "def function():"
And I set end_line_string to "return result"
Then the StreamedChangeSection instance should be created successfully
And the start_line should be 10
And the end_line should be 20
And the start_line_string should be "def function():"
And the end_line_string should be "return result"
Scenario: Create StreamedChangeSection with single line change
Given I import the StreamedChangeSection class
When I create a StreamedChangeSection with start_line 1
And I set end_line to 1
And I set start_line_string to "import os"
And I set end_line_string to "import os"
Then the StreamedChangeSection instance should be created successfully
And the start_line should be 1
And the end_line should be 1
And the start_line_string should be "import os"
And the end_line_string should be "import os"
Scenario: Create StreamedChangeSection with large line numbers
Given I import the StreamedChangeSection class
When I create a StreamedChangeSection with start_line 1000
And I set end_line to 2000
And I set start_line_string to "class LargeClass:"
And I set end_line_string to "# End of class"
Then the StreamedChangeSection instance should be created successfully
And the start_line should be 1000
And the end_line should be 2000
Scenario: Create StreamedChangeSection using field aliases
Given I import the StreamedChangeSection class
When I create a StreamedChangeSection using aliases
Then the StreamedChangeSection should accept the aliased fields
And the section values should be properly mapped
Scenario: Validate StreamedChangeSection configuration
Given I import the StreamedChangeSection class
When I create a StreamedChangeSection with valid data
Then the section configuration should have str_strip_whitespace as true
And the section configuration should have validate_assignment as true
And the section configuration should have arbitrary_types_allowed as false
And the section configuration should have populate_by_name as true
And the section configuration should have use_enum_values as true
Scenario: Create StreamedChangeWithLineNums with included boundaries
Given I import the StreamedChangeWithLineNums class
And I have a valid StreamedChangeSection for old section
When I create a StreamedChangeWithLineNums with the old section
And I set start_line_included to true
And I set end_line_included to true
And I set new content to "Updated code block"
Then the StreamedChangeWithLineNums instance should be created successfully
And the old section should match the provided section
And the start_line_included should be true
And the end_line_included should be true
And the new content should be "Updated code block"
Scenario: Create StreamedChangeWithLineNums with excluded boundaries
Given I import the StreamedChangeWithLineNums class
And I have a valid StreamedChangeSection for old section
When I create a StreamedChangeWithLineNums with the old section
And I set start_line_included to false
And I set end_line_included to false
And I set new content to "Replacement text"
Then the StreamedChangeWithLineNums instance should be created successfully
And the start_line_included should be false
And the end_line_included should be false
And the new content should be "Replacement text"
Scenario: Create StreamedChangeWithLineNums with mixed boundaries
Given I import the StreamedChangeWithLineNums class
And I have a valid StreamedChangeSection for old section
When I create a StreamedChangeWithLineNums with the old section
And I set start_line_included to true
And I set end_line_included to false
And I set new content to "Partial replacement"
Then the StreamedChangeWithLineNums instance should be created successfully
And the start_line_included should be true
And the end_line_included should be false
Scenario: Create StreamedChangeWithLineNums with empty new content
Given I import the StreamedChangeWithLineNums class
And I have a valid StreamedChangeSection for old section
When I create a StreamedChangeWithLineNums with the old section
And I set start_line_included to true
And I set end_line_included to true
And I set new content to ""
Then the StreamedChangeWithLineNums instance should be created successfully
And the new content should be ""
Scenario: Create StreamedChangeWithLineNums with multiline new content
Given I import the StreamedChangeWithLineNums class
And I have a valid StreamedChangeSection for old section
When I create a StreamedChangeWithLineNums with the old section
And I set start_line_included to true
And I set end_line_included to true
And I set new content to multiline text
Then the StreamedChangeWithLineNums instance should be created successfully
And the new content should contain multiple lines
Scenario: Create StreamedChangeWithLineNums using field aliases
Given I import the StreamedChangeWithLineNums class
And I have a valid StreamedChangeSection for old section
When I create a StreamedChangeWithLineNums using aliases
Then the StreamedChangeWithLineNums should accept the aliased fields
And the change values should be properly mapped
Scenario: Validate StreamedChangeWithLineNums configuration
Given I import the StreamedChangeWithLineNums class
When I create a StreamedChangeWithLineNums with valid data
Then the change configuration should have str_strip_whitespace as true
And the change configuration should have validate_assignment as true
And the change configuration should have arbitrary_types_allowed as false
And the change configuration should have populate_by_name as true
And the change configuration should have use_enum_values as true
Scenario: StreamedChangeSection strips whitespace from strings
Given I import the StreamedChangeSection class
When I create a StreamedChangeSection with whitespace in strings
Then the whitespace should be stripped from string fields
And the section should be created successfully
Scenario: StreamedChangeWithLineNums strips whitespace from new content
Given I import the StreamedChangeWithLineNums class
And I have a valid StreamedChangeSection for old section
When I create a StreamedChangeWithLineNums with whitespace in new content
Then the whitespace should be stripped from new field
And the change should be created successfully
Scenario: Complex StreamedChangeWithLineNums with nested section
Given I import the StreamedChangeWithLineNums class
When I create a complex StreamedChangeWithLineNums
Then the nested StreamedChangeSection should be accessible
And all nested fields should be properly initialized
Scenario: StreamedChangeSection with special characters in strings
Given I import the StreamedChangeSection class
When I create a StreamedChangeSection with special characters
Then the special characters should be preserved
And the section should handle unicode properly
Scenario: Validate StreamedChangeSection field types
Given I import the StreamedChangeSection class
When I create a StreamedChangeSection with different field types
Then the integer fields should be validated
And the string fields should be validated
Scenario: Validate StreamedChangeWithLineNums field types
Given I import the StreamedChangeWithLineNums class
And I have a valid StreamedChangeSection for old section
When I create a StreamedChangeWithLineNums with different field types
Then the boolean fields should be validated
And the nested object should be validated
Scenario: StreamedChangeSection with boundary line numbers
Given I import the StreamedChangeSection class
When I create a StreamedChangeSection with start_line 0
And I set end_line to 0
And I set start_line_string to "# First line"
And I set end_line_string to "# First line"
Then the StreamedChangeSection instance should be created successfully
And the start_line should be 0
And the end_line should be 0
Scenario: StreamedChangeWithLineNums represents code deletion
Given I import the StreamedChangeWithLineNums class
And I have a StreamedChangeSection representing lines to delete
When I create a StreamedChangeWithLineNums for deletion
And I set start_line_included to true
And I set end_line_included to true
And I set new content to ""
Then the StreamedChangeWithLineNums instance should represent a deletion
And the old section should contain the deleted content