diff --git a/src/Orbit.Api/RateLimiting/DistributedRateLimitAttribute.cs b/src/Orbit.Api/RateLimiting/DistributedRateLimitAttribute.cs index 9d057f83..9deedcad 100644 --- a/src/Orbit.Api/RateLimiting/DistributedRateLimitAttribute.cs +++ b/src/Orbit.Api/RateLimiting/DistributedRateLimitAttribute.cs @@ -222,6 +222,8 @@ public static string BuildRefreshTokenPartitionKey(string policyName, string ref private static string HashRefreshToken(string refreshToken) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(refreshToken))); + // S107 false positive: [LoggerMessage] source-gen binds each parameter to a named {placeholder} in the template, so folding these into a parameter object would drop the structured-log fields. https://learn.microsoft.com/dotnet/core/extensions/logger-message-generator +#pragma warning disable S107 [LoggerMessage( EventId = 1, Level = LogLevel.Warning, @@ -236,6 +238,7 @@ private static partial void LogRateLimitRejected( string method, string path, string requestId); +#pragma warning restore S107 [LoggerMessage( EventId = 2, diff --git a/src/Orbit.Application/Chat/Commands/ProcessUserChatCommand.Ai.cs b/src/Orbit.Application/Chat/Commands/ProcessUserChatCommand.Ai.cs index a801bb6a..27d922e4 100644 --- a/src/Orbit.Application/Chat/Commands/ProcessUserChatCommand.Ai.cs +++ b/src/Orbit.Application/Chat/Commands/ProcessUserChatCommand.Ai.cs @@ -67,13 +67,14 @@ private async Task> RequestInitialAiResponseAsync( LogCallingAiIntentService(logger, toolDeclarations.Count); return await ai.IntentService.SendWithToolsAsync( - request.Message, - systemPrompt, - toolDeclarations, - request.UserId, - request.ImageData, - request.ImageMimeType, - request.History, + new AiToolRequest( + request.Message, + systemPrompt, + toolDeclarations, + request.UserId, + request.ImageData, + request.ImageMimeType, + request.History), aiStreamSink, cancellationToken); } diff --git a/src/Orbit.Domain/Interfaces/IAiIntentService.cs b/src/Orbit.Domain/Interfaces/IAiIntentService.cs index 3b312ef9..51f9a2be 100644 --- a/src/Orbit.Domain/Interfaces/IAiIntentService.cs +++ b/src/Orbit.Domain/Interfaces/IAiIntentService.cs @@ -6,13 +6,7 @@ namespace Orbit.Domain.Interfaces; public interface IAiIntentService { Task> SendWithToolsAsync( - string userMessage, - string systemPrompt, - IReadOnlyList toolDeclarations, - Guid userId = default, - byte[]? imageData = null, - string? imageMimeType = null, - IReadOnlyList? history = null, + AiToolRequest request, Func? streamSink = null, CancellationToken cancellationToken = default); diff --git a/src/Orbit.Domain/Models/AiToolModels.cs b/src/Orbit.Domain/Models/AiToolModels.cs index b3bed3a0..284e8ed1 100644 --- a/src/Orbit.Domain/Models/AiToolModels.cs +++ b/src/Orbit.Domain/Models/AiToolModels.cs @@ -4,6 +4,21 @@ namespace Orbit.Domain.Models; public record AiToolCall(string Name, string Id, JsonElement Args); +/// +/// A single tool-enabled AI turn: the user message, system prompt, and tool declarations plus the +/// optional per-request routing (), multimodal ( / +/// ), and prior inputs. Bundled so the streaming +/// sink and cancellation token stay as the only standalone arguments to SendWithToolsAsync. +/// +public sealed record AiToolRequest( + string UserMessage, + string SystemPrompt, + IReadOnlyList ToolDeclarations, + Guid UserId = default, + byte[]? ImageData = null, + string? ImageMimeType = null, + IReadOnlyList? History = null); + public record AiToolCallResult( string Name, string Id, diff --git a/src/Orbit.Infrastructure/Services/AgentOperationExecutor.cs b/src/Orbit.Infrastructure/Services/AgentOperationExecutor.cs index eb4d179a..fc098d00 100644 --- a/src/Orbit.Infrastructure/Services/AgentOperationExecutor.cs +++ b/src/Orbit.Infrastructure/Services/AgentOperationExecutor.cs @@ -79,14 +79,14 @@ private async Task DenyDirectUserFlowAsync( var summary = $"{execution.Operation.DisplayName} requires a direct client flow."; await TryAuditAsync( - CreateAuditContext( + new AuditContext( execution.Request, execution.Capability, AgentPolicyDecisionStatus.Denied, AgentOperationStatus.Denied, summary, RedactArguments(execution.Arguments), - error: "direct_user_flow_required"), + Error: "direct_user_flow_required"), cancellationToken); return DeniedResponse(execution, summary, "direct_user_flow_required", "direct_user_flow_required"); @@ -98,14 +98,14 @@ private async Task DenyOwnershipAsync( CancellationToken cancellationToken) { await TryAuditAsync( - CreateAuditContext( + new AuditContext( execution.Request, execution.Capability, AgentPolicyDecisionStatus.Denied, AgentOperationStatus.Denied, execution.Summary, RedactArguments(execution.Arguments), - error: denialReason), + Error: denialReason), cancellationToken); return DeniedResponse(execution, execution.Summary, denialReason, denialReason); @@ -138,16 +138,16 @@ private async Task DenyByPolicyAsync( CancellationToken cancellationToken) { await TryAuditAsync( - CreateAuditContext( + new AuditContext( execution.Request, execution.Capability, AgentPolicyDecisionStatus.Denied, AgentOperationStatus.Denied, execution.Summary, RedactArguments(execution.Arguments), - error: policyDecision.Reason, - shadowPolicyDecision: policyDecision.ShadowStatus, - shadowReason: policyDecision.ShadowReason), + Error: policyDecision.Reason, + ShadowPolicyDecision: policyDecision.ShadowStatus, + ShadowReason: policyDecision.ShadowReason), cancellationToken); return DeniedResponse(execution, execution.Summary, policyDecision.Reason, policyDecision.Reason ?? "denied"); @@ -159,16 +159,16 @@ private async Task RequireConfirmationAsync( CancellationToken cancellationToken) { await TryAuditAsync( - CreateAuditContext( + new AuditContext( execution.Request, execution.Capability, AgentPolicyDecisionStatus.ConfirmationRequired, AgentOperationStatus.PendingConfirmation, execution.Summary, RedactArguments(execution.Arguments), - error: policyDecision.Reason, - shadowPolicyDecision: policyDecision.ShadowStatus, - shadowReason: policyDecision.ShadowReason), + Error: policyDecision.Reason, + ShadowPolicyDecision: policyDecision.ShadowStatus, + ShadowReason: policyDecision.ShadowReason), cancellationToken); return AgentOperationResponseFactory.ConfirmationRequired( @@ -194,16 +194,16 @@ private async Task ExecuteToolAsync( catch (Exception ex) { await TryAuditAsync( - CreateAuditContext( + new AuditContext( execution.Request, execution.Capability, AgentPolicyDecisionStatus.Allowed, AgentOperationStatus.Failed, execution.Summary, RedactArguments(execution.Arguments), - error: ex.Message, - shadowPolicyDecision: policyDecision.ShadowStatus, - shadowReason: policyDecision.ShadowReason), + Error: ex.Message, + ShadowPolicyDecision: policyDecision.ShadowStatus, + ShadowReason: policyDecision.ShadowReason), cancellationToken); return AgentOperationResponseFactory.Failed( @@ -252,7 +252,7 @@ private async Task BuildToolOutcomeResponseAsync( outcomeStatus = isPayGateDenial ? AgentOperationStatus.Denied : AgentOperationStatus.Failed; await TryAuditAsync( - CreateAuditContext( + new AuditContext( execution.Request, execution.Capability, AgentPolicyDecisionStatus.Allowed, @@ -305,33 +305,6 @@ private IReadOnlyList GetGrantedScopes(AgentExecuteOperationRequest requ .ToList(); } - private static AuditContext CreateAuditContext( - AgentExecuteOperationRequest request, - AgentCapability capability, - AgentPolicyDecisionStatus policyDecision, - AgentOperationStatus outcomeStatus, - string summary, - string? redactedArguments, - string? targetId = null, - string? targetName = null, - string? error = null, - AgentPolicyDecisionStatus? shadowPolicyDecision = null, - string? shadowReason = null) - { - return new AuditContext( - request, - capability, - policyDecision, - outcomeStatus, - summary, - redactedArguments, - targetId, - targetName, - error, - shadowPolicyDecision, - shadowReason); - } - private async Task TryAuditAsync( AuditContext context, CancellationToken cancellationToken) @@ -382,9 +355,9 @@ private sealed record AuditContext( AgentOperationStatus OutcomeStatus, string Summary, string? RedactedArguments, - string? TargetId, - string? TargetName, - string? Error, - AgentPolicyDecisionStatus? ShadowPolicyDecision, - string? ShadowReason); + string? TargetId = null, + string? TargetName = null, + string? Error = null, + AgentPolicyDecisionStatus? ShadowPolicyDecision = null, + string? ShadowReason = null); } diff --git a/src/Orbit.Infrastructure/Services/AiIntentService.cs b/src/Orbit.Infrastructure/Services/AiIntentService.cs index 37581d84..5df29106 100644 --- a/src/Orbit.Infrastructure/Services/AiIntentService.cs +++ b/src/Orbit.Infrastructure/Services/AiIntentService.cs @@ -26,16 +26,12 @@ public sealed partial class AiIntentService( }; public async Task> SendWithToolsAsync( - string userMessage, - string systemPrompt, - IReadOnlyList toolDeclarations, - Guid userId = default, - byte[]? imageData = null, - string? imageMimeType = null, - IReadOnlyList? history = null, + AiToolRequest request, Func? streamSink = null, CancellationToken cancellationToken = default) { + var (userMessage, systemPrompt, toolDeclarations, userId, imageData, imageMimeType, history) = request; + var messages = new List { new SystemChatMessage(systemPrompt) @@ -236,7 +232,7 @@ private async Task AppendContentDeltasAsync( System.Diagnostics.Stopwatch stopwatch, bool firstTokenLogged) { - foreach (var part in update.ContentUpdate.Where(part => !string.IsNullOrEmpty(part.Text))) + foreach (var text in update.ContentUpdate.Select(part => part.Text).Where(text => !string.IsNullOrEmpty(text))) { if (!firstTokenLogged) { @@ -244,8 +240,8 @@ private async Task AppendContentDeltasAsync( LogFirstContentToken(logger, stopwatch.ElapsedMilliseconds); } - contentBuilder.Append(part.Text); - await streamSink(AiStreamEvent.Delta(part.Text)); + contentBuilder.Append(text); + await streamSink(AiStreamEvent.Delta(text)); } return firstTokenLogged; diff --git a/src/Orbit.Infrastructure/Services/PushNotificationService.cs b/src/Orbit.Infrastructure/Services/PushNotificationService.cs index 168db6e7..14a0f0a3 100644 --- a/src/Orbit.Infrastructure/Services/PushNotificationService.cs +++ b/src/Orbit.Infrastructure/Services/PushNotificationService.cs @@ -69,10 +69,9 @@ private async Task SendFcm( } const int FcmBatchSize = 500; - var subsList = subs as List ?? subs.ToList(); - for (int offset = 0; offset < subsList.Count; offset += FcmBatchSize) + for (int offset = 0; offset < subs.Count; offset += FcmBatchSize) { - var chunk = subsList.Skip(offset).Take(FcmBatchSize).ToList(); + var chunk = subs.Skip(offset).Take(FcmBatchSize).ToList(); var messages = chunk.Select(s => new Message { Token = s.Endpoint, diff --git a/tests/Orbit.Application.Tests/Commands/Chat/ProcessUserChatCommandHandlerTests.cs b/tests/Orbit.Application.Tests/Commands/Chat/ProcessUserChatCommandHandlerTests.cs index 10b1ce95..3ea8bbb5 100644 --- a/tests/Orbit.Application.Tests/Commands/Chat/ProcessUserChatCommandHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Commands/Chat/ProcessUserChatCommandHandlerTests.cs @@ -221,18 +221,14 @@ private void SetupUserAndPayGate(User? user = null, bool payGatePass = true) private void SetupAiResponse(AiResponse response) { _aiIntentService.SendWithToolsAsync( - Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any(), - Arg.Any(), Arg.Any(), - Arg.Any?>(), Arg.Any?>(), Arg.Any()) + Arg.Any(), Arg.Any?>(), Arg.Any()) .Returns(Result.Success(response)); } private void SetupAiFailure(string error) { _aiIntentService.SendWithToolsAsync( - Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any(), - Arg.Any(), Arg.Any(), - Arg.Any?>(), Arg.Any?>(), Arg.Any()) + Arg.Any(), Arg.Any?>(), Arg.Any()) .Returns(Result.Failure(error)); } @@ -630,9 +626,7 @@ public async Task Handle_WithoutStreamSink_PassesNullSinkToIntentService() result.IsSuccess.Should().BeTrue(); result.Value.AiMessage.Should().Be("Hello there"); await _aiIntentService.Received(1).SendWithToolsAsync( - Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any(), - Arg.Any(), Arg.Any(), - Arg.Any?>(), + Arg.Any(), Arg.Is?>(sink => sink == null), Arg.Any()); } @@ -647,12 +641,9 @@ public async Task Handle_SendsToolsInDeterministicOrdinalOrder() await handler.Handle(new ProcessUserChatCommand(UserId, "Hello AI"), CancellationToken.None); await _aiIntentService.Received(1).SendWithToolsAsync( - Arg.Any(), Arg.Any(), - Arg.Is>(declarations => - ToolNames(declarations).SequenceEqual(ExpectedOrderedToolNames)), - Arg.Any(), - Arg.Any(), Arg.Any(), - Arg.Any?>(), Arg.Any?>(), Arg.Any()); + Arg.Is(request => + ToolNames(request.ToolDeclarations).SequenceEqual(ExpectedOrderedToolNames)), + Arg.Any?>(), Arg.Any()); } [Fact] @@ -665,11 +656,8 @@ public async Task Handle_TrivialGreeting_SkipsToolDeclarations() await handler.Handle(new ProcessUserChatCommand(UserId, "thanks"), CancellationToken.None); await _aiIntentService.Received(1).SendWithToolsAsync( - Arg.Any(), Arg.Any(), - Arg.Is>(declarations => declarations.Count == 0), - Arg.Any(), - Arg.Any(), Arg.Any(), - Arg.Any?>(), Arg.Any?>(), Arg.Any()); + Arg.Is(request => request.ToolDeclarations.Count == 0), + Arg.Any?>(), Arg.Any()); } [Fact] @@ -683,11 +671,8 @@ await handler.Handle( new ProcessUserChatCommand(UserId, "ok", ConfirmationToken: "confirm-token"), CancellationToken.None); await _aiIntentService.Received(1).SendWithToolsAsync( - Arg.Any(), Arg.Any(), - Arg.Is>(declarations => declarations.Count == 1), - Arg.Any(), - Arg.Any(), Arg.Any(), - Arg.Any?>(), Arg.Any?>(), Arg.Any()); + Arg.Is(request => request.ToolDeclarations.Count == 1), + Arg.Any?>(), Arg.Any()); } [Fact] @@ -704,11 +689,8 @@ await handler.Handle( CancellationToken.None); await _aiIntentService.Received(1).SendWithToolsAsync( - Arg.Any(), Arg.Any(), - Arg.Is>(declarations => declarations.Count == 1), - Arg.Any(), - Arg.Any(), Arg.Any(), - Arg.Any?>(), Arg.Any?>(), Arg.Any()); + Arg.Is(request => request.ToolDeclarations.Count == 1), + Arg.Any?>(), Arg.Any()); } [Fact] @@ -722,9 +704,8 @@ public async Task Handle_AssemblesSystemPromptStaticBeforeDynamic() string? capturedPrompt = null; _aiIntentService.SendWithToolsAsync( - Arg.Any(), Arg.Do(prompt => capturedPrompt = prompt), Arg.Any>(), Arg.Any(), - Arg.Any(), Arg.Any(), - Arg.Any?>(), Arg.Any?>(), Arg.Any()) + Arg.Do(request => capturedPrompt = request.SystemPrompt), + Arg.Any?>(), Arg.Any()) .Returns(Result.Success(new AiResponse { TextMessage = "ok" })); var handler = CreateHandler(); @@ -1134,11 +1115,8 @@ public async Task Handle_WithImageAttachment_PassesImageDataToAiService() result.Value.AiMessage.Should().Be("I see your image!"); await _aiIntentService.Received(1).SendWithToolsAsync( - Arg.Any(), Arg.Any(), Arg.Any>(), - Arg.Any(), - Arg.Is(b => b != null && b.Length == 4), - Arg.Is(s => s == "image/png"), - Arg.Any?>(), + Arg.Is(request => + request.ImageData != null && request.ImageData.Length == 4 && request.ImageMimeType == "image/png"), Arg.Any?>(), Arg.Any()); } @@ -1161,9 +1139,7 @@ public async Task Handle_WithChatHistory_PassesHistoryToAiService() result.IsSuccess.Should().BeTrue(); await _aiIntentService.Received(1).SendWithToolsAsync( - Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any(), - Arg.Any(), Arg.Any(), - Arg.Is?>(h => h != null && h.Count == 2), + Arg.Is(request => request.History != null && request.History.Count == 2), Arg.Any?>(), Arg.Any()); } diff --git a/tests/Orbit.Infrastructure.Tests/AI/AiIntentServiceStreamingTests.cs b/tests/Orbit.Infrastructure.Tests/AI/AiIntentServiceStreamingTests.cs index 3751ca34..0e0580f5 100644 --- a/tests/Orbit.Infrastructure.Tests/AI/AiIntentServiceStreamingTests.cs +++ b/tests/Orbit.Infrastructure.Tests/AI/AiIntentServiceStreamingTests.cs @@ -24,7 +24,7 @@ public async Task SendWithToolsAsync_StreamingTextRound_EmitsDeltasAndReturnsFul var body = RoleChunk() + ContentChunk("Hel") + ContentChunk("lo!") + FinishChunk("stop") + Done(); var (service, sink) = BuildService(new SseHandler(body)); - var result = await service.SendWithToolsAsync("hello", "system", [], streamSink: sink.Handle); + var result = await service.SendWithToolsAsync(new AiToolRequest("hello", "system", []), streamSink: sink.Handle); result.IsSuccess.Should().BeTrue(); result.Value.TextMessage.Should().Be("Hello!"); @@ -45,7 +45,7 @@ public async Task SendWithToolsAsync_StreamingToolRound_AccumulatesToolCallAcros + Done(); var (service, sink) = BuildService(new SseHandler(body)); - var result = await service.SendWithToolsAsync("create it", "system", [], streamSink: sink.Handle); + var result = await service.SendWithToolsAsync(new AiToolRequest("create it", "system", []), streamSink: sink.Handle); result.IsSuccess.Should().BeTrue(); result.Value.HasToolCalls.Should().BeTrue(); @@ -68,7 +68,7 @@ public async Task SendWithToolsAsync_ContentBeforeToolCalls_EmitsResetAfterDelta + Done(); var (service, sink) = BuildService(new SseHandler(body)); - var result = await service.SendWithToolsAsync("check goals", "system", [], streamSink: sink.Handle); + var result = await service.SendWithToolsAsync(new AiToolRequest("check goals", "system", []), streamSink: sink.Handle); result.IsSuccess.Should().BeTrue(); result.Value.HasToolCalls.Should().BeTrue(); @@ -83,7 +83,7 @@ public async Task SendWithToolsAsync_MidStreamDrop_ReturnsFailure() var prefix = RoleChunk() + ContentChunk("Hel"); var (service, sink) = BuildService(new DroppingHandler(prefix)); - var result = await service.SendWithToolsAsync("hello", "system", [], streamSink: sink.Handle); + var result = await service.SendWithToolsAsync(new AiToolRequest("hello", "system", []), streamSink: sink.Handle); result.IsFailure.Should().BeTrue(); result.Error.Should().Be("AI service temporarily unavailable"); @@ -100,7 +100,7 @@ public async Task SendWithToolsAsync_NullSink_UsesBufferedCompletion() var handler = new JsonHandler(completion); var (service, sink) = BuildService(handler); - var result = await service.SendWithToolsAsync("hello", "system", []); + var result = await service.SendWithToolsAsync(new AiToolRequest("hello", "system", [])); result.IsSuccess.Should().BeTrue(); result.Value.TextMessage.Should().Be("Hi there"); @@ -118,7 +118,7 @@ public async Task SendWithToolsAsync_BufferedLengthFinish_LogsTruncationWarningA """; var (service, logger) = BuildServiceWithRecordingLogger(new JsonHandler(completion)); - var result = await service.SendWithToolsAsync("hello", "system", []); + var result = await service.SendWithToolsAsync(new AiToolRequest("hello", "system", [])); result.IsSuccess.Should().BeTrue(); result.Value.TextMessage.Should().Be("partial list"); @@ -132,7 +132,7 @@ public async Task SendWithToolsAsync_StreamingLengthFinish_LogsTruncationWarning var (service, logger) = BuildServiceWithRecordingLogger(new SseHandler(body)); var sink = new CollectingSink(); - var result = await service.SendWithToolsAsync("hello", "system", [], streamSink: sink.Handle); + var result = await service.SendWithToolsAsync(new AiToolRequest("hello", "system", []), streamSink: sink.Handle); result.IsSuccess.Should().BeTrue(); result.Value.TextMessage.Should().Be("partial"); @@ -147,7 +147,7 @@ public async Task SendWithToolsAsync_WithUserId_SetsEndUserIdForCacheRouting() var service = new AiIntentService(aiClient, NullLogger.Instance); var userId = Guid.NewGuid(); - await service.SendWithToolsAsync("hello", "system", [], userId); + await service.SendWithToolsAsync(new AiToolRequest("hello", "system", [], userId)); handler.LastRequestBody.Should().Contain(userId.ToString("N")); } @@ -159,7 +159,7 @@ public async Task SendWithToolsAsync_HistoryWithinWindow_DoesNotSummarize() var aiClient = new AiCompletionClient(BuildChatClient(handler), NullLogger.Instance, Substitute.For()); var service = new AiIntentService(aiClient, NullLogger.Instance); - await service.SendWithToolsAsync("hello", "system", [], Guid.NewGuid(), history: BuildHistory(40)); + await service.SendWithToolsAsync(new AiToolRequest("hello", "system", [], Guid.NewGuid(), History: BuildHistory(40))); handler.RequestCount.Should().Be(1); } @@ -171,7 +171,7 @@ public async Task SendWithToolsAsync_HistoryOverflowsWindow_SummarizesOlderMessa var aiClient = new AiCompletionClient(BuildChatClient(handler), NullLogger.Instance, Substitute.For()); var service = new AiIntentService(aiClient, NullLogger.Instance); - await service.SendWithToolsAsync("hello", "system", [], Guid.NewGuid(), history: BuildHistory(50)); + await service.SendWithToolsAsync(new AiToolRequest("hello", "system", [], Guid.NewGuid(), History: BuildHistory(50))); handler.RequestCount.Should().Be(2); } diff --git a/tests/Orbit.Infrastructure.Tests/BackgroundJobs/ScheduledJobRegistryTests.cs b/tests/Orbit.Infrastructure.Tests/BackgroundJobs/ScheduledJobRegistryTests.cs index 471c95c0..e6261ff4 100644 --- a/tests/Orbit.Infrastructure.Tests/BackgroundJobs/ScheduledJobRegistryTests.cs +++ b/tests/Orbit.Infrastructure.Tests/BackgroundJobs/ScheduledJobRegistryTests.cs @@ -22,18 +22,20 @@ public class ScheduledJobRegistryTests { private static readonly IConfiguration EmptyConfiguration = new ConfigurationBuilder().Build(); - public static TheoryData AllScheduledJobs() + public static TheoryData AllScheduledJobTypes() { - var data = new TheoryData(); + var data = new TheoryData(); foreach (var job in BuildAll()) - data.Add(job); + data.Add(job.GetType()); return data; } [Theory] - [MemberData(nameof(AllScheduledJobs))] - public void Job_HasNameAndCronExpression(IScheduledJob job) + [MemberData(nameof(AllScheduledJobTypes))] + public void Job_HasNameAndCronExpression(Type jobType) { + var job = BuildAll().Single(scheduledJob => scheduledJob.GetType() == jobType); + job.Name.Should().NotBeNullOrWhiteSpace(); job.CronExpression.Should().NotBeNullOrWhiteSpace(); } diff --git a/tests/Orbit.Infrastructure.Tests/Persistence/SyncChangesIndexConfigurationTests.cs b/tests/Orbit.Infrastructure.Tests/Persistence/SyncChangesIndexConfigurationTests.cs index e89a209c..03bce0ba 100644 --- a/tests/Orbit.Infrastructure.Tests/Persistence/SyncChangesIndexConfigurationTests.cs +++ b/tests/Orbit.Infrastructure.Tests/Persistence/SyncChangesIndexConfigurationTests.cs @@ -24,15 +24,19 @@ public void Dispose() GC.SuppressFinalize(this); } + private static readonly string[] UserIdUpdatedAtColumns = ["UserId", "UpdatedAtUtc"]; + private static readonly string[] HabitIdUpdatedAtColumns = ["HabitId", "UpdatedAtUtc"]; + private static readonly string[] GoalIdUpdatedAtColumns = ["GoalId", "UpdatedAtUtc"]; + public static TheoryData ChangeQueryIndexCases() => new() { - { typeof(Habit), new[] { "UserId", "UpdatedAtUtc" } }, - { typeof(HabitLog), new[] { "HabitId", "UpdatedAtUtc" } }, - { typeof(Goal), new[] { "UserId", "UpdatedAtUtc" } }, - { typeof(GoalProgressLog), new[] { "GoalId", "UpdatedAtUtc" } }, - { typeof(Tag), new[] { "UserId", "UpdatedAtUtc" } }, - { typeof(Notification), new[] { "UserId", "UpdatedAtUtc" } }, - { typeof(ChecklistTemplate), new[] { "UserId", "UpdatedAtUtc" } }, + { typeof(Habit), UserIdUpdatedAtColumns }, + { typeof(HabitLog), HabitIdUpdatedAtColumns }, + { typeof(Goal), UserIdUpdatedAtColumns }, + { typeof(GoalProgressLog), GoalIdUpdatedAtColumns }, + { typeof(Tag), UserIdUpdatedAtColumns }, + { typeof(Notification), UserIdUpdatedAtColumns }, + { typeof(ChecklistTemplate), UserIdUpdatedAtColumns }, }; [Theory]