bf52a9c648
The 4-tier invariant precedence chain (plan > action > project > global) was broken at the domain layer — merge_invariants() and InvariantSet.merge() only accepted 3 parameters (plan, project, global), silently dropping all action- scoped invariants. Added action_invariants as a fourth parameter with proper backward compatibility (default to empty list). Updated module docstrings, InvariantScope docstring, and InvariantService.get_effective_invariants() to reflect the correct precedence chain. Added comprehensive BDD test scenarios covering four-tier merge precedence, action-before-project ordering, and effective invariant computation with all four scopes. ISSUES CLOSED: #9126
1712 lines
62 KiB
Gherkin
1712 lines
62 KiB
Gherkin
Feature: Consolidated Domain Models
|
|
Combined scenarios from: domain_models, enums_coverage, invariant_models, namespaced_project_model, session_model, subplan_model, orguserconfig_coverage
|
|
|
|
# ============================================================
|
|
# Originally from: domain_models.feature
|
|
# Feature: Domain Models Validation
|
|
# ============================================================
|
|
|
|
Scenario: Create valid Project model
|
|
Given I have valid project data
|
|
When I create a Project model
|
|
Then the project model should be created successfully
|
|
And the project name should be validated
|
|
And the project path should be absolute
|
|
|
|
|
|
Scenario: Project name validation
|
|
Given I have project data with invalid name "123!@#"
|
|
When I try to create a Project model
|
|
Then a validation error should be raised
|
|
And the error should mention "Name must be alphanumeric"
|
|
|
|
|
|
Scenario: Create valid Plan model
|
|
Given I have valid plan data
|
|
When I create a Plan model
|
|
Then the plan model should be created successfully
|
|
And the plan status should be "pending"
|
|
And the plan should have timestamps
|
|
|
|
|
|
Scenario: Plan status transitions
|
|
Given I have a plan with status "pending"
|
|
When I update the plan status to "building"
|
|
Then the updated plan status should be "building"
|
|
And the updated_at timestamp should be updated
|
|
|
|
|
|
Scenario: Create valid Context model
|
|
Given I have valid context data
|
|
When I create a Context model
|
|
Then the context model should be created successfully
|
|
And the context type should be "file"
|
|
And the context should have a path
|
|
|
|
|
|
Scenario: Create valid Change model
|
|
Given I have valid change data
|
|
When I create a Change model
|
|
Then the change model should be created successfully
|
|
And the change operation should be valid
|
|
And the change should not be applied by default
|
|
|
|
|
|
Scenario: ChangeSet statistics
|
|
Given I have a changeset with multiple changes
|
|
When I get the changeset statistics
|
|
Then the stats should show correct counts
|
|
And the stats should count creates, modifies, and deletes
|
|
|
|
|
|
Scenario: Import auto-generated models
|
|
When I import the auto-generated models
|
|
Then all models should load without errors
|
|
And the models should have proper Pydantic configuration
|
|
|
|
# TODO: Uncomment when step definitions are implemented
|
|
# Scenario: Create an AIModel with default values
|
|
# Given I create an "AIModel" with the following data:
|
|
# | name | value |
|
|
# | provider_id | test |
|
|
# | model_id | test |
|
|
# Then the "AIModel" object should have the following attributes:
|
|
# | name | value |
|
|
# | provider_id | test |
|
|
# | model_id | test |
|
|
# | is_chat | False |
|
|
# | is_image_to_text | False |
|
|
# | is_multi_modal | False |
|
|
# | is_fine_tunable | False |
|
|
# | is_function_calling | False |
|
|
#
|
|
# TODO: Uncomment when step definitions are implemented
|
|
# Scenario: Create a Provider with default values
|
|
# Given I create a "Provider" with the following data:
|
|
# | name | value |
|
|
# | provider_id | test |
|
|
# | name | test |
|
|
# Then the "Provider" object should have the following attributes:
|
|
# | name | value |
|
|
# | provider_id | test |
|
|
# | name | test |
|
|
#
|
|
# TODO: Uncomment when step definitions are implemented
|
|
# Scenario: Create a CustomAIModel with default values
|
|
# Given I create a "CustomAIModel" with the following data:
|
|
# | name | value |
|
|
# | provider_id | test |
|
|
# | model_id | test |
|
|
# Then the "CustomAIModel" object should have the following attributes:
|
|
# | name | value |
|
|
# | provider_id | test |
|
|
# | model_id | test |
|
|
# | is_chat | False |
|
|
# | is_image_to_text | False |
|
|
# | is_multi_modal | False |
|
|
# | is_fine_tunable | False |
|
|
# | is_function_calling | False |
|
|
|
|
|
|
Scenario: Context update result aggregates statistics
|
|
Given I have context update statistics
|
|
When I create a ContextUpdateResult summary
|
|
Then the context update summary should report correct totals
|
|
And the token diffs should remain non-negative
|
|
|
|
|
|
Scenario: Context update summary parameters accept camelCase payloads
|
|
Given I have summary counts for context updates
|
|
When I create a SummaryForUpdateContextParams model
|
|
Then the summary counts should match the source data
|
|
|
|
|
|
Scenario: Create cloud billing fields from camelCase payload
|
|
Given I have sample cloud billing data
|
|
When I create a CloudBillingFields model
|
|
Then the billing fields should expose decimal values
|
|
|
|
|
|
Scenario: Create organization with billing fields
|
|
Given I have organization data with billing
|
|
When I create an Org domain model
|
|
Then the org should include billing configuration
|
|
|
|
|
|
Scenario: Create a user with default plan config
|
|
Given I have user data referencing a default plan config
|
|
When I create a User model
|
|
Then the user should include the default plan config
|
|
|
|
|
|
Scenario: Create an org role definition
|
|
Given I have an org role definition
|
|
When I create an OrgRole model
|
|
Then the org role should retain its label
|
|
|
|
|
|
Scenario: Create an org user association
|
|
Given I have an org user association
|
|
When I create an OrgUser model
|
|
Then the org user config should be attached
|
|
|
|
|
|
Scenario: Create an invite record with timestamps
|
|
Given I have invite metadata
|
|
When I create an Invite model
|
|
Then the invite should track acceptance timestamps
|
|
|
|
|
|
Scenario: Create a credits transaction entry
|
|
Given I have credits transaction data
|
|
When I create a CreditsTransaction model
|
|
Then the credits transaction should track balances
|
|
|
|
|
|
# ============================================================
|
|
# Originally from: enums_coverage.feature
|
|
# Feature: Domain Enums Coverage
|
|
# ============================================================
|
|
|
|
@unit @enums
|
|
Scenario: ModelPublisher enum has all expected values
|
|
Given I import the ModelPublisher enum
|
|
When I check all ModelPublisher values
|
|
Then ModelPublisher.OPENAI should equal "ModelPublisherOpenAI"
|
|
And ModelPublisher.ANTHROPIC should equal "ModelPublisherAnthropic"
|
|
And ModelPublisher.GOOGLE should equal "ModelPublisherGoogle"
|
|
And ModelPublisher.DEEPSEEK should equal "ModelPublisherDeepSeek"
|
|
And ModelPublisher.PERPLEXITY should equal "ModelPublisherPerplexity"
|
|
And ModelPublisher.QWEN should equal "ModelPublisherQwen"
|
|
And ModelPublisher.MISTRAL should equal "ModelPublisherMistral"
|
|
|
|
|
|
@unit @enums
|
|
Scenario: ModelPublisher enum is string-based
|
|
Given I have a ModelPublisher enum value
|
|
When I convert ModelPublisher.OPENAI to string
|
|
Then it should be a string type
|
|
And it should equal "ModelPublisherOpenAI"
|
|
|
|
|
|
@unit @enums
|
|
Scenario: ModelPublisher enum membership check
|
|
Given I import the ModelPublisher enum
|
|
When I check enum membership
|
|
Then "ModelPublisherOpenAI" should be a valid ModelPublisher value
|
|
And "InvalidPublisher" should not be a valid ModelPublisher value
|
|
|
|
|
|
@unit @enums
|
|
Scenario: ModelPublisher enum iteration
|
|
Given I import the ModelPublisher enum
|
|
When I iterate over all ModelPublisher values
|
|
Then I should get 7 enum members
|
|
And each member should be a ModelPublisher instance
|
|
|
|
|
|
@unit @enums
|
|
Scenario: ModelErrKind enum has all expected values
|
|
Given I import the ModelErrKind enum
|
|
When I check all ModelErrKind values
|
|
Then ModelErrKind.OVERLOADED should equal "ErrOverloaded"
|
|
And ModelErrKind.CONTEXT_TOO_LONG should equal "ErrContextTooLong"
|
|
And ModelErrKind.RATE_LIMITED should equal "ErrRateLimited"
|
|
And ModelErrKind.SUBSCRIPTION_QUOTA_EXHAUSTED should equal "ErrSubscriptionQuotaExhausted"
|
|
And ModelErrKind.OTHER should equal "ErrOther"
|
|
And ModelErrKind.CACHE_SUPPORT should equal "ErrCacheSupport"
|
|
|
|
|
|
@unit @enums
|
|
Scenario: ModelErrKind enum is string-based
|
|
Given I have a ModelErrKind enum value
|
|
When I convert ModelErrKind.RATE_LIMITED to string
|
|
Then it should be a string type
|
|
And it should equal "ErrRateLimited"
|
|
|
|
|
|
@unit @enums
|
|
Scenario: ModelErrKind enum membership check
|
|
Given I import the ModelErrKind enum
|
|
When I check enum membership
|
|
Then "ErrOverloaded" should be a valid ModelErrKind value
|
|
And "InvalidError" should not be a valid ModelErrKind value
|
|
|
|
|
|
@unit @enums
|
|
Scenario: ModelErrKind enum iteration
|
|
Given I import the ModelErrKind enum
|
|
When I iterate over all ModelErrKind values
|
|
Then I should get 6 enum members
|
|
And each member should be a ModelErrKind instance
|
|
|
|
|
|
@unit @enums
|
|
Scenario: FallbackType enum has all expected values
|
|
Given I import the FallbackType enum
|
|
When I check all FallbackType values
|
|
Then FallbackType.ERROR should equal "FallbackTypeError"
|
|
And FallbackType.CONTEXT should equal "FallbackTypeContext"
|
|
And FallbackType.PROVIDER should equal "FallbackTypeProvider"
|
|
|
|
|
|
@unit @enums
|
|
Scenario: FallbackType enum is string-based
|
|
Given I have a FallbackType enum value
|
|
When I convert FallbackType.ERROR to string
|
|
Then it should be a string type
|
|
And it should equal "FallbackTypeError"
|
|
|
|
|
|
@unit @enums
|
|
Scenario: FallbackType enum membership check
|
|
Given I import the FallbackType enum
|
|
When I check enum membership
|
|
Then "FallbackTypeError" should be a valid FallbackType value
|
|
And "InvalidFallback" should not be a valid FallbackType value
|
|
|
|
|
|
@unit @enums
|
|
Scenario: FallbackType enum iteration
|
|
Given I import the FallbackType enum
|
|
When I iterate over all FallbackType values
|
|
Then I should get 3 enum members
|
|
And each member should be a FallbackType instance
|
|
|
|
|
|
@unit @enums
|
|
Scenario: ModelProvider enum has all expected values
|
|
Given I import the ModelProvider enum
|
|
When I check all ModelProvider values
|
|
Then ModelProvider.OPENROUTER should equal "ModelProviderOpenRouter"
|
|
And ModelProvider.OPENAI should equal "ModelProviderOpenAI"
|
|
And ModelProvider.ANTHROPIC should equal "ModelProviderAnthropic"
|
|
And ModelProvider.ANTHROPIC_CLAUDE_MAX should equal "ModelProviderAnthropicClaudeMax"
|
|
And ModelProvider.GOOGLE_AI_STUDIO should equal "ModelProviderGoogleAIStudio"
|
|
And ModelProvider.GOOGLE_VERTEX should equal "ModelProviderGoogleVertex"
|
|
And ModelProvider.AZURE_OPENAI should equal "ModelProviderAzureOpenAI"
|
|
And ModelProvider.DEEPSEEK should equal "ModelProviderDeepSeek"
|
|
And ModelProvider.PERPLEXITY should equal "ModelProviderPerplexity"
|
|
And ModelProvider.AMAZON_BEDROCK should equal "ModelProviderAmazonBedrock"
|
|
|
|
|
|
@unit @enums
|
|
Scenario: ModelProvider enum is string-based
|
|
Given I have a ModelProvider enum value
|
|
When I convert ModelProvider.OPENAI to string
|
|
Then it should be a string type
|
|
And it should equal "ModelProviderOpenAI"
|
|
|
|
|
|
@unit @enums
|
|
Scenario: ModelProvider enum membership check
|
|
Given I import the ModelProvider enum
|
|
When I check enum membership
|
|
Then "ModelProviderOpenAI" should be a valid ModelProvider value
|
|
And "InvalidProvider" should not be a valid ModelProvider value
|
|
|
|
|
|
@unit @enums
|
|
Scenario: ModelProvider enum iteration
|
|
Given I import the ModelProvider enum
|
|
When I iterate over all ModelProvider values
|
|
Then I should get 10 enum members
|
|
And each member should be a ModelProvider instance
|
|
|
|
|
|
@unit @enums
|
|
Scenario: Enum value comparison
|
|
Given I have ModelPublisher enum values
|
|
When I compare enum values
|
|
Then ModelPublisher.OPENAI should equal ModelPublisher.OPENAI
|
|
And ModelPublisher.OPENAI should not equal ModelPublisher.ANTHROPIC
|
|
|
|
|
|
@unit @enums
|
|
Scenario: Enum name attribute access
|
|
Given I import the ModelPublisher enum
|
|
When I access the name attribute of ModelPublisher.OPENAI
|
|
Then the name should be "OPENAI"
|
|
|
|
|
|
@unit @enums
|
|
Scenario: Enum value attribute access
|
|
Given I import the ModelPublisher enum
|
|
When I access the value attribute of ModelPublisher.OPENAI
|
|
Then the value should be "ModelPublisherOpenAI"
|
|
|
|
|
|
@unit @enums
|
|
Scenario: Creating enum from string value
|
|
Given I import the ModelProvider enum
|
|
When I create ModelProvider from value "ModelProviderOpenAI"
|
|
Then I should get ModelProvider.OPENAI
|
|
|
|
|
|
@unit @enums
|
|
Scenario: Enum hash consistency
|
|
Given I have ModelErrKind enum values
|
|
When I use enum values as dictionary keys
|
|
Then the enum values should work as dictionary keys
|
|
And the hash should be consistent
|
|
|
|
|
|
@unit @enums
|
|
Scenario: Enum string representation
|
|
Given I have a FallbackType enum value
|
|
When I get the string representation
|
|
Then repr should contain "FallbackType.ERROR"
|
|
And str should equal "FallbackTypeError"
|
|
|
|
# ============================================================
|
|
# Originally from: invariant_models.feature
|
|
# Feature: Invariant Models and Enforcement
|
|
# ============================================================
|
|
|
|
Scenario: Create a global invariant
|
|
When I create an invariant with text "Never delete production data" scope "global" and source "system"
|
|
Then the invariant should be created
|
|
And the invariant scope should be "global"
|
|
And the invariant source name should be "system"
|
|
And the invariant should be active
|
|
|
|
|
|
Scenario: Create a project invariant
|
|
When I create an invariant with text "All API changes need tests" scope "project" and source "myapp"
|
|
Then the invariant should be created
|
|
And the invariant scope should be "project"
|
|
And the invariant source name should be "myapp"
|
|
|
|
|
|
Scenario: Create an action invariant
|
|
When I create an invariant with text "Minimum 80% coverage" scope "action" and source "local/code-coverage"
|
|
Then the invariant should be created
|
|
And the invariant scope should be "action"
|
|
|
|
|
|
Scenario: Create a plan invariant
|
|
When I create an invariant with text "Use Python 3.13 only" scope "plan" and source "PLAN123"
|
|
Then the invariant should be created
|
|
And the invariant scope should be "plan"
|
|
|
|
|
|
Scenario: Invariant gets a ULID id
|
|
When I create an invariant with text "Test constraint" scope "global" and source "system"
|
|
Then the invariant id should be a valid ULID
|
|
|
|
|
|
Scenario: Invariant text is stripped of whitespace
|
|
When I create an invariant with text " padded text " scope "global" and source "system"
|
|
Then the invariant text should be "padded text"
|
|
|
|
# === Empty Invariant Text Rejection ===
|
|
|
|
|
|
Scenario: Empty invariant text is rejected
|
|
When I try to create an invariant with empty text
|
|
Then a validation error should be raised
|
|
And the error should mention "at least 1 character"
|
|
|
|
|
|
Scenario: Whitespace-only invariant text is rejected
|
|
When I try to create an invariant with whitespace-only text
|
|
Then a validation error should be raised
|
|
|
|
# === InvariantScope Enum ===
|
|
|
|
|
|
Scenario: InvariantScope has four values
|
|
Then InvariantScope should have values "global, project, action, plan"
|
|
|
|
# === Merge Precedence (plan > action > project > global) ===
|
|
|
|
|
|
Scenario: Merge with no duplicates preserves all invariants
|
|
Given I have plan invariants
|
|
| text | source |
|
|
| Plan constraint | plan1 |
|
|
And I have project invariants
|
|
| text | source |
|
|
| Project constraint | proj1 |
|
|
And I have global invariants
|
|
| text | source |
|
|
| Global constraint | system |
|
|
When I merge the invariants
|
|
Then the merged set should have 3 invariants
|
|
And the merged invariant at index 0 should have text "Plan constraint"
|
|
And the merged invariant at index 1 should have text "Project constraint"
|
|
And the merged invariant at index 2 should have text "Global constraint"
|
|
|
|
|
|
Scenario: Plan invariant overrides project invariant with same text
|
|
Given I have plan invariants
|
|
| text | source |
|
|
| Log all changes | plan1 |
|
|
And I have project invariants
|
|
| text | source |
|
|
| Log all changes | proj1 |
|
|
And I have global invariants
|
|
| text | source |
|
|
When I merge the invariants
|
|
Then the merged set should have 1 invariants
|
|
And the merged invariant at index 0 should have scope "plan"
|
|
|
|
|
|
Scenario: Project invariant overrides global invariant with same text
|
|
Given I have plan invariants
|
|
| text | source |
|
|
And I have project invariants
|
|
| text | source |
|
|
| Log all changes | proj1 |
|
|
And I have global invariants
|
|
| text | source |
|
|
| Log all changes | system |
|
|
When I merge the invariants
|
|
Then the merged set should have 1 invariants
|
|
And the merged invariant at index 0 should have scope "project"
|
|
|
|
|
|
Scenario: Merge precedence is case-insensitive for de-duplication
|
|
Given I have plan invariants
|
|
| text | source |
|
|
| LOG ALL CHANGES | plan1 |
|
|
And I have global invariants
|
|
| text | source |
|
|
| log all changes | system |
|
|
And I have project invariants
|
|
| text | source |
|
|
When I merge the invariants
|
|
Then the merged set should have 1 invariants
|
|
And the merged invariant at index 0 should have text "LOG ALL CHANGES"
|
|
|
|
|
|
Scenario: Merge with all four scopes preserves action tier — Issue #9126
|
|
Given I have plan invariants
|
|
| text | source |
|
|
| Plan rule | plan1 |
|
|
And I have action invariants
|
|
| text | source |
|
|
| Action rule | action1 |
|
|
And I have project invariants
|
|
| text | source |
|
|
| Project rule | proj1 |
|
|
And I have global invariants
|
|
| text | source |
|
|
| Global rule | system |
|
|
When I merge the invariants
|
|
Then the merged set should have 4 invariants
|
|
And plan invariants appear before action invariants in merge
|
|
And action invariants appear before project invariants in merge
|
|
And the merged invariant at index 0 should have text "Plan rule"
|
|
And the merged invariant at index 1 should have text "Action rule"
|
|
And the merged invariant at index 2 should have text "Project rule"
|
|
And the merged invariant at index 3 should have text "Global rule"
|
|
|
|
|
|
Scenario: Action invariant overrides project with same text
|
|
Given I have plan invariants
|
|
| text | source |
|
|
And I have action invariants
|
|
| text | source |
|
|
| Log all changes | action1 |
|
|
And I have project invariants
|
|
| text | source |
|
|
| Log all changes | proj1 |
|
|
And I have global invariants
|
|
| text | source |
|
|
When I merge the invariants
|
|
Then the merged set should have 1 invariants
|
|
And the merged invariant at index 0 should have scope "action"
|
|
|
|
|
|
Scenario: Action invariant is included in de-duplication with plan override
|
|
Given I have plan invariants
|
|
| text | source |
|
|
| Log all changes | plan1 |
|
|
And I have action invariants
|
|
| text | source |
|
|
| Log all changes | action1 |
|
|
And I have project invariants
|
|
| text | source |
|
|
And I have global invariants
|
|
| text | source |
|
|
When I merge the invariants
|
|
Then the merged set should have 1 invariants
|
|
And the merged invariant at index 0 should have scope "plan"
|
|
|
|
|
|
Scenario: Inactive action invariants are excluded from merge
|
|
Given I have plan invariants with an inactive entry
|
|
And I have action invariants
|
|
| text | source |
|
|
| Active | action1 |
|
|
And I have project invariants
|
|
| text | source |
|
|
And I have global invariants
|
|
| text | source |
|
|
When I merge the invariants
|
|
Then the merged set should have 1 invariants
|
|
And the merged invariant at index 0 should have scope "action"
|
|
|
|
|
|
Scenario: Inactive invariants are excluded from merge
|
|
Given I have plan invariants with an inactive entry
|
|
And I have project invariants
|
|
| text | source |
|
|
And I have global invariants
|
|
| text | source |
|
|
When I merge the invariants
|
|
Then the merged set should have 0 invariants
|
|
|
|
# === InvariantSet merge class method ===
|
|
|
|
|
|
Scenario: InvariantSet.merge produces correct result
|
|
Given I have plan invariants
|
|
| text | source |
|
|
| Plan rule | plan1 |
|
|
And I have project invariants
|
|
| text | source |
|
|
| Project rule | proj1 |
|
|
And I have global invariants
|
|
| text | source |
|
|
| Global rule | system |
|
|
When I merge using InvariantSet
|
|
Then the invariant set should have 3 invariants
|
|
|
|
|
|
Scenario: InvariantSet.merge with four-tier precedence — Issue #9126
|
|
Given I have plan invariants
|
|
| text | source |
|
|
| Plan rule | plan1 |
|
|
And I have action invariants
|
|
| text | source |
|
|
| Action rule | action1 |
|
|
And I have project invariants
|
|
| text | source |
|
|
| Project rule | proj1 |
|
|
And I have global invariants
|
|
| text | source |
|
|
| Global rule | system |
|
|
When I merge using InvariantSet
|
|
Then the invariant set should have 4 invariants
|
|
|
|
|
|
# === Service: Add/List/Remove ===
|
|
|
|
|
|
Scenario: Service add invariant
|
|
Given an invariant service
|
|
When I add an invariant via service with text "No secrets in logs" scope "global" source "system"
|
|
Then the service should return the created invariant
|
|
And the service invariant text should be "No secrets in logs"
|
|
|
|
|
|
Scenario: Service add invariant rejects empty text
|
|
Given an invariant service
|
|
When I try to add an invariant via service with empty text
|
|
Then a service validation error should be raised
|
|
|
|
|
|
Scenario: Service add invariant rejects empty source
|
|
Given an invariant service
|
|
When I try to add an invariant via service with empty source
|
|
Then a service validation error should be raised
|
|
|
|
|
|
Scenario: Service list all invariants
|
|
Given an invariant service with 3 invariants
|
|
When I list all invariants via service
|
|
Then the service should return 3 invariants
|
|
|
|
|
|
Scenario: Service list invariants filtered by scope
|
|
Given an invariant service with mixed scopes
|
|
When I list invariants via service with scope "global"
|
|
Then all returned invariants should have scope "global"
|
|
|
|
|
|
Scenario: Service list invariants filtered by source
|
|
Given an invariant service with mixed sources
|
|
When I list invariants via service with source "myapp"
|
|
Then all returned invariants should have source "myapp"
|
|
|
|
|
|
Scenario: Service remove invariant soft-deletes
|
|
Given an invariant service with 1 invariant
|
|
When I remove the invariant via service
|
|
Then the invariant should be inactive
|
|
And listing active invariants should return 0
|
|
|
|
|
|
Scenario: Service remove non-existent invariant raises not found
|
|
Given an invariant service
|
|
When I try to remove a non-existent invariant
|
|
Then a not found error should be raised
|
|
|
|
|
|
Scenario: Service remove with empty ID raises validation error
|
|
Given an invariant service
|
|
When I try to remove an invariant with empty ID
|
|
Then a service validation error should be raised
|
|
|
|
# === Effective Invariant Computation ===
|
|
|
|
|
|
Scenario: Effective invariants for a plan with project context
|
|
Given an invariant service with invariants at all scopes
|
|
When I get effective invariants for plan "plan1" and project "proj1"
|
|
Then the effective set should contain plan invariants first
|
|
And the effective set should contain project invariants second
|
|
And the effective set should contain global invariants last
|
|
|
|
|
|
Scenario: Effective invariants include action scope — Issue #9126
|
|
Given an invariant service with invariants at all four scopes
|
|
When I get effective invariants for plan "plan1", action "action1", and project "proj1"
|
|
Then the effective set should contain plan invariants first
|
|
And the effective set should contain action invariants second
|
|
And the effective set should contain project invariants after action
|
|
And the effective set should contain global invariants last
|
|
And the effective set should have 4 invariants
|
|
|
|
|
|
Scenario: Effective invariants de-duplicate across scopes
|
|
Given an invariant service with duplicate text across scopes
|
|
When I get effective invariants for plan "plan1" and project "proj1"
|
|
Then the effective set should have no duplicates
|
|
|
|
# === Enforcement Record Creation ===
|
|
|
|
|
|
Scenario: Enforce invariants creates records
|
|
Given an invariant service with 2 invariants
|
|
When I enforce invariants for plan "PLAN_XYZ"
|
|
Then 2 enforcement records should be created
|
|
And each enforcement record should be marked as enforced
|
|
And each enforcement record should have a decision ID
|
|
|
|
|
|
Scenario: Enforce invariants with actor response
|
|
Given an invariant service with 1 invariant
|
|
When I enforce invariants with actor response "All constraints satisfied"
|
|
Then the enforcement record actor response should be "All constraints satisfied"
|
|
|
|
|
|
Scenario: Enforce invariants with empty plan ID raises error
|
|
Given an invariant service
|
|
When I try to enforce invariants with empty plan ID
|
|
Then a service validation error should be raised
|
|
|
|
# === InvariantViolation Model ===
|
|
|
|
|
|
Scenario: Create an invariant violation
|
|
When I create an invariant violation with severity "error"
|
|
Then the violation should be created
|
|
And the invariant violation severity should be "error"
|
|
|
|
|
|
Scenario: Violation severity is case-insensitive
|
|
When I create an invariant violation with severity "WARNING"
|
|
Then the invariant violation severity should be "warning"
|
|
|
|
|
|
Scenario: Invalid violation severity is rejected
|
|
When I try to create a violation with invalid severity "critical"
|
|
Then a validation error should be raised
|
|
|
|
# === InvariantEnforcementRecord Model ===
|
|
|
|
|
|
Scenario: Create an enforcement record
|
|
When I create an enforcement record with enforced true
|
|
Then the record should be created
|
|
And the record enforced flag should be true
|
|
|
|
|
|
Scenario: Create an enforcement record with enforced false
|
|
When I create an enforcement record with enforced false
|
|
Then the record enforced flag should be false
|
|
|
|
|
|
|
|
# ============================================================
|
|
# Immutability contract (frozen=True) — Issue #3116
|
|
# ============================================================
|
|
|
|
Scenario: Invariant model is immutable after creation
|
|
When I create an invariant with text "Never delete production data" scope "global" and source "system"
|
|
Then mutating the invariant active field should raise an error
|
|
|
|
Scenario: InvariantViolation model is immutable after creation
|
|
When I create an invariant violation with severity "error"
|
|
Then mutating the violation severity field should raise an error
|
|
|
|
Scenario: InvariantEnforcementRecord model is immutable after creation
|
|
When I create an enforcement record with enforced true
|
|
Then mutating the record enforced field should raise an error
|
|
|
|
Scenario: Invariant model is hashable
|
|
When I create an invariant with text "Hashable constraint" scope "global" and source "system"
|
|
Then the invariant should be hashable
|
|
|
|
Scenario: InvariantViolation model is hashable
|
|
When I create an invariant violation with severity "warning"
|
|
Then the violation should be hashable
|
|
|
|
Scenario: InvariantEnforcementRecord model is hashable
|
|
When I create an enforcement record with enforced true
|
|
Then the record should be hashable
|
|
|
|
Scenario: InvariantSet model is immutable after creation
|
|
Given I have plan invariants
|
|
| text | source |
|
|
| Plan rule | plan1 |
|
|
When I merge using InvariantSet
|
|
Then mutating the invariant set invariants field should raise an error
|
|
|
|
Scenario: InvariantSet model is hashable
|
|
Given I have plan invariants
|
|
| text | source |
|
|
| Hashable rule | plan1 |
|
|
When I merge using InvariantSet
|
|
Then the invariant set should be hashable
|
|
|
|
Scenario: Service soft-delete creates new instance with active=False
|
|
Given an invariant service with 1 invariant
|
|
When I remove the invariant via service
|
|
Then the invariant should be inactive
|
|
And listing active invariants should return 0
|
|
And the removed invariant is a different object than the original
|
|
And the original invariant should still be active
|
|
|
|
# ============================================================
|
|
# Originally from: namespaced_project_model.feature
|
|
# Feature: Namespaced Project Domain Model
|
|
# ============================================================
|
|
|
|
Scenario: Parse bare name defaults to local namespace
|
|
When I parse the project namespaced name "my-project"
|
|
Then the nsproject parsed namespace should be "local"
|
|
And the nsproject parsed name should be "my-project"
|
|
And the nsproject parsed server should be empty
|
|
|
|
|
|
Scenario: Parse name with explicit namespace
|
|
When I parse the project namespaced name "freemo/code-coverage"
|
|
Then the nsproject parsed namespace should be "freemo"
|
|
And the nsproject parsed name should be "code-coverage"
|
|
And the nsproject parsed server should be empty
|
|
|
|
|
|
Scenario: Parse name with server qualifier
|
|
When I parse the project namespaced name "dev:freemo/code-coverage"
|
|
Then the nsproject parsed server should be "dev"
|
|
And the nsproject parsed namespace should be "freemo"
|
|
And the nsproject parsed name should be "code-coverage"
|
|
|
|
|
|
Scenario: Parse local namespace
|
|
When I parse the project namespaced name "local/my-project"
|
|
Then the nsproject parsed namespace should be "local"
|
|
And the nsproject parsed name should be "my-project"
|
|
|
|
|
|
Scenario: ParsedName qualified_name with server
|
|
When I parse the project namespaced name "prod:myorg/api"
|
|
Then the nsproject qualified name should be "prod:myorg/api"
|
|
|
|
|
|
Scenario: ParsedName qualified_name without server
|
|
When I parse the project namespaced name "myorg/api"
|
|
Then the nsproject qualified name should be "myorg/api"
|
|
|
|
|
|
Scenario: ParsedName namespaced_name
|
|
When I parse the project namespaced name "dev:myorg/api"
|
|
Then the nsproject namespaced name should be "myorg/api"
|
|
|
|
|
|
Scenario: ParsedName is_local for bare name
|
|
When I parse the project namespaced name "my-project"
|
|
Then the nsproject parsed name should be local
|
|
|
|
|
|
Scenario: ParsedName is_remote with server
|
|
When I parse the project namespaced name "dev:myorg/api"
|
|
Then the nsproject parsed name should be remote
|
|
|
|
|
|
Scenario: ParsedName is_remote with non-local namespace
|
|
When I parse the project namespaced name "freemo/api"
|
|
Then the nsproject parsed name should be remote
|
|
|
|
|
|
Scenario: Reject empty name
|
|
When I try to parse an empty project namespaced name
|
|
Then a nsproject validation error should be raised
|
|
|
|
|
|
Scenario: Reject reserved namespace system
|
|
When I try to parse the project namespaced name "system/my-project"
|
|
Then a nsproject validation error should be raised
|
|
|
|
|
|
Scenario: Reject reserved namespace admin
|
|
When I try to parse the project namespaced name "admin/my-project"
|
|
Then a nsproject validation error should be raised
|
|
|
|
|
|
Scenario: Reject provider namespace openai
|
|
When I try to parse the project namespaced name "openai/my-project"
|
|
Then a nsproject validation error should be raised
|
|
|
|
|
|
Scenario: Reject provider namespace anthropic
|
|
When I try to parse the project namespaced name "anthropic/my-model"
|
|
Then a nsproject validation error should be raised
|
|
|
|
|
|
Scenario: Reject invalid characters in name
|
|
When I try to parse the project namespaced name "my@project"
|
|
Then a nsproject validation error should be raised
|
|
|
|
# TemporalScope enum
|
|
|
|
|
|
Scenario: TemporalScope has three values
|
|
Then the nsproject TemporalScope enum should have values "current, recent, all"
|
|
|
|
# ContextConfig model
|
|
|
|
|
|
Scenario: ContextConfig defaults
|
|
Given a default ContextConfig
|
|
Then the nsproject ctx max_file_size should be 1000000
|
|
And the nsproject ctx max_total_size should be 52428800
|
|
And the nsproject ctx indexing_strategy should be "full_text"
|
|
And the nsproject ctx chunking_policy should be "smart"
|
|
And the nsproject ctx chunk_size should be 1000
|
|
And the nsproject ctx summarize should be true
|
|
And the nsproject ctx auto_refresh should be true
|
|
And the nsproject ctx temporal_scope should be "current"
|
|
And the nsproject ctx retention_policy should be empty
|
|
|
|
|
|
Scenario: ContextConfig merges default ignore patterns
|
|
Given a ContextConfig with custom ignore patterns
|
|
Then the nsproject ctx ignore patterns should include defaults
|
|
And the nsproject ctx ignore patterns should include the custom pattern
|
|
|
|
|
|
Scenario: ContextConfig with memory tier settings
|
|
Given a ContextConfig with hot_max_tokens 4096 and warm_max_decisions 50
|
|
Then the nsproject ctx hot_max_tokens should be 4096
|
|
And the nsproject ctx warm_max_decisions should be 50
|
|
|
|
|
|
Scenario: ContextConfig is frozen
|
|
Given a default ContextConfig
|
|
When I try to mutate a ContextConfig field
|
|
Then a nsproject validation error should be raised
|
|
|
|
|
|
Scenario: ContextConfig rejects zero max_file_size
|
|
When I try to create a ContextConfig with max_file_size 0
|
|
Then a nsproject validation error should be raised
|
|
|
|
|
|
Scenario: ContextConfig rejects negative chunk_size
|
|
When I try to create a ContextConfig with chunk_size -1
|
|
Then a nsproject validation error should be raised
|
|
|
|
# LinkedResource model
|
|
|
|
|
|
Scenario: Create a LinkedResource with defaults
|
|
Given a LinkedResource with resource_id "01HZQX7K8M3N4P5R6S7T8V9W0X"
|
|
Then the nsproject linked rid should be "01HZQX7K8M3N4P5R6S7T8V9W0X"
|
|
And the nsproject linked read_only should be false
|
|
And the nsproject linked alias should be empty
|
|
And the nsproject linked_at should be set
|
|
|
|
|
|
Scenario: Create a LinkedResource with alias
|
|
Given a LinkedResource with id "01HZQX7K8M3N4P5R6S7T8V9W0X" and alias "main-repo"
|
|
Then the nsproject linked alias should be "main-repo"
|
|
|
|
|
|
Scenario: LinkedResource rejects invalid ULID
|
|
When I try to create a LinkedResource with resource_id "invalid"
|
|
Then a nsproject validation error should be raised
|
|
|
|
|
|
Scenario: LinkedResource rejects empty alias
|
|
When I try to create a LinkedResource with empty alias
|
|
Then a nsproject validation error should be raised
|
|
|
|
|
|
Scenario: LinkedResource is frozen
|
|
Given a LinkedResource with resource_id "01HZQX7K8M3N4P5R6S7T8V9W0X"
|
|
When I try to mutate a LinkedResource field
|
|
Then a nsproject validation error should be raised
|
|
|
|
# NamespacedProject model
|
|
|
|
|
|
Scenario: Create a minimal NamespacedProject
|
|
Given a NamespacedProject with name "my-project"
|
|
Then the nsproject name should be "my-project"
|
|
And the nsproject namespace should be "local"
|
|
And the nsproject server should be empty
|
|
And the nsproject namespaced name should be "local/my-project"
|
|
And the nsproject qualified name should be "local/my-project"
|
|
And the nsproject linked resources should be empty
|
|
And the nsproject created_at should be set
|
|
And the nsproject updated_at should be set
|
|
|
|
|
|
Scenario: Create a NamespacedProject with namespace
|
|
Given a project "api" under namespace "freemo"
|
|
Then the nsproject namespace should be "freemo"
|
|
And the nsproject namespaced name should be "freemo/api"
|
|
|
|
|
|
Scenario: Create a NamespacedProject with server
|
|
Given a project "api" under namespace "freemo" on server "dev"
|
|
Then the nsproject server should be "dev"
|
|
And the nsproject qualified name should be "dev:freemo/api"
|
|
|
|
|
|
Scenario: NamespacedProject is_local
|
|
Given a NamespacedProject with name "my-project"
|
|
Then the nsproject should be local
|
|
|
|
|
|
Scenario: NamespacedProject is_remote with namespace
|
|
Given a project "api" under namespace "freemo"
|
|
Then the nsproject should be remote
|
|
|
|
|
|
Scenario: NamespacedProject is_remote with server
|
|
Given a project "api" under namespace "freemo" on server "dev"
|
|
Then the nsproject should be remote
|
|
|
|
# NamespacedProject - validation
|
|
|
|
|
|
Scenario: NamespacedProject rejects invalid name
|
|
When I try to create a NamespacedProject with name "123invalid"
|
|
Then a nsproject validation error should be raised
|
|
|
|
|
|
Scenario: NamespacedProject rejects reserved namespace
|
|
When I try to create a NamespacedProject with namespace "system"
|
|
Then a nsproject validation error should be raised
|
|
|
|
|
|
Scenario: NamespacedProject rejects provider namespace
|
|
When I try to create a NamespacedProject with namespace "openai"
|
|
Then a nsproject validation error should be raised
|
|
|
|
|
|
Scenario: NamespacedProject rejects duplicate linked resources
|
|
When I try to create a NamespacedProject with duplicate linked resources
|
|
Then a nsproject validation error should be raised
|
|
|
|
# NamespacedProject - domain methods
|
|
|
|
|
|
Scenario: Link a resource to a project
|
|
Given a NamespacedProject with name "my-project"
|
|
When I link resource "01HZQX7K8M3N4P5R6S7T8V9W0X" to the project
|
|
Then the nsproject should have 1 linked resource
|
|
And the nsproject first linked resource_id should be "01HZQX7K8M3N4P5R6S7T8V9W0X"
|
|
|
|
|
|
Scenario: Link a resource with read_only and alias
|
|
Given a NamespacedProject with name "my-project"
|
|
When I link resource "01HZQX7K8M3N4P5R6S7T8V9W0X" with read_only true and alias "docs"
|
|
Then the nsproject should have 1 linked resource
|
|
And the nsproject first linked resource should be read_only
|
|
And the nsproject first linked resource alias should be "docs"
|
|
|
|
|
|
Scenario: Unlink a resource from a project
|
|
Given a NamespacedProject with name "my-project"
|
|
And I link resource "01HZQX7K8M3N4P5R6S7T8V9W0X" to the project
|
|
When I unlink resource "01HZQX7K8M3N4P5R6S7T8V9W0X" from the project
|
|
Then the nsproject linked resources should be empty
|
|
|
|
|
|
Scenario: Link duplicate resource fails
|
|
Given a NamespacedProject with name "my-project"
|
|
And I link resource "01HZQX7K8M3N4P5R6S7T8V9W0X" to the project
|
|
When I try to link resource "01HZQX7K8M3N4P5R6S7T8V9W0X" again
|
|
Then a nsproject validation error should be raised
|
|
|
|
|
|
Scenario: Unlink non-existent resource fails
|
|
Given a NamespacedProject with name "my-project"
|
|
When I try to unlink resource "01HZQX7K8M3N4P5R6S7T8V9W0X" from the project
|
|
Then a nsproject validation error should be raised
|
|
|
|
|
|
Scenario: Get linked resource by resource_id
|
|
Given a NamespacedProject with name "my-project"
|
|
And I link resource "01HZQX7K8M3N4P5R6S7T8V9W0X" to the project
|
|
When I get linked resource "01HZQX7K8M3N4P5R6S7T8V9W0X" from the project
|
|
Then the nsproject retrieved link should not be empty
|
|
|
|
|
|
Scenario: Get linked resource by alias
|
|
Given a NamespacedProject with name "my-project"
|
|
And I link resource "01HZQX7K8M3N4P5R6S7T8V9W0X" with read_only false and alias "main"
|
|
When I get linked resource by alias "main" from the project
|
|
Then the nsproject retrieved link should not be empty
|
|
And the nsproject retrieved link alias should be "main"
|
|
|
|
|
|
Scenario: Get non-existent linked resource returns None
|
|
Given a NamespacedProject with name "my-project"
|
|
When I get linked resource "01HZQX7K8M3N4P5R6S7T8V9W0X" from the project
|
|
Then the nsproject retrieved link should be empty
|
|
|
|
|
|
Scenario: Link resource with empty resource_id fails
|
|
Given a NamespacedProject with name "my-project"
|
|
When I try to link an empty resource_id to the project
|
|
Then a nsproject validation error should be raised
|
|
|
|
|
|
Scenario: Unlink resource with empty resource_id fails
|
|
Given a NamespacedProject with name "my-project"
|
|
When I try to unlink an empty resource_id from the project
|
|
Then a nsproject validation error should be raised
|
|
|
|
|
|
# ============================================================
|
|
# Originally from: session_model.feature
|
|
# Feature: Session Domain Model
|
|
# ============================================================
|
|
|
|
Scenario: Create a session with valid ULID
|
|
When I create a session with a valid ULID
|
|
Then the session model should be created
|
|
And the session model session_id should be a valid ULID
|
|
|
|
|
|
Scenario: Create a session with actor name
|
|
When I create a session with actor name "local/orchestrator"
|
|
Then the session model should be created
|
|
And the session model actor_name should be "local/orchestrator"
|
|
|
|
|
|
Scenario: Create a session without actor name
|
|
When I create a session without actor name
|
|
Then the session model should be created
|
|
And the session model actor_name should be empty
|
|
|
|
|
|
Scenario: Session has default namespace local
|
|
When I create a session with a valid ULID
|
|
Then the session model namespace should be "local"
|
|
|
|
# ---- Session ID Validation ----
|
|
|
|
|
|
Scenario: Session rejects invalid ULID format
|
|
When I try to create a session with invalid ID "not-a-ulid"
|
|
Then a session model validation error should be raised
|
|
|
|
# ---- Actor Name Validation ----
|
|
|
|
|
|
Scenario: Session rejects invalid actor name format
|
|
When I try to create a session with invalid actor name "badformat"
|
|
Then a session model validation error should be raised
|
|
|
|
|
|
Scenario: Session accepts None actor name
|
|
When I create a session without actor name
|
|
Then the session model should be created
|
|
|
|
# ---- Message Append with Auto-sequencing ----
|
|
|
|
|
|
Scenario: Append a user message to session
|
|
Given a session with no messages
|
|
When I append a user message "Hello"
|
|
Then the session model should have 1 message
|
|
And the session last message content should be "Hello"
|
|
And the session last message role should be "user"
|
|
And the session last message sequence should be 0
|
|
|
|
|
|
Scenario: Append multiple messages with auto-sequencing
|
|
Given a session with no messages
|
|
When I append a user message "Hello"
|
|
And I append an assistant message "Hi there"
|
|
Then the session model should have 2 messages
|
|
And the session last message sequence should be 1
|
|
|
|
# ---- Message Role Validation ----
|
|
|
|
|
|
Scenario: Tool message requires tool_call_id
|
|
When I try to create a tool message without tool_call_id
|
|
Then a session model validation error should be raised
|
|
And the session model error should mention "tool_call_id"
|
|
|
|
|
|
Scenario: Tool message with tool_call_id is valid
|
|
When I create a tool message with tool_call_id "call_123"
|
|
Then the session message should be created
|
|
And the session message tool_call_id should be "call_123"
|
|
|
|
|
|
Scenario: User message does not require tool_call_id
|
|
When I create a user message "Hello"
|
|
Then the session message should be created
|
|
|
|
# ---- Content Validation ----
|
|
|
|
|
|
Scenario: Message rejects whitespace-only content
|
|
When I try to create a message with whitespace-only content
|
|
Then a session model validation error should be raised
|
|
And the session model error should mention "whitespace"
|
|
|
|
|
|
Scenario: Message rejects empty content
|
|
When I try to create a message with empty content
|
|
Then a session model validation error should be raised
|
|
|
|
# ---- Message Ordering ----
|
|
|
|
|
|
Scenario: Messages must be ordered by sequence
|
|
When I try to create a session with out-of-order messages
|
|
Then a session model validation error should be raised
|
|
And the session model error should mention "ordered by sequence"
|
|
|
|
# ---- Plan Linking ----
|
|
|
|
|
|
Scenario: Link a plan to session
|
|
Given a session with no messages
|
|
When I link plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" to the session
|
|
Then the session should have 1 linked plan
|
|
|
|
|
|
Scenario: Plan linking deduplicates
|
|
Given a session with no messages
|
|
When I link plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" to the session
|
|
And I link plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" to the session
|
|
Then the session should have 1 linked plan
|
|
|
|
|
|
Scenario: Link multiple plans
|
|
Given a session with no messages
|
|
When I link plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" to the session
|
|
And I link plan "01BX5ZZKBKACTAV9WEVGEMMVRY" to the session
|
|
Then the session should have 2 linked plans
|
|
|
|
# ---- Token Usage Tracking ----
|
|
|
|
|
|
Scenario: Default token usage is zero
|
|
When I create a session with a valid ULID
|
|
Then the session token usage input_tokens should be 0
|
|
And the session token usage output_tokens should be 0
|
|
And the session token usage estimated_cost should be 0.0
|
|
|
|
|
|
Scenario: Token usage with values
|
|
When I create a session with token usage 100 input 50 output 0.05 cost
|
|
Then the session token usage input_tokens should be 100
|
|
And the session token usage output_tokens should be 50
|
|
And the session token usage estimated_cost should be 0.05
|
|
|
|
|
|
Scenario: Token usage rejects negative values
|
|
When I try to create token usage with negative input tokens
|
|
Then a session model validation error should be raised
|
|
|
|
# ---- Export Dict ----
|
|
|
|
|
|
Scenario: Export dict contains checksum
|
|
Given a session with some messages
|
|
When I export the session
|
|
Then the export dict should have key "checksum"
|
|
And the export dict should have key "schema_version"
|
|
And the export dict should have key "messages"
|
|
And the export dict should have key "session_id"
|
|
|
|
|
|
Scenario: Export dict round-trip preserves data
|
|
Given a session with some messages
|
|
When I export the session
|
|
Then the export dict messages count should match session message count
|
|
|
|
# ---- CLI Dict ----
|
|
|
|
|
|
Scenario: CLI dict has required fields
|
|
Given a session with some messages
|
|
When I get the session model CLI dict
|
|
Then the session cli dict should have key "session_summary"
|
|
And the session cli dict should have key "token_usage"
|
|
And the session cli dict session_summary should have key "id"
|
|
And the session cli dict session_summary should have key "messages"
|
|
And the session cli dict session_summary should have key "created"
|
|
And the session cli dict session_summary should have key "updated"
|
|
|
|
|
|
Scenario: CLI dict includes recent messages
|
|
Given a session with some messages
|
|
When I get the session model CLI dict
|
|
Then the session cli dict should have key "recent_messages"
|
|
And the session cli dict recent_messages text key should be "text"
|
|
|
|
|
|
Scenario: CLI dict includes actor when set
|
|
When I create a session with actor name "local/orchestrator"
|
|
And I get the session model CLI dict
|
|
Then the session cli dict session_summary should have key "actor"
|
|
|
|
|
|
Scenario: CLI dict includes automation when set
|
|
When I create a session with automation "review"
|
|
And I get the session model CLI dict
|
|
Then the session cli dict session_summary should have key "automation"
|
|
And the session cli dict session_summary automation should be "review"
|
|
|
|
|
|
Scenario: CLI dict linked_plans uses spec-compliant objects
|
|
Given a session with linked plans
|
|
When I get the session model CLI dict
|
|
Then the session cli dict should have key "linked_plans"
|
|
And the session cli dict linked_plans should contain plan_id field
|
|
|
|
|
|
Scenario: CLI dict token_usage estimated_cost is formatted string
|
|
Given a session with token usage cost 0.0184
|
|
When I get the session model CLI dict
|
|
Then the session cli dict token_usage estimated_cost should be a string starting with "$"
|
|
|
|
# ---- Empty Session Properties ----
|
|
|
|
|
|
Scenario: Empty session has zero message count
|
|
Given a session with no messages
|
|
Then the session message count should be 0
|
|
And the session is_empty should be true
|
|
And the session last_message should be none
|
|
|
|
# ---- get_messages with limit/offset ----
|
|
|
|
|
|
Scenario: get_messages with no limit returns all
|
|
Given a session with 5 messages
|
|
When I get messages with no limit
|
|
Then I should get 5 messages
|
|
|
|
|
|
Scenario: get_messages with limit
|
|
Given a session with 5 messages
|
|
When I get messages with limit 3
|
|
Then I should get 3 messages
|
|
|
|
|
|
Scenario: get_messages with offset
|
|
Given a session with 5 messages
|
|
When I get messages with offset 2
|
|
Then I should get 3 messages
|
|
|
|
|
|
Scenario: get_messages with limit and offset
|
|
Given a session with 5 messages
|
|
When I get messages with limit 2 and offset 1
|
|
Then I should get 2 messages
|
|
|
|
# ---- Session Error Types ----
|
|
|
|
|
|
Scenario: SessionServiceError is base error
|
|
Then SessionServiceError should be an Exception subclass
|
|
|
|
|
|
Scenario: SessionNotFoundError inherits from SessionServiceError
|
|
Then SessionNotFoundError should be a SessionServiceError subclass
|
|
|
|
|
|
Scenario: SessionExportError inherits from SessionServiceError
|
|
Then SessionExportError should be a SessionServiceError subclass
|
|
|
|
|
|
Scenario: SessionImportError inherits from SessionServiceError
|
|
Then SessionImportError should be a SessionServiceError subclass
|
|
|
|
# ---- MessageRole enum ----
|
|
|
|
|
|
Scenario: MessageRole has expected values
|
|
Then the MessageRole enum should have values "user,assistant,system,tool"
|
|
|
|
# ---- Session metadata ----
|
|
|
|
|
|
Scenario: Session supports metadata
|
|
When I create a session with metadata
|
|
Then the session metadata should contain key "env"
|
|
|
|
# ---- Export dict linked plans ----
|
|
|
|
|
|
Scenario: Export dict includes linked plan IDs
|
|
Given a session with no messages
|
|
When I link plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" to the session
|
|
And I export the session
|
|
Then the export dict should have key "linked_plan_ids"
|
|
|
|
# ---- Robot Smoke Alignment: Serialization Order ----
|
|
|
|
|
|
Scenario: Export dict keys are in expected serialization order
|
|
Given a session with some messages
|
|
When I export the session
|
|
Then the export dict keys should be in expected serialization order
|
|
|
|
|
|
Scenario: Export dict messages are ordered by sequence
|
|
Given a session with some messages
|
|
When I export the session
|
|
Then the export dict messages should be ordered by sequence
|
|
|
|
|
|
# ============================================================
|
|
# Originally from: subplan_model.feature
|
|
# Feature: Subplan Model Hierarchy and Configuration
|
|
# ============================================================
|
|
|
|
Scenario: Root plan has no parent and root equals self
|
|
Given I create a subplan-test root plan
|
|
Then the subplan-test plan should not be a subplan
|
|
And the subplan-test plan should be a root plan
|
|
And the subplan-test plan depth should be 0
|
|
|
|
|
|
Scenario: Root plan with explicit root_plan_id equal to plan_id
|
|
Given I create a subplan-test plan with root_plan_id equal to plan_id
|
|
Then the subplan-test plan should be a root plan
|
|
And the subplan-test plan should not be a subplan
|
|
|
|
# Child plan identity
|
|
|
|
|
|
Scenario: Child plan with parent_plan_id set is a subplan
|
|
Given I create a subplan-test child plan with a parent
|
|
Then the subplan-test plan should be a subplan
|
|
And the subplan-test plan should not be a root plan
|
|
And the subplan-test plan attempt should be 1
|
|
|
|
|
|
Scenario: Root plan id propagates to nested children
|
|
Given I create a subplan-test three-level hierarchy
|
|
Then the subplan-test grandchild root_plan_id should match the root plan_id
|
|
|
|
# Default values for new plans
|
|
|
|
|
|
Scenario: New plan defaults to strategize phase and queued state
|
|
Given I create a subplan-test default plan
|
|
Then the subplan-test plan phase should be "strategize"
|
|
And the subplan-test plan processing state should be "queued"
|
|
And the subplan-test plan should not be terminal
|
|
And the subplan-test plan should not be errored
|
|
|
|
|
|
Scenario: New plan defaults to no automation profile
|
|
Given I create a subplan-test default plan
|
|
Then the subplan-test plan automation profile should be none
|
|
|
|
|
|
Scenario: New plan has no subplan config by default
|
|
Given I create a subplan-test default plan
|
|
Then the subplan-test plan subplan_config should be none
|
|
|
|
|
|
Scenario: New plan has empty subplan statuses by default
|
|
Given I create a subplan-test default plan
|
|
Then the subplan-test plan should have 0 subplan statuses
|
|
|
|
# Subplan config storage and retrieval
|
|
|
|
|
|
Scenario: Plan stores sequential subplan config
|
|
Given I create a subplan-test plan with sequential subplan config
|
|
Then the subplan-test plan subplan_config execution_mode should be "sequential"
|
|
And the subplan-test plan subplan_config merge_strategy should be "git_three_way"
|
|
And the subplan-test plan subplan_config fail_fast should be false
|
|
|
|
|
|
Scenario: Plan stores parallel subplan config with custom settings
|
|
Given I create a subplan-test plan with parallel subplan config
|
|
Then the subplan-test plan subplan_config execution_mode should be "parallel"
|
|
And the subplan-test plan subplan_config max_parallel should be 10
|
|
And the subplan-test plan subplan_config retry_failed should be true
|
|
And the subplan-test plan subplan_config max_retries should be 3
|
|
|
|
|
|
Scenario: SubplanConfig defaults are sensible
|
|
Given I create a subplan-test default subplan config
|
|
Then the subplan-test subplan config execution_mode should be "sequential"
|
|
And the subplan-test subplan config merge_strategy should be "git_three_way"
|
|
And the subplan-test subplan config max_parallel should be 5
|
|
And the subplan-test subplan config fail_fast should be false
|
|
And the subplan-test subplan config retry_failed should be true
|
|
And the subplan-test subplan config max_retries should be 2
|
|
And the subplan-test subplan config timeout should be none
|
|
|
|
# Plan hierarchy helpers
|
|
|
|
|
|
Scenario: Plan with subplan statuses reports has_subplans true
|
|
Given I create a subplan-test plan with one subplan status
|
|
Then the subplan-test plan should have subplans
|
|
|
|
|
|
Scenario: Plan without subplan statuses reports has_subplans false
|
|
Given I create a subplan-test default plan
|
|
Then the subplan-test plan should not have subplans
|
|
|
|
|
|
Scenario: Depth returns negative one for non-root plans
|
|
Given I create a subplan-test child plan with a parent
|
|
Then the subplan-test plan depth should be -1
|
|
|
|
# Child plans follow same lifecycle as root plans
|
|
|
|
|
|
Scenario: Child plan transitions follow standard lifecycle
|
|
Then subplan-test transition from "action" to "strategize" should be valid
|
|
And subplan-test transition from "strategize" to "execute" should be valid
|
|
And subplan-test transition from "execute" to "apply" should be valid
|
|
|
|
|
|
Scenario: Child plan invalid transitions are rejected
|
|
Then subplan-test transition from "strategize" to "apply" should be invalid
|
|
And subplan-test transition from "action" to "execute" should be invalid
|
|
And subplan-test transition from "action" to "apply" should be invalid
|
|
|
|
# Dependency guardrails
|
|
|
|
|
|
Scenario: SubplanFailureHandler stops others when fail_fast is true
|
|
Given I create a subplan-test fail-fast config with errored status
|
|
Then the subplan-test failure handler should stop others
|
|
|
|
|
|
Scenario: SubplanFailureHandler does not stop others for parallel without fail_fast
|
|
Given I create a subplan-test parallel-no-fail-fast config with errored status
|
|
Then the subplan-test failure handler should not stop others
|
|
|
|
|
|
Scenario: SubplanFailureHandler retries retriable failure
|
|
Given I create a subplan-test config with retriable timeout error
|
|
Then the subplan-test failure handler should retry
|
|
|
|
|
|
Scenario: SubplanFailureHandler does not retry non-retriable error
|
|
Given I create a subplan-test config with non-retriable configuration error
|
|
Then the subplan-test failure handler should not retry
|
|
|
|
|
|
Scenario: SubplanFailureHandler does not retry when retries disabled
|
|
Given I create a subplan-test config with retries disabled and retriable error
|
|
Then the subplan-test failure handler should not retry
|
|
|
|
|
|
Scenario: SubplanFailureHandler does not retry when max retries exceeded
|
|
Given I create a subplan-test config with max retries exceeded
|
|
Then the subplan-test failure handler should not retry
|
|
|
|
# CLI dict rendering for subplan hierarchy
|
|
|
|
|
|
Scenario: CLI dict includes parent_plan_id for child plans
|
|
Given I create a subplan-test child plan with a parent
|
|
And I build a subplan-test plan with the child identity
|
|
Then the subplan-test cli dict should contain parent_plan_id
|
|
|
|
|
|
Scenario: CLI dict includes subplan_count when subplans exist
|
|
Given I create a subplan-test plan with one subplan status
|
|
Then the subplan-test cli dict should contain subplan_count of 1
|
|
|
|
|
|
Scenario: CLI dict omits parent_plan_id for root plans
|
|
Given I create a subplan-test root plan
|
|
And I build a subplan-test plan with the root identity
|
|
Then the subplan-test cli dict should not contain parent_plan_id
|
|
|
|
# Validation guardrails
|
|
|
|
|
|
Scenario: Invalid ULID for parent_plan_id raises validation error
|
|
When I try to create a subplan-test identity with invalid parent ULID
|
|
Then a validation error should be raised
|
|
|
|
|
|
Scenario: SubplanConfig max_parallel below minimum raises validation error
|
|
When I try to create a subplan-test config with max_parallel 0
|
|
Then a validation error should be raised
|
|
|
|
|
|
Scenario: SubplanConfig max_parallel above maximum raises validation error
|
|
When I try to create a subplan-test config with max_parallel 51
|
|
Then a validation error should be raised
|
|
|
|
|
|
Scenario: SubplanConfig max_retries below minimum raises validation error
|
|
When I try to create a subplan-test config with max_retries -1
|
|
Then a validation error should be raised
|
|
|
|
|
|
# ============================================================
|
|
# Originally from: orguserconfig_coverage.feature
|
|
# Feature: Org User Config Domain Model Coverage
|
|
# ============================================================
|
|
|
|
Scenario: Import and instantiate OrgUserConfig with all fields
|
|
Given the cleveragents package is available
|
|
When I import OrgUserConfig from orguserconfig
|
|
Then the OrgUserConfig class should be available
|
|
And I can create an OrgUserConfig with all required fields
|
|
|
|
|
|
Scenario: Create OrgUserConfig with prompted_claude_max field
|
|
Given the cleveragents package is available
|
|
When I create an OrgUserConfig with prompted_claude_max set to True
|
|
Then the OrgUserConfig should have prompted_claude_max as True
|
|
And the field should be accessible via the alias "promptedClaudeMax"
|
|
|
|
|
|
Scenario: Create OrgUserConfig with use_claude_subscription field
|
|
Given the cleveragents package is available
|
|
When I create an OrgUserConfig with use_claude_subscription set to False
|
|
Then the OrgUserConfig should have use_claude_subscription as False
|
|
And the field should be accessible via the alias "useClaudeSubscription"
|
|
|
|
|
|
Scenario: Create OrgUserConfig with claude_subscription_cooldown_started_at field
|
|
Given the cleveragents package is available
|
|
When I create an OrgUserConfig with a specific cooldown timestamp
|
|
Then the OrgUserConfig should have the correct timestamp
|
|
And the field should be accessible via the alias "claudeSubscriptionCooldownStartedAt"
|
|
|
|
|
|
Scenario: Validate OrgUserConfig strips whitespace from string fields
|
|
Given the cleveragents package is available
|
|
When I create an OrgUserConfig with fields containing whitespace
|
|
Then OrgUserConfig string fields should have whitespace stripped
|
|
|
|
|
|
Scenario: Validate OrgUserConfig assignment validation
|
|
Given the cleveragents package is available
|
|
When I create an OrgUserConfig instance
|
|
And I update its fields with new values for OrgUserConfig
|
|
Then the OrgUserConfig assignment validation should be triggered
|
|
And the OrgUserConfig values should be properly validated
|
|
|
|
|
|
Scenario: Validate OrgUserConfig populate by name with camelCase aliases
|
|
Given the cleveragents package is available
|
|
When I create an OrgUserConfig using camelCase field aliases
|
|
Then the OrgUserConfig fields should be populated correctly by name
|
|
And both snake_case and camelCase access should work
|
|
|
|
|
|
Scenario: Validate OrgUserConfig with dictionary input using aliases
|
|
Given the cleveragents package is available
|
|
When I create an OrgUserConfig from a dictionary with camelCase keys
|
|
Then the model should correctly map aliased fields
|
|
And all fields should be accessible with snake_case names
|
|
|
|
|
|
Scenario: Test OrgUserConfig serialization with aliases
|
|
Given the cleveragents package is available
|
|
When I create an OrgUserConfig and serialize it
|
|
Then the serialized output should use the defined aliases
|
|
And the model should be deserializable from the serialized form
|
|
|
|
|
|
Scenario: Test OrgUserConfig validation with invalid datetime
|
|
Given the cleveragents package is available
|
|
When I try to create an OrgUserConfig with an invalid datetime string
|
|
Then an OrgUserConfig validation error should be raised
|
|
And the error should indicate the datetime field
|
|
|
|
|
|
Scenario: Test OrgUserConfig validation with missing required fields
|
|
Given the cleveragents package is available
|
|
When I try to create an OrgUserConfig without required fields
|
|
Then an OrgUserConfig validation error should be raised
|
|
And the error should list all missing required fields
|
|
|
|
|
|
Scenario: Test OrgUserConfig model_config settings
|
|
Given the cleveragents package is available
|
|
When I inspect the OrgUserConfig model configuration
|
|
Then OrgUserConfig str_strip_whitespace should be True
|
|
And OrgUserConfig validate_assignment should be True
|
|
And OrgUserConfig arbitrary_types_allowed should be False
|
|
And OrgUserConfig populate_by_name should be True
|
|
And OrgUserConfig use_enum_values should be True
|
|
|
|
|
|
Scenario: Test OrgUserConfig with timezone-aware datetime
|
|
Given the cleveragents package is available
|
|
When I create an OrgUserConfig with a timezone-aware datetime
|
|
Then the model should handle the timezone information correctly
|
|
And the datetime should be properly stored
|
|
|
|
|
|
Scenario: Test OrgUserConfig copy with update
|
|
Given the cleveragents package is available
|
|
When I create an OrgUserConfig and copy it with updates
|
|
Then the OrgUserConfig copy should have the updated values
|
|
And the OrgUserConfig original should remain unchanged
|
|
|
|
|
|
Scenario: Test OrgUserConfig model_dump with aliases
|
|
Given the cleveragents package is available
|
|
When I create an OrgUserConfig and call model_dump with by_alias=True
|
|
Then the output should use camelCase aliases
|
|
When I call model_dump with by_alias=False
|
|
Then the output should use snake_case field names
|
|
|
|
|
|
Scenario: Test OrgUserConfig model_dump_json
|
|
Given the cleveragents package is available
|
|
When I create an OrgUserConfig and call model_dump_json
|
|
Then the JSON output should be valid
|
|
And it should use the configured aliases by default
|
|
|
|
|
|
Scenario: Test OrgUserConfig field types validation
|
|
Given the cleveragents package is available
|
|
When I try to create an OrgUserConfig with wrong field types
|
|
Then validation errors should be raised for type mismatches
|
|
And the OrgUserConfig errors should clearly indicate the expected types
|
|
|
|
|
|
Scenario: Test OrgUserConfig __init__ coverage
|
|
Given the cleveragents package is available
|
|
When I import the orguserconfig __init__ module
|
|
Then the module should be loaded successfully
|
|
And the OrgUserConfig class should be importable from it
|