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
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
555 lines
21 KiB
Gherkin
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
|