1010using ModularityKit . Mutator . Abstractions . Results ;
1111using ModularityKit . Mutator . Governance . Abstractions . Execution . Contracts ;
1212using ModularityKit . Mutator . Governance . Abstractions . Execution . Model ;
13+ using ModularityKit . Mutator . Governance . Abstractions . Execution . Model . Compensation ;
14+ using ModularityKit . Mutator . Governance . Abstractions . Execution . Model . Links ;
1315using ModularityKit . Mutator . Governance . Abstractions . Lifecycle . Model ;
1416using ModularityKit . Mutator . Governance . Abstractions . Requests . Factory ;
1517using ModularityKit . Mutator . Governance . Abstractions . Requests . Decisions ;
@@ -75,9 +77,9 @@ public async Task ExecuteApproved_executes_request_persists_resulting_version_an
7577 Assert . NotNull ( result . MutationResult ) ;
7678 Assert . Equal ( "v11" , result . ResultingStateVersion ) ;
7779 Assert . Equal ( MutationRequestStatus . Executed , result . Request . Status ) ;
78- Assert . Equal ( "v11" , result . Request . ResultingStateVersion ) ;
79- Assert . Equal ( "v11" , result . Request . ExpectedStateVersion ) ;
80- Assert . NotNull ( result . Request . ExecutedAt ) ;
80+ Assert . Equal ( "v11" , result . Request . Versioning . ResultingStateVersion ) ;
81+ Assert . Equal ( "v11" , result . Request . Versioning . ExpectedStateVersion ) ;
82+ Assert . NotNull ( result . Request . Versioning . ExecutedAt ) ;
8183 Assert . Single ( result . Request . SideEffects ) ;
8284 Assert . Equal ( "RoleElevated" , result . Request . SideEffects [ 0 ] . Type ) ;
8385 Assert . Equal ( "governance.execution-effect" , result . Request . SideEffects [ 0 ] . DataContractType ) ;
@@ -180,7 +182,7 @@ public async Task ExecuteApproved_requires_renewed_approval_before_execution_whe
180182 Assert . Null ( result . MutationResult ) ;
181183 Assert . Equal ( MutationRequestStatus . Pending , result . Request . Status ) ;
182184 Assert . Equal ( PendingMutationReason . Approval , result . Request . PendingReason ) ;
183- Assert . Equal ( "v15" , result . Request . ExpectedStateVersion ) ;
185+ Assert . Equal ( "v15" , result . Request . Versioning . ExpectedStateVersion ) ;
184186 Assert . Equal ( MutationRequestVersionResolutionOutcome . RequiresRenewedApproval , result . Resolution . Outcome ) ;
185187 }
186188
@@ -223,8 +225,8 @@ public async Task ExecuteApproved_revalidates_and_executes_against_latest_state_
223225 Assert . Equal ( MutationRequestVersionResolutionOutcome . RevalidateOnLatestState , result . Resolution . Outcome ) ;
224226 Assert . Equal ( MutationRequestStatus . Executed , result . Request . Status ) ;
225227 Assert . Equal ( "v16" , result . ResultingStateVersion ) ;
226- Assert . Equal ( "v16" , result . Request . ResultingStateVersion ) ;
227- Assert . Equal ( "v16" , result . Request . ExpectedStateVersion ) ;
228+ Assert . Equal ( "v16" , result . Request . Versioning . ResultingStateVersion ) ;
229+ Assert . Equal ( "v16" , result . Request . Versioning . ExpectedStateVersion ) ;
228230 Assert . Equal (
229231 MutationRequestDecisionType . Lifecycle ( MutationRequestLifecycleDecisionType . Executed ) ,
230232 result . Request . Decisions [ ^ 1 ] . Type ) ;
@@ -233,6 +235,110 @@ public async Task ExecuteApproved_revalidates_and_executes_against_latest_state_
233235 decision => decision . Type == MutationRequestDecisionType . VersionResolution ( MutationRequestVersionResolutionDecisionType . RevalidationRequired ) ) ;
234236 }
235237
238+ [ Fact ]
239+ public async Task ExecuteApproved_executes_operator_rollback_compensation_and_links_execution_history ( )
240+ {
241+ var services = new ServiceCollection ( ) ;
242+ services . AddMutators ( MutationEngineOptions . Strict ) ;
243+ await using var provider = services . BuildServiceProvider ( ) ;
244+
245+ var engine = provider . GetRequiredService < IMutationEngine > ( ) ;
246+ var auditor = provider . GetRequiredService < IMutationAuditor > ( ) ;
247+ var historyStore = provider . GetRequiredService < IMutationHistoryStore > ( ) ;
248+ var requestStore = new InMemoryMutationRequestStore ( ) ;
249+ var resolutionManager = new MutationRequestVersionResolutionManager ( requestStore , new MutationRequestVersionResolver ( ) ) ;
250+ var executionManager = new GovernanceExecutionManager ( requestStore , resolutionManager , engine ) ;
251+
252+ var originalRequest = await requestStore . Create ( MutationRequestFactory . Approved < RoleState , PromoteRoleMutation > (
253+ stateId : "tenant-42:roles" ,
254+ intent : new MutationIntent
255+ {
256+ OperationName = "GrantRole" ,
257+ Category = "Security" ,
258+ Description = "Grant elevated access" ,
259+ IsReversible = true
260+ } ,
261+ context : MutationContext . User ( "requester" , "Requester" , "Need access" ) ,
262+ expectedStateVersion : "v10" ) ) ;
263+
264+ var originalState = RoleState . Create ( "tenant-42:roles" , role : "Reader" , version : "v10" ) ;
265+ var originalMutation = new PromoteRoleMutation (
266+ MutationContext . User ( "operator-1" , "Operator One" , "Execute approved role promotion" ) ,
267+ nextVersion : "v11" ) ;
268+
269+ var originalResult = await executionManager . ExecuteApproved (
270+ originalRequest . RequestId ,
271+ originalMutation ,
272+ originalState ,
273+ governanceContext : MutationContext . Service ( "governance-runtime" , "Execute approved request" ) ,
274+ strategy : VersionedRequestResolutionStrategy . RejectStale ) ;
275+
276+ var compensationPlan = new GovernedCompensationPlan
277+ {
278+ OriginalRequestId = originalRequest . RequestId ,
279+ Kind = GovernedCompensationKind . Rollback ,
280+ Trigger = GovernedCompensationTrigger . OperatorRollback ,
281+ Reason = "Operator reverted the elevated role after incident review."
282+ } ;
283+
284+ var compensationRequest = await requestStore . Create ( CompensationMutationRequestFactory . Approved < RoleState , RollbackRoleMutation > (
285+ stateId : "tenant-42:roles" ,
286+ intent : new MutationIntent
287+ {
288+ OperationName = "RollbackRole" ,
289+ Category = "Security" ,
290+ Description = "Restore the previous tenant role"
291+ } ,
292+ context : MutationContext . User ( "operator-2" , "Operator Two" , "Rollback approved role mutation" ) ,
293+ compensation : compensationPlan ,
294+ expectedStateVersion : "v11" ) ) ;
295+
296+ var compensationMutation = new RollbackRoleMutation (
297+ MutationContext . User ( "operator-2" , "Operator Two" , "Rollback approved role mutation" ) ,
298+ nextVersion : "v12" ) ;
299+
300+ var compensationResult = await executionManager . ExecuteApproved (
301+ compensationRequest . RequestId ,
302+ compensationMutation ,
303+ originalResult . MutationResult ! . NewState ! ,
304+ governanceContext : MutationContext . Service ( "governance-runtime" , "Execute operator rollback" ) ,
305+ strategy : VersionedRequestResolutionStrategy . RejectStale ) ;
306+
307+ Assert . True ( compensationResult . WasExecuted ) ;
308+ Assert . Equal ( GovernedExecutionKind . Compensation , compensationResult . ExecutionKind ) ;
309+ Assert . NotNull ( compensationResult . Compensation ) ;
310+ Assert . Equal ( originalRequest . RequestId , compensationResult . Compensation ! . OriginalRequestId ) ;
311+ Assert . Equal ( GovernedCompensationKind . Rollback , compensationResult . Compensation . Kind ) ;
312+ Assert . Contains (
313+ compensationResult . Request . Execution . RelatedExecutions ,
314+ link => link . RequestId == originalRequest . RequestId &&
315+ link . Type == GovernedExecutionLinkType . Compensates ) ;
316+
317+ var compensatedOriginalRequest = await requestStore . Get ( originalRequest . RequestId ) ;
318+ Assert . NotNull ( compensatedOriginalRequest ) ;
319+ Assert . Contains (
320+ compensatedOriginalRequest ! . Execution . RelatedExecutions ,
321+ link => link . RequestId == compensationRequest . RequestId &&
322+ link . Type == GovernedExecutionLinkType . CompensatedBy &&
323+ link . ExecutionKind == GovernedExecutionKind . Compensation ) ;
324+ Assert . Contains (
325+ compensatedOriginalRequest . Decisions ,
326+ decision => decision . Type == MutationRequestDecisionType . Lifecycle ( MutationRequestLifecycleDecisionType . Compensated ) ) ;
327+
328+ var auditEntries = await auditor . GetAuditLogAsync ( originalRequest . StateId ) ;
329+ var history = await historyStore . GetHistoryAsync ( originalRequest . StateId ) ;
330+
331+ Assert . Equal ( 2 , auditEntries . Count ) ;
332+ Assert . Equal ( 2 , history . Entries . Count ) ;
333+ Assert . Equal ( "Compensation" , auditEntries [ 1 ] . Context . Metadata [ "GovernanceExecutionKind" ] ) ;
334+ Assert . Equal ( "Compensation" , history . Entries [ 1 ] . Context . Metadata [ "GovernanceExecutionKind" ] ) ;
335+
336+ var auditCompensation = Assert . IsAssignableFrom < IReadOnlyDictionary < string , object > > (
337+ auditEntries [ 1 ] . Context . Metadata [ "GovernanceCompensation" ] ) ;
338+ Assert . Equal ( originalRequest . RequestId , auditCompensation [ "OriginalRequestId" ] ) ;
339+ Assert . Equal ( "Rollback" , auditCompensation [ "Kind" ] ) ;
340+ }
341+
236342 private sealed record RoleState ( string StateId , string Role , string Version ) : IVersionedState
237343 {
238344 public static RoleState Create ( string stateId , string role , string version ) => new ( stateId , role , version ) ;
@@ -282,6 +388,51 @@ public ValidationResult Validate(RoleState state)
282388 public MutationResult < RoleState > Simulate ( RoleState state ) => Apply ( state ) ;
283389 }
284390
391+ private sealed class RollbackRoleMutation ( MutationContext context , string nextVersion ) : IMutation < RoleState >
392+ {
393+ public MutationIntent Intent { get ; } = new ( )
394+ {
395+ OperationName = "RollbackRole" ,
396+ Category = "Security" ,
397+ Description = "Rollback tenant role to Reader" ,
398+ IsReversible = false
399+ } ;
400+
401+ public MutationContext Context { get ; } = context ;
402+
403+ public MutationResult < RoleState > Apply ( RoleState state )
404+ {
405+ var newState = state with
406+ {
407+ Role = "Reader" ,
408+ Version = nextVersion
409+ } ;
410+
411+ return MutationResult < RoleState > . Success (
412+ newState ,
413+ ChangeSet . Single ( StateChange . Modified ( "Role" , state . Role , newState . Role ) ) ,
414+ [
415+ SideEffect . Create (
416+ type : "RoleRollback" ,
417+ description : "Governed compensation restored the previous role" ,
418+ data : new GovernanceExecutionSideEffectData
419+ {
420+ RequestStateId = state . StateId ,
421+ NewRole = newState . Role
422+ } )
423+ ] ) ;
424+ }
425+
426+ public ValidationResult Validate ( RoleState state )
427+ {
428+ return state . Role == "Reader"
429+ ? ValidationResult . WithError ( "Role" , "Role is already Reader." )
430+ : ValidationResult . Success ( ) ;
431+ }
432+
433+ public MutationResult < RoleState > Simulate ( RoleState state ) => Apply ( state ) ;
434+ }
435+
285436 [ SideEffectDataContract ( "governance.execution-effect" , 1 ) ]
286437 private sealed record GovernanceExecutionSideEffectData
287438 {
0 commit comments