08868e1e55
Add Behave BDD scenarios covering: - ServerLifecycle.start() with mocked uvicorn (full lifecycle) - run_server() convenience function with Settings defaults and overrides - Signal handler installation and non-main-thread edge case - TeamCollaborationService validation edge cases (empty args) - SessionRegistry validation edge cases and get_active_sessions - VersionConflictError negative version validation - Untracked resource version stamp creation path These tests cover previously uncovered validation branches and the server lifecycle start/shutdown paths to maintain >=97% coverage.
473 lines
22 KiB
Gherkin
473 lines
22 KiB
Gherkin
Feature: Team collaboration — multi-user sessions, RBAC, and conflict resolution
|
|
As a CleverAgents team lead
|
|
I want team collaboration features with role-based access control
|
|
So that multiple users can work concurrently on shared projects
|
|
|
|
# ── TeamRole enum ────────────────────────────────────────────────
|
|
|
|
Scenario: TeamRole enum contains expected members
|
|
Given the TeamRole enum
|
|
Then the TeamRole enum should contain "owner"
|
|
And the TeamRole enum should contain "admin"
|
|
And the TeamRole enum should contain "member"
|
|
And the TeamRole enum should contain "viewer"
|
|
|
|
# ── TeamPermission enum ──────────────────────────────────────────
|
|
|
|
Scenario: TeamPermission enum contains expected members
|
|
Given the TeamPermission enum
|
|
Then the TeamPermission enum should contain "read"
|
|
And the TeamPermission enum should contain "write"
|
|
And the TeamPermission enum should contain "admin"
|
|
And the TeamPermission enum should contain "manage_members"
|
|
|
|
# ── ConflictResolutionStrategy enum ──────────────────────────────
|
|
|
|
Scenario: ConflictResolutionStrategy enum contains expected members
|
|
Given the ConflictResolutionStrategy enum
|
|
Then the ConflictResolutionStrategy enum should contain "last_writer_wins"
|
|
And the ConflictResolutionStrategy enum should contain "reject"
|
|
And the ConflictResolutionStrategy enum should contain "merge"
|
|
|
|
# ── TEAM_ROLE_PERMISSIONS constant ───────────────────────────────
|
|
|
|
Scenario: OWNER role has all 4 permissions
|
|
Given the TEAM_ROLE_PERMISSIONS constant
|
|
Then the "owner" team role should have 4 allowed permissions
|
|
|
|
Scenario: ADMIN role has all 4 permissions
|
|
Given the TEAM_ROLE_PERMISSIONS constant
|
|
Then the "admin" team role should have 4 allowed permissions
|
|
|
|
Scenario: MEMBER role has read and write
|
|
Given the TEAM_ROLE_PERMISSIONS constant
|
|
Then the "member" team role should have 2 allowed permissions
|
|
|
|
Scenario: VIEWER role has only read
|
|
Given the TEAM_ROLE_PERMISSIONS constant
|
|
Then the "viewer" team role should have 1 allowed permissions
|
|
|
|
# ── TeamMember model ─────────────────────────────────────────────
|
|
|
|
Scenario: Create a valid TeamMember
|
|
Given a team member "alice" with role "owner" and email "alice@example.com"
|
|
Then the team member user_id should be "alice"
|
|
And the team member role should be "owner"
|
|
And the team member email should be "alice@example.com"
|
|
|
|
Scenario: TeamMember email validation rejects missing @
|
|
When I try to create a team member with email "invalid-email"
|
|
Then a team member validation error should be raised
|
|
|
|
Scenario: TeamMember has_permission returns True for allowed action
|
|
Given a team member "bob" with role "member" and email "bob@example.com"
|
|
Then the team member should have "read" permission
|
|
And the team member should have "write" permission
|
|
|
|
Scenario: TeamMember has_permission returns False for denied action
|
|
Given a team member "carol" with role "viewer" and email "carol@example.com"
|
|
Then the team member should not have "write" permission
|
|
And the team member should not have "admin" permission
|
|
|
|
# ── UserSession model ────────────────────────────────────────────
|
|
|
|
Scenario: Create a valid UserSession
|
|
Given a user session for user "alice" on project "proj-1"
|
|
Then the user session user_id should be "alice"
|
|
And the user session project_id should be "proj-1"
|
|
And the user session should be active
|
|
|
|
Scenario: Deactivate a UserSession
|
|
Given a user session for user "alice" on project "proj-1"
|
|
When the session is deactivated
|
|
Then the user session should not be active
|
|
|
|
Scenario: Touch updates last_active timestamp
|
|
Given a user session for user "alice" on project "proj-1"
|
|
When the session is touched
|
|
Then the last_active timestamp should be updated
|
|
|
|
# ── VersionStamp model ───────────────────────────────────────────
|
|
|
|
Scenario: Create a valid VersionStamp
|
|
Given a version stamp for resource "res-1" by user "alice"
|
|
Then the version stamp version should be 1
|
|
And the version stamp updated_by should be "alice"
|
|
|
|
Scenario: Increment a VersionStamp
|
|
Given a version stamp for resource "res-1" by user "alice"
|
|
When the version stamp is incremented by "bob"
|
|
Then the new version stamp version should be 2
|
|
And the new version stamp updated_by should be "bob"
|
|
|
|
Scenario: VersionStamp increment rejects empty user
|
|
Given a version stamp for resource "res-1" by user "alice"
|
|
Then incrementing with empty user should raise ValueError
|
|
|
|
# ── ConflictDetectionResult model ────────────────────────────────
|
|
|
|
Scenario: Create a no-conflict result
|
|
Given a conflict detection result with no conflict for "res-1"
|
|
Then the result detected should be False
|
|
And the result resource_id should be "res-1"
|
|
|
|
# ── VersionConflictError ─────────────────────────────────────────
|
|
|
|
Scenario: VersionConflictError carries version info
|
|
When a VersionConflictError is raised for "res-1" expected 1 actual 2
|
|
Then the error resource_id should be "res-1"
|
|
And the error expected_version should be 1
|
|
And the error actual_version should be 2
|
|
|
|
Scenario: VersionConflictError rejects empty resource_id
|
|
Then creating VersionConflictError with empty resource_id raises ValueError
|
|
|
|
# ── SessionRegistry ──────────────────────────────────────────────
|
|
|
|
Scenario: Register and retrieve a session
|
|
Given a session registry
|
|
And a user session "sess-1" for user "alice" on project "proj-1"
|
|
When the session is registered
|
|
Then the registry should contain the session "sess-1"
|
|
And the registry active count should be 1
|
|
|
|
Scenario: Unregister a session
|
|
Given a session registry
|
|
And a user session "sess-1" for user "alice" on project "proj-1"
|
|
When the session is registered
|
|
And the session "sess-1" is unregistered
|
|
Then the registry should not contain the session "sess-1"
|
|
And the registry active count should be 0
|
|
|
|
Scenario: Get sessions for a specific user
|
|
Given a session registry
|
|
And a user session "sess-1" for user "alice" on project "proj-1"
|
|
And a user session "sess-2" for user "alice" on project "proj-2"
|
|
And a user session "sess-3" for user "bob" on project "proj-1"
|
|
When all sessions are registered
|
|
Then the registry should have 2 sessions for user "alice"
|
|
And the registry should have 1 sessions for user "bob"
|
|
|
|
Scenario: Get sessions for a specific project
|
|
Given a session registry
|
|
And a user session "sess-1" for user "alice" on project "proj-1"
|
|
And a user session "sess-2" for user "bob" on project "proj-1"
|
|
And a user session "sess-3" for user "carol" on project "proj-2"
|
|
When all sessions are registered
|
|
Then the registry should have 2 sessions for project "proj-1"
|
|
And the registry should have 1 sessions for project "proj-2"
|
|
|
|
Scenario: Clear all sessions
|
|
Given a session registry
|
|
And a user session "sess-1" for user "alice" on project "proj-1"
|
|
And a user session "sess-2" for user "bob" on project "proj-2"
|
|
When all sessions are registered
|
|
And the registry is cleared
|
|
Then the registry active count should be 0
|
|
|
|
Scenario: Duplicate session registration raises ValueError
|
|
Given a session registry
|
|
And a user session "sess-1" for user "alice" on project "proj-1"
|
|
When the session is registered
|
|
Then registering the same session again raises ValueError
|
|
|
|
Scenario: Session registry generate_session_id returns valid ULID
|
|
Given a session registry
|
|
Then the generated session ID should be a valid ULID
|
|
|
|
# ── TeamCollaborationService — member management ─────────────────
|
|
|
|
Scenario: Add and retrieve a team member via service
|
|
Given a team collaboration service
|
|
When I add member "alice" with role "owner" and email "alice@example.com"
|
|
Then the service should have 1 members
|
|
And getting member "alice" should return the member
|
|
|
|
Scenario: Adding duplicate member raises TeamMemberExistsError
|
|
Given a team collaboration service
|
|
When I add member "alice" with role "owner" and email "alice@example.com"
|
|
Then adding member "alice" again raises TeamMemberExistsError
|
|
|
|
Scenario: Remove a team member via service
|
|
Given a team collaboration service
|
|
When I add member "alice" with role "owner" and email "alice@example.com"
|
|
And I remove member "alice"
|
|
Then the service should have 0 members
|
|
|
|
Scenario: Removing non-existent member raises TeamMemberNotFoundError
|
|
Given a team collaboration service
|
|
Then removing member "ghost" raises TeamMemberNotFoundError
|
|
|
|
Scenario: Update a member role
|
|
Given a team collaboration service
|
|
When I add member "alice" with role "member" and email "alice@example.com"
|
|
And I update member "alice" role to "admin"
|
|
Then member "alice" should have role "admin"
|
|
|
|
# ── TeamCollaborationService — permission checks ─────────────────
|
|
|
|
Scenario: Owner has all permissions
|
|
Given a team collaboration service
|
|
When I add member "alice" with role "owner" and email "alice@example.com"
|
|
Then member "alice" should have "read" team permission
|
|
And member "alice" should have "write" team permission
|
|
And member "alice" should have "admin" team permission
|
|
And member "alice" should have "manage_members" team permission
|
|
|
|
Scenario: Viewer has only read permission
|
|
Given a team collaboration service
|
|
When I add member "viewer-user" with role "viewer" and email "view@example.com"
|
|
Then member "viewer-user" should have "read" team permission
|
|
And member "viewer-user" should not have "write" team permission
|
|
|
|
Scenario: Enforce permission raises PermissionError for viewer writing
|
|
Given a team collaboration service
|
|
When I add member "viewer-user" with role "viewer" and email "view@example.com"
|
|
Then enforcing "write" on "viewer-user" raises PermissionError
|
|
|
|
# ── TeamCollaborationService — session management ────────────────
|
|
|
|
Scenario: Register a session for a team member
|
|
Given a team collaboration service
|
|
When I add member "alice" with role "owner" and email "alice@example.com"
|
|
And I register a session for "alice" on project "proj-1"
|
|
Then the project "proj-1" should have 1 active sessions
|
|
|
|
Scenario: Multiple users have concurrent sessions on same project
|
|
Given a team collaboration service
|
|
When I add member "alice" with role "owner" and email "alice@example.com"
|
|
And I add member "bob" with role "member" and email "bob@example.com"
|
|
And I register a session for "alice" on project "proj-1"
|
|
And I register a session for "bob" on project "proj-1"
|
|
Then the project "proj-1" should have 2 active sessions
|
|
|
|
Scenario: Unregister a session reduces active count
|
|
Given a team collaboration service
|
|
When I add member "alice" with role "owner" and email "alice@example.com"
|
|
And I register a session for "alice" on project "proj-1"
|
|
And I unregister the last session
|
|
Then the project "proj-1" should have 0 active sessions
|
|
|
|
Scenario: Removing a member cleans up their sessions
|
|
Given a team collaboration service
|
|
When I add member "alice" with role "owner" and email "alice@example.com"
|
|
And I register a session for "alice" on project "proj-1"
|
|
And I remove member "alice"
|
|
Then the project "proj-1" should have 0 active sessions
|
|
|
|
# ── TeamCollaborationService — conflict resolution ───────────────
|
|
|
|
Scenario: Create and retrieve a version stamp
|
|
Given a team collaboration service
|
|
When I create a version stamp for "res-1" by "alice"
|
|
Then the version stamp for "res-1" should have version 1
|
|
|
|
Scenario: Update version with matching expected version succeeds
|
|
Given a team collaboration service
|
|
When I create a version stamp for "res-1" by "alice"
|
|
And I update version for "res-1" expecting 1 by "bob"
|
|
Then the version stamp for "res-1" should have version 2
|
|
|
|
Scenario: Update version with mismatched version using reject strategy raises error
|
|
Given a team collaboration service
|
|
When I create a version stamp for "res-1" by "alice"
|
|
And I update version for "res-1" expecting 1 by "bob"
|
|
Then updating "res-1" expecting 1 by "carol" with reject raises VersionConflictError
|
|
|
|
Scenario: Update version with last-writer-wins resolves conflict
|
|
Given a team collaboration service
|
|
When I create a version stamp for "res-1" by "alice"
|
|
And I update version for "res-1" expecting 1 by "bob"
|
|
And I update "res-1" expecting 1 by "carol" with last_writer_wins
|
|
Then the conflict result should be detected and resolved
|
|
And the version stamp for "res-1" should have version 3
|
|
|
|
Scenario: Detect conflict without resolving
|
|
Given a team collaboration service
|
|
When I create a version stamp for "res-1" by "alice"
|
|
And I update version for "res-1" expecting 1 by "bob"
|
|
Then detecting conflict on "res-1" expecting 1 should return True
|
|
And detecting conflict on "res-1" expecting 2 should return False
|
|
|
|
Scenario: Update version with merge strategy resolves conflict
|
|
Given a team collaboration service
|
|
When I create a version stamp for "res-1" by "alice"
|
|
And I update version for "res-1" expecting 1 by "bob"
|
|
And I update "res-1" expecting 1 by "carol" with merge
|
|
Then the conflict result should be detected and resolved
|
|
|
|
# ── Error hierarchy ──────────────────────────────────────────────
|
|
|
|
Scenario: TeamMemberNotFoundError is a TeamCollaborationError
|
|
Then TeamMemberNotFoundError should be a subclass of TeamCollaborationError
|
|
|
|
Scenario: TeamMemberExistsError is a TeamCollaborationError
|
|
Then TeamMemberExistsError should be a subclass of TeamCollaborationError
|
|
|
|
Scenario: VersionConflictError is a TeamCollaborationError
|
|
Then VersionConflictError should be a subclass of TeamCollaborationError
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Service edge case validations
|
|
# -----------------------------------------------------------------------
|
|
|
|
Scenario: Service session_registry property returns the registry
|
|
Given a fresh TeamCollaborationService
|
|
Then the service session_registry should be a SessionRegistry instance
|
|
|
|
Scenario: Service add_member rejects empty user_id
|
|
Given a fresh TeamCollaborationService
|
|
When I try to add a member with empty user_id via service
|
|
Then a collab ValueError should be raised mentioning "user_id"
|
|
|
|
Scenario: Service add_member rejects empty display_name
|
|
Given a fresh TeamCollaborationService
|
|
When I try to add a member with empty display_name via service
|
|
Then a collab ValueError should be raised mentioning "display_name"
|
|
|
|
Scenario: Service add_member rejects empty email
|
|
Given a fresh TeamCollaborationService
|
|
When I try to add a member with empty email via service
|
|
Then a collab ValueError should be raised mentioning "email"
|
|
|
|
Scenario: Service remove_member rejects empty user_id
|
|
Given a fresh TeamCollaborationService
|
|
When I try to remove a member with empty user_id via service
|
|
Then a collab ValueError should be raised mentioning "user_id"
|
|
|
|
Scenario: Service get_member rejects empty user_id
|
|
Given a fresh TeamCollaborationService
|
|
When I try to get a member with empty user_id via service
|
|
Then a collab ValueError should be raised mentioning "user_id"
|
|
|
|
Scenario: Service get_member for non-existent user raises error
|
|
Given a fresh TeamCollaborationService
|
|
When I try to get member "ghost" via service
|
|
Then a TeamMemberNotFoundError should be raised
|
|
|
|
Scenario: Service update_member_role rejects empty user_id
|
|
Given a fresh TeamCollaborationService
|
|
When I try to update role with empty user_id via service
|
|
Then a collab ValueError should be raised mentioning "user_id"
|
|
|
|
Scenario: Service register_session rejects empty user_id
|
|
Given a fresh TeamCollaborationService
|
|
When I try to register session with empty user_id via service
|
|
Then a collab ValueError should be raised mentioning "user_id"
|
|
|
|
Scenario: Service register_session rejects empty project_id
|
|
Given a fresh TeamCollaborationService
|
|
And a team member "u1" with role "member"
|
|
When I try to register session with empty project_id for user "u1"
|
|
Then a collab ValueError should be raised mentioning "project_id"
|
|
|
|
Scenario: Service unregister_session rejects empty session_id
|
|
Given a fresh TeamCollaborationService
|
|
When I try to unregister session with empty session_id
|
|
Then a collab ValueError should be raised mentioning "session_id"
|
|
|
|
Scenario: Service get_active_sessions_for_project rejects empty project_id
|
|
Given a fresh TeamCollaborationService
|
|
When I try to get sessions for empty project_id
|
|
Then a collab ValueError should be raised mentioning "project_id"
|
|
|
|
Scenario: Service create_version_stamp rejects empty resource_id
|
|
Given a fresh TeamCollaborationService
|
|
When I try to create version stamp with empty resource_id
|
|
Then a collab ValueError should be raised mentioning "resource_id"
|
|
|
|
Scenario: Service create_version_stamp rejects empty user_id
|
|
Given a fresh TeamCollaborationService
|
|
When I try to create version stamp with empty user_id
|
|
Then a collab ValueError should be raised mentioning "user_id"
|
|
|
|
Scenario: Service get_version_stamp rejects empty resource_id
|
|
Given a fresh TeamCollaborationService
|
|
When I try to get version stamp with empty resource_id
|
|
Then a collab ValueError should be raised mentioning "resource_id"
|
|
|
|
Scenario: Service update_version rejects empty resource_id
|
|
Given a fresh TeamCollaborationService
|
|
When I try to update version with empty resource_id
|
|
Then a collab ValueError should be raised mentioning "resource_id"
|
|
|
|
Scenario: Service update_version rejects empty user_id
|
|
Given a fresh TeamCollaborationService
|
|
When I try to update version with empty user_id
|
|
Then a collab ValueError should be raised mentioning "user_id"
|
|
|
|
Scenario: Service update_version rejects zero expected_version
|
|
Given a fresh TeamCollaborationService
|
|
When I try to update version with expected_version 0
|
|
Then a collab ValueError should be raised mentioning "expected_version"
|
|
|
|
Scenario: Service update_version creates stamp for untracked resource
|
|
Given a fresh TeamCollaborationService
|
|
When I update version for untracked resource "new-res" by user "u1" expecting version 1
|
|
Then the result should indicate no conflict detected
|
|
And the result details should contain "New resource created"
|
|
|
|
Scenario: Service detect_conflict rejects empty resource_id
|
|
Given a fresh TeamCollaborationService
|
|
When I try to detect conflict with empty resource_id
|
|
Then a collab ValueError should be raised mentioning "resource_id"
|
|
|
|
Scenario: Service detect_conflict rejects zero expected_version
|
|
Given a fresh TeamCollaborationService
|
|
When I try to detect conflict with expected_version 0
|
|
Then a collab ValueError should be raised mentioning "expected_version"
|
|
|
|
Scenario: Service detect_conflict returns False for untracked resource
|
|
Given a fresh TeamCollaborationService
|
|
When I detect conflict for untracked resource "phantom" expecting version 1
|
|
Then the conflict result should be False
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Domain model edge case validations
|
|
# -----------------------------------------------------------------------
|
|
|
|
Scenario: VersionConflictError rejects negative expected_version
|
|
When I try to create VersionConflictError with expected_version -1
|
|
Then a collab ValueError should be raised mentioning "expected_version"
|
|
|
|
Scenario: VersionConflictError rejects negative actual_version
|
|
When I try to create VersionConflictError with actual_version -1
|
|
Then a collab ValueError should be raised mentioning "actual_version"
|
|
|
|
Scenario: SessionRegistry register rejects empty session_id
|
|
Given a fresh SessionRegistry
|
|
When I try to register a session with empty session_id
|
|
Then a collab ValueError should be raised mentioning "session_id"
|
|
|
|
Scenario: SessionRegistry unregister rejects empty session_id
|
|
Given a fresh SessionRegistry
|
|
When I try to unregister a session with empty session_id
|
|
Then a collab ValueError should be raised mentioning "session_id"
|
|
|
|
Scenario: SessionRegistry unregister returns False for unknown session
|
|
Given a fresh SessionRegistry
|
|
When I unregister session "nonexistent-id"
|
|
Then the unregister result should be False
|
|
|
|
Scenario: SessionRegistry get rejects empty session_id
|
|
Given a fresh SessionRegistry
|
|
When I try to get session with empty session_id
|
|
Then a collab ValueError should be raised mentioning "session_id"
|
|
|
|
Scenario: SessionRegistry list_active_sessions returns only active ones
|
|
Given a fresh SessionRegistry
|
|
And a registered session "s1" for user "u1" in project "p1"
|
|
And a registered session "s2" for user "u2" in project "p1"
|
|
When I deactivate session "s1" in the registry
|
|
Then get_active_sessions should return 1 session
|
|
|
|
Scenario: SessionRegistry get_sessions_for_user rejects empty user_id
|
|
Given a fresh SessionRegistry
|
|
When I try to get sessions for empty user_id
|
|
Then a collab ValueError should be raised mentioning "user_id"
|
|
|
|
Scenario: SessionRegistry get_sessions_for_project rejects empty project_id
|
|
Given a fresh SessionRegistry
|
|
When I try to get sessions for empty project_id via registry
|
|
Then a collab ValueError should be raised mentioning "project_id"
|