use super::*;
async fn assert_pending_rounds_checkpoint(h: &Harness, expected: &LoopState) {
let durable = h
.storage
.load_session(&h.session.id)
.await
.unwrap()
.unwrap();
assert_eq!(
counted_loop::read_loop_state(&durable).unwrap().as_ref(),
Some(expected),
"the exact state, pending objective, revisions and operation receipts must be durable"
);
assert_eq!(h.policy().generation(), Some(expected.generation.as_str()));
assert!(h.policy().active_counted().is_some());
assert!(!h.policy().stop.is_cancelled());
assert!(!h.cancel.is_cancelled());
}
fn assert_pending_rounds_receipt(
result: &ToolResult,
before: &LoopState,
after: &LoopState,
effective_iteration: u32,
) -> LoopControlReceipt {
assert!(result.success, "{}", result.result);
let receipt: LoopControlReceipt = serde_json::from_str(&result.result).unwrap();
let old = before.control.as_ref().unwrap();
let control = after.control.as_ref().unwrap();
assert_eq!(receipt.generation, before.generation);
assert_eq!(receipt.accepted_revision, old.control_revision + 1);
assert_eq!(receipt.applied_revision_at_acceptance, old.applied_revision);
assert_eq!(receipt.objective_revision, 2);
assert_eq!(receipt.effective_iteration, Some(effective_iteration));
assert!(!receipt.applied);
assert_eq!(control.control_revision, old.control_revision + 1);
assert_eq!(control.applied_revision, old.applied_revision);
assert_eq!(control.operations.len(), old.operations.len() + 1);
assert_eq!(
&control.operations[..old.operations.len()],
old.operations.as_slice(),
"earlier receipts must remain immutable"
);
assert_eq!(control.operations.last().unwrap().receipt, receipt);
assert_eq!(after.objective, "A");
assert_eq!(after.objective_revision, 1);
assert_eq!(
control.pending_objective,
Some(PendingLoopObjective {
origin: None,
objective: "B".into(),
objective_revision: 2,
})
);
assert_eq!(
(
after.iterations_started,
after.iterations_completed,
after.rounds_started
),
(
before.iterations_started,
before.iterations_completed,
before.rounds_started
),
"accepting a command must not refund any consumption"
);
receipt
}
#[tokio::test]
async fn self_control_active_new_objective_requires_two_future_rounds() {
let mut h = Harness::new().await;
let start_command = start("A", 2, 20);
let start_result = h.command(&start_command, 0).await;
assert!(start_result.success, "{}", start_result.result);
h.admit().await;
let before = h.state();
assert_eq!(
(
before.iterations_started,
before.iterations_completed,
before.rounds_started
),
(1, 0, 2)
);
assert!(before.control.as_ref().unwrap().pending_objective.is_none());
let command = h.update(Some("B".into()), None, Some(3));
let result = h.command(&command, 1).await;
denied(&result, "no_budget_for_new_objective");
assert_eq!(
h.state(),
before,
"rejection must not install the new objective or receipt"
);
assert_pending_rounds_checkpoint(&h, &before).await;
let retry = h.command(&command, 1).await;
denied(&retry, "no_budget_for_new_objective");
assert_eq!(retry.result, result.result);
assert_eq!(h.state(), before);
assert_pending_rounds_checkpoint(&h, &before).await;
assert_eq!(
h.command(&start_command, 1).await.result,
start_result.result,
"the original immutable receipt remains retrievable before CAS"
);
assert_eq!(h.state(), before);
assert_pending_rounds_checkpoint(&h, &before).await;
}
#[tokio::test]
async fn self_control_pending_objective_bound_only_update_cannot_leave_one_future_round() {
let mut h = Harness::new().await;
let start_command = start("A", 2, 20);
let start_result = h.command(&start_command, 0).await;
assert!(start_result.success, "{}", start_result.result);
h.admit().await;
let update_b = h.update(Some("B".into()), None, None);
let update_result = h.command(&update_b, 1).await;
assert!(update_result.success, "{}", update_result.result);
let before = h.state();
let control = before.control.as_ref().unwrap();
assert_eq!((control.control_revision, control.applied_revision), (2, 1));
assert_eq!(control.pending_objective.as_ref().unwrap().objective, "B");
assert_eq!(
(
before.iterations_started,
before.iterations_completed,
before.rounds_started
),
(1, 0, 2)
);
for iterations in [None, Some(2)] {
let command = h.update(None, iterations, Some(3));
let result = h.command(&command, 1).await;
denied(&result, "no_budget_for_new_objective");
assert_eq!(
h.state(),
before,
"bound-only {iterations:?} changed pending state"
);
assert_pending_rounds_checkpoint(&h, &before).await;
}
assert_eq!(h.command(&update_b, 1).await.result, update_result.result);
assert_eq!(
h.command(&start_command, 1).await.result,
start_result.result
);
assert_eq!(
h.state(),
before,
"immutable receipt replay must not apply the pending goal"
);
assert_pending_rounds_checkpoint(&h, &before).await;
}
#[tokio::test]
async fn self_control_active_new_or_pending_objective_accepts_exactly_two_future_rounds() {
for pending_before in [false, true] {
let mut h = Harness::new().await;
assert!(h.command(&start("A", 2, 20), 0).await.success);
h.admit().await;
if pending_before {
let update_b = h.update(Some("B".into()), None, None);
assert!(h.command(&update_b, 1).await.success);
}
let before = h.state();
let command = h.update((!pending_before).then(|| "B".into()), None, Some(4));
let result = h.command(&command, 1).await;
assert!(
result.success,
"pending_before={pending_before}: {}",
result.result
);
let accepted = h.state();
let receipt = assert_pending_rounds_receipt(&result, &before, &accepted, 2);
assert_eq!((accepted.max_iterations, accepted.max_total_rounds), (2, 4));
assert_eq!(accepted.max_total_rounds - accepted.rounds_started, 2);
if pending_before {
assert_eq!(
accepted.control.as_ref().unwrap().pending_objective,
before.control.as_ref().unwrap().pending_objective
);
}
assert_pending_rounds_checkpoint(&h, &accepted).await;
h.admit().await;
let current_terminal_round = h.state();
assert_eq!(
(
current_terminal_round.iterations_started,
current_terminal_round.iterations_completed,
current_terminal_round.rounds_started
),
(1, 0, 3)
);
assert_eq!(current_terminal_round.objective, "A");
assert_eq!(current_terminal_round.control, accepted.control);
assert!(matches!(
counted_loop::terminal(&mut h.session, &h.config)
.await
.unwrap(),
counted_loop::TerminalDecision::Continue
));
let completed_a = h.state();
assert_eq!(
(
completed_a.iterations_started,
completed_a.iterations_completed,
completed_a.rounds_started
),
(1, 1, 3)
);
assert_eq!(completed_a.objective, "A");
assert!(completed_a
.control
.as_ref()
.unwrap()
.pending_objective
.is_some());
h.admit().await;
let admitted_b = h.state();
assert_eq!(
(
admitted_b.iterations_started,
admitted_b.iterations_completed,
admitted_b.rounds_started
),
(2, 1, 4)
);
assert_eq!(
(admitted_b.objective.as_str(), admitted_b.objective_revision),
("B", 2)
);
let applied = admitted_b.control.as_ref().unwrap();
assert!(applied.pending_objective.is_none());
assert_eq!(applied.applied_revision, applied.control_revision);
assert_eq!(applied.iteration_objective_revision, 2);
assert_eq!(
applied.operations,
accepted.control.as_ref().unwrap().operations
);
assert_eq!(applied.operations.last().unwrap().receipt, receipt);
assert_eq!(h.command(&command, 3).await.result, result.result);
assert_eq!(
h.state(),
admitted_b,
"receipt delivery cannot reset the consumed budget"
);
assert_pending_rounds_checkpoint(&h, &admitted_b).await;
assert!(matches!(
counted_loop::terminal(&mut h.session, &h.config)
.await
.unwrap(),
counted_loop::TerminalDecision::Finalize
));
let finalizing = h.state();
assert_eq!(finalizing.status, LoopStatus::Finalizing);
assert_eq!(
(
finalizing.iterations_started,
finalizing.iterations_completed,
finalizing.rounds_started
),
(2, 2, 4)
);
assert_pending_rounds_checkpoint(&h, &finalizing).await;
}
}
#[tokio::test]
async fn self_control_pre_first_new_or_pending_objective_can_use_one_future_round() {
for pending_before in [false, true] {
let mut h = Harness::new().await;
let start_command = start("A", 1, 20);
let start_result = h.command(&start_command, 0).await;
assert!(start_result.success, "{}", start_result.result);
if pending_before {
let update_b = h.update(Some("B".into()), None, None);
assert!(h.command(&update_b, 0).await.success);
}
let before = h.state();
assert_eq!(
(
before.iterations_started,
before.iterations_completed,
before.rounds_started
),
(0, 0, 1)
);
assert_eq!(before.control.as_ref().unwrap().applied_revision, 0);
let command = h.update((!pending_before).then(|| "B".into()), None, Some(2));
let result = h.command(&command, 0).await;
assert!(
result.success,
"pending_before={pending_before}: {}",
result.result
);
let accepted = h.state();
let receipt = assert_pending_rounds_receipt(&result, &before, &accepted, 1);
assert_eq!((accepted.max_iterations, accepted.max_total_rounds), (1, 2));
assert_eq!(accepted.max_total_rounds - accepted.rounds_started, 1);
assert_eq!(
accepted
.control
.as_ref()
.unwrap()
.iteration_objective_revision,
0
);
if pending_before {
assert_eq!(
accepted.control.as_ref().unwrap().pending_objective,
before.control.as_ref().unwrap().pending_objective
);
}
assert_pending_rounds_checkpoint(&h, &accepted).await;
h.admit().await;
let admitted_b = h.state();
assert_eq!(
(
admitted_b.iterations_started,
admitted_b.iterations_completed,
admitted_b.rounds_started
),
(1, 0, 2)
);
assert_eq!(
(admitted_b.objective.as_str(), admitted_b.objective_revision),
("B", 2)
);
let applied = admitted_b.control.as_ref().unwrap();
assert!(applied.pending_objective.is_none());
assert_eq!(applied.applied_revision, applied.control_revision);
assert_eq!(applied.iteration_objective_revision, 2);
assert_eq!(
applied.operations,
accepted.control.as_ref().unwrap().operations
);
assert_eq!(applied.operations.last().unwrap().receipt, receipt);
assert_eq!(h.command(&command, 1).await.result, result.result);
assert_eq!(
h.command(&start_command, 1).await.result,
start_result.result
);
assert_eq!(h.state(), admitted_b);
assert_pending_rounds_checkpoint(&h, &admitted_b).await;
assert!(matches!(
counted_loop::terminal(&mut h.session, &h.config)
.await
.unwrap(),
counted_loop::TerminalDecision::Finalize
));
let finalizing = h.state();
assert_eq!(finalizing.status, LoopStatus::Finalizing);
assert_eq!(
(
finalizing.iterations_started,
finalizing.iterations_completed,
finalizing.rounds_started
),
(1, 1, 2)
);
assert_pending_rounds_checkpoint(&h, &finalizing).await;
}
}