//! Lease-bound command service over the transport-neutral configuration core. use core::num::NonZeroU32; use kaptan_config::{ BeginError, CommitError, ConfigManager, ConfigModel, ConfigState, ConfigStore, FieldWriteError, SetError, }; #[cfg(test)] use super::query::{ConfigInfo, DescriptorResponse, EnumOptionResponse, ValueResponse}; use super::{query::ServiceResponse, replay::RememberedResult}; #[cfg(test)] use crate::command::Authorized; use crate::{ command::{ConfigCommand, Principal, Target}, protocol::{ConfigReply, Operation, ProtocolError, ResultKind}, }; #[derive(Clone, Copy)] pub(super) struct Session { pub(super) id: u32, pub(super) revision: u32, } /// Owns only the one GCS transaction lease and bounded protocol state. /// /// This protocol never accepts raw frames or interprets transport authority /// evidence. The composition owner admits a command, binds its principal, and /// may expire the protocol on lease or link loss. The same owner retains the /// one [`ConfigManager`] and one runtime log sink queue. pub struct ConfigProtocol { pub(super) session: Option, /// Sole configuration principal admitted until explicit lease expiry. pub(super) principal: Option, pub(super) next_session_id: Option, pub(super) target: Target, /// Sole most-recent result retained for the admitted stop-and-wait client. pub(super) remembered: Option, pub(super) state: ConfigState, } impl ConfigProtocol { /// Creates a custom configuration-protocol state machine. pub const fn new(target: Target) -> Self { Self { session: None, principal: None, next_session_id: Some(NonZeroU32::MIN), target, remembered: None, state: ConfigState::Active, } } /// Dispatches one composition-admitted configuration command. The /// `principal` isolates transaction ownership; the first accepted `Begin` /// binds this protocol to that exact source until [`Self::expire`]. This /// adapter neither authenticates the source nor decides whether the /// transport may configure the board. The authoritative /// `KAPTAN_CONFIG_RESULT` response remains the complete configuration /// audit; generic human logging is intentionally independent. /// /// An emitter error is a delivery failure, not an operation rollback. A /// mutating command may already be durable and its sole most-recent result /// is remembered before `emit` returns an error. Binding one stop-and-wait /// client prevents another source from evicting that result; expiry clears /// both the binding and replay state. Descriptor and enum-option queries /// each emit exactly one response and are safe to retry by index. pub fn dispatch_from_composition( &mut self, manager: &mut ConfigManager, principal: Principal, command: ConfigCommand, mut emit: F, ) -> Result<(), E> where C: ConfigModel, S: ConfigStore, F: FnMut(ServiceResponse) -> Result<(), E>, { self.refresh(manager); self.dispatch_inner(manager, principal, command, &mut emit) } /// Dispatches a host/SITL test command through the same protocol path. #[cfg(test)] pub fn dispatch( &mut self, manager: &mut ConfigManager, authorized: Authorized, emit: F, ) -> Result<(), E> where C: ConfigModel, S: ConfigStore, F: FnMut(ServiceResponse) -> Result<(), E>, { let (principal, command) = authorized.into_parts(); self.dispatch_from_composition(manager, principal, command, emit) } /// Expires the admitted principal and drops its RAM transaction, if any. pub fn expire(&mut self, manager: &mut ConfigManager) -> bool where C: ConfigModel, S: ConfigStore, { let had_session = self.session.take().is_some(); if had_session { manager.abort(); } let expired = self.principal.take().is_some(); self.remembered = None; expired } fn dispatch_inner( &mut self, manager: &mut ConfigManager, owner: Principal, command: ConfigCommand, emit: &mut F, ) -> Result<(), E> where C: ConfigModel, S: ConfigStore, F: FnMut(ServiceResponse) -> Result<(), E>, { if command.target() != self.target { return emit(ServiceResponse::Result( self.rejected(command, ProtocolError::BadTarget), )); } if self.principal.is_some_and(|principal| principal != owner) { return emit(ServiceResponse::Result( self.rejected(command, ProtocolError::SessionBusy), )); } if let Some(cached) = self.cached(owner, command) { return emit(ServiceResponse::Result(cached)); } let response = match command { ConfigCommand::InfoRequest { request_id, .. } => { ServiceResponse::Info(self.info(manager, request_id)) } ConfigCommand::DescriptorRequest { request_id, index, .. } => self .descriptor_response(manager, request_id, index) .map_or_else( || ServiceResponse::Result(self.rejected(command, ProtocolError::InvalidQuery)), ServiceResponse::Descriptor, ), ConfigCommand::EnumOptionRequest { request_id, descriptor_index, option_index, .. } => self .enum_option_response(manager, request_id, descriptor_index, option_index) .map_or_else( || ServiceResponse::Result(self.rejected(command, ProtocolError::InvalidQuery)), ServiceResponse::EnumOption, ), ConfigCommand::ValueRequest { request_id, session_id, field, source, .. } => self.value_response(manager, owner, request_id, session_id, field, source), ConfigCommand::Begin { .. } => { ServiceResponse::Result(self.begin(manager, owner, command)) } ConfigCommand::Set { .. } | ConfigCommand::Validate { .. } | ConfigCommand::Commit { .. } | ConfigCommand::Abort { .. } => { ServiceResponse::Result(self.in_session(manager, owner, command)) } }; self.refresh(manager); let response = match response { ServiceResponse::Result(reply) => { self.remember(owner, command, reply); ServiceResponse::Result(reply) } response => response, }; emit(response) } fn begin( &mut self, manager: &mut ConfigManager, owner: Principal, command: ConfigCommand, ) -> ConfigReply where C: ConfigModel, S: ConfigStore, { let ConfigCommand::Begin { request_id, .. } = command else { return self.rejected(command, ProtocolError::Internal); }; if let Some(session) = self.session { return self.reply( request_id, session.id, session.revision, Operation::Begin, ResultKind::Rejected, Some(ProtocolError::SessionBusy), ); } let Some(id) = self.allocate_session_id() else { return self.rejected(command, ProtocolError::Internal); }; match manager.begin() { Ok(()) => { self.principal = Some(owner); self.session = Some(Session { id, revision: 0 }); self.reply( request_id, id, 0, Operation::Begin, ResultKind::Accepted, None, ) } Err(BeginError::TransactionActive) => { self.rejected(command, ProtocolError::SessionBusy) } } } fn in_session( &mut self, manager: &mut ConfigManager, owner: Principal, command: ConfigCommand, ) -> ConfigReply where C: ConfigModel, S: ConfigStore, { let Some(session) = self.session else { return self.rejected(command, ProtocolError::InvalidSession); }; let session_id = command_session_id(command); if session.id != session_id || self.principal != Some(owner) { return self.rejected(command, ProtocolError::InvalidSession); } if let Some(expected_revision) = command_expected_revision(command) && expected_revision != session.revision { return self.rejected(command, ProtocolError::StaleRevision); } match command { ConfigCommand::Set { request_id, field, value, .. } => { let Some(next_revision) = session.revision.checked_add(1) else { return self.rejected(command, ProtocolError::Internal); }; match manager.set(field, value) { Ok(_) => { let Some(session) = self.session.as_mut() else { return self.rejected(command, ProtocolError::Internal); }; session.revision = next_revision; let (session_id, revision) = (session.id, session.revision); self.reply( request_id, session_id, revision, Operation::Set, ResultKind::Accepted, None, ) } Err(SetError::NoTransaction) => self.rejected(command, ProtocolError::BadState), Err(SetError::Field(error)) => self.rejected(command, field_error(error)), } } ConfigCommand::Validate { request_id, .. } => match manager.validate() { Ok(()) => self.reply( request_id, session.id, session.revision, Operation::Validate, ResultKind::Accepted, None, ), Err(CommitError::NoTransaction) => self.rejected(command, ProtocolError::BadState), Err(CommitError::InvalidCandidate(violation)) => { self.rejected(command, ProtocolError::Violation(violation)) } Err(CommitError::Store(_)) => self.rejected(command, ProtocolError::Internal), }, ConfigCommand::Commit { request_id, .. } => match manager.commit() { Ok(()) => { self.session = None; self.refresh(manager); self.reply( request_id, 0, session.revision, Operation::Commit, ResultKind::CommittedRestartRequired, None, ) } Err(CommitError::NoTransaction) => self.rejected(command, ProtocolError::BadState), Err(CommitError::InvalidCandidate(violation)) => { self.rejected(command, ProtocolError::Violation(violation)) } Err(CommitError::Store(_)) => self.rejected(command, ProtocolError::StorageFailure), }, ConfigCommand::Abort { request_id, .. } => { manager.abort(); self.session = None; self.refresh(manager); self.reply( request_id, 0, session.revision, Operation::Abort, ResultKind::Accepted, None, ) } ConfigCommand::InfoRequest { .. } | ConfigCommand::DescriptorRequest { .. } | ConfigCommand::EnumOptionRequest { .. } | ConfigCommand::ValueRequest { .. } | ConfigCommand::Begin { .. } => self.rejected(command, ProtocolError::Internal), } } fn allocate_session_id(&mut self) -> Option { let id = self.next_session_id?; self.next_session_id = id.get().checked_add(1).and_then(NonZeroU32::new); Some(id.get()) } fn refresh(&mut self, manager: &ConfigManager) where C: ConfigModel, S: ConfigStore, { self.state = manager.state(); } pub(super) fn reply( &self, request_id: u32, session_id: u32, revision: u32, operation: Operation, result: ResultKind, error: Option, ) -> ConfigReply { ConfigReply { request_id, session_id, revision, operation, result, error, state: self.state, } } pub(super) fn rejected(&self, command: ConfigCommand, error: ProtocolError) -> ConfigReply { self.reply( command.request_id(), command_session_id(command), self.session.map_or(0, |session| session.revision), operation(command), ResultKind::Rejected, Some(error), ) } pub(super) fn rejected_for( &self, request_id: u32, session_id: u32, error: ProtocolError, ) -> ConfigReply { self.reply( request_id, session_id, self.session.map_or(0, |session| session.revision), Operation::Query, ResultKind::Rejected, Some(error), ) } } fn command_session_id(command: ConfigCommand) -> u32 { match command { ConfigCommand::InfoRequest { .. } | ConfigCommand::DescriptorRequest { .. } | ConfigCommand::EnumOptionRequest { .. } | ConfigCommand::Begin { .. } => 0, ConfigCommand::ValueRequest { session_id, .. } => session_id, ConfigCommand::Set { session_id, .. } | ConfigCommand::Validate { session_id, .. } | ConfigCommand::Commit { session_id, .. } | ConfigCommand::Abort { session_id, .. } => session_id, } } fn command_expected_revision(command: ConfigCommand) -> Option { match command { ConfigCommand::Set { expected_revision, .. } | ConfigCommand::Validate { expected_revision, .. } | ConfigCommand::Commit { expected_revision, .. } => Some(expected_revision), ConfigCommand::InfoRequest { .. } | ConfigCommand::DescriptorRequest { .. } | ConfigCommand::EnumOptionRequest { .. } | ConfigCommand::ValueRequest { .. } | ConfigCommand::Begin { .. } | ConfigCommand::Abort { .. } => None, } } const fn operation(command: ConfigCommand) -> Operation { match command { ConfigCommand::InfoRequest { .. } | ConfigCommand::DescriptorRequest { .. } | ConfigCommand::EnumOptionRequest { .. } | ConfigCommand::ValueRequest { .. } => Operation::Query, ConfigCommand::Begin { .. } => Operation::Begin, ConfigCommand::Set { .. } => Operation::Set, ConfigCommand::Validate { .. } => Operation::Validate, ConfigCommand::Commit { .. } => Operation::Commit, ConfigCommand::Abort { .. } => Operation::Abort, } } fn field_error(error: FieldWriteError) -> ProtocolError { match error { FieldWriteError::UnknownField(field) => ProtocolError::UnknownField(field), FieldWriteError::WrongType { field, .. } => ProtocolError::TypeMismatch(field), FieldWriteError::Violation(violation) => ProtocolError::Violation(violation), } } #[cfg(test)] mod tests { use std::{cell::Cell, rc::Rc}; use kaptan_config::{ ConfigManager, ConfigModel, ConfigStore, ConfigValue, ConfigViolation, EnumOption, FieldDescriptor, FieldId, FieldWriteError, ValidationRule, }; use super::*; use crate::{Authorized, ConfigCommand, ProtocolError, ResultKind, Target, ValueSource}; const VALUE: FieldId = FieldId::new(1); const ENUM_OPTIONS: [EnumOption; 3] = [(0, "Zero"), (1, "One"), (2, "Two")]; const DESCRIPTOR: FieldDescriptor = FieldDescriptor { id: VALUE, display_key: "test.value", kind: kaptan_config::ValueKind::U32, minimum: Some(ConfigValue::U32(0)), maximum: Some(ConfigValue::U32(10)), enum_options: &ENUM_OPTIONS, unit: "count", }; #[derive(Clone)] struct Candidate(u32); impl ConfigModel for Candidate { type Active = u32; type Context = (); const SCHEMA_ID: [u8; 16] = *b"CONFIGSERVICE-01"; fn descriptor(index: u16) -> Option<&'static FieldDescriptor> { (index == 0).then_some(&DESCRIPTOR) } fn field_count() -> u16 { 1 } fn field_value(&self, id: FieldId) -> Option { (id == VALUE).then_some(ConfigValue::U32(self.0)) } fn active_field_value(active: &Self::Active, id: FieldId) -> Option { (id == VALUE).then_some(ConfigValue::U32(*active)) } fn set_field(&mut self, id: FieldId, value: ConfigValue) -> Result<(), FieldWriteError> { if id != VALUE { return Err(FieldWriteError::UnknownField(id)); } let ConfigValue::U32(value) = value else { return Err(FieldWriteError::WrongType { field: id, expected: kaptan_config::ValueKind::U32, actual: value.kind(), }); }; if value > 10 { return Err(FieldWriteError::Violation(ConfigViolation::field( id, ValidationRule::AboveMaximum, ))); } self.0 = value; Ok(()) } fn validate(&self, _: &Self::Context) -> Result { Ok(self.0) } } struct Store; impl ConfigStore for Store { type Error = (); fn load(&mut self) -> Result, Self::Error> { Ok(None) } fn persist(&mut self, _: &Candidate) -> Result<(), Self::Error> { Ok(()) } } const fn multi_descriptor(id: u16) -> FieldDescriptor { FieldDescriptor { id: FieldId::new(id), display_key: "test.multi", kind: kaptan_config::ValueKind::U32, minimum: None, maximum: None, enum_options: &[], unit: "count", } } const MULTI_DESCRIPTORS: [FieldDescriptor; 6] = [ multi_descriptor(1), multi_descriptor(2), multi_descriptor(3), multi_descriptor(4), multi_descriptor(5), multi_descriptor(6), ]; #[derive(Clone)] struct MultiCandidate([u32; 6]); impl ConfigModel for MultiCandidate { type Active = [u32; 6]; type Context = (); const SCHEMA_ID: [u8; 16] = *b"CONFIGSERVICE-02"; fn descriptor(index: u16) -> Option<&'static FieldDescriptor> { MULTI_DESCRIPTORS.get(usize::from(index)) } fn field_count() -> u16 { MULTI_DESCRIPTORS.len() as u16 } fn field_value(&self, id: FieldId) -> Option { id.get() .checked_sub(1) .and_then(|index| self.0.get(usize::from(index))) .copied() .map(ConfigValue::U32) } fn active_field_value(active: &Self::Active, id: FieldId) -> Option { id.get() .checked_sub(1) .and_then(|index| active.get(usize::from(index))) .copied() .map(ConfigValue::U32) } fn set_field(&mut self, id: FieldId, value: ConfigValue) -> Result<(), FieldWriteError> { let Some(index) = id.get().checked_sub(1).map(usize::from) else { return Err(FieldWriteError::UnknownField(id)); }; let Some(field) = self.0.get_mut(index) else { return Err(FieldWriteError::UnknownField(id)); }; let ConfigValue::U32(value) = value else { return Err(FieldWriteError::WrongType { field: id, expected: kaptan_config::ValueKind::U32, actual: value.kind(), }); }; *field = value; Ok(()) } fn validate(&self, _: &Self::Context) -> Result { Ok(self.0) } } struct MultiStore; impl ConfigStore for MultiStore { type Error = (); fn load(&mut self) -> Result, Self::Error> { Ok(None) } fn persist(&mut self, _: &MultiCandidate) -> Result<(), Self::Error> { Ok(()) } } struct CountingStore(Rc>); impl ConfigStore for CountingStore { type Error = (); fn load(&mut self) -> Result, Self::Error> { Ok(None) } fn persist(&mut self, _: &Candidate) -> Result<(), Self::Error> { self.0.set(self.0.get() + 1); Ok(()) } } /// Test-only composition root. Production code must retain these two /// objects separately; this wrapper keeps protocol examples compact while /// exercising the same externally-owned-manager dispatch API. struct TestService { manager: ConfigManager, protocol: ConfigProtocol, } impl TestService { fn new(manager: ConfigManager, target: Target) -> Self { Self { manager, protocol: ConfigProtocol::new(target), } } fn dispatch_all( &mut self, command: Authorized, ) -> std::vec::Vec { let mut responses = std::vec::Vec::new(); self.protocol .dispatch(&mut self.manager, command, |response| { responses.push(response); Ok::<(), core::convert::Infallible>(()) }) .unwrap(); responses } fn dispatch(&mut self, command: Authorized) -> ServiceResponse { let mut responses = self.dispatch_all(command); assert_eq!(responses.len(), 1, "test expected one service response"); responses.pop().unwrap() } } fn result(response: ServiceResponse) -> ConfigReply { match response { ServiceResponse::Result(reply) => reply, ServiceResponse::Info(_) | ServiceResponse::Descriptor(_) | ServiceResponse::EnumOption(_) | ServiceResponse::Value(_) => { panic!("test expected a result response") } } } #[test] fn retries_are_idempotent_and_wrong_owner_cannot_mutate() { let manager = ConfigManager::new(Candidate(2), (), Store).unwrap(); let mut service = TestService::new(manager, Target::new(1, 1)); let begin = ConfigCommand::Begin { request_id: 1, target: Target::new(1, 1), }; let started = result(service.dispatch(Authorized::host(7, begin))); assert_eq!(started.result, ResultKind::Accepted); let set = ConfigCommand::Set { request_id: 2, session_id: started.session_id, expected_revision: 0, field: VALUE, value: ConfigValue::U32(3), target: Target::new(1, 1), }; let accepted = result(service.dispatch(Authorized::host(7, set))); assert_eq!(accepted.result, ResultKind::Accepted); assert_eq!(result(service.dispatch(Authorized::host(7, set))), accepted); let conflict = result(service.dispatch(Authorized::host( 7, ConfigCommand::Set { request_id: 2, session_id: started.session_id, expected_revision: 0, field: VALUE, value: ConfigValue::U32(4), target: Target::new(1, 1), }, ))); assert_eq!(conflict.error, Some(ProtocolError::RequestConflict)); assert_eq!( service.manager.transaction_value(VALUE), Some(ConfigValue::U32(3)) ); let foreign = result(service.dispatch(Authorized::host( 8, ConfigCommand::Set { request_id: 3, session_id: started.session_id, expected_revision: 1, field: VALUE, value: ConfigValue::U32(3), target: Target::new(1, 1), }, ))); assert_eq!(foreign.error, Some(ProtocolError::SessionBusy)); } #[test] fn only_the_most_recent_stop_and_wait_result_is_replayed() { let manager = ConfigManager::new(Candidate(2), (), Store).unwrap(); let mut service = TestService::new(manager, Target::new(1, 1)); let begin = ConfigCommand::Begin { request_id: 1, target: Target::new(1, 1), }; let started = result(service.dispatch(Authorized::host(7, begin))); let set = ConfigCommand::Set { request_id: 2, session_id: started.session_id, expected_revision: 0, field: VALUE, value: ConfigValue::U32(3), target: Target::new(1, 1), }; assert_eq!( result(service.dispatch(Authorized::host(7, set))).result, ResultKind::Accepted ); // Advancing a stop-and-wait client replaces the preceding result. // Reusing that older command is handled from current state instead of // retaining an unbounded request history. assert_eq!( result(service.dispatch(Authorized::host(7, begin))).error, Some(ProtocolError::SessionBusy) ); } #[test] fn busy_owner_can_identify_and_abort_its_abandoned_session() { let manager = ConfigManager::new(Candidate(2), (), Store).unwrap(); let mut service = TestService::new(manager, Target::new(1, 1)); let started = result(service.dispatch(Authorized::host( 7, ConfigCommand::Begin { request_id: 1, target: Target::new(1, 1), }, ))); let owned_busy = result(service.dispatch(Authorized::host( 7, ConfigCommand::Begin { request_id: 2, target: Target::new(1, 1), }, ))); assert_eq!(owned_busy.error, Some(ProtocolError::SessionBusy)); assert_eq!(owned_busy.session_id, started.session_id); let foreign_busy = result(service.dispatch(Authorized::host( 8, ConfigCommand::Begin { request_id: 3, target: Target::new(1, 1), }, ))); assert_eq!(foreign_busy.error, Some(ProtocolError::SessionBusy)); assert_eq!(foreign_busy.session_id, 0); let aborted = result(service.dispatch(Authorized::host( 7, ConfigCommand::Abort { request_id: 4, session_id: owned_busy.session_id, target: Target::new(1, 1), }, ))); assert_eq!(aborted.result, ResultKind::Accepted); } #[test] fn exact_validation_issue_reaches_result() { let manager = ConfigManager::new(Candidate(2), (), Store).unwrap(); let mut service = TestService::new(manager, Target::new(1, 1)); let begin = result(service.dispatch(Authorized::host( 7, ConfigCommand::Begin { request_id: 1, target: Target::new(1, 1), }, ))); let rejected = result(service.dispatch(Authorized::host( 7, ConfigCommand::Set { request_id: 2, session_id: begin.session_id, expected_revision: 0, field: VALUE, value: ConfigValue::U32(11), target: Target::new(1, 1), }, ))); assert!( matches!(rejected.error, Some(ProtocolError::Violation(issue)) if issue.rule() == ValidationRule::AboveMaximum) ); } #[test] fn discovery_and_value_reads_are_service_responses() { let manager = ConfigManager::new(Candidate(2), (), Store).unwrap(); let mut service = TestService::new(manager, Target::new(1, 1)); let descriptors = service.dispatch(Authorized::host( 7, ConfigCommand::DescriptorRequest { request_id: 1, index: 0, target: Target::new(1, 1), }, )); assert!(matches!( descriptors, ServiceResponse::Descriptor(DescriptorResponse { index: 0, descriptor: DESCRIPTOR, .. }) )); let value = service.dispatch(Authorized::host( 7, ConfigCommand::ValueRequest { request_id: 2, session_id: 0, field: VALUE, source: ValueSource::Active, target: Target::new(1, 1), }, )); assert!(matches!( value, ServiceResponse::Value(ValueResponse { value: ConfigValue::U32(2), .. }) )); let info = service.dispatch(Authorized::host( 7, ConfigCommand::InfoRequest { request_id: 3, target: Target::new(1, 1), }, )); assert!(matches!( info, ServiceResponse::Info(ConfigInfo { request_id: 3, field_count: 1, .. }) )); } #[test] fn descriptors_advertise_the_composition_factory_not_a_schema_fallback() { let manager = ConfigManager::new(Candidate(7), (), Store).unwrap(); let mut service = TestService::new(manager, Target::new(1, 1)); let response = service.dispatch(Authorized::host( 7, ConfigCommand::DescriptorRequest { request_id: 1, index: 0, target: Target::new(1, 1), }, )); let ServiceResponse::Descriptor(response) = response else { panic!("expected descriptor response"); }; assert_eq!(response.default, ConfigValue::U32(7)); } #[test] fn enum_options_are_explicitly_indexed_and_retry_the_rejected_request() { let mut manager = ConfigManager::new(Candidate(2), (), Store).unwrap(); let mut protocol = ConfigProtocol::new(Target::new(1, 1)); let command = ConfigCommand::EnumOptionRequest { request_id: 10, descriptor_index: 0, option_index: 0, target: Target::new(1, 1), }; let mut rejected = None; assert_eq!( protocol.dispatch(&mut manager, Authorized::host(7, command), |response| { rejected = Some(response); Err(()) }), Err(()) ); assert!(matches!( rejected, Some(ServiceResponse::EnumOption(EnumOptionResponse { option_index: 0, option: (0, "Zero"), .. })) )); let mut retry = std::vec::Vec::new(); protocol .dispatch(&mut manager, Authorized::host(7, command), |response| { retry.push(response); Ok::<(), core::convert::Infallible>(()) }) .unwrap(); assert_eq!(retry.len(), 1, "one request identifies one option"); assert!(matches!( retry[0], ServiceResponse::EnumOption(EnumOptionResponse { option_index: 0, option: (0, "Zero"), .. }) )); let next = ConfigCommand::EnumOptionRequest { request_id: 11, descriptor_index: 0, option_index: 1, target: Target::new(1, 1), }; let mut second = None; protocol .dispatch(&mut manager, Authorized::host(7, next), |response| { second = Some(response); Ok::<(), core::convert::Infallible>(()) }) .unwrap(); assert!(matches!( second, Some(ServiceResponse::EnumOption(EnumOptionResponse { option_index: 1, option: (1, "One"), .. })) )); } #[test] fn descriptor_queries_are_bounded_ordered_and_stop_at_the_schema_end() { let mut manager = ConfigManager::new(MultiCandidate([0; 6]), (), MultiStore).unwrap(); let mut protocol = ConfigProtocol::new(Target::new(1, 1)); let mut indices = std::vec::Vec::new(); protocol .dispatch( &mut manager, Authorized::host( 7, ConfigCommand::DescriptorRequest { request_id: 1, index: 1, target: Target::new(1, 1), }, ), |response| { let ServiceResponse::Descriptor(response) = response else { panic!("expected a descriptor response"); }; indices.push(response.index); Ok::<(), core::convert::Infallible>(()) }, ) .unwrap(); assert_eq!(indices, [1]); indices.clear(); protocol .dispatch( &mut manager, Authorized::host( 7, ConfigCommand::DescriptorRequest { request_id: 2, index: 4, target: Target::new(1, 1), }, ), |response| { let ServiceResponse::Descriptor(response) = response else { panic!("expected a descriptor response"); }; indices.push(response.index); Ok::<(), core::convert::Infallible>(()) }, ) .unwrap(); assert_eq!(indices, [4]); } #[test] fn descriptor_request_retries_the_same_index_after_delivery_fails() { let mut manager = ConfigManager::new(MultiCandidate([0; 6]), (), MultiStore).unwrap(); let mut protocol = ConfigProtocol::new(Target::new(1, 1)); let command = ConfigCommand::DescriptorRequest { request_id: 1, index: 0, target: Target::new(1, 1), }; let mut indices = std::vec::Vec::new(); let failure = protocol.dispatch(&mut manager, Authorized::host(7, command), |response| { let ServiceResponse::Descriptor(response) = response else { panic!("expected a descriptor response"); }; indices.push(response.index); Err(()) }); assert_eq!(failure, Err(())); assert_eq!(indices, [0]); indices.clear(); protocol .dispatch(&mut manager, Authorized::host(7, command), |response| { let ServiceResponse::Descriptor(response) = response else { panic!("expected a descriptor response"); }; indices.push(response.index); Ok::<(), core::convert::Infallible>(()) }) .unwrap(); assert_eq!(indices, [0]); indices.clear(); protocol .dispatch( &mut manager, Authorized::host( 7, ConfigCommand::DescriptorRequest { request_id: 2, index: 1, target: Target::new(1, 1), }, ), |response| { let ServiceResponse::Descriptor(response) = response else { panic!("expected a descriptor response"); }; indices.push(response.index); Ok::<(), core::convert::Infallible>(()) }, ) .unwrap(); assert_eq!(indices, [1]); } #[test] fn invalid_descriptor_index_is_a_structured_query_error() { let manager = ConfigManager::new(Candidate(2), (), Store).unwrap(); let mut service = TestService::new(manager, Target::new(1, 1)); let reply = result(service.dispatch(Authorized::host( 7, ConfigCommand::DescriptorRequest { request_id: 1, index: 1, target: Target::new(1, 1), }, ))); assert_eq!(reply.error, Some(ProtocolError::InvalidQuery)); } #[test] fn a_lost_commit_response_does_not_repeat_flash_persistence() { let persists = Rc::new(Cell::new(0)); let store = CountingStore(Rc::clone(&persists)); let mut manager = ConfigManager::new(Candidate(2), (), store).unwrap(); let mut protocol = ConfigProtocol::new(Target::new(1, 1)); let mut begin_reply = None; protocol .dispatch( &mut manager, Authorized::host( 7, ConfigCommand::Begin { request_id: 1, target: Target::new(1, 1), }, ), |response| { begin_reply = Some(result(response)); Ok::<(), core::convert::Infallible>(()) }, ) .unwrap(); let begin_reply = begin_reply.unwrap(); let commit = ConfigCommand::Commit { request_id: 2, session_id: begin_reply.session_id, expected_revision: 0, target: Target::new(1, 1), }; assert_eq!( protocol.dispatch(&mut manager, Authorized::host(7, commit), |_| Err(())), Err(()) ); assert_eq!(persists.get(), 1); let foreign = ConfigCommand::Begin { request_id: 3, target: Target::new(1, 1), }; let mut foreign_reply = None; protocol .dispatch(&mut manager, Authorized::host(8, foreign), |response| { foreign_reply = Some(result(response)); Ok::<(), core::convert::Infallible>(()) }) .unwrap(); assert_eq!( foreign_reply.unwrap().error, Some(ProtocolError::SessionBusy) ); let mut retry_reply = None; protocol .dispatch(&mut manager, Authorized::host(7, commit), |response| { retry_reply = Some(result(response)); Ok::<(), core::convert::Infallible>(()) }) .unwrap(); assert_eq!(persists.get(), 1); assert_eq!( retry_reply.unwrap().result, ResultKind::CommittedRestartRequired ); // Expiry releases the principal even though COMMIT already closed its // RAM transaction, allowing the next direct-link configurator in. assert!(protocol.expire(&mut manager)); let mut next_owner = None; protocol .dispatch(&mut manager, Authorized::host(8, foreign), |response| { next_owner = Some(result(response)); Ok::<(), core::convert::Infallible>(()) }) .unwrap(); assert_eq!(next_owner.unwrap().result, ResultKind::Accepted); } #[test] fn configuration_result_remains_the_structured_failure_authority() { let manager = ConfigManager::new(Candidate(2), (), Store).unwrap(); let mut service = TestService::new(manager, Target::new(1, 1)); let begin = result(service.dispatch(Authorized::host( 7, ConfigCommand::Begin { request_id: 1, target: Target::new(1, 1), }, ))); let rejected = result(service.dispatch(Authorized::host( 7, ConfigCommand::Set { request_id: 2, session_id: begin.session_id, expected_revision: 0, field: FieldId::new(2), value: ConfigValue::U32(3), target: Target::new(1, 1), }, ))); assert_eq!( rejected.error, Some(ProtocolError::UnknownField(FieldId::new(2))) ); assert_eq!(rejected.request_id, 2); assert_eq!(rejected.primary_field(), FieldId::new(2).get()); } }