Skip to content

Commit e261e34

Browse files
committed
Test: Add governance runtime characterization coverage
Added - Governance test project for request lifecycle and version resolution behavior - Segregated characterization tests for lifecycle atomicity, store overwrite semantics, and resolution persistence - Shared test support for seeded requests and stale-snapshot stores Result Governance runtime risks now have executable characterization coverage so concurrency, store-contract, and resolution-persistence behavior can be validated before changing the execution model.
1 parent 24fa1d3 commit e261e34

7 files changed

Lines changed: 248 additions & 1 deletion

ModularityKit.Mutator.slnx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
<Project Path="Examples/Governance/RequestLifecycle/RequestLifecycle.csproj" />
1212
<Project Path="Examples/Governance/VersionedResolution/VersionedResolution.csproj" />
1313
</Folder>
14-
<Folder Name="/Tests/" />
14+
<Folder Name="/Tests/">
15+
<Project Path="Tests/ModularityKit.Mutator.Governance.Tests/ModularityKit.Mutator.Governance.Tests.csproj" />
16+
</Folder>
1517
<Folder Name="/Benchmarks/">
1618
<Project Path="Benchmarks/ModularityKit.Mutator.Benchmarks.csproj" />
1719
</Folder>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using ModularityKit.Mutator.Abstractions.Context;
2+
using ModularityKit.Mutator.Governance.Abstractions.Lifecycle;
3+
using ModularityKit.Mutator.Governance.Runtime.Lifecycle;
4+
using ModularityKit.Mutator.Governance.Tests.TestSupport;
5+
using Xunit;
6+
7+
namespace ModularityKit.Mutator.Governance.Tests.Lifecycle;
8+
9+
public sealed class MutationRequestLifecycleAtomicityTests
10+
{
11+
[Fact]
12+
public async Task Stale_snapshot_transition_can_succeed_after_a_prior_lifecycle_update()
13+
{
14+
var request = MutationRequestTestFactory.CreatePendingRequest();
15+
var store = new StaleSnapshotMutationRequestStore(request);
16+
var manager = new MutationRequestLifecycleManager(store);
17+
18+
var approved = await manager.Approve(
19+
request.RequestId,
20+
MutationContext.User("approver", "Approver", "Approve request"));
21+
22+
var canceled = await manager.Cancel(
23+
request.RequestId,
24+
MutationContext.User("operator", "Operator", "Cancel request"));
25+
26+
Assert.Equal(2, store.StoreCount);
27+
Assert.All(store.GetSnapshots, snapshot => Assert.Equal(MutationRequestStatus.Pending, snapshot.Status));
28+
Assert.Equal(MutationRequestStatus.Approved, approved.Status);
29+
Assert.Equal(MutationRequestStatus.Canceled, canceled.Status);
30+
Assert.NotEqual(approved.Status, canceled.Status);
31+
Assert.Contains(store.Current.Status, new[] { MutationRequestStatus.Approved, MutationRequestStatus.Canceled });
32+
}
33+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using ModularityKit.Mutator.Abstractions.Context;
2+
using ModularityKit.Mutator.Governance.Abstractions.Lifecycle;
3+
using ModularityKit.Mutator.Governance.Abstractions.Requests;
4+
using ModularityKit.Mutator.Governance.Runtime.Storage;
5+
using ModularityKit.Mutator.Governance.Tests.TestSupport;
6+
using Xunit;
7+
8+
namespace ModularityKit.Mutator.Governance.Tests.Lifecycle;
9+
10+
public sealed class MutationRequestStoreContractTests
11+
{
12+
[Fact]
13+
public async Task Store_contract_allows_blind_overwrite_without_expected_revision_or_status()
14+
{
15+
var store = new InMemoryMutationRequestStore();
16+
var request = MutationRequestTestFactory.CreatePendingRequest();
17+
18+
await store.Store(request);
19+
20+
var overwritten = request with
21+
{
22+
Status = MutationRequestStatus.Canceled,
23+
PendingReason = null,
24+
Decisions =
25+
[
26+
.. request.Decisions,
27+
MutationRequestDecision.Create(
28+
MutationRequestDecisionType.Canceled,
29+
MutationContext.User("operator", "Operator", "Canceled without guard"))
30+
]
31+
};
32+
33+
await store.Store(overwritten);
34+
35+
var loaded = await store.Get(request.RequestId);
36+
37+
Assert.NotNull(loaded);
38+
Assert.Equal(MutationRequestStatus.Canceled, loaded.Status);
39+
Assert.Equal(overwritten.Decisions.Count, loaded.Decisions.Count);
40+
}
41+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
12+
<PackageReference Include="xunit" Version="2.9.3" />
13+
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4">
14+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
15+
<PrivateAssets>all</PrivateAssets>
16+
</PackageReference>
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<ProjectReference Include="..\..\src\ModularityKit.Mutator.Governance.csproj" />
21+
</ItemGroup>
22+
23+
</Project>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using ModularityKit.Mutator.Abstractions.Context;
2+
using ModularityKit.Mutator.Governance.Abstractions.Lifecycle;
3+
using ModularityKit.Mutator.Governance.Abstractions.Resolution;
4+
using ModularityKit.Mutator.Governance.Runtime.Resolution;
5+
using ModularityKit.Mutator.Governance.Runtime.Storage;
6+
using ModularityKit.Mutator.Governance.Tests.TestSupport;
7+
using Xunit;
8+
9+
namespace ModularityKit.Mutator.Governance.Tests.Resolution;
10+
11+
public sealed class MutationRequestVersionResolutionPersistenceTests
12+
{
13+
[Fact]
14+
public async Task Resolve_does_not_persist_decision_history_unless_caller_saves_the_result()
15+
{
16+
var store = new InMemoryMutationRequestStore();
17+
var resolver = new MutationRequestVersionResolver();
18+
var request = MutationRequestTestFactory.CreateApprovedSecurityRequest("v10");
19+
20+
await store.Store(request);
21+
22+
var resolution = resolver.Resolve(
23+
request,
24+
currentStateVersion: "v15",
25+
resolutionContext: MutationContext.User("approver", "Approver", "Resolve request"),
26+
strategy: VersionedRequestResolutionStrategy.RejectStale);
27+
28+
var loaded = await store.Get(request.RequestId);
29+
30+
Assert.NotNull(loaded);
31+
Assert.Equal(2, loaded.Decisions.Count);
32+
Assert.Equal(3, resolution.Request.Decisions.Count);
33+
Assert.Equal(MutationRequestStatus.Approved, loaded.Status);
34+
Assert.Equal(MutationRequestStatus.Rejected, resolution.Request.Status);
35+
}
36+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using ModularityKit.Mutator.Abstractions.Context;
2+
using ModularityKit.Mutator.Abstractions.Intent;
3+
using ModularityKit.Mutator.Governance.Abstractions.Lifecycle;
4+
using ModularityKit.Mutator.Governance.Abstractions.Requests;
5+
6+
namespace ModularityKit.Mutator.Governance.Tests.TestSupport;
7+
8+
internal static class MutationRequestTestFactory
9+
{
10+
public static MutationRequest CreatePendingRequest()
11+
{
12+
return MutationRequest.Pending(
13+
stateId: "tenant-42:quota",
14+
stateType: "QuotaPolicy",
15+
mutationType: "IncreaseQuotaMutation",
16+
intent: new MutationIntent
17+
{
18+
OperationName = "IncreaseQuota",
19+
Category = "Billing",
20+
Description = "Raise quota"
21+
},
22+
context: MutationContext.User("alice", "Alice", "Need more quota"),
23+
pendingReason: PendingMutationReason.Approval,
24+
expectedStateVersion: "v12");
25+
}
26+
27+
public static MutationRequest CreateApprovedSecurityRequest(string expectedStateVersion)
28+
{
29+
return MutationRequest.Approved(
30+
stateId: "tenant-42:roles",
31+
stateType: "IamRoleState",
32+
mutationType: "GrantRoleMutation",
33+
intent: new MutationIntent
34+
{
35+
OperationName = "GrantRole",
36+
Category = "Security",
37+
Description = "Grant elevated access"
38+
},
39+
context: MutationContext.User("requester", "Requester", "Need access"),
40+
expectedStateVersion: expectedStateVersion);
41+
}
42+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using ModularityKit.Mutator.Governance.Abstractions.Lifecycle;
2+
using ModularityKit.Mutator.Governance.Abstractions.Requests;
3+
using ModularityKit.Mutator.Governance.Abstractions.Storage;
4+
5+
namespace ModularityKit.Mutator.Governance.Tests.TestSupport;
6+
7+
internal sealed class StaleSnapshotMutationRequestStore(MutationRequest seedRequest) : IMutationRequestStore
8+
{
9+
private readonly object _gate = new();
10+
private readonly MutationRequest _seedRequest = seedRequest;
11+
private readonly List<MutationRequest> _getSnapshots = [];
12+
private MutationRequest _current = seedRequest;
13+
14+
public int StoreCount { get; private set; }
15+
16+
public MutationRequest Current
17+
{
18+
get
19+
{
20+
lock (_gate)
21+
{
22+
return _current;
23+
}
24+
}
25+
}
26+
27+
public IReadOnlyList<MutationRequest> GetSnapshots => _getSnapshots;
28+
29+
public Task Store(
30+
MutationRequest request,
31+
CancellationToken cancellationToken = default)
32+
{
33+
lock (_gate)
34+
{
35+
StoreCount++;
36+
_current = request;
37+
}
38+
39+
return Task.CompletedTask;
40+
}
41+
42+
public Task<MutationRequest?> Get(
43+
string requestId,
44+
CancellationToken cancellationToken = default)
45+
{
46+
lock (_gate)
47+
{
48+
var snapshot = _seedRequest;
49+
_getSnapshots.Add(snapshot);
50+
51+
return Task.FromResult<MutationRequest?>(snapshot);
52+
}
53+
}
54+
55+
public Task<IReadOnlyList<MutationRequest>> GetByStateId(
56+
string stateId,
57+
CancellationToken cancellationToken = default)
58+
=> Task.FromResult<IReadOnlyList<MutationRequest>>([]);
59+
60+
public Task<IReadOnlyList<MutationRequest>> GetPendingByStateId(
61+
string stateId,
62+
PendingMutationReason? reason = null,
63+
CancellationToken cancellationToken = default)
64+
=> Task.FromResult<IReadOnlyList<MutationRequest>>([]);
65+
66+
public Task<IReadOnlyList<MutationRequest>> GetPending(
67+
PendingMutationReason? reason = null,
68+
CancellationToken cancellationToken = default)
69+
=> Task.FromResult<IReadOnlyList<MutationRequest>>([]);
70+
}

0 commit comments

Comments
 (0)