Require planning before activation replay

This commit is contained in:
2026-05-25 15:52:13 -04:00
parent a71a7ce84f
commit 1b8b6cabf0
+48 -1
View File
@@ -340,6 +340,12 @@ fn hydrate_session(
record.sequence record.sequence
)); ));
} }
if runtime_state != RuntimeState::Planning {
return Err(anyhow!(
"commitment activation requires planning runtime at sequence {}",
record.sequence
));
}
if record.runtime_state != RuntimeState::Active { if record.runtime_state != RuntimeState::Active {
return Err(anyhow!( return Err(anyhow!(
"commitment activation runtime mismatch at sequence {}", "commitment activation runtime mismatch at sequence {}",
@@ -951,7 +957,48 @@ mod tests {
let err = SessionController::new(&path) let err = SessionController::new(&path)
.expect_err("standalone commitment without policy must be rejected"); .expect_err("standalone commitment without policy must be rejected");
assert!(error_chain_contains(&err, "active policy")); assert!(error_chain_contains(&err, "planning"));
fs::remove_file(path).unwrap();
}
#[test]
fn rejects_commitment_created_before_planning_transition_on_replay() {
let path = temp_log_path("commitment-before-planning");
let commitment_id = "commitment-test";
{
let mut log = EventLog::open(&path).unwrap();
log.append_batch([
PendingEventRecord {
event_type: EventType::CommitmentCreated,
runtime_state: RuntimeState::Active,
commitment_id: Some(commitment_id.to_string()),
payload_json: commitment_payload(commitment_id, CommitmentState::Active),
},
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::PolicyApplied,
runtime_state: RuntimeState::Active,
commitment_id: Some(commitment_id.to_string()),
payload_json: policy_payload(commitment_id),
},
])
.unwrap();
}
let err = SessionController::new(&path)
.expect_err("commitment activation before planning must be rejected");
assert!(error_chain_contains(&err, "planning"));
fs::remove_file(path).unwrap(); fs::remove_file(path).unwrap();
} }