use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::time::{Duration, SystemTime};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HandoffCapsule {
pub session_meta: SessionMetadata,
pub git_state: GitState,
pub inbox_state: InboxState,
pub active_reservations: Vec<FileReservation>,
pub claimed_beads: Vec<BeadClaim>,
pub dirty_paths: DirtyPathSummary,
pub proof_commands: Vec<ProofCommand>,
pub first_blocker: Option<BlockerInfo>,
pub pushed_commits: Vec<CommitInfo>,
pub remaining_risks: Vec<RiskAssessment>,
pub created_at: SystemTime,
pub content_hash: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
pub struct SessionMetadata {
pub agent_id: String,
pub session_duration: Duration,
pub last_active: SystemTime,
pub session_type: SessionType,
pub docs_receipts: Vec<DocReceipt>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
pub enum SessionType {
Interactive,
Automated,
Background,
Emergency,
}
#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
pub struct DocReceipt {
pub doc_path: String,
pub content_hash: String,
pub read_at: SystemTime,
}
#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
pub struct GitState {
pub current_branch: String,
pub main_hash: String,
pub working_tree_clean: bool,
pub staged_changes: Vec<String>,
pub untracked_files: Vec<String>,
pub last_remote_sync: Option<SystemTime>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
pub struct InboxState {
pub unread_count: u32,
pub pending_acks: Vec<String>,
pub last_check: SystemTime,
pub critical_unacked: Vec<MessageRef>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
pub struct MessageRef {
pub message_id: String,
pub sender: String,
pub priority: MessagePriority,
pub received_at: SystemTime,
}
#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
pub enum MessagePriority {
Low,
Normal,
High,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
pub struct FileReservation {
pub id: String,
pub paths: Vec<String>,
pub exclusive: bool,
pub expires_at: SystemTime,
pub reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BeadClaim {
pub bead_id: String,
pub title: String,
pub status: BeadStatus,
pub claimed_at: SystemTime,
pub progress: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
pub enum BeadStatus {
Open,
InProgress,
Blocked,
Closed,
}
#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
pub struct DirtyPathSummary {
pub owned_files: BTreeSet<String>,
pub peer_modified: BTreeMap<String, String>, pub potential_conflicts: Vec<ConflictInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
pub struct ConflictInfo {
pub file_path: String,
pub competing_agents: Vec<String>,
pub severity: ConflictSeverity,
}
#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
pub enum ConflictSeverity {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
pub struct ProofCommand {
pub command_id: String,
pub command_line: String,
pub started_at: SystemTime,
pub expected_completion: Option<SystemTime>,
pub command_type: ProofCommandType,
}
#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
pub enum ProofCommandType {
Compile,
Test,
Lint,
Format,
Benchmark,
Other(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
pub struct BlockerInfo {
pub blocker_type: BlockerType,
pub description: String,
pub estimated_resolution: Option<Duration>,
pub dependencies: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
pub enum BlockerType {
GitConflict,
FileReservation,
BeadDependency,
ProofFailure,
NetworkIssue,
ResourceContention,
Other(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
pub struct CommitInfo {
pub commit_hash: String,
pub message_summary: String,
pub files_changed: Vec<String>,
pub pushed_at: SystemTime,
pub beads_synced: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
pub struct RiskAssessment {
pub category: RiskCategory,
pub level: RiskLevel,
pub description: String,
pub mitigations: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
pub enum RiskCategory {
StaleDocumentation,
UnacknowledgedMessages,
FileConflicts,
DependencyChanges,
ProofCommandFailure,
ResourceExpiration,
Other(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
pub enum RiskLevel {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum HandoffDecision {
Continue,
NarrowRefreshRequired {
refresh_targets: Vec<RefreshTarget>,
},
CoordinateFirst {
coordination_needed: Vec<CoordinationRequirement>,
},
UnsafeToContinue {
reasons: Vec<SafetyViolation>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum RefreshTarget {
Documentation,
InboxMessages,
FileReservations,
BeadStatus,
GitState,
ProofCommands,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoordinationRequirement {
pub requirement_type: CoordinationType,
pub target_agents: Vec<String>,
pub estimated_time: Duration,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CoordinationType {
FileReservationHandoff,
BeadTransfer,
ConflictResolution,
ProofCommandSync,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SafetyViolation {
pub category: ViolationCategory,
pub reason: String,
pub evidence: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ViolationCategory {
StaleGitState,
ExpiredReservations,
UnresolvedConflicts,
CriticalUnacknowledgedMessages,
FailedProofCommands,
IntegrityCheckFailure,
}
#[derive(Debug)]
pub struct HandoffVerifier {
staleness_thresholds: StalenessThresholds,
}
#[derive(Debug, Clone)]
pub struct StalenessThresholds {
pub docs_max_age: Duration,
pub inbox_max_age: Duration,
pub proof_command_timeout: Duration,
pub git_sync_max_age: Duration,
}
impl Default for StalenessThresholds {
fn default() -> Self {
Self {
docs_max_age: Duration::from_secs(3600), inbox_max_age: Duration::from_secs(300), proof_command_timeout: Duration::from_secs(1800), git_sync_max_age: Duration::from_secs(600), }
}
}
impl HandoffVerifier {
pub fn new() -> Self {
Self {
staleness_thresholds: StalenessThresholds::default(),
}
}
pub fn with_thresholds(thresholds: StalenessThresholds) -> Self {
Self {
staleness_thresholds: thresholds,
}
}
pub fn verify_handoff(&self, capsule: &HandoffCapsule) -> HandoffDecision {
let mut violations = Vec::new();
let mut refresh_targets = BTreeSet::new();
let mut coordination_requirements = Vec::new();
if let Some(violation) = self.check_integrity(capsule) {
violations.push(violation);
}
if let Some(violation) = self.check_git_state(&capsule.git_state) {
violations.push(violation);
} else if self.is_git_state_stale(&capsule.git_state) {
refresh_targets.insert(RefreshTarget::GitState);
}
if self.are_docs_stale(&capsule.session_meta.docs_receipts) {
if self.is_critically_stale(&capsule.session_meta.docs_receipts) {
violations.push(SafetyViolation {
category: ViolationCategory::StaleGitState,
reason: "Documentation critically out of date".to_string(),
evidence: "Docs not read in over 24 hours".to_string(),
});
} else {
refresh_targets.insert(RefreshTarget::Documentation);
}
}
if self.is_inbox_critical(&capsule.inbox_state) {
violations.push(SafetyViolation {
category: ViolationCategory::CriticalUnacknowledgedMessages,
reason: "Critical messages require immediate attention".to_string(),
evidence: format!(
"{} critical unacknowledged messages",
capsule.inbox_state.critical_unacked.len()
),
});
} else if self.is_inbox_stale(&capsule.inbox_state) {
refresh_targets.insert(RefreshTarget::InboxMessages);
}
if let Some(coord_req) = self.check_file_reservations(&capsule.active_reservations) {
coordination_requirements.push(coord_req);
}
if let Some(coord_req) = self.check_bead_claims(capsule) {
coordination_requirements.push(coord_req);
}
if let Some(violation) = self.check_proof_commands(&capsule.proof_commands) {
violations.push(violation);
} else if self.are_proof_commands_stale(&capsule.proof_commands) {
refresh_targets.insert(RefreshTarget::ProofCommands);
}
if let Some(coord_req) = self.check_file_conflicts(&capsule.dirty_paths) {
coordination_requirements.push(coord_req);
}
if !violations.is_empty() {
HandoffDecision::UnsafeToContinue {
reasons: violations,
}
} else if !coordination_requirements.is_empty() {
HandoffDecision::CoordinateFirst {
coordination_needed: coordination_requirements,
}
} else if !refresh_targets.is_empty() {
HandoffDecision::NarrowRefreshRequired {
refresh_targets: refresh_targets.into_iter().collect(),
}
} else {
HandoffDecision::Continue
}
}
fn check_integrity(&self, capsule: &HandoffCapsule) -> Option<SafetyViolation> {
if capsule.content_hash.is_empty() {
return Some(SafetyViolation {
category: ViolationCategory::IntegrityCheckFailure,
reason: "Missing content hash".to_string(),
evidence: "Capsule integrity cannot be verified".to_string(),
});
}
let computed_hash = self.compute_content_hash(capsule);
if computed_hash != capsule.content_hash {
Some(SafetyViolation {
category: ViolationCategory::IntegrityCheckFailure,
reason: "Content hash mismatch".to_string(),
evidence: format!(
"Expected: {}, Computed: {}",
capsule.content_hash, computed_hash
),
})
} else {
None
}
}
fn compute_content_hash(&self, capsule: &HandoffCapsule) -> String {
use sha2::{Digest, Sha256};
let mut capsule_for_hash = capsule.clone();
capsule_for_hash.content_hash = String::new();
if let Ok(json_bytes) = serde_json::to_vec(&capsule_for_hash) {
let mut hasher = Sha256::new();
hasher.update(b"asupersync.agent_swarm.handoff_capsule.v1\0");
hasher.update(json_bytes);
hex::encode(hasher.finalize())
} else {
"serialization-failed".to_string()
}
}
fn check_git_state(&self, git_state: &GitState) -> Option<SafetyViolation> {
if git_state.current_branch != "main" {
return Some(SafetyViolation {
category: ViolationCategory::StaleGitState,
reason: "Not on main branch".to_string(),
evidence: format!("Current branch: {}", git_state.current_branch),
});
}
if !git_state.working_tree_clean && !git_state.staged_changes.is_empty() {
return Some(SafetyViolation {
category: ViolationCategory::UnresolvedConflicts,
reason: "Uncommitted staged changes".to_string(),
evidence: format!("{} staged files", git_state.staged_changes.len()),
});
}
None
}
fn is_git_state_stale(&self, git_state: &GitState) -> bool {
if let Some(last_sync) = git_state.last_remote_sync {
SystemTime::now()
.duration_since(last_sync)
.unwrap_or(Duration::MAX)
> self.staleness_thresholds.git_sync_max_age
} else {
true }
}
fn are_docs_stale(&self, docs_receipts: &[DocReceipt]) -> bool {
if docs_receipts.is_empty() {
return true;
}
docs_receipts.iter().any(|receipt| {
SystemTime::now()
.duration_since(receipt.read_at)
.unwrap_or(Duration::MAX)
> self.staleness_thresholds.docs_max_age
})
}
fn is_critically_stale(&self, docs_receipts: &[DocReceipt]) -> bool {
docs_receipts.iter().any(|receipt| {
SystemTime::now()
.duration_since(receipt.read_at)
.unwrap_or(Duration::MAX)
> Duration::from_secs(24 * 3600) })
}
fn is_inbox_critical(&self, inbox_state: &InboxState) -> bool {
!inbox_state.critical_unacked.is_empty()
}
fn is_inbox_stale(&self, inbox_state: &InboxState) -> bool {
SystemTime::now()
.duration_since(inbox_state.last_check)
.unwrap_or(Duration::MAX)
> self.staleness_thresholds.inbox_max_age
}
fn check_file_reservations(
&self,
reservations: &[FileReservation],
) -> Option<CoordinationRequirement> {
let now = SystemTime::now();
let expired_count = reservations.iter().filter(|r| r.expires_at < now).count();
if expired_count > 0 {
let mut targets: Vec<String> = reservations
.iter()
.filter(|reservation| reservation.expires_at < now)
.map(|reservation| format!("reservation:{}", reservation.id))
.collect();
targets.sort();
targets.dedup();
Some(CoordinationRequirement {
requirement_type: CoordinationType::FileReservationHandoff,
target_agents: targets,
estimated_time: Duration::from_secs(300), })
} else {
None
}
}
fn check_bead_claims(&self, capsule: &HandoffCapsule) -> Option<CoordinationRequirement> {
let now = SystemTime::now();
let mut needs_coordination = Vec::new();
for claim in &capsule.claimed_beads {
let bead_id = claim.bead_id.trim();
if bead_id.is_empty() {
needs_coordination.push("bead:<blank>".to_string());
continue;
}
if !claim.progress.is_finite() || !(0.0..=1.0).contains(&claim.progress) {
needs_coordination.push(format!("bead:{bead_id}:invalid-progress"));
continue;
}
match claim.status {
BeadStatus::Open => {
needs_coordination.push(format!("bead:{bead_id}:not-claimed"));
}
BeadStatus::InProgress => {
let claim_age = now
.duration_since(claim.claimed_at)
.unwrap_or(Duration::MAX);
let has_recent_evidence = capsule
.proof_commands
.iter()
.any(|cmd| cmd.command_line.contains(bead_id))
|| capsule
.pushed_commits
.iter()
.any(|commit| commit.message_summary.contains(bead_id));
if claim_age > self.staleness_thresholds.git_sync_max_age
&& !has_recent_evidence
{
needs_coordination.push(format!("bead:{bead_id}:stale-claim"));
}
}
BeadStatus::Blocked => {
let blocker_matches = capsule.first_blocker.as_ref().is_some_and(|blocker| {
blocker
.dependencies
.iter()
.any(|dependency| dependency.contains(bead_id))
|| blocker.description.contains(bead_id)
});
if !blocker_matches {
needs_coordination.push(format!("bead:{bead_id}:missing-blocker"));
}
}
BeadStatus::Closed => {
if claim.progress < 1.0 {
needs_coordination.push(format!("bead:{bead_id}:closed-without-progress"));
}
}
}
}
if needs_coordination.is_empty() {
None
} else {
needs_coordination.sort();
needs_coordination.dedup();
Some(CoordinationRequirement {
requirement_type: CoordinationType::BeadTransfer,
target_agents: needs_coordination,
estimated_time: Duration::from_secs(300),
})
}
}
fn check_proof_commands(&self, commands: &[ProofCommand]) -> Option<SafetyViolation> {
let now = SystemTime::now();
let timed_out = commands.iter().any(|cmd| {
now.duration_since(cmd.started_at).unwrap_or(Duration::MAX)
> self.staleness_thresholds.proof_command_timeout
});
if timed_out {
Some(SafetyViolation {
category: ViolationCategory::FailedProofCommands,
reason: "Proof commands timed out".to_string(),
evidence: "Commands running longer than threshold".to_string(),
})
} else {
None
}
}
fn are_proof_commands_stale(&self, commands: &[ProofCommand]) -> bool {
!commands.is_empty() }
fn check_file_conflicts(
&self,
dirty_paths: &DirtyPathSummary,
) -> Option<CoordinationRequirement> {
if !dirty_paths.potential_conflicts.is_empty() {
Some(CoordinationRequirement {
requirement_type: CoordinationType::ConflictResolution,
target_agents: dirty_paths
.potential_conflicts
.iter()
.flat_map(|c| c.competing_agents.clone())
.collect(),
estimated_time: Duration::from_secs(600), })
} else {
None
}
}
}
impl Default for HandoffVerifier {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_capsule() -> HandoffCapsule {
let mut capsule = HandoffCapsule {
session_meta: SessionMetadata {
agent_id: "test-agent".to_string(),
session_duration: Duration::from_secs(3600),
last_active: SystemTime::now(),
session_type: SessionType::Interactive,
docs_receipts: vec![DocReceipt {
doc_path: "AGENTS.md".to_string(),
content_hash: "abc123".to_string(),
read_at: SystemTime::now(),
}],
},
git_state: GitState {
current_branch: "main".to_string(),
main_hash: "deadbeef".to_string(),
working_tree_clean: true,
staged_changes: vec![],
untracked_files: vec![],
last_remote_sync: Some(SystemTime::now()),
},
inbox_state: InboxState {
unread_count: 0,
pending_acks: vec![],
last_check: SystemTime::now(),
critical_unacked: vec![],
},
active_reservations: vec![],
claimed_beads: vec![],
dirty_paths: DirtyPathSummary {
owned_files: BTreeSet::new(),
peer_modified: BTreeMap::new(),
potential_conflicts: vec![],
},
proof_commands: vec![],
first_blocker: None,
pushed_commits: vec![],
remaining_risks: vec![],
created_at: SystemTime::now(),
content_hash: String::new(),
};
refresh_content_hash(&mut capsule);
capsule
}
fn refresh_content_hash(capsule: &mut HandoffCapsule) {
capsule.content_hash = HandoffVerifier::new().compute_content_hash(capsule);
}
#[test]
fn test_fresh_handoff_continues() {
let verifier = HandoffVerifier::new();
let capsule = create_test_capsule();
match verifier.verify_handoff(&capsule) {
HandoffDecision::Continue => {}
other => panic!("Expected Continue, got {:?}", other),
}
}
#[test]
fn test_stale_docs_requires_refresh() {
let verifier = HandoffVerifier::new();
let mut capsule = create_test_capsule();
capsule.session_meta.docs_receipts[0].read_at =
SystemTime::now() - Duration::from_secs(7200); refresh_content_hash(&mut capsule);
match verifier.verify_handoff(&capsule) {
HandoffDecision::NarrowRefreshRequired { refresh_targets } => {
assert!(refresh_targets.contains(&RefreshTarget::Documentation));
}
other => panic!("Expected NarrowRefreshRequired, got {:?}", other),
}
}
#[test]
fn test_wrong_branch_unsafe() {
let verifier = HandoffVerifier::new();
let mut capsule = create_test_capsule();
capsule.git_state.current_branch = "feature-branch".to_string();
refresh_content_hash(&mut capsule);
match verifier.verify_handoff(&capsule) {
HandoffDecision::UnsafeToContinue { reasons } => {
assert!(
reasons
.iter()
.any(|r| matches!(r.category, ViolationCategory::StaleGitState))
);
}
other => panic!("Expected UnsafeToContinue, got {:?}", other),
}
}
#[test]
fn test_critical_messages_unsafe() {
let verifier = HandoffVerifier::new();
let mut capsule = create_test_capsule();
capsule.inbox_state.critical_unacked.push(MessageRef {
message_id: "critical-msg".to_string(),
sender: "admin".to_string(),
priority: MessagePriority::Critical,
received_at: SystemTime::now(),
});
refresh_content_hash(&mut capsule);
match verifier.verify_handoff(&capsule) {
HandoffDecision::UnsafeToContinue { reasons } => {
assert!(reasons.iter().any(|r| matches!(
r.category,
ViolationCategory::CriticalUnacknowledgedMessages
)));
}
other => panic!("Expected UnsafeToContinue, got {:?}", other),
}
}
#[test]
fn test_expired_reservations_need_coordination() {
let verifier = HandoffVerifier::new();
let mut capsule = create_test_capsule();
capsule.active_reservations.push(FileReservation {
id: "expired-res".to_string(),
paths: vec!["src/**".to_string()],
exclusive: true,
expires_at: SystemTime::now() - Duration::from_secs(60), reason: "test".to_string(),
});
refresh_content_hash(&mut capsule);
match verifier.verify_handoff(&capsule) {
HandoffDecision::CoordinateFirst {
coordination_needed,
} => {
assert!(coordination_needed.iter().any(|c| matches!(
c.requirement_type,
CoordinationType::FileReservationHandoff
)));
}
other => panic!("Expected CoordinateFirst, got {:?}", other),
}
}
#[test]
fn test_invalid_bead_claims_need_coordination() {
let verifier = HandoffVerifier::new();
let mut capsule = create_test_capsule();
capsule.claimed_beads.push(BeadClaim {
bead_id: "asupersync-test-bead".to_string(),
title: "stale claim".to_string(),
status: BeadStatus::InProgress,
claimed_at: SystemTime::now() - Duration::from_secs(3600),
progress: 0.5,
});
refresh_content_hash(&mut capsule);
match verifier.verify_handoff(&capsule) {
HandoffDecision::CoordinateFirst {
coordination_needed,
} => {
let bead_coordination = coordination_needed
.iter()
.find(|c| matches!(c.requirement_type, CoordinationType::BeadTransfer));
assert!(bead_coordination.is_some());
assert!(
bead_coordination
.unwrap()
.target_agents
.iter()
.any(|target| target.contains("stale-claim"))
);
}
other => panic!("Expected CoordinateFirst, got {:?}", other),
}
}
#[test]
fn test_file_conflicts_need_coordination() {
let verifier = HandoffVerifier::new();
let mut capsule = create_test_capsule();
capsule.dirty_paths.potential_conflicts.push(ConflictInfo {
file_path: "src/conflicted.rs".to_string(),
competing_agents: vec!["agent1".to_string(), "agent2".to_string()],
severity: ConflictSeverity::Medium,
});
refresh_content_hash(&mut capsule);
match verifier.verify_handoff(&capsule) {
HandoffDecision::CoordinateFirst {
coordination_needed,
} => {
assert!(
coordination_needed.iter().any(|c| matches!(
c.requirement_type,
CoordinationType::ConflictResolution
))
);
}
other => panic!("Expected CoordinateFirst, got {:?}", other),
}
}
#[test]
fn test_missing_content_hash_unsafe() {
let verifier = HandoffVerifier::new();
let mut capsule = create_test_capsule();
capsule.content_hash = String::new();
match verifier.verify_handoff(&capsule) {
HandoffDecision::UnsafeToContinue { reasons } => {
assert!(
reasons
.iter()
.any(|r| matches!(r.category, ViolationCategory::IntegrityCheckFailure))
);
}
other => panic!("Expected UnsafeToContinue, got {:?}", other),
}
}
#[test]
fn test_invalid_content_hash_unsafe() {
let verifier = HandoffVerifier::new();
let mut capsule = create_test_capsule();
capsule.content_hash = "invalid-hash-that-wont-match".to_string();
match verifier.verify_handoff(&capsule) {
HandoffDecision::UnsafeToContinue { reasons } => {
assert!(
reasons
.iter()
.any(|r| matches!(r.category, ViolationCategory::IntegrityCheckFailure))
);
assert!(
reasons
.iter()
.any(|r| r.evidence.contains("Expected:")
&& r.evidence.contains("Computed:"))
);
}
other => panic!("Expected UnsafeToContinue, got {:?}", other),
}
}
#[test]
fn test_valid_content_hash_continues() {
let verifier = HandoffVerifier::new();
let mut capsule = create_test_capsule();
let correct_hash = verifier.compute_content_hash(&capsule);
capsule.content_hash = correct_hash;
match verifier.verify_handoff(&capsule) {
HandoffDecision::Continue => {}
other => panic!("Expected Continue, got {:?}", other),
}
}
#[test]
fn test_proof_command_timeout_unsafe() {
let verifier = HandoffVerifier::new();
let mut capsule = create_test_capsule();
capsule.proof_commands.push(ProofCommand {
command_id: "old-proof".to_string(),
command_line: "cargo test".to_string(),
started_at: SystemTime::now() - Duration::from_secs(3600), expected_completion: None,
command_type: ProofCommandType::Test,
});
refresh_content_hash(&mut capsule);
match verifier.verify_handoff(&capsule) {
HandoffDecision::UnsafeToContinue { reasons } => {
assert!(
reasons
.iter()
.any(|r| matches!(r.category, ViolationCategory::FailedProofCommands))
);
}
other => panic!("Expected UnsafeToContinue, got {:?}", other),
}
}
#[test]
fn test_capsule_serialization() {
let capsule = create_test_capsule();
let json = serde_json::to_string(&capsule).unwrap();
let deserialized: HandoffCapsule = serde_json::from_str(&json).unwrap();
assert_eq!(
capsule.session_meta.agent_id,
deserialized.session_meta.agent_id
);
assert_eq!(
capsule.git_state.current_branch,
deserialized.git_state.current_branch
);
}
}