use mur_common::identity::{
AgentIdentity, ChainError, ChainOptions, RotationAttestation, RotationReason, verify_chain,
};
const TEST_UUID: &str = "01JQX4TM8Y9K7VQH6B2N3R5DPE";
fn bootstrap(pubkey: &str) -> RotationAttestation {
RotationAttestation::new(
TEST_UUID,
"",
pubkey,
0,
0,
"2026-04-25T10:00:00+08:00",
RotationReason::Scheduled,
)
.into_bootstrap()
}
fn signed_step(
old: &AgentIdentity,
new: &AgentIdentity,
old_v: u32,
new_v: u32,
reason: RotationReason,
) -> RotationAttestation {
let mut a = RotationAttestation::new(
TEST_UUID,
old.pubkey_text(),
new.pubkey_text(),
old_v,
new_v,
format!("2026-04-25T11:0{old_v}:00+08:00"),
reason,
);
a.sign(old.signing_key());
a
}
#[test]
fn empty_chain_is_missing_bootstrap() {
let err = verify_chain(&[], ChainOptions::default()).unwrap_err();
assert!(matches!(err, ChainError::MissingBootstrap));
}
#[test]
fn chain_without_bootstrap_first_rejected() {
let id = AgentIdentity::generate();
let id2 = AgentIdentity::generate();
let chain = vec![signed_step(&id, &id2, 0, 1, RotationReason::Scheduled)];
let err = verify_chain(&chain, ChainOptions::default()).unwrap_err();
assert!(matches!(err, ChainError::MissingBootstrap));
}
#[test]
fn single_bootstrap_chain_ok() {
let id = AgentIdentity::generate();
let chain = vec![bootstrap(&id.pubkey_text())];
let out = verify_chain(&chain, ChainOptions::default()).expect("ok");
assert_eq!(out.head_key_version, 0);
assert_eq!(out.head_pubkey, id.pubkey_text());
assert_eq!(out.length, 1);
}
#[test]
fn five_step_chain_ok() {
let mut chain = vec![];
let mut prev = AgentIdentity::generate();
chain.push(bootstrap(&prev.pubkey_text()));
for v in 1..=5 {
let next = AgentIdentity::generate();
chain.push(signed_step(
&prev,
&next,
v - 1,
v,
RotationReason::Scheduled,
));
prev = next;
}
let out = verify_chain(&chain, ChainOptions::default()).expect("ok");
assert_eq!(out.head_key_version, 5);
assert_eq!(out.head_pubkey, prev.pubkey_text());
assert_eq!(out.length, 6);
}
#[test]
fn version_skip_rejected() {
let mut chain = vec![];
let prev = AgentIdentity::generate();
let next = AgentIdentity::generate();
let next2 = AgentIdentity::generate();
chain.push(bootstrap(&prev.pubkey_text()));
chain.push(signed_step(&prev, &next, 0, 1, RotationReason::Scheduled));
chain.push(signed_step(&next, &next2, 1, 3, RotationReason::Scheduled));
let err = verify_chain(&chain, ChainOptions::default()).unwrap_err();
assert!(matches!(
err,
ChainError::VersionSkip {
expected: 2,
got: 3
}
));
}
#[test]
fn pubkey_discontinuity_rejected() {
let prev = AgentIdentity::generate();
let next = AgentIdentity::generate();
let other = AgentIdentity::generate();
let mut chain = vec![bootstrap(&prev.pubkey_text())];
let mut step = signed_step(&prev, &next, 0, 1, RotationReason::Scheduled);
step.old_pubkey = other.pubkey_text();
step.sign(other.signing_key());
chain.push(step);
let err = verify_chain(&chain, ChainOptions::default()).unwrap_err();
assert!(matches!(
err,
ChainError::PubkeyDiscontinuity { at_version: 1 }
));
}
#[test]
fn duplicate_version_rejected() {
let prev = AgentIdentity::generate();
let next = AgentIdentity::generate();
let next2 = AgentIdentity::generate();
let mut chain = vec![bootstrap(&prev.pubkey_text())];
chain.push(signed_step(&prev, &next, 0, 1, RotationReason::Scheduled));
let mut dup = signed_step(&next, &next2, 1, 1, RotationReason::Scheduled);
dup.new_key_version = 1; chain.push(dup);
let err = verify_chain(&chain, ChainOptions::default()).unwrap_err();
assert!(
matches!(
err,
ChainError::DuplicateVersion(1) | ChainError::VersionSkip { .. }
),
"expected DuplicateVersion or VersionSkip, got {err:?}"
);
}
#[test]
fn bad_signature_rejected() {
let prev = AgentIdentity::generate();
let next = AgentIdentity::generate();
let mut chain = vec![bootstrap(&prev.pubkey_text())];
let mut step = signed_step(&prev, &next, 0, 1, RotationReason::Scheduled);
step.new_pubkey = AgentIdentity::generate().pubkey_text();
chain.push(step);
let err = verify_chain(&chain, ChainOptions::default()).unwrap_err();
assert!(matches!(
err,
ChainError::BadSignature { at_version: 1, .. }
));
}
#[test]
fn emergency_in_chain_rejected_by_default() {
let prev = AgentIdentity::generate();
let next = AgentIdentity::generate();
let mut chain = vec![bootstrap(&prev.pubkey_text())];
let emerg = RotationAttestation::new(
TEST_UUID,
prev.pubkey_text(),
next.pubkey_text(),
0,
1,
"2026-04-25T11:00:00+08:00",
RotationReason::Emergency,
);
chain.push(emerg);
let err = verify_chain(&chain, ChainOptions::default()).unwrap_err();
assert!(matches!(
err,
ChainError::EmergencyDisallowed { at_version: 1 }
));
}
#[test]
fn emergency_in_chain_allowed_with_opt_in() {
let prev = AgentIdentity::generate();
let next = AgentIdentity::generate();
let mut chain = vec![bootstrap(&prev.pubkey_text())];
let emerg = RotationAttestation::new(
TEST_UUID,
prev.pubkey_text(),
next.pubkey_text(),
0,
1,
"2026-04-25T11:00:00+08:00",
RotationReason::Emergency,
);
chain.push(emerg);
let out = verify_chain(
&chain,
ChainOptions {
allow_emergency: true,
},
)
.expect("emergency-allowed must pass");
assert_eq!(out.head_key_version, 1);
}
#[test]
fn hundred_step_chain() {
let mut chain = vec![];
let mut prev = AgentIdentity::generate();
chain.push(bootstrap(&prev.pubkey_text()));
for v in 1..=100u32 {
let next = AgentIdentity::generate();
chain.push(signed_step(
&prev,
&next,
v - 1,
v,
RotationReason::Scheduled,
));
prev = next;
}
let out = verify_chain(&chain, ChainOptions::default()).expect("100-step ok");
assert_eq!(out.head_key_version, 100);
assert_eq!(out.length, 101);
}