Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
test(core): cover mixed sync and async policy evaluation
  • Loading branch information
rian-be committed Jun 30, 2026
commit c0e08372886ef7a11e994f6081c2acaa810bfa74
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ public async Task ExecuteAsync_uses_sync_policy_path_without_async_override()
Assert.Equal("updated", result.NewState!.Value);
}

[Fact]
public async Task ExecuteAsync_allows_sync_and_async_policies_to_coexist_without_ambiguous_ordering()
{
var engine = CreateEngine();
var observed = new List<string>();

engine.RegisterPolicy(new ObservedSyncAllowPolicy(observed));
engine.RegisterPolicy(new ObservedAsyncAllowPolicy(observed));

var result = await engine.ExecuteAsync(new SampleMutation(), new SampleState("initial"));

Assert.True(result.IsSuccess);
Assert.Equal(["async", "sync"], observed);
}

private static IMutationEngine CreateEngine(Action<MutationEngineOptions>? configure = null)
{
var services = new ServiceCollection();
Expand Down Expand Up @@ -113,6 +128,36 @@ public PolicyDecision Evaluate(IMutation<SampleState> mutation, SampleState stat
=> PolicyDecision.Allow(Name, "Synchronous policy allowed the mutation.");
}

private sealed class ObservedSyncAllowPolicy(List<string> observed) : IMutationPolicy<SampleState>
{
public string Name => "ObservedSyncAllow";
public int Priority => 10;
public string? Description => "Records synchronous policy evaluation order.";

public PolicyDecision Evaluate(IMutation<SampleState> mutation, SampleState state)
{
observed.Add("sync");
return PolicyDecision.Allow(Name, "Synchronous policy allowed the mutation.");
}
}

private sealed class ObservedAsyncAllowPolicy(List<string> observed) : IMutationPolicy<SampleState>
{
public string Name => "ObservedAsyncAllow";
public int Priority => 100;
public string? Description => "Records asynchronous policy evaluation order.";

public async Task<PolicyDecision> EvaluateAsync(
IMutation<SampleState> mutation,
SampleState state,
CancellationToken cancellationToken = default)
{
await Task.Delay(10, cancellationToken);
observed.Add("async");
return PolicyDecision.Allow(Name, "Asynchronous policy allowed the mutation.");
}
}

private sealed class AsyncBlockingPolicy : IMutationPolicy<SampleState>
{
public string Name => "AsyncBlocking";
Expand Down
9 changes: 9 additions & 0 deletions src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,20 @@ Core runtime concurrency is controlled by `MutationEngineOptions.MaxConcurrentMu
- implement `Evaluate(...)` for lightweight in-process rules
- implement `EvaluateAsync(..., CancellationToken)` for external identity, ticketing, quota, or compliance checks
- the runtime evaluates policies in descending `Priority` order
- sync and async policies can be registered together; they participate in the same priority-ordered pipeline
- when `MutationEngineOptions.PolicyEvaluationTimeout` is set, the timeout is applied per policy evaluation
- caller cancellation still flows through unchanged
- policy failures are surfaced as `PolicyEvaluationException`
- policy timeouts are surfaced as `PolicyEvaluationTimeoutException`

Typical integration cases include:

- external approval evidence checks
- actor/identity resolution against IAM or directory systems
- ticket status validation before execution
- quota lookups in remote control planes
- compliance or risk verification before commit

```csharp
public sealed class RequireApprovedTicketPolicy : IMutationPolicy<QuotaState>
{
Expand Down
Loading