Accept legacy review return events

This commit is contained in:
2026-05-26 17:25:11 -04:00
parent 25aeb58ae8
commit 1706e8afd8
+84
View File
@@ -775,6 +775,10 @@ fn hydrate_session(
record.sequence
)
})?;
let legacy_unlinked_review_return = record.commitment_id.is_none()
&& !review_completed
&& commitment.state == CommitmentState::Active;
if !legacy_unlinked_review_return {
ensure_record_commitment(&record, &commitment.id)?;
if !review_completed {
return Err(anyhow!(
@@ -789,6 +793,7 @@ fn hydrate_session(
));
}
}
}
_ => {}
}
@@ -2318,6 +2323,85 @@ mod tests {
fs::remove_file(path).unwrap();
}
#[test]
fn replay_accepts_legacy_unlinked_review_to_planning() {
let path = temp_log_path("legacy-review-return");
let first_commitment_id = "commitment-legacy-first";
let second_commitment_id = "commitment-legacy-second";
{
let mut log = EventLog::open(&path).unwrap();
log.append_batch([
PendingEventRecord {
event_type: EventType::RuntimeTransition,
runtime_state: RuntimeState::Planning,
commitment_id: None,
payload_json: json!({
"schema_version": POLICY_SCHEMA_VERSION,
"action": "enter_planning",
"from": "locked",
"to": "planning",
}),
},
PendingEventRecord {
event_type: EventType::CommitmentCreated,
runtime_state: RuntimeState::Active,
commitment_id: Some(first_commitment_id.to_string()),
payload_json: commitment_payload(first_commitment_id, CommitmentState::Active),
},
PendingEventRecord {
event_type: EventType::PolicyApplied,
runtime_state: RuntimeState::Active,
commitment_id: Some(first_commitment_id.to_string()),
payload_json: policy_payload(first_commitment_id),
},
PendingEventRecord {
event_type: EventType::RuntimeTransition,
runtime_state: RuntimeState::Review,
commitment_id: Some(first_commitment_id.to_string()),
payload_json: json!({
"schema_version": POLICY_SCHEMA_VERSION,
"action": "complete_for_review",
"from": "active",
"to": "review",
}),
},
PendingEventRecord {
event_type: EventType::RuntimeTransition,
runtime_state: RuntimeState::Planning,
commitment_id: None,
payload_json: json!({
"schema_version": POLICY_SCHEMA_VERSION,
"action": "return_to_planning_after_review",
"from": "review",
"to": "planning",
}),
},
PendingEventRecord {
event_type: EventType::CommitmentCreated,
runtime_state: RuntimeState::Active,
commitment_id: Some(second_commitment_id.to_string()),
payload_json: commitment_payload(second_commitment_id, CommitmentState::Active),
},
PendingEventRecord {
event_type: EventType::PolicyApplied,
runtime_state: RuntimeState::Active,
commitment_id: Some(second_commitment_id.to_string()),
payload_json: policy_payload(second_commitment_id),
},
])
.unwrap();
}
let reopened = SessionController::new(&path).unwrap();
assert_eq!(reopened.runtime_state(), RuntimeState::Active);
assert_eq!(
reopened.active_commitment().unwrap().id,
second_commitment_id
);
fs::remove_file(path).unwrap();
}
#[test]
fn replay_tolerates_review_completed_without_changing_runtime() {
let path = temp_log_path("review-completed-no-transition");