Skip to content

Commit a734206

Browse files
committed
Feat: Clarify governance revalidation semantics
1 parent db9a817 commit a734206

5 files changed

Lines changed: 101 additions & 3 deletions

File tree

Examples/Governance/VersionedResolution/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ It is the direct runnable example for the semantics introduced around `ExpectedS
1212
- resolving stale requests with `RevalidateOnLatestState`
1313
- persisting a resolved outcome through `MutationRequestVersionResolutionManager`
1414
- inspecting the resulting lifecycle state and appended decision history
15+
- observing that revalidation is represented as `Pending` with `PendingMutationReason.Revalidation`
1516

1617
## Key files
1718

@@ -67,6 +68,7 @@ The sample prints one block per resolution strategy and one persisted-resolution
6768
- selected outcome
6869
- whether the request was stale
6970
- resulting request status
71+
- the revalidation pending reason when the latest-state branch is selected
7072
- updated expected version
7173
- last decision recorded during resolution
7274
- persisted request revision for the runtime path

Tests/ModularityKit.Mutator.Governance.Tests/Execution/GovernanceExecutionManagerTests.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,48 @@ public async Task ExecuteApproved_requires_renewed_approval_before_execution_whe
143143
Assert.Equal(MutationRequestVersionResolutionOutcome.RequiresRenewedApproval, result.Resolution.Outcome);
144144
}
145145

146+
[Fact]
147+
public async Task ExecuteApproved_revalidates_and_executes_against_latest_state_when_strategy_demands_it()
148+
{
149+
var services = new ServiceCollection();
150+
services.AddMutators(MutationEngineOptions.Strict);
151+
await using var provider = services.BuildServiceProvider();
152+
153+
var engine = provider.GetRequiredService<IMutationEngine>();
154+
var requestStore = new InMemoryMutationRequestStore();
155+
var resolutionManager = new MutationRequestVersionResolutionManager(requestStore, new MutationRequestVersionResolver());
156+
var executionManager = new GovernanceExecutionManager(requestStore, resolutionManager, engine);
157+
158+
var request = await requestStore.Create(MutationRequestTestFactory.CreateApprovedSecurityRequest("v10"));
159+
var mutation = new PromoteRoleMutation(
160+
MutationContext.User("operator-1", "Operator One", "Execute approved role promotion"),
161+
nextVersion: "v16");
162+
var state = RoleState.Create("tenant-42:roles", role: "Reader", version: "v15");
163+
164+
var result = await executionManager.ExecuteApproved(
165+
request.RequestId,
166+
mutation,
167+
state,
168+
currentStateVersion: state.Version,
169+
resultingStateVersionProvider: updated => updated.Version,
170+
governanceContext: MutationContext.Service("governance-runtime", "Revalidate and execute"),
171+
strategy: VersionedRequestResolutionStrategy.RevalidateOnLatestState);
172+
173+
Assert.True(result.WasExecuted);
174+
Assert.NotNull(result.MutationResult);
175+
Assert.Equal(MutationRequestVersionResolutionOutcome.RevalidateOnLatestState, result.Resolution.Outcome);
176+
Assert.Equal(MutationRequestStatus.Executed, result.Request.Status);
177+
Assert.Equal("v16", result.ResultingStateVersion);
178+
Assert.Equal("v16", result.Request.ResultingStateVersion);
179+
Assert.Equal("v16", result.Request.ExpectedStateVersion);
180+
Assert.Equal(
181+
MutationRequestDecisionType.Lifecycle(MutationRequestLifecycleDecisionType.Executed),
182+
result.Request.Decisions[^1].Type);
183+
Assert.Contains(
184+
result.Request.Decisions,
185+
decision => decision.Type == MutationRequestDecisionType.VersionResolution(MutationRequestVersionResolutionDecisionType.RevalidationRequired));
186+
}
187+
146188
private sealed record RoleState(string StateId, string Role, string Version)
147189
{
148190
public static RoleState Create(string stateId, string role, string version) => new(stateId, role, version);

Tests/ModularityKit.Mutator.Governance.Tests/Resolution/MutationRequestVersionResolutionPersistenceTests.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using ModularityKit.Mutator.Governance.Abstractions.Lifecycle.Model;
44
using ModularityKit.Mutator.Governance.Abstractions.Requests.Decisions;
55
using ModularityKit.Mutator.Governance.Abstractions.Requests.Model;
6+
using ModularityKit.Mutator.Governance.Abstractions.Resolution.Model;
67
using ModularityKit.Mutator.Governance.Abstractions.Resolution.Strategies;
78
using ModularityKit.Mutator.Governance.Runtime.Resolution.Execution;
89
using ModularityKit.Mutator.Governance.Runtime.Storage;
@@ -63,6 +64,35 @@ public async Task ResolveAndStore_persists_decision_history_and_state()
6364
Assert.Equal(loaded, resolution.Request);
6465
}
6566

67+
[Fact]
68+
public async Task ResolveAndStore_revalidation_marks_request_pending_for_revalidation()
69+
{
70+
var store = new InMemoryMutationRequestStore();
71+
var resolver = new MutationRequestVersionResolver();
72+
var manager = new MutationRequestVersionResolutionManager(store, resolver);
73+
var request = await store.Create(MutationRequestTestFactory.CreateApprovedSecurityRequest("v10"));
74+
75+
var resolution = await manager.ResolveAndStore(
76+
request.RequestId,
77+
currentStateVersion: "v15",
78+
resolutionContext: MutationContext.User("approver", "Approver", "Revalidate request"),
79+
strategy: VersionedRequestResolutionStrategy.RevalidateOnLatestState);
80+
81+
var loaded = await store.Get(request.RequestId);
82+
83+
Assert.NotNull(loaded);
84+
Assert.Equal(3, loaded.Decisions.Count);
85+
Assert.Equal(
86+
MutationRequestDecisionType.VersionResolution(MutationRequestVersionResolutionDecisionType.RevalidationRequired),
87+
loaded.Decisions[^1].Type);
88+
Assert.Equal(MutationRequestStatus.Pending, loaded.Status);
89+
Assert.Equal(PendingMutationReason.Revalidation, loaded.PendingReason);
90+
Assert.Equal("v15", loaded.ExpectedStateVersion);
91+
Assert.Equal(1, loaded.Revision);
92+
Assert.Equal(MutationRequestVersionResolutionOutcome.RevalidateOnLatestState, resolution.Outcome);
93+
Assert.Equal(loaded, resolution.Request);
94+
}
95+
6696
[Fact]
6797
public async Task ResolveAndStore_throws_not_found_for_missing_request()
6898
{

src/Governance/Abstractions/Lifecycle/Model/PendingMutationReason.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,34 @@ namespace ModularityKit.Mutator.Governance.Abstractions.Lifecycle.Model;
55
/// </summary>
66
public enum PendingMutationReason
77
{
8+
/// <summary>
9+
/// The request is waiting for approval.
10+
/// </summary>
811
Approval = 0,
12+
13+
/// <summary>
14+
/// The request is waiting for an external check or integration response.
15+
/// </summary>
916
ExternalCheck = 1,
17+
18+
/// <summary>
19+
/// The request is waiting for a scheduled execution window.
20+
/// </summary>
1021
Schedule = 2,
22+
23+
/// <summary>
24+
/// The request is waiting for a dependency to become ready.
25+
/// </summary>
1126
Dependency = 3,
27+
28+
/// <summary>
29+
/// The request is waiting because of quota constraints.
30+
/// </summary>
1231
Quota = 4,
13-
ManualReview = 5
32+
33+
/// <summary>
34+
/// The request is waiting for manual review.
35+
/// </summary>
36+
ManualReview = 5,
37+
Revalidation = 6
1438
}

src/Governance/Runtime/Resolution/Execution/MutationRequestVersionResolutionState.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ public static MutationRequest ApplyRevalidationRequired(
7171
return AppendDecision(
7272
request with
7373
{
74-
Status = MutationRequestStatus.Approved,
75-
PendingReason = null,
74+
Status = MutationRequestStatus.Pending,
75+
PendingReason = PendingMutationReason.Revalidation,
7676
ExpectedStateVersion = currentStateVersion,
7777
UpdatedAt = decision.Timestamp
7878
},

0 commit comments

Comments
 (0)