Skip to content

Commit 359e711

Browse files
committed
docs(runtime): replace inheritdoc with explicit XML docs in MutationEngine
Replace all /// <inheritdoc /> usages in MutationEngine with explicit XML documentation matching the IMutationEngine interface contract: - ExecuteAsync - ExecuteBatchAsync (both overloads, with remarks) - RegisterPolicy, RegisterInterceptor (with remarks) - GetHistoryAsync, GetStatisticsAsync Add param/returns/exception tags to private helpers: - ExecutePipelineAsync - CreateConcurrencyGate (ArgumentNullException, ArgumentOutOfRangeException)
1 parent 82c9f66 commit 359e711

4 files changed

Lines changed: 344 additions & 277 deletions

File tree

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using ModularityKit.Mutator.Abstractions;
3+
using ModularityKit.Mutator.Abstractions.Changes;
4+
using ModularityKit.Mutator.Abstractions.Context;
5+
using ModularityKit.Mutator.Abstractions.Engine;
6+
using ModularityKit.Mutator.Abstractions.Exceptions;
7+
using ModularityKit.Mutator.Abstractions.Intent;
8+
using ModularityKit.Mutator.Abstractions.Policies;
9+
using ModularityKit.Mutator.Abstractions.Results;
10+
using ModularityKit.Mutator.Runtime;
11+
using Xunit;
12+
13+
namespace ModularityKit.Mutator.Tests.Runtime;
14+
15+
public sealed class MutationEnginePolicyTests
16+
{
17+
[Fact]
18+
public async Task ExecuteAsync_supports_async_policy_evaluation()
19+
{
20+
var engine = CreateEngine();
21+
engine.RegisterPolicy(new AsyncBlockingPolicy());
22+
23+
var result = await engine.ExecuteAsync(new SampleMutation(), new SampleState("initial"));
24+
25+
Assert.False(result.IsSuccess);
26+
Assert.Single(result.PolicyDecisions);
27+
Assert.Equal("AsyncBlocking", result.PolicyDecisions[0].PolicyName);
28+
Assert.Equal("External compliance check rejected the mutation.", result.PolicyDecisions[0].Reason);
29+
}
30+
31+
[Fact]
32+
public async Task ExecuteAsync_throws_policy_evaluation_timeout_exception_for_slow_policy()
33+
{
34+
var engine = CreateEngine(options => options.PolicyEvaluationTimeout = TimeSpan.FromMilliseconds(50));
35+
engine.RegisterPolicy(new SlowAsyncPolicy());
36+
37+
var exception = await Assert.ThrowsAsync<PolicyEvaluationTimeoutException>(() =>
38+
engine.ExecuteAsync(new SampleMutation(), new SampleState("initial")));
39+
40+
Assert.Equal("SlowExternalCheck", exception.PolicyName);
41+
}
42+
43+
[Fact]
44+
public async Task ExecuteAsync_wraps_policy_evaluation_failures()
45+
{
46+
var engine = CreateEngine();
47+
engine.RegisterPolicy(new FailingAsyncPolicy());
48+
49+
var exception = await Assert.ThrowsAsync<PolicyEvaluationException>(() =>
50+
engine.ExecuteAsync(new SampleMutation(), new SampleState("initial")));
51+
52+
Assert.Equal("FailingExternalCheck", exception.PolicyName);
53+
Assert.IsType<InvalidOperationException>(exception.InnerException);
54+
}
55+
56+
[Fact]
57+
public async Task ExecuteAsync_preserves_caller_cancellation_during_policy_evaluation()
58+
{
59+
var engine = CreateEngine();
60+
engine.RegisterPolicy(new CancelAwareAsyncPolicy());
61+
using var cancellationSource = new CancellationTokenSource(millisecondsDelay: 50);
62+
63+
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
64+
engine.ExecuteAsync(new SampleMutation(), new SampleState("initial"), cancellationSource.Token));
65+
}
66+
67+
[Fact]
68+
public async Task ExecuteAsync_uses_sync_policy_path_without_async_override()
69+
{
70+
var engine = CreateEngine();
71+
engine.RegisterPolicy(new SyncAllowPolicy());
72+
73+
var result = await engine.ExecuteAsync(new SampleMutation(), new SampleState("initial"));
74+
75+
Assert.True(result.IsSuccess);
76+
Assert.Equal("updated", result.NewState!.Value);
77+
}
78+
79+
private static IMutationEngine CreateEngine(Action<MutationEngineOptions>? configure = null)
80+
{
81+
var services = new ServiceCollection();
82+
services.AddMutators(configure: configure);
83+
84+
var provider = services.BuildServiceProvider();
85+
return provider.GetRequiredService<IMutationEngine>();
86+
}
87+
88+
private sealed record SampleState(string Value);
89+
90+
private sealed class SampleMutation : MutationBase<SampleState>
91+
{
92+
public SampleMutation()
93+
: base(
94+
CreateIntent(
95+
operationName: "UpdateSample",
96+
category: "Test",
97+
description: "Exercise policy evaluation"),
98+
MutationContext.System("Policy test") with { StateId = "sample-1" })
99+
{
100+
}
101+
102+
public override MutationResult<SampleState> Apply(SampleState state)
103+
=> Success(state with { Value = "updated" }, StateChange.Modified("Value", state.Value, "updated"));
104+
}
105+
106+
private sealed class SyncAllowPolicy : IMutationPolicy<SampleState>
107+
{
108+
public string Name => "SyncAllow";
109+
public int Priority => 10;
110+
public string? Description => "Simple synchronous allow policy.";
111+
112+
public PolicyDecision Evaluate(IMutation<SampleState> mutation, SampleState state)
113+
=> PolicyDecision.Allow(Name, "Synchronous policy allowed the mutation.");
114+
}
115+
116+
private sealed class AsyncBlockingPolicy : IMutationPolicy<SampleState>
117+
{
118+
public string Name => "AsyncBlocking";
119+
public int Priority => 100;
120+
public string? Description => "Simulates an external compliance check.";
121+
122+
public async Task<PolicyDecision> EvaluateAsync(
123+
IMutation<SampleState> mutation,
124+
SampleState state,
125+
CancellationToken cancellationToken = default)
126+
{
127+
await Task.Delay(10, cancellationToken);
128+
return PolicyDecision.Deny("External compliance check rejected the mutation.", Name);
129+
}
130+
}
131+
132+
private sealed class SlowAsyncPolicy : IMutationPolicy<SampleState>
133+
{
134+
public string Name => "SlowExternalCheck";
135+
public int Priority => 100;
136+
public string? Description => "Simulates a slow external dependency.";
137+
138+
public async Task<PolicyDecision> EvaluateAsync(
139+
IMutation<SampleState> mutation,
140+
SampleState state,
141+
CancellationToken cancellationToken = default)
142+
{
143+
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
144+
return PolicyDecision.Allow(Name, "Finished too late.");
145+
}
146+
}
147+
148+
private sealed class FailingAsyncPolicy : IMutationPolicy<SampleState>
149+
{
150+
public string Name => "FailingExternalCheck";
151+
public int Priority => 100;
152+
public string? Description => "Simulates an external dependency failure.";
153+
154+
public Task<PolicyDecision> EvaluateAsync(
155+
IMutation<SampleState> mutation,
156+
SampleState state,
157+
CancellationToken cancellationToken = default)
158+
=> throw new InvalidOperationException("Remote ticketing system unavailable.");
159+
}
160+
161+
private sealed class CancelAwareAsyncPolicy : IMutationPolicy<SampleState>
162+
{
163+
public string Name => "CancelAware";
164+
public int Priority => 100;
165+
public string? Description => "Waits for cancellation.";
166+
167+
public async Task<PolicyDecision> EvaluateAsync(
168+
IMutation<SampleState> mutation,
169+
SampleState state,
170+
CancellationToken cancellationToken = default)
171+
{
172+
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
173+
return PolicyDecision.Allow(Name, "Completed.");
174+
}
175+
}
176+
}

