use mail4agent_api::{
Address, ParticipantId, RoomId, SendRequest, SessionAttested, SessionCard, SessionCorroborated, SessionDeclared,
SessionId,
};
use mail4agent_core::{MailboxEngine, ParticipantPermissions};
use crate::SqliteMailStore;
fn engine() -> MailboxEngine<SqliteMailStore> {
MailboxEngine::new(SqliteMailStore::open_in_memory().expect("in-memory store opens and migrates"))
}
fn permissions(may_send: bool, may_read: bool, operator: bool) -> ParticipantPermissions {
ParticipantPermissions { may_send, may_read, operator }
}
fn send_request(to: Address) -> SendRequest {
SendRequest {
to,
subject: "subject".to_string(),
body: "body".to_string(),
reply_to: None,
correlation: None,
refs: Vec::new(),
idempotency_key: None,
}
}
fn bare_card(pid: u32, started_at_unix_ms: u64) -> SessionCard {
SessionCard {
attested: SessionAttested { pid, started_at_unix_ms, exe: None },
corroborated: SessionCorroborated { provider_session_id: None, model: None, cwd: None },
declared: SessionDeclared::default(),
}
}
fn always_alive(_pid: u32, _started_at_unix_ms: u64) -> bool {
true
}
#[test]
fn full_mailbox_scenario_matches_in_memory_store_outcomes() {
let mut engine = engine();
let alice = ParticipantId::new("alice").expect("valid participant id");
let bob = ParticipantId::new("bob").expect("valid participant id");
let room_id = RoomId::new("room-1").expect("valid room id");
engine.register_participant(alice.clone(), None, permissions(true, true, false)).expect("alice registers");
engine.register_participant(bob.clone(), None, permissions(true, true, false)).expect("bob registers");
engine.create_room(room_id.clone(), 1_000).expect("room is created");
engine.add_room_member(&room_id, bob.clone()).expect("bob joins the room");
let alice_addr = Address::Direct { participant: alice.clone() };
let bob_addr = Address::Direct { participant: bob.clone() };
let direct = engine
.send(&alice_addr, send_request(Address::Direct { participant: bob.clone() }), 2_000)
.expect("direct send succeeds");
let to_room = engine
.send(&alice_addr, send_request(Address::Room { room: room_id.clone() }), 3_000)
.expect("room send succeeds");
let alice_inbox = engine.inbox(&alice_addr, 0, 50).expect("alice reads her inbox");
assert!(alice_inbox.messages.is_empty());
let bob_inbox = engine.inbox(&bob_addr, 0, 50).expect("bob reads his inbox");
let bob_ids: std::collections::BTreeSet<_> = bob_inbox.messages.iter().map(|message| message.message_id.clone()).collect();
assert_eq!(bob_ids, std::collections::BTreeSet::from([direct.message_id.clone(), to_room.message_id.clone()]));
assert_eq!(bob_inbox.unread, 2);
let first_ack = engine.ack(&bob_addr, &direct.message_id, 4_000).expect("bob acks the direct message");
let second_ack = engine.ack(&bob_addr, &direct.message_id, 9_000).expect("bob acks it again");
assert_eq!(first_ack, second_ack);
assert_eq!(first_ack.acked_at_unix_ms, 4_000);
let bob_inbox_after_ack = engine.inbox(&bob_addr, 0, 50).expect("bob reads his inbox again");
assert_eq!(bob_inbox_after_ack.messages.len(), 2, "acking does not remove a message from the inbox");
assert_eq!(bob_inbox_after_ack.unread, 1, "only the room message is still unread");
engine.remove_room_member(&room_id, &bob).expect("bob leaves the room");
let bob_inbox_after_leaving = engine.inbox(&bob_addr, 0, 50).expect("bob reads his inbox a third time");
assert_eq!(
bob_inbox_after_leaving.messages.iter().map(|message| &message.message_id).collect::<Vec<_>>(),
vec![&direct.message_id]
);
assert_eq!(bob_inbox_after_leaving.unread, 0);
let directory = engine.directory(&alice_addr, &always_alive).expect("alice reads the directory");
let participant_ids: std::collections::BTreeSet<_> =
directory.participants.iter().map(|entry| entry.id.clone()).collect();
assert_eq!(participant_ids, std::collections::BTreeSet::from([alice, bob]));
let room_entry = directory.rooms.iter().find(|entry| entry.id == room_id).expect("the room is listed");
assert!(!room_entry.member, "alice never joined this room");
}
#[test]
fn a_sessions_full_arc_matches_in_memory_store_outcomes() {
let mut engine = engine();
let claude = ParticipantId::new("claude").expect("valid participant id");
let bob = ParticipantId::new("bob").expect("valid participant id");
engine.register_participant(claude.clone(), None, permissions(true, true, false)).expect("claude registers");
engine.register_participant(bob.clone(), None, permissions(true, true, false)).expect("bob registers");
let session = SessionId::new("s-7f3a0000").expect("valid session id");
engine
.ensure_session(claude.clone(), session.clone(), bare_card(4242, 1_000), 1_000)
.expect("the session registers");
let session_addr = Address::Session { participant: claude.clone(), session: session.clone() };
let account_addr = Address::Direct { participant: claude.clone() };
let bob_addr = Address::Direct { participant: bob };
let to_session = engine
.send(&bob_addr, send_request(Address::Session { participant: claude.clone(), session: session.clone() }), 2_000)
.expect("direct-to-session send succeeds");
let to_account = engine
.send(&bob_addr, send_request(Address::Direct { participant: claude.clone() }), 3_000)
.expect("direct-to-account send succeeds");
let page = engine.inbox(&session_addr, 0, 50).expect("the session reads its inbox");
let ids: std::collections::BTreeSet<_> = page.messages.iter().map(|message| message.message_id.clone()).collect();
assert_eq!(
ids,
std::collections::BTreeSet::from([to_session.message_id.clone(), to_account.message_id.clone()]),
"the session's inbox holds both its own mail and its account's"
);
assert_eq!(page.unread, 2);
let account_inbox = engine.inbox(&account_addr, 0, 50).expect("the account reads its inbox");
assert_eq!(
account_inbox.messages.iter().map(|message| &message.message_id).collect::<Vec<_>>(),
vec![&to_account.message_id],
"session-scoped mail must not leak into the account's own inbox"
);
let ack = engine.ack(&session_addr, &to_session.message_id, 4_000).expect("the session acks its own mail");
assert_eq!(ack.reader, session_addr);
let page_after_ack = engine.inbox(&session_addr, 0, 50).expect("the session reads its inbox again");
assert_eq!(page_after_ack.messages.len(), 2, "acking does not remove a message from the inbox");
assert_eq!(page_after_ack.unread, 1, "only the still-unacked account mail remains unread");
}