2d4b330f75
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Failing after 14s
CI / build (pull_request) Successful in 16s
CI / quality (pull_request) Successful in 17s
CI / typecheck (pull_request) Successful in 30s
CI / coverage (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Has been skipped
CI / security (pull_request) Successful in 33s
CI / integration_tests (pull_request) Successful in 2m49s
CI / unit_tests (pull_request) Successful in 5m25s
CI / docker (pull_request) Has been skipped
243 lines
9.6 KiB
Gherkin
243 lines
9.6 KiB
Gherkin
Feature: Invariant Models and Enforcement
|
|
As a developer
|
|
I want invariant models that enforce natural-language constraints on plans
|
|
So that plan execution respects configured constraints at every scope level
|
|
|
|
# === Invariant Creation with Different Scopes ===
|
|
|
|
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 > 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: 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
|
|
|
|
# === 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 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
|