using ColaFlow.Modules.Mcp.Domain.Entities; using ColaFlow.Modules.Mcp.Domain.Events; using ColaFlow.Modules.Mcp.Domain.ValueObjects; using Xunit; namespace ColaFlow.Modules.Mcp.Tests.Domain; public class TaskLockTests { [Fact] public void Acquire_ValidInput_Success() { // Arrange var resourceType = "Epic"; var resourceId = Guid.NewGuid(); var lockHolderId = Guid.NewGuid(); var tenantId = Guid.NewGuid(); // Act var taskLock = TaskLock.Acquire( resourceType: resourceType, resourceId: resourceId, lockHolderType: "AI_AGENT", lockHolderId: lockHolderId, tenantId: tenantId, lockHolderName: "Claude", purpose: "Creating new epic" ); // Assert Assert.NotEqual(Guid.Empty, taskLock.Id); Assert.Equal(resourceType, taskLock.ResourceType); Assert.Equal(resourceId, taskLock.ResourceId); Assert.Equal("AI_AGENT", taskLock.LockHolderType); Assert.Equal(lockHolderId, taskLock.LockHolderId); Assert.Equal(tenantId, taskLock.TenantId); Assert.Equal("Claude", taskLock.LockHolderName); Assert.Equal("Creating new epic", taskLock.Purpose); Assert.Equal(TaskLockStatus.Active, taskLock.Status); Assert.True(taskLock.ExpiresAt > DateTime.UtcNow); Assert.Null(taskLock.ReleasedAt); } [Fact] public void Acquire_RaisesTaskLockAcquiredEvent() { // Arrange & Act var taskLock = TaskLock.Acquire( "Epic", Guid.NewGuid(), "AI_AGENT", Guid.NewGuid(), Guid.NewGuid() ); // Assert Assert.Single(taskLock.DomainEvents); var domainEvent = taskLock.DomainEvents.First(); Assert.IsType(domainEvent); var acquiredEvent = (TaskLockAcquiredEvent)domainEvent; Assert.Equal(taskLock.Id, acquiredEvent.LockId); Assert.Equal("Epic", acquiredEvent.ResourceType); } [Fact] public void Acquire_EmptyResourceType_ThrowsException() { // Act & Assert Assert.Throws(() => TaskLock.Acquire( resourceType: "", resourceId: Guid.NewGuid(), lockHolderType: "AI_AGENT", lockHolderId: Guid.NewGuid(), tenantId: Guid.NewGuid() )); } [Fact] public void Acquire_InvalidLockHolderType_ThrowsException() { // Act & Assert Assert.Throws(() => TaskLock.Acquire( resourceType: "Epic", resourceId: Guid.NewGuid(), lockHolderType: "INVALID", lockHolderId: Guid.NewGuid(), tenantId: Guid.NewGuid() )); } [Fact] public void Acquire_LowercaseLockHolderType_NormalizedToUppercase() { // Act var taskLock = TaskLock.Acquire( resourceType: "Epic", resourceId: Guid.NewGuid(), lockHolderType: "ai_agent", lockHolderId: Guid.NewGuid(), tenantId: Guid.NewGuid() ); // Assert Assert.Equal("AI_AGENT", taskLock.LockHolderType); } [Fact] public void Acquire_CustomExpirationMinutes_SetsCorrectExpiration() { // Arrange var before = DateTime.UtcNow; // Act var taskLock = TaskLock.Acquire( resourceType: "Epic", resourceId: Guid.NewGuid(), lockHolderType: "AI_AGENT", lockHolderId: Guid.NewGuid(), tenantId: Guid.NewGuid(), expirationMinutes: 10 ); // Assert var after = DateTime.UtcNow; Assert.True(taskLock.ExpiresAt >= before.AddMinutes(10)); Assert.True(taskLock.ExpiresAt <= after.AddMinutes(10)); } [Fact] public void Release_ActiveLock_Success() { // Arrange var taskLock = TaskLock.Acquire( "Epic", Guid.NewGuid(), "AI_AGENT", Guid.NewGuid(), Guid.NewGuid() ); taskLock.ClearDomainEvents(); // Act taskLock.Release(); // Assert Assert.Equal(TaskLockStatus.Released, taskLock.Status); Assert.NotNull(taskLock.ReleasedAt); Assert.True(taskLock.ReleasedAt <= DateTime.UtcNow); } [Fact] public void Release_RaisesTaskLockReleasedEvent() { // Arrange var taskLock = TaskLock.Acquire( "Epic", Guid.NewGuid(), "AI_AGENT", Guid.NewGuid(), Guid.NewGuid() ); taskLock.ClearDomainEvents(); // Act taskLock.Release(); // Assert Assert.Single(taskLock.DomainEvents); Assert.IsType(taskLock.DomainEvents.First()); } [Fact] public void Release_AlreadyReleased_ThrowsException() { // Arrange var taskLock = TaskLock.Acquire( "Epic", Guid.NewGuid(), "AI_AGENT", Guid.NewGuid(), Guid.NewGuid() ); taskLock.Release(); // Act & Assert Assert.Throws(() => taskLock.Release()); } [Fact] public void MarkAsExpired_ExpiredActiveLock_Success() { // Arrange var taskLock = TaskLock.Acquire( "Epic", Guid.NewGuid(), "AI_AGENT", Guid.NewGuid(), Guid.NewGuid(), expirationMinutes: 5 ); // Use reflection to set ExpiresAt to the past var expiresAtProperty = typeof(TaskLock).GetProperty("ExpiresAt"); expiresAtProperty!.SetValue(taskLock, DateTime.UtcNow.AddMinutes(-1)); taskLock.ClearDomainEvents(); // Act taskLock.MarkAsExpired(); // Assert Assert.Equal(TaskLockStatus.Expired, taskLock.Status); } [Fact] public void MarkAsExpired_RaisesTaskLockExpiredEvent() { // Arrange var taskLock = TaskLock.Acquire( "Epic", Guid.NewGuid(), "AI_AGENT", Guid.NewGuid(), Guid.NewGuid(), expirationMinutes: 5 ); // Use reflection to set ExpiresAt to the past var expiresAtProperty = typeof(TaskLock).GetProperty("ExpiresAt"); expiresAtProperty!.SetValue(taskLock, DateTime.UtcNow.AddMinutes(-1)); taskLock.ClearDomainEvents(); // Act taskLock.MarkAsExpired(); // Assert Assert.Single(taskLock.DomainEvents); Assert.IsType(taskLock.DomainEvents.First()); } [Fact] public void MarkAsExpired_NotYetExpired_ThrowsException() { // Arrange var taskLock = TaskLock.Acquire( "Epic", Guid.NewGuid(), "AI_AGENT", Guid.NewGuid(), Guid.NewGuid(), expirationMinutes: 5 ); // Act & Assert Assert.Throws(() => taskLock.MarkAsExpired()); } [Fact] public void MarkAsExpired_AlreadyReleased_DoesNothing() { // Arrange var taskLock = TaskLock.Acquire( "Epic", Guid.NewGuid(), "AI_AGENT", Guid.NewGuid(), Guid.NewGuid(), expirationMinutes: 5 ); // Use reflection to set ExpiresAt to the past var expiresAtProperty = typeof(TaskLock).GetProperty("ExpiresAt"); expiresAtProperty!.SetValue(taskLock, DateTime.UtcNow.AddMinutes(-1)); taskLock.Release(); var statusBefore = taskLock.Status; taskLock.ClearDomainEvents(); // Act taskLock.MarkAsExpired(); // Assert Assert.Equal(statusBefore, taskLock.Status); Assert.Empty(taskLock.DomainEvents); } [Fact] public void ExtendExpiration_ActiveLock_Success() { // Arrange var taskLock = TaskLock.Acquire( "Epic", Guid.NewGuid(), "AI_AGENT", Guid.NewGuid(), Guid.NewGuid(), expirationMinutes: 5 ); var expirationBefore = taskLock.ExpiresAt; // Act taskLock.ExtendExpiration(10); // Assert Assert.True(taskLock.ExpiresAt > expirationBefore); Assert.True(taskLock.ExpiresAt >= expirationBefore.AddMinutes(10)); } [Fact] public void ExtendExpiration_ReleasedLock_ThrowsException() { // Arrange var taskLock = TaskLock.Acquire( "Epic", Guid.NewGuid(), "AI_AGENT", Guid.NewGuid(), Guid.NewGuid() ); taskLock.Release(); // Act & Assert Assert.Throws(() => taskLock.ExtendExpiration(10)); } [Fact] public void ExtendExpiration_BeyondMaxDuration_ThrowsException() { // Arrange var taskLock = TaskLock.Acquire( "Epic", Guid.NewGuid(), "AI_AGENT", Guid.NewGuid(), Guid.NewGuid(), expirationMinutes: 60 ); // Use reflection to set AcquiredAt to 1.5 hours ago // Current expiration would be at 30 minutes from now (1.5 hours + 60 minutes = 2.5 hours, then - 30min = 2 hours) // Extending by more than 1 minute will exceed the 2-hour max var acquiredAtProperty = typeof(TaskLock).GetProperty("AcquiredAt"); var expiresAtProperty = typeof(TaskLock).GetProperty("ExpiresAt"); var pastTime = DateTime.UtcNow.AddHours(-1.5); acquiredAtProperty!.SetValue(taskLock, pastTime); // Set expires to 1 hour 55 minutes from now (2 hours total - 5 minutes buffer) expiresAtProperty!.SetValue(taskLock, DateTime.UtcNow.AddMinutes(115)); // Try to extend by 10 minutes - this would push expiration to 2 hours 5 minutes total, exceeding limit // Act & Assert Assert.Throws(() => taskLock.ExtendExpiration(10)); } [Fact] public void IsExpired_ExpiredLock_ReturnsTrue() { // Arrange var taskLock = TaskLock.Acquire( "Epic", Guid.NewGuid(), "AI_AGENT", Guid.NewGuid(), Guid.NewGuid(), expirationMinutes: 5 ); // Use reflection to set ExpiresAt to the past var expiresAtProperty = typeof(TaskLock).GetProperty("ExpiresAt"); expiresAtProperty!.SetValue(taskLock, DateTime.UtcNow.AddMinutes(-1)); // Act & Assert Assert.True(taskLock.IsExpired()); } [Fact] public void IsExpired_NotExpiredLock_ReturnsFalse() { // Arrange var taskLock = TaskLock.Acquire( "Epic", Guid.NewGuid(), "AI_AGENT", Guid.NewGuid(), Guid.NewGuid(), expirationMinutes: 5 ); // Act & Assert Assert.False(taskLock.IsExpired()); } [Fact] public void IsValid_ActiveNotExpired_ReturnsTrue() { // Arrange var taskLock = TaskLock.Acquire( "Epic", Guid.NewGuid(), "AI_AGENT", Guid.NewGuid(), Guid.NewGuid(), expirationMinutes: 5 ); // Act & Assert Assert.True(taskLock.IsValid()); } [Fact] public void IsValid_Released_ReturnsFalse() { // Arrange var taskLock = TaskLock.Acquire( "Epic", Guid.NewGuid(), "AI_AGENT", Guid.NewGuid(), Guid.NewGuid() ); taskLock.Release(); // Act & Assert Assert.False(taskLock.IsValid()); } [Fact] public void IsValid_Expired_ReturnsFalse() { // Arrange var taskLock = TaskLock.Acquire( "Epic", Guid.NewGuid(), "AI_AGENT", Guid.NewGuid(), Guid.NewGuid(), expirationMinutes: 5 ); // Use reflection to set ExpiresAt to the past var expiresAtProperty = typeof(TaskLock).GetProperty("ExpiresAt"); expiresAtProperty!.SetValue(taskLock, DateTime.UtcNow.AddMinutes(-1)); // Act & Assert Assert.False(taskLock.IsValid()); } [Fact] public void IsHeldBy_CorrectHolder_ReturnsTrue() { // Arrange var holderId = Guid.NewGuid(); var taskLock = TaskLock.Acquire( "Epic", Guid.NewGuid(), "AI_AGENT", holderId, Guid.NewGuid() ); // Act & Assert Assert.True(taskLock.IsHeldBy(holderId)); } [Fact] public void IsHeldBy_DifferentHolder_ReturnsFalse() { // Arrange var taskLock = TaskLock.Acquire( "Epic", Guid.NewGuid(), "AI_AGENT", Guid.NewGuid(), Guid.NewGuid() ); // Act & Assert Assert.False(taskLock.IsHeldBy(Guid.NewGuid())); } [Fact] public void IsHeldByAiAgent_AiAgentLock_ReturnsTrue() { // Arrange var taskLock = TaskLock.Acquire( "Epic", Guid.NewGuid(), "AI_AGENT", Guid.NewGuid(), Guid.NewGuid() ); // Act & Assert Assert.True(taskLock.IsHeldByAiAgent()); } [Fact] public void IsHeldByAiAgent_UserLock_ReturnsFalse() { // Arrange var taskLock = TaskLock.Acquire( "Epic", Guid.NewGuid(), "USER", Guid.NewGuid(), Guid.NewGuid() ); // Act & Assert Assert.False(taskLock.IsHeldByAiAgent()); } [Fact] public void IsHeldByUser_UserLock_ReturnsTrue() { // Arrange var taskLock = TaskLock.Acquire( "Epic", Guid.NewGuid(), "USER", Guid.NewGuid(), Guid.NewGuid() ); // Act & Assert Assert.True(taskLock.IsHeldByUser()); } [Fact] public void IsHeldByUser_AiAgentLock_ReturnsFalse() { // Arrange var taskLock = TaskLock.Acquire( "Epic", Guid.NewGuid(), "AI_AGENT", Guid.NewGuid(), Guid.NewGuid() ); // Act & Assert Assert.False(taskLock.IsHeldByUser()); } [Fact] public void GetRemainingTime_ActiveLock_ReturnsPositiveTimeSpan() { // Arrange var taskLock = TaskLock.Acquire( "Epic", Guid.NewGuid(), "AI_AGENT", Guid.NewGuid(), Guid.NewGuid(), expirationMinutes: 5 ); // Act var remaining = taskLock.GetRemainingTime(); // Assert Assert.True(remaining.TotalMinutes > 0); Assert.True(remaining.TotalMinutes <= 5); } [Fact] public void GetRemainingTime_ExpiredLock_ReturnsZero() { // Arrange var taskLock = TaskLock.Acquire( "Epic", Guid.NewGuid(), "AI_AGENT", Guid.NewGuid(), Guid.NewGuid(), expirationMinutes: 5 ); // Use reflection to set ExpiresAt to the past var expiresAtProperty = typeof(TaskLock).GetProperty("ExpiresAt"); expiresAtProperty!.SetValue(taskLock, DateTime.UtcNow.AddMinutes(-1)); // Act var remaining = taskLock.GetRemainingTime(); // Assert Assert.Equal(TimeSpan.Zero, remaining); } [Fact] public void GetSummary_ReturnsFormattedString() { // Arrange var taskLock = TaskLock.Acquire( resourceType: "Epic", resourceId: Guid.NewGuid(), lockHolderType: "AI_AGENT", lockHolderId: Guid.NewGuid(), tenantId: Guid.NewGuid(), lockHolderName: "Claude" ); // Act var summary = taskLock.GetSummary(); // Assert Assert.Contains("Epic", summary); Assert.Contains("Claude", summary); Assert.Contains("AI_AGENT", summary); Assert.Contains("Active", summary); } }