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 "."