181 lines
7.0 KiB
Gherkin
181 lines
7.0 KiB
Gherkin
Feature: ChangeSet Models and Invocation Tracking
|
|
As a developer
|
|
I want ChangeSet models with deterministic ordering and invocation tracking
|
|
So that all tool executions and their changes are auditable
|
|
|
|
# ---- ChangeType Alias ----
|
|
|
|
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 "."
|