src/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,47 @@ Core runtime concurrency is controlled by `MutationEngineOptions.MaxConcurrentMu
102102
- `PolicyDecision`
103103
- `PolicyRequirement`
104104

105+
### Async policies and external checks
106+
107+
`IMutationPolicy<TState>` supports both synchronous and asynchronous evaluation.
108+
109+
- implement `Evaluate(...)` for lightweight in-process rules
110+
- implement `EvaluateAsync(..., CancellationToken)` for external identity, ticketing, quota, or compliance checks
111+
- the runtime evaluates policies in descending `Priority` order
112+
- when `MutationEngineOptions.PolicyEvaluationTimeout` is set, the timeout is applied per policy evaluation
113+
- caller cancellation still flows through unchanged
114+
- policy failures are surfaced as `PolicyEvaluationException`
115+
- policy timeouts are surfaced as `PolicyEvaluationTimeoutException`
116+
117+
```csharp
118+
public sealed class RequireApprovedTicketPolicy : IMutationPolicy<QuotaState>
119+
{
120+
private readonly ITicketGateway _tickets;
121+
122+
public RequireApprovedTicketPolicy(ITicketGateway tickets)
123+
{
124+
_tickets = tickets;
125+
}
126+
127+
public string Name => "RequireApprovedTicket";
128+
public int Priority => 200;
129+
public string? Description => "Checks ticket approval status before quota changes execute.";
130+
131+
public async Task<PolicyDecision> EvaluateAsync(
132+
IMutation<QuotaState> mutation,
133+
QuotaState state,
134+
CancellationToken cancellationToken = default)
135+
{
136+
var ticketId = mutation.Context.CorrelationId;
137+
var approved = await _tickets.IsApprovedAsync(ticketId!, cancellationToken);
138+
139+
return approved
140+
? PolicyDecision.Allow(Name, "External ticket is approved.")
141+
: PolicyDecision.Deny("External ticket is not approved.", Name);
142+
}
143+
}
144+
```
145+
105146
### Results and changes
106147

107148
- `MutationResult<TState>`

0 commit comments

Comments
 (0)