Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions Examples/Governance/VersionedResolution/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ It is the direct runnable example for the semantics introduced around `ExpectedS
- resolving stale requests with `RevalidateOnLatestState`
- persisting a resolved outcome through `MutationRequestVersionResolutionManager`
- inspecting the resulting lifecycle state and appended decision history
- observing that revalidation is represented as `Pending` with `PendingMutationReason.Revalidation`

## Key files

Expand Down Expand Up @@ -67,6 +68,7 @@ The sample prints one block per resolution strategy and one persisted-resolution
- selected outcome
- whether the request was stale
- resulting request status
- the revalidation pending reason when the latest-state branch is selected
- updated expected version
- last decision recorded during resolution
- persisted request revision for the runtime path
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,48 @@ public async Task ExecuteApproved_requires_renewed_approval_before_execution_whe
Assert.Equal(MutationRequestVersionResolutionOutcome.RequiresRenewedApproval, result.Resolution.Outcome);
}

[Fact]
public async Task ExecuteApproved_revalidates_and_executes_against_latest_state_when_strategy_demands_it()
{
var services = new ServiceCollection();
services.AddMutators(MutationEngineOptions.Strict);
await using var provider = services.BuildServiceProvider();

var engine = provider.GetRequiredService<IMutationEngine>();
var requestStore = new InMemoryMutationRequestStore();
var resolutionManager = new MutationRequestVersionResolutionManager(requestStore, new MutationRequestVersionResolver());
var executionManager = new GovernanceExecutionManager(requestStore, resolutionManager, engine);

var request = await requestStore.Create(MutationRequestTestFactory.CreateApprovedSecurityRequest("v10"));
var mutation = new PromoteRoleMutation(
MutationContext.User("operator-1", "Operator One", "Execute approved role promotion"),
nextVersion: "v16");
var state = RoleState.Create("tenant-42:roles", role: "Reader", version: "v15");

var result = await executionManager.ExecuteApproved(
request.RequestId,
mutation,
state,
currentStateVersion: state.Version,
resultingStateVersionProvider: updated => updated.Version,
governanceContext: MutationContext.Service("governance-runtime", "Revalidate and execute"),
strategy: VersionedRequestResolutionStrategy.RevalidateOnLatestState);

Assert.True(result.WasExecuted);
Assert.NotNull(result.MutationResult);
Assert.Equal(MutationRequestVersionResolutionOutcome.RevalidateOnLatestState, result.Resolution.Outcome);
Assert.Equal(MutationRequestStatus.Executed, result.Request.Status);
Assert.Equal("v16", result.ResultingStateVersion);
Assert.Equal("v16", result.Request.ResultingStateVersion);
Assert.Equal("v16", result.Request.ExpectedStateVersion);
Assert.Equal(
MutationRequestDecisionType.Lifecycle(MutationRequestLifecycleDecisionType.Executed),
result.Request.Decisions[^1].Type);
Assert.Contains(
result.Request.Decisions,
decision => decision.Type == MutationRequestDecisionType.VersionResolution(MutationRequestVersionResolutionDecisionType.RevalidationRequired));
}

private sealed record RoleState(string StateId, string Role, string Version)
{
public static RoleState Create(string stateId, string role, string version) => new(stateId, role, version);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using ModularityKit.Mutator.Governance.Abstractions.Lifecycle.Model;
using ModularityKit.Mutator.Governance.Abstractions.Requests.Decisions;
using ModularityKit.Mutator.Governance.Abstractions.Requests.Model;
using ModularityKit.Mutator.Governance.Abstractions.Resolution.Model;
using ModularityKit.Mutator.Governance.Abstractions.Resolution.Strategies;
using ModularityKit.Mutator.Governance.Runtime.Resolution.Execution;
using ModularityKit.Mutator.Governance.Runtime.Storage;
Expand Down Expand Up @@ -63,6 +64,35 @@ public async Task ResolveAndStore_persists_decision_history_and_state()
Assert.Equal(loaded, resolution.Request);
}

[Fact]
public async Task ResolveAndStore_revalidation_marks_request_pending_for_revalidation()
{
var store = new InMemoryMutationRequestStore();
var resolver = new MutationRequestVersionResolver();
var manager = new MutationRequestVersionResolutionManager(store, resolver);
var request = await store.Create(MutationRequestTestFactory.CreateApprovedSecurityRequest("v10"));

var resolution = await manager.ResolveAndStore(
request.RequestId,
currentStateVersion: "v15",
resolutionContext: MutationContext.User("approver", "Approver", "Revalidate request"),
strategy: VersionedRequestResolutionStrategy.RevalidateOnLatestState);

var loaded = await store.Get(request.RequestId);

Assert.NotNull(loaded);
Assert.Equal(3, loaded.Decisions.Count);
Assert.Equal(
MutationRequestDecisionType.VersionResolution(MutationRequestVersionResolutionDecisionType.RevalidationRequired),
loaded.Decisions[^1].Type);
Assert.Equal(MutationRequestStatus.Pending, loaded.Status);
Assert.Equal(PendingMutationReason.Revalidation, loaded.PendingReason);
Assert.Equal("v15", loaded.ExpectedStateVersion);
Assert.Equal(1, loaded.Revision);
Assert.Equal(MutationRequestVersionResolutionOutcome.RevalidateOnLatestState, resolution.Outcome);
Assert.Equal(loaded, resolution.Request);
}

[Fact]
public async Task ResolveAndStore_throws_not_found_for_missing_request()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,24 @@ namespace ModularityKit.Mutator.Governance.Abstractions.Approval.Model;
/// </summary>
public enum MutationApprovalRequirementStatus
{
/// <summary>
/// The approval requirement is still waiting for a decision.
/// </summary>
Pending = 0,
/// <summary>
/// The approval requirement has been approved.
/// </summary>
Approved = 1,
/// <summary>
/// The approval requirement has been rejected.
/// </summary>
Rejected = 2,
/// <summary>
/// The approval requirement has been satisfied by quorum or equivalent policy.
/// </summary>
Satisfied = 3,
/// <summary>
/// The approval requirement expired before it was resolved.
/// </summary>
Expired = 4
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,36 @@ namespace ModularityKit.Mutator.Governance.Abstractions.Lifecycle.Model;
/// </summary>
public enum MutationRequestStatus
{
/// <summary>
/// The request has been created but not yet processed.
/// </summary>
Created = 0,
/// <summary>
/// The request is waiting for an external governance condition.
/// </summary>
Pending = 1,
/// <summary>
/// The request has been approved and may proceed to execution.
/// </summary>
Approved = 2,
/// <summary>
/// The request has been rejected and will not proceed.
/// </summary>
Rejected = 3,
/// <summary>
/// The request has been canceled by an explicit action.
/// </summary>
Canceled = 4,
/// <summary>
/// The request expired before it could be completed.
/// </summary>
Expired = 5,
/// <summary>
/// The request has been superseded by another request.
/// </summary>
Superseded = 6,
/// <summary>
/// The request has been executed successfully.
/// </summary>
Executed = 7
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,38 @@ namespace ModularityKit.Mutator.Governance.Abstractions.Lifecycle.Model;
/// </summary>
public enum PendingMutationReason
{
/// <summary>
/// The request is waiting for approval.
/// </summary>
Approval = 0,

/// <summary>
/// The request is waiting for an external check or integration response.
/// </summary>
ExternalCheck = 1,

/// <summary>
/// The request is waiting for a scheduled execution window.
/// </summary>
Schedule = 2,

/// <summary>
/// The request is waiting for a dependency to become ready.
/// </summary>
Dependency = 3,

/// <summary>
/// The request is waiting because of quota constraints.
/// </summary>
Quota = 4,
ManualReview = 5

/// <summary>
/// The request is waiting for manual review.
/// </summary>
ManualReview = 5,

/// <summary>
/// The request is pending because it must be revalidated against the latest state before execution.
/// </summary>
Revalidation = 6
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,24 @@ namespace ModularityKit.Mutator.Governance.Abstractions.Requests.Decisions;
/// </summary>
public enum MutationRequestApprovalDecisionType
{
/// <summary>
/// The approval requirement was requested.
/// </summary>
Requested = 0,
/// <summary>
/// The approval requirement was granted.
/// </summary>
Granted = 1,
/// <summary>
/// The approval requirement was rejected.
/// </summary>
Rejected = 2,
/// <summary>
/// The approval quorum for a group was satisfied.
/// </summary>
QuorumSatisfied = 3,
/// <summary>
/// The approval requirement expired.
/// </summary>
Expired = 4
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@ namespace ModularityKit.Mutator.Governance.Abstractions.Requests.Decisions;
/// </summary>
public enum MutationRequestDecisionCategory
{
/// <summary>
/// Lifecycle decisions describe request state transitions.
/// </summary>
Lifecycle = 0,
/// <summary>
/// Approval decisions describe request-level approval processing.
/// </summary>
Approval = 1,
/// <summary>
/// Version-resolution decisions describe stale and version-aware handling.
/// </summary>
VersionResolution = 2
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,36 @@ namespace ModularityKit.Mutator.Governance.Abstractions.Requests.Decisions;
/// </summary>
public enum MutationRequestLifecycleDecisionType
{
/// <summary>
/// The request was submitted into the governance system.
/// </summary>
Submitted = 0,
/// <summary>
/// The request entered a pending lifecycle state.
/// </summary>
Pending = 1,
/// <summary>
/// The request was approved.
/// </summary>
Approved = 2,
/// <summary>
/// The request was rejected.
/// </summary>
Rejected = 3,
/// <summary>
/// The request was canceled.
/// </summary>
Canceled = 4,
/// <summary>
/// The request expired before completion.
/// </summary>
Expired = 5,
/// <summary>
/// The request was superseded by another request.
/// </summary>
Superseded = 6,
/// <summary>
/// The request executed successfully.
/// </summary>
Executed = 7
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,20 @@ namespace ModularityKit.Mutator.Governance.Abstractions.Requests.Decisions;
/// </summary>
public enum MutationRequestVersionResolutionDecisionType
{
/// <summary>
/// The request version matched the current state version.
/// </summary>
Validated = 0,
/// <summary>
/// The request must be revalidated against the latest state.
/// </summary>
RevalidationRequired = 1,
/// <summary>
/// The request must obtain renewed approval before proceeding.
/// </summary>
RenewedApprovalRequired = 2,
/// <summary>
/// The request was rejected because it was stale.
/// </summary>
RejectedAsStale = 3
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,20 @@ namespace ModularityKit.Mutator.Governance.Abstractions.Resolution.Model;
/// </summary>
public enum MutationRequestVersionResolutionOutcome
{
/// <summary>
/// The request can be executed with its approved version.
/// </summary>
ExecuteApprovedVersion = 0,
/// <summary>
/// The request should be revalidated on the latest state.
/// </summary>
RevalidateOnLatestState = 1,
/// <summary>
/// The request was rejected as stale.
/// </summary>
RejectedAsStale = 2,
/// <summary>
/// The request requires renewed approval.
/// </summary>
RequiresRenewedApproval = 3
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@ namespace ModularityKit.Mutator.Governance.Abstractions.Resolution.Strategies;
/// </summary>
public enum VersionedRequestResolutionStrategy
{
/// <summary>
/// Reject the request if the observed state version differs from the expected version.
/// </summary>
RejectStale = 0,
/// <summary>
/// Send the request back through approval when the state has drifted.
/// </summary>
RequireRenewedApproval = 1,
/// <summary>
/// Revalidate the request against the latest state before execution.
/// </summary>
RevalidateOnLatestState = 2
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ public static MutationRequest ApplyRevalidationRequired(
return AppendDecision(
request with
{
Status = MutationRequestStatus.Approved,
PendingReason = null,
Status = MutationRequestStatus.Pending,
PendingReason = PendingMutationReason.Revalidation,
ExpectedStateVersion = currentStateVersion,
UpdatedAt = decision.Timestamp
},
Expand Down
Loading