#[test]
fn test_vm_simple_send_recv() {
let local_types = simple_send_recv_types();
let global = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
let image = CodeImage::from_local_types(&local_types, &global);
let mut machine = ProtocolMachine::new(ProtocolMachineConfig::default());
let _sid = machine.load_choreography(&image).unwrap();
let handler = PassthroughHandler;
machine.run(&handler, 100).unwrap();
assert!(machine.coroutines.iter().all(|c| c.is_terminal()));
}
#[test]
fn test_canonical_dispatch_uses_send_decision_and_handle_recv() {
let local_types = simple_send_recv_types();
let global = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
let image = CodeImage::from_local_types(&local_types, &global);
let mut machine = ProtocolMachine::new(ProtocolMachineConfig::default());
machine.load_choreography(&image).expect("load choreography");
let handler = CallbackAuditHandler::default();
machine.run(&handler, 100).expect("run should succeed");
assert!(handler.send_decision_calls.load(Ordering::Relaxed) > 0);
assert!(handler.handle_recv_calls.load(Ordering::Relaxed) > 0);
assert_eq!(handler.handle_send_calls.load(Ordering::Relaxed), 0);
}
#[test]
fn test_canonical_dispatch_does_not_call_handle_choose() {
let image = choice_image_with_explicit_offer_choose();
let mut machine = ProtocolMachine::new(ProtocolMachineConfig::default());
machine.load_choreography(&image).expect("load choreography");
let handler = CallbackAuditHandler::default();
machine.run(&handler, 100).expect("run should succeed");
assert_eq!(handler.handle_choose_calls.load(Ordering::Relaxed), 0);
assert!(handler.send_decision_calls.load(Ordering::Relaxed) > 0);
assert!(handler.handle_recv_calls.load(Ordering::Relaxed) > 0);
}
#[test]
fn test_payload_validation_structural_rejects_annotated_type_mismatch() {
let local_types = typed_send_recv_types(Some(ValType::Nat));
let global = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
let image = CodeImage::from_local_types(&local_types, &global);
let mut machine = ProtocolMachine::new(ProtocolMachineConfig {
payload_validation_mode: PayloadValidationMode::Structural,
..ProtocolMachineConfig::default()
});
machine.load_choreography(&image).expect("load choreography");
let err = machine
.run(&AdversarialBoolSendHandler, 100)
.expect_err("annotated payload type mismatch should fault");
match err {
ProtocolMachineError::Fault {
fault:
Fault::TypeViolation {
expected, actual, ..
},
..
} => {
assert_eq!(expected, ValType::Nat);
assert_eq!(actual, ValType::Bool);
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn test_payload_validation_off_allows_annotated_type_mismatch_for_compatibility() {
let local_types = typed_send_recv_types(Some(ValType::Nat));
let global = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
let image = CodeImage::from_local_types(&local_types, &global);
let mut machine = ProtocolMachine::new(ProtocolMachineConfig {
payload_validation_mode: PayloadValidationMode::Off,
..ProtocolMachineConfig::default()
});
machine.load_choreography(&image).expect("load choreography");
machine.run(&AdversarialBoolSendHandler, 100)
.expect("off mode preserves compatibility behavior");
}
#[test]
fn test_payload_validation_strict_schema_requires_annotations_for_send_recv() {
let local_types = typed_send_recv_types(None);
let global = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
let image = CodeImage::from_local_types(&local_types, &global);
let mut machine = ProtocolMachine::new(ProtocolMachineConfig {
payload_validation_mode: PayloadValidationMode::StrictSchema,
..ProtocolMachineConfig::default()
});
machine.load_choreography(&image).expect("load choreography");
let err = machine
.run(&PassthroughHandler, 100)
.expect_err("strict schema mode should require explicit payload annotations");
match err {
ProtocolMachineError::Fault {
fault: Fault::TypeViolation { message, .. },
..
} => {
assert!(message.contains("requires explicit ValType annotation"));
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn test_payload_validation_size_bound_rejects_oversized_payloads() {
let local_types = typed_send_recv_types(None);
let global = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
let image = CodeImage::from_local_types(&local_types, &global);
let mut machine = ProtocolMachine::new(ProtocolMachineConfig {
max_payload_bytes: 16,
..ProtocolMachineConfig::default()
});
machine.load_choreography(&image).expect("load choreography");
let err = machine
.run(&OversizedPayloadSendHandler, 100)
.expect_err("payload above max_payload_bytes should fault");
match err {
ProtocolMachineError::Fault {
fault: Fault::TypeViolation { message, .. },
..
} => {
assert!(message.contains("exceeds max_payload_bytes"));
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn test_step_round_advances_at_most_one_coroutine_when_concurrency_gt_one() {
let mut local_types = BTreeMap::new();
local_types.insert("A".to_string(), LocalTypeR::End);
local_types.insert("B".to_string(), LocalTypeR::End);
let mut programs = BTreeMap::new();
programs.insert("A".to_string(), vec![Instr::Halt]);
programs.insert("B".to_string(), vec![Instr::Halt]);
let image = CodeImage {
programs,
global_type: GlobalType::End,
local_types,
};
let mut machine = ProtocolMachine::new(ProtocolMachineConfig::default());
machine.load_choreography(&image).expect("load choreography");
let handler = PassthroughHandler;
assert_eq!(machine.scheduler_step_count(), 0);
let first = machine.step_round(&handler, 8).expect("first round");
assert!(matches!(first, StepResult::Continue));
assert_eq!(machine.scheduler_step_count(), 1);
assert_eq!(
machine.coroutines
.iter()
.filter(|c| matches!(c.status, CoroStatus::Done))
.count(),
1
);
let second = machine.step_round(&handler, 8).expect("second round");
assert!(matches!(second, StepResult::AllDone));
assert_eq!(machine.scheduler_step_count(), 2);
}
#[test]
fn test_step_round_with_no_eligible_coroutines_does_not_increment_step_count() {
let mut local_types = BTreeMap::new();
local_types.insert("A".to_string(), LocalTypeR::End);
let mut programs = BTreeMap::new();
programs.insert("A".to_string(), vec![Instr::Halt]);
let image = CodeImage {
programs,
global_type: GlobalType::End,
local_types,
};
let mut machine = ProtocolMachine::new(ProtocolMachineConfig::default());
machine.load_choreography(&image).expect("load choreography");
machine.pause_role("A");
let handler = PassthroughHandler;
assert_eq!(machine.scheduler_step_count(), 0);
let result = machine.step_round(&handler, 1).expect("step round");
assert!(matches!(result, StepResult::Stuck));
assert_eq!(machine.scheduler_step_count(), 0);
}
#[test]
fn test_resume_role_rebuilds_ready_eligibility_and_allows_progress() {
let mut local_types = BTreeMap::new();
local_types.insert("A".to_string(), LocalTypeR::End);
let mut programs = BTreeMap::new();
programs.insert("A".to_string(), vec![Instr::Halt]);
let image = CodeImage {
programs,
global_type: GlobalType::End,
local_types,
};
let mut machine = ProtocolMachine::new(ProtocolMachineConfig::default());
machine.load_choreography(&image).expect("load choreography");
machine.pause_role("A");
let handler = PassthroughHandler;
let stuck = machine.step_round(&handler, 1).expect("paused step round");
assert!(matches!(stuck, StepResult::Stuck));
assert_eq!(machine.scheduler_step_count(), 0);
machine.resume_role("A");
let resumed = machine.step_round(&handler, 1).expect("resumed step round");
assert!(matches!(resumed, StepResult::AllDone));
assert_eq!(machine.scheduler_step_count(), 1);
}
#[test]
fn test_yield_advances_pc_and_sets_spawn_wait_blocked_status() {
let mut local_types = BTreeMap::new();
local_types.insert("A".to_string(), LocalTypeR::End);
let mut programs = BTreeMap::new();
programs.insert("A".to_string(), vec![Instr::Yield, Instr::Halt]);
let image = CodeImage {
programs,
global_type: GlobalType::End,
local_types,
};
let mut machine = ProtocolMachine::new(ProtocolMachineConfig::default());
machine.load_choreography(&image).expect("load choreography");
let handler = PassthroughHandler;
let first = machine.step_round(&handler, 1).expect("yield step");
assert!(matches!(first, StepResult::Continue));
let coro = machine.coroutine(0).expect("coroutine exists");
assert_eq!(coro.pc, 1);
assert!(matches!(
coro.status,
CoroStatus::Blocked(BlockReason::Spawn)
));
let second = machine.step_round(&handler, 1).expect("halt step");
assert!(matches!(second, StepResult::AllDone));
}
#[test]
fn test_run_status_reports_all_done_stuck_and_max_rounds_exceeded() {
let all_done_image = CodeImage {
programs: {
let mut m = BTreeMap::new();
m.insert("A".to_string(), vec![Instr::Halt]);
m
},
global_type: GlobalType::End,
local_types: {
let mut m = BTreeMap::new();
m.insert("A".to_string(), LocalTypeR::End);
m
},
};
let mut all_done_vm = ProtocolMachine::new(ProtocolMachineConfig::default());
all_done_vm
.load_choreography(&all_done_image)
.expect("load all-done choreography");
let all_done = all_done_vm
.run(&PassthroughHandler, 8)
.expect("all-done run must succeed");
assert_eq!(all_done, RunStatus::AllDone);
let stuck_image = CodeImage {
programs: {
let mut m = BTreeMap::new();
m.insert("A".to_string(), vec![Instr::Receive { chan: 0, dst: 1 }]);
m
},
global_type: GlobalType::End,
local_types: {
let mut m = BTreeMap::new();
m.insert(
"A".to_string(),
LocalTypeR::recv("B", Label::new("m"), LocalTypeR::End),
);
m
},
};
let mut stuck_vm = ProtocolMachine::new(ProtocolMachineConfig::default());
stuck_vm
.load_choreography(&stuck_image)
.expect("load stuck choreography");
let stuck = stuck_vm
.run(&PassthroughHandler, 8)
.expect("stuck run must return status, not fault");
assert_eq!(stuck, RunStatus::Stuck);
let max_image = CodeImage {
programs: {
let mut m = BTreeMap::new();
m.insert("A".to_string(), vec![Instr::Jump { target: 0 }]);
m
},
global_type: GlobalType::End,
local_types: {
let mut m = BTreeMap::new();
m.insert("A".to_string(), LocalTypeR::End);
m
},
};
let mut max_vm = ProtocolMachine::new(ProtocolMachineConfig::default());
max_vm
.load_choreography(&max_image)
.expect("load loop choreography");
let exhausted = max_vm
.run(&PassthroughHandler, 8)
.expect("bounded loop run must return status");
assert_eq!(exhausted, RunStatus::MaxRoundsExceeded);
}
fn open_test_image(open_instr: Instr) -> CodeImage {
let mut local_types = BTreeMap::new();
local_types.insert("A".to_string(), LocalTypeR::End);
let mut programs = BTreeMap::new();
programs.insert("A".to_string(), vec![open_instr, Instr::Halt]);
CodeImage {
programs,
global_type: GlobalType::End,
local_types,
}
}
fn open_buffer_pressure_image() -> CodeImage {
let reachable_handlers = vec![(("A".to_string(), "B".to_string()), "hAB".to_string())];
let send_twice = LocalTypeR::send(
"B",
Label::new("m"),
LocalTypeR::send("B", Label::new("m"), LocalTypeR::End),
);
let recv_twice = LocalTypeR::recv(
"A",
Label::new("m"),
LocalTypeR::recv("A", Label::new("m"), LocalTypeR::End),
);
let mut local_types = BTreeMap::new();
local_types.insert("A".to_string(), LocalTypeR::End);
let mut programs = BTreeMap::new();
programs.insert(
"A".to_string(),
vec![
Instr::Open {
roles: vec!["A".to_string(), "B".to_string()],
local_types: vec![("A".to_string(), send_twice), ("B".to_string(), recv_twice)],
handlers: reachable_handlers,
dsts: vec![("A".to_string(), 1), ("B".to_string(), 2)],
},
Instr::Set {
dst: 3,
val: crate::instr::ImmValue::Nat(7),
},
Instr::Send { chan: 1, val: 3 },
Instr::Send { chan: 1, val: 3 },
Instr::Halt,
],
);
CodeImage {
programs,
global_type: GlobalType::End,
local_types,
}
}
#[test]
fn test_open_faults_on_arity_mismatch() {
let image = open_test_image(Instr::Open {
roles: vec!["A".to_string()],
local_types: vec![("A".to_string(), LocalTypeR::End)],
handlers: vec![(("A".to_string(), "A".to_string()), "h".to_string())],
dsts: vec![("B".to_string(), 0)],
});
let mut machine = ProtocolMachine::new(ProtocolMachineConfig::default());
machine.load_choreography(&image).expect("load choreography");
let handler = PassthroughHandler;
let err = machine
.step_round(&handler, 1)
.expect_err("expected open arity fault");
match err {
ProtocolMachineError::Fault {
fault: Fault::Close { message },
..
} => assert_eq!(message, "open arity mismatch"),
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn test_open_faults_when_handler_coverage_is_incomplete() {
let image = open_test_image(Instr::Open {
roles: vec!["A".to_string(), "B".to_string()],
local_types: vec![
(
"A".to_string(),
LocalTypeR::send("B", Label::new("go"), LocalTypeR::End),
),
(
"B".to_string(),
LocalTypeR::recv("A", Label::new("go"), LocalTypeR::End),
),
],
handlers: vec![],
dsts: vec![("A".to_string(), 0), ("B".to_string(), 1)],
});
let mut machine = ProtocolMachine::new(ProtocolMachineConfig::default());
machine.load_choreography(&image).expect("load choreography");
let handler = PassthroughHandler;
let err = machine
.step_round(&handler, 1)
.expect_err("expected handler coverage fault");
match err {
ProtocolMachineError::Fault {
fault: Fault::Speculation { message },
..
} => assert_eq!(message, "handler bindings missing"),
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn test_open_initializes_local_types_handlers_and_endpoints() {
let reachable_handlers = vec![
(("A".to_string(), "B".to_string()), "hAB".to_string()),
];
let image = open_test_image(Instr::Open {
roles: vec!["A".to_string(), "B".to_string()],
local_types: vec![
(
"A".to_string(),
LocalTypeR::send("B", Label::new("go"), LocalTypeR::End),
),
(
"B".to_string(),
LocalTypeR::recv("A", Label::new("go"), LocalTypeR::End),
),
],
handlers: reachable_handlers.clone(),
dsts: vec![("A".to_string(), 0), ("B".to_string(), 1)],
});
let mut machine = ProtocolMachine::new(ProtocolMachineConfig::default());
machine.load_choreography(&image).expect("load choreography");
let handler = PassthroughHandler;
let result = machine.step_round(&handler, 1).expect("open step");
assert!(matches!(result, StepResult::Continue));
let sid = match machine
.trace()
.iter()
.rev()
.find(|event| matches!(event, ObsEvent::Opened { .. }))
.expect("opened event emitted")
{
ObsEvent::Opened { session, .. } => *session,
_ => unreachable!(),
};
assert!(
machine.trace().iter().any(|event| matches!(
event,
ObsEvent::Opened { roles, .. }
if roles == &vec!["A".to_string(), "B".to_string()]
)),
"opened event must preserve canonical role strings at the trace boundary"
);
let session = machine.sessions().get(sid).expect("opened session exists");
assert_eq!(session.local_types.len(), 2);
for ((sender, receiver), handler_id) in reachable_handlers {
let edge = Edge::new(sid, sender, receiver);
assert_eq!(session.edge_handlers.get(&edge), Some(&handler_id));
}
assert_eq!(machine.role_symbol_count(), 2);
assert_eq!(machine.label_symbol_count(), 0);
assert_eq!(machine.handler_symbol_count(), 2);
assert_eq!(machine.edge_symbol_count(), 1);
let coro = machine.coroutine(0).expect("coroutine exists");
assert!(matches!(coro.regs[0], Value::Endpoint(_)));
assert!(matches!(coro.regs[1], Value::Endpoint(_)));
}
#[test]
fn test_runtime_open_uses_configured_buffer_capacity_for_new_sessions() {
let image = open_buffer_pressure_image();
let cfg = ProtocolMachineConfig {
buffer_config: BufferConfig {
mode: crate::buffer::BufferMode::Fifo,
initial_capacity: 1,
policy: crate::buffer::BackpressurePolicy::Error,
},
..ProtocolMachineConfig::default()
};
let mut machine = ProtocolMachine::new(cfg);
machine.load_choreography(&image).expect("load choreography");
let err = machine
.run(&PassthroughHandler, 32)
.expect_err("second open-session send must fault with capacity=1,error policy");
match err {
ProtocolMachineError::Fault {
fault: Fault::BufferFull { .. },
..
} => {}
other => panic!("expected BufferFull fault, got {other:?}"),
}
}
#[test]
fn test_runtime_open_allocates_session_id_after_loaded_session_without_collision() {
let image = open_test_image(Instr::Open {
roles: vec!["A".to_string(), "B".to_string()],
local_types: vec![
("A".to_string(), LocalTypeR::End),
("B".to_string(), LocalTypeR::End),
],
handlers: vec![],
dsts: vec![("A".to_string(), 0), ("B".to_string(), 1)],
});
let mut machine = ProtocolMachine::new(ProtocolMachineConfig::default());
let sid0 = machine.load_choreography(&image).expect("load choreography");
assert_eq!(sid0, 0);
let handler = PassthroughHandler;
let result = machine.step_round(&handler, 1).expect("open step");
assert!(matches!(result, StepResult::Continue));
let opened: Vec<SessionId> = machine
.trace()
.iter()
.filter_map(|event| match event {
ObsEvent::Opened { session, .. } => Some(*session),
_ => None,
})
.collect();
assert_eq!(opened, vec![0, 1], "expected monotonic opened sessions");
assert!(
machine.sessions().get(0).is_some(),
"bootstrap session must remain"
);
assert!(
machine.sessions().get(1).is_some(),
"runtime-open session must exist"
);
assert_eq!(machine.next_session_id(), 2);
}
#[test]
fn test_monitor_precheck_rejects_duplicate_choose_labels() {
let image = open_test_image(Instr::Choose {
chan: 0,
table: vec![("L".to_string(), 0), ("L".to_string(), 1)],
});
let mut machine = ProtocolMachine::new(ProtocolMachineConfig::default());
machine.load_choreography(&image).expect("load choreography");
let handler = PassthroughHandler;
let err = machine
.step_round(&handler, 1)
.expect_err("duplicate labels must fail monitor precheck");
match err {
ProtocolMachineError::Fault {
fault: Fault::Speculation { message },
..
} => assert!(message.contains("duplicate choose labels")),
other => panic!("unexpected error: {other:?}"),
}
}