use std::error::Error;
use liminal::durability::bridge::block_on;
use liminal_protocol::wire::{
AttachAttemptToken, ClientRequest, CommonStaleAuthorityEnvelope, ConnectionIncarnation,
CredentialAttachRequest, Generation, ObserverBackpressure, ServerValue, StaleAuthority,
};
use crate::server::participant::{ParticipantConnectionConversations, ParticipantDispatch};
use super::ProductionParticipantHandler;
use super::tests::{
decode_server_value, dispatch_outcome, open_disk_store_for_tests, test_participant_config,
};
use super::tests_receipts::{GEN_ONE, attach, attach_request, detach, enroll, generation};
use super::tests_w1b_pending_died_restart::pending_restart_fixture;
fn client_outcome(outcome: &ParticipantDispatch) -> String {
match outcome {
ParticipantDispatch::NotParticipant => "NO FRAME (frame disowned)".to_string(),
ParticipantDispatch::Respond(_) => "a response frame, connection open".to_string(),
ParticipantDispatch::RespondThenClose(_) => {
"a BEST-EFFORT frame, then close (may still be silence under load)".to_string()
}
ParticipantDispatch::Fatal(error) => {
format!("NO FRAME AT ALL — silent close. Server-only text: {error}")
}
}
}
fn attach_against_pending_finalization(
generation_value: u64,
token_byte: u8,
) -> Result<ParticipantDispatch, Box<dyn Error>> {
let fixture = pending_restart_fixture()?;
let generation =
Generation::new(generation_value).ok_or("zero generation in the #14 attach fixture")?;
dispatch_outcome(
&fixture.handler,
ConnectionIncarnation::new(97, 9),
&mut ParticipantConnectionConversations::default(),
ClientRequest::CredentialAttach(CredentialAttachRequest {
conversation_id: fixture.conversation_id,
participant_id: fixture.participant_id,
capability_generation: generation,
attach_secret: fixture.attach_secret,
attach_attempt_token: AttachAttemptToken::new([token_byte; 16]),
accept_marker_delivery_seq: None,
}),
)
}
#[test]
fn attach_against_pending_finalization_must_not_answer_with_silence() -> Result<(), Box<dyn Error>>
{
let outcome = attach_against_pending_finalization(2, 0xA2)?;
assert!(
matches!(outcome, ParticipantDispatch::Respond(_)),
"a credential attach that cannot be admitted must still TELL the client so, on the \
Respond path that guarantees delivery and leaves the connection open. Instead the \
client got: {}",
client_outcome(&outcome)
);
Ok(())
}
#[test]
fn the_pending_finalization_refusal_carries_the_observer_backpressure_row()
-> Result<(), Box<dyn Error>> {
let outcome = attach_against_pending_finalization(2, 0xA3)?;
let ParticipantDispatch::Respond(response) = &outcome else {
return Err(format!(
"the refusal must reach the client as a frame, got: {}",
client_outcome(&outcome)
)
.into());
};
let value = decode_server_value(response)?;
let ServerValue::ObserverBackpressure(ObserverBackpressure::CredentialAttach {
request,
state,
}) = value
else {
return Err(format!(
"the pending-finalization refusal must present the credential-attach \
ObserverBackpressure row, got: {value:?}"
)
.into());
};
assert_eq!(
request.attach_attempt_token,
AttachAttemptToken::new([0xA3; 16]),
"the refusal must echo the presented attempt token"
);
assert_eq!(
state.backpressure_epoch(),
state.observer_progress(),
"an initial refusal epoch is exactly the progress value observed by the serialized \
operation (ObserverBackpressureState::initial)"
);
Ok(())
}
#[test]
fn the_presented_refusal_commits_nothing_and_leaves_a_usable_authority()
-> Result<(), Box<dyn Error>> {
let fixture = pending_restart_fixture()?;
let incarnation = ConnectionIncarnation::new(97, 9);
let current = generation(2)?;
let refuse = |token_byte: u8| {
dispatch_outcome(
&fixture.handler,
incarnation,
&mut ParticipantConnectionConversations::default(),
ClientRequest::CredentialAttach(CredentialAttachRequest {
conversation_id: fixture.conversation_id,
participant_id: fixture.participant_id,
capability_generation: current,
attach_secret: fixture.attach_secret,
attach_attempt_token: AttachAttemptToken::new([token_byte; 16]),
accept_marker_delivery_seq: None,
}),
)
};
assert!(
block_on(fixture.log.read_at(fixture.specific_sequence))??.is_none(),
"fixture precondition: the durable slot after the Died row is empty"
);
for token_byte in [0xC1_u8, 0xC2, 0xC3] {
let outcome = refuse(token_byte)?;
let ParticipantDispatch::Respond(response) = &outcome else {
return Err(format!(
"every repeat of the refusal must answer with a frame, got: {}",
client_outcome(&outcome)
)
.into());
};
let value = decode_server_value(response)?;
assert!(
matches!(
value,
ServerValue::ObserverBackpressure(ObserverBackpressure::CredentialAttach { .. })
),
"the refusal must be stable across repeats, got: {value:?}"
);
assert!(
block_on(fixture.log.read_at(fixture.specific_sequence))??.is_none(),
"a refused attach appended a durable row -- the refusal committed something"
);
}
let outcome = dispatch_outcome(
&fixture.handler,
incarnation,
&mut ParticipantConnectionConversations::default(),
ClientRequest::CredentialAttach(CredentialAttachRequest {
conversation_id: fixture.conversation_id,
participant_id: fixture.participant_id,
capability_generation: GEN_ONE,
attach_secret: fixture.attach_secret,
attach_attempt_token: AttachAttemptToken::new([0xC9; 16]),
accept_marker_delivery_seq: None,
}),
)?;
let ParticipantDispatch::Respond(response) = &outcome else {
return Err(format!(
"the conversation must still serve requests after a presented refusal, got: {}",
client_outcome(&outcome)
)
.into());
};
let value = decode_server_value(response)?;
let ServerValue::StaleAuthority(StaleAuthority::Live {
current_generation, ..
}) = value
else {
return Err(format!(
"a stale attach after a presented refusal must still be refused from live \
authority, got: {value:?}"
)
.into());
};
assert_eq!(
current_generation, current,
"the retained owner still carries the live generation"
);
Ok(())
}
#[test]
fn stale_generation_attach_is_already_refused_with_a_frame() -> Result<(), Box<dyn Error>> {
for (generation_value, token_byte) in [(1_u64, 0xB1_u8), (3, 0xB3)] {
let outcome = attach_against_pending_finalization(generation_value, token_byte)?;
let ParticipantDispatch::Respond(response) = &outcome else {
return Err(format!(
"generation {generation_value} should already be refused with a frame, got: {}",
client_outcome(&outcome)
)
.into());
};
let value = decode_server_value(response)?;
assert!(
matches!(
value,
ServerValue::StaleAuthority(StaleAuthority::Live {
request: CommonStaleAuthorityEnvelope::CredentialAttach(_),
..
})
),
"generation {generation_value} should refuse with the credential-attach \
StaleAuthority::Live row, got: {value:?}"
);
}
Ok(())
}
#[test]
fn live_path_stale_generation_is_answered_by_lookup_with_the_current_generation()
-> Result<(), Box<dyn Error>> {
let home = tempfile::tempdir()?;
let data_dir = home.path().join("durability");
let incarnation = ConnectionIncarnation::new(904, 1);
let store = open_disk_store_for_tests(&data_dir)?;
let handler = ProductionParticipantHandler::new(store, test_participant_config())?;
let conversation_id = 9041;
let receipt = enroll(&handler, incarnation, conversation_id, [11; 16])?;
let participant_id = receipt.participant_id();
detach(
&handler,
incarnation,
conversation_id,
participant_id,
GEN_ONE,
[12; 16],
)?;
let bound = attach(
&handler,
incarnation,
attach_request(
conversation_id,
participant_id,
GEN_ONE,
receipt.attach_secret(),
[13; 16],
),
)?;
let current = generation(2)?;
assert_eq!(bound.capability_generation(), current);
let outcome = dispatch_outcome(
&handler,
incarnation,
&mut ParticipantConnectionConversations::default(),
attach_request(
conversation_id,
participant_id,
GEN_ONE,
bound.attach_secret(),
[14; 16],
),
)?;
let ParticipantDispatch::Respond(response) = &outcome else {
return Err(format!(
"a stale-generation attach must be refused with a frame, got: {}",
client_outcome(&outcome)
)
.into());
};
let value = decode_server_value(response)?;
let ServerValue::StaleAuthority(StaleAuthority::Live {
request: CommonStaleAuthorityEnvelope::CredentialAttach(envelope),
current_generation,
}) = value
else {
return Err(
format!("expected the credential-attach StaleAuthority row, got: {value:?}").into(),
);
};
assert_eq!(envelope.conversation_id, conversation_id);
assert_eq!(
current_generation, current,
"the refusal must carry the CURRENT live generation so the client can re-drive"
);
Ok(())
